@atlaspack/core 2.30.2 → 2.31.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.
- package/CHANGELOG.md +29 -0
- package/dist/Atlaspack.js +2 -0
- package/dist/Transformation.js +24 -1
- package/dist/atlaspack-v3/AtlaspackV3.js +3 -0
- package/dist/atlaspack-v3/NapiWorkerPool.js +10 -0
- package/dist/atlaspack-v3/worker/compat/plugin-config.js +10 -22
- package/dist/atlaspack-v3/worker/compat/plugin-options.js +15 -0
- package/dist/atlaspack-v3/worker/side-effect-detector.js +243 -0
- package/dist/atlaspack-v3/worker/worker.js +140 -66
- package/dist/requests/ConfigRequest.js +24 -0
- package/lib/Atlaspack.js +5 -1
- package/lib/Transformation.js +31 -1
- package/lib/atlaspack-v3/AtlaspackV3.js +3 -0
- package/lib/atlaspack-v3/NapiWorkerPool.js +10 -0
- package/lib/atlaspack-v3/worker/compat/plugin-config.js +8 -27
- package/lib/atlaspack-v3/worker/compat/plugin-options.js +15 -0
- package/lib/atlaspack-v3/worker/side-effect-detector.js +215 -0
- package/lib/atlaspack-v3/worker/worker.js +152 -72
- package/lib/requests/ConfigRequest.js +25 -0
- package/lib/types/InternalConfig.d.ts +1 -2
- package/lib/types/atlaspack-v3/AtlaspackV3.d.ts +2 -1
- package/lib/types/atlaspack-v3/NapiWorkerPool.d.ts +1 -0
- package/lib/types/atlaspack-v3/index.d.ts +1 -0
- package/lib/types/atlaspack-v3/worker/compat/plugin-config.d.ts +3 -11
- package/lib/types/atlaspack-v3/worker/compat/plugin-options.d.ts +1 -0
- package/lib/types/atlaspack-v3/worker/side-effect-detector.d.ts +76 -0
- package/lib/types/atlaspack-v3/worker/worker.d.ts +26 -6
- package/lib/types/requests/ConfigRequest.d.ts +9 -1
- package/package.json +14 -14
- package/src/Atlaspack.ts +2 -0
- package/src/InternalConfig.ts +1 -1
- package/src/Transformation.ts +37 -2
- package/src/atlaspack-v3/AtlaspackV3.ts +8 -0
- package/src/atlaspack-v3/NapiWorkerPool.ts +17 -0
- package/src/atlaspack-v3/index.ts +1 -0
- package/src/atlaspack-v3/worker/compat/plugin-config.ts +8 -40
- package/src/atlaspack-v3/worker/compat/plugin-options.ts +15 -0
- package/src/atlaspack-v3/worker/side-effect-detector.ts +298 -0
- package/src/atlaspack-v3/worker/worker.ts +288 -172
- package/src/requests/ConfigRequest.ts +39 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import {AsyncLocalStorage} from 'async_hooks';
|
|
2
|
+
import type {Async} from '@atlaspack/types';
|
|
3
|
+
|
|
4
|
+
export interface FsUsage {
|
|
5
|
+
method: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface EnvUsage {
|
|
10
|
+
vars: Set<string>;
|
|
11
|
+
didEnumerate: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SideEffects {
|
|
15
|
+
fsUsage: FsUsage[];
|
|
16
|
+
envUsage: EnvUsage;
|
|
17
|
+
packageName: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type OriginalMethods = Record<string, any>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Side effect detector using AsyncLocalStorage to track filesystem and environment variable
|
|
24
|
+
* access across concurrent async operations in a Node.js worker thread.
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* const detector = new SideEffectDetector();
|
|
28
|
+
* detector.install();
|
|
29
|
+
*
|
|
30
|
+
* const [result, sideEffects] = await detector.monitorSideEffects(async () => {
|
|
31
|
+
* return await someOperation();
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* console.log(sideEffects.fsUsage); // Array of filesystem accesses
|
|
35
|
+
* console.log(sideEffects.envUsage); // Array of environment variable accesses
|
|
36
|
+
*/
|
|
37
|
+
export class SideEffectDetector {
|
|
38
|
+
private asyncStorage: AsyncLocalStorage<SideEffects>;
|
|
39
|
+
private patchesInstalled: boolean;
|
|
40
|
+
private originalMethods: OriginalMethods;
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
this.asyncStorage = new AsyncLocalStorage<SideEffects>();
|
|
44
|
+
this.patchesInstalled = false;
|
|
45
|
+
this.originalMethods = {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Install global patches for filesystem and environment variable monitoring.
|
|
50
|
+
* This should be called once when the worker starts up.
|
|
51
|
+
*/
|
|
52
|
+
install(): void {
|
|
53
|
+
if (this.patchesInstalled) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this._patchFilesystem();
|
|
58
|
+
this._patchProcessEnv();
|
|
59
|
+
|
|
60
|
+
this.patchesInstalled = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Monitor side effects for an async operation.
|
|
65
|
+
*
|
|
66
|
+
* @param {Function} fn - Async function to monitor
|
|
67
|
+
* @param {Object} options - Optional configuration
|
|
68
|
+
* @param {string} options.label - Optional label for debugging
|
|
69
|
+
* @returns {Promise<[any, SideEffects]>} Tuple of [result, sideEffects]
|
|
70
|
+
*/
|
|
71
|
+
monitorSideEffects<T>(
|
|
72
|
+
packageName: string,
|
|
73
|
+
fn: () => Async<T>,
|
|
74
|
+
): Async<[T, SideEffects]> {
|
|
75
|
+
if (!this.patchesInstalled) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'SideEffectDetector: install() must be called before monitorSideEffects()',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const context: SideEffects = {
|
|
82
|
+
fsUsage: [],
|
|
83
|
+
envUsage: {
|
|
84
|
+
vars: new Set(),
|
|
85
|
+
didEnumerate: false,
|
|
86
|
+
},
|
|
87
|
+
packageName: packageName,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return this.asyncStorage.run(context, async () => {
|
|
91
|
+
const result = await fn();
|
|
92
|
+
|
|
93
|
+
return [result, context] as [T, SideEffects];
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the current monitoring context, if any.
|
|
99
|
+
* Useful for debugging or custom instrumentation.
|
|
100
|
+
*
|
|
101
|
+
* @returns {Object|null} Current context or null if not monitoring
|
|
102
|
+
*/
|
|
103
|
+
getCurrentContext(): SideEffects | null {
|
|
104
|
+
return this.asyncStorage.getStore() || null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if currently monitoring side effects.
|
|
109
|
+
*
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
isMonitoring(): boolean {
|
|
113
|
+
return this.asyncStorage.getStore() !== undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Patch filesystem methods to record access.
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
private _patchFilesystem(): void {
|
|
121
|
+
// Inline require this to avoid babel transformer issue
|
|
122
|
+
const fs = require('fs');
|
|
123
|
+
const methodsToPatch = [
|
|
124
|
+
// Sync methods
|
|
125
|
+
'readFileSync',
|
|
126
|
+
'writeFileSync',
|
|
127
|
+
'appendFileSync',
|
|
128
|
+
'existsSync',
|
|
129
|
+
'statSync',
|
|
130
|
+
'lstatSync',
|
|
131
|
+
'readdirSync',
|
|
132
|
+
'mkdirSync',
|
|
133
|
+
'rmdirSync',
|
|
134
|
+
'unlinkSync',
|
|
135
|
+
'copyFileSync',
|
|
136
|
+
'renameSync',
|
|
137
|
+
'chmodSync',
|
|
138
|
+
'chownSync',
|
|
139
|
+
|
|
140
|
+
// Async methods
|
|
141
|
+
'readFile',
|
|
142
|
+
'writeFile',
|
|
143
|
+
'appendFile',
|
|
144
|
+
'stat',
|
|
145
|
+
'lstat',
|
|
146
|
+
'readdir',
|
|
147
|
+
'mkdir',
|
|
148
|
+
'rmdir',
|
|
149
|
+
'unlink',
|
|
150
|
+
'copyFile',
|
|
151
|
+
'rename',
|
|
152
|
+
'chmod',
|
|
153
|
+
'chown',
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
methodsToPatch.forEach((method) => {
|
|
157
|
+
if (typeof fs[method] === 'function') {
|
|
158
|
+
this.originalMethods[method] = fs[method];
|
|
159
|
+
const self = this;
|
|
160
|
+
|
|
161
|
+
// @ts-expect-error Dynamic method patching
|
|
162
|
+
fs[method] = function (path, ...args) {
|
|
163
|
+
// Record filesystem access in current context
|
|
164
|
+
const context = self.asyncStorage.getStore();
|
|
165
|
+
if (context) {
|
|
166
|
+
context.fsUsage.push({
|
|
167
|
+
method,
|
|
168
|
+
path: typeof path === 'string' ? path : path?.toString(),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return self.originalMethods[method].call(this, path, ...args);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Handle fs.promises methods
|
|
178
|
+
if (fs.promises) {
|
|
179
|
+
const promiseMethodsToPatch = [
|
|
180
|
+
'readFile',
|
|
181
|
+
'writeFile',
|
|
182
|
+
'appendFile',
|
|
183
|
+
'stat',
|
|
184
|
+
'lstat',
|
|
185
|
+
'readdir',
|
|
186
|
+
'mkdir',
|
|
187
|
+
'rmdir',
|
|
188
|
+
'unlink',
|
|
189
|
+
'copyFile',
|
|
190
|
+
'rename',
|
|
191
|
+
'chmod',
|
|
192
|
+
'chown',
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const promises = fs.promises as unknown as Record<
|
|
196
|
+
string,
|
|
197
|
+
(...args: any[]) => any
|
|
198
|
+
>;
|
|
199
|
+
promiseMethodsToPatch.forEach((method) => {
|
|
200
|
+
if (typeof promises[method] === 'function') {
|
|
201
|
+
const originalKey = `promises_${method}`;
|
|
202
|
+
this.originalMethods[originalKey] = promises[method];
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
204
|
+
const self = this;
|
|
205
|
+
|
|
206
|
+
promises[method] = function (path: unknown, ...args: unknown[]) {
|
|
207
|
+
const context = self.asyncStorage.getStore();
|
|
208
|
+
if (context) {
|
|
209
|
+
context.fsUsage.push({
|
|
210
|
+
method: `promises.${method}`,
|
|
211
|
+
path: typeof path === 'string' ? path : String(path),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return self.originalMethods[originalKey].call(this, path, ...args);
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Patch process.env to record environment variable access.
|
|
224
|
+
* @private
|
|
225
|
+
*/
|
|
226
|
+
private _patchProcessEnv(): void {
|
|
227
|
+
if (this.originalMethods.processEnv) {
|
|
228
|
+
return; // Already patched
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
this.originalMethods.processEnv = process.env;
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
233
|
+
const self = this;
|
|
234
|
+
// The following environment variables are allowed to be accessed by transformers
|
|
235
|
+
const allowedVars = new Set([
|
|
236
|
+
'ATLASPACK_ENABLE_SENTRY',
|
|
237
|
+
// TODO we should also add the other atlaspack env vars here
|
|
238
|
+
'NODE_V8_COVERAGE',
|
|
239
|
+
'VSCODE_INSPECTOR_OPTIONS',
|
|
240
|
+
'NODE_INSPECTOR_IPC',
|
|
241
|
+
'FORCE_COLOR',
|
|
242
|
+
'NO_COLOR',
|
|
243
|
+
'TTY',
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
// Create a proxy that intercepts property access
|
|
247
|
+
process.env = new Proxy(this.originalMethods.processEnv, {
|
|
248
|
+
get(target, property) {
|
|
249
|
+
const context = self.asyncStorage.getStore();
|
|
250
|
+
if (context && typeof property === 'string') {
|
|
251
|
+
// Only record if this is a real environment variable access
|
|
252
|
+
// (not internal properties like 'constructor', 'valueOf', etc.)
|
|
253
|
+
if (
|
|
254
|
+
!allowedVars.has(property) &&
|
|
255
|
+
(property in target || !property.startsWith('_'))
|
|
256
|
+
) {
|
|
257
|
+
context.envUsage.vars.add(property);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return target[property];
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
set(target, property, value) {
|
|
264
|
+
const context = self.asyncStorage.getStore();
|
|
265
|
+
if (context && typeof property === 'string') {
|
|
266
|
+
if (!allowedVars.has(property) && property in target) {
|
|
267
|
+
context.envUsage.vars.add(property);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
target[property] = value;
|
|
271
|
+
return true;
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
has(target, property) {
|
|
275
|
+
const context = self.asyncStorage.getStore();
|
|
276
|
+
if (context && typeof property === 'string') {
|
|
277
|
+
if (!allowedVars.has(property) && property in target) {
|
|
278
|
+
context.envUsage.vars.add(property);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return property in target;
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
ownKeys(target) {
|
|
285
|
+
const context = self.asyncStorage.getStore();
|
|
286
|
+
if (context) {
|
|
287
|
+
context.envUsage.didEnumerate = true;
|
|
288
|
+
}
|
|
289
|
+
return Object.keys(target);
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Default instance for convenience. Most workers will only need one detector.
|
|
297
|
+
*/
|
|
298
|
+
export const defaultDetector = new SideEffectDetector();
|