@atlaspack/core 2.35.0 → 2.38.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/dist/Atlaspack.js +41 -6
  3. package/dist/atlaspack-v3/AtlaspackV3.js +7 -4
  4. package/dist/atlaspack-v3/fs.js +1 -0
  5. package/dist/atlaspack-v3/worker/worker.js +11 -2
  6. package/dist/requests/AssetGraphRequestRust.js +5 -1
  7. package/dist/requests/AtlaspackBuildRequest.js +3 -0
  8. package/dist/requests/BundleGraphRequest.js +9 -6
  9. package/dist/requests/BundleGraphRequestRust.js +6 -1
  10. package/dist/requests/BundleGraphRequestUtils.js +133 -2
  11. package/dist/requests/WriteBundleRequest.js +168 -17
  12. package/lib/Atlaspack.js +47 -15
  13. package/lib/atlaspack-v3/AtlaspackV3.js +7 -4
  14. package/lib/atlaspack-v3/fs.js +1 -0
  15. package/lib/atlaspack-v3/worker/worker.js +13 -2
  16. package/lib/requests/AssetGraphRequestRust.js +5 -1
  17. package/lib/requests/AtlaspackBuildRequest.js +9 -0
  18. package/lib/requests/BundleGraphRequest.js +10 -7
  19. package/lib/requests/BundleGraphRequestRust.js +6 -1
  20. package/lib/requests/BundleGraphRequestUtils.js +132 -2
  21. package/lib/requests/WriteBundleRequest.js +181 -13
  22. package/lib/types/atlaspack-v3/AtlaspackV3.d.ts +3 -2
  23. package/lib/types/atlaspack-v3/fs.d.ts +1 -0
  24. package/lib/types/requests/BundleGraphRequestUtils.d.ts +7 -0
  25. package/lib/types/requests/WriteBundleRequest.d.ts +33 -0
  26. package/package.json +15 -15
  27. package/src/Atlaspack.ts +54 -12
  28. package/src/atlaspack-v3/AtlaspackV3.ts +22 -4
  29. package/src/atlaspack-v3/fs.ts +5 -0
  30. package/src/atlaspack-v3/worker/worker.ts +11 -2
  31. package/src/requests/AssetGraphRequestRust.ts +5 -1
  32. package/src/requests/AtlaspackBuildRequest.ts +4 -0
  33. package/src/requests/BundleGraphRequest.ts +11 -6
  34. package/src/requests/BundleGraphRequestRust.ts +8 -1
  35. package/src/requests/BundleGraphRequestUtils.ts +157 -1
  36. package/src/requests/WriteBundleRequest.ts +202 -22
  37. package/test/requests/WriteBundleRequest.test.ts +363 -0
  38. package/tsconfig.tsbuildinfo +1 -1
@@ -1,7 +1,10 @@
1
1
  import assert from 'assert';
2
+ import {Readable} from 'stream';
2
3
  import SourceMap from '@atlaspack/source-map';
3
4
  import {
4
5
  applyReplacementsToSourceMap,
6
+ applyReplacementsToVLQMappings,
7
+ SourceMapHashRefRewriteStream,
5
8
  type HashRefReplacement,
6
9
  } from '../../src/requests/WriteBundleRequest';
7
10
 
@@ -69,6 +72,35 @@ function buildCodeWithHashRefs(
69
72
  };
70
73
  }
71
74
 
75
+ /**
76
+ * Drains a Readable stream into a single Buffer.
77
+ */
78
+ function streamToBuffer(stream: Readable): Promise<Buffer> {
79
+ return new Promise((resolve, reject) => {
80
+ const chunks: Buffer[] = [];
81
+ stream.on('data', (chunk: Buffer) => chunks.push(chunk));
82
+ stream.on('end', () => resolve(Buffer.concat(chunks)));
83
+ stream.on('error', reject);
84
+ });
85
+ }
86
+
87
+ /**
88
+ * Cross-checks applyReplacementsToVLQMappings against the native
89
+ * applyReplacementsToSourceMap by building a SourceMap, running both
90
+ * implementations, and asserting identical VLQ output.
91
+ */
92
+ function crossCheck(sm: SourceMap, replacements: HashRefReplacement[]): void {
93
+ const vlqBefore = sm.toVLQ().mappings;
94
+ const vlqResult = applyReplacementsToVLQMappings(vlqBefore, replacements);
95
+ applyReplacementsToSourceMap(sm, replacements);
96
+ const nativeResult = sm.toVLQ().mappings;
97
+ assert.strictEqual(
98
+ vlqResult,
99
+ nativeResult,
100
+ `VLQ result differs from native:\n VLQ: ${vlqResult}\n native: ${nativeResult}`,
101
+ );
102
+ }
103
+
72
104
  describe('applyReplacementsToSourceMap', () => {
73
105
  describe('with correct replacement coordinates', () => {
74
106
  it('should correctly adjust a single HASH_REF replacement', () => {
@@ -237,3 +269,334 @@ describe('applyReplacementsToSourceMap', () => {
237
269
  });
238
270
  });
239
271
  });
272
+
273
+ describe('applyReplacementsToVLQMappings', () => {
274
+ it('returns the same string for empty replacements', () => {
275
+ const sm = new SourceMap('/');
276
+ sm.addIndexedMapping({
277
+ generated: {line: 1, column: 10},
278
+ original: {line: 1, column: 0},
279
+ source: 'test.js',
280
+ });
281
+ const vlq = sm.toVLQ().mappings;
282
+ assert.strictEqual(applyReplacementsToVLQMappings(vlq, []), vlq);
283
+ });
284
+
285
+ it('returns the same string for a zero-delta replacement', () => {
286
+ const sm = new SourceMap('/');
287
+ sm.addIndexedMapping({
288
+ generated: {line: 1, column: 30},
289
+ original: {line: 1, column: 0},
290
+ source: 'test.js',
291
+ });
292
+ const vlq = sm.toVLQ().mappings;
293
+ const repl: HashRefReplacement[] = [
294
+ {line: 0, column: 0, originalLength: 10, newLength: 10},
295
+ ];
296
+ assert.strictEqual(applyReplacementsToVLQMappings(vlq, repl), vlq);
297
+ });
298
+
299
+ it('single replacement agrees with native', () => {
300
+ const {correctReplacements, identifierPositions} = buildCodeWithHashRefs([
301
+ {type: 'hashref'},
302
+ {type: 'code', text: ';var x=SOME_IDENT;'},
303
+ ]);
304
+ const sm = new SourceMap('/');
305
+ sm.addIndexedMapping({
306
+ generated: {line: 1, column: identifierPositions.get('SOME_IDENT')!},
307
+ original: {line: 10, column: 5},
308
+ source: 'test.js',
309
+ });
310
+ crossCheck(sm, correctReplacements);
311
+ });
312
+
313
+ it('multiple replacements on the same line agree with native', () => {
314
+ const {correctReplacements, identifierPositions} = buildCodeWithHashRefs([
315
+ {type: 'hashref'},
316
+ {type: 'code', text: ';var a=IDENT_ALPHA;require("'},
317
+ {type: 'hashref'},
318
+ {type: 'code', text: '");var b=IDENT_BETA;require("'},
319
+ {type: 'hashref'},
320
+ {type: 'code', text: '");var c=IDENT_GAMMA;'},
321
+ ]);
322
+ const sm = new SourceMap('/');
323
+ for (const [name, origLine] of [
324
+ ['IDENT_ALPHA', 10],
325
+ ['IDENT_BETA', 20],
326
+ ['IDENT_GAMMA', 30],
327
+ ] as const) {
328
+ sm.addIndexedMapping({
329
+ generated: {line: 1, column: identifierPositions.get(name)!},
330
+ original: {line: origLine, column: 0},
331
+ source: 'test.js',
332
+ });
333
+ }
334
+ crossCheck(sm, correctReplacements);
335
+ });
336
+
337
+ it('10 replacements on the same line agree with native', () => {
338
+ const segments: Array<{type: 'code'; text: string} | {type: 'hashref'}> =
339
+ [];
340
+ for (let i = 0; i < 10; i++) {
341
+ segments.push({type: 'hashref'});
342
+ segments.push({
343
+ type: 'code',
344
+ text: `;var x${i}=TARGET_${String(i).padStart(2, '0')};require("`,
345
+ });
346
+ }
347
+ segments.push({type: 'code', text: '");'});
348
+
349
+ const {correctReplacements, identifierPositions} =
350
+ buildCodeWithHashRefs(segments);
351
+
352
+ const sm = new SourceMap('/');
353
+ for (let i = 0; i < 10; i++) {
354
+ const name = `TARGET_${String(i).padStart(2, '0')}`;
355
+ sm.addIndexedMapping({
356
+ generated: {line: 1, column: identifierPositions.get(name)!},
357
+ original: {line: (i + 1) * 10, column: 0},
358
+ source: 'test.js',
359
+ });
360
+ }
361
+ crossCheck(sm, correctReplacements);
362
+ });
363
+
364
+ it('mapping before the threshold is unaffected', () => {
365
+ const {correctReplacements, identifierPositions} = buildCodeWithHashRefs([
366
+ {type: 'code', text: 'var BEFORE_HASH=1;'},
367
+ {type: 'hashref'},
368
+ {type: 'code', text: ';var AFTER_HASH=2;'},
369
+ ]);
370
+ const sm = new SourceMap('/');
371
+ sm.addIndexedMapping({
372
+ generated: {line: 1, column: identifierPositions.get('BEFORE_HASH')!},
373
+ original: {line: 1, column: 0},
374
+ source: 'test.js',
375
+ });
376
+ sm.addIndexedMapping({
377
+ generated: {line: 1, column: identifierPositions.get('AFTER_HASH')!},
378
+ original: {line: 2, column: 0},
379
+ source: 'test.js',
380
+ });
381
+ crossCheck(sm, correctReplacements);
382
+ });
383
+
384
+ it('mapping on a different line is unaffected', () => {
385
+ const sm = new SourceMap('/');
386
+ // Line 0 (VLQ line index 0): hash ref at col 0, mapping at col 50
387
+ // Line 1 (VLQ line index 1): mapping at col 5 – should be untouched
388
+ sm.addIndexedMapping({
389
+ generated: {line: 1, column: 50},
390
+ original: {line: 10, column: 0},
391
+ source: 'test.js',
392
+ });
393
+ sm.addIndexedMapping({
394
+ generated: {line: 2, column: 5},
395
+ original: {line: 20, column: 0},
396
+ source: 'test.js',
397
+ });
398
+
399
+ const replacements: HashRefReplacement[] = [
400
+ {
401
+ line: 0,
402
+ column: 0,
403
+ originalLength: HASH_REF_LEN,
404
+ newLength: REPLACEMENT_LEN,
405
+ },
406
+ ];
407
+
408
+ const vlqBefore = sm.toVLQ().mappings;
409
+ const vlqResult = applyReplacementsToVLQMappings(vlqBefore, replacements);
410
+ applyReplacementsToSourceMap(sm, replacements);
411
+
412
+ // Verify the line-1 mapping is unchanged by checking parsed values
413
+ const mappings = sm.getMap().mappings;
414
+ const line2Mapping = mappings.find((m) => m.original?.line === 20);
415
+ assert.ok(line2Mapping, 'Line 2 mapping should exist');
416
+ assert.strictEqual(line2Mapping!.generated.column, 5);
417
+
418
+ // Also verify VLQ agrees with native
419
+ assert.strictEqual(vlqResult, sm.toVLQ().mappings);
420
+ });
421
+ });
422
+
423
+ describe('SourceMapHashRefRewriteStream', () => {
424
+ async function applyStream(
425
+ json: string,
426
+ replacements: HashRefReplacement[],
427
+ chunkSize?: number,
428
+ ): Promise<string> {
429
+ const inputBuf = Buffer.from(json, 'utf8');
430
+ let readable: Readable;
431
+ if (chunkSize != null) {
432
+ readable = new Readable({
433
+ read() {
434
+ let offset = 0;
435
+ while (offset < inputBuf.length) {
436
+ this.push(inputBuf.slice(offset, offset + chunkSize));
437
+ offset += chunkSize;
438
+ }
439
+ this.push(null);
440
+ },
441
+ });
442
+ } else {
443
+ readable = Readable.from([inputBuf]);
444
+ }
445
+ const outBuf = await streamToBuffer(
446
+ readable.pipe(new SourceMapHashRefRewriteStream(replacements)),
447
+ );
448
+ return outBuf.toString('utf8');
449
+ }
450
+
451
+ it('full round-trip: mappings field is correctly rewritten', async () => {
452
+ const {correctReplacements, identifierPositions} = buildCodeWithHashRefs([
453
+ {type: 'hashref'},
454
+ {type: 'code', text: ';var x=SOME_IDENT;'},
455
+ ]);
456
+
457
+ const sm = new SourceMap('/');
458
+ sm.addIndexedMapping({
459
+ generated: {line: 1, column: identifierPositions.get('SOME_IDENT')!},
460
+ original: {line: 10, column: 5},
461
+ source: 'test.js',
462
+ });
463
+
464
+ const vlqBefore = sm.toVLQ().mappings;
465
+ const expectedMappings = applyReplacementsToVLQMappings(
466
+ vlqBefore,
467
+ correctReplacements,
468
+ );
469
+
470
+ const mapJson = await sm.stringify({format: 'string'});
471
+ const outputJson = await applyStream(
472
+ mapJson as string,
473
+ correctReplacements,
474
+ );
475
+ const parsed = JSON.parse(outputJson);
476
+
477
+ assert.strictEqual(parsed.mappings, expectedMappings);
478
+ });
479
+
480
+ it('bytes after mappings (sourcesContent) pass through unchanged', async () => {
481
+ const sm = new SourceMap('/');
482
+ sm.addIndexedMapping({
483
+ generated: {line: 1, column: 30},
484
+ original: {line: 5, column: 0},
485
+ source: 'test.js',
486
+ });
487
+ sm.setSourceContent('test.js', 'const x = 1;\nconst y = 2;\n');
488
+
489
+ const replacements: HashRefReplacement[] = [
490
+ {
491
+ line: 0,
492
+ column: 0,
493
+ originalLength: HASH_REF_LEN,
494
+ newLength: REPLACEMENT_LEN,
495
+ },
496
+ ];
497
+
498
+ const mapJson = (await sm.stringify({format: 'string'})) as string;
499
+ const outputJson = await applyStream(mapJson, replacements);
500
+ const parsedInput = JSON.parse(mapJson);
501
+ const parsedOutput = JSON.parse(outputJson);
502
+
503
+ // sourcesContent must be byte-for-byte identical
504
+ assert.deepStrictEqual(
505
+ parsedOutput.sourcesContent,
506
+ parsedInput.sourcesContent,
507
+ );
508
+ assert.deepStrictEqual(parsedOutput.sources, parsedInput.sources);
509
+ assert.deepStrictEqual(parsedOutput.names, parsedInput.names);
510
+ });
511
+
512
+ it('handles chunk boundaries mid-key', async () => {
513
+ const {correctReplacements, identifierPositions} = buildCodeWithHashRefs([
514
+ {type: 'hashref'},
515
+ {type: 'code', text: ';var x=SOME_IDENT;'},
516
+ ]);
517
+
518
+ const sm = new SourceMap('/');
519
+ sm.addIndexedMapping({
520
+ generated: {line: 1, column: identifierPositions.get('SOME_IDENT')!},
521
+ original: {line: 10, column: 5},
522
+ source: 'test.js',
523
+ });
524
+
525
+ const vlqBefore = sm.toVLQ().mappings;
526
+ const expectedMappings = applyReplacementsToVLQMappings(
527
+ vlqBefore,
528
+ correctReplacements,
529
+ );
530
+
531
+ const mapJson = (await sm.stringify({format: 'string'})) as string;
532
+
533
+ // Test multiple chunk sizes to exercise boundary conditions.
534
+ for (const chunkSize of [1, 3, 7, 11, 13]) {
535
+ const outputJson = await applyStream(
536
+ mapJson,
537
+ correctReplacements,
538
+ chunkSize,
539
+ );
540
+ const parsed = JSON.parse(outputJson);
541
+ assert.strictEqual(
542
+ parsed.mappings,
543
+ expectedMappings,
544
+ `Chunk size ${chunkSize}: mappings mismatch`,
545
+ );
546
+ }
547
+ });
548
+
549
+ it('handles chunk boundaries mid-value', async () => {
550
+ // Use a source map with a longer mappings string to ensure the VLQ value
551
+ // spans multiple chunks for small chunk sizes.
552
+ const sm = new SourceMap('/');
553
+ for (let i = 0; i < 20; i++) {
554
+ sm.addIndexedMapping({
555
+ generated: {line: 1, column: i * 5},
556
+ original: {line: i + 1, column: 0},
557
+ source: 'test.js',
558
+ });
559
+ }
560
+
561
+ const replacements: HashRefReplacement[] = [
562
+ {
563
+ line: 0,
564
+ column: 10,
565
+ originalLength: HASH_REF_LEN,
566
+ newLength: REPLACEMENT_LEN,
567
+ },
568
+ ];
569
+
570
+ const vlqBefore = sm.toVLQ().mappings;
571
+ const expectedMappings = applyReplacementsToVLQMappings(
572
+ vlqBefore,
573
+ replacements,
574
+ );
575
+
576
+ const mapJson = (await sm.stringify({format: 'string'})) as string;
577
+
578
+ for (const chunkSize of [1, 5, 8]) {
579
+ const outputJson = await applyStream(mapJson, replacements, chunkSize);
580
+ const parsed = JSON.parse(outputJson);
581
+ assert.strictEqual(
582
+ parsed.mappings,
583
+ expectedMappings,
584
+ `Chunk size ${chunkSize}: mappings mismatch`,
585
+ );
586
+ }
587
+ });
588
+
589
+ it('no-op for empty replacements – output equals input', async () => {
590
+ const sm = new SourceMap('/');
591
+ sm.addIndexedMapping({
592
+ generated: {line: 1, column: 10},
593
+ original: {line: 1, column: 0},
594
+ source: 'test.js',
595
+ });
596
+
597
+ const mapJson = (await sm.stringify({format: 'string'})) as string;
598
+ const outputJson = await applyStream(mapJson, []);
599
+
600
+ assert.strictEqual(outputJson, mapJson);
601
+ });
602
+ });