Skip to content

Commit 36b458d

Browse files
committed
follow-up to #4109
1 parent 8b8437c commit 36b458d

File tree

3 files changed

+107
-10
lines changed

3 files changed

+107
-10
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@
3434
});
3535
```
3636
37+
* Basic support for index source maps ([#3439](https://github.com/evanw/esbuild/issues/3439), [#4109](https://github.com/evanw/esbuild/pull/4109))
38+
39+
The source map specification has an optional mode called [index source maps](https://tc39.es/ecma426/#sec-index-source-map) that makes it easier for tools to create an aggregate JavaScript file by concatenating many smaller JavaScript files with source maps, and then generate an aggregate source map by simply providing the original source maps along with some offset information. My understanding is that this is rarely used in practice. I'm only aware of two uses of it in the wild: [ClojureScript](https://clojurescript.org/) and [Turbopack](https://turbo.build/pack/).
40+
41+
This release provides basic support for indexed source maps. However, the implementation has not been tested on a real app (just on very simple test input). If you are using index source maps in a real app, please try this out and report back if anything isn't working for you.
42+
43+
Note that this is also not a complete implementation. For example, index source maps technically allows nesting source maps to an arbitrary depth, while esbuild's implementation in this release only supports a single level of nesting. It's unclear whether supporting more than one level of nesting is important or not given the lack of available test cases.
44+
45+
This feature was contributed by [@clyfish](https://github.com/clyfish).
46+
3747
## 0.25.1
3848
3949
* Fix incorrect paths in inline source maps ([#4070](https://github.com/evanw/esbuild/issues/4070), [#4075](https://github.com/evanw/esbuild/issues/4075), [#4105](https://github.com/evanw/esbuild/issues/4105))

internal/js_parser/sourcemap_parser.go

+28-10
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {
3838
hasSections := false
3939

4040
for _, prop := range obj.Properties {
41-
if helpers.UTF16ToString(prop.Key.Data.(*js_ast.EString).Value) != "sections" {
41+
if !helpers.UTF16EqualsString(prop.Key.Data.(*js_ast.EString).Value, "sections") {
4242
continue
4343
}
4444

@@ -66,11 +66,17 @@ func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {
6666
}
6767
}
6868
}
69+
} else {
70+
log.AddError(&tracker, logger.Range{Loc: sectionProp.ValueOrNil.Loc}, "Expected \"offset\" to be an object")
71+
return nil
6972
}
7073

7174
case "map":
7275
if mapValue, ok := sectionProp.ValueOrNil.Data.(*js_ast.EObject); ok {
7376
sectionSourceMap = mapValue
77+
} else {
78+
log.AddError(&tracker, logger.Range{Loc: sectionProp.ValueOrNil.Loc}, "Expected \"map\" to be an object")
79+
return nil
7480
}
7581
}
7682
}
@@ -85,7 +91,7 @@ func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {
8591
}
8692
}
8793
} else {
88-
log.AddError(&tracker, logger.Range{Loc: prop.Key.Loc}, "Invalid \"sections\"")
94+
log.AddError(&tracker, logger.Range{Loc: prop.ValueOrNil.Loc}, "Expected \"sections\" to be an array")
8995
return nil
9096
}
9197

@@ -105,10 +111,6 @@ func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {
105111
var mappings mappingArray
106112
var generatedLine int32
107113
var generatedColumn int32
108-
var sourceIndex int32
109-
var originalLine int32
110-
var originalColumn int32
111-
var originalName int32
112114
needSort := false
113115

114116
for _, section := range sections {
@@ -180,10 +182,10 @@ func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {
180182

181183
generatedLine = lineOffset
182184
generatedColumn = columnOffset
183-
sourceIndex = sourceOffset
184-
originalLine = 0
185-
originalColumn = 0
186-
originalName = nameOffset
185+
sourceIndex := sourceOffset
186+
var originalLine int32
187+
var originalColumn int32
188+
originalName := nameOffset
187189

188190
current := 0
189191
errorText := ""
@@ -362,9 +364,25 @@ func ParseSourceMap(log logger.Log, source logger.Source) *sourcemap.SourceMap {
362364
}
363365

364366
if len(sourcesContentArray) > 0 {
367+
// It's possible that one of the source maps inside "sections" has
368+
// different lengths for the "sources" and "sourcesContent" arrays.
369+
// This is bad because we need to us a single index to get the name
370+
// of the source from "sources[i]" and the content of the source
371+
// from "sourcesContent[i]".
372+
//
373+
// So if a previous source map had a shorter "sourcesContent" array
374+
// than its "sources" array (or if the previous source map just had
375+
// no "sourcesContent" array), expand our aggregated array to the
376+
// right length by padding it out with empty entries.
365377
sourcesContent = append(sourcesContent, make([]sourcemap.SourceContent, int(sourceOffset)-len(sourcesContent))...)
366378

367379
for i, item := range sourcesContentArray {
380+
// Make sure we don't ever record more "sourcesContent" entries
381+
// than there are "sources" entries, which is possible because
382+
// these are two separate arrays in the source map JSON. We need
383+
// to avoid this because that would mess up our shared indexing
384+
// of the "sources" and "sourcesContent" arrays. See the above
385+
// comment for more details.
368386
if i == sourcesLen {
369387
break
370388
}

scripts/verify-source-map.js

+69
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,69 @@ const toSearchAbsoluteSourcesURL = {
496496
test: 'input.js',
497497
}
498498

499+
// This test case was generated using the "shadow-cljs" tool by someone who has
500+
// no idea how to write Clojure code (i.e. me). See the following GitHub issue
501+
// for more details: https://github.com/evanw/esbuild/issues/3439
502+
//
503+
// Note that the mappings in the Clojure output strangely seem to be really
504+
// buggy. Many sub-expressions with two operands map the operands switched,
505+
// strings are way off, and there's even one mapping that's floating off in
506+
// space past the end of the line. This appears to just be bad output from the
507+
// Clojure tooling itself though, and not a problem with esbuild.
508+
//
509+
// For the example code below, I manually edited the mapping for the "done"
510+
// string to line up correctly so that this test can pass (it was off by
511+
// five lines).
512+
const testCaseIndexSourceMap = {
513+
'entry.js': `
514+
import './app.main.js'
515+
console.log('testing')
516+
`,
517+
'app.main.js': `export const $APP = {};
518+
export const shadow$provide = {};
519+
export const $jscomp = {};
520+
/*
521+
522+
Copyright The Closure Library Authors.
523+
SPDX-License-Identifier: Apache-2.0
524+
*/
525+
console.log(function $app$lib$log_many$$($G__6268$jscomp$1_i$jscomp$282$$, $collection$$) {
526+
return $G__6268$jscomp$1_i$jscomp$282$$ < $collection$$.length ? (console.` +
527+
`log($collection$$.at($G__6268$jscomp$1_i$jscomp$282$$)), $G__6268$jscom` +
528+
`p$1_i$jscomp$282$$ += 1, $app$lib$log_many$$.$cljs$core$IFn$_invoke$ari` +
529+
`ty$2$ ? $app$lib$log_many$$.$cljs$core$IFn$_invoke$arity$2$($G__6268$js` +
530+
`comp$1_i$jscomp$282$$, $collection$$) : $app$lib$log_many$$.call(null, ` +
531+
`$G__6268$jscomp$1_i$jscomp$282$$, $collection$$)) : "done";
532+
}(0, Object.keys(console)));
533+
export const render = {}.render;
534+
535+
//# sourceMappingURL=app.main.js.map`,
536+
'app.main.js.map': `{"version":3,"file":"app.main.js","sections":[{"offset` +
537+
`":{"line":3,"column":0},"map":{"version":3,"file":"app.main.js","lineCo` +
538+
`unt":10,"mappings":"A;;;;;AAGMA,OAAAA,CAAAA,GAAAA,CCDAC,QAAAA,oBAAAA,CA` +
539+
`AUC,gCAAVD,EAAYE,aAAZF,CAAYE;AAAlB,SACSD,gCADT,GACWC,aAAUA,CAAAA,MADrB,` +
540+
`IAGYH,OAAAA,CAAAA,GAAAA,CAAgBG,aAAAA,CAAAA,EAAAA,CAAWD,gCAAXC,CAAhBH,CA` +
541+
`CN,EAAUE,gCAAV,IAAaA,CAAb,EAAAE,mBAAAC,CAAAA,+BAAA,GAAAD,mBAAAC,CAAAA,+` +
542+
`BAAA,CAAAC,gCAAA,EAAkBH,aAAlB,CAAA,GAAAI,mBAAAA,CAAAA,IAAAA,CAAAA,IAAAA` +
543+
`,EAAAD,gCAAAC,EAAkBJ,aAAlBI,CAJN,IAKI,MALJ;AAAkBJ,CDCD,CAACF,CAAD,EAAgB` +
544+
`O,MAAOC,CAAAA,IAAP,CAAiBT,OAAjB,CAAhB,CAAXA,CAAAA;AEFN,sBFDkBU,EECaC,CA` +
545+
`AAA,MAA/B;;","sources":["app/app.cljs","app/lib.cljs","shadow/module/ap` +
546+
`p.main/append.js"],"sourcesContent":["(ns app.app\\n (:require [app.li` +
547+
`b :as lib]))\\n\\n(.log js/console (lib/log-many 0 (.keys js/Object js/` +
548+
`console)))\\n","(ns app.lib)\\n\\n(defn log-many [i collection]\\n (if` +
549+
` (< i (.-length collection))\\n (do\\n (.log js/console (.at co` +
550+
`llection i))\\n (log-many (+ i 1) collection))\\n \\"done\\"))` +
551+
`\\n","\\nshadow$export(\\"render\\",app.app.render);"],"names":["js/con` +
552+
`sole","app.lib/log-many","i","collection","app.lib.log_manycljs$core$IF` +
553+
`n$_invoke$arity$2","cljs$core$IFn$_invoke$arity$2","G__6268","G__6269",` +
554+
`"Object","js/Object","app.apprender","render"],"x_google_ignoreList":[2` +
555+
`]}}]}`,
556+
}
557+
558+
const toSearchIndexSourceMap = {
559+
'done': 'app/lib.cljs',
560+
}
561+
499562
// This case covers a crash when esbuild would generate an invalid source map
500563
// containing a mapping with an index of a source that's out of bounds of the
501564
// "sources" array. This happened when generating the namespace exports chunk
@@ -1149,6 +1212,12 @@ async function main() {
11491212
entryPoints: ['entry.js'],
11501213
crlf,
11511214
}),
1215+
check('indexed-source-map' + suffix, testCaseIndexSourceMap, toSearchIndexSourceMap, {
1216+
outfile: 'out.js',
1217+
flags: flags.concat('--bundle'),
1218+
entryPoints: ['entry.js'],
1219+
crlf,
1220+
}),
11521221
check('issue-4070' + suffix, testCaseNestedFoldersIssue4070, toSearchNestedFoldersIssue4070, {
11531222
outfile: 'out.js',
11541223
flags: flags.concat('--bundle'),

0 commit comments

Comments
 (0)