@atlaspack/core 2.16.2-dev.55 → 2.16.2-dev.69

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 (89) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/_empty.js +0 -0
  3. package/lib/AssetGraph.js +17 -6
  4. package/lib/Atlaspack.js +2 -5
  5. package/lib/AtlaspackConfig.schema.js +1 -7
  6. package/lib/BundleGraph.js +10 -8
  7. package/lib/Dependency.js +6 -2
  8. package/lib/Environment.js +4 -3
  9. package/lib/EnvironmentManager.js +137 -0
  10. package/lib/InternalConfig.js +3 -2
  11. package/lib/PackagerRunner.js +15 -9
  12. package/lib/RequestTracker.js +56 -46
  13. package/lib/Transformation.js +2 -2
  14. package/lib/UncommittedAsset.js +3 -2
  15. package/lib/applyRuntimes.js +2 -1
  16. package/lib/assetUtils.js +2 -1
  17. package/lib/atlaspack-v3/AtlaspackV3.js +44 -1
  18. package/lib/atlaspack-v3/NapiWorkerPool.js +1 -1
  19. package/lib/atlaspack-v3/worker/compat/environment.js +2 -2
  20. package/lib/atlaspack-v3/worker/compat/mutable-asset.js +6 -6
  21. package/lib/atlaspack-v3/worker/compat/plugin-config.js +5 -5
  22. package/lib/atlaspack-v3/worker/napi-worker.js +3 -0
  23. package/lib/atlaspack-v3/worker/worker.js +11 -4
  24. package/lib/dumpGraphToGraphViz.js +1 -1
  25. package/lib/index.js +9 -1
  26. package/lib/internal-plugins.js +9 -0
  27. package/lib/isSuperPackage.js +23 -0
  28. package/lib/loadAtlaspackPlugin.js +15 -0
  29. package/lib/public/Asset.js +3 -2
  30. package/lib/public/Bundle.js +2 -1
  31. package/lib/public/Config.js +86 -19
  32. package/lib/public/Dependency.js +2 -1
  33. package/lib/public/MutableBundleGraph.js +2 -1
  34. package/lib/public/PluginOptions.js +3 -0
  35. package/lib/public/Target.js +2 -1
  36. package/lib/requests/AssetRequest.js +2 -1
  37. package/lib/requests/AtlaspackConfigRequest.js +44 -29
  38. package/lib/requests/ConfigRequest.js +27 -4
  39. package/lib/requests/TargetRequest.js +18 -16
  40. package/lib/requests/WriteBundleRequest.js +5 -2
  41. package/lib/requests/WriteBundlesRequest.js +1 -0
  42. package/lib/resolveOptions.js +5 -4
  43. package/lib/worker.js +9 -26
  44. package/package.json +21 -18
  45. package/src/AssetGraph.js +12 -6
  46. package/src/Atlaspack.js +8 -7
  47. package/src/BundleGraph.js +21 -9
  48. package/src/Dependency.js +13 -5
  49. package/src/Environment.js +8 -5
  50. package/src/EnvironmentManager.js +145 -0
  51. package/src/InternalConfig.js +6 -5
  52. package/src/PackagerRunner.js +17 -11
  53. package/src/RequestTracker.js +39 -13
  54. package/src/UncommittedAsset.js +7 -2
  55. package/src/applyRuntimes.js +6 -1
  56. package/src/assetUtils.js +4 -3
  57. package/src/atlaspack-v3/AtlaspackV3.js +53 -2
  58. package/src/atlaspack-v3/NapiWorkerPool.js +5 -1
  59. package/src/atlaspack-v3/worker/compat/plugin-config.js +1 -1
  60. package/src/atlaspack-v3/worker/worker.js +11 -4
  61. package/src/index.js +2 -0
  62. package/src/internal-plugins.js +4 -0
  63. package/src/isSuperPackage.js +28 -0
  64. package/src/loadAtlaspackPlugin.js +23 -1
  65. package/src/public/Asset.js +9 -2
  66. package/src/public/Bundle.js +2 -1
  67. package/src/public/Config.js +110 -29
  68. package/src/public/Dependency.js +2 -1
  69. package/src/public/MutableBundleGraph.js +2 -1
  70. package/src/public/PluginOptions.js +4 -0
  71. package/src/public/Target.js +2 -1
  72. package/src/requests/AssetRequest.js +2 -1
  73. package/src/requests/AtlaspackConfigRequest.js +77 -31
  74. package/src/requests/ConfigRequest.js +33 -9
  75. package/src/requests/TargetRequest.js +19 -25
  76. package/src/requests/WriteBundleRequest.js +6 -7
  77. package/src/requests/WriteBundlesRequest.js +1 -0
  78. package/src/resolveOptions.js +2 -0
  79. package/src/types.js +10 -7
  80. package/src/worker.js +8 -7
  81. package/test/Environment.test.js +43 -34
  82. package/test/EnvironmentManager.test.js +192 -0
  83. package/test/PublicEnvironment.test.js +10 -7
  84. package/test/RequestTracker.test.js +1 -4
  85. package/test/public/Config.test.js +108 -0
  86. package/test/requests/ConfigRequest.test.js +187 -3
  87. package/test/test-utils.js +3 -2
  88. package/lib/atlaspack-v3/worker/index.js +0 -6
  89. /package/src/atlaspack-v3/worker/{index.js → napi-worker.js} +0 -0
@@ -30,15 +30,15 @@ import nullthrows from 'nullthrows';
30
30
 
31
31
  import {
32
32
  ATLASPACK_VERSION,
33
- VALID,
34
- INITIAL_BUILD,
35
33
  FILE_CREATE,
36
- FILE_UPDATE,
37
34
  FILE_DELETE,
35
+ FILE_UPDATE,
38
36
  ENV_CHANGE,
37
+ ERROR,
38
+ INITIAL_BUILD,
39
39
  OPTION_CHANGE,
40
40
  STARTUP,
41
- ERROR,
41
+ VALID,
42
42
  } from './constants';
43
43
  import type {AtlaspackV3} from './atlaspack-v3/AtlaspackV3';
44
44
  import {
@@ -71,6 +71,11 @@ import type {
71
71
  import {BuildAbortError, assertSignalNotAborted, hashFromOption} from './utils';
72
72
  import {performance} from 'perf_hooks';
73
73
 
74
+ import {
75
+ loadEnvironmentsFromCache,
76
+ writeEnvironmentsToCache,
77
+ } from './EnvironmentManager';
78
+
74
79
  export const requestGraphEdgeTypes = {
75
80
  subrequest: 2,
76
81
  invalidated_by_update: 3,
@@ -144,7 +149,7 @@ type OptionNode = {|
144
149
  type ConfigKeyNode = {|
145
150
  id: ContentKey,
146
151
  +type: typeof CONFIG_KEY,
147
- configKey: string,
152
+ configKey: string[],
148
153
  contentHash: string,
149
154
  |};
150
155
 
@@ -216,7 +221,7 @@ export type RunAPI<TResult: RequestResult> = {|
216
221
  invalidateOnFileUpdate: (ProjectPath) => void,
217
222
  invalidateOnConfigKeyChange: (
218
223
  filePath: ProjectPath,
219
- configKey: string,
224
+ configKey: string[],
220
225
  contentHash: string,
221
226
  ) => void,
222
227
  invalidateOnStartup: () => void,
@@ -283,10 +288,12 @@ const nodeFromOption = (option: string, value: mixed): RequestGraphNode => ({
283
288
 
284
289
  const nodeFromConfigKey = (
285
290
  fileName: ProjectPath,
286
- configKey: string,
291
+ configKey: string[],
287
292
  contentHash: string,
288
293
  ): RequestGraphNode => ({
289
- id: `config_key:${fromProjectPathRelative(fileName)}:${configKey}`,
294
+ id: `config_key:${fromProjectPathRelative(fileName)}:${JSON.stringify(
295
+ configKey,
296
+ )}`,
290
297
  type: CONFIG_KEY,
291
298
  configKey,
292
299
  contentHash,
@@ -527,7 +534,7 @@ export class RequestGraph extends ContentGraph<
527
534
  invalidateOnConfigKeyChange(
528
535
  requestNodeId: NodeId,
529
536
  filePath: ProjectPath,
530
- configKey: string,
537
+ configKey: string[],
531
538
  contentHash: string,
532
539
  ) {
533
540
  let configKeyNodeId = this.addNode(
@@ -714,8 +721,8 @@ export class RequestGraph extends ContentGraph<
714
721
  env: string,
715
722
  value: string | void,
716
723
  ) {
717
- let envNode = nodeFromEnv(env, value);
718
- let envNodeId = this.addNode(envNode);
724
+ const envNode = nodeFromEnv(env, value);
725
+ const envNodeId = this.addNode(envNode);
719
726
 
720
727
  if (
721
728
  !this.hasEdge(
@@ -1109,11 +1116,22 @@ export class RequestGraph extends ContentGraph<
1109
1116
  }
1110
1117
 
1111
1118
  let configKeyNodes = this.configKeyNodes.get(_filePath);
1112
- if (configKeyNodes && (type === 'delete' || type === 'update')) {
1119
+
1120
+ // With granular invalidations we will always run this block,
1121
+ // so even if we get a create event (for whatever reason), we will still
1122
+ // try to limit invalidations from config key changes through hashing.
1123
+ //
1124
+ // Currently create events can invalidate a large number of nodes due to
1125
+ // "create above" invalidations.
1126
+ const isConfigKeyChange =
1127
+ getFeatureFlag('granularTsConfigInvalidation') ||
1128
+ type === 'delete' ||
1129
+ type === 'update';
1130
+ if (configKeyNodes && isConfigKeyChange) {
1113
1131
  for (let nodeId of configKeyNodes) {
1114
1132
  let isInvalid = type === 'delete';
1115
1133
 
1116
- if (type === 'update') {
1134
+ if (type !== 'delete') {
1117
1135
  let node = this.getNode(nodeId);
1118
1136
  invariant(node && node.type === CONFIG_KEY);
1119
1137
 
@@ -1559,6 +1577,10 @@ export default class RequestTracker {
1559
1577
  size: this.graph.nodes.length,
1560
1578
  });
1561
1579
 
1580
+ if (getFeatureFlag('environmentDeduplication')) {
1581
+ await writeEnvironmentsToCache(options.cache);
1582
+ }
1583
+
1562
1584
  let serialisedGraph = this.graph.serialize();
1563
1585
 
1564
1586
  // Delete an existing request graph cache, to prevent invalid states
@@ -1843,6 +1865,10 @@ async function loadRequestGraph(options): Async<RequestGraph> {
1843
1865
  },
1844
1866
  });
1845
1867
 
1868
+ if (getFeatureFlag('environmentDeduplication')) {
1869
+ await loadEnvironmentsFromCache(options.cache);
1870
+ }
1871
+
1846
1872
  const hasRequestGraphInCache = getFeatureFlag('cachePerformanceImprovements')
1847
1873
  ? await options.cache.has(requestGraphKey)
1848
1874
  : await options.cache.hasLargeBlob(requestGraphKey);
@@ -36,6 +36,7 @@ import {
36
36
  fromProjectPathRelative,
37
37
  } from './projectPath';
38
38
  import {getFeatureFlag} from '@atlaspack/feature-flags';
39
+ import {fromEnvironmentId} from './EnvironmentManager';
39
40
 
40
41
  type UncommittedAssetOptions = {|
41
42
  value: Asset,
@@ -321,7 +322,11 @@ export default class UncommittedAsset {
321
322
  ...rest,
322
323
  // $FlowFixMe "convert" the $ReadOnlyMaps to the interal mutable one
323
324
  symbols,
324
- env: mergeEnvironments(this.options.projectRoot, this.value.env, env),
325
+ env: mergeEnvironments(
326
+ this.options.projectRoot,
327
+ fromEnvironmentId(this.value.env),
328
+ env,
329
+ ),
325
330
  sourceAssetId: this.value.id,
326
331
  sourcePath: fromProjectPath(
327
332
  this.options.projectRoot,
@@ -386,7 +391,7 @@ export default class UncommittedAsset {
386
391
  isSource: this.value.isSource,
387
392
  env: mergeEnvironments(
388
393
  this.options.projectRoot,
389
- this.value.env,
394
+ fromEnvironmentId(this.value.env),
390
395
  result.env,
391
396
  ),
392
397
  dependencies:
@@ -34,6 +34,7 @@ import {createDevDependency, runDevDepRequest} from './requests/DevDepRequest';
34
34
  import {toProjectPath, fromProjectPathRelative} from './projectPath';
35
35
  import {tracer, PluginTracer} from '@atlaspack/profiler';
36
36
  import {DefaultMap} from '@atlaspack/utils';
37
+ import {fromEnvironmentId} from './EnvironmentManager';
37
38
 
38
39
  type RuntimeConnection = {|
39
40
  bundle: InternalBundle,
@@ -159,7 +160,11 @@ export default async function applyRuntimes<TResult: RequestResult>({
159
160
  let assetGroup = {
160
161
  code,
161
162
  filePath: toProjectPath(options.projectRoot, sourceName),
162
- env: mergeEnvironments(options.projectRoot, bundle.env, env),
163
+ env: mergeEnvironments(
164
+ options.projectRoot,
165
+ fromEnvironmentId(bundle.env),
166
+ env,
167
+ ),
163
168
  // Runtime assets should be considered source, as they should be
164
169
  // e.g. compiled to run in the target environment
165
170
  isSource: true,
package/src/assetUtils.js CHANGED
@@ -16,7 +16,6 @@ import type {
16
16
  Asset,
17
17
  RequestInvalidation,
18
18
  Dependency,
19
- Environment,
20
19
  AtlaspackOptions,
21
20
  } from './types';
22
21
 
@@ -40,6 +39,8 @@ import {hashString, createAssetId as createAssetIdRust} from '@atlaspack/rust';
40
39
  import {BundleBehavior as BundleBehaviorMap} from './types';
41
40
  import {PluginTracer} from '@atlaspack/profiler';
42
41
  import {identifierRegistry} from './IdentifierRegistry';
42
+ import type {EnvironmentRef} from './EnvironmentManager';
43
+ import {toEnvironmentId} from './EnvironmentManager';
43
44
 
44
45
  export type AssetOptions = {|
45
46
  id?: string,
@@ -56,7 +57,7 @@ export type AssetOptions = {|
56
57
  bundleBehavior?: ?BundleBehavior,
57
58
  isBundleSplittable?: ?boolean,
58
59
  isSource: boolean,
59
- env: Environment,
60
+ env: EnvironmentRef,
60
61
  meta?: Meta,
61
62
  outputHash?: ?string,
62
63
  pipeline?: ?string,
@@ -71,7 +72,7 @@ export type AssetOptions = {|
71
72
 
72
73
  export function createAssetIdFromOptions(options: AssetOptions): string {
73
74
  const data = {
74
- environmentId: options.env.id,
75
+ environmentId: toEnvironmentId(options.env),
75
76
  filePath: options.filePath,
76
77
  code: options.code,
77
78
  pipeline: options.pipeline,
@@ -12,6 +12,8 @@ import {NapiWorkerPool} from './NapiWorkerPool';
12
12
  import ThrowableDiagnostic from '@atlaspack/diagnostic';
13
13
  import type {Event} from '@parcel/watcher';
14
14
  import type {NapiWorkerPool as INapiWorkerPool} from '@atlaspack/types';
15
+ import {isSuperPackage} from '../isSuperPackage';
16
+ import path from 'path';
15
17
 
16
18
  export type AtlaspackV3Options = {|
17
19
  fs?: AtlaspackNapiOptions['fs'],
@@ -23,9 +25,54 @@ export type AtlaspackV3Options = {|
23
25
  lmdb: Lmdb,
24
26
  featureFlags?: {[string]: string | boolean},
25
27
  napiWorkerPool?: INapiWorkerPool,
26
- ...AtlaspackNapiOptions['options'],
28
+ ...$Diff<
29
+ AtlaspackNapiOptions['options'],
30
+ {|
31
+ jsPaths: AtlaspackNapiOptions['options']['jsPaths'],
32
+ useBuiltinConfigs: AtlaspackNapiOptions['options']['useBuiltinConfigs'],
33
+ |},
34
+ >,
27
35
  |};
28
36
 
37
+ function getJsPaths(): AtlaspackNapiOptions['options']['jsPaths'] {
38
+ const dirname = /*#__ATLASPACK_IGNORE__*/ __dirname;
39
+
40
+ if (isSuperPackage()) {
41
+ // dirname: atlaspack/lib/core/core/atlaspack-v3
42
+ // core: atlaspack/lib/core/core/index.js
43
+ const corePath = path.join(dirname, '..');
44
+ // esmodule helpers: atlaspack/lib/transformers/js/esmodule-helpers.js
45
+ const esmoduleHelpersPath = path.join(
46
+ dirname,
47
+ '../../../transformers/js/esmodule-helpers.js',
48
+ );
49
+
50
+ // empty file: atlaspack/lib/core/core/_empty.js
51
+ const emptyFile = path.join(dirname, '_empty.js');
52
+
53
+ return {
54
+ corePath,
55
+ esmoduleHelpersSpecifier: path.relative(corePath, esmoduleHelpersPath),
56
+ esmoduleHelpersIncludeNodeModules: 'atlaspack',
57
+ emptyFile,
58
+ };
59
+ }
60
+
61
+ // dirname: @atlaspack/core/lib/atlaspack-v3
62
+ // core: @atlaspack/core
63
+ const corePath = path.join(dirname, '../..');
64
+ // empty file: atlaspack/lib/core/core/_empty.js
65
+ const emptyFile = path.join(dirname, '_empty.js');
66
+
67
+ return {
68
+ corePath,
69
+ esmoduleHelpersSpecifier:
70
+ '@atlaspack/transformer-js/src/esmodule-helpers.js',
71
+ esmoduleHelpersIncludeNodeModules: '@atlaspack/transformer-js',
72
+ emptyFile,
73
+ };
74
+ }
75
+
29
76
  export class AtlaspackV3 {
30
77
  _atlaspack_napi: AtlaspackNapi;
31
78
 
@@ -52,7 +99,11 @@ export class AtlaspackV3 {
52
99
  fs,
53
100
  packageManager,
54
101
  threads,
55
- options,
102
+ options: {
103
+ ...options,
104
+ jsPaths: getJsPaths(),
105
+ useBuiltinConfigs: isSuperPackage(),
106
+ },
56
107
  napiWorkerPool,
57
108
  },
58
109
  lmdb,
@@ -6,7 +6,11 @@ import process from 'process';
6
6
  import type {Transferable} from '@atlaspack/rust';
7
7
  import {getAvailableThreads} from '@atlaspack/rust';
8
8
 
9
- const WORKER_PATH = path.join(__dirname, 'worker', 'index.js');
9
+ const WORKER_PATH = path.join(
10
+ /*#__ATLASPACK_IGNORE__*/ __dirname,
11
+ 'worker',
12
+ 'napi-worker.js',
13
+ );
10
14
  const ATLASPACK_NAPI_WORKERS =
11
15
  process.env.ATLASPACK_NAPI_WORKERS &&
12
16
  parseInt(process.env.ATLASPACK_NAPI_WORKERS, 10);
@@ -107,7 +107,7 @@ export class PluginConfig implements IPluginConfig {
107
107
  exclude?: boolean,
108
108
  |}
109
109
  | {|
110
- configKey?: string,
110
+ readTracking?: boolean,
111
111
  |},
112
112
  ): Promise<?ConfigResultWithFilePath<T>> {
113
113
  return this.#inner.getConfigFrom(searchPath, filePaths, options);
@@ -25,6 +25,7 @@ import {
25
25
  bundleBehaviorMap,
26
26
  dependencyPriorityMap,
27
27
  } from './compat';
28
+ import atlaspackInternalPlugins from '../../internal-plugins';
28
29
 
29
30
  const CONFIG = Symbol.for('parcel-plugin-config');
30
31
 
@@ -41,10 +42,16 @@ export class AtlaspackWorker {
41
42
 
42
43
  loadPlugin: JsCallable<[LoadPluginOptions], Promise<void>> = jsCallable(
43
44
  async ({kind, specifier, resolveFrom}) => {
44
- let customRequire = module.createRequire(resolveFrom);
45
- let resolvedPath = customRequire.resolve(specifier);
46
- // $FlowFixMe
47
- let resolvedModule = await import(resolvedPath);
45
+ let resolvedModule;
46
+ if (atlaspackInternalPlugins && atlaspackInternalPlugins[specifier]) {
47
+ // If the plugin is available inside the package then use it
48
+ resolvedModule = atlaspackInternalPlugins[specifier]();
49
+ } else {
50
+ let customRequire = module.createRequire(resolveFrom);
51
+ let resolvedPath = customRequire.resolve(specifier);
52
+ // $FlowFixMe
53
+ resolvedModule = await import(resolvedPath);
54
+ }
48
55
 
49
56
  let instance = undefined;
50
57
  if (resolvedModule.default && resolvedModule.default[CONFIG]) {
package/src/index.js CHANGED
@@ -10,4 +10,6 @@ export {
10
10
  INTERNAL_TRANSFORM,
11
11
  } from './Atlaspack';
12
12
 
13
+ export {isSuperPackage} from './isSuperPackage';
14
+
13
15
  export * from './atlaspack-v3';
@@ -0,0 +1,4 @@
1
+ // @flow
2
+ // This file is overridden in super package builds
3
+ // $FlowFixMe
4
+ export default {};
@@ -0,0 +1,28 @@
1
+ // @flow strict-local
2
+ let fs = require('fs');
3
+ let {findAncestorFile} = require('@atlaspack/rust');
4
+
5
+ let dirname = /*#__ATLASPACK_IGNORE__*/ __dirname;
6
+
7
+ function isSuperPackage(): boolean {
8
+ if (!dirname) {
9
+ return false;
10
+ }
11
+
12
+ let packageJson = JSON.parse(
13
+ // $FlowFixMe
14
+ fs.readFileSync(findAncestorFile(['package.json'], dirname, '/'), 'utf8'),
15
+ );
16
+
17
+ return packageJson.name === 'atlaspack';
18
+ }
19
+
20
+ let result;
21
+
22
+ module.exports.isSuperPackage = (): boolean => {
23
+ if (result == null) {
24
+ result = isSuperPackage();
25
+ }
26
+
27
+ return result;
28
+ };
@@ -8,8 +8,14 @@ import ThrowableDiagnostic, {
8
8
  generateJSONCodeHighlights,
9
9
  md,
10
10
  } from '@atlaspack/diagnostic';
11
+ import {version as ATLASPACK_VERSION} from '../package.json';
12
+ import atlaspackInternalPlugins from './internal-plugins';
11
13
  import {findAlternativeNodeModules} from '@atlaspack/utils';
12
- import {type ProjectPath, toProjectPath} from './projectPath';
14
+ import {
15
+ type ProjectPath,
16
+ toProjectPath,
17
+ toProjectPathUnsafe,
18
+ } from './projectPath';
13
19
 
14
20
  const NODE_MODULES = `${path.sep}node_modules${path.sep}`;
15
21
  const CONFIG = Symbol.for('parcel-plugin-config');
@@ -24,6 +30,22 @@ export default async function loadPlugin<T>(
24
30
  version: Semver,
25
31
  resolveFrom: ProjectPath,
26
32
  |}> {
33
+ if (atlaspackInternalPlugins && atlaspackInternalPlugins[pluginName]) {
34
+ let plugin = atlaspackInternalPlugins[pluginName]();
35
+ plugin = plugin.default || plugin;
36
+ plugin = plugin[CONFIG];
37
+ if (!plugin) {
38
+ throw new Error(
39
+ `Plugin ${pluginName} is not a valid Atlaspack plugin, should export an instance of a Atlaspack plugin ex. "export default new Reporter({ ... })".`,
40
+ );
41
+ }
42
+ return {
43
+ plugin,
44
+ version: ATLASPACK_VERSION,
45
+ resolveFrom: toProjectPathUnsafe(options.projectRoot),
46
+ };
47
+ }
48
+
27
49
  let resolveFrom = configPath;
28
50
 
29
51
  // Config packages can reference plugins, but cannot contain other plugins within them.
@@ -36,6 +36,7 @@ import {
36
36
  BundleBehaviorNames,
37
37
  } from '../types';
38
38
  import {toInternalSourceLocation} from '../utils';
39
+ import {fromEnvironmentId} from '../EnvironmentManager';
39
40
 
40
41
  const inspect = Symbol.for('nodejs.util.inspect.custom');
41
42
 
@@ -101,7 +102,10 @@ class BaseAsset {
101
102
  }
102
103
 
103
104
  get env(): IEnvironment {
104
- return new Environment(this.#asset.value.env, this.#asset.options);
105
+ return new Environment(
106
+ fromEnvironmentId(this.#asset.value.env),
107
+ this.#asset.options,
108
+ );
105
109
  }
106
110
 
107
111
  get fs(): FileSystem {
@@ -210,7 +214,10 @@ export class Asset extends BaseAsset implements IAsset {
210
214
  }
211
215
 
212
216
  get env(): IEnvironment {
213
- this.#env ??= new Environment(this.#asset.value.env, this.#asset.options);
217
+ this.#env ??= new Environment(
218
+ fromEnvironmentId(this.#asset.value.env),
219
+ this.#asset.options,
220
+ );
214
221
  return this.#env;
215
222
  }
216
223
 
@@ -34,6 +34,7 @@ import {
34
34
  import Target from './Target';
35
35
  import {BundleBehaviorNames} from '../types';
36
36
  import {fromProjectPath} from '../projectPath';
37
+ import {fromEnvironmentId} from '../EnvironmentManager';
37
38
 
38
39
  const inspect = Symbol.for('nodejs.util.inspect.custom');
39
40
 
@@ -123,7 +124,7 @@ export class Bundle implements IBundle {
123
124
  }
124
125
 
125
126
  get env(): IEnvironment {
126
- return new Environment(this.#bundle.env, this.#options);
127
+ return new Environment(fromEnvironmentId(this.#bundle.env), this.#options);
127
128
  }
128
129
 
129
130
  get needsStableName(): ?boolean {
@@ -21,12 +21,89 @@ import {
21
21
  import Environment from './Environment';
22
22
  import {fromProjectPath, toProjectPath} from '../projectPath';
23
23
  import {getFeatureFlag} from '@atlaspack/feature-flags';
24
+ import {fromEnvironmentId} from '../EnvironmentManager';
24
25
 
25
26
  const internalConfigToConfig: DefaultWeakMap<
26
27
  AtlaspackOptions,
27
28
  WeakMap<Config, PublicConfig>,
28
29
  > = new DefaultWeakMap(() => new WeakMap());
29
30
 
31
+ /**
32
+ * Implements read tracking over an object.
33
+ *
34
+ * Calling this function with a non-trivial object like a class instance will fail to work.
35
+ *
36
+ * We track reads to fields that resolve to:
37
+ *
38
+ * - primitive values
39
+ * - arrays
40
+ *
41
+ * That is, reading a nested field `a.b.c` will make a single call to `onRead` with the path
42
+ * `['a', 'b', 'c']`.
43
+ *
44
+ * In case the value is null or an array, we will track the read as well.
45
+ *
46
+ * Iterating over `Object.keys(obj.field)` will register a read for the `['field']` path.
47
+ * Other reads work normally.
48
+ *
49
+ * @example
50
+ *
51
+ * const usedPaths = new Set();
52
+ * const onRead = (path) => {
53
+ * usedPaths.add(path);
54
+ * };
55
+ *
56
+ * const config = makeConfigProxy(onRead, {a: {b: {c: 'd'}}})
57
+ * console.log(config.a.b.c);
58
+ * console.log(Array.from(usedPaths));
59
+ * // We get a single read for the path
60
+ * // ['a', 'b', 'c']
61
+ *
62
+ */
63
+ export function makeConfigProxy<T>(
64
+ onRead: (path: string[]) => void,
65
+ config: T,
66
+ ): T {
67
+ const reportedPaths = new Set();
68
+ const reportPath = (path) => {
69
+ if (reportedPaths.has(path)) {
70
+ return;
71
+ }
72
+ reportedPaths.add(path);
73
+ onRead(path);
74
+ };
75
+
76
+ const makeProxy = (target, path) => {
77
+ return new Proxy(target, {
78
+ ownKeys(target) {
79
+ reportPath(path);
80
+
81
+ // $FlowFixMe
82
+ return Object.getOwnPropertyNames(target);
83
+ },
84
+ get(target, prop) {
85
+ // $FlowFixMe
86
+ const value = target[prop];
87
+
88
+ if (
89
+ typeof value === 'object' &&
90
+ value != null &&
91
+ !Array.isArray(value)
92
+ ) {
93
+ return makeProxy(value, [...path, prop]);
94
+ }
95
+
96
+ reportPath([...path, prop]);
97
+
98
+ return value;
99
+ },
100
+ });
101
+ };
102
+
103
+ // $FlowFixMe
104
+ return makeProxy(config, []);
105
+ }
106
+
30
107
  export default class PublicConfig implements IConfig {
31
108
  #config /*: Config */;
32
109
  #pkg /*: ?PackageJSON */;
@@ -46,7 +123,7 @@ export default class PublicConfig implements IConfig {
46
123
  }
47
124
 
48
125
  get env(): Environment {
49
- return new Environment(this.#config.env, this.#options);
126
+ return new Environment(fromEnvironmentId(this.#config.env), this.#options);
50
127
  }
51
128
 
52
129
  get searchPath(): FilePath {
@@ -76,7 +153,7 @@ export default class PublicConfig implements IConfig {
76
153
  );
77
154
  }
78
155
 
79
- invalidateOnConfigKeyChange(filePath: FilePath, configKey: string) {
156
+ invalidateOnConfigKeyChange(filePath: FilePath, configKey: string[]) {
80
157
  this.#config.invalidateOnConfigKeyChange.push({
81
158
  filePath: toProjectPath(this.#options.projectRoot, filePath),
82
159
  configKey,
@@ -144,9 +221,10 @@ export default class PublicConfig implements IConfig {
144
221
  |}
145
222
  | ?{|
146
223
  /**
147
- * If specified, only invalidate when this config key changes.
224
+ * If specified, this function will return a proxy object that will track reads to
225
+ * config fields and only register invalidations for when those keys change.
148
226
  */
149
- configKey?: string,
227
+ readTracking?: boolean,
150
228
  |},
151
229
  ): Promise<?ConfigResultWithFilePath<T>> {
152
230
  let packageKey = options?.packageKey;
@@ -157,7 +235,7 @@ export default class PublicConfig implements IConfig {
157
235
 
158
236
  if (pkg && pkg.contents[packageKey]) {
159
237
  // Invalidate only when the package key changes
160
- this.invalidateOnConfigKeyChange(pkg.filePath, packageKey);
238
+ this.invalidateOnConfigKeyChange(pkg.filePath, [packageKey]);
161
239
 
162
240
  return {
163
241
  contents: pkg.contents[packageKey],
@@ -166,27 +244,24 @@ export default class PublicConfig implements IConfig {
166
244
  }
167
245
  }
168
246
 
169
- if (getFeatureFlag('granularTsConfigInvalidation')) {
170
- const configKey = options?.configKey;
171
- if (configKey != null) {
172
- for (let fileName of fileNames) {
173
- let config = await this.getConfigFrom(searchPath, [fileName], {
174
- exclude: true,
247
+ const readTracking = options?.readTracking;
248
+ if (readTracking === true) {
249
+ for (let fileName of fileNames) {
250
+ const config = await this.getConfigFrom(searchPath, [fileName], {
251
+ exclude: true,
252
+ });
253
+
254
+ if (config != null) {
255
+ return Promise.resolve({
256
+ contents: makeConfigProxy((keyPath) => {
257
+ this.invalidateOnConfigKeyChange(config.filePath, keyPath);
258
+ }, config.contents),
259
+ filePath: config.filePath,
175
260
  });
176
-
177
- if (config && config.contents[configKey]) {
178
- // Invalidate only when the package key changes
179
- this.invalidateOnConfigKeyChange(config.filePath, configKey);
180
-
181
- return {
182
- contents: config.contents[configKey],
183
- filePath: config.filePath,
184
- };
185
- }
186
261
  }
187
-
188
- // fall through so that file above invalidations are registered
189
262
  }
263
+
264
+ // fall through so that file above invalidations are registered
190
265
  }
191
266
 
192
267
  if (fileNames.length === 0) {
@@ -268,11 +343,15 @@ export default class PublicConfig implements IConfig {
268
343
 
269
344
  getConfig<T>(
270
345
  filePaths: Array<FilePath>,
271
- options: ?{|
272
- packageKey?: string,
273
- parse?: boolean,
274
- exclude?: boolean,
275
- |},
346
+ options:
347
+ | ?{|
348
+ packageKey?: string,
349
+ parse?: boolean,
350
+ exclude?: boolean,
351
+ |}
352
+ | {|
353
+ readTracking?: boolean,
354
+ |},
276
355
  ): Promise<?ConfigResultWithFilePath<T>> {
277
356
  return this.getConfigFrom(this.searchPath, filePaths, options);
278
357
  }
@@ -282,7 +361,9 @@ export default class PublicConfig implements IConfig {
282
361
  return this.#pkg;
283
362
  }
284
363
 
285
- let pkgConfig = await this.getConfig<PackageJSON>(['package.json']);
364
+ let pkgConfig = await this.getConfig<PackageJSON>(['package.json'], {
365
+ readTracking: getFeatureFlag('granularTsConfigInvalidation'),
366
+ });
286
367
  if (!pkgConfig) {
287
368
  return null;
288
369
  }