@atlaspack/core 2.35.0 → 2.36.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # @atlaspack/core
2
2
 
3
+ ## 2.36.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1047](https://github.com/atlassian-labs/atlaspack/pull/1047) [`de388ff`](https://github.com/atlassian-labs/atlaspack/commit/de388ff76d39dece97ad475fcccdb6efb6283bfc) Thanks [@marcins](https://github.com/marcins)! - Change approach to source map offset for hashRefs - use a streaming approach to avoid loading large sourcemaps into memory.
8
+
9
+ ### Patch Changes
10
+
11
+ - [#1025](https://github.com/atlassian-labs/atlaspack/pull/1025) [`f7878b2`](https://github.com/atlassian-labs/atlaspack/commit/f7878b2f19a0a3bbd0e79d0b4a4e1479646043b7) Thanks [@mattcompiles](https://github.com/mattcompiles)! - Add native ideal graph bundling algorithm behind nativeBundling feature flag
12
+
13
+ - Updated dependencies [[`f7878b2`](https://github.com/atlassian-labs/atlaspack/commit/f7878b2f19a0a3bbd0e79d0b4a4e1479646043b7), [`22bb49c`](https://github.com/atlassian-labs/atlaspack/commit/22bb49c5708798d259f98c8b5c10850b2f4f5f1b), [`bddd21a`](https://github.com/atlassian-labs/atlaspack/commit/bddd21a5313974ca333c02b2da1c6f85d1afaaea), [`de388ff`](https://github.com/atlassian-labs/atlaspack/commit/de388ff76d39dece97ad475fcccdb6efb6283bfc)]:
14
+ - @atlaspack/rust@3.24.1
15
+ - @atlaspack/source-map@3.3.0
16
+ - @atlaspack/cache@3.2.51
17
+ - @atlaspack/fs@2.15.51
18
+ - @atlaspack/logger@2.14.48
19
+ - @atlaspack/utils@3.3.8
20
+ - @atlaspack/package-manager@2.14.56
21
+ - @atlaspack/profiler@2.15.17
22
+ - @atlaspack/workers@2.14.56
23
+ - @atlaspack/graph@3.6.18
24
+ - @atlaspack/plugin@2.14.56
25
+ - @atlaspack/types@2.15.46
26
+
3
27
  ## 2.35.0
4
28
 
5
29
  ### Minor Changes
@@ -263,14 +263,17 @@ class BundlerRunner {
263
263
  measurement = tracer.createMeasurement(plugin.name, 'bundling:bundle', measurementFilename);
264
264
  }
265
265
  // this the normal bundle workflow (bundle, optimizing, run-times, naming)
266
- await bundler.bundle({
267
- bundleGraph: mutableBundleGraph,
268
- config: this.configs.get(plugin.name)?.result,
269
- options: this.pluginOptions,
270
- logger,
271
- tracer,
266
+ await (0, logger_1.instrumentAsync)('bundle (V2)', async () => {
267
+ await bundler.bundle({
268
+ bundleGraph: mutableBundleGraph,
269
+ config: this.configs.get(plugin.name)?.result,
270
+ options: this.pluginOptions,
271
+ logger,
272
+ tracer,
273
+ });
272
274
  });
273
275
  measurement && measurement.end();
276
+ (0, BundleGraphRequestUtils_1.dumpBundleGraphSnapshot)(internalBundleGraph, 'js');
274
277
  if (this.pluginOptions.mode === 'production') {
275
278
  let optimizeMeasurement;
276
279
  try {
@@ -71,6 +71,7 @@ function createBundleGraphRequestRust(input) {
71
71
  }
72
72
  // Don’t reuse previous JS result yet; we just rebuild from scratch.
73
73
  let { bundleGraph, changedAssets } = (0, logger_1.instrument)('atlaspack_v3_getBundleGraph', () => getBundleGraph(serializedBundleGraph));
74
+ (0, BundleGraphRequestUtils_1.dumpBundleGraphSnapshot)(bundleGraph, 'rust');
74
75
  const runner = new NativeBundlerRunner({ api, options }, input.optionsRef);
75
76
  await runner.loadConfigs();
76
77
  // Name all bundles
@@ -37,15 +37,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.validateBundles = validateBundles;
40
+ exports.dumpBundleGraphSnapshot = dumpBundleGraphSnapshot;
40
41
  exports.nameBundle = nameBundle;
41
42
  exports.loadPluginConfigWithDevDeps = loadPluginConfigWithDevDeps;
42
43
  exports.runDevDepRequest = runDevDepRequest;
43
44
  const assert_1 = __importDefault(require("assert"));
45
+ const fs_1 = __importDefault(require("fs"));
44
46
  const nullthrows_1 = __importDefault(require("nullthrows"));
47
+ const path_1 = __importDefault(require("path"));
45
48
  const logger_1 = require("@atlaspack/logger");
46
49
  const diagnostic_1 = __importStar(require("@atlaspack/diagnostic"));
47
50
  const utils_1 = require("@atlaspack/utils");
48
- const BundleGraph_1 = __importDefault(require("../public/BundleGraph"));
51
+ const BundleGraph_1 = require("../BundleGraph");
52
+ const BundleGraph_2 = __importDefault(require("../public/BundleGraph"));
49
53
  const Bundle_1 = require("../public/Bundle");
50
54
  const InternalConfig_1 = require("../InternalConfig");
51
55
  const DevDepRequest_1 = require("./DevDepRequest");
@@ -64,12 +68,139 @@ function validateBundles(bundleGraph) {
64
68
  ...(0, utils_1.setSymmetricDifference)(new Set(bundleNames), new Set((0, utils_1.unique)(bundleNames))),
65
69
  ].join());
66
70
  }
71
+ /**
72
+ * Dump a canonical JSON snapshot of the bundle graph for parity comparison.
73
+ * Gated by ATLASPACK_DUMP_BUNDLE_GRAPH environment variable which specifies the output directory.
74
+ * The snapshot captures bundle identity, type, contained assets, and bundle group structure
75
+ * in a deterministic, sorted format suitable for diffing.
76
+ */
77
+ function dumpBundleGraphSnapshot(bundleGraph, variant) {
78
+ let outDir = process.env.ATLASPACK_DUMP_BUNDLE_GRAPH;
79
+ if (!outDir)
80
+ return;
81
+ let filename = variant === 'js' ? 'bundle-graph-js.json' : 'bundle-graph-rust.json';
82
+ let outPath = path_1.default.join(outDir, filename);
83
+ fs_1.default.mkdirSync(outDir, { recursive: true });
84
+ let bundles = bundleGraph.getBundles();
85
+ let bundlesSnapshot = bundles
86
+ .map((bundle) => {
87
+ let bundleNodeId = bundleGraph._graph.getNodeIdByContentKey(bundle.id);
88
+ let containedAssetNodeIds = bundleGraph._graph.getNodeIdsConnectedFrom(bundleNodeId, BundleGraph_1.bundleGraphEdgeTypes.contains);
89
+ let containedAssets = containedAssetNodeIds
90
+ .map((nodeId) => bundleGraph._graph.getNode(nodeId))
91
+ .flatMap((node) => {
92
+ if (node?.type !== 'asset')
93
+ return [];
94
+ return [
95
+ {
96
+ id: node.value.id,
97
+ filePath: (0, projectPath_1.fromProjectPathRelative)(node.value.filePath),
98
+ },
99
+ ];
100
+ })
101
+ .sort((a, b) => a.filePath.localeCompare(b.filePath));
102
+ // Resolve mainEntry and entry asset file paths
103
+ let mainEntryPath = null;
104
+ let entryAssetPaths = [];
105
+ if (bundle.mainEntryId) {
106
+ let mainEntryNodeId = bundleGraph._graph.getNodeIdByContentKey(bundle.mainEntryId);
107
+ let mainEntryNode = bundleGraph._graph.getNode(mainEntryNodeId);
108
+ if (mainEntryNode?.type === 'asset') {
109
+ mainEntryPath = (0, projectPath_1.fromProjectPathRelative)(mainEntryNode.value.filePath);
110
+ }
111
+ }
112
+ for (let entryId of bundle.entryAssetIds) {
113
+ let entryNodeId = bundleGraph._graph.getNodeIdByContentKey(entryId);
114
+ let entryNode = bundleGraph._graph.getNode(entryNodeId);
115
+ if (entryNode?.type === 'asset') {
116
+ entryAssetPaths.push((0, projectPath_1.fromProjectPathRelative)(entryNode.value.filePath));
117
+ }
118
+ }
119
+ entryAssetPaths.sort();
120
+ return {
121
+ id: bundle.id,
122
+ type: bundle.type,
123
+ bundleBehavior: bundle.bundleBehavior ?? null,
124
+ needsStableName: bundle.needsStableName,
125
+ isSplittable: bundle.isSplittable,
126
+ isPlaceholder: bundle.isPlaceholder,
127
+ mainEntryPath,
128
+ entryAssetPaths,
129
+ assets: containedAssets.map((a) => a.filePath),
130
+ };
131
+ })
132
+ .sort((a, b) => {
133
+ // Sort by mainEntryPath first, then by sorted assets as tiebreaker
134
+ let aKey = a.mainEntryPath || a.assets.join(',');
135
+ let bKey = b.mainEntryPath || b.assets.join(',');
136
+ return aKey.localeCompare(bKey);
137
+ });
138
+ let bundleGroupsSnapshot = bundleGraph._graph.nodes
139
+ .flatMap((node) => {
140
+ if (node?.type !== 'bundle_group')
141
+ return [];
142
+ let bundleGroup = node.value;
143
+ // Resolve entry asset file path
144
+ let entryAssetPath = null;
145
+ try {
146
+ let entryNodeId = bundleGraph._graph.getNodeIdByContentKey(bundleGroup.entryAssetId);
147
+ let entryNode = bundleGraph._graph.getNode(entryNodeId);
148
+ if (entryNode?.type === 'asset') {
149
+ entryAssetPath = (0, projectPath_1.fromProjectPathRelative)(entryNode.value.filePath);
150
+ }
151
+ }
152
+ catch {
153
+ // Content key not found
154
+ }
155
+ let bundlesInGroup = bundleGraph.getBundlesInBundleGroup(bundleGroup);
156
+ let bundlePaths = bundlesInGroup
157
+ .map((b) => {
158
+ // Use mainEntry file path if available, otherwise bundle id as fallback
159
+ if (b.mainEntryId) {
160
+ try {
161
+ let nodeId = bundleGraph._graph.getNodeIdByContentKey(b.mainEntryId);
162
+ let node = bundleGraph._graph.getNode(nodeId);
163
+ if (node?.type === 'asset') {
164
+ return (0, projectPath_1.fromProjectPathRelative)(node.value.filePath);
165
+ }
166
+ }
167
+ catch {
168
+ // fallback
169
+ }
170
+ }
171
+ return `[bundle:${b.id}]`;
172
+ })
173
+ .sort();
174
+ return [
175
+ {
176
+ entryAssetPath: entryAssetPath ?? `[unknown:${bundleGroup.entryAssetId}]`,
177
+ bundlePaths,
178
+ },
179
+ ];
180
+ })
181
+ .sort((a, b) => a.entryAssetPath.localeCompare(b.entryAssetPath));
182
+ let totalAssets = bundleGraph._graph.nodes.filter((node) => node?.type === 'asset').length;
183
+ let snapshot = {
184
+ version: 1,
185
+ variant,
186
+ stats: {
187
+ totalBundles: bundlesSnapshot.length,
188
+ totalBundleGroups: bundleGroupsSnapshot.length,
189
+ totalAssets,
190
+ },
191
+ bundles: bundlesSnapshot,
192
+ bundleGroups: bundleGroupsSnapshot,
193
+ };
194
+ fs_1.default.writeFileSync(outPath, JSON.stringify(snapshot, null, 2), 'utf8');
195
+ // eslint-disable-next-line no-console
196
+ console.log(`[BundleGraphSnapshot] Wrote ${variant} snapshot to ${outPath}`);
197
+ }
67
198
  /**
68
199
  * Names a bundle by running through the configured namers until one returns a name.
69
200
  */
70
201
  async function nameBundle(namers, internalBundle, internalBundleGraph, options, pluginOptions, configs) {
71
202
  const bundle = Bundle_1.Bundle.get(internalBundle, internalBundleGraph, options);
72
- const bundleGraph = new BundleGraph_1.default(internalBundleGraph, Bundle_1.NamedBundle.get.bind(Bundle_1.NamedBundle), options);
203
+ const bundleGraph = new BundleGraph_2.default(internalBundleGraph, Bundle_1.NamedBundle.get.bind(Bundle_1.NamedBundle), options);
73
204
  for (const namer of namers) {
74
205
  let measurement;
75
206
  try {
@@ -36,8 +36,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.SourceMapHashRefRewriteStream = void 0;
39
40
  exports.default = createWriteBundleRequest;
40
41
  exports.applyReplacementsToSourceMap = applyReplacementsToSourceMap;
42
+ exports.applyReplacementsToVLQMappings = applyReplacementsToVLQMappings;
41
43
  exports.computeSourceMapRoot = computeSourceMapRoot;
42
44
  const constants_1 = require("../constants");
43
45
  const nullthrows_1 = __importDefault(require("nullthrows"));
@@ -56,10 +58,12 @@ const profiler_1 = require("@atlaspack/profiler");
56
58
  const RequestTracker_1 = require("../RequestTracker");
57
59
  const feature_flags_1 = require("@atlaspack/feature-flags");
58
60
  const EnvironmentManager_1 = require("../EnvironmentManager");
59
- const source_map_1 = __importDefault(require("@atlaspack/source-map"));
61
+ const source_map_1 = require("@atlaspack/source-map");
60
62
  const HASH_REF_PREFIX_LEN = constants_1.HASH_REF_PREFIX.length;
61
63
  const BOUNDARY_LENGTH = constants_1.HASH_REF_PREFIX.length + 32 - 1;
62
64
  const HASH_REF_PLACEHOLDER_LEN = HASH_REF_PREFIX_LEN + constants_1.HASH_REF_HASH_LEN;
65
+ // The JSON key prefix we scan for in the source map stream.
66
+ const MAPPINGS_KEY_BUF = Buffer.from('"mappings":"');
63
67
  /**
64
68
  * Writes a bundle to the dist directory, replacing hash references with the final content hashes.
65
69
  */
@@ -136,28 +140,14 @@ async function run({ input, options, api }) {
136
140
  await writeFiles(contentStream, info, hashRefToNameHash, options, config, outputFS, filePath, writeOptions, devDeps, api, bundleReplacements);
137
141
  const hasSourceMap = await options.cache.has(mapKey);
138
142
  if (mapKey && env.sourceMap && !env.sourceMap.inline && hasSourceMap) {
143
+ const mapEntry = await options.cache.getBlob(mapKey);
139
144
  let mapStream;
140
145
  if ((0, feature_flags_1.getFeatureFlag)('fixSourceMapHashRefs') &&
141
146
  bundleReplacements &&
142
147
  bundleReplacements.length > 0) {
143
- const mapEntry = await options.cache.getBlob(mapKey);
144
- const mapBuffer = Buffer.isBuffer(mapEntry)
145
- ? mapEntry
146
- : Buffer.from(mapEntry);
147
- const projectRoot = typeof options.projectRoot === 'string'
148
- ? options.projectRoot
149
- : String(options.projectRoot);
150
- const sourceMap = new source_map_1.default(projectRoot, mapBuffer);
151
- applyReplacementsToSourceMap(sourceMap, bundleReplacements);
152
- const mapJson = await sourceMap.stringify({
153
- format: 'string',
154
- file: name,
155
- sourceRoot: computeSourceMapRoot(bundle, options),
156
- });
157
- mapStream = (0, utils_1.blobToStream)(Buffer.from(typeof mapJson === 'string' ? mapJson : JSON.stringify(mapJson), 'utf8'));
148
+ mapStream = (0, utils_1.blobToStream)(mapEntry).pipe(new SourceMapHashRefRewriteStream(bundleReplacements));
158
149
  }
159
150
  else {
160
- const mapEntry = await options.cache.getBlob(mapKey);
161
151
  mapStream = (0, utils_1.blobToStream)(mapEntry);
162
152
  }
163
153
  await writeFiles(mapStream, info, hashRefToNameHash, options, config, outputFS, (0, projectPath_1.toProjectPathUnsafe)((0, projectPath_1.fromProjectPathRelative)(filePath) + '.map'), writeOptions, devDeps, api);
@@ -192,6 +182,167 @@ function applyReplacementsToSourceMap(sourceMap, replacements) {
192
182
  }
193
183
  }
194
184
  }
185
+ /**
186
+ * Applies hash-ref replacement column offsets directly to a VLQ mappings
187
+ * string without deserializing the full source map into a native struct.
188
+ *
189
+ * Each replacement r describes a hash-ref that was substituted in the output
190
+ * file. r.column is in the progressively-shifted post-replacement coordinate
191
+ * space (matching the already-shifted source map state after all previous
192
+ * offsetColumns calls), so thresholds are applied sequentially against the
193
+ * running absCol values exactly as the native offsetColumns implementation does.
194
+ */
195
+ function applyReplacementsToVLQMappings(mappings, replacements) {
196
+ if (replacements.length === 0)
197
+ return mappings;
198
+ // Group replacements by line (0-indexed), sorted by column ascending.
199
+ const byLine = new Map();
200
+ for (const r of replacements) {
201
+ let arr = byLine.get(r.line);
202
+ if (!arr) {
203
+ arr = [];
204
+ byLine.set(r.line, arr);
205
+ }
206
+ arr.push(r);
207
+ }
208
+ for (const arr of byLine.values()) {
209
+ arr.sort((a, b) => a.column - b.column);
210
+ }
211
+ const lines = mappings.split(';');
212
+ const resultLines = [];
213
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
214
+ const lineReps = byLine.get(lineIdx);
215
+ if (!lineReps || lineReps.length === 0) {
216
+ resultLines.push(lines[lineIdx]);
217
+ continue;
218
+ }
219
+ const line = lines[lineIdx];
220
+ if (!line) {
221
+ resultLines.push('');
222
+ continue;
223
+ }
224
+ // Decode segment column deltas to absolute columns.
225
+ const segments = line.split(',');
226
+ const colVlqEnds = [];
227
+ const absCols = [];
228
+ let absCol = 0;
229
+ for (const seg of segments) {
230
+ const { value: colDelta, nextPos } = (0, source_map_1.decodeVLQ)(seg, 0);
231
+ absCol += colDelta;
232
+ colVlqEnds.push(nextPos);
233
+ absCols.push(absCol);
234
+ }
235
+ // Apply each replacement's column shift sequentially against the
236
+ // current absCol values (which have already been adjusted by previous
237
+ // replacements on this line), mirroring the sequential offsetColumns calls.
238
+ for (const r of lineReps) {
239
+ const delta = r.newLength - r.originalLength;
240
+ if (delta === 0)
241
+ continue;
242
+ const threshold = r.column + r.originalLength;
243
+ for (let i = 0; i < absCols.length; i++) {
244
+ if (absCols[i] >= threshold) {
245
+ absCols[i] += delta;
246
+ }
247
+ }
248
+ }
249
+ // Re-encode with updated absolute columns; only the leading column VLQ
250
+ // field of each segment changes – the tail bytes are sliced unchanged.
251
+ const resultSegments = [];
252
+ let prevAbsCol = 0;
253
+ for (let i = 0; i < segments.length; i++) {
254
+ const newDelta = absCols[i] - prevAbsCol;
255
+ prevAbsCol = absCols[i];
256
+ resultSegments.push((0, source_map_1.encodeVLQ)(newDelta) + segments[i].slice(colVlqEnds[i]));
257
+ }
258
+ resultLines.push(resultSegments.join(','));
259
+ }
260
+ return resultLines.join(';');
261
+ }
262
+ /**
263
+ * A Transform stream that rewrites the "mappings" VLQ field of a source map
264
+ * JSON to account for hash-ref replacements, without ever loading the full
265
+ * JSON object or the native Rust SourceMapInner into memory.
266
+ *
267
+ * Field order in cached source maps (from partialVlqMapToSourceMap / toVLQ):
268
+ * mappings → sources → sourcesContent → names → version → file → sourceRoot
269
+ *
270
+ * "mappings" is the very first field, so we scan only a tiny header before
271
+ * switching to zero-copy passthrough for the bulk sourcesContent bytes.
272
+ */
273
+ class SourceMapHashRefRewriteStream extends stream_1.Transform {
274
+ constructor(replacements) {
275
+ super();
276
+ this.replacements = replacements;
277
+ this.state = 'scanning';
278
+ this.scanBuf = Buffer.alloc(0);
279
+ this.mappingsBufs = [];
280
+ }
281
+ // @ts-expect-error TS7006
282
+ _transform(chunk, _encoding, cb) {
283
+ if (this.state === 'passthrough') {
284
+ this.push(chunk);
285
+ cb();
286
+ return;
287
+ }
288
+ if (this.state === 'scanning') {
289
+ const combined = Buffer.concat([this.scanBuf, chunk]);
290
+ const idx = combined.indexOf(MAPPINGS_KEY_BUF);
291
+ if (idx === -1) {
292
+ // Key not yet found – hold back enough bytes to handle a split key.
293
+ const keepLen = Math.min(combined.length, MAPPINGS_KEY_BUF.length - 1);
294
+ if (combined.length > keepLen) {
295
+ this.push(combined.slice(0, combined.length - keepLen));
296
+ }
297
+ this.scanBuf = combined.slice(combined.length - keepLen);
298
+ cb();
299
+ return;
300
+ }
301
+ // Emit everything up to and including the key.
302
+ const keyEnd = idx + MAPPINGS_KEY_BUF.length;
303
+ this.push(combined.slice(0, keyEnd));
304
+ this.scanBuf = Buffer.alloc(0);
305
+ this.state = 'buffering';
306
+ this._bufferingTransform(combined.slice(keyEnd), cb);
307
+ return;
308
+ }
309
+ // state === 'buffering'
310
+ this._bufferingTransform(chunk, cb);
311
+ }
312
+ // @ts-expect-error TS7006
313
+ _bufferingTransform(chunk, cb) {
314
+ // Mappings values contain only base64 chars, ';', and ',' – no escaping –
315
+ // so scanning for the closing '"' (0x22) is safe.
316
+ const closeIdx = chunk.indexOf(0x22);
317
+ if (closeIdx === -1) {
318
+ this.mappingsBufs.push(chunk);
319
+ cb();
320
+ return;
321
+ }
322
+ this.mappingsBufs.push(chunk.slice(0, closeIdx));
323
+ // VLQ chars are all ASCII (<128), so latin1 round-trips without loss.
324
+ const mappingsStr = Buffer.concat(this.mappingsBufs).toString('latin1');
325
+ const rewritten = applyReplacementsToVLQMappings(mappingsStr, this.replacements);
326
+ this.push(Buffer.from(rewritten, 'latin1'));
327
+ // Emit the closing '"' and everything remaining in one push.
328
+ this.push(chunk.slice(closeIdx));
329
+ this.state = 'passthrough';
330
+ this.mappingsBufs = [];
331
+ cb();
332
+ }
333
+ // @ts-expect-error TS7006
334
+ _flush(cb) {
335
+ if (this.state === 'scanning' && this.scanBuf.length > 0) {
336
+ this.push(this.scanBuf);
337
+ }
338
+ else if (this.state === 'buffering') {
339
+ // Malformed JSON – flush whatever we buffered as-is.
340
+ this.push(Buffer.concat(this.mappingsBufs));
341
+ }
342
+ cb();
343
+ }
344
+ }
345
+ exports.SourceMapHashRefRewriteStream = SourceMapHashRefRewriteStream;
195
346
  /**
196
347
  * Computes the sourceRoot for a source map file. This is the relative path from
197
348
  * the output directory back to the project root, so that source paths (stored
@@ -293,7 +293,6 @@ class BundlerRunner {
293
293
  }
294
294
  didIncrementallyBundle = true;
295
295
  } else {
296
- var _this$configs$get;
297
296
  internalBundleGraph = _BundleGraph.default.fromAssetGraph(graph, this.options.mode === 'production');
298
297
  (0, _assert().default)(internalBundleGraph != null); // ensures the graph was created
299
298
 
@@ -309,14 +308,18 @@ class BundlerRunner {
309
308
  }
310
309
 
311
310
  // this the normal bundle workflow (bundle, optimizing, run-times, naming)
312
- await bundler.bundle({
313
- bundleGraph: mutableBundleGraph,
314
- config: (_this$configs$get = this.configs.get(plugin.name)) === null || _this$configs$get === void 0 ? void 0 : _this$configs$get.result,
315
- options: this.pluginOptions,
316
- logger,
317
- tracer
311
+ await (0, _logger().instrumentAsync)('bundle (V2)', async () => {
312
+ var _this$configs$get;
313
+ await bundler.bundle({
314
+ bundleGraph: mutableBundleGraph,
315
+ config: (_this$configs$get = this.configs.get(plugin.name)) === null || _this$configs$get === void 0 ? void 0 : _this$configs$get.result,
316
+ options: this.pluginOptions,
317
+ logger,
318
+ tracer
319
+ });
318
320
  });
319
321
  measurement && measurement.end();
322
+ (0, _BundleGraphRequestUtils.dumpBundleGraphSnapshot)(internalBundleGraph, 'js');
320
323
  if (this.pluginOptions.mode === 'production') {
321
324
  let optimizeMeasurement;
322
325
  try {
@@ -96,6 +96,7 @@ function createBundleGraphRequestRust(input) {
96
96
  bundleGraph,
97
97
  changedAssets
98
98
  } = (0, _logger().instrument)('atlaspack_v3_getBundleGraph', () => getBundleGraph(serializedBundleGraph));
99
+ (0, _BundleGraphRequestUtils.dumpBundleGraphSnapshot)(bundleGraph, 'rust');
99
100
  const runner = new NativeBundlerRunner({
100
101
  api,
101
102
  options
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.dumpBundleGraphSnapshot = dumpBundleGraphSnapshot;
6
7
  exports.loadPluginConfigWithDevDeps = loadPluginConfigWithDevDeps;
7
8
  exports.nameBundle = nameBundle;
8
9
  exports.runDevDepRequest = runDevDepRequest;
@@ -14,6 +15,13 @@ function _assert() {
14
15
  };
15
16
  return data;
16
17
  }
18
+ function _fs() {
19
+ const data = _interopRequireDefault(require("fs"));
20
+ _fs = function () {
21
+ return data;
22
+ };
23
+ return data;
24
+ }
17
25
  function _nullthrows() {
18
26
  const data = _interopRequireDefault(require("nullthrows"));
19
27
  _nullthrows = function () {
@@ -21,6 +29,13 @@ function _nullthrows() {
21
29
  };
22
30
  return data;
23
31
  }
32
+ function _path() {
33
+ const data = _interopRequireDefault(require("path"));
34
+ _path = function () {
35
+ return data;
36
+ };
37
+ return data;
38
+ }
24
39
  function _logger() {
25
40
  const data = require("@atlaspack/logger");
26
41
  _logger = function () {
@@ -42,7 +57,8 @@ function _utils() {
42
57
  };
43
58
  return data;
44
59
  }
45
- var _BundleGraph = _interopRequireDefault(require("../public/BundleGraph"));
60
+ var _BundleGraph = require("../BundleGraph");
61
+ var _BundleGraph2 = _interopRequireDefault(require("../public/BundleGraph"));
46
62
  var _Bundle = require("../public/Bundle");
47
63
  var _InternalConfig = require("../InternalConfig");
48
64
  var _DevDepRequest = require("./DevDepRequest");
@@ -75,12 +91,126 @@ function validateBundles(bundleGraph) {
75
91
  _assert().default.deepEqual(bundleNames, (0, _utils().unique)(bundleNames), 'Bundles must have unique name. Conflicting names: ' + [...(0, _utils().setSymmetricDifference)(new Set(bundleNames), new Set((0, _utils().unique)(bundleNames)))].join());
76
92
  }
77
93
 
94
+ /**
95
+ * Dump a canonical JSON snapshot of the bundle graph for parity comparison.
96
+ * Gated by ATLASPACK_DUMP_BUNDLE_GRAPH environment variable which specifies the output directory.
97
+ * The snapshot captures bundle identity, type, contained assets, and bundle group structure
98
+ * in a deterministic, sorted format suitable for diffing.
99
+ */
100
+ function dumpBundleGraphSnapshot(bundleGraph, variant) {
101
+ let outDir = process.env.ATLASPACK_DUMP_BUNDLE_GRAPH;
102
+ if (!outDir) return;
103
+ let filename = variant === 'js' ? 'bundle-graph-js.json' : 'bundle-graph-rust.json';
104
+ let outPath = _path().default.join(outDir, filename);
105
+ _fs().default.mkdirSync(outDir, {
106
+ recursive: true
107
+ });
108
+ let bundles = bundleGraph.getBundles();
109
+ let bundlesSnapshot = bundles.map(bundle => {
110
+ let bundleNodeId = bundleGraph._graph.getNodeIdByContentKey(bundle.id);
111
+ let containedAssetNodeIds = bundleGraph._graph.getNodeIdsConnectedFrom(bundleNodeId, _BundleGraph.bundleGraphEdgeTypes.contains);
112
+ let containedAssets = containedAssetNodeIds.map(nodeId => bundleGraph._graph.getNode(nodeId)).flatMap(node => {
113
+ if ((node === null || node === void 0 ? void 0 : node.type) !== 'asset') return [];
114
+ return [{
115
+ id: node.value.id,
116
+ filePath: (0, _projectPath.fromProjectPathRelative)(node.value.filePath)
117
+ }];
118
+ }).sort((a, b) => a.filePath.localeCompare(b.filePath));
119
+
120
+ // Resolve mainEntry and entry asset file paths
121
+ let mainEntryPath = null;
122
+ let entryAssetPaths = [];
123
+ if (bundle.mainEntryId) {
124
+ let mainEntryNodeId = bundleGraph._graph.getNodeIdByContentKey(bundle.mainEntryId);
125
+ let mainEntryNode = bundleGraph._graph.getNode(mainEntryNodeId);
126
+ if ((mainEntryNode === null || mainEntryNode === void 0 ? void 0 : mainEntryNode.type) === 'asset') {
127
+ mainEntryPath = (0, _projectPath.fromProjectPathRelative)(mainEntryNode.value.filePath);
128
+ }
129
+ }
130
+ for (let entryId of bundle.entryAssetIds) {
131
+ let entryNodeId = bundleGraph._graph.getNodeIdByContentKey(entryId);
132
+ let entryNode = bundleGraph._graph.getNode(entryNodeId);
133
+ if ((entryNode === null || entryNode === void 0 ? void 0 : entryNode.type) === 'asset') {
134
+ entryAssetPaths.push((0, _projectPath.fromProjectPathRelative)(entryNode.value.filePath));
135
+ }
136
+ }
137
+ entryAssetPaths.sort();
138
+ return {
139
+ id: bundle.id,
140
+ type: bundle.type,
141
+ bundleBehavior: bundle.bundleBehavior ?? null,
142
+ needsStableName: bundle.needsStableName,
143
+ isSplittable: bundle.isSplittable,
144
+ isPlaceholder: bundle.isPlaceholder,
145
+ mainEntryPath,
146
+ entryAssetPaths,
147
+ assets: containedAssets.map(a => a.filePath)
148
+ };
149
+ }).sort((a, b) => {
150
+ // Sort by mainEntryPath first, then by sorted assets as tiebreaker
151
+ let aKey = a.mainEntryPath || a.assets.join(',');
152
+ let bKey = b.mainEntryPath || b.assets.join(',');
153
+ return aKey.localeCompare(bKey);
154
+ });
155
+ let bundleGroupsSnapshot = bundleGraph._graph.nodes.flatMap(node => {
156
+ if ((node === null || node === void 0 ? void 0 : node.type) !== 'bundle_group') return [];
157
+ let bundleGroup = node.value;
158
+
159
+ // Resolve entry asset file path
160
+ let entryAssetPath = null;
161
+ try {
162
+ let entryNodeId = bundleGraph._graph.getNodeIdByContentKey(bundleGroup.entryAssetId);
163
+ let entryNode = bundleGraph._graph.getNode(entryNodeId);
164
+ if ((entryNode === null || entryNode === void 0 ? void 0 : entryNode.type) === 'asset') {
165
+ entryAssetPath = (0, _projectPath.fromProjectPathRelative)(entryNode.value.filePath);
166
+ }
167
+ } catch {
168
+ // Content key not found
169
+ }
170
+ let bundlesInGroup = bundleGraph.getBundlesInBundleGroup(bundleGroup);
171
+ let bundlePaths = bundlesInGroup.map(b => {
172
+ // Use mainEntry file path if available, otherwise bundle id as fallback
173
+ if (b.mainEntryId) {
174
+ try {
175
+ let nodeId = bundleGraph._graph.getNodeIdByContentKey(b.mainEntryId);
176
+ let node = bundleGraph._graph.getNode(nodeId);
177
+ if ((node === null || node === void 0 ? void 0 : node.type) === 'asset') {
178
+ return (0, _projectPath.fromProjectPathRelative)(node.value.filePath);
179
+ }
180
+ } catch {
181
+ // fallback
182
+ }
183
+ }
184
+ return `[bundle:${b.id}]`;
185
+ }).sort();
186
+ return [{
187
+ entryAssetPath: entryAssetPath ?? `[unknown:${bundleGroup.entryAssetId}]`,
188
+ bundlePaths
189
+ }];
190
+ }).sort((a, b) => a.entryAssetPath.localeCompare(b.entryAssetPath));
191
+ let totalAssets = bundleGraph._graph.nodes.filter(node => (node === null || node === void 0 ? void 0 : node.type) === 'asset').length;
192
+ let snapshot = {
193
+ version: 1,
194
+ variant,
195
+ stats: {
196
+ totalBundles: bundlesSnapshot.length,
197
+ totalBundleGroups: bundleGroupsSnapshot.length,
198
+ totalAssets
199
+ },
200
+ bundles: bundlesSnapshot,
201
+ bundleGroups: bundleGroupsSnapshot
202
+ };
203
+ _fs().default.writeFileSync(outPath, JSON.stringify(snapshot, null, 2), 'utf8');
204
+ // eslint-disable-next-line no-console
205
+ console.log(`[BundleGraphSnapshot] Wrote ${variant} snapshot to ${outPath}`);
206
+ }
207
+
78
208
  /**
79
209
  * Names a bundle by running through the configured namers until one returns a name.
80
210
  */
81
211
  async function nameBundle(namers, internalBundle, internalBundleGraph, options, pluginOptions, configs) {
82
212
  const bundle = _Bundle.Bundle.get(internalBundle, internalBundleGraph, options);
83
- const bundleGraph = new _BundleGraph.default(internalBundleGraph, _Bundle.NamedBundle.get.bind(_Bundle.NamedBundle), options);
213
+ const bundleGraph = new _BundleGraph2.default(internalBundleGraph, _Bundle.NamedBundle.get.bind(_Bundle.NamedBundle), options);
84
214
  for (const namer of namers) {
85
215
  let measurement;
86
216
  try {