@atlaspack/core 2.33.0 → 2.34.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.
@@ -13,6 +13,7 @@ import type {ProjectPath} from '../projectPath';
13
13
  import {HASH_REF_HASH_LEN, HASH_REF_PREFIX} from '../constants';
14
14
  import nullthrows from 'nullthrows';
15
15
  import path from 'path';
16
+ import url from 'url';
16
17
  import {NamedBundle} from '../public/Bundle';
17
18
  import {blobToStream, TapStream} from '@atlaspack/utils';
18
19
  import {Readable, Transform, pipeline} from 'stream';
@@ -40,9 +41,18 @@ import {PluginTracer, tracer} from '@atlaspack/profiler';
40
41
  import {requestTypes} from '../RequestTracker';
41
42
  import {getFeatureFlag} from '@atlaspack/feature-flags';
42
43
  import {fromEnvironmentId} from '../EnvironmentManager';
44
+ import SourceMap from '@atlaspack/source-map';
43
45
 
44
46
  const HASH_REF_PREFIX_LEN = HASH_REF_PREFIX.length;
45
47
  const BOUNDARY_LENGTH = HASH_REF_PREFIX.length + 32 - 1;
48
+ const HASH_REF_PLACEHOLDER_LEN = HASH_REF_PREFIX_LEN + HASH_REF_HASH_LEN;
49
+
50
+ export type HashRefReplacement = {
51
+ line: number;
52
+ column: number;
53
+ originalLength: number;
54
+ newLength: number;
55
+ };
46
56
 
47
57
  type WriteBundleRequestInput = {
48
58
  bundleGraph: BundleGraph;
@@ -158,6 +168,9 @@ async function run({input, options, api}) {
158
168
  let {devDeps, invalidDevDeps} = await getDevDepRequests(api);
159
169
  invalidateDevDeps(invalidDevDeps, options, config);
160
170
 
171
+ const bundleReplacements = getFeatureFlag('fixSourceMapHashRefs')
172
+ ? []
173
+ : undefined;
161
174
  await writeFiles(
162
175
  contentStream,
163
176
  info,
@@ -169,17 +182,50 @@ async function run({input, options, api}) {
169
182
  writeOptions,
170
183
  devDeps,
171
184
  api,
185
+ bundleReplacements,
172
186
  );
173
187
 
174
188
  const hasSourceMap = getFeatureFlag('cachePerformanceImprovements')
175
189
  ? await options.cache.hasLargeBlob(mapKey)
176
190
  : await options.cache.has(mapKey);
177
191
  if (mapKey && env.sourceMap && !env.sourceMap.inline && hasSourceMap) {
178
- const mapEntry = getFeatureFlag('cachePerformanceImprovements')
179
- ? await options.cache.getLargeBlob(mapKey)
180
- : await options.cache.getBlob(mapKey);
192
+ let mapStream: Readable;
193
+ if (
194
+ getFeatureFlag('fixSourceMapHashRefs') &&
195
+ bundleReplacements &&
196
+ bundleReplacements.length > 0
197
+ ) {
198
+ const mapEntry = getFeatureFlag('cachePerformanceImprovements')
199
+ ? await options.cache.getLargeBlob(mapKey)
200
+ : await options.cache.getBlob(mapKey);
201
+ const mapBuffer = Buffer.isBuffer(mapEntry)
202
+ ? mapEntry
203
+ : Buffer.from(mapEntry);
204
+ const projectRoot =
205
+ typeof options.projectRoot === 'string'
206
+ ? options.projectRoot
207
+ : String(options.projectRoot);
208
+ const sourceMap = new SourceMap(projectRoot, mapBuffer);
209
+ applyReplacementsToSourceMap(sourceMap, bundleReplacements);
210
+ const mapJson = await sourceMap.stringify({
211
+ format: 'string',
212
+ file: name,
213
+ sourceRoot: computeSourceMapRoot(bundle, options),
214
+ });
215
+ mapStream = blobToStream(
216
+ Buffer.from(
217
+ typeof mapJson === 'string' ? mapJson : JSON.stringify(mapJson),
218
+ 'utf8',
219
+ ),
220
+ );
221
+ } else {
222
+ const mapEntry = getFeatureFlag('cachePerformanceImprovements')
223
+ ? await options.cache.getLargeBlob(mapKey)
224
+ : await options.cache.getBlob(mapKey);
225
+ mapStream = blobToStream(mapEntry);
226
+ }
181
227
  await writeFiles(
182
- blobToStream(mapEntry),
228
+ mapStream,
183
229
  info,
184
230
  hashRefToNameHash,
185
231
  options,
@@ -206,6 +252,88 @@ async function run({input, options, api}) {
206
252
  return res;
207
253
  }
208
254
 
255
+ export function applyReplacementsToSourceMap(
256
+ sourceMap: SourceMap,
257
+ replacements: HashRefReplacement[],
258
+ ): void {
259
+ if (replacements.length === 0) return;
260
+ const sorted = [...replacements].sort(
261
+ (a, b) => a.line - b.line || a.column - b.column,
262
+ );
263
+ for (const r of sorted) {
264
+ const delta = r.newLength - r.originalLength;
265
+ if (delta !== 0) {
266
+ // r.column is in post-replacement coordinates (matching the already-shifted
267
+ // source map state after previous offsetColumns calls). The end of the
268
+ // placeholder in these coordinates is simply r.column + r.originalLength.
269
+ const offsetStartColumn = r.column + r.originalLength;
270
+ const line1Based = r.line + 1;
271
+ if (line1Based >= 1 && offsetStartColumn + delta >= 0) {
272
+ sourceMap.offsetColumns(line1Based, offsetStartColumn, delta);
273
+ }
274
+ }
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Computes the sourceRoot for a source map file. This is the relative path from
280
+ * the output directory back to the project root, so that source paths (stored
281
+ * relative to projectRoot) resolve correctly from the .map file location.
282
+ *
283
+ * Returns undefined when sources are inlined (inlineSources), since the browser
284
+ * doesn't need to fetch them and sourceRoot would be unnecessary.
285
+ *
286
+ * This logic must stay in sync with PackagerRunner.generateSourceMap.
287
+ */
288
+ export function computeSourceMapRoot(
289
+ bundle: Bundle,
290
+ options: AtlaspackOptions,
291
+ ): string | undefined {
292
+ let name = nullthrows(bundle.name);
293
+ let filePath = joinProjectPath(bundle.target.distDir, name);
294
+ let fullPath = fromProjectPath(options.projectRoot, filePath);
295
+ let sourceRoot: string = path.relative(
296
+ path.dirname(fullPath),
297
+ options.projectRoot,
298
+ );
299
+
300
+ let inlineSources = false;
301
+
302
+ const bundleEnv = fromEnvironmentId(bundle.env);
303
+ if (bundle.target) {
304
+ const bundleTargetEnv = fromEnvironmentId(bundle.target.env);
305
+
306
+ if (bundleEnv.sourceMap && bundleEnv.sourceMap.sourceRoot !== undefined) {
307
+ sourceRoot = bundleEnv.sourceMap.sourceRoot;
308
+ } else if (options.serveOptions && bundleTargetEnv.context === 'browser') {
309
+ sourceRoot = '/__parcel_source_root';
310
+ }
311
+
312
+ if (
313
+ bundleEnv.sourceMap &&
314
+ bundleEnv.sourceMap.inlineSources !== undefined
315
+ ) {
316
+ inlineSources = bundleEnv.sourceMap.inlineSources;
317
+ } else if (bundleTargetEnv.context !== 'node') {
318
+ inlineSources = options.mode === 'production';
319
+ }
320
+ }
321
+
322
+ let isInlineMap = bundleEnv.sourceMap && bundleEnv.sourceMap.inline;
323
+
324
+ if (getFeatureFlag('omitSourcesContentInMemory') && !isInlineMap) {
325
+ if (!(bundleEnv.sourceMap && bundleEnv.sourceMap.inlineSources === false)) {
326
+ inlineSources = true;
327
+ }
328
+ }
329
+
330
+ if (inlineSources) {
331
+ return undefined;
332
+ }
333
+
334
+ return url.format(url.parse(sourceRoot + '/'));
335
+ }
336
+
209
337
  async function writeFiles(
210
338
  // @ts-expect-error TS2503
211
339
  inputStream: stream.Readable,
@@ -218,6 +346,7 @@ async function writeFiles(
218
346
  writeOptions: FileOptions | null | undefined,
219
347
  devDeps: Map<string, string>,
220
348
  api: RunAPI<PackagedBundleInfo>,
349
+ bundleReplacements?: HashRefReplacement[],
221
350
  ) {
222
351
  let compressors = await config.getCompressors(
223
352
  fromProjectPathRelative(filePath),
@@ -225,7 +354,7 @@ async function writeFiles(
225
354
  let fullPath = fromProjectPath(options.projectRoot, filePath);
226
355
 
227
356
  let stream = info.hashReferences.length
228
- ? inputStream.pipe(replaceStream(hashRefToNameHash))
357
+ ? inputStream.pipe(replaceStream(hashRefToNameHash, bundleReplacements))
229
358
  : inputStream;
230
359
 
231
360
  let promises: Array<Promise<undefined>> = [];
@@ -314,9 +443,30 @@ async function runCompressor(
314
443
  }
315
444
  }
316
445
 
317
- function replaceStream(hashRefToNameHash: Map<string, string>) {
446
+ function advanceLineColumn(
447
+ line: number,
448
+ column: number,
449
+ buf: Buffer,
450
+ ): {line: number; column: number} {
451
+ for (let i = 0; i < buf.length; i++) {
452
+ if (buf[i] === 0x0a) {
453
+ line++;
454
+ column = 0;
455
+ } else {
456
+ column++;
457
+ }
458
+ }
459
+ return {line, column};
460
+ }
461
+
462
+ function replaceStream(
463
+ hashRefToNameHash: Map<string, string>,
464
+ replacements?: HashRefReplacement[],
465
+ ) {
318
466
  let boundaryStr = Buffer.alloc(0);
319
467
  let replaced = Buffer.alloc(0);
468
+ let outputLine = 0;
469
+ let outputColumn = 0;
320
470
  return new Transform({
321
471
  transform(
322
472
  chunk: Buffer | string,
@@ -347,22 +497,42 @@ function replaceStream(hashRefToNameHash: Map<string, string>) {
347
497
  .subarray(matchI, matchI + HASH_REF_PREFIX_LEN + HASH_REF_HASH_LEN)
348
498
  .toString();
349
499
  let replacement = Buffer.from(hashRefToNameHash.get(match) ?? match);
500
+ // Copy pre-match content FIRST so position calculation includes it
350
501
  replaced.set(str.subarray(lastMatchI, matchI), replacedLength);
351
502
  replacedLength += matchI - lastMatchI;
503
+ if (replacements) {
504
+ const pos = advanceLineColumn(
505
+ outputLine,
506
+ outputColumn,
507
+ replaced.subarray(0, replacedLength),
508
+ );
509
+ replacements.push({
510
+ line: pos.line,
511
+ column: pos.column,
512
+ originalLength: HASH_REF_PLACEHOLDER_LEN,
513
+ newLength: replacement.byteLength,
514
+ });
515
+ }
352
516
  replaced.set(replacement, replacedLength);
353
517
  replacedLength += replacement.byteLength;
354
518
  lastMatchI = matchI + HASH_REF_PREFIX_LEN + HASH_REF_HASH_LEN;
355
519
  }
356
520
  }
357
521
 
522
+ const pushLen = replacedLength - BOUNDARY_LENGTH;
523
+ const pushed = advanceLineColumn(
524
+ outputLine,
525
+ outputColumn,
526
+ replaced.subarray(0, pushLen),
527
+ );
528
+ outputLine = pushed.line;
529
+ outputColumn = pushed.column;
530
+
358
531
  boundaryStr = replaced.subarray(
359
532
  replacedLength - BOUNDARY_LENGTH,
360
533
  replacedLength,
361
534
  );
362
- let strUpToBoundary = replaced.subarray(
363
- 0,
364
- replacedLength - BOUNDARY_LENGTH,
365
- );
535
+ let strUpToBoundary = replaced.subarray(0, pushLen);
366
536
  cb(null, strUpToBoundary);
367
537
  },
368
538
 
@@ -22,6 +22,16 @@ import {createPackageRequest} from './PackageRequest';
22
22
  import createWriteBundleRequest from './WriteBundleRequest';
23
23
  import {debugTools} from '@atlaspack/utils';
24
24
 
25
+ /** Length of the hash suffix in output filenames (e.g. .runtime.13dc01ac.js). */
26
+ const NAME_HASH_DISPLAY_LEN = 8;
27
+
28
+ /** Use at most NAME_HASH_DISPLAY_LEN chars for the name hash so filenames stay short. */
29
+ function nameHashForFilename(hash: string): string {
30
+ return hash.length <= NAME_HASH_DISPLAY_LEN
31
+ ? hash
32
+ : hash.slice(-NAME_HASH_DISPLAY_LEN);
33
+ }
34
+
25
35
  type WriteBundlesRequestInput = {
26
36
  bundleGraph: BundleGraph;
27
37
  optionsRef: SharedReference;
@@ -111,12 +121,12 @@ async function run({
111
121
  // Do not package and write placeholder bundles to disk. We just
112
122
  // need to update the name so other bundles can reference it.
113
123
  if (bundle.isPlaceholder) {
114
- let hash = bundle.id.slice(-8);
115
- hashRefToNameHash.set(bundle.hashReference, hash);
124
+ const nameHash = nameHashForFilename(bundle.id);
125
+ hashRefToNameHash.set(bundle.hashReference, nameHash);
116
126
  let name = nullthrows(
117
127
  bundle.name,
118
128
  `Expected ${bundle.type} bundle to have a name`,
119
- ).replace(bundle.hashReference, hash);
129
+ ).replace(bundle.hashReference, nameHash);
120
130
  res.set(bundle.id, {
121
131
  filePath: joinProjectPath(bundle.target.distDir, name),
122
132
  bundleId: bundle.id,
@@ -178,9 +188,9 @@ async function run({
178
188
  if (!info.hashReferences.length) {
179
189
  hashRefToNameHash.set(
180
190
  bundle.hashReference,
181
- options.shouldContentHash
182
- ? info.hash.slice(-8)
183
- : bundle.id.slice(-8),
191
+ nameHashForFilename(
192
+ options.shouldContentHash ? info.hash : bundle.id,
193
+ ),
184
194
  );
185
195
  let writeBundleRequest = createWriteBundleRequest({
186
196
  bundle,
@@ -256,13 +266,15 @@ function assignComplexNameHashes(
256
266
  }
257
267
  hashRefToNameHash.set(
258
268
  bundle.hashReference,
259
- options.shouldContentHash
260
- ? hashString(
261
- [...getBundlesIncludedInHash(bundle.id, bundleInfoMap)]
262
- .map((bundleId) => bundleInfoMap[bundleId].hash)
263
- .join(':'),
264
- ).slice(-8)
265
- : bundle.id.slice(-8),
269
+ nameHashForFilename(
270
+ options.shouldContentHash
271
+ ? hashString(
272
+ [...getBundlesIncludedInHash(bundle.id, bundleInfoMap)]
273
+ .map((bundleId) => bundleInfoMap[bundleId].hash)
274
+ .join(':'),
275
+ )
276
+ : bundle.id,
277
+ ),
266
278
  );
267
279
  }
268
280
  }
@@ -0,0 +1,239 @@
1
+ import assert from 'assert';
2
+ import SourceMap from '@atlaspack/source-map';
3
+ import {
4
+ applyReplacementsToSourceMap,
5
+ type HashRefReplacement,
6
+ } from '../../src/requests/WriteBundleRequest';
7
+
8
+ const HASH_REF = 'HASH_REF_0123456789abcdef'; // 25 chars
9
+ const HASH_REPLACEMENT = 'a1b2c3d4'; // 8 chars
10
+ const HASH_REF_LEN = HASH_REF.length; // 25
11
+ const REPLACEMENT_LEN = HASH_REPLACEMENT.length; // 8
12
+
13
+ /**
14
+ * Build a single-line code string with HASH_REF placeholders at known positions.
15
+ * Returns original code, replaced code, correct replacement coordinates, and
16
+ * identifier position tracking in both coordinate spaces.
17
+ */
18
+ function buildCodeWithHashRefs(
19
+ segments: Array<{type: 'code'; text: string} | {type: 'hashref'}>,
20
+ ) {
21
+ let code = '';
22
+ const hashPositions: Array<{column: number}> = [];
23
+ const identifierPositions = new Map<string, number>();
24
+
25
+ for (const seg of segments) {
26
+ if (seg.type === 'hashref') {
27
+ hashPositions.push({column: code.length});
28
+ code += HASH_REF;
29
+ } else {
30
+ const regex = /[A-Z][A-Z_0-9]{3,}/g;
31
+ let match;
32
+ while ((match = regex.exec(seg.text)) !== null) {
33
+ identifierPositions.set(match[0], code.length + match.index);
34
+ }
35
+ code += seg.text;
36
+ }
37
+ }
38
+
39
+ // Build replaced code and compute CORRECT replacement coordinates
40
+ let replacedCode = '';
41
+ const correctReplacements: HashRefReplacement[] = [];
42
+ let srcIdx = 0;
43
+ for (const hp of hashPositions) {
44
+ replacedCode += code.slice(srcIdx, hp.column);
45
+ correctReplacements.push({
46
+ line: 0,
47
+ column: replacedCode.length,
48
+ originalLength: HASH_REF_LEN,
49
+ newLength: REPLACEMENT_LEN,
50
+ });
51
+ replacedCode += HASH_REPLACEMENT;
52
+ srcIdx = hp.column + HASH_REF_LEN;
53
+ }
54
+ replacedCode += code.slice(srcIdx);
55
+
56
+ // Track where identifiers end up in the replaced code
57
+ const replacedIdentifierPositions = new Map<string, number>();
58
+ for (const [name] of identifierPositions) {
59
+ const idx = replacedCode.indexOf(name);
60
+ if (idx >= 0) replacedIdentifierPositions.set(name, idx);
61
+ }
62
+
63
+ return {
64
+ originalCode: code,
65
+ replacedCode,
66
+ correctReplacements,
67
+ identifierPositions,
68
+ replacedIdentifierPositions,
69
+ };
70
+ }
71
+
72
+ describe('applyReplacementsToSourceMap', () => {
73
+ describe('with correct replacement coordinates', () => {
74
+ it('should correctly adjust a single HASH_REF replacement', () => {
75
+ const {
76
+ correctReplacements,
77
+ identifierPositions,
78
+ replacedIdentifierPositions,
79
+ } = buildCodeWithHashRefs([
80
+ {type: 'hashref'},
81
+ {type: 'code', text: ';var x=SOME_IDENT;'},
82
+ ]);
83
+
84
+ const origCol = identifierPositions.get('SOME_IDENT')!;
85
+ const expectedCol = replacedIdentifierPositions.get('SOME_IDENT')!;
86
+
87
+ const sm = new SourceMap('/');
88
+ sm.addIndexedMapping({
89
+ generated: {line: 1, column: origCol},
90
+ original: {line: 10, column: 5},
91
+ source: 'test.js',
92
+ });
93
+
94
+ applyReplacementsToSourceMap(sm, correctReplacements);
95
+
96
+ const mappings = sm.getMap().mappings;
97
+ const mapping = mappings.find(
98
+ (m) => m.original?.line === 10 && m.original?.column === 5,
99
+ );
100
+ assert.ok(mapping, 'Mapping should exist');
101
+ assert.strictEqual(mapping!.generated.column, expectedCol);
102
+ });
103
+
104
+ it('should correctly adjust multiple HASH_REF replacements on the same line', () => {
105
+ const {
106
+ correctReplacements,
107
+ identifierPositions,
108
+ replacedIdentifierPositions,
109
+ } = buildCodeWithHashRefs([
110
+ {type: 'hashref'},
111
+ {type: 'code', text: ';var a=IDENT_ALPHA;require("'},
112
+ {type: 'hashref'},
113
+ {type: 'code', text: '");var b=IDENT_BETA;require("'},
114
+ {type: 'hashref'},
115
+ {type: 'code', text: '");var c=IDENT_GAMMA;'},
116
+ ]);
117
+
118
+ const sm = new SourceMap('/');
119
+ sm.addIndexedMapping({
120
+ generated: {line: 1, column: identifierPositions.get('IDENT_ALPHA')!},
121
+ original: {line: 10, column: 0},
122
+ source: 'test.js',
123
+ });
124
+ sm.addIndexedMapping({
125
+ generated: {line: 1, column: identifierPositions.get('IDENT_BETA')!},
126
+ original: {line: 20, column: 0},
127
+ source: 'test.js',
128
+ });
129
+ sm.addIndexedMapping({
130
+ generated: {line: 1, column: identifierPositions.get('IDENT_GAMMA')!},
131
+ original: {line: 30, column: 0},
132
+ source: 'test.js',
133
+ });
134
+
135
+ applyReplacementsToSourceMap(sm, correctReplacements);
136
+
137
+ const mappings = sm.getMap().mappings;
138
+ for (const [name, origLine] of [
139
+ ['IDENT_ALPHA', 10],
140
+ ['IDENT_BETA', 20],
141
+ ['IDENT_GAMMA', 30],
142
+ ] as const) {
143
+ const mapping = mappings.find((m) => m.original?.line === origLine);
144
+ const expectedCol = replacedIdentifierPositions.get(name)!;
145
+ assert.ok(mapping, `${name} mapping should exist`);
146
+ assert.strictEqual(
147
+ mapping!.generated.column,
148
+ expectedCol,
149
+ `${name}: expected col ${expectedCol}, got ${mapping!.generated.column}`,
150
+ );
151
+ }
152
+ });
153
+
154
+ it('should handle 10 replacements on the same line', () => {
155
+ const segments: Array<{type: 'code'; text: string} | {type: 'hashref'}> =
156
+ [];
157
+ for (let i = 0; i < 10; i++) {
158
+ segments.push({type: 'hashref'});
159
+ segments.push({
160
+ type: 'code',
161
+ text: `;var x${i}=TARGET_${String(i).padStart(2, '0')};require("`,
162
+ });
163
+ }
164
+ segments.push({type: 'code', text: '");'});
165
+
166
+ const {
167
+ correctReplacements,
168
+ identifierPositions,
169
+ replacedIdentifierPositions,
170
+ } = buildCodeWithHashRefs(segments);
171
+
172
+ const sm = new SourceMap('/');
173
+ for (let i = 0; i < 10; i++) {
174
+ const name = `TARGET_${String(i).padStart(2, '0')}`;
175
+ sm.addIndexedMapping({
176
+ generated: {line: 1, column: identifierPositions.get(name)!},
177
+ original: {line: (i + 1) * 10, column: 0},
178
+ source: 'test.js',
179
+ });
180
+ }
181
+
182
+ applyReplacementsToSourceMap(sm, correctReplacements);
183
+
184
+ const mappings = sm.getMap().mappings;
185
+ for (let i = 0; i < 10; i++) {
186
+ const name = `TARGET_${String(i).padStart(2, '0')}`;
187
+ const mapping = mappings.find((m) => m.original?.line === (i + 1) * 10);
188
+ const expectedCol = replacedIdentifierPositions.get(name)!;
189
+ assert.ok(mapping, `${name} mapping should exist`);
190
+ assert.strictEqual(
191
+ mapping!.generated.column,
192
+ expectedCol,
193
+ `${name}: expected col ${expectedCol}, got ${mapping!.generated.column}`,
194
+ );
195
+ }
196
+ });
197
+
198
+ it('should not affect mappings before the first HASH_REF', () => {
199
+ const {
200
+ correctReplacements,
201
+ identifierPositions,
202
+ replacedIdentifierPositions,
203
+ } = buildCodeWithHashRefs([
204
+ {type: 'code', text: 'var BEFORE_HASH=1;'},
205
+ {type: 'hashref'},
206
+ {type: 'code', text: ';var AFTER_HASH=2;'},
207
+ ]);
208
+
209
+ const sm = new SourceMap('/');
210
+ sm.addIndexedMapping({
211
+ generated: {line: 1, column: identifierPositions.get('BEFORE_HASH')!},
212
+ original: {line: 1, column: 0},
213
+ source: 'test.js',
214
+ });
215
+ sm.addIndexedMapping({
216
+ generated: {line: 1, column: identifierPositions.get('AFTER_HASH')!},
217
+ original: {line: 2, column: 0},
218
+ source: 'test.js',
219
+ });
220
+
221
+ applyReplacementsToSourceMap(sm, correctReplacements);
222
+
223
+ const mappings = sm.getMap().mappings;
224
+ const beforeMapping = mappings.find((m) => m.original?.line === 1);
225
+ assert.ok(beforeMapping);
226
+ assert.strictEqual(
227
+ beforeMapping!.generated.column,
228
+ replacedIdentifierPositions.get('BEFORE_HASH'),
229
+ );
230
+
231
+ const afterMapping = mappings.find((m) => m.original?.line === 2);
232
+ assert.ok(afterMapping);
233
+ assert.strictEqual(
234
+ afterMapping!.generated.column,
235
+ replacedIdentifierPositions.get('AFTER_HASH'),
236
+ );
237
+ });
238
+ });
239
+ });