Skip to content

Commit

Permalink
fix #2711: check files for missing sources content
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Dec 3, 2022
1 parent 9ce5f94 commit 5067786
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 14 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,12 @@

## Unreleased

* Search for missing source map code on the file system ([#2711](https://github.com/evanw/esbuild/issues/2711))

[Source maps](https://sourcemaps.info/spec.html) are JSON files that map from compiled code back to the original code. They provide the original source code using two arrays: `sources` (required) and `sourcesContent` (optional). When bundling is enabled, esbuild is able to bundle code with source maps that was compiled by other tools (e.g. with Webpack) and emit source maps that map all the way back to the original code (e.g. before Webpack compiled it).

Previously if the input source maps omitted the optional `sourcesContent` array, esbuild would use `null` for the source content in the source map that it generates (since the source content isn't available). However, sometimes the original source code is actually still present on the file system. With this release, esbuild will now try to find the original source code using the path in the `sources` array and will use that instead of `null` if it was found.

* Fix parsing bug with TypeScript `infer` and `extends` ([#2712](https://github.com/evanw/esbuild/issues/2712))

This release fixes a bug where esbuild incorrectly failed to parse valid TypeScript code that nests `extends` inside `infer` inside `extends`, such as in the example below:
Expand Down
49 changes: 38 additions & 11 deletions internal/bundler/bundler.go
Expand Up @@ -465,19 +465,22 @@ func parseFile(args parseArgs) {
case *graph.CSSRepr:
sourceMapComment = repr.AST.SourceMapComment
}

if sourceMapComment.Text != "" {
tracker := logger.MakeLineColumnTracker(&source)

if path, contents := extractSourceMapFromComment(args.log, args.fs, &args.caches.FSCache,
args.res, &source, &tracker, sourceMapComment, absResolveDir); contents != nil {
prettyPath := args.res.PrettyPath(path)
log := logger.NewDeferLog(logger.DeferLogNoVerboseOrDebug, args.log.Overrides)
result.file.inputFile.InputSourceMap = js_parser.ParseSourceMap(log, logger.Source{

sourceMap := js_parser.ParseSourceMap(log, logger.Source{
KeyPath: path,
PrettyPath: prettyPath,
Contents: *contents,
})
msgs := log.Done()
if len(msgs) > 0 {

if msgs := log.Done(); len(msgs) > 0 {
var text string
if path.Namespace == "file" {
text = fmt.Sprintf("The source map %q was referenced by the file %q here:", prettyPath, args.prettyPath)
Expand All @@ -490,6 +493,28 @@ func parseFile(args parseArgs) {
args.log.AddMsg(msg)
}
}

// If "sourcesContent" isn't present, try filling it in using the file system
if sourceMap != nil && sourceMap.SourcesContent == nil && !args.options.ExcludeSourcesContent {
for _, source := range sourceMap.Sources {
var absPath string
if args.fs.IsAbs(source) {
absPath = source
} else if path.Namespace == "file" {
absPath = args.fs.Join(args.fs.Dir(path.Text), source)
} else {
sourceMap.SourcesContent = append(sourceMap.SourcesContent, sourcemap.SourceContent{})
continue
}
var sourceContent sourcemap.SourceContent
if contents, err, _ := args.caches.FSCache.ReadFile(args.fs, absPath); err == nil {
sourceContent.Value = helpers.StringToUTF16(contents)
}
sourceMap.SourcesContent = append(sourceMap.SourcesContent, sourceContent)
}
}

result.file.inputFile.InputSourceMap = sourceMap
}
}
}
Expand Down Expand Up @@ -2426,14 +2451,16 @@ func (b *Bundle) computeDataForSourceMapsInParallel(options *config.Options, rea
// Missing contents become a "null" literal
quotedContents := nullContents
if i < len(sm.SourcesContent) {
if value := sm.SourcesContent[i]; value.Quoted != "" {
if options.ASCIIOnly && !isASCIIOnly(value.Quoted) {
// Re-quote non-ASCII values if output is ASCII-only
quotedContents = helpers.QuoteForJSON(helpers.UTF16ToString(value.Value), options.ASCIIOnly)
} else {
// Otherwise just use the value directly from the input file
quotedContents = []byte(value.Quoted)
}
if value := sm.SourcesContent[i]; value.Quoted != "" && (!options.ASCIIOnly || !isASCIIOnly(value.Quoted)) {
// Just use the value directly from the input file
quotedContents = []byte(value.Quoted)
} else if value.Value != nil {
// Re-quote non-ASCII values if output is ASCII-only.
// Also quote values that haven't been quoted yet
// (happens when the entire "sourcesContent" array is
// absent and the source has been found on the file
// system using the "sources" array).
quotedContents = helpers.QuoteForJSON(helpers.UTF16ToString(value.Value), options.ASCIIOnly)
}
}
result.quotedContents[i] = quotedContents
Expand Down
6 changes: 3 additions & 3 deletions internal/js_parser/sourcemap_parser.go
Expand Up @@ -53,7 +53,7 @@ func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {

case "sources":
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
sources = nil
sources = []string{}
for _, item := range value.Items {
if element, ok := item.Data.(*js_ast.EString); ok {
sources = append(sources, helpers.UTF16ToString(element.Value))
Expand All @@ -65,7 +65,7 @@ func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {

case "sourcesContent":
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
sourcesContent = nil
sourcesContent = []sourcemap.SourceContent{}
for _, item := range value.Items {
if element, ok := item.Data.(*js_ast.EString); ok {
sourcesContent = append(sourcesContent, sourcemap.SourceContent{
Expand All @@ -80,7 +80,7 @@ func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {

case "names":
if value, ok := prop.ValueOrNil.Data.(*js_ast.EArray); ok {
names = nil
names = []string{}
for _, item := range value.Items {
if element, ok := item.Data.(*js_ast.EString); ok {
names = append(names, helpers.UTF16ToString(element.Value))
Expand Down
34 changes: 34 additions & 0 deletions scripts/verify-source-map.js
Expand Up @@ -377,6 +377,31 @@ const testCaseNames = {
`
}

const testCaseMissingSourcesContent = {
'foo.js': `// foo.ts
var foo = { bar: "bar" };
console.log({ foo });
//# sourceMappingURL=maps/foo.js.map
`,
'maps/foo.js.map': `{
"version": 3,
"sources": ["src/foo.ts"],
"mappings": ";AAGA,IAAM,MAAW,EAAE,KAAK,MAAM;AAC9B,QAAQ,IAAI,EAAE,IAAI,CAAC;",
"names": []
}
`,
'maps/src/foo.ts': `interface Foo {
bar: string
}
const foo: Foo = { bar: 'bar' }
console.log({ foo })
`,
}

const toSearchMissingSourcesContent = {
bar: 'src/foo.ts',
}

async function check(kind, testCase, toSearch, { ext, flags, entryPoints, crlf, followUpFlags = [] }) {
let failed = 0

Expand Down Expand Up @@ -445,6 +470,7 @@ async function check(kind, testCase, toSearch, { ext, flags, entryPoints, crlf,
recordCheck(source === inSource, `expected source: ${inSource}, observed source: ${source}`)

const inCode = map.sourceContentFor(source)
if (inCode === null) throw new Error(`Got null for source content for "${source}"`)
let inIndex = inCode.indexOf(`"${id}"`)
if (inIndex < 0) inIndex = inCode.indexOf(`'${id}'`)
if (inIndex < 0) throw new Error(`Failed to find "${id}" in input`)
Expand Down Expand Up @@ -818,6 +844,14 @@ async function main() {
entryPoints: ['entry.js'],
crlf,
}),

// Checks for loading missing sources content in nested source maps
check('missing-sources-content' + suffix, testCaseMissingSourcesContent, toSearchMissingSourcesContent, {
ext: 'js',
flags: flags.concat('--outfile=out.js', '--bundle'),
entryPoints: ['foo.js'],
crlf,
}),
)
}
}
Expand Down

0 comments on commit 5067786

Please sign in to comment.