@atlaspack/core 2.17.3 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/lib/AssetGraph.js +17 -6
  3. package/lib/Atlaspack.js +3 -1
  4. package/lib/BundleGraph.js +6 -5
  5. package/lib/Dependency.js +6 -2
  6. package/lib/Environment.js +4 -3
  7. package/lib/EnvironmentManager.js +137 -0
  8. package/lib/InternalConfig.js +3 -2
  9. package/lib/PackagerRunner.js +52 -15
  10. package/lib/RequestTracker.js +191 -89
  11. package/lib/UncommittedAsset.js +20 -2
  12. package/lib/applyRuntimes.js +2 -1
  13. package/lib/assetUtils.js +2 -1
  14. package/lib/atlaspack-v3/worker/worker.js +8 -0
  15. package/lib/public/Asset.js +3 -2
  16. package/lib/public/Bundle.js +2 -1
  17. package/lib/public/BundleGraph.js +21 -5
  18. package/lib/public/Config.js +98 -3
  19. package/lib/public/Dependency.js +2 -1
  20. package/lib/public/MutableBundleGraph.js +2 -1
  21. package/lib/public/Target.js +2 -1
  22. package/lib/requests/AssetGraphRequest.js +13 -1
  23. package/lib/requests/AssetRequest.js +2 -1
  24. package/lib/requests/BundleGraphRequest.js +13 -1
  25. package/lib/requests/ConfigRequest.js +27 -4
  26. package/lib/requests/TargetRequest.js +18 -16
  27. package/lib/requests/WriteBundleRequest.js +15 -3
  28. package/lib/requests/WriteBundlesRequest.js +1 -0
  29. package/lib/resolveOptions.js +4 -2
  30. package/package.json +13 -13
  31. package/src/AssetGraph.js +12 -6
  32. package/src/Atlaspack.js +5 -4
  33. package/src/BundleGraph.js +13 -8
  34. package/src/Dependency.js +13 -5
  35. package/src/Environment.js +8 -5
  36. package/src/EnvironmentManager.js +145 -0
  37. package/src/InternalConfig.js +6 -5
  38. package/src/PackagerRunner.js +72 -20
  39. package/src/RequestTracker.js +330 -146
  40. package/src/UncommittedAsset.js +23 -3
  41. package/src/applyRuntimes.js +6 -1
  42. package/src/assetUtils.js +4 -3
  43. package/src/atlaspack-v3/worker/compat/plugin-config.js +9 -5
  44. package/src/atlaspack-v3/worker/worker.js +7 -0
  45. package/src/public/Asset.js +9 -2
  46. package/src/public/Bundle.js +2 -1
  47. package/src/public/BundleGraph.js +22 -5
  48. package/src/public/Config.js +129 -14
  49. package/src/public/Dependency.js +2 -1
  50. package/src/public/MutableBundleGraph.js +2 -1
  51. package/src/public/Target.js +2 -1
  52. package/src/requests/AssetGraphRequest.js +13 -3
  53. package/src/requests/AssetRequest.js +2 -1
  54. package/src/requests/BundleGraphRequest.js +13 -3
  55. package/src/requests/ConfigRequest.js +33 -9
  56. package/src/requests/TargetRequest.js +19 -25
  57. package/src/requests/WriteBundleRequest.js +14 -8
  58. package/src/requests/WriteBundlesRequest.js +1 -0
  59. package/src/resolveOptions.js +4 -2
  60. package/src/types.js +9 -7
  61. package/test/Environment.test.js +43 -34
  62. package/test/EnvironmentManager.test.js +192 -0
  63. package/test/PublicEnvironment.test.js +10 -7
  64. package/test/RequestTracker.test.js +115 -3
  65. package/test/public/Config.test.js +108 -0
  66. package/test/requests/ConfigRequest.test.js +187 -3
  67. package/test/test-utils.js +4 -9
@@ -5,19 +5,36 @@ import nullthrows from 'nullthrows';
5
5
  import RequestTracker, {
6
6
  type RunAPI,
7
7
  cleanUpOrphans,
8
+ runInvalidation,
9
+ getBiggestFSEventsInvalidations,
10
+ invalidateRequestGraphFSEvents,
8
11
  } from '../src/RequestTracker';
9
12
  import {Graph} from '@atlaspack/graph';
13
+ import {LMDBLiteCache} from '@atlaspack/cache';
10
14
  import WorkerFarm from '@atlaspack/workers';
11
15
  import {DEFAULT_OPTIONS} from './test-utils';
12
16
  import {FILE_CREATE, FILE_UPDATE, INITIAL_BUILD} from '../src/constants';
13
17
  import {makeDeferredWithPromise} from '@atlaspack/utils';
14
18
  import {toProjectPath} from '../src/projectPath';
15
19
  import {DEFAULT_FEATURE_FLAGS, setFeatureFlags} from '../../feature-flags/src';
20
+ import sinon from 'sinon';
21
+ import type {AtlaspackOptions} from '../src/types';
16
22
 
17
- const options = DEFAULT_OPTIONS;
23
+ const options = {
24
+ ...DEFAULT_OPTIONS,
25
+ cache: new LMDBLiteCache(DEFAULT_OPTIONS.cacheDir),
26
+ };
18
27
  const farm = new WorkerFarm({workerPath: require.resolve('../src/worker')});
19
28
 
20
29
  describe('RequestTracker', () => {
30
+ beforeEach(async () => {
31
+ await options.cache.ensure();
32
+
33
+ for (const key of options.cache.keys()) {
34
+ await options.cache.getNativeRef().delete(key);
35
+ }
36
+ });
37
+
21
38
  it('should not run requests that have not been invalidated', async () => {
22
39
  let tracker = new RequestTracker({farm, options});
23
40
  await tracker.runRequest({
@@ -482,7 +499,7 @@ describe('RequestTracker', () => {
482
499
  input: null,
483
500
  });
484
501
  const requestId = tracker.graph.getNodeIdByContentKey('abc');
485
- const invalidated = await tracker.respondToFSEvents(
502
+ const {didInvalidate: invalidated} = await tracker.respondToFSEvents(
486
503
  [
487
504
  {
488
505
  type: 'update',
@@ -521,7 +538,7 @@ describe('RequestTracker', () => {
521
538
  input: null,
522
539
  });
523
540
  const requestId = tracker.graph.getNodeIdByContentKey('abc');
524
- const invalidated = await tracker.respondToFSEvents(
541
+ const {didInvalidate: invalidated} = await tracker.respondToFSEvents(
525
542
  [
526
543
  {
527
544
  type: 'create',
@@ -584,3 +601,98 @@ root --- node1 --- node2 ----------- orphan1 --- orphan2
584
601
  assert.equal(Array.from(graph.getAllEdges()).length, 3);
585
602
  });
586
603
  });
604
+
605
+ describe('runInvalidation', () => {
606
+ it('calls an invalidationFn and tracks the number of invalidated nodes', async () => {
607
+ const mockRequestTracker = {
608
+ getInvalidNodeCount: sinon.stub(),
609
+ };
610
+
611
+ mockRequestTracker.getInvalidNodeCount.returns(10000);
612
+ const result = await runInvalidation(mockRequestTracker, {
613
+ key: 'fsEvents',
614
+ fn: () => {
615
+ mockRequestTracker.getInvalidNodeCount.returns(30000);
616
+ return {
617
+ biggestInvalidations: [{path: 'my-file', count: 10000}],
618
+ };
619
+ },
620
+ });
621
+
622
+ assert.equal(result.key, 'fsEvents');
623
+ assert.equal(result.count, 20000);
624
+ assert.deepEqual(result.detail, {
625
+ biggestInvalidations: [{path: 'my-file', count: 10000}],
626
+ });
627
+ assert(result.duration > 0, 'Duration was not reported');
628
+ });
629
+ });
630
+
631
+ describe('invalidateRequestGraphFSEvents', () => {
632
+ it('calls requestGraph.respondToFSEvents and returns the biggest invalidations', async () => {
633
+ const requestGraph = {
634
+ respondToFSEvents: sinon.stub(),
635
+ };
636
+
637
+ requestGraph.respondToFSEvents.returns({
638
+ invalidationsByPath: new Map([
639
+ ['file-1', 10],
640
+ ['file-2', 5000],
641
+ ['file-3', 8000],
642
+ ]),
643
+ });
644
+ // $FlowFixMe
645
+ const options: AtlaspackOptions = {
646
+ unstableFileInvalidations: undefined,
647
+ };
648
+
649
+ const result = await invalidateRequestGraphFSEvents(requestGraph, options, [
650
+ {
651
+ path: 'file-1',
652
+ type: 'create',
653
+ },
654
+ {
655
+ path: 'file-2',
656
+ type: 'update',
657
+ },
658
+ {
659
+ path: 'file-3',
660
+ type: 'delete',
661
+ },
662
+ ]);
663
+
664
+ assert.deepEqual(result.biggestInvalidations, [
665
+ {path: 'file-3', count: 8000},
666
+ {path: 'file-2', count: 5000},
667
+ {path: 'file-1', count: 10},
668
+ ]);
669
+ assert.equal(requestGraph.respondToFSEvents.callCount, 1);
670
+ assert.deepEqual(requestGraph.respondToFSEvents.args[0], [
671
+ [
672
+ {path: 'file-1', type: 'create'},
673
+ {path: 'file-2', type: 'update'},
674
+ {path: 'file-3', type: 'delete'},
675
+ ],
676
+ options,
677
+ 10000,
678
+ true,
679
+ ]);
680
+ });
681
+ });
682
+
683
+ describe('getBiggestFSEventsInvalidations', () => {
684
+ it('returns the paths that invalidated the most nodes', () => {
685
+ const invalidationsByPath = new Map([
686
+ ['file-1', 10],
687
+ ['file-2', 5000],
688
+ ['file-3', 8000],
689
+ ['file-4', 1000],
690
+ ['file-5', 1000],
691
+ ]);
692
+
693
+ assert.deepEqual(getBiggestFSEventsInvalidations(invalidationsByPath, 2), [
694
+ {path: 'file-3', count: 8000},
695
+ {path: 'file-2', count: 5000},
696
+ ]);
697
+ });
698
+ });
@@ -0,0 +1,108 @@
1
+ // @flow strict-local
2
+
3
+ import sinon from 'sinon';
4
+ import {makeConfigProxy} from '../../src/public/Config';
5
+ import assert from 'assert';
6
+
7
+ describe('makeConfigProxy', () => {
8
+ it('tracks reads to nested fields', () => {
9
+ const onRead = sinon.spy();
10
+ const target = {a: {b: {c: 'd'}}};
11
+ const config = makeConfigProxy(onRead, target);
12
+ config.a.b.c;
13
+ assert.ok(onRead.calledWith(['a', 'b', 'c']));
14
+ assert.ok(onRead.calledOnce);
15
+ });
16
+
17
+ it('works for reading package.json dependencies', () => {
18
+ const packageJson = {
19
+ dependencies: {
20
+ react: '18.2.0',
21
+ },
22
+ };
23
+
24
+ const onRead = sinon.spy();
25
+ const config = makeConfigProxy(onRead, packageJson);
26
+ assert.equal(config.dependencies.react, '18.2.0');
27
+ // $FlowFixMe
28
+ assert.equal(config.dependencies.preact, undefined);
29
+ assert.ok(onRead.calledWith(['dependencies', 'react']));
30
+ assert.ok(onRead.calledWith(['dependencies', 'preact']));
31
+ assert.equal(onRead.callCount, 2);
32
+ });
33
+
34
+ it('will track reads for any missing or null keys', () => {
35
+ const packageJson = {
36
+ dependencies: {
37
+ react: '18.2.0',
38
+ },
39
+ };
40
+
41
+ const onRead = sinon.spy();
42
+ const config = makeConfigProxy(onRead, packageJson);
43
+
44
+ // $FlowFixMe
45
+ assert.equal(config.alias?.react, undefined);
46
+ assert.ok(onRead.calledWith(['alias']));
47
+ assert.equal(onRead.callCount, 1);
48
+ });
49
+
50
+ it('iterating over keys works normally and will register a read for the key being enumerated', () => {
51
+ const packageJson = {
52
+ nested: {
53
+ dependencies: {
54
+ react: '18.2.0',
55
+ 'react-dom': '18.2.0',
56
+ 'react-router': '6.14.2',
57
+ },
58
+ },
59
+ };
60
+
61
+ const onRead = sinon.spy();
62
+ const config = makeConfigProxy(onRead, packageJson);
63
+ assert.equal(Object.keys(config.nested.dependencies).length, 3);
64
+
65
+ assert.ok(onRead.calledWith(['nested', 'dependencies']));
66
+ });
67
+
68
+ it('if a key has an array value we will track a read for that key', () => {
69
+ const packageJson = {
70
+ scripts: ['build', 'test'],
71
+ };
72
+
73
+ const onRead = sinon.spy();
74
+ const config = makeConfigProxy(onRead, packageJson);
75
+ assert.equal(config.scripts[0], 'build');
76
+ assert.equal(onRead.callCount, 1);
77
+ assert.ok(onRead.calledWith(['scripts']));
78
+ });
79
+
80
+ it('if a key array value is iterated over we will track a read for that key', () => {
81
+ const packageJson = {
82
+ scripts: ['build', 'test'],
83
+ };
84
+
85
+ const onRead = sinon.spy();
86
+ const config = makeConfigProxy(onRead, packageJson);
87
+ let scriptCount = 0;
88
+ // eslint-disable-next-line no-unused-vars
89
+ for (const _script of config.scripts) {
90
+ scriptCount += 1;
91
+ }
92
+ assert.equal(scriptCount, 2);
93
+ assert.ok(onRead.calledWith(['scripts']));
94
+ assert.equal(onRead.callCount, 1);
95
+ });
96
+
97
+ it('if a key array value length is verified we will track a read for that key', () => {
98
+ const packageJson = {
99
+ scripts: ['build', 'test'],
100
+ };
101
+
102
+ const onRead = sinon.spy();
103
+ const config = makeConfigProxy(onRead, packageJson);
104
+ assert.equal(config.scripts.length, 2);
105
+ assert.ok(onRead.calledWith(['scripts']));
106
+ assert.equal(onRead.callCount, 1);
107
+ });
108
+ });
@@ -12,7 +12,10 @@ import type {
12
12
  ConfigRequestResult,
13
13
  } from '../../src/requests/ConfigRequest';
14
14
  import type {RunAPI} from '../../src/RequestTracker';
15
- import {runConfigRequest} from '../../src/requests/ConfigRequest';
15
+ import {
16
+ getValueAtPath,
17
+ runConfigRequest,
18
+ } from '../../src/requests/ConfigRequest';
16
19
  import {toProjectPath} from '../../src/projectPath';
17
20
 
18
21
  // $FlowFixMe unclear-type forgive me
@@ -228,7 +231,7 @@ describe('ConfigRequest tests', () => {
228
231
  ...baseRequest,
229
232
  invalidateOnConfigKeyChange: [
230
233
  {
231
- configKey: 'key1',
234
+ configKey: ['key1'],
232
235
  filePath: toProjectPath(projectRoot, 'config.json'),
233
236
  },
234
237
  ],
@@ -244,8 +247,189 @@ describe('ConfigRequest tests', () => {
244
247
  const call = mockCast(mockRunApi.invalidateOnConfigKeyChange).getCall(0);
245
248
  assert.deepEqual(
246
249
  call.args,
247
- ['config.json', 'key1', hashString('"value1"')],
250
+ ['config.json', ['key1'], hashString('"value1"')],
248
251
  'Invalidate was called for key1',
249
252
  );
250
253
  });
251
254
  });
255
+
256
+ describe('getValueAtPath', () => {
257
+ it('can get a key from an object', () => {
258
+ const obj = {a: {b: {c: 'd'}}};
259
+ assert.equal(getValueAtPath(obj, ['a', 'b', 'c']), 'd');
260
+ });
261
+
262
+ it('returns the original object when key array is empty', () => {
263
+ const obj = {a: 1, b: 2};
264
+ assert.deepEqual(getValueAtPath(obj, []), obj);
265
+ });
266
+
267
+ it('can access single-level properties', () => {
268
+ const obj = {name: 'test', age: 25};
269
+ assert.equal(getValueAtPath(obj, ['name']), 'test');
270
+ assert.equal(getValueAtPath(obj, ['age']), 25);
271
+ });
272
+
273
+ it('returns undefined for non-existent keys', () => {
274
+ const obj = {a: {b: 'value'}};
275
+ assert.equal(getValueAtPath(obj, ['nonexistent']), undefined);
276
+ assert.equal(getValueAtPath(obj, ['a', 'nonexistent']), undefined);
277
+ assert.equal(getValueAtPath(obj, ['a', 'b', 'nonexistent']), undefined);
278
+ });
279
+
280
+ it('handles null and undefined values in the path', () => {
281
+ const obj = {a: null, b: {c: undefined}};
282
+ assert.equal(getValueAtPath(obj, ['a']), null);
283
+ assert.equal(getValueAtPath(obj, ['b', 'c']), undefined);
284
+ });
285
+
286
+ it('does not throw when trying to access property of null', () => {
287
+ const obj = {a: null};
288
+ assert.equal(getValueAtPath(obj, ['a', 'b']), undefined);
289
+ });
290
+
291
+ it('does not throw when trying to access property of undefined', () => {
292
+ const obj = {a: undefined};
293
+ assert.equal(getValueAtPath(obj, ['a', 'b']), undefined);
294
+ });
295
+
296
+ it('can access nested arrays and objects', () => {
297
+ const obj = {
298
+ data: [
299
+ {name: 'item1', props: {color: 'red'}},
300
+ {name: 'item2', props: {color: 'blue'}},
301
+ ],
302
+ };
303
+ assert.equal(getValueAtPath(obj, ['data', '0', 'name']), 'item1');
304
+ assert.equal(getValueAtPath(obj, ['data', '1', 'props', 'color']), 'blue');
305
+ });
306
+
307
+ it('handles numeric keys as strings', () => {
308
+ const obj = {'0': 'first', '1': {nested: 'value'}};
309
+ assert.equal(getValueAtPath(obj, ['0']), 'first');
310
+ assert.equal(getValueAtPath(obj, ['1', 'nested']), 'value');
311
+ });
312
+
313
+ it('handles keys with special characters', () => {
314
+ const obj = {
315
+ 'key-with-dashes': 'value1',
316
+ 'key.with.dots': {
317
+ 'nested-key': 'value2',
318
+ },
319
+ 'key with spaces': 'value3',
320
+ '@special$chars#': 'value4',
321
+ };
322
+ assert.equal(getValueAtPath(obj, ['key-with-dashes']), 'value1');
323
+ assert.equal(
324
+ getValueAtPath(obj, ['key.with.dots', 'nested-key']),
325
+ 'value2',
326
+ );
327
+ assert.equal(getValueAtPath(obj, ['key with spaces']), 'value3');
328
+ assert.equal(getValueAtPath(obj, ['@special$chars#']), 'value4');
329
+ });
330
+
331
+ it('handles falsy values correctly', () => {
332
+ const obj = {
333
+ zero: 0,
334
+ false: false,
335
+ emptyString: '',
336
+ nullValue: null,
337
+ undefinedValue: undefined,
338
+ nested: {
339
+ zero: 0,
340
+ false: false,
341
+ },
342
+ };
343
+ assert.equal(getValueAtPath(obj, ['zero']), 0);
344
+ assert.equal(getValueAtPath(obj, ['false']), false);
345
+ assert.equal(getValueAtPath(obj, ['emptyString']), '');
346
+ assert.equal(getValueAtPath(obj, ['nullValue']), null);
347
+ assert.equal(getValueAtPath(obj, ['undefinedValue']), undefined);
348
+ assert.equal(getValueAtPath(obj, ['nested', 'zero']), 0);
349
+ assert.equal(getValueAtPath(obj, ['nested', 'false']), false);
350
+ });
351
+
352
+ it('handles deep nesting', () => {
353
+ const obj = {
354
+ level1: {
355
+ level2: {
356
+ level3: {
357
+ level4: {
358
+ level5: {
359
+ deepValue: 'found',
360
+ },
361
+ },
362
+ },
363
+ },
364
+ },
365
+ };
366
+ assert.equal(
367
+ getValueAtPath(obj, [
368
+ 'level1',
369
+ 'level2',
370
+ 'level3',
371
+ 'level4',
372
+ 'level5',
373
+ 'deepValue',
374
+ ]),
375
+ 'found',
376
+ );
377
+ });
378
+
379
+ it('handles Date objects', () => {
380
+ const date = new Date('2023-01-01');
381
+ const obj = {
382
+ timestamp: date,
383
+ nested: {
384
+ date: date,
385
+ },
386
+ };
387
+ assert.equal(getValueAtPath(obj, ['timestamp']), date);
388
+ assert.equal(getValueAtPath(obj, ['nested', 'date']), date);
389
+ });
390
+
391
+ it('handles complex nested structures with mixed types', () => {
392
+ const obj = {
393
+ users: [
394
+ {
395
+ id: 1,
396
+ profile: {
397
+ settings: {
398
+ theme: 'dark',
399
+ notifications: true,
400
+ },
401
+ },
402
+ },
403
+ {
404
+ id: 2,
405
+ profile: {
406
+ settings: {
407
+ theme: 'light',
408
+ notifications: false,
409
+ },
410
+ },
411
+ },
412
+ ],
413
+ config: {
414
+ version: '1.0.0',
415
+ features: ['feature1', 'feature2'],
416
+ },
417
+ };
418
+
419
+ assert.equal(
420
+ getValueAtPath(obj, ['users', '0', 'profile', 'settings', 'theme']),
421
+ 'dark',
422
+ );
423
+ assert.equal(
424
+ getValueAtPath(obj, [
425
+ 'users',
426
+ '1',
427
+ 'profile',
428
+ 'settings',
429
+ 'notifications',
430
+ ]),
431
+ false,
432
+ );
433
+ assert.equal(getValueAtPath(obj, ['config', 'features', '0']), 'feature1');
434
+ });
435
+ });
@@ -1,19 +1,14 @@
1
1
  // @flow strict-local
2
2
 
3
- import type {Environment, AtlaspackOptions, Target} from '../src/types';
3
+ import type {AtlaspackOptions, Target} from '../src/types';
4
4
 
5
5
  import {DEFAULT_FEATURE_FLAGS} from '@atlaspack/feature-flags';
6
- import {FSCache} from '@atlaspack/cache';
7
- import tempy from 'tempy';
8
- import {inputFS, outputFS} from '@atlaspack/test-utils';
6
+ import {inputFS, outputFS, cache, cacheDir} from '@atlaspack/test-utils';
9
7
  import {relativePath} from '@atlaspack/utils';
10
8
  import {NodePackageManager} from '@atlaspack/package-manager';
11
9
  import {createEnvironment} from '../src/Environment';
12
10
  import {toProjectPath} from '../src/projectPath';
13
-
14
- let cacheDir = tempy.directory();
15
- export let cache: FSCache = new FSCache(outputFS, cacheDir);
16
- cache.ensure();
11
+ import type {EnvironmentRef} from '../src/EnvironmentManager';
17
12
 
18
13
  export const DEFAULT_OPTIONS: AtlaspackOptions = {
19
14
  cacheDir,
@@ -57,7 +52,7 @@ export const DEFAULT_OPTIONS: AtlaspackOptions = {
57
52
  },
58
53
  };
59
54
 
60
- export const DEFAULT_ENV: Environment = createEnvironment({
55
+ export const DEFAULT_ENV: EnvironmentRef = createEnvironment({
61
56
  context: 'browser',
62
57
  engines: {
63
58
  browsers: ['> 1%'],