@volter/tabnode 0.5.27 → 0.5.29
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/dist/assets/{runtime-worker-DPvtXRd8.js → runtime-worker-Dv_vTkXr.js} +202 -93
- package/dist/index.mjs +203 -94
- package/dist/shims/async_hooks.d.ts +12 -21
- package/package.json +1 -1
- package/src/runtime.ts +157 -9
- package/src/shims/async_hooks.ts +83 -83
- package/src/shims/child_process.ts +31 -6
package/src/runtime.ts
CHANGED
|
@@ -117,6 +117,29 @@ function __substrateGuestConstructor(Constructor: unknown, process: Process): un
|
|
|
117
117
|
return __substrateFunctionScope(__substrateGuestGlobal(process), String(compiled));
|
|
118
118
|
} });
|
|
119
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* `vm.runInThisContext` runs its script in the calling process's global scope,
|
|
122
|
+
* as Node's does. The shim evaluated it in the host's, so a script saw the
|
|
123
|
+
* worker's own timers: jiti evaluates every module it loads this way, and
|
|
124
|
+
* pi-mcp-adapter's `setInterval(...).unref()` threw "unref is not a function"
|
|
125
|
+
* because the browser's `setInterval` returns a number. The script runs inside
|
|
126
|
+
* the process's guest global, as a guest `Function` does
|
|
127
|
+
* (`__substrateGuestConstructor`).
|
|
128
|
+
*/
|
|
129
|
+
const __substrateScriptScope = Function("__substrateScriptGlobals", "__substrateScriptSource", "with (__substrateScriptGlobals) { return eval(__substrateScriptSource); }");
|
|
130
|
+
const __substrateGuestVms = new WeakMap<Process, unknown>();
|
|
131
|
+
function __substrateGuestVm(process: Process): unknown {
|
|
132
|
+
let vm = __substrateGuestVms.get(process);
|
|
133
|
+
if (vm) return vm;
|
|
134
|
+
const run = (code: string): unknown => __substrateScriptScope(__substrateGuestGlobal(process), String(code));
|
|
135
|
+
class Script extends vmShim.Script {
|
|
136
|
+
override runInThisContext(_options?: object): unknown { return run((this as unknown as { code: string }).code); }
|
|
137
|
+
}
|
|
138
|
+
vm = { ...vmShim, Script, runInThisContext: (code: string, _options?: object) => run(code), default: undefined };
|
|
139
|
+
(vm as { default: unknown }).default = vm;
|
|
140
|
+
__substrateGuestVms.set(process, vm);
|
|
141
|
+
return vm;
|
|
142
|
+
}
|
|
120
143
|
/**
|
|
121
144
|
* A `.ts`, `.mts` or `.cts` file runs with its types stripped, as Node runs
|
|
122
145
|
* one since 22.18, before the module's imports are read -- the entry a `node`
|
|
@@ -413,6 +436,97 @@ function collectDirectEval(node: any, replacements: Array<[number, number, strin
|
|
|
413
436
|
replacements.push([source.end, source.end, ")"]);
|
|
414
437
|
}
|
|
415
438
|
|
|
439
|
+
/**
|
|
440
|
+
* Node's AsyncLocalStorage context follows an `await`; in a tab nothing can
|
|
441
|
+
* observe one (see shims/async_hooks), so the body follows it itself. An async
|
|
442
|
+
* function takes its frame on entry and puts it back wherever it resumes:
|
|
443
|
+
* after each `await`, at the start of a `catch` or `finally` (a rejected
|
|
444
|
+
* `await` resumes there), and in and after a `for await`. An async arrow with
|
|
445
|
+
* an expression body takes the frame at each `await`, which is the same
|
|
446
|
+
* frame. Playwright names each API call from a zone it keeps in one; a zone
|
|
447
|
+
* left over from another call made its errors lose `locator.click:`. Only
|
|
448
|
+
* insertions, and no line breaks, so a stack frame keeps its line.
|
|
449
|
+
*/
|
|
450
|
+
const ASYNC_FRAME = '__substrateAsyncFrame';
|
|
451
|
+
// Every fragment the pass inserts is one of these strings or ends in the
|
|
452
|
+
// marker, so a function's source can be given back as it was written (below).
|
|
453
|
+
const FOLLOW_MARK = '/*substrate*/';
|
|
454
|
+
const FOLLOW_TAKE = `const ${ASYNC_FRAME}=__substrateContext();`;
|
|
455
|
+
const FOLLOW_RESTORE = `__substrateRestore(${ASYNC_FRAME});`;
|
|
456
|
+
const FOLLOW_RESUME_FRAMED = `__substrateResume(${ASYNC_FRAME},`;
|
|
457
|
+
const FOLLOW_RESUME_TAKEN = '__substrateResume(__substrateContext(),';
|
|
458
|
+
function __substrateFollowAwaits(code: string): string {
|
|
459
|
+
if (!/\bawait\b/.test(code)) return code;
|
|
460
|
+
let ast;
|
|
461
|
+
try {
|
|
462
|
+
ast = acorn.parse(code, { ecmaVersion: "latest", sourceType: "script", allowReturnOutsideFunction: true, allowHashBang: true, allowAwaitOutsideFunction: true });
|
|
463
|
+
} catch {
|
|
464
|
+
return code;
|
|
465
|
+
}
|
|
466
|
+
const edits: Array<[number, number, string]> = [];
|
|
467
|
+
const insert = (at: number, text: string): void => { edits.push([at, at, text]); };
|
|
468
|
+
const isFunction = (node: any): boolean => node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
|
|
469
|
+
// `framed`: the nearest function is async and holds the frame in ASYNC_FRAME.
|
|
470
|
+
const visit = (node: any, framed: boolean, parent: any): void => {
|
|
471
|
+
if (isFunction(node)) {
|
|
472
|
+
framed = node.async && node.body.type === 'BlockStatement';
|
|
473
|
+
if (framed) {
|
|
474
|
+
const directives = node.body.body.filter((statement: any) => statement.directive !== undefined);
|
|
475
|
+
const at = directives.length ? directives[directives.length - 1].end : node.body.start + 1;
|
|
476
|
+
insert(at, FOLLOW_TAKE);
|
|
477
|
+
}
|
|
478
|
+
} else if (node.type === 'AwaitExpression') {
|
|
479
|
+
insert(node.start, framed ? FOLLOW_RESUME_FRAMED : FOLLOW_RESUME_TAKEN);
|
|
480
|
+
insert(node.end, `${FOLLOW_MARK})`);
|
|
481
|
+
} else if (framed && node.type === 'CatchClause') {
|
|
482
|
+
insert(node.body.start + 1, FOLLOW_RESTORE);
|
|
483
|
+
} else if (framed && node.type === 'TryStatement' && node.finalizer) {
|
|
484
|
+
insert(node.finalizer.start + 1, FOLLOW_RESTORE);
|
|
485
|
+
} else if (framed && node.type === 'ForOfStatement' && node.await) {
|
|
486
|
+
// The loop's own wrap first: where its end is the body's, an insertion
|
|
487
|
+
// made later lands before it, so the body's brace closes inside.
|
|
488
|
+
const whole = parent?.type === 'LabeledStatement' ? parent : node;
|
|
489
|
+
insert(whole.start, `{${FOLLOW_MARK}`);
|
|
490
|
+
insert(whole.end, `;${FOLLOW_RESTORE}${FOLLOW_MARK}}`);
|
|
491
|
+
if (node.body.type === 'BlockStatement') insert(node.body.start + 1, FOLLOW_RESTORE);
|
|
492
|
+
else { insert(node.body.start, `{${FOLLOW_MARK}${FOLLOW_RESTORE}`); insert(node.body.end, `${FOLLOW_MARK}}`); }
|
|
493
|
+
}
|
|
494
|
+
for (const key of Object.keys(node)) {
|
|
495
|
+
if (key === 'type' || key === 'start' || key === 'end' || key === 'loc' || key === 'range') continue;
|
|
496
|
+
const child = node[key];
|
|
497
|
+
if (!child || typeof child !== 'object') continue;
|
|
498
|
+
if (Array.isArray(child)) {
|
|
499
|
+
for (const item of child) if (item && typeof item.type === 'string') visit(item, framed, node);
|
|
500
|
+
} else if (typeof child.type === 'string') visit(child, framed, node);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
visit(ast, false, undefined);
|
|
504
|
+
return edits.length ? applyReplacements(code, edits) : code;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* A function's source as it was written. Playwright sends the source of the
|
|
509
|
+
* function a test evaluates to the browser, where the pass's calls do not
|
|
510
|
+
* exist; the pass only inserts, so removing its fragments gives the text back.
|
|
511
|
+
*/
|
|
512
|
+
function __substrateUnfollowAwaits(source: string): string {
|
|
513
|
+
if (!source.includes('__substrate')) return source;
|
|
514
|
+
return source
|
|
515
|
+
.replaceAll(`;${FOLLOW_RESTORE}${FOLLOW_MARK}}`, '')
|
|
516
|
+
.replaceAll(`{${FOLLOW_MARK}${FOLLOW_RESTORE}`, '')
|
|
517
|
+
.replaceAll(`{${FOLLOW_MARK}`, '')
|
|
518
|
+
.replaceAll(`${FOLLOW_MARK}}`, '')
|
|
519
|
+
.replaceAll(`${FOLLOW_MARK})`, '')
|
|
520
|
+
.replaceAll(FOLLOW_TAKE, '')
|
|
521
|
+
.replaceAll(FOLLOW_RESTORE, '')
|
|
522
|
+
.replaceAll(FOLLOW_RESUME_FRAMED, '')
|
|
523
|
+
.replaceAll(FOLLOW_RESUME_TAKEN, '');
|
|
524
|
+
}
|
|
525
|
+
forGuestRealm(() => {
|
|
526
|
+
const native = Function.prototype.toString;
|
|
527
|
+
takeFromHost(Function.prototype, 'toString', function toString(this: unknown): string { return __substrateUnfollowAwaits(native.call(this)); });
|
|
528
|
+
});
|
|
529
|
+
|
|
416
530
|
/** Applies insertions and replacements from the end, so earlier offsets hold. */
|
|
417
531
|
function applyReplacements(code: string, replacements: Array<[number, number, string]>): string {
|
|
418
532
|
replacements.sort((a, b) => b[0] - a[0] || b[1] - a[1]);
|
|
@@ -425,7 +539,7 @@ Object.defineProperty(globalThis, "__substrateEvalSource", {
|
|
|
425
539
|
configurable: true,
|
|
426
540
|
value: (source: unknown): unknown => typeof source === "string" ? __substrateRewriteDynamicImportsInScript(source) : source,
|
|
427
541
|
});
|
|
428
|
-
/** A file's source made a body: shebang stripped, types stripped, ESM lowered, dynamic imports rewritten. */
|
|
542
|
+
/** A file's source made a body: shebang stripped, types stripped, ESM lowered, dynamic imports rewritten, awaits followed. */
|
|
429
543
|
function __substratePrepareBody(rawCode: string, resolvedPath: string, format: string | undefined, process: unknown): string {
|
|
430
544
|
let code = rawCode;
|
|
431
545
|
|
|
@@ -456,11 +570,15 @@ function __substratePrepareBody(rawCode: string, resolvedPath: string, format: s
|
|
|
456
570
|
// A `.cjs` module skips the ESM transform, and with it the rewrite of
|
|
457
571
|
// `import(...)` to the engine's dynamic import, so its
|
|
458
572
|
// `import("fs/promises")` reached the browser's own import and failed on
|
|
459
|
-
// the bare specifier. The dynamic-import rewrite applies to `.cjs` too
|
|
460
|
-
|
|
573
|
+
// the bare specifier. The dynamic-import rewrite applies to `.cjs` too,
|
|
574
|
+
// from the syntax tree as for every other CommonJS module: the text
|
|
575
|
+
// rewrite also renamed a method called `import`, and jiti's
|
|
576
|
+
// `async import(e,t){...}` became `__dynamicImport`, so `jiti.import` was
|
|
577
|
+
// undefined and every Pi extension failed to load.
|
|
578
|
+
code = __substrateRewriteDynamicImportsInScript(code);
|
|
461
579
|
}
|
|
462
580
|
|
|
463
|
-
return code;
|
|
581
|
+
return __substrateFollowAwaits(code);
|
|
464
582
|
}
|
|
465
583
|
|
|
466
584
|
/**
|
|
@@ -472,7 +590,7 @@ function __substratePrepareBody(rawCode: string, resolvedPath: string, format: s
|
|
|
472
590
|
* place of the passes when the file it read hashes to one.
|
|
473
591
|
*/
|
|
474
592
|
export const PREPARED_MODULES_DIR = '/.tabnode/prepared';
|
|
475
|
-
const PREPARED_MODULES_FORMAT = 'tabnode-prepared-
|
|
593
|
+
const PREPARED_MODULES_FORMAT = 'tabnode-prepared-2';
|
|
476
594
|
/** The name a prepared body goes under: the hash of the file as read, and how it is compiled. Undefined for a file no body is prepared for. */
|
|
477
595
|
export function preparedModuleKey(rawCode: string, resolvedPath: string): string | undefined {
|
|
478
596
|
const extension = /\.(js|cjs|mjs)$/u.exec(resolvedPath)?.[1];
|
|
@@ -726,6 +844,27 @@ async function __substrateImportChain(
|
|
|
726
844
|
return doors.__loadFromURL(resolved.url, loaded.format ?? resolved.format, loaded.source ?? void 0);
|
|
727
845
|
}
|
|
728
846
|
|
|
847
|
+
/**
|
|
848
|
+
* A module's `import.meta`: `url`, `dirname`, `filename` and Node's synchronous
|
|
849
|
+
* `resolve` (Node 20.6+). Pi's extension loader resolves its own packages with
|
|
850
|
+
* `import.meta.resolve(specifier)`, and without it every Pi extension failed to
|
|
851
|
+
* load with "import_meta.resolve is not a function". A relative or URL specifier
|
|
852
|
+
* resolves against the module's URL, a builtin to `node:<name>`, and a bare one
|
|
853
|
+
* through the module's own resolver, the one `import()` uses here. Unlike Node,
|
|
854
|
+
* a relative specifier naming a missing file resolves instead of throwing.
|
|
855
|
+
*/
|
|
856
|
+
function createImportMeta(moduleRequire: RequireFunction, url: string, dirname: string, filename: string): Record<string, unknown> {
|
|
857
|
+
const resolve = (specifier: unknown, parent?: unknown): string => {
|
|
858
|
+
const id = typeof specifier === 'string' ? specifier : String(specifier);
|
|
859
|
+
const base = parent === undefined ? url : String(parent);
|
|
860
|
+
if (id.startsWith('./') || id.startsWith('../') || id.startsWith('/')) return new URL(id, base).href;
|
|
861
|
+
if (/^[a-zA-Z][a-zA-Z\d+.-]*:/.test(id)) return id.startsWith('node:') ? id : new URL(id).href;
|
|
862
|
+
if (Object.prototype.hasOwnProperty.call(builtinModules, id)) return `node:${id}`;
|
|
863
|
+
return 'file://' + moduleRequire.resolve(id);
|
|
864
|
+
};
|
|
865
|
+
return { url, dirname, filename, resolve };
|
|
866
|
+
}
|
|
867
|
+
|
|
729
868
|
function createDynamicImport(moduleRequire: RequireFunction, process: Process, parentURL?: string): (specifier: unknown) => Promise<unknown> {
|
|
730
869
|
return async (specifier: unknown): Promise<unknown> => {
|
|
731
870
|
try {
|
|
@@ -1962,7 +2101,7 @@ function createRequire(
|
|
|
1962
2101
|
dirname,
|
|
1963
2102
|
process,
|
|
1964
2103
|
consoleWrapper,
|
|
1965
|
-
|
|
2104
|
+
createImportMeta(moduleRequire, importMetaUrl, dirname, resolvedPath),
|
|
1966
2105
|
dynamicImport,
|
|
1967
2106
|
__substrateGuestGlobal,
|
|
1968
2107
|
__substrateGuestConstructor
|
|
@@ -2130,6 +2269,11 @@ function createRequire(
|
|
|
2130
2269
|
if (id === 'module') {
|
|
2131
2270
|
return __substrateModule();
|
|
2132
2271
|
}
|
|
2272
|
+
// Ahead of the per-process builtin table: node-lib's own modules require
|
|
2273
|
+
// `vm` first and that table keeps the first answer for the process.
|
|
2274
|
+
if (id === 'vm') {
|
|
2275
|
+
return __substrateGuestVm(process);
|
|
2276
|
+
}
|
|
2133
2277
|
if (id === 'wasi') {
|
|
2134
2278
|
return __substrateGuestWasi(fsShim, process);
|
|
2135
2279
|
}
|
|
@@ -2445,8 +2589,12 @@ export class Runtime {
|
|
|
2445
2589
|
// A `.cjs` module skips the ESM transform, and with it the rewrite of
|
|
2446
2590
|
// `import(...)` to the engine's dynamic import, so its
|
|
2447
2591
|
// `import("fs/promises")` reached the browser's own import and failed on
|
|
2448
|
-
// the bare specifier. The dynamic-import rewrite applies to `.cjs` too
|
|
2449
|
-
|
|
2592
|
+
// the bare specifier. The dynamic-import rewrite applies to `.cjs` too,
|
|
2593
|
+
// from the syntax tree as for every other CommonJS module: the text
|
|
2594
|
+
// rewrite also renamed a method called `import`, and jiti's
|
|
2595
|
+
// `async import(e,t){...}` became `__dynamicImport`, so `jiti.import` was
|
|
2596
|
+
// undefined and every Pi extension failed to load.
|
|
2597
|
+
code = __substrateRewriteDynamicImportsInScript(code);
|
|
2450
2598
|
}
|
|
2451
2599
|
|
|
2452
2600
|
// Execute code
|
|
@@ -2492,7 +2640,7 @@ export class Runtime {
|
|
|
2492
2640
|
dirname,
|
|
2493
2641
|
this.process,
|
|
2494
2642
|
consoleWrapper,
|
|
2495
|
-
|
|
2643
|
+
createImportMeta(require, importMetaUrl, dirname, filename),
|
|
2496
2644
|
dynamicImport,
|
|
2497
2645
|
__substrateGuestGlobal,
|
|
2498
2646
|
__substrateGuestConstructor
|
package/src/shims/async_hooks.ts
CHANGED
|
@@ -1,109 +1,105 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* async_hooks shim
|
|
2
|
+
* async_hooks shim: AsyncLocalStorage and AsyncResource carry context the way
|
|
3
|
+
* Node's do; the hooks themselves are not available in a browser.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import { forGuestRealm, takeFromHost } from '../host-globals';
|
|
6
7
|
|
|
7
|
-
export class AsyncResource {
|
|
8
|
-
constructor(_type: string, _options?: object) {}
|
|
9
|
-
|
|
10
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
-
runInAsyncScope<T>(fn: (...args: any[]) => T, thisArg?: unknown, ...args: any[]): T {
|
|
12
|
-
return fn.apply(thisArg, args);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
emitDestroy(): this { return this; }
|
|
16
|
-
asyncId(): number { return 0; }
|
|
17
|
-
triggerAsyncId(): number { return 0; }
|
|
18
|
-
|
|
19
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
-
static bind<T extends (...args: any[]) => any>(fn: T, _type?: string): T {
|
|
21
|
-
return fn;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
8
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
9
|
+
* The context a continuation runs in: every storage's store, as one frame.
|
|
10
|
+
* `run` makes a new frame for its callback and puts the old one back when the
|
|
11
|
+
* callback returns, as Node's does; a frame is never changed after it is
|
|
12
|
+
* made, so a snapshot is the frame itself.
|
|
13
|
+
*
|
|
14
|
+
* A tab has no async hooks and no AsyncContext, and an `await` of a native
|
|
15
|
+
* promise bypasses any `then` a patch installs. The engine follows it at the
|
|
16
|
+
* source instead: each module body it compiles captures its async function's
|
|
17
|
+
* frame on entry and restores it where the function resumes (after each
|
|
18
|
+
* `await`, in a `catch` or `finally`, around a `for await`), through
|
|
19
|
+
* `__substrateResume` and `__substrateRestore` below. A continuation's frame
|
|
20
|
+
* holds only for the turn it runs in; the next turn starts from the root, as
|
|
21
|
+
* a macrotask of Node's does. Code the engine did not compile (its own) runs
|
|
22
|
+
* in whichever frame is current when it is called.
|
|
32
23
|
*/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
type ContextFrame = ReadonlyMap<AsyncLocalStorage<unknown>, unknown>;
|
|
25
|
+
const ROOT_FRAME: ContextFrame = new Map();
|
|
26
|
+
let currentFrame: ContextFrame = ROOT_FRAME;
|
|
27
|
+
const nativeQueueMicrotask = globalThis.queueMicrotask.bind(globalThis);
|
|
28
|
+
let resetQueued = false;
|
|
29
|
+
const resetFrame = (): void => { resetQueued = false; currentFrame = ROOT_FRAME; };
|
|
30
|
+
/** A resumed continuation's frame, current until the turn it runs in ends. */
|
|
31
|
+
const resumeFrame = (frame: ContextFrame): void => {
|
|
32
|
+
if (frame === currentFrame) return;
|
|
33
|
+
currentFrame = frame;
|
|
34
|
+
if (!resetQueued) { resetQueued = true; nativeQueueMicrotask(resetFrame); }
|
|
35
|
+
};
|
|
36
|
+
const withFrame = <R>(frame: ContextFrame, callback: () => R): R => {
|
|
37
|
+
const previous = currentFrame;
|
|
38
|
+
currentFrame = frame;
|
|
39
|
+
try {
|
|
40
|
+
return callback();
|
|
41
|
+
} finally {
|
|
42
|
+
currentFrame = previous;
|
|
40
43
|
}
|
|
44
|
+
};
|
|
41
45
|
|
|
46
|
+
export class AsyncLocalStorage<T> {
|
|
42
47
|
static snapshot() {
|
|
43
|
-
const captured =
|
|
44
|
-
|
|
45
|
-
return (callback: (...args: unknown[]) => unknown, ...args: unknown[]) => {
|
|
46
|
-
const previous = new Map<AsyncLocalStorage<unknown>, unknown>();
|
|
47
|
-
for (const [storage, store] of captured) { previous.set(storage, storage.store); storage.store = store; }
|
|
48
|
-
try {
|
|
49
|
-
return callback(...args);
|
|
50
|
-
} finally {
|
|
51
|
-
for (const [storage, store] of previous) storage.store = store;
|
|
52
|
-
}
|
|
53
|
-
};
|
|
48
|
+
const captured = currentFrame;
|
|
49
|
+
return (callback: (...args: unknown[]) => unknown, ...args: unknown[]) => withFrame(captured, () => callback(...args));
|
|
54
50
|
}
|
|
55
51
|
static bind(callback: (...args: unknown[]) => unknown) {
|
|
56
52
|
const snapshot = AsyncLocalStorage.snapshot();
|
|
57
53
|
return (...args: unknown[]) => snapshot(callback, ...args);
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
disable(): void {
|
|
56
|
+
disable(): void {
|
|
57
|
+
if (!currentFrame.has(this as AsyncLocalStorage<unknown>)) return;
|
|
58
|
+
const next = new Map(currentFrame);
|
|
59
|
+
next.delete(this as AsyncLocalStorage<unknown>);
|
|
60
|
+
currentFrame = next;
|
|
61
|
+
}
|
|
61
62
|
|
|
62
63
|
getStore(): T | undefined {
|
|
63
|
-
return this
|
|
64
|
+
return currentFrame.get(this as AsyncLocalStorage<unknown>) as T | undefined;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
/**
|
|
67
|
-
* A store entered for an async callback used to be left the moment the
|
|
68
|
-
* callback returned its promise, so the first `await` inside a Server Action
|
|
69
|
-
* lost Next's request store and `cookies()` was "outside a request scope". A
|
|
70
|
-
* tab has no async_hooks and no AsyncContext, and an `await` of a native
|
|
71
|
-
* promise bypasses any `then` a patch could install, so the continuations
|
|
72
|
-
* cannot be followed. What can be done honestly is keep the store current
|
|
73
|
-
* until the callback's promise settles: exact for one run at a time, and
|
|
74
|
-
* last-entered-wins where two async runs overlap.
|
|
75
|
-
*/
|
|
76
67
|
run<R>(store: T, callback: (...args: unknown[]) => R, ...args: unknown[]): R {
|
|
77
|
-
const
|
|
78
|
-
this
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
result = callback(...args);
|
|
82
|
-
} catch (error) {
|
|
83
|
-
this.store = prev;
|
|
84
|
-
throw error;
|
|
85
|
-
}
|
|
86
|
-
if (result && typeof (result as { then?: unknown }).then === 'function') {
|
|
87
|
-
const restore = () => { if (this.store === store) this.store = prev; };
|
|
88
|
-
(result as unknown as Promise<unknown>).then(restore, restore);
|
|
89
|
-
return result;
|
|
90
|
-
}
|
|
91
|
-
this.store = prev;
|
|
92
|
-
return result;
|
|
68
|
+
const next = new Map(currentFrame);
|
|
69
|
+
next.set(this as AsyncLocalStorage<unknown>, store);
|
|
70
|
+
return withFrame(next, () => callback(...args));
|
|
93
71
|
}
|
|
94
72
|
|
|
95
73
|
exit<R>(callback: () => R): R {
|
|
96
|
-
const
|
|
97
|
-
this
|
|
98
|
-
|
|
99
|
-
return callback();
|
|
100
|
-
} finally {
|
|
101
|
-
this.store = prev;
|
|
102
|
-
}
|
|
74
|
+
const next = new Map(currentFrame);
|
|
75
|
+
next.delete(this as AsyncLocalStorage<unknown>);
|
|
76
|
+
return withFrame(next, callback);
|
|
103
77
|
}
|
|
104
78
|
|
|
105
79
|
enterWith(store: T): void {
|
|
106
|
-
|
|
80
|
+
const next = new Map(currentFrame);
|
|
81
|
+
next.set(this as AsyncLocalStorage<unknown>, store);
|
|
82
|
+
currentFrame = next;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Runs its callbacks in the context it was made in, as Node's does. */
|
|
87
|
+
export class AsyncResource {
|
|
88
|
+
private readonly frame = currentFrame;
|
|
89
|
+
constructor(_type: string, _options?: object) {}
|
|
90
|
+
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
runInAsyncScope<T>(fn: (...args: any[]) => T, thisArg?: unknown, ...args: any[]): T {
|
|
93
|
+
return withFrame(this.frame, () => fn.apply(thisArg, args));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
emitDestroy(): this { return this; }
|
|
97
|
+
asyncId(): number { return 0; }
|
|
98
|
+
triggerAsyncId(): number { return 0; }
|
|
99
|
+
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
static bind<T extends (...args: any[]) => any>(fn: T, _type?: string): T {
|
|
102
|
+
return AsyncLocalStorage.bind(fn) as T;
|
|
107
103
|
}
|
|
108
104
|
}
|
|
109
105
|
|
|
@@ -112,10 +108,10 @@ export class AsyncLocalStorage<T> {
|
|
|
112
108
|
* schedules: a timer, an immediate, a microtask, a nextTick, a promise
|
|
113
109
|
* callback. A tab has no async hooks, so each of those is wrapped once to
|
|
114
110
|
* capture the stores current when it was scheduled and restore them when it
|
|
115
|
-
* runs. A native `await` is the one continuation this cannot follow
|
|
116
|
-
*
|
|
117
|
-
* server renderer schedules its work on immediates and promises, and
|
|
118
|
-
* Next's request store through them.
|
|
111
|
+
* runs. A native `await` is the one continuation this cannot follow: the
|
|
112
|
+
* compiled module body follows it, through the three globals installed here.
|
|
113
|
+
* React's server renderer schedules its work on immediates and promises, and
|
|
114
|
+
* read Next's request store through them.
|
|
119
115
|
*
|
|
120
116
|
* The wrapping is a guest's, not the realm's. It used to run at load, so
|
|
121
117
|
* importing the engine into a Node process replaced that process's own
|
|
@@ -145,6 +141,10 @@ forGuestRealm(() => {
|
|
|
145
141
|
): Promise<TFulfilled | TRejected> { return then.call(this, carried(onFulfilled), carried(onRejected)) as Promise<TFulfilled | TRejected>; };
|
|
146
142
|
takeFromHost(Promise.prototype, 'then', carriedThen);
|
|
147
143
|
takeFromHost(globalThis, '__substrateCarried', carried);
|
|
144
|
+
// An async function's frame, taken on entry; put back where it resumes.
|
|
145
|
+
takeFromHost(globalThis, '__substrateContext', () => currentFrame);
|
|
146
|
+
takeFromHost(globalThis, '__substrateResume', <V>(frame: ContextFrame, value: V): V => { resumeFrame(frame); return value; });
|
|
147
|
+
takeFromHost(globalThis, '__substrateRestore', (frame: ContextFrame): void => { resumeFrame(frame); });
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
export interface AsyncHook {
|
|
@@ -1143,7 +1143,26 @@ function readPackageJson(cwd: string): { pkgJson: PackageJson; error?: undefined
|
|
|
1143
1143
|
* Handle `npm run [script]` — execute a script from package.json
|
|
1144
1144
|
*/
|
|
1145
1145
|
async function handleNpmRun(args: string[], ctx: CommandContext): Promise<JustBashExecResult> {
|
|
1146
|
-
|
|
1146
|
+
// npm's own options come before the script's name and are npm's
|
|
1147
|
+
// (`npm run --silent vgai -- mcp .`, as a project's `.mcp.json` launches its
|
|
1148
|
+
// server); what follows the name goes to the script, everything after `--`
|
|
1149
|
+
// untouched. The name used to be `args[0]`, so `--silent` was looked up as a
|
|
1150
|
+
// script, and the script's own arguments were dropped.
|
|
1151
|
+
let scriptName: string | undefined;
|
|
1152
|
+
let silent = false;
|
|
1153
|
+
let ifPresent = false;
|
|
1154
|
+
const passed: string[] = [];
|
|
1155
|
+
for (let index = 0; index < args.length; index++) {
|
|
1156
|
+
const arg = args[index]!;
|
|
1157
|
+
if (arg === '--') { if (scriptName !== undefined) passed.push(...args.slice(index + 1)); break; }
|
|
1158
|
+
if (arg.startsWith('-') && arg !== '-') {
|
|
1159
|
+
if (arg === '--silent' || arg === '-s' || arg === '--quiet' || arg === '-q' || arg === '--loglevel=silent') silent = true;
|
|
1160
|
+
else if (arg === '--if-present') ifPresent = true;
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
if (scriptName === undefined) scriptName = arg;
|
|
1164
|
+
else passed.push(arg);
|
|
1165
|
+
}
|
|
1147
1166
|
|
|
1148
1167
|
// "npm run" with no script name: list available scripts
|
|
1149
1168
|
if (!scriptName) {
|
|
@@ -1158,6 +1177,7 @@ async function handleNpmRun(args: string[], ctx: CommandContext): Promise<JustBa
|
|
|
1158
1177
|
const scriptCommand = scripts[scriptName];
|
|
1159
1178
|
|
|
1160
1179
|
if (!scriptCommand) {
|
|
1180
|
+
if (ifPresent) return { stdout: '', stderr: '', exitCode: 0 };
|
|
1161
1181
|
const available = Object.keys(scripts);
|
|
1162
1182
|
let msg = `npm ERR! Missing script: "${scriptName}"\n`;
|
|
1163
1183
|
if (available.length > 0) {
|
|
@@ -1193,7 +1213,7 @@ async function handleNpmRun(args: string[], ctx: CommandContext): Promise<JustBa
|
|
|
1193
1213
|
// Run pre<script> if it exists
|
|
1194
1214
|
const preScript = scripts[`pre${scriptName}`];
|
|
1195
1215
|
if (preScript) {
|
|
1196
|
-
allStderr += `\n> ${label} pre${scriptName}\n> ${preScript}\n\n`;
|
|
1216
|
+
if (!silent) allStderr += `\n> ${label} pre${scriptName}\n> ${preScript}\n\n`;
|
|
1197
1217
|
const preResult = await ctx.exec(preScript, { cwd: ctx.cwd, env: npmEnv });
|
|
1198
1218
|
allStdout += preResult.stdout;
|
|
1199
1219
|
allStderr += preResult.stderr;
|
|
@@ -1202,9 +1222,14 @@ async function handleNpmRun(args: string[], ctx: CommandContext): Promise<JustBa
|
|
|
1202
1222
|
}
|
|
1203
1223
|
}
|
|
1204
1224
|
|
|
1205
|
-
// Run the main script
|
|
1206
|
-
|
|
1207
|
-
|
|
1225
|
+
// Run the main script, with the arguments npm passes it, each quoted.
|
|
1226
|
+
const command = passed.length > 0
|
|
1227
|
+
? `${scriptCommand} ${passed.map((arg) => /^[\w@%+=:,./-]+$/.test(arg) ? arg : `'${arg.replace(/'/g, `'\\''`)}'`).join(' ')}`
|
|
1228
|
+
: scriptCommand;
|
|
1229
|
+
if (!silent) allStderr += `\n> ${label} ${scriptName}\n> ${command}\n\n`;
|
|
1230
|
+
// The script reads npm's standard input: a `.mcp.json` server launched as
|
|
1231
|
+
// `npm run … -- serve` speaks JSON-RPC on it.
|
|
1232
|
+
const mainResult = await ctx.exec(command, { cwd: ctx.cwd, env: npmEnv, ...(typeof ctx.stdin === 'string' ? { stdin: ctx.stdin } : {}) });
|
|
1208
1233
|
allStdout += mainResult.stdout;
|
|
1209
1234
|
allStderr += mainResult.stderr;
|
|
1210
1235
|
|
|
@@ -1215,7 +1240,7 @@ async function handleNpmRun(args: string[], ctx: CommandContext): Promise<JustBa
|
|
|
1215
1240
|
// Run post<script> if it exists
|
|
1216
1241
|
const postScript = scripts[`post${scriptName}`];
|
|
1217
1242
|
if (postScript) {
|
|
1218
|
-
allStderr += `\n> ${label} post${scriptName}\n> ${postScript}\n\n`;
|
|
1243
|
+
if (!silent) allStderr += `\n> ${label} post${scriptName}\n> ${postScript}\n\n`;
|
|
1219
1244
|
const postResult = await ctx.exec(postScript, { cwd: ctx.cwd, env: npmEnv });
|
|
1220
1245
|
allStdout += postResult.stdout;
|
|
1221
1246
|
allStderr += postResult.stderr;
|