@atlaspack/core 2.16.2-canary.57 → 2.16.2-canary.58
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/RequestTracker.js +11 -3
- package/lib/public/Config.js +84 -18
- package/lib/requests/ConfigRequest.js +27 -4
- package/package.json +17 -17
- package/src/InternalConfig.js +1 -1
- package/src/RequestTracker.js +20 -7
- 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/public/Config.test.js +108 -0
- package/test/requests/ConfigRequest.test.js +187 -3
package/lib/RequestTracker.js
CHANGED
|
@@ -162,7 +162,7 @@ const nodeFromOption = (option, value) => ({
|
|
|
162
162
|
hash: (0, _utils2.hashFromOption)(value)
|
|
163
163
|
});
|
|
164
164
|
const nodeFromConfigKey = (fileName, configKey, contentHash) => ({
|
|
165
|
-
id: `config_key:${(0, _projectPath.fromProjectPathRelative)(fileName)}:${configKey}`,
|
|
165
|
+
id: `config_key:${(0, _projectPath.fromProjectPathRelative)(fileName)}:${JSON.stringify(configKey)}`,
|
|
166
166
|
type: CONFIG_KEY,
|
|
167
167
|
configKey,
|
|
168
168
|
contentHash
|
|
@@ -695,10 +695,18 @@ class RequestGraph extends _graph().ContentGraph {
|
|
|
695
695
|
this.removeNode(nodeId, removeOrphans);
|
|
696
696
|
}
|
|
697
697
|
let configKeyNodes = this.configKeyNodes.get(_filePath);
|
|
698
|
-
|
|
698
|
+
|
|
699
|
+
// With granular invalidations we will always run this block,
|
|
700
|
+
// so even if we get a create event (for whatever reason), we will still
|
|
701
|
+
// try to limit invalidations from config key changes through hashing.
|
|
702
|
+
//
|
|
703
|
+
// Currently create events can invalidate a large number of nodes due to
|
|
704
|
+
// "create above" invalidations.
|
|
705
|
+
const isConfigKeyChange = (0, _featureFlags().getFeatureFlag)('granularTsConfigInvalidation') || type === 'delete' || type === 'update';
|
|
706
|
+
if (configKeyNodes && isConfigKeyChange) {
|
|
699
707
|
for (let nodeId of configKeyNodes) {
|
|
700
708
|
let isInvalid = type === 'delete';
|
|
701
|
-
if (type
|
|
709
|
+
if (type !== 'delete') {
|
|
702
710
|
let node = this.getNode(nodeId);
|
|
703
711
|
(0, _assert().default)(node && node.type === CONFIG_KEY);
|
|
704
712
|
let contentHash = await (0, _ConfigRequest.getConfigKeyContentHash)(_filePath, node.configKey, options);
|
package/lib/public/Config.js
CHANGED
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.default = void 0;
|
|
7
|
+
exports.makeConfigProxy = makeConfigProxy;
|
|
7
8
|
function _assert() {
|
|
8
9
|
const data = _interopRequireDefault(require("assert"));
|
|
9
10
|
_assert = function () {
|
|
@@ -37,6 +38,71 @@ function _featureFlags() {
|
|
|
37
38
|
var _EnvironmentManager = require("../EnvironmentManager");
|
|
38
39
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
39
40
|
const internalConfigToConfig = new (_utils().DefaultWeakMap)(() => new WeakMap());
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Implements read tracking over an object.
|
|
44
|
+
*
|
|
45
|
+
* Calling this function with a non-trivial object like a class instance will fail to work.
|
|
46
|
+
*
|
|
47
|
+
* We track reads to fields that resolve to:
|
|
48
|
+
*
|
|
49
|
+
* - primitive values
|
|
50
|
+
* - arrays
|
|
51
|
+
*
|
|
52
|
+
* That is, reading a nested field `a.b.c` will make a single call to `onRead` with the path
|
|
53
|
+
* `['a', 'b', 'c']`.
|
|
54
|
+
*
|
|
55
|
+
* In case the value is null or an array, we will track the read as well.
|
|
56
|
+
*
|
|
57
|
+
* Iterating over `Object.keys(obj.field)` will register a read for the `['field']` path.
|
|
58
|
+
* Other reads work normally.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
*
|
|
62
|
+
* const usedPaths = new Set();
|
|
63
|
+
* const onRead = (path) => {
|
|
64
|
+
* usedPaths.add(path);
|
|
65
|
+
* };
|
|
66
|
+
*
|
|
67
|
+
* const config = makeConfigProxy(onRead, {a: {b: {c: 'd'}}})
|
|
68
|
+
* console.log(config.a.b.c);
|
|
69
|
+
* console.log(Array.from(usedPaths));
|
|
70
|
+
* // We get a single read for the path
|
|
71
|
+
* // ['a', 'b', 'c']
|
|
72
|
+
*
|
|
73
|
+
*/
|
|
74
|
+
function makeConfigProxy(onRead, config) {
|
|
75
|
+
const reportedPaths = new Set();
|
|
76
|
+
const reportPath = path => {
|
|
77
|
+
if (reportedPaths.has(path)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
reportedPaths.add(path);
|
|
81
|
+
onRead(path);
|
|
82
|
+
};
|
|
83
|
+
const makeProxy = (target, path) => {
|
|
84
|
+
return new Proxy(target, {
|
|
85
|
+
ownKeys(target) {
|
|
86
|
+
reportPath(path);
|
|
87
|
+
|
|
88
|
+
// $FlowFixMe
|
|
89
|
+
return Object.getOwnPropertyNames(target);
|
|
90
|
+
},
|
|
91
|
+
get(target, prop) {
|
|
92
|
+
// $FlowFixMe
|
|
93
|
+
const value = target[prop];
|
|
94
|
+
if (typeof value === 'object' && value != null && !Array.isArray(value)) {
|
|
95
|
+
return makeProxy(value, [...path, prop]);
|
|
96
|
+
}
|
|
97
|
+
reportPath([...path, prop]);
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// $FlowFixMe
|
|
104
|
+
return makeProxy(config, []);
|
|
105
|
+
}
|
|
40
106
|
class PublicConfig {
|
|
41
107
|
#config /*: Config */;
|
|
42
108
|
#pkg /*: ?PackageJSON */;
|
|
@@ -127,32 +193,30 @@ class PublicConfig {
|
|
|
127
193
|
});
|
|
128
194
|
if (pkg && pkg.contents[packageKey]) {
|
|
129
195
|
// Invalidate only when the package key changes
|
|
130
|
-
this.invalidateOnConfigKeyChange(pkg.filePath, packageKey);
|
|
196
|
+
this.invalidateOnConfigKeyChange(pkg.filePath, [packageKey]);
|
|
131
197
|
return {
|
|
132
198
|
contents: pkg.contents[packageKey],
|
|
133
199
|
filePath: pkg.filePath
|
|
134
200
|
};
|
|
135
201
|
}
|
|
136
202
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
203
|
+
const readTracking = options === null || options === void 0 ? void 0 : options.readTracking;
|
|
204
|
+
if (readTracking === true) {
|
|
205
|
+
for (let fileName of fileNames) {
|
|
206
|
+
const config = await this.getConfigFrom(searchPath, [fileName], {
|
|
207
|
+
exclude: true
|
|
208
|
+
});
|
|
209
|
+
if (config != null) {
|
|
210
|
+
return Promise.resolve({
|
|
211
|
+
contents: makeConfigProxy(keyPath => {
|
|
212
|
+
this.invalidateOnConfigKeyChange(config.filePath, keyPath);
|
|
213
|
+
}, config.contents),
|
|
214
|
+
filePath: config.filePath
|
|
143
215
|
});
|
|
144
|
-
if (config && config.contents[configKey]) {
|
|
145
|
-
// Invalidate only when the package key changes
|
|
146
|
-
this.invalidateOnConfigKeyChange(config.filePath, configKey);
|
|
147
|
-
return {
|
|
148
|
-
contents: config.contents[configKey],
|
|
149
|
-
filePath: config.filePath
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
216
|
}
|
|
153
|
-
|
|
154
|
-
// fall through so that file above invalidations are registered
|
|
155
217
|
}
|
|
218
|
+
|
|
219
|
+
// fall through so that file above invalidations are registered
|
|
156
220
|
}
|
|
157
221
|
|
|
158
222
|
if (fileNames.length === 0) {
|
|
@@ -219,7 +283,9 @@ class PublicConfig {
|
|
|
219
283
|
if (this.#pkg) {
|
|
220
284
|
return this.#pkg;
|
|
221
285
|
}
|
|
222
|
-
let pkgConfig = await this.getConfig(['package.json']
|
|
286
|
+
let pkgConfig = await this.getConfig(['package.json'], {
|
|
287
|
+
readTracking: (0, _featureFlags().getFeatureFlag)('granularTsConfigInvalidation')
|
|
288
|
+
});
|
|
223
289
|
if (!pkgConfig) {
|
|
224
290
|
return null;
|
|
225
291
|
}
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.getConfigHash = getConfigHash;
|
|
7
7
|
exports.getConfigKeyContentHash = getConfigKeyContentHash;
|
|
8
8
|
exports.getConfigRequests = getConfigRequests;
|
|
9
|
+
exports.getValueAtPath = getValueAtPath;
|
|
9
10
|
exports.loadPluginConfig = loadPluginConfig;
|
|
10
11
|
exports.runConfigRequest = runConfigRequest;
|
|
11
12
|
function _utils() {
|
|
@@ -86,19 +87,41 @@ async function loadPluginConfig(loadedPlugin, config, options) {
|
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Return value at a given key path within an object.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* const obj = { a: { b: { c: 'd' } } };
|
|
96
|
+
* getValueAtPath(obj, ['a', 'b', 'c']); // 'd'
|
|
97
|
+
* getValueAtPath(obj, ['a', 'b', 'd']); // undefined
|
|
98
|
+
* getValueAtPath(obj, ['a', 'b']); // { c: 'd' }
|
|
99
|
+
* getValueAtPath(obj, ['a', 'b', 'c', 'd']); // undefined
|
|
100
|
+
*/
|
|
101
|
+
function getValueAtPath(obj, key) {
|
|
102
|
+
let current = obj;
|
|
103
|
+
for (let part of key) {
|
|
104
|
+
if (current == null) {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
current = current[part];
|
|
108
|
+
}
|
|
109
|
+
return current;
|
|
110
|
+
}
|
|
89
111
|
const configKeyCache = (0, _buildCache().createBuildCache)();
|
|
90
112
|
async function getConfigKeyContentHash(filePath, configKey, options) {
|
|
91
|
-
let cacheKey = `${(0, _projectPath.fromProjectPathRelative)(filePath)}:${configKey}`;
|
|
113
|
+
let cacheKey = `${(0, _projectPath.fromProjectPathRelative)(filePath)}:${JSON.stringify(configKey)}`;
|
|
92
114
|
let cachedValue = configKeyCache.get(cacheKey);
|
|
93
115
|
if (cachedValue) {
|
|
94
116
|
return cachedValue;
|
|
95
117
|
}
|
|
96
|
-
|
|
97
|
-
|
|
118
|
+
const conf = await (0, _utils().readConfig)(options.inputFS, (0, _projectPath.fromProjectPath)(options.projectRoot, filePath));
|
|
119
|
+
const value = getValueAtPath(conf === null || conf === void 0 ? void 0 : conf.config, configKey);
|
|
120
|
+
if (conf == null || value == null) {
|
|
98
121
|
// This can occur when a config key has been removed entirely during `respondToFSEvents`
|
|
99
122
|
return '';
|
|
100
123
|
}
|
|
101
|
-
|
|
124
|
+
const contentHash = typeof value === 'object' ? (0, _utils().hashObject)(value) : (0, _rust().hashString)(JSON.stringify(value));
|
|
102
125
|
configKeyCache.set(cacheKey, contentHash);
|
|
103
126
|
return contentHash;
|
|
104
127
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaspack/core",
|
|
3
|
-
"version": "2.16.2-canary.
|
|
3
|
+
"version": "2.16.2-canary.58+1e32d4eae",
|
|
4
4
|
"license": "(MIT OR Apache-2.0)",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -21,21 +21,21 @@
|
|
|
21
21
|
"check-ts": "tsc --noEmit index.d.ts"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@atlaspack/build-cache": "2.13.3-canary.
|
|
25
|
-
"@atlaspack/cache": "3.1.1-canary.
|
|
26
|
-
"@atlaspack/diagnostic": "2.14.1-canary.
|
|
27
|
-
"@atlaspack/events": "2.14.1-canary.
|
|
28
|
-
"@atlaspack/feature-flags": "2.14.1-canary.
|
|
29
|
-
"@atlaspack/fs": "2.14.5-canary.
|
|
30
|
-
"@atlaspack/graph": "3.4.1-canary.
|
|
31
|
-
"@atlaspack/logger": "2.14.5-canary.
|
|
32
|
-
"@atlaspack/package-manager": "2.14.5-canary.
|
|
33
|
-
"@atlaspack/plugin": "2.14.5-canary.
|
|
34
|
-
"@atlaspack/profiler": "2.14.1-canary.
|
|
35
|
-
"@atlaspack/rust": "3.2.1-canary.
|
|
36
|
-
"@atlaspack/types": "2.14.5-canary.
|
|
37
|
-
"@atlaspack/utils": "2.14.5-canary.
|
|
38
|
-
"@atlaspack/workers": "2.14.5-canary.
|
|
24
|
+
"@atlaspack/build-cache": "2.13.3-canary.126+1e32d4eae",
|
|
25
|
+
"@atlaspack/cache": "3.1.1-canary.58+1e32d4eae",
|
|
26
|
+
"@atlaspack/diagnostic": "2.14.1-canary.126+1e32d4eae",
|
|
27
|
+
"@atlaspack/events": "2.14.1-canary.126+1e32d4eae",
|
|
28
|
+
"@atlaspack/feature-flags": "2.14.1-canary.126+1e32d4eae",
|
|
29
|
+
"@atlaspack/fs": "2.14.5-canary.58+1e32d4eae",
|
|
30
|
+
"@atlaspack/graph": "3.4.1-canary.126+1e32d4eae",
|
|
31
|
+
"@atlaspack/logger": "2.14.5-canary.58+1e32d4eae",
|
|
32
|
+
"@atlaspack/package-manager": "2.14.5-canary.58+1e32d4eae",
|
|
33
|
+
"@atlaspack/plugin": "2.14.5-canary.58+1e32d4eae",
|
|
34
|
+
"@atlaspack/profiler": "2.14.1-canary.126+1e32d4eae",
|
|
35
|
+
"@atlaspack/rust": "3.2.1-canary.58+1e32d4eae",
|
|
36
|
+
"@atlaspack/types": "2.14.5-canary.58+1e32d4eae",
|
|
37
|
+
"@atlaspack/utils": "2.14.5-canary.58+1e32d4eae",
|
|
38
|
+
"@atlaspack/workers": "2.14.5-canary.58+1e32d4eae",
|
|
39
39
|
"@mischnic/json-sourcemap": "^0.1.0",
|
|
40
40
|
"@parcel/source-map": "^2.1.1",
|
|
41
41
|
"base-x": "^3.0.8",
|
|
@@ -71,5 +71,5 @@
|
|
|
71
71
|
"./src/serializerCore.js": "./src/serializerCore.browser.js"
|
|
72
72
|
},
|
|
73
73
|
"type": "commonjs",
|
|
74
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "1e32d4eae6b3af3968e8a0ef97d35b4347fd4196"
|
|
75
75
|
}
|
package/src/InternalConfig.js
CHANGED
|
@@ -24,7 +24,7 @@ type ConfigOpts = {|
|
|
|
24
24
|
invalidateOnFileChange?: Set<ProjectPath>,
|
|
25
25
|
invalidateOnConfigKeyChange?: Array<{|
|
|
26
26
|
filePath: ProjectPath,
|
|
27
|
-
configKey: string,
|
|
27
|
+
configKey: string[],
|
|
28
28
|
|}>,
|
|
29
29
|
invalidateOnFileCreate?: Array<InternalFileCreateInvalidation>,
|
|
30
30
|
invalidateOnEnvChange?: Set<string>,
|
package/src/RequestTracker.js
CHANGED
|
@@ -144,7 +144,7 @@ type OptionNode = {|
|
|
|
144
144
|
type ConfigKeyNode = {|
|
|
145
145
|
id: ContentKey,
|
|
146
146
|
+type: typeof CONFIG_KEY,
|
|
147
|
-
configKey: string,
|
|
147
|
+
configKey: string[],
|
|
148
148
|
contentHash: string,
|
|
149
149
|
|};
|
|
150
150
|
|
|
@@ -216,7 +216,7 @@ export type RunAPI<TResult: RequestResult> = {|
|
|
|
216
216
|
invalidateOnFileUpdate: (ProjectPath) => void,
|
|
217
217
|
invalidateOnConfigKeyChange: (
|
|
218
218
|
filePath: ProjectPath,
|
|
219
|
-
configKey: string,
|
|
219
|
+
configKey: string[],
|
|
220
220
|
contentHash: string,
|
|
221
221
|
) => void,
|
|
222
222
|
invalidateOnStartup: () => void,
|
|
@@ -283,10 +283,12 @@ const nodeFromOption = (option: string, value: mixed): RequestGraphNode => ({
|
|
|
283
283
|
|
|
284
284
|
const nodeFromConfigKey = (
|
|
285
285
|
fileName: ProjectPath,
|
|
286
|
-
configKey: string,
|
|
286
|
+
configKey: string[],
|
|
287
287
|
contentHash: string,
|
|
288
288
|
): RequestGraphNode => ({
|
|
289
|
-
id: `config_key:${fromProjectPathRelative(fileName)}:${
|
|
289
|
+
id: `config_key:${fromProjectPathRelative(fileName)}:${JSON.stringify(
|
|
290
|
+
configKey,
|
|
291
|
+
)}`,
|
|
290
292
|
type: CONFIG_KEY,
|
|
291
293
|
configKey,
|
|
292
294
|
contentHash,
|
|
@@ -527,7 +529,7 @@ export class RequestGraph extends ContentGraph<
|
|
|
527
529
|
invalidateOnConfigKeyChange(
|
|
528
530
|
requestNodeId: NodeId,
|
|
529
531
|
filePath: ProjectPath,
|
|
530
|
-
configKey: string,
|
|
532
|
+
configKey: string[],
|
|
531
533
|
contentHash: string,
|
|
532
534
|
) {
|
|
533
535
|
let configKeyNodeId = this.addNode(
|
|
@@ -1109,11 +1111,22 @@ export class RequestGraph extends ContentGraph<
|
|
|
1109
1111
|
}
|
|
1110
1112
|
|
|
1111
1113
|
let configKeyNodes = this.configKeyNodes.get(_filePath);
|
|
1112
|
-
|
|
1114
|
+
|
|
1115
|
+
// With granular invalidations we will always run this block,
|
|
1116
|
+
// so even if we get a create event (for whatever reason), we will still
|
|
1117
|
+
// try to limit invalidations from config key changes through hashing.
|
|
1118
|
+
//
|
|
1119
|
+
// Currently create events can invalidate a large number of nodes due to
|
|
1120
|
+
// "create above" invalidations.
|
|
1121
|
+
const isConfigKeyChange =
|
|
1122
|
+
getFeatureFlag('granularTsConfigInvalidation') ||
|
|
1123
|
+
type === 'delete' ||
|
|
1124
|
+
type === 'update';
|
|
1125
|
+
if (configKeyNodes && isConfigKeyChange) {
|
|
1113
1126
|
for (let nodeId of configKeyNodes) {
|
|
1114
1127
|
let isInvalid = type === 'delete';
|
|
1115
1128
|
|
|
1116
|
-
if (type
|
|
1129
|
+
if (type !== 'delete') {
|
|
1117
1130
|
let node = this.getNode(nodeId);
|
|
1118
1131
|
invariant(node && node.type === CONFIG_KEY);
|
|
1119
1132
|
|
|
@@ -107,7 +107,7 @@ export class PluginConfig implements IPluginConfig {
|
|
|
107
107
|
exclude?: boolean,
|
|
108
108
|
|}
|
|
109
109
|
| {|
|
|
110
|
-
|
|
110
|
+
readTracking?: boolean,
|
|
111
111
|
|},
|
|
112
112
|
): Promise<?ConfigResultWithFilePath<T>> {
|
|
113
113
|
return this.#inner.getConfigFrom(searchPath, filePaths, options);
|
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,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 {
|
|
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
|
+
});
|