@atlaspack/packager-js 2.25.10 → 2.25.11-dev-fix-sourcemaps-50acc1acf.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,6 +53,29 @@ const REPLACEMENT_RE =
53
53
  /\n|import\s+"([0-9a-f]{16,20}:.+?)";|(?:\$[0-9a-f]{16,20}\$exports)|(?:\$[0-9a-f]{16,20}\$(?:import|importAsync|require)\$[0-9a-f]+(?:\$[0-9a-f]+)?)/g;
54
54
 
55
55
  const BUILTINS = Object.keys(globals.builtin);
56
+
57
+ /**
58
+ * Joins filtered hoisted parcelRequire values into the bundle output,
59
+ * returning the text to append and the number of newlines it adds.
60
+ *
61
+ * Returns `{text: '', lineCount: 0}` when there are no values, to avoid
62
+ * emitting a stray leading `\n` (which would over-count lineCount and
63
+ * desynchronise the bundle source-map offset bookkeeping).
64
+ *
65
+ * Exported for direct unit testing of the lineCount bookkeeping.
66
+ */
67
+ export function appendHoistedValues(values: ReadonlyArray<string>): {
68
+ text: string;
69
+ lineCount: number;
70
+ } {
71
+ if (values.length === 0) {
72
+ return {text: '', lineCount: 0};
73
+ }
74
+ return {
75
+ text: '\n' + values.join('\n'),
76
+ lineCount: values.length,
77
+ };
78
+ }
56
79
  const GLOBALS_BY_CONTEXT = {
57
80
  browser: new Set([...BUILTINS, ...Object.keys(globals.browser)]),
58
81
  'web-worker': new Set([...BUILTINS, ...Object.keys(globals.worker)]),
@@ -191,9 +214,13 @@ export class ScopeHoistingPackager {
191
214
  let [content, map, lines] = this.visitAsset(asset);
192
215
 
193
216
  if (sourceMap && map) {
217
+ this.addAssetBoundaryMarker(sourceMap, asset, lineCount);
194
218
  sourceMap.addSourceMap(map, lineCount);
195
219
  } else if (this.bundle.env.sourceMap) {
196
220
  sourceMap = map;
221
+ if (sourceMap) {
222
+ this.addAssetBoundaryMarker(sourceMap, asset, lineCount);
223
+ }
197
224
  }
198
225
 
199
226
  res += content + '\n';
@@ -334,6 +361,7 @@ export class ScopeHoistingPackager {
334
361
  this.parcelRequireName,
335
362
  );
336
363
  if (sourceMap && map) {
364
+ this.addAssetBoundaryMarker(sourceMap, mainEntry, lineCount);
337
365
  // @ts-expect-error TS2339 - addSourceMap method exists but missing from @parcel/source-map type definitions
338
366
  sourceMap.addSourceMap(map, lineCount);
339
367
  }
@@ -696,6 +724,34 @@ export class ScopeHoistingPackager {
696
724
  return this.buildAsset(asset, code, map);
697
725
  }
698
726
 
727
+ /**
728
+ * Insert a single "boundary marker" mapping at the start of the line where
729
+ * `asset`'s source map is about to be appended. Without this marker, source
730
+ * map consumers (and downstream optimizers such as swc that need to compose
731
+ * via nearest-neighbour lookups) will attribute the columns at the start of
732
+ * this asset's region to the *previous* asset's last mapping, because there
733
+ * is no mapping anchoring the start of the new asset. This is especially
734
+ * visible for assets with sparse maps (e.g. codegen'd `.graphql.ts` files
735
+ * from Relay) whose first mapping may be hundreds of columns into the line.
736
+ *
737
+ * The marker points at `(asset.filePath, 1, 0)`. Any mapping the asset's
738
+ * own map provides at the same generated position will override this one
739
+ * (last-write-wins inside `parcel_sourcemap`).
740
+ */
741
+ addAssetBoundaryMarker(
742
+ sourceMap: SourceMap,
743
+ asset: Asset,
744
+ lineOffset: number,
745
+ ): void {
746
+ if (!sourceMap) return;
747
+ let source = this.getAssetFilePath(asset);
748
+ sourceMap.addIndexedMapping({
749
+ generated: {line: lineOffset + 1, column: 0},
750
+ original: {line: 1, column: 0},
751
+ source,
752
+ });
753
+ }
754
+
699
755
  getAssetFilePath(asset: Asset): string {
700
756
  return path.relative(this.options.projectRoot, asset.filePath);
701
757
  }
@@ -712,6 +768,13 @@ export class ScopeHoistingPackager {
712
768
  this.bundle.env.sourceMap && map
713
769
  ? new SourceMap(this.options.projectRoot, map)
714
770
  : null;
771
+ // Anchor the start of this asset's region with a boundary marker so the
772
+ // first columns of the asset don't fall back to a previous asset's
773
+ // mapping during downstream source-map composition. See the
774
+ // addAssetBoundaryMarker JSDoc for the full rationale.
775
+ if (sourceMap) {
776
+ this.addAssetBoundaryMarker(sourceMap, asset, 0);
777
+ }
715
778
 
716
779
  // If this asset is skipped, just add dependencies and not the asset's content.
717
780
  if (this.shouldSkipAsset(asset)) {
@@ -751,6 +814,7 @@ export class ScopeHoistingPackager {
751
814
  let [code, map, lines] = this.visitAsset(resolved);
752
815
  depCode += code + '\n';
753
816
  if (sourceMap && map) {
817
+ this.addAssetBoundaryMarker(sourceMap, resolved, lineCount);
754
818
  sourceMap.addSourceMap(map, lineCount);
755
819
  }
756
820
  lineCount += lines + 1;
@@ -788,7 +852,9 @@ export class ScopeHoistingPackager {
788
852
  code += append;
789
853
 
790
854
  let lineCount = 0;
791
- let depContent: Array<[string, SourceMap | null | undefined, number]> = [];
855
+ let depContent: Array<
856
+ [string, SourceMap | null | undefined, number, Asset?]
857
+ > = [];
792
858
  if (depMap.size === 0 && replacements.size === 0) {
793
859
  // If there are no dependencies or replacements, use a simple function to count the number of lines.
794
860
  lineCount = countLines(code) - 1;
@@ -877,7 +943,13 @@ export class ScopeHoistingPackager {
877
943
  }
878
944
  } else {
879
945
  if (shouldWrap) {
880
- depContent.push(this.visitAsset(resolved));
946
+ let visited = this.visitAsset(resolved);
947
+ depContent.push([
948
+ visited[0],
949
+ visited[1],
950
+ visited[2],
951
+ resolved,
952
+ ]);
881
953
  } else {
882
954
  let [depCode, depMap, depLines] =
883
955
  this.visitAsset(resolved);
@@ -906,6 +978,7 @@ export class ScopeHoistingPackager {
906
978
  }
907
979
 
908
980
  if (map) {
981
+ this.addAssetBoundaryMarker(sourceMap, resolved, lineCount);
909
982
  sourceMap.addSourceMap(map, lineCount);
910
983
  }
911
984
  }
@@ -959,10 +1032,13 @@ ${code}
959
1032
  lineCount += 1;
960
1033
  }
961
1034
 
962
- for (let [depCode, map, lines] of depContent) {
1035
+ for (let [depCode, map, lines, depAsset] of depContent) {
963
1036
  if (!depCode) continue;
964
1037
  code += depCode + '\n';
965
1038
  if (sourceMap && map) {
1039
+ if (depAsset) {
1040
+ this.addAssetBoundaryMarker(sourceMap, depAsset, lineCount);
1041
+ }
966
1042
  sourceMap.addSourceMap(map, lineCount);
967
1043
  }
968
1044
  lineCount += lines + 1;
@@ -1453,8 +1529,13 @@ ${code}
1453
1529
  this.seenHoistedRequires.add(val);
1454
1530
  }
1455
1531
 
1456
- res += '\n' + hoistedValues.join('\n');
1457
- lineCount += hoisted.size;
1532
+ // Only emit the leading `\n` and bump the line count when we
1533
+ // actually have values to write. Otherwise `res += '\n' + ''` would
1534
+ // emit a stray blank line and the line count would over-count by
1535
+ // `hoisted.size` (which includes the now-filtered entries).
1536
+ let appended = appendHoistedValues(hoistedValues);
1537
+ res += appended.text;
1538
+ lineCount += appended.lineCount;
1458
1539
  } else {
1459
1540
  res += '\n' + [...hoisted.values()].join('\n');
1460
1541
  lineCount += hoisted.size;
@@ -0,0 +1,94 @@
1
+ import assert from 'assert';
2
+
3
+ import {appendHoistedValues} from '../src/ScopeHoistingPackager';
4
+
5
+ describe('appendHoistedValues', function () {
6
+ // This helper exists because the previous implementation in
7
+ // `getHoistedParcelRequires` would emit `res += '\n' + ''` and then
8
+ // `lineCount += hoisted.size` whenever the filtered hoisted set was empty
9
+ // (or smaller than the unfiltered set). Both bugs desynchronised the
10
+ // source-map line-offset bookkeeping in `ScopeHoistingPackager`. These
11
+ // tests pin the corrected behaviour.
12
+
13
+ it('returns an empty result and zero lineCount when given no values', function () {
14
+ const out = appendHoistedValues([]);
15
+ assert.strictEqual(
16
+ out.text,
17
+ '',
18
+ 'no text should be appended when there are zero hoisted values (avoids stray leading \\n)',
19
+ );
20
+ assert.strictEqual(
21
+ out.lineCount,
22
+ 0,
23
+ 'lineCount must be 0 when no values were appended (was previously off-by-`hoisted.size`)',
24
+ );
25
+ });
26
+
27
+ it('emits exactly one leading \\n plus the joined values, with lineCount equal to value count', function () {
28
+ const out = appendHoistedValues([
29
+ 'parcelRequire("aaaaaaa");',
30
+ 'parcelRequire("bbbbbbb");',
31
+ 'parcelRequire("ccccccc");',
32
+ ]);
33
+ assert.strictEqual(
34
+ out.text,
35
+ '\nparcelRequire("aaaaaaa");\nparcelRequire("bbbbbbb");\nparcelRequire("ccccccc");',
36
+ 'joined values must be prefixed with one \\n and separated by \\n',
37
+ );
38
+ assert.strictEqual(
39
+ out.lineCount,
40
+ 3,
41
+ 'lineCount must equal the number of newlines actually written (3 for 3 values: 1 leading + 2 separators = 3 \\n)',
42
+ );
43
+ // Cross-check: count actual newlines in the returned text.
44
+ assert.strictEqual(
45
+ out.text.split('\n').length - 1,
46
+ out.lineCount,
47
+ 'lineCount must equal the actual number of \\n characters in the appended text',
48
+ );
49
+ });
50
+
51
+ it('handles a single value correctly', function () {
52
+ const out = appendHoistedValues(['parcelRequire("only");']);
53
+ assert.strictEqual(out.text, '\nparcelRequire("only");');
54
+ assert.strictEqual(out.lineCount, 1);
55
+ assert.strictEqual(
56
+ out.text.split('\n').length - 1,
57
+ out.lineCount,
58
+ 'lineCount must equal the actual number of \\n characters in the appended text',
59
+ );
60
+ });
61
+
62
+ it('lineCount always equals the newline count of the emitted text', function () {
63
+ // Property: for *any* input, the lineCount must match the actual number
64
+ // of newline characters in `text`. This is the invariant the
65
+ // ScopeHoistingPackager relies on to track lineOffset for the merged
66
+ // source map. The previous bug violated this invariant when filtering
67
+ // dropped some entries (lineCount was `hoisted.size`, but text only
68
+ // contained `filteredValues.length` newlines).
69
+ for (const values of [
70
+ [],
71
+ ['x'],
72
+ ['x', 'y'],
73
+ ['x', 'y', 'z'],
74
+ ['p\nq', 'r\ns'], // values containing embedded newlines — lineCount
75
+ // intentionally tracks only the separator newlines we emit (1 leading
76
+ // + N-1 separators = N), NOT the embedded ones; the caller is
77
+ // responsible for not embedding extra newlines in hoisted require
78
+ // strings. Document the assumption with this guard:
79
+ ]) {
80
+ const out = appendHoistedValues(values);
81
+ if (values.length === 0) {
82
+ assert.strictEqual(out.text, '');
83
+ assert.strictEqual(out.lineCount, 0);
84
+ } else if (values.every((v) => !v.includes('\n'))) {
85
+ const actualNewlines = out.text.split('\n').length - 1;
86
+ assert.strictEqual(
87
+ out.lineCount,
88
+ actualNewlines,
89
+ `lineCount mismatch for values=${JSON.stringify(values)}: claimed ${out.lineCount}, actual ${actualNewlines}`,
90
+ );
91
+ }
92
+ }
93
+ });
94
+ });