@atlaspack/core 2.12.1-canary.3431 → 2.12.1-canary.3434

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/lib/Atlaspack.js CHANGED
@@ -151,6 +151,11 @@ class Atlaspack {
151
151
  if (this.#initialized) {
152
152
  return;
153
153
  }
154
+ const featureFlags = {
155
+ ..._featureFlags().DEFAULT_FEATURE_FLAGS,
156
+ ...this.#initialOptions.featureFlags
157
+ };
158
+ (0, _featureFlags().setFeatureFlags)(featureFlags);
154
159
  await _sourceMap().init;
155
160
  await (_rust().init === null || _rust().init === void 0 ? void 0 : (0, _rust().init)());
156
161
  try {
@@ -162,7 +167,10 @@ class Atlaspack {
162
167
  // Fallthrough
163
168
  _logger().default.warn(e);
164
169
  }
165
- let resolvedOptions = await (0, _resolveOptions.default)(this.#initialOptions);
170
+ let resolvedOptions = await (0, _resolveOptions.default)({
171
+ ...this.#initialOptions,
172
+ featureFlags
173
+ });
166
174
  this.#resolvedOptions = resolvedOptions;
167
175
  let rustAtlaspack;
168
176
  if (resolvedOptions.featureFlags.atlaspackV3) {
@@ -192,7 +200,6 @@ class Atlaspack {
192
200
  }
193
201
  });
194
202
  }
195
- (0, _featureFlags().setFeatureFlags)(resolvedOptions.featureFlags);
196
203
  let {
197
204
  config
198
205
  } = await (0, _AtlaspackConfigRequest.loadAtlaspackConfig)(resolvedOptions);
@@ -520,6 +527,9 @@ class Atlaspack {
520
527
  });
521
528
  return this.#farm.takeHeapSnapshot();
522
529
  }
530
+ async unstable_invalidate() {
531
+ await this._init();
532
+ }
523
533
  async unstable_transform(options) {
524
534
  var _options$env;
525
535
  if (!this.#initialized) {
@@ -44,6 +44,13 @@ var _utils2 = require("./utils");
44
44
  var _Environment = require("./public/Environment");
45
45
  var _projectPath = require("./projectPath");
46
46
  var _constants = require("./constants");
47
+ function _featureFlags() {
48
+ const data = require("@atlaspack/feature-flags");
49
+ _featureFlags = function () {
50
+ return data;
51
+ };
52
+ return data;
53
+ }
47
54
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
48
55
  const bundleGraphEdgeTypes = exports.bundleGraphEdgeTypes = {
49
56
  // A lack of an edge type indicates to follow the edge while traversing
@@ -107,16 +114,19 @@ class BundleGraph {
107
114
  /** The internal core Graph structure */
108
115
 
109
116
  _bundlePublicIds /*: Set<string> */ = new Set();
117
+ _conditions /*: Map<string, Condition> */ = new Map();
110
118
  constructor({
111
119
  graph,
112
120
  publicIdByAssetId,
113
121
  assetPublicIds,
114
- bundleContentHashes
122
+ bundleContentHashes,
123
+ conditions
115
124
  }) {
116
125
  this._graph = graph;
117
126
  this._assetPublicIds = assetPublicIds;
118
127
  this._publicIdByAssetId = publicIdByAssetId;
119
128
  this._bundleContentHashes = bundleContentHashes;
129
+ this._conditions = conditions;
120
130
  }
121
131
 
122
132
  /**
@@ -128,6 +138,8 @@ class BundleGraph {
128
138
  let assetGroupIds = new Map();
129
139
  let dependencies = new Map();
130
140
  let assetGraphNodeIdToBundleGraphNodeId = new Map();
141
+ let conditions = new Map();
142
+ let placeholderToDependency = new Map();
131
143
  let assetGraphRootNode = assetGraph.rootNodeId != null ? assetGraph.getNode(assetGraph.rootNodeId) : null;
132
144
  (0, _assert().default)(assetGraphRootNode != null && assetGraphRootNode.type === 'root');
133
145
  assetGraph.dfsFast(nodeId => {
@@ -146,6 +158,17 @@ class BundleGraph {
146
158
  }
147
159
  } else if (node != null && node.type === 'asset_group') {
148
160
  assetGroupIds.set(nodeId, assetGraph.getNodeIdsConnectedFrom(nodeId));
161
+ } else if ((0, _featureFlags().getFeatureFlag)('conditionalBundlingApi') && node != null && node.type === 'dependency') {
162
+ var _dep$meta;
163
+ // The dependency placeholders in the `importCond` calls that will be in the transformed
164
+ // code need to be mapped to the "real" dependencies, so we need access to a map of placeholders
165
+ // to dependencies
166
+ const dep = node.value;
167
+ // $FlowFixMe[incompatible-type] Meta is untyped
168
+ const placeholder = (_dep$meta = dep.meta) === null || _dep$meta === void 0 ? void 0 : _dep$meta.placeholder;
169
+ if (placeholder != null) {
170
+ placeholderToDependency.set(placeholder, dep);
171
+ }
149
172
  }
150
173
  });
151
174
  let walkVisited = new Set();
@@ -153,6 +176,34 @@ class BundleGraph {
153
176
  if (walkVisited.has(nodeId)) return;
154
177
  walkVisited.add(nodeId);
155
178
  let node = (0, _nullthrows().default)(assetGraph.getNode(nodeId));
179
+ if ((0, _featureFlags().getFeatureFlag)('conditionalBundlingApi') && node.type === 'asset') {
180
+ const asset = node.value;
181
+ if (Array.isArray(asset.meta.conditions)) {
182
+ // $FlowFixMe
183
+ for (const condition of asset.meta.conditions) {
184
+ // Resolve the placeholders that were attached to the asset in JSTransformer to dependencies,
185
+ // as well as create a public id for the condition.
186
+ const {
187
+ key,
188
+ ifTruePlaceholder,
189
+ ifFalsePlaceholder
190
+ } = condition;
191
+ const condHash = (0, _rust().hashString)(`${key}:${ifTruePlaceholder}:${ifFalsePlaceholder}`);
192
+ const condPublicId = (0, _utils2.getPublicId)(condHash, v => conditions.has(v));
193
+ if (conditions.has(condHash)) {
194
+ throw new Error('Unexpected duplicate asset');
195
+ } else {
196
+ conditions.set(condHash, {
197
+ publicId: condPublicId,
198
+ assets: new Set([asset]),
199
+ key,
200
+ ifTrueDependency: (0, _nullthrows().default)(placeholderToDependency.get(ifTruePlaceholder), 'ifTruePlaceholder was undefined'),
201
+ ifFalseDependency: (0, _nullthrows().default)(placeholderToDependency.get(ifFalsePlaceholder), 'ifFalsePlaceholder was undefined')
202
+ });
203
+ }
204
+ }
205
+ }
206
+ }
156
207
  if (node.type === 'dependency' && node.value.symbols != null && node.value.env.shouldScopeHoist &&
157
208
  // Disable in dev mode because this feature is at odds with safeToIncrementallyBundle
158
209
  isProduction) {
@@ -338,7 +389,8 @@ class BundleGraph {
338
389
  graph,
339
390
  assetPublicIds,
340
391
  bundleContentHashes: new Map(),
341
- publicIdByAssetId
392
+ publicIdByAssetId,
393
+ conditions
342
394
  });
343
395
  }
344
396
  serialize() {
@@ -347,7 +399,8 @@ class BundleGraph {
347
399
  graph: this._graph.serialize(),
348
400
  assetPublicIds: this._assetPublicIds,
349
401
  bundleContentHashes: this._bundleContentHashes,
350
- publicIdByAssetId: this._publicIdByAssetId
402
+ publicIdByAssetId: this._publicIdByAssetId,
403
+ conditions: this._conditions
351
404
  };
352
405
  }
353
406
  static deserialize(serialized) {
@@ -355,7 +408,8 @@ class BundleGraph {
355
408
  graph: _graph().ContentGraph.deserialize(serialized.graph),
356
409
  assetPublicIds: serialized.assetPublicIds,
357
410
  bundleContentHashes: serialized.bundleContentHashes,
358
- publicIdByAssetId: serialized.publicIdByAssetId
411
+ publicIdByAssetId: serialized.publicIdByAssetId,
412
+ conditions: serialized.conditions
359
413
  });
360
414
  }
361
415
  createBundle(opts) {
@@ -768,7 +822,7 @@ class BundleGraph {
768
822
  return true;
769
823
  }
770
824
  let assetNodeId = (0, _nullthrows().default)(this._graph.getNodeIdByContentKey(asset.id));
771
- if (this._graph.getNodeIdsConnectedTo(assetNodeId, bundleGraphEdgeTypes.references).map(id => this._graph.getNode(id)).some(node => (node === null || node === void 0 ? void 0 : node.type) === 'dependency' && node.value.priority === _types.Priority.lazy && node.value.specifierType !== _types.SpecifierType.url)) {
825
+ if (this._graph.getNodeIdsConnectedTo(assetNodeId, bundleGraphEdgeTypes.references).map(id => this._graph.getNode(id)).some(node => (node === null || node === void 0 ? void 0 : node.type) === 'dependency' && (node.value.priority === _types.Priority.lazy || node.value.priority === _types.Priority.conditional) && node.value.specifierType !== _types.SpecifierType.url)) {
772
826
  // If this asset is referenced by any async dependency, it's referenced.
773
827
  return true;
774
828
  }
@@ -465,18 +465,32 @@ class RequestGraph extends _graph().ContentGraph {
465
465
  return node;
466
466
  });
467
467
  }
468
- invalidateFileNameNode(node, filePath, matchNodes) {
468
+ invalidateFileNameNode(node, filePath, matchNodes, invalidateNode) {
469
469
  // If there is an edge between this file_name node and one of the original file nodes pointed to
470
470
  // by the original file_name node, and the matched node is inside the current directory, invalidate
471
471
  // all connected requests pointed to by the file node.
472
- let dirname = _path2().default.dirname((0, _projectPath.fromProjectPathRelative)(filePath));
472
+
473
473
  let nodeId = this.getNodeIdByContentKey(node.id);
474
- for (let matchNode of matchNodes) {
475
- let matchNodeId = this.getNodeIdByContentKey(matchNode.id);
476
- if (this.hasEdge(nodeId, matchNodeId, requestGraphEdgeTypes.invalidated_by_create_above) && (0, _utils().isDirectoryInside)((0, _projectPath.fromProjectPathRelative)((0, _projectPath.toProjectPathUnsafe)(matchNode.id)), dirname)) {
477
- let connectedNodes = this.getNodeIdsConnectedTo(matchNodeId, requestGraphEdgeTypes.invalidated_by_create);
474
+ let dirname = _path2().default.dirname((0, _projectPath.fromProjectPathRelative)(filePath));
475
+ if ((0, _featureFlags().getFeatureFlag)('fixQuadraticCacheInvalidation')) {
476
+ while (dirname !== '/') {
477
+ if (!this.hasContentKey(dirname)) break;
478
+ const matchNodeId = this.getNodeIdByContentKey(dirname);
479
+ if (!this.hasEdge(nodeId, matchNodeId, requestGraphEdgeTypes.invalidated_by_create_above)) break;
480
+ const connectedNodes = this.getNodeIdsConnectedTo(matchNodeId, requestGraphEdgeTypes.invalidated_by_create);
478
481
  for (let connectedNode of connectedNodes) {
479
- this.invalidateNode(connectedNode, _constants.FILE_CREATE);
482
+ invalidateNode(connectedNode, _constants.FILE_CREATE);
483
+ }
484
+ dirname = _path2().default.dirname(dirname);
485
+ }
486
+ } else {
487
+ for (let matchNode of matchNodes) {
488
+ let matchNodeId = this.getNodeIdByContentKey(matchNode.id);
489
+ if (this.hasEdge(nodeId, matchNodeId, requestGraphEdgeTypes.invalidated_by_create_above) && (0, _utils().isDirectoryInside)((0, _projectPath.fromProjectPathRelative)((0, _projectPath.toProjectPathUnsafe)(matchNode.id)), dirname)) {
490
+ let connectedNodes = this.getNodeIdsConnectedTo(matchNodeId, requestGraphEdgeTypes.invalidated_by_create);
491
+ for (let connectedNode of connectedNodes) {
492
+ this.invalidateNode(connectedNode, _constants.FILE_CREATE);
493
+ }
480
494
  }
481
495
  }
482
496
  }
@@ -489,7 +503,7 @@ class RequestGraph extends _graph().ContentGraph {
489
503
  if (this.hasEdge(nodeId, this.getNodeIdByContentKey(contentKey), requestGraphEdgeTypes.dirname)) {
490
504
  let parent = (0, _nullthrows().default)(this.getNodeByContentKey(contentKey));
491
505
  (0, _assert().default)(parent.type === FILE_NAME);
492
- this.invalidateFileNameNode(parent, (0, _projectPath.toProjectPathUnsafe)(dirname), matchNodes);
506
+ this.invalidateFileNameNode(parent, (0, _projectPath.toProjectPathUnsafe)(dirname), matchNodes, invalidateNode);
493
507
  }
494
508
  }
495
509
  }
@@ -498,12 +512,38 @@ class RequestGraph extends _graph().ContentGraph {
498
512
  let count = 0;
499
513
  let predictedTime = 0;
500
514
  let startTime = Date.now();
501
- const removeOrphans = !(0, _featureFlags().getFeatureFlag)('fixQuadraticCacheInvalidation');
515
+ const enableOptimization = (0, _featureFlags().getFeatureFlag)('fixQuadraticCacheInvalidation');
516
+ const removeOrphans = !enableOptimization;
517
+ const invalidatedNodes = new Set();
518
+ const invalidateNode = (nodeId, reason) => {
519
+ if (enableOptimization && invalidatedNodes.has(nodeId)) {
520
+ return;
521
+ }
522
+ invalidatedNodes.add(nodeId);
523
+ this.invalidateNode(nodeId, reason);
524
+ };
525
+ const aboveCache = new Map();
526
+ const getAbove = fileNameNodeId => {
527
+ const cachedResult = aboveCache.get(fileNameNodeId);
528
+ if (enableOptimization && cachedResult) {
529
+ return cachedResult;
530
+ }
531
+ let above = [];
532
+ const children = this.getNodeIdsConnectedTo(fileNameNodeId, requestGraphEdgeTypes.invalidated_by_create_above);
533
+ for (const nodeId of children) {
534
+ let node = (0, _nullthrows().default)(this.getNode(nodeId));
535
+ if (node.type === FILE) {
536
+ above.push(node);
537
+ }
538
+ }
539
+ aboveCache.set(fileNameNodeId, above);
540
+ return above;
541
+ };
502
542
  for (let {
503
543
  path: _path,
504
544
  type
505
545
  } of events) {
506
- if (++count === 256) {
546
+ if (!enableOptimization && process.env.ATLASPACK_DISABLE_CACHE_TIMEOUT !== 'true' && ++count === 256) {
507
547
  let duration = Date.now() - startTime;
508
548
  predictedTime = duration * (events.length >> 8);
509
549
  if (predictedTime > threshold) {
@@ -550,13 +590,13 @@ class RequestGraph extends _graph().ContentGraph {
550
590
  let nodes = this.getNodeIdsConnectedTo(nodeId, requestGraphEdgeTypes.invalidated_by_update);
551
591
  for (let connectedNode of nodes) {
552
592
  didInvalidate = true;
553
- this.invalidateNode(connectedNode, _constants.FILE_UPDATE);
593
+ invalidateNode(connectedNode, _constants.FILE_UPDATE);
554
594
  }
555
595
  if (type === 'create') {
556
596
  let nodes = this.getNodeIdsConnectedTo(nodeId, requestGraphEdgeTypes.invalidated_by_create);
557
597
  for (let connectedNode of nodes) {
558
598
  didInvalidate = true;
559
- this.invalidateNode(connectedNode, _constants.FILE_CREATE);
599
+ invalidateNode(connectedNode, _constants.FILE_CREATE);
560
600
  }
561
601
  }
562
602
  } else if (type === 'create') {
@@ -566,17 +606,10 @@ class RequestGraph extends _graph().ContentGraph {
566
606
  let fileNameNodeId = this.getNodeIdByContentKey('file_name:' + basename);
567
607
 
568
608
  // Find potential file nodes to be invalidated if this file name pattern matches
569
- let above = [];
570
- for (const nodeId of this.getNodeIdsConnectedTo(fileNameNodeId, requestGraphEdgeTypes.invalidated_by_create_above)) {
571
- let node = (0, _nullthrows().default)(this.getNode(nodeId));
572
- // these might also be `glob` nodes which get handled below, we only care about files here.
573
- if (node.type === FILE) {
574
- above.push(node);
575
- }
576
- }
609
+ let above = getAbove(fileNameNodeId);
577
610
  if (above.length > 0) {
578
611
  didInvalidate = true;
579
- this.invalidateFileNameNode(fileNameNode, _filePath, above);
612
+ this.invalidateFileNameNode(fileNameNode, _filePath, above, invalidateNode);
580
613
  }
581
614
  }
582
615
  for (let globeNodeId of this.globNodeIds) {
@@ -586,7 +619,7 @@ class RequestGraph extends _graph().ContentGraph {
586
619
  let connectedNodes = this.getNodeIdsConnectedTo(globeNodeId, requestGraphEdgeTypes.invalidated_by_create);
587
620
  for (let connectedNode of connectedNodes) {
588
621
  didInvalidate = true;
589
- this.invalidateNode(connectedNode, _constants.FILE_CREATE);
622
+ invalidateNode(connectedNode, _constants.FILE_CREATE);
590
623
  }
591
624
  }
592
625
  }
@@ -594,7 +627,7 @@ class RequestGraph extends _graph().ContentGraph {
594
627
  let nodeId = this.getNodeIdByContentKey(filePath);
595
628
  for (let connectedNode of this.getNodeIdsConnectedTo(nodeId, requestGraphEdgeTypes.invalidated_by_delete)) {
596
629
  didInvalidate = true;
597
- this.invalidateNode(connectedNode, _constants.FILE_DELETE);
630
+ invalidateNode(connectedNode, _constants.FILE_DELETE);
598
631
  }
599
632
 
600
633
  // Delete the file node since it doesn't exist anymore.
@@ -614,7 +647,7 @@ class RequestGraph extends _graph().ContentGraph {
614
647
  }
615
648
  if (isInvalid) {
616
649
  for (let connectedNode of this.getNodeIdsConnectedTo(nodeId, requestGraphEdgeTypes.invalidated_by_update)) {
617
- this.invalidateNode(connectedNode, type === 'delete' ? _constants.FILE_DELETE : _constants.FILE_UPDATE);
650
+ invalidateNode(connectedNode, type === 'delete' ? _constants.FILE_DELETE : _constants.FILE_UPDATE);
618
651
  }
619
652
  didInvalidate = true;
620
653
  this.removeNode(nodeId, removeOrphans);
@@ -632,7 +665,9 @@ class RequestGraph extends _graph().ContentGraph {
632
665
  meta: {
633
666
  trackableEvent: 'fsevent_response_time',
634
667
  duration,
635
- predictedTime
668
+ predictedTime,
669
+ numberOfEvents: events.length,
670
+ numberOfInvalidatedNodes: invalidatedNodes.size
636
671
  }
637
672
  });
638
673
  return didInvalidate && this.invalidNodeIds.size > 0;
@@ -1035,7 +1070,16 @@ async function loadRequestGraph(options) {
1035
1070
  let cacheKey = getCacheKey(options);
1036
1071
  let requestGraphKey = `requestGraph-${cacheKey}`;
1037
1072
  let timeout;
1038
- const snapshotPath = _path2().default.join(options.cacheDir, `snapshot-${cacheKey}` + '.txt');
1073
+ const snapshotKey = `snapshot-${cacheKey}`;
1074
+ const snapshotPath = _path2().default.join(options.cacheDir, snapshotKey + '.txt');
1075
+ _logger().default.verbose({
1076
+ origin: '@atlaspack/core',
1077
+ message: 'Loading request graph',
1078
+ meta: {
1079
+ cacheKey,
1080
+ snapshotKey
1081
+ }
1082
+ });
1039
1083
  if (await options.cache.hasLargeBlob(requestGraphKey)) {
1040
1084
  try {
1041
1085
  let {
@@ -1075,6 +1119,14 @@ async function loadRequestGraph(options) {
1075
1119
  return new RequestGraph();
1076
1120
  }
1077
1121
  }
1122
+ _logger().default.verbose({
1123
+ origin: '@atlaspack/core',
1124
+ message: 'Cache entry for request tracker was not found, initializing a clean cache.',
1125
+ meta: {
1126
+ cacheKey,
1127
+ snapshotKey
1128
+ }
1129
+ });
1078
1130
  return new RequestGraph();
1079
1131
  }
1080
1132
  function logErrorOnBailout(options, snapshotPath, e) {
@@ -1101,6 +1153,9 @@ function logErrorOnBailout(options, snapshotPath, e) {
1101
1153
  }
1102
1154
  }
1103
1155
  function cleanUpOrphans(graph) {
1156
+ if (graph.rootNodeId == null) {
1157
+ return [];
1158
+ }
1104
1159
  const reachableNodes = new Set();
1105
1160
  graph.traverse(nodeId => {
1106
1161
  reachableNodes.add(nodeId);
@@ -108,7 +108,7 @@ function propagateSymbols({
108
108
  namespaceReexportedSymbols.add('*');
109
109
  } else {
110
110
  for (let incomingDep of incomingDeps) {
111
- if (incomingDep.value.symbols == null) {
111
+ if (incomingDep.value.symbols == null || incomingDep.value.priority === _types.Priority.conditional) {
112
112
  if (incomingDep.value.sourceAssetId == null) {
113
113
  // The root dependency on non-library builds
114
114
  isEntry = true;
@@ -180,5 +180,6 @@ napi().registerWorker(_worker_threads().workerData.tx_worker, new AtlaspackWorke
180
180
  const PriorityMap = {
181
181
  sync: 0,
182
182
  parallel: 1,
183
- lazy: 2
183
+ lazy: 2,
184
+ conditional: 3
184
185
  };
@@ -189,5 +189,73 @@ class BundleGraph {
189
189
  getEntryRoot(target) {
190
190
  return this.#graph.getEntryRoot(this.#options.projectRoot, (0, _Target.targetToInternalTarget)(target));
191
191
  }
192
+
193
+ // Given a set of dependencies, return any conditions where those dependencies are either
194
+ // the true or false dependency for those conditions. This is currently used to work out which
195
+ // conditions belong to a bundle in packaging.
196
+ getConditionsForDependencies(deps) {
197
+ const conditions = new Set();
198
+ const depIds = deps.map(dep => dep.id);
199
+ for (const condition of this.#graph._conditions.values()) {
200
+ if (depIds.includes(condition.ifTrueDependency.id) || depIds.includes(condition.ifFalseDependency.id)) {
201
+ const [trueAsset, falseAsset] = [condition.ifTrueDependency, condition.ifFalseDependency].map(dep => {
202
+ const resolved = (0, _nullthrows().default)(this.#graph.resolveAsyncDependency(dep));
203
+ if (resolved.type === 'asset') {
204
+ return resolved.value;
205
+ } else {
206
+ return this.#graph.getAssetById(resolved.value.entryAssetId);
207
+ }
208
+ });
209
+ conditions.add({
210
+ publicId: condition.publicId,
211
+ key: condition.key,
212
+ ifTrueDependency: (0, _nullthrows().default)(deps.find(dep => dep.id === condition.ifTrueDependency.id), 'ifTrueDependency was null'),
213
+ ifFalseDependency: (0, _nullthrows().default)(deps.find(dep => dep.id === condition.ifFalseDependency.id), 'ifFalseDependency was null'),
214
+ ifTrueAssetId: this.#graph.getAssetPublicId(trueAsset),
215
+ ifFalseAssetId: this.#graph.getAssetPublicId(falseAsset)
216
+ });
217
+ }
218
+ }
219
+ return conditions;
220
+ }
221
+
222
+ // This is used to generate information for building a manifest that can
223
+ // be used by a webserver to understand which conditions are used by which bundles,
224
+ // and which bundles those conditions require depending on what they evaluate to.
225
+ getConditionalBundleMapping() {
226
+ let bundleConditions = new Map();
227
+
228
+ // Convert the internal references in conditions to public API references
229
+ for (const cond of this.#graph._conditions.values()) {
230
+ let assets = Array.from(cond.assets).map(asset => (0, _nullthrows().default)(this.getAssetById(asset.id)));
231
+ let bundles = new Set();
232
+ let ifTrueBundles = [];
233
+ let ifFalseBundles = [];
234
+ for (const asset of assets) {
235
+ const bundlesWithAsset = this.getBundlesWithAsset(asset);
236
+ for (const bundle of bundlesWithAsset) {
237
+ bundles.add(bundle);
238
+ }
239
+ const assetDeps = this.getDependencies(asset);
240
+ const depToBundles = dep => {
241
+ const publicDep = (0, _nullthrows().default)(assetDeps.find(assetDep => dep.id === assetDep.id));
242
+ const resolved = (0, _nullthrows().default)(this.resolveAsyncDependency(publicDep));
243
+ (0, _assert().default)(resolved.type === 'bundle_group');
244
+ return this.getBundlesInBundleGroup(resolved.value);
245
+ };
246
+ ifTrueBundles.push(...depToBundles(cond.ifTrueDependency));
247
+ ifFalseBundles.push(...depToBundles(cond.ifFalseDependency));
248
+ }
249
+ for (let bundle of bundles) {
250
+ const conditions = bundleConditions.get(bundle) ?? new Map();
251
+ conditions.set(cond.key, {
252
+ ifTrueBundles,
253
+ ifFalseBundles
254
+ });
255
+ bundleConditions.set(bundle, conditions);
256
+ }
257
+ }
258
+ return bundleConditions;
259
+ }
192
260
  }
193
261
  exports.default = BundleGraph;
@@ -197,6 +197,8 @@ async function resolveOptions(initialOptions) {
197
197
  outputFormat: initialOptions === null || initialOptions === void 0 || (_initialOptions$defau8 = initialOptions.defaultTargetOptions) === null || _initialOptions$defau8 === void 0 ? void 0 : _initialOptions$defau8.outputFormat,
198
198
  isLibrary: initialOptions === null || initialOptions === void 0 || (_initialOptions$defau9 = initialOptions.defaultTargetOptions) === null || _initialOptions$defau9 === void 0 ? void 0 : _initialOptions$defau9.isLibrary
199
199
  },
200
+ // unused, feature-flags are set above this to allow this function to use
201
+ // feature-flags
200
202
  featureFlags: {
201
203
  ..._featureFlags().DEFAULT_FEATURE_FLAGS,
202
204
  ...(initialOptions === null || initialOptions === void 0 ? void 0 : initialOptions.featureFlags)
package/lib/types.js CHANGED
@@ -13,7 +13,8 @@ const SpecifierType = exports.SpecifierType = {
13
13
  const Priority = exports.Priority = {
14
14
  sync: 0,
15
15
  parallel: 1,
16
- lazy: 2
16
+ lazy: 2,
17
+ conditional: 3
17
18
  };
18
19
 
19
20
  // Must match package_json.rs in node-resolver-rs.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaspack/core",
3
- "version": "2.12.1-canary.3431+e27623c56",
3
+ "version": "2.12.1-canary.3434+c61bf3cde",
4
4
  "license": "(MIT OR Apache-2.0)",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -20,20 +20,20 @@
20
20
  "check-ts": "tsc --noEmit index.d.ts"
21
21
  },
22
22
  "dependencies": {
23
- "@atlaspack/cache": "2.12.1-canary.3431+e27623c56",
24
- "@atlaspack/diagnostic": "2.12.1-canary.3431+e27623c56",
25
- "@atlaspack/events": "2.12.1-canary.3431+e27623c56",
26
- "@atlaspack/feature-flags": "2.12.1-canary.3431+e27623c56",
27
- "@atlaspack/fs": "2.12.1-canary.3431+e27623c56",
28
- "@atlaspack/graph": "3.2.1-canary.3431+e27623c56",
29
- "@atlaspack/logger": "2.12.1-canary.3431+e27623c56",
30
- "@atlaspack/package-manager": "2.12.1-canary.3431+e27623c56",
31
- "@atlaspack/plugin": "2.12.1-canary.3431+e27623c56",
32
- "@atlaspack/profiler": "2.12.1-canary.3431+e27623c56",
33
- "@atlaspack/rust": "2.12.1-canary.3431+e27623c56",
34
- "@atlaspack/types": "2.12.1-canary.3431+e27623c56",
35
- "@atlaspack/utils": "2.12.1-canary.3431+e27623c56",
36
- "@atlaspack/workers": "2.12.1-canary.3431+e27623c56",
23
+ "@atlaspack/cache": "2.12.1-canary.3434+c61bf3cde",
24
+ "@atlaspack/diagnostic": "2.12.1-canary.3434+c61bf3cde",
25
+ "@atlaspack/events": "2.12.1-canary.3434+c61bf3cde",
26
+ "@atlaspack/feature-flags": "2.12.1-canary.3434+c61bf3cde",
27
+ "@atlaspack/fs": "2.12.1-canary.3434+c61bf3cde",
28
+ "@atlaspack/graph": "3.2.1-canary.3434+c61bf3cde",
29
+ "@atlaspack/logger": "2.12.1-canary.3434+c61bf3cde",
30
+ "@atlaspack/package-manager": "2.12.1-canary.3434+c61bf3cde",
31
+ "@atlaspack/plugin": "2.12.1-canary.3434+c61bf3cde",
32
+ "@atlaspack/profiler": "2.12.1-canary.3434+c61bf3cde",
33
+ "@atlaspack/rust": "2.12.1-canary.3434+c61bf3cde",
34
+ "@atlaspack/types": "2.12.1-canary.3434+c61bf3cde",
35
+ "@atlaspack/utils": "2.12.1-canary.3434+c61bf3cde",
36
+ "@atlaspack/workers": "2.12.1-canary.3434+c61bf3cde",
37
37
  "@mischnic/json-sourcemap": "^0.1.0",
38
38
  "@parcel/source-map": "^2.1.1",
39
39
  "base-x": "^3.0.8",
@@ -57,5 +57,5 @@
57
57
  "browser": {
58
58
  "./src/serializerCore.js": "./src/serializerCore.browser.js"
59
59
  },
60
- "gitHead": "e27623c56ff5bea187652ecb09db27db527ecd9e"
60
+ "gitHead": "c61bf3cde53d1cdb5ae13541c644a6e3ef5521e1"
61
61
  }
package/src/Atlaspack.js CHANGED
@@ -57,7 +57,7 @@ import {
57
57
  fromProjectPathRelative,
58
58
  } from './projectPath';
59
59
  import {tracer} from '@atlaspack/profiler';
60
- import {setFeatureFlags} from '@atlaspack/feature-flags';
60
+ import {setFeatureFlags, DEFAULT_FEATURE_FLAGS} from '@atlaspack/feature-flags';
61
61
  import {AtlaspackV3, toFileSystemV3} from './atlaspack-v3';
62
62
 
63
63
  registerCoreWithSerializer();
@@ -105,6 +105,12 @@ export default class Atlaspack {
105
105
  return;
106
106
  }
107
107
 
108
+ const featureFlags = {
109
+ ...DEFAULT_FEATURE_FLAGS,
110
+ ...this.#initialOptions.featureFlags,
111
+ };
112
+ setFeatureFlags(featureFlags);
113
+
108
114
  await initSourcemaps;
109
115
  await initRust?.();
110
116
  try {
@@ -117,9 +123,10 @@ export default class Atlaspack {
117
123
  logger.warn(e);
118
124
  }
119
125
 
120
- let resolvedOptions: AtlaspackOptions = await resolveOptions(
121
- this.#initialOptions,
122
- );
126
+ let resolvedOptions: AtlaspackOptions = await resolveOptions({
127
+ ...this.#initialOptions,
128
+ featureFlags,
129
+ });
123
130
  this.#resolvedOptions = resolvedOptions;
124
131
 
125
132
  let rustAtlaspack: AtlaspackV3;
@@ -152,8 +159,6 @@ export default class Atlaspack {
152
159
  });
153
160
  }
154
161
 
155
- setFeatureFlags(resolvedOptions.featureFlags);
156
-
157
162
  let {config} = await loadAtlaspackConfig(resolvedOptions);
158
163
  this.#config = new AtlaspackConfig(config, resolvedOptions);
159
164
 
@@ -544,6 +549,10 @@ export default class Atlaspack {
544
549
  return this.#farm.takeHeapSnapshot();
545
550
  }
546
551
 
552
+ async unstable_invalidate(): Promise<void> {
553
+ await this._init();
554
+ }
555
+
547
556
  async unstable_transform(
548
557
  options: AtlaspackTransformOptions,
549
558
  ): Promise<Array<Asset>> {
@@ -6,6 +6,7 @@ import type {
6
6
  Symbol,
7
7
  TraversalActions,
8
8
  BundleBehavior as IBundleBehavior,
9
+ ConditionMeta,
9
10
  } from '@atlaspack/types';
10
11
  import type {
11
12
  ContentKey,
@@ -26,6 +27,7 @@ import type {
26
27
  Environment,
27
28
  InternalSourceLocation,
28
29
  Target,
30
+ Condition,
29
31
  } from './types';
30
32
  import type AssetGraph from './AssetGraph';
31
33
  import type {ProjectPath} from './projectPath';
@@ -46,6 +48,7 @@ import {getBundleGroupId, getPublicId} from './utils';
46
48
  import {ISOLATED_ENVS} from './public/Environment';
47
49
  import {fromProjectPath, fromProjectPathRelative} from './projectPath';
48
50
  import {HASH_REF_PREFIX} from './constants';
51
+ import {getFeatureFlag} from '@atlaspack/feature-flags';
49
52
 
50
53
  export const bundleGraphEdgeTypes = {
51
54
  // A lack of an edge type indicates to follow the edge while traversing
@@ -91,6 +94,7 @@ type BundleGraphOpts = {|
91
94
  bundleContentHashes: Map<string, string>,
92
95
  assetPublicIds: Set<string>,
93
96
  publicIdByAssetId: Map<string, string>,
97
+ conditions: Map<string, Condition>,
94
98
  |};
95
99
 
96
100
  type SerializedBundleGraph = {|
@@ -99,6 +103,7 @@ type SerializedBundleGraph = {|
99
103
  bundleContentHashes: Map<string, string>,
100
104
  assetPublicIds: Set<string>,
101
105
  publicIdByAssetId: Map<string, string>,
106
+ conditions: Map<string, Condition>,
102
107
  |};
103
108
 
104
109
  function makeReadOnlySet<T>(set: Set<T>): $ReadOnlySet<T> {
@@ -139,22 +144,26 @@ export default class BundleGraph {
139
144
  /** The internal core Graph structure */
140
145
  _graph: ContentGraph<BundleGraphNode, BundleGraphEdgeType>;
141
146
  _bundlePublicIds /*: Set<string> */ = new Set<string>();
147
+ _conditions /*: Map<string, Condition> */ = new Map<string, Condition>();
142
148
 
143
149
  constructor({
144
150
  graph,
145
151
  publicIdByAssetId,
146
152
  assetPublicIds,
147
153
  bundleContentHashes,
154
+ conditions,
148
155
  }: {|
149
156
  graph: ContentGraph<BundleGraphNode, BundleGraphEdgeType>,
150
157
  publicIdByAssetId: Map<string, string>,
151
158
  assetPublicIds: Set<string>,
152
159
  bundleContentHashes: Map<string, string>,
160
+ conditions: Map<string, Condition>,
153
161
  |}) {
154
162
  this._graph = graph;
155
163
  this._assetPublicIds = assetPublicIds;
156
164
  this._publicIdByAssetId = publicIdByAssetId;
157
165
  this._bundleContentHashes = bundleContentHashes;
166
+ this._conditions = conditions;
158
167
  }
159
168
 
160
169
  /**
@@ -171,6 +180,9 @@ export default class BundleGraph {
171
180
  let assetGroupIds = new Map();
172
181
  let dependencies = new Map();
173
182
  let assetGraphNodeIdToBundleGraphNodeId = new Map<NodeId, NodeId>();
183
+ let conditions = new Map<string, Condition>();
184
+
185
+ let placeholderToDependency = new Map<string, Dependency>();
174
186
 
175
187
  let assetGraphRootNode =
176
188
  assetGraph.rootNodeId != null
@@ -195,6 +207,20 @@ export default class BundleGraph {
195
207
  }
196
208
  } else if (node != null && node.type === 'asset_group') {
197
209
  assetGroupIds.set(nodeId, assetGraph.getNodeIdsConnectedFrom(nodeId));
210
+ } else if (
211
+ getFeatureFlag('conditionalBundlingApi') &&
212
+ node != null &&
213
+ node.type === 'dependency'
214
+ ) {
215
+ // The dependency placeholders in the `importCond` calls that will be in the transformed
216
+ // code need to be mapped to the "real" dependencies, so we need access to a map of placeholders
217
+ // to dependencies
218
+ const dep = node.value;
219
+ // $FlowFixMe[incompatible-type] Meta is untyped
220
+ const placeholder: string | void = dep.meta?.placeholder;
221
+ if (placeholder != null) {
222
+ placeholderToDependency.set(placeholder, dep);
223
+ }
198
224
  }
199
225
  });
200
226
 
@@ -204,6 +230,51 @@ export default class BundleGraph {
204
230
  walkVisited.add(nodeId);
205
231
 
206
232
  let node = nullthrows(assetGraph.getNode(nodeId));
233
+
234
+ if (getFeatureFlag('conditionalBundlingApi') && node.type === 'asset') {
235
+ const asset = node.value;
236
+ if (Array.isArray(asset.meta.conditions)) {
237
+ // $FlowFixMe
238
+ for (const condition of (asset.meta.conditions: ConditionMeta[])) {
239
+ // Resolve the placeholders that were attached to the asset in JSTransformer to dependencies,
240
+ // as well as create a public id for the condition.
241
+ const {
242
+ key,
243
+ ifTruePlaceholder,
244
+ ifFalsePlaceholder,
245
+ }: {
246
+ key: string,
247
+ ifTruePlaceholder: string,
248
+ ifFalsePlaceholder: string,
249
+ ...
250
+ } = condition;
251
+
252
+ const condHash = hashString(
253
+ `${key}:${ifTruePlaceholder}:${ifFalsePlaceholder}`,
254
+ );
255
+ const condPublicId = getPublicId(condHash, v => conditions.has(v));
256
+
257
+ if (conditions.has(condHash)) {
258
+ throw new Error('Unexpected duplicate asset');
259
+ } else {
260
+ conditions.set(condHash, {
261
+ publicId: condPublicId,
262
+ assets: new Set([asset]),
263
+ key,
264
+ ifTrueDependency: nullthrows(
265
+ placeholderToDependency.get(ifTruePlaceholder),
266
+ 'ifTruePlaceholder was undefined',
267
+ ),
268
+ ifFalseDependency: nullthrows(
269
+ placeholderToDependency.get(ifFalsePlaceholder),
270
+ 'ifFalsePlaceholder was undefined',
271
+ ),
272
+ });
273
+ }
274
+ }
275
+ }
276
+ }
277
+
207
278
  if (
208
279
  node.type === 'dependency' &&
209
280
  node.value.symbols != null &&
@@ -440,11 +511,13 @@ export default class BundleGraph {
440
511
  );
441
512
  }
442
513
  }
514
+
443
515
  return new BundleGraph({
444
516
  graph,
445
517
  assetPublicIds,
446
518
  bundleContentHashes: new Map(),
447
519
  publicIdByAssetId,
520
+ conditions,
448
521
  });
449
522
  }
450
523
 
@@ -455,6 +528,7 @@ export default class BundleGraph {
455
528
  assetPublicIds: this._assetPublicIds,
456
529
  bundleContentHashes: this._bundleContentHashes,
457
530
  publicIdByAssetId: this._publicIdByAssetId,
531
+ conditions: this._conditions,
458
532
  };
459
533
  }
460
534
 
@@ -464,6 +538,7 @@ export default class BundleGraph {
464
538
  assetPublicIds: serialized.assetPublicIds,
465
539
  bundleContentHashes: serialized.bundleContentHashes,
466
540
  publicIdByAssetId: serialized.publicIdByAssetId,
541
+ conditions: serialized.conditions,
467
542
  });
468
543
  }
469
544
 
@@ -1215,7 +1290,8 @@ export default class BundleGraph {
1215
1290
  .some(
1216
1291
  node =>
1217
1292
  node?.type === 'dependency' &&
1218
- node.value.priority === Priority.lazy &&
1293
+ (node.value.priority === Priority.lazy ||
1294
+ node.value.priority === Priority.conditional) &&
1219
1295
  node.value.specifierType !== SpecifierType.url,
1220
1296
  )
1221
1297
  ) {
@@ -818,32 +818,59 @@ export class RequestGraph extends ContentGraph<
818
818
  node: FileNameNode,
819
819
  filePath: ProjectPath,
820
820
  matchNodes: Array<FileNode>,
821
+ invalidateNode: (NodeId, InvalidateReason) => void,
821
822
  ) {
822
823
  // If there is an edge between this file_name node and one of the original file nodes pointed to
823
824
  // by the original file_name node, and the matched node is inside the current directory, invalidate
824
825
  // all connected requests pointed to by the file node.
825
- let dirname = path.dirname(fromProjectPathRelative(filePath));
826
826
 
827
827
  let nodeId = this.getNodeIdByContentKey(node.id);
828
- for (let matchNode of matchNodes) {
829
- let matchNodeId = this.getNodeIdByContentKey(matchNode.id);
830
- if (
831
- this.hasEdge(
832
- nodeId,
833
- matchNodeId,
834
- requestGraphEdgeTypes.invalidated_by_create_above,
835
- ) &&
836
- isDirectoryInside(
837
- fromProjectPathRelative(toProjectPathUnsafe(matchNode.id)),
838
- dirname,
828
+ let dirname = path.dirname(fromProjectPathRelative(filePath));
829
+
830
+ if (getFeatureFlag('fixQuadraticCacheInvalidation')) {
831
+ while (dirname !== '/') {
832
+ if (!this.hasContentKey(dirname)) break;
833
+ const matchNodeId = this.getNodeIdByContentKey(dirname);
834
+ if (
835
+ !this.hasEdge(
836
+ nodeId,
837
+ matchNodeId,
838
+ requestGraphEdgeTypes.invalidated_by_create_above,
839
+ )
839
840
  )
840
- ) {
841
- let connectedNodes = this.getNodeIdsConnectedTo(
841
+ break;
842
+
843
+ const connectedNodes = this.getNodeIdsConnectedTo(
842
844
  matchNodeId,
843
845
  requestGraphEdgeTypes.invalidated_by_create,
844
846
  );
845
847
  for (let connectedNode of connectedNodes) {
846
- this.invalidateNode(connectedNode, FILE_CREATE);
848
+ invalidateNode(connectedNode, FILE_CREATE);
849
+ }
850
+
851
+ dirname = path.dirname(dirname);
852
+ }
853
+ } else {
854
+ for (let matchNode of matchNodes) {
855
+ let matchNodeId = this.getNodeIdByContentKey(matchNode.id);
856
+ if (
857
+ this.hasEdge(
858
+ nodeId,
859
+ matchNodeId,
860
+ requestGraphEdgeTypes.invalidated_by_create_above,
861
+ ) &&
862
+ isDirectoryInside(
863
+ fromProjectPathRelative(toProjectPathUnsafe(matchNode.id)),
864
+ dirname,
865
+ )
866
+ ) {
867
+ let connectedNodes = this.getNodeIdsConnectedTo(
868
+ matchNodeId,
869
+ requestGraphEdgeTypes.invalidated_by_create,
870
+ );
871
+ for (let connectedNode of connectedNodes) {
872
+ this.invalidateNode(connectedNode, FILE_CREATE);
873
+ }
847
874
  }
848
875
  }
849
876
  }
@@ -866,6 +893,7 @@ export class RequestGraph extends ContentGraph<
866
893
  parent,
867
894
  toProjectPathUnsafe(dirname),
868
895
  matchNodes,
896
+ invalidateNode,
869
897
  );
870
898
  }
871
899
  }
@@ -880,10 +908,45 @@ export class RequestGraph extends ContentGraph<
880
908
  let count = 0;
881
909
  let predictedTime = 0;
882
910
  let startTime = Date.now();
883
- const removeOrphans = !getFeatureFlag('fixQuadraticCacheInvalidation');
911
+ const enableOptimization = getFeatureFlag('fixQuadraticCacheInvalidation');
912
+ const removeOrphans = !enableOptimization;
913
+
914
+ const invalidatedNodes = new Set();
915
+ const invalidateNode = (nodeId, reason) => {
916
+ if (enableOptimization && invalidatedNodes.has(nodeId)) {
917
+ return;
918
+ }
919
+ invalidatedNodes.add(nodeId);
920
+ this.invalidateNode(nodeId, reason);
921
+ };
922
+ const aboveCache = new Map();
923
+ const getAbove = fileNameNodeId => {
924
+ const cachedResult = aboveCache.get(fileNameNodeId);
925
+ if (enableOptimization && cachedResult) {
926
+ return cachedResult;
927
+ }
928
+
929
+ let above = [];
930
+ const children = this.getNodeIdsConnectedTo(
931
+ fileNameNodeId,
932
+ requestGraphEdgeTypes.invalidated_by_create_above,
933
+ );
934
+ for (const nodeId of children) {
935
+ let node = nullthrows(this.getNode(nodeId));
936
+ if (node.type === FILE) {
937
+ above.push(node);
938
+ }
939
+ }
940
+ aboveCache.set(fileNameNodeId, above);
941
+ return above;
942
+ };
884
943
 
885
944
  for (let {path: _path, type} of events) {
886
- if (++count === 256) {
945
+ if (
946
+ !enableOptimization &&
947
+ process.env.ATLASPACK_DISABLE_CACHE_TIMEOUT !== 'true' &&
948
+ ++count === 256
949
+ ) {
887
950
  let duration = Date.now() - startTime;
888
951
  predictedTime = duration * (events.length >> 8);
889
952
  if (predictedTime > threshold) {
@@ -939,7 +1002,7 @@ export class RequestGraph extends ContentGraph<
939
1002
 
940
1003
  for (let connectedNode of nodes) {
941
1004
  didInvalidate = true;
942
- this.invalidateNode(connectedNode, FILE_UPDATE);
1005
+ invalidateNode(connectedNode, FILE_UPDATE);
943
1006
  }
944
1007
 
945
1008
  if (type === 'create') {
@@ -949,7 +1012,7 @@ export class RequestGraph extends ContentGraph<
949
1012
  );
950
1013
  for (let connectedNode of nodes) {
951
1014
  didInvalidate = true;
952
- this.invalidateNode(connectedNode, FILE_CREATE);
1015
+ invalidateNode(connectedNode, FILE_CREATE);
953
1016
  }
954
1017
  }
955
1018
  } else if (type === 'create') {
@@ -961,21 +1024,15 @@ export class RequestGraph extends ContentGraph<
961
1024
  );
962
1025
 
963
1026
  // Find potential file nodes to be invalidated if this file name pattern matches
964
- let above: Array<FileNode> = [];
965
- for (const nodeId of this.getNodeIdsConnectedTo(
966
- fileNameNodeId,
967
- requestGraphEdgeTypes.invalidated_by_create_above,
968
- )) {
969
- let node = nullthrows(this.getNode(nodeId));
970
- // these might also be `glob` nodes which get handled below, we only care about files here.
971
- if (node.type === FILE) {
972
- above.push(node);
973
- }
974
- }
975
-
1027
+ let above: Array<FileNode> = getAbove(fileNameNodeId);
976
1028
  if (above.length > 0) {
977
1029
  didInvalidate = true;
978
- this.invalidateFileNameNode(fileNameNode, _filePath, above);
1030
+ this.invalidateFileNameNode(
1031
+ fileNameNode,
1032
+ _filePath,
1033
+ above,
1034
+ invalidateNode,
1035
+ );
979
1036
  }
980
1037
  }
981
1038
 
@@ -990,7 +1047,7 @@ export class RequestGraph extends ContentGraph<
990
1047
  );
991
1048
  for (let connectedNode of connectedNodes) {
992
1049
  didInvalidate = true;
993
- this.invalidateNode(connectedNode, FILE_CREATE);
1050
+ invalidateNode(connectedNode, FILE_CREATE);
994
1051
  }
995
1052
  }
996
1053
  }
@@ -1001,7 +1058,7 @@ export class RequestGraph extends ContentGraph<
1001
1058
  requestGraphEdgeTypes.invalidated_by_delete,
1002
1059
  )) {
1003
1060
  didInvalidate = true;
1004
- this.invalidateNode(connectedNode, FILE_DELETE);
1061
+ invalidateNode(connectedNode, FILE_DELETE);
1005
1062
  }
1006
1063
 
1007
1064
  // Delete the file node since it doesn't exist anymore.
@@ -1033,7 +1090,7 @@ export class RequestGraph extends ContentGraph<
1033
1090
  nodeId,
1034
1091
  requestGraphEdgeTypes.invalidated_by_update,
1035
1092
  )) {
1036
- this.invalidateNode(
1093
+ invalidateNode(
1037
1094
  connectedNode,
1038
1095
  type === 'delete' ? FILE_DELETE : FILE_UPDATE,
1039
1096
  );
@@ -1057,6 +1114,8 @@ export class RequestGraph extends ContentGraph<
1057
1114
  trackableEvent: 'fsevent_response_time',
1058
1115
  duration,
1059
1116
  predictedTime,
1117
+ numberOfEvents: events.length,
1118
+ numberOfInvalidatedNodes: invalidatedNodes.size,
1060
1119
  },
1061
1120
  });
1062
1121
 
@@ -1615,6 +1674,15 @@ async function loadRequestGraph(options): Async<RequestGraph> {
1615
1674
  let timeout;
1616
1675
  const snapshotKey = `snapshot-${cacheKey}`;
1617
1676
  const snapshotPath = path.join(options.cacheDir, snapshotKey + '.txt');
1677
+
1678
+ logger.verbose({
1679
+ origin: '@atlaspack/core',
1680
+ message: 'Loading request graph',
1681
+ meta: {
1682
+ cacheKey,
1683
+ snapshotKey,
1684
+ },
1685
+ });
1618
1686
  if (await options.cache.hasLargeBlob(requestGraphKey)) {
1619
1687
  try {
1620
1688
  let {requestGraph} = await readAndDeserializeRequestGraph(
@@ -1670,6 +1738,15 @@ async function loadRequestGraph(options): Async<RequestGraph> {
1670
1738
  }
1671
1739
  }
1672
1740
 
1741
+ logger.verbose({
1742
+ origin: '@atlaspack/core',
1743
+ message:
1744
+ 'Cache entry for request tracker was not found, initializing a clean cache.',
1745
+ meta: {
1746
+ cacheKey,
1747
+ snapshotKey,
1748
+ },
1749
+ });
1673
1750
  return new RequestGraph();
1674
1751
  }
1675
1752
 
@@ -1705,6 +1782,10 @@ function logErrorOnBailout(
1705
1782
  }
1706
1783
 
1707
1784
  export function cleanUpOrphans<N, E: number>(graph: Graph<N, E>): NodeId[] {
1785
+ if (graph.rootNodeId == null) {
1786
+ return [];
1787
+ }
1788
+
1708
1789
  const reachableNodes = new Set();
1709
1790
  graph.traverse(nodeId => {
1710
1791
  reachableNodes.add(nodeId);
@@ -16,7 +16,7 @@ import nullthrows from 'nullthrows';
16
16
  import {setEqual} from '@atlaspack/utils';
17
17
  import logger from '@atlaspack/logger';
18
18
  import {md, convertSourceLocationToHighlight} from '@atlaspack/diagnostic';
19
- import {BundleBehavior} from './types';
19
+ import {BundleBehavior, Priority} from './types';
20
20
  import {fromProjectPathRelative, fromProjectPath} from './projectPath';
21
21
 
22
22
  export function propagateSymbols({
@@ -101,7 +101,10 @@ export function propagateSymbols({
101
101
  namespaceReexportedSymbols.add('*');
102
102
  } else {
103
103
  for (let incomingDep of incomingDeps) {
104
- if (incomingDep.value.symbols == null) {
104
+ if (
105
+ incomingDep.value.symbols == null ||
106
+ incomingDep.value.priority === Priority.conditional
107
+ ) {
105
108
  if (incomingDep.value.sourceAssetId == null) {
106
109
  // The root dependency on non-library builds
107
110
  isEntry = true;
@@ -217,4 +217,5 @@ const PriorityMap = {
217
217
  sync: 0,
218
218
  parallel: 1,
219
219
  lazy: 2,
220
+ conditional: 3,
220
221
  };
@@ -332,4 +332,110 @@ export default class BundleGraph<TBundle: IBundle>
332
332
  targetToInternalTarget(target),
333
333
  );
334
334
  }
335
+
336
+ // Given a set of dependencies, return any conditions where those dependencies are either
337
+ // the true or false dependency for those conditions. This is currently used to work out which
338
+ // conditions belong to a bundle in packaging.
339
+ getConditionsForDependencies(deps: Array<IDependency>): Set<{|
340
+ publicId: string,
341
+ key: string,
342
+ ifTrueDependency: IDependency,
343
+ ifFalseDependency: IDependency,
344
+ ifTrueAssetId: string,
345
+ ifFalseAssetId: string,
346
+ |}> {
347
+ const conditions = new Set();
348
+ const depIds = deps.map(dep => dep.id);
349
+ for (const condition of this.#graph._conditions.values()) {
350
+ if (
351
+ depIds.includes(condition.ifTrueDependency.id) ||
352
+ depIds.includes(condition.ifFalseDependency.id)
353
+ ) {
354
+ const [trueAsset, falseAsset] = [
355
+ condition.ifTrueDependency,
356
+ condition.ifFalseDependency,
357
+ ].map(dep => {
358
+ const resolved = nullthrows(this.#graph.resolveAsyncDependency(dep));
359
+ if (resolved.type === 'asset') {
360
+ return resolved.value;
361
+ } else {
362
+ return this.#graph.getAssetById(resolved.value.entryAssetId);
363
+ }
364
+ });
365
+
366
+ conditions.add({
367
+ publicId: condition.publicId,
368
+ key: condition.key,
369
+ ifTrueDependency: nullthrows(
370
+ deps.find(dep => dep.id === condition.ifTrueDependency.id),
371
+ 'ifTrueDependency was null',
372
+ ),
373
+ ifFalseDependency: nullthrows(
374
+ deps.find(dep => dep.id === condition.ifFalseDependency.id),
375
+ 'ifFalseDependency was null',
376
+ ),
377
+ ifTrueAssetId: this.#graph.getAssetPublicId(trueAsset),
378
+ ifFalseAssetId: this.#graph.getAssetPublicId(falseAsset),
379
+ });
380
+ }
381
+ }
382
+
383
+ return conditions;
384
+ }
385
+
386
+ // This is used to generate information for building a manifest that can
387
+ // be used by a webserver to understand which conditions are used by which bundles,
388
+ // and which bundles those conditions require depending on what they evaluate to.
389
+ getConditionalBundleMapping(): Map<
390
+ TBundle,
391
+ Map<
392
+ string,
393
+ {|
394
+ ifTrueBundles: Array<TBundle>,
395
+ ifFalseBundles: Array<TBundle>,
396
+ |},
397
+ >,
398
+ > {
399
+ let bundleConditions = new Map();
400
+
401
+ // Convert the internal references in conditions to public API references
402
+ for (const cond of this.#graph._conditions.values()) {
403
+ let assets = Array.from(cond.assets).map(asset =>
404
+ nullthrows(this.getAssetById(asset.id)),
405
+ );
406
+ let bundles = new Set<TBundle>();
407
+ let ifTrueBundles = [];
408
+ let ifFalseBundles = [];
409
+ for (const asset of assets) {
410
+ const bundlesWithAsset = this.getBundlesWithAsset(asset);
411
+ for (const bundle of bundlesWithAsset) {
412
+ bundles.add(bundle);
413
+ }
414
+ const assetDeps = this.getDependencies(asset);
415
+ const depToBundles = dep => {
416
+ const publicDep = nullthrows(
417
+ assetDeps.find(assetDep => dep.id === assetDep.id),
418
+ );
419
+ const resolved = nullthrows(this.resolveAsyncDependency(publicDep));
420
+ invariant(resolved.type === 'bundle_group');
421
+ return this.getBundlesInBundleGroup(resolved.value);
422
+ };
423
+ ifTrueBundles.push(...depToBundles(cond.ifTrueDependency));
424
+ ifFalseBundles.push(...depToBundles(cond.ifFalseDependency));
425
+ }
426
+
427
+ for (let bundle of bundles) {
428
+ const conditions = bundleConditions.get(bundle) ?? new Map();
429
+
430
+ conditions.set(cond.key, {
431
+ ifTrueBundles,
432
+ ifFalseBundles,
433
+ });
434
+
435
+ bundleConditions.set(bundle, conditions);
436
+ }
437
+ }
438
+
439
+ return bundleConditions;
440
+ }
335
441
  }
@@ -231,6 +231,8 @@ export default async function resolveOptions(
231
231
  outputFormat: initialOptions?.defaultTargetOptions?.outputFormat,
232
232
  isLibrary: initialOptions?.defaultTargetOptions?.isLibrary,
233
233
  },
234
+ // unused, feature-flags are set above this to allow this function to use
235
+ // feature-flags
234
236
  featureFlags: {...DEFAULT_FEATURE_FLAGS, ...initialOptions?.featureFlags},
235
237
  parcelVersion: ATLASPACK_VERSION,
236
238
  };
package/src/types.js CHANGED
@@ -115,6 +115,7 @@ export const Priority = {
115
115
  sync: 0,
116
116
  parallel: 1,
117
117
  lazy: 2,
118
+ conditional: 3,
118
119
  };
119
120
 
120
121
  // Must match package_json.rs in node-resolver-rs.
@@ -542,6 +543,7 @@ export type Bundle = {|
542
543
  displayName: ?string,
543
544
  pipeline: ?string,
544
545
  manualSharedBundle?: ?string,
546
+ conditions?: Map<string, string>,
545
547
  |};
546
548
 
547
549
  export type BundleNode = {|
@@ -580,3 +582,11 @@ export type ValidationOpts = {|
580
582
  |};
581
583
 
582
584
  export type ReportFn = (event: ReporterEvent) => void | Promise<void>;
585
+
586
+ export type Condition = {|
587
+ publicId: string,
588
+ assets: Set<Asset>,
589
+ key: string,
590
+ ifTrueDependency: Dependency,
591
+ ifFalseDependency: Dependency,
592
+ |};
@@ -42,6 +42,7 @@ describe('Public Bundle', () => {
42
42
  assetPublicIds: new Set(),
43
43
  publicIdByAssetId: new Map(),
44
44
  bundleContentHashes: new Map(),
45
+ conditions: new Map(),
45
46
  });
46
47
  });
47
48
 
@@ -9,8 +9,10 @@ import RequestTracker, {
9
9
  import {Graph} from '@atlaspack/graph';
10
10
  import WorkerFarm from '@atlaspack/workers';
11
11
  import {DEFAULT_OPTIONS} from './test-utils';
12
- import {INITIAL_BUILD} from '../src/constants';
12
+ import {FILE_CREATE, FILE_UPDATE, INITIAL_BUILD} from '../src/constants';
13
13
  import {makeDeferredWithPromise} from '@atlaspack/utils';
14
+ import {toProjectPath} from '../src/projectPath';
15
+ import {DEFAULT_FEATURE_FLAGS, setFeatureFlags} from '../../feature-flags/src';
14
16
 
15
17
  const options = DEFAULT_OPTIONS;
16
18
  const farm = new WorkerFarm({workerPath: require.resolve('../src/worker.js')});
@@ -449,6 +451,97 @@ describe('RequestTracker', () => {
449
451
 
450
452
  assert.equal(tracker.graph.getNode(nodeId)?.invalidateReason, 1);
451
453
  });
454
+
455
+ describe('respondToFSEvents', () => {
456
+ [true, false].forEach(value => {
457
+ beforeEach(() => {
458
+ setFeatureFlags({
459
+ ...DEFAULT_FEATURE_FLAGS,
460
+ fixQuadraticCacheInvalidation: value === true ? 'NEW' : 'OLD',
461
+ });
462
+ });
463
+
464
+ afterEach(() => {
465
+ setFeatureFlags({
466
+ ...DEFAULT_FEATURE_FLAGS,
467
+ });
468
+ });
469
+
470
+ describe(`optimizations feature-flag ${String(value)}`, () => {
471
+ it('should invalidate file requests on file changes', async () => {
472
+ let tracker = new RequestTracker({farm, options});
473
+ await tracker.runRequest({
474
+ id: 'abc',
475
+ type: 7,
476
+ // $FlowFixMe string isn't a valid result
477
+ run: async ({api}: {api: RunAPI<string | void>, ...}) => {
478
+ api.invalidateOnFileUpdate(toProjectPath('', 'my-file'));
479
+ let result = await Promise.resolve('hello');
480
+ api.storeResult(result);
481
+ },
482
+ input: null,
483
+ });
484
+ const requestId = tracker.graph.getNodeIdByContentKey('abc');
485
+ const invalidated = await tracker.respondToFSEvents(
486
+ [
487
+ {
488
+ type: 'update',
489
+ path: 'my-file',
490
+ },
491
+ ],
492
+ Number.MAX_VALUE,
493
+ );
494
+ assert.equal(invalidated, true);
495
+ assert.equal(
496
+ tracker.graph.getNode(requestId)?.invalidateReason,
497
+ FILE_UPDATE,
498
+ );
499
+ assert.deepEqual(Array.from(tracker.graph.invalidNodeIds), [
500
+ requestId,
501
+ ]);
502
+ });
503
+
504
+ it('should invalidate file name requests with invalidate above invalidations', async () => {
505
+ let tracker = new RequestTracker({farm, options});
506
+ await tracker.runRequest({
507
+ id: 'abc',
508
+ type: 7,
509
+ // $FlowFixMe string isn't a valid result
510
+ run: async ({api}: {api: RunAPI<string | void>, ...}) => {
511
+ api.invalidateOnFileCreate({
512
+ fileName: 'package.json',
513
+ aboveFilePath: toProjectPath(
514
+ '',
515
+ './node_modules/something/package.json',
516
+ ),
517
+ });
518
+ let result = await Promise.resolve('hello');
519
+ api.storeResult(result);
520
+ },
521
+ input: null,
522
+ });
523
+ const requestId = tracker.graph.getNodeIdByContentKey('abc');
524
+ const invalidated = await tracker.respondToFSEvents(
525
+ [
526
+ {
527
+ type: 'create',
528
+ path: './package.json',
529
+ },
530
+ ],
531
+ Number.MAX_VALUE,
532
+ );
533
+ assert.equal(invalidated, true);
534
+ assert.equal(
535
+ tracker.graph.getNode(requestId)?.invalidateReason,
536
+ FILE_CREATE,
537
+ );
538
+ assert.deepEqual(Array.from(tracker.graph.invalidNodeIds), [
539
+ requestId,
540
+ ]);
541
+ });
542
+ });
543
+ });
544
+ });
452
545
  });
453
546
 
454
547
  describe('cleanUpOrphans', () => {