@atlaspack/core 2.16.2-canary.57 → 2.16.2-canary.59
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/EnvironmentManager.js +57 -0
- package/lib/RequestTracker.js +18 -3
- package/lib/public/Config.js +84 -18
- package/lib/requests/ConfigRequest.js +27 -4
- package/package.json +17 -17
- package/src/EnvironmentManager.js +70 -1
- package/src/InternalConfig.js +1 -1
- package/src/RequestTracker.js +37 -11
- package/src/atlaspack-v3/worker/compat/plugin-config.js +1 -1
- package/src/public/Config.js +108 -28
- package/src/requests/ConfigRequest.js +33 -9
- package/src/types.js +1 -1
- package/test/EnvironmentManager.test.js +192 -0
- package/test/public/Config.test.js +108 -0
- package/test/requests/ConfigRequest.test.js +187 -3
package/src/public/Config.js
CHANGED
|
@@ -28,6 +28,82 @@ const internalConfigToConfig: DefaultWeakMap<
|
|
|
28
28
|
WeakMap<Config, PublicConfig>,
|
|
29
29
|
> = new DefaultWeakMap(() => new WeakMap());
|
|
30
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
|
+
|
|
31
107
|
export default class PublicConfig implements IConfig {
|
|
32
108
|
#config /*: Config */;
|
|
33
109
|
#pkg /*: ?PackageJSON */;
|
|
@@ -77,7 +153,7 @@ export default class PublicConfig implements IConfig {
|
|
|
77
153
|
);
|
|
78
154
|
}
|
|
79
155
|
|
|
80
|
-
invalidateOnConfigKeyChange(filePath: FilePath, configKey: string) {
|
|
156
|
+
invalidateOnConfigKeyChange(filePath: FilePath, configKey: string[]) {
|
|
81
157
|
this.#config.invalidateOnConfigKeyChange.push({
|
|
82
158
|
filePath: toProjectPath(this.#options.projectRoot, filePath),
|
|
83
159
|
configKey,
|
|
@@ -145,9 +221,10 @@ export default class PublicConfig implements IConfig {
|
|
|
145
221
|
|}
|
|
146
222
|
| ?{|
|
|
147
223
|
/**
|
|
148
|
-
* If specified,
|
|
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.
|
|
149
226
|
*/
|
|
150
|
-
|
|
227
|
+
readTracking?: boolean,
|
|
151
228
|
|},
|
|
152
229
|
): Promise<?ConfigResultWithFilePath<T>> {
|
|
153
230
|
let packageKey = options?.packageKey;
|
|
@@ -158,7 +235,7 @@ export default class PublicConfig implements IConfig {
|
|
|
158
235
|
|
|
159
236
|
if (pkg && pkg.contents[packageKey]) {
|
|
160
237
|
// Invalidate only when the package key changes
|
|
161
|
-
this.invalidateOnConfigKeyChange(pkg.filePath, packageKey);
|
|
238
|
+
this.invalidateOnConfigKeyChange(pkg.filePath, [packageKey]);
|
|
162
239
|
|
|
163
240
|
return {
|
|
164
241
|
contents: pkg.contents[packageKey],
|
|
@@ -167,27 +244,24 @@ export default class PublicConfig implements IConfig {
|
|
|
167
244
|
}
|
|
168
245
|
}
|
|
169
246
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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,
|
|
176
260
|
});
|
|
177
|
-
|
|
178
|
-
if (config && config.contents[configKey]) {
|
|
179
|
-
// Invalidate only when the package key changes
|
|
180
|
-
this.invalidateOnConfigKeyChange(config.filePath, configKey);
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
contents: config.contents[configKey],
|
|
184
|
-
filePath: config.filePath,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
261
|
}
|
|
188
|
-
|
|
189
|
-
// fall through so that file above invalidations are registered
|
|
190
262
|
}
|
|
263
|
+
|
|
264
|
+
// fall through so that file above invalidations are registered
|
|
191
265
|
}
|
|
192
266
|
|
|
193
267
|
if (fileNames.length === 0) {
|
|
@@ -269,11 +343,15 @@ export default class PublicConfig implements IConfig {
|
|
|
269
343
|
|
|
270
344
|
getConfig<T>(
|
|
271
345
|
filePaths: Array<FilePath>,
|
|
272
|
-
options:
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
346
|
+
options:
|
|
347
|
+
| ?{|
|
|
348
|
+
packageKey?: string,
|
|
349
|
+
parse?: boolean,
|
|
350
|
+
exclude?: boolean,
|
|
351
|
+
|}
|
|
352
|
+
| {|
|
|
353
|
+
readTracking?: boolean,
|
|
354
|
+
|},
|
|
277
355
|
): Promise<?ConfigResultWithFilePath<T>> {
|
|
278
356
|
return this.getConfigFrom(this.searchPath, filePaths, options);
|
|
279
357
|
}
|
|
@@ -283,7 +361,9 @@ export default class PublicConfig implements IConfig {
|
|
|
283
361
|
return this.#pkg;
|
|
284
362
|
}
|
|
285
363
|
|
|
286
|
-
let pkgConfig = await this.getConfig<PackageJSON>(['package.json']
|
|
364
|
+
let pkgConfig = await this.getConfig<PackageJSON>(['package.json'], {
|
|
365
|
+
readTracking: getFeatureFlag('granularTsConfigInvalidation'),
|
|
366
|
+
});
|
|
287
367
|
if (!pkgConfig) {
|
|
288
368
|
return null;
|
|
289
369
|
}
|
|
@@ -63,7 +63,7 @@ export type ConfigRequest = {
|
|
|
63
63
|
invalidateOnFileChange: Set<ProjectPath>,
|
|
64
64
|
invalidateOnConfigKeyChange: Array<{|
|
|
65
65
|
filePath: ProjectPath,
|
|
66
|
-
configKey: string,
|
|
66
|
+
configKey: string[],
|
|
67
67
|
|}>,
|
|
68
68
|
invalidateOnFileCreate: Array<InternalFileCreateInvalidation>,
|
|
69
69
|
invalidateOnEnvChange: Set<string>,
|
|
@@ -108,34 +108,58 @@ export async function loadPluginConfig<T: PluginWithLoadConfig>(
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Return value at a given key path within an object.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* const obj = { a: { b: { c: 'd' } } };
|
|
116
|
+
* getValueAtPath(obj, ['a', 'b', 'c']); // 'd'
|
|
117
|
+
* getValueAtPath(obj, ['a', 'b', 'd']); // undefined
|
|
118
|
+
* getValueAtPath(obj, ['a', 'b']); // { c: 'd' }
|
|
119
|
+
* getValueAtPath(obj, ['a', 'b', 'c', 'd']); // undefined
|
|
120
|
+
*/
|
|
121
|
+
export function getValueAtPath(obj: Object, key: string[]): any {
|
|
122
|
+
let current = obj;
|
|
123
|
+
for (let part of key) {
|
|
124
|
+
if (current == null) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
current = current[part];
|
|
128
|
+
}
|
|
129
|
+
return current;
|
|
130
|
+
}
|
|
131
|
+
|
|
111
132
|
const configKeyCache = createBuildCache();
|
|
112
133
|
|
|
113
134
|
export async function getConfigKeyContentHash(
|
|
114
135
|
filePath: ProjectPath,
|
|
115
|
-
configKey: string,
|
|
136
|
+
configKey: string[],
|
|
116
137
|
options: AtlaspackOptions,
|
|
117
138
|
): Async<string> {
|
|
118
|
-
let cacheKey = `${fromProjectPathRelative(filePath)}:${
|
|
139
|
+
let cacheKey = `${fromProjectPathRelative(filePath)}:${JSON.stringify(
|
|
140
|
+
configKey,
|
|
141
|
+
)}`;
|
|
119
142
|
let cachedValue = configKeyCache.get(cacheKey);
|
|
120
143
|
|
|
121
144
|
if (cachedValue) {
|
|
122
145
|
return cachedValue;
|
|
123
146
|
}
|
|
124
147
|
|
|
125
|
-
|
|
148
|
+
const conf = await readConfig(
|
|
126
149
|
options.inputFS,
|
|
127
150
|
fromProjectPath(options.projectRoot, filePath),
|
|
128
151
|
);
|
|
129
152
|
|
|
130
|
-
|
|
153
|
+
const value = getValueAtPath(conf?.config, configKey);
|
|
154
|
+
if (conf == null || value == null) {
|
|
131
155
|
// This can occur when a config key has been removed entirely during `respondToFSEvents`
|
|
132
156
|
return '';
|
|
133
157
|
}
|
|
134
158
|
|
|
135
|
-
|
|
136
|
-
typeof
|
|
137
|
-
? hashObject(
|
|
138
|
-
: hashString(JSON.stringify(
|
|
159
|
+
const contentHash =
|
|
160
|
+
typeof value === 'object'
|
|
161
|
+
? hashObject(value)
|
|
162
|
+
: hashString(JSON.stringify(value));
|
|
139
163
|
|
|
140
164
|
configKeyCache.set(cacheKey, contentHash);
|
|
141
165
|
|
package/src/types.js
CHANGED
|
@@ -499,7 +499,7 @@ export type Config = {|
|
|
|
499
499
|
invalidateOnFileChange: Set<ProjectPath>,
|
|
500
500
|
invalidateOnConfigKeyChange: Array<{|
|
|
501
501
|
filePath: ProjectPath,
|
|
502
|
-
configKey: string,
|
|
502
|
+
configKey: string[],
|
|
503
503
|
|}>,
|
|
504
504
|
invalidateOnFileCreate: Array<InternalFileCreateInvalidation>,
|
|
505
505
|
invalidateOnEnvChange: Set<string>,
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// @flow strict-local
|
|
2
|
+
|
|
3
|
+
import assert from 'assert';
|
|
4
|
+
import nullthrows from 'nullthrows';
|
|
5
|
+
import sinon from 'sinon';
|
|
6
|
+
import {ATLASPACK_VERSION} from '../src/constants';
|
|
7
|
+
import {DEFAULT_FEATURE_FLAGS, setFeatureFlags} from '@atlaspack/feature-flags';
|
|
8
|
+
import {setAllEnvironments, getAllEnvironments} from '@atlaspack/rust';
|
|
9
|
+
import {
|
|
10
|
+
loadEnvironmentsFromCache,
|
|
11
|
+
writeEnvironmentsToCache,
|
|
12
|
+
} from '../src/EnvironmentManager';
|
|
13
|
+
import {DEFAULT_OPTIONS} from './test-utils';
|
|
14
|
+
import {LMDBLiteCache} from '@atlaspack/cache';
|
|
15
|
+
|
|
16
|
+
const options = {
|
|
17
|
+
...DEFAULT_OPTIONS,
|
|
18
|
+
cache: new LMDBLiteCache(DEFAULT_OPTIONS.cacheDir),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('EnvironmentManager', () => {
|
|
22
|
+
const env1 = {
|
|
23
|
+
id: 'd821e85f6b50315e',
|
|
24
|
+
context: 'browser',
|
|
25
|
+
engines: {browsers: ['> 0.25%']},
|
|
26
|
+
includeNodeModules: true,
|
|
27
|
+
outputFormat: 'global',
|
|
28
|
+
isLibrary: false,
|
|
29
|
+
shouldOptimize: false,
|
|
30
|
+
shouldScopeHoist: false,
|
|
31
|
+
loc: undefined,
|
|
32
|
+
sourceMap: undefined,
|
|
33
|
+
sourceType: 'module',
|
|
34
|
+
unstableSingleFileOutput: false,
|
|
35
|
+
};
|
|
36
|
+
const env2 = {
|
|
37
|
+
id: 'de92f48baa8448d2',
|
|
38
|
+
context: 'node',
|
|
39
|
+
engines: {
|
|
40
|
+
browsers: [],
|
|
41
|
+
node: '>= 8',
|
|
42
|
+
},
|
|
43
|
+
includeNodeModules: false,
|
|
44
|
+
outputFormat: 'commonjs',
|
|
45
|
+
isLibrary: true,
|
|
46
|
+
shouldOptimize: true,
|
|
47
|
+
shouldScopeHoist: true,
|
|
48
|
+
loc: null,
|
|
49
|
+
sourceMap: null,
|
|
50
|
+
sourceType: 'module',
|
|
51
|
+
unstableSingleFileOutput: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
await options.cache.ensure();
|
|
56
|
+
|
|
57
|
+
for (const key of options.cache.keys()) {
|
|
58
|
+
await options.cache.getNativeRef().delete(key);
|
|
59
|
+
}
|
|
60
|
+
setAllEnvironments([]);
|
|
61
|
+
|
|
62
|
+
setFeatureFlags({
|
|
63
|
+
...DEFAULT_FEATURE_FLAGS,
|
|
64
|
+
environmentDeduplication: true,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should store environments by ID in the cache', async () => {
|
|
69
|
+
setAllEnvironments([env1]);
|
|
70
|
+
await writeEnvironmentsToCache(options.cache);
|
|
71
|
+
|
|
72
|
+
const cachedEnv1 = await options.cache.get(
|
|
73
|
+
`Environment/${ATLASPACK_VERSION}/${env1.id}`,
|
|
74
|
+
);
|
|
75
|
+
assert.deepEqual(cachedEnv1, env1, 'Environment 1 should be cached');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should list all environment IDs in the environment manager', async () => {
|
|
79
|
+
const environmentIds = [env1.id, env2.id];
|
|
80
|
+
setAllEnvironments([env1, env2]);
|
|
81
|
+
await writeEnvironmentsToCache(options.cache);
|
|
82
|
+
|
|
83
|
+
const cachedEnvIds = await options.cache.get(
|
|
84
|
+
`EnvironmentManager/${ATLASPACK_VERSION}`,
|
|
85
|
+
);
|
|
86
|
+
const cachedIdsArray = nullthrows(cachedEnvIds);
|
|
87
|
+
assert.equal(
|
|
88
|
+
cachedIdsArray.length,
|
|
89
|
+
environmentIds.length,
|
|
90
|
+
'Should have same number of IDs',
|
|
91
|
+
);
|
|
92
|
+
assert(
|
|
93
|
+
environmentIds.every((id) => cachedIdsArray.includes(id)),
|
|
94
|
+
'All environment IDs should be present in cache',
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should write all environments to cache using writeEnvironmentsToCache', async () => {
|
|
99
|
+
setAllEnvironments([env1, env2]);
|
|
100
|
+
await writeEnvironmentsToCache(options.cache);
|
|
101
|
+
|
|
102
|
+
// Verify each environment was stored individually
|
|
103
|
+
const cachedEnv1 = await options.cache.get(
|
|
104
|
+
`Environment/${ATLASPACK_VERSION}/${env1.id}`,
|
|
105
|
+
);
|
|
106
|
+
const cachedEnv2 = await options.cache.get(
|
|
107
|
+
`Environment/${ATLASPACK_VERSION}/${env2.id}`,
|
|
108
|
+
);
|
|
109
|
+
assert.deepEqual(cachedEnv1, env1, 'Environment 1 should be cached');
|
|
110
|
+
assert.deepEqual(cachedEnv2, env2, 'Environment 2 should be cached');
|
|
111
|
+
|
|
112
|
+
// Verify environment IDs were stored in manager
|
|
113
|
+
const cachedEnvIds = await options.cache.get(
|
|
114
|
+
`EnvironmentManager/${ATLASPACK_VERSION}`,
|
|
115
|
+
);
|
|
116
|
+
const cachedIdsArray = nullthrows(cachedEnvIds);
|
|
117
|
+
assert(
|
|
118
|
+
cachedIdsArray.length === 2 &&
|
|
119
|
+
[env1.id, env2.id].every((id) => cachedIdsArray.includes(id)),
|
|
120
|
+
'Environment IDs should be stored in manager',
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should load environments from cache on loadRequestGraph on a subsequent build', async () => {
|
|
125
|
+
// Simulate cache written on a first build
|
|
126
|
+
setAllEnvironments([env1, env2]);
|
|
127
|
+
await writeEnvironmentsToCache(options.cache);
|
|
128
|
+
|
|
129
|
+
await loadEnvironmentsFromCache(options.cache);
|
|
130
|
+
|
|
131
|
+
const loadedEnvironments = getAllEnvironments();
|
|
132
|
+
assert.equal(
|
|
133
|
+
loadedEnvironments.length,
|
|
134
|
+
2,
|
|
135
|
+
'Should load 2 environments from cache',
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const env1Loaded = loadedEnvironments.find((e) => e.id === env1.id);
|
|
139
|
+
const env2Loaded = loadedEnvironments.find((e) => e.id === env2.id);
|
|
140
|
+
|
|
141
|
+
assert.deepEqual(
|
|
142
|
+
env1Loaded,
|
|
143
|
+
env1,
|
|
144
|
+
'First environment should match cached environment',
|
|
145
|
+
);
|
|
146
|
+
assert.deepEqual(
|
|
147
|
+
env2Loaded,
|
|
148
|
+
env2,
|
|
149
|
+
'Second environment should match cached environment',
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle empty cache gracefully without calling setAllEnvironments', async () => {
|
|
154
|
+
const setAllEnvironmentsSpy = sinon.spy(setAllEnvironments);
|
|
155
|
+
|
|
156
|
+
await assert.doesNotReject(
|
|
157
|
+
loadEnvironmentsFromCache(options.cache),
|
|
158
|
+
'loadEnvironmentsFromCache should not throw when cache is empty',
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
assert.equal(
|
|
162
|
+
setAllEnvironmentsSpy.callCount,
|
|
163
|
+
0,
|
|
164
|
+
'setAllEnvironments should not be called when loading from empty cache',
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should not load environments from a different version', async () => {
|
|
169
|
+
const setAllEnvironmentsSpy = sinon.spy(setAllEnvironments);
|
|
170
|
+
const differentVersion = '2.17.2'; // A different version than ATLASPACK_VERSION
|
|
171
|
+
|
|
172
|
+
// Store an environment with a different version
|
|
173
|
+
await options.cache.set(`Environment/${differentVersion}/${env1.id}`, env1);
|
|
174
|
+
await options.cache.set(`EnvironmentManager/${differentVersion}`, [
|
|
175
|
+
env1.id,
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
await loadEnvironmentsFromCache(options.cache);
|
|
179
|
+
|
|
180
|
+
assert.equal(
|
|
181
|
+
setAllEnvironmentsSpy.callCount,
|
|
182
|
+
0,
|
|
183
|
+
'setAllEnvironments should not be called when loading from different version',
|
|
184
|
+
);
|
|
185
|
+
const loadedEnvironments = getAllEnvironments();
|
|
186
|
+
assert.equal(
|
|
187
|
+
loadedEnvironments.length,
|
|
188
|
+
0,
|
|
189
|
+
'Should not load any environments from different version',
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -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
|
+
});
|