From 5067786cac67548f0b544981a54c8f058acbb39c Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Sat, 3 Dec 2022 14:55:14 -0500 Subject: [PATCH] fix #2711: check files for missing sources content --- CHANGELOG.md | 6 ++++ internal/bundler/bundler.go | 49 ++++++++++++++++++++------ internal/js_parser/sourcemap_parser.go | 6 ++-- scripts/verify-source-map.js | 34 ++++++++++++++++++ 4 files changed, 81 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cab99938883..e717f1fb859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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: diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 396bea66b47..66ab02b5083 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -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) @@ -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 } } } @@ -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 diff --git a/internal/js_parser/sourcemap_parser.go b/internal/js_parser/sourcemap_parser.go index c04270313c0..c83d76774d8 100644 --- a/internal/js_parser/sourcemap_parser.go +++ b/internal/js_parser/sourcemap_parser.go @@ -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)) @@ -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{ @@ -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)) diff --git a/scripts/verify-source-map.js b/scripts/verify-source-map.js index c1c138f9339..ca67252fd20 100644 --- a/scripts/verify-source-map.js +++ b/scripts/verify-source-map.js @@ -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 @@ -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`) @@ -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, + }), ) } }