@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/Atlaspack.js +2 -0
  3. package/dist/Transformation.js +24 -1
  4. package/dist/atlaspack-v3/AtlaspackV3.js +3 -0
  5. package/dist/atlaspack-v3/NapiWorkerPool.js +10 -0
  6. package/dist/atlaspack-v3/worker/compat/plugin-config.js +10 -22
  7. package/dist/atlaspack-v3/worker/compat/plugin-options.js +15 -0
  8. package/dist/atlaspack-v3/worker/side-effect-detector.js +243 -0
  9. package/dist/atlaspack-v3/worker/worker.js +140 -66
  10. package/dist/requests/ConfigRequest.js +24 -0
  11. package/lib/Atlaspack.js +5 -1
  12. package/lib/Transformation.js +31 -1
  13. package/lib/atlaspack-v3/AtlaspackV3.js +3 -0
  14. package/lib/atlaspack-v3/NapiWorkerPool.js +10 -0
  15. package/lib/atlaspack-v3/worker/compat/plugin-config.js +8 -27
  16. package/lib/atlaspack-v3/worker/compat/plugin-options.js +15 -0
  17. package/lib/atlaspack-v3/worker/side-effect-detector.js +215 -0
  18. package/lib/atlaspack-v3/worker/worker.js +152 -72
  19. package/lib/requests/ConfigRequest.js +25 -0
  20. package/lib/types/InternalConfig.d.ts +1 -2
  21. package/lib/types/atlaspack-v3/AtlaspackV3.d.ts +2 -1
  22. package/lib/types/atlaspack-v3/NapiWorkerPool.d.ts +1 -0
  23. package/lib/types/atlaspack-v3/index.d.ts +1 -0
  24. package/lib/types/atlaspack-v3/worker/compat/plugin-config.d.ts +3 -11
  25. package/lib/types/atlaspack-v3/worker/compat/plugin-options.d.ts +1 -0
  26. package/lib/types/atlaspack-v3/worker/side-effect-detector.d.ts +76 -0
  27. package/lib/types/atlaspack-v3/worker/worker.d.ts +26 -6
  28. package/lib/types/requests/ConfigRequest.d.ts +9 -1
  29. package/package.json +14 -14
  30. package/src/Atlaspack.ts +2 -0
  31. package/src/InternalConfig.ts +1 -1
  32. package/src/Transformation.ts +37 -2
  33. package/src/atlaspack-v3/AtlaspackV3.ts +8 -0
  34. package/src/atlaspack-v3/NapiWorkerPool.ts +17 -0
  35. package/src/atlaspack-v3/index.ts +1 -0
  36. package/src/atlaspack-v3/worker/compat/plugin-config.ts +8 -40
  37. package/src/atlaspack-v3/worker/compat/plugin-options.ts +15 -0
  38. package/src/atlaspack-v3/worker/side-effect-detector.ts +298 -0
  39. package/src/atlaspack-v3/worker/worker.ts +288 -172
  40. package/src/requests/ConfigRequest.ts +39 -0
  41. 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();