@volter/tabnode 0.5.16 → 0.5.18
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-Bw8vbYik.js → runtime-worker-C-l9pCp3.js} +4374 -365
- package/dist/code-transforms.d.ts +2 -1
- package/dist/index.mjs +4319 -322
- package/dist/node-lib/binding/zlib.d.ts +14 -1
- package/dist/node-lib/internals/amaro.d.ts +31 -0
- package/dist/node-lib/internals/runtime.d.ts +1 -0
- package/dist/node-lib/node-versions.d.ts +1 -0
- package/dist/node-lib/typescript-module.d.ts +11 -0
- package/dist/process-tokens.d.ts +2 -0
- package/dist/shims/perf_hooks.d.ts +0 -4
- package/dist/shims/process.d.ts +3 -0
- package/dist/shims/shell-jobs.d.ts +12 -0
- package/dist/shims/streamed-output.d.ts +14 -0
- package/dist/shims/tls.d.ts +3 -1
- package/dist/shims/worker_threads.d.ts +4 -0
- package/package.json +2 -1
- package/src/code-transforms.ts +28 -13
- package/src/node-lib/binding/zlib.ts +80 -3
- package/src/node-lib/internal/modules/typescript.js +183 -0
- package/src/node-lib/internals/amaro.ts +51 -0
- package/src/node-lib/internals/index.ts +2 -0
- package/src/node-lib/internals/runtime.ts +22 -3
- package/src/node-lib/node-versions.ts +4 -1
- package/src/node-lib/sources.ts +2 -0
- package/src/node-lib/typescript-module.ts +50 -0
- package/src/process-tokens.ts +6 -0
- package/src/runtime.ts +97 -19
- package/src/shims/child_process.ts +41 -13
- package/src/shims/esbuild.ts +12 -1
- package/src/shims/perf_hooks.ts +18 -0
- package/src/shims/process.ts +2 -2
- package/src/shims/shell-jobs.ts +451 -0
- package/src/shims/streamed-output.ts +25 -0
- package/src/shims/string_decoder.ts +5 -0
- package/src/shims/tls.ts +13 -1
- package/src/shims/worker_threads.ts +19 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `internal/deps/amaro/dist/index`: the type stripper Node carries.
|
|
3
|
+
*
|
|
4
|
+
* Node v22.18.0 runs a `.ts`, `.mts` or `.cts` file by erasing its types with
|
|
5
|
+
* amaro 1.1.0 (swc's stripper, built to WebAssembly), vendored under
|
|
6
|
+
* `deps/amaro`, and its own `lib/internal/modules/typescript.js` (vendored
|
|
7
|
+
* here unmodified) is what calls it. amaro is not native: it is JavaScript and
|
|
8
|
+
* WebAssembly, and this engine takes the same release as a dependency rather
|
|
9
|
+
* than writing a stripper of its own. esbuild, which the engine also carries,
|
|
10
|
+
* is not a substitute: it reprints the program (a stack's line and column move)
|
|
11
|
+
* and it transforms `enum`, `namespace` and parameter properties where Node, in
|
|
12
|
+
* strip-only mode, refuses them.
|
|
13
|
+
*
|
|
14
|
+
* amaro's glue loads its WebAssembly synchronously from bytes inlined in the
|
|
15
|
+
* package and reads `util` and `buffer`, so it is loaded through the host's own
|
|
16
|
+
* `require` where the host is Node. A browser realm has no such door: there the
|
|
17
|
+
* page supplies its stripper as `globalThis.__substrateStripTypes`, which the
|
|
18
|
+
* module loader asks first, and this answers absent.
|
|
19
|
+
*/
|
|
20
|
+
type AmaroTransform = (source: string, options: Record<string, unknown>) => { code: string; map?: string };
|
|
21
|
+
interface Amaro { transformSync: AmaroTransform }
|
|
22
|
+
|
|
23
|
+
/** The host's `process`, taken before a guest's takes the global name. */
|
|
24
|
+
const hostProcess = typeof process !== 'undefined' && process !== null
|
|
25
|
+
? (process as unknown as { getBuiltinModule?: (name: string) => unknown })
|
|
26
|
+
: null;
|
|
27
|
+
|
|
28
|
+
let loaded: Amaro | null | undefined;
|
|
29
|
+
|
|
30
|
+
/** amaro, loaded once from beside this engine, or null where the realm cannot load it. */
|
|
31
|
+
export function hostAmaro(): Amaro | null {
|
|
32
|
+
if (loaded !== undefined) return loaded;
|
|
33
|
+
loaded = null;
|
|
34
|
+
try {
|
|
35
|
+
const moduleBuiltin = typeof hostProcess?.getBuiltinModule === 'function'
|
|
36
|
+
? hostProcess.getBuiltinModule('module') as { createRequire?: (from: string) => (id: string) => unknown } | undefined
|
|
37
|
+
: undefined;
|
|
38
|
+
if (moduleBuiltin && typeof moduleBuiltin.createRequire === 'function') {
|
|
39
|
+
const amaro = moduleBuiltin.createRequire(import.meta.url)('amaro') as Amaro;
|
|
40
|
+
if (typeof amaro?.transformSync === 'function') loaded = amaro;
|
|
41
|
+
}
|
|
42
|
+
} catch { /* no host require, or amaro not installed beside the engine */ }
|
|
43
|
+
return loaded;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The module Node's `typescript.js` requires; absent, the error Node raises when built without amaro. */
|
|
47
|
+
export function internalAmaro(): Amaro {
|
|
48
|
+
const amaro = hostAmaro();
|
|
49
|
+
if (amaro) return amaro;
|
|
50
|
+
throw Object.assign(new Error('Node.js is not compiled with TypeScript support'), { code: 'ERR_NO_TYPESCRIPT' });
|
|
51
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
internalAbortController, internalBlob, internalFile, internalWebStreamsAdapters, createWebStreamsAdapters,
|
|
27
27
|
} from './buffer-and-streams';
|
|
28
28
|
import { internalBootstrapRealm, internalUrl, internalEncoding } from './modules';
|
|
29
|
+
import { internalAmaro } from './amaro';
|
|
29
30
|
|
|
30
31
|
/** Built on the first ask, for the reason `./binding/index.ts` gives. */
|
|
31
32
|
// eslint-disable-next-line no-var, vars-on-top
|
|
@@ -69,6 +70,7 @@ export function nodeLibInternal(name: string, require?: (name: string) => any, p
|
|
|
69
70
|
'internal/process/warning': () => internalProcessWarning,
|
|
70
71
|
'internal/source_map/source_map_cache': () => internalSourceMapCache,
|
|
71
72
|
'internal/deps/undici/undici': () => internalUndici,
|
|
73
|
+
'internal/deps/amaro/dist/index': () => internalAmaro(),
|
|
72
74
|
'internal/readline/interface': () => internalReadlineInterface,
|
|
73
75
|
'internal/worker/js_transferable': () => internalJsTransferable,
|
|
74
76
|
};;
|
|
@@ -219,8 +219,9 @@ export const internalEventTarget = {
|
|
|
219
219
|
};
|
|
220
220
|
|
|
221
221
|
/**
|
|
222
|
-
* `internal/options`: the command-line options Node's `net`
|
|
223
|
-
* takes no command line of its own, so each is
|
|
222
|
+
* `internal/options`: the command-line options Node's `net`, `http` and
|
|
223
|
+
* `util` read. The engine takes no command line of its own, so each is
|
|
224
|
+
* Node's own default.
|
|
224
225
|
*/
|
|
225
226
|
const optionValues: Record<string, unknown> = {
|
|
226
227
|
'--network-family-autoselection': true,
|
|
@@ -231,10 +232,28 @@ const optionValues: Record<string, unknown> = {
|
|
|
231
232
|
'--pending-deprecation': false,
|
|
232
233
|
'--report-uncaught-exception': false,
|
|
233
234
|
'--experimental-print-required-tla': false,
|
|
235
|
+
'--max-http-header-size': 16 * 1024,
|
|
236
|
+
'--insecure-http-parser': false,
|
|
237
|
+
'--enable-source-maps': false,
|
|
238
|
+
'--experimental-transform-types': false,
|
|
234
239
|
};
|
|
235
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Options one run was started with, answered only while a call made for that
|
|
243
|
+
* run is on the stack. Node reads an option of its own process; the engine
|
|
244
|
+
* holds many processes in one realm, so a caller that knows whose call it is
|
|
245
|
+
* (the module loader stripping a `.ts` file for a run started with
|
|
246
|
+
* `--experimental-transform-types`) says so for the length of that call.
|
|
247
|
+
*/
|
|
248
|
+
let optionOverrides: Record<string, unknown> | null = null;
|
|
249
|
+
export function withOptionValues<T>(overrides: Record<string, unknown>, fn: () => T): T {
|
|
250
|
+
const previous = optionOverrides;
|
|
251
|
+
optionOverrides = { ...(previous ?? {}), ...overrides };
|
|
252
|
+
try { return fn(); } finally { optionOverrides = previous; }
|
|
253
|
+
}
|
|
254
|
+
|
|
236
255
|
export const internalOptions = {
|
|
237
|
-
getOptionValue: (name: string): unknown => optionValues[name],
|
|
256
|
+
getOptionValue: (name: string): unknown => (optionOverrides !== null && name in optionOverrides ? optionOverrides[name] : optionValues[name]),
|
|
238
257
|
getEmbedderOptions: () => ({ shouldNotRegisterESMLoader: false, noGlobalSearchPaths: false, noBrowserGlobals: false }),
|
|
239
258
|
};
|
|
240
259
|
|
|
@@ -25,10 +25,13 @@ export function nodeVersions(node: string = NODE_LTS_VERSION): {
|
|
|
25
25
|
uv: string;
|
|
26
26
|
webcontainer: string;
|
|
27
27
|
openssl: string;
|
|
28
|
+
amaro: string;
|
|
28
29
|
} {
|
|
29
30
|
// `https.js` calls `assertCrypto()`, which is `!process.versions.openssl`.
|
|
30
31
|
// The engine has crypto (the noble-hashes binding); Node reports openssl
|
|
31
32
|
// whenever it does. A worker compiles `internal/util` against this table
|
|
32
33
|
// before any guest process exists, so the name has to live here.
|
|
33
|
-
|
|
34
|
+
// `amaro` is the type stripper a `.ts` file runs through, the release Node
|
|
35
|
+
// v22.18.0 carries; `internal/util` reads it for `assertTypeScript()`.
|
|
36
|
+
return { node, v8: "11.3.244.8", uv: "1.44.2", webcontainer: "1", openssl: "3.0.15", amaro: "1.1.0" };
|
|
34
37
|
}
|
package/src/node-lib/sources.ts
CHANGED
|
@@ -43,6 +43,7 @@ import INTERNAL_VALIDATORS from './internal/validators.js?raw';
|
|
|
43
43
|
import INTERNAL_ABORT_LISTENER from './internal/events/abort_listener.js?raw';
|
|
44
44
|
import INTERNAL_CHILD_PROCESS_SERIALIZATION from './internal/child_process/serialization.js?raw';
|
|
45
45
|
import INTERNAL_MODULES_CUSTOMIZATION_HOOKS from './internal/modules/customization_hooks.js?raw';
|
|
46
|
+
import INTERNAL_MODULES_TYPESCRIPT from './internal/modules/typescript.js?raw';
|
|
46
47
|
import EVENTS from './events.js?raw';
|
|
47
48
|
import INTERNAL_CONSTANTS from './internal/constants.js?raw';
|
|
48
49
|
import INTERNAL_MIME from './internal/mime.js?raw';
|
|
@@ -142,6 +143,7 @@ export const NODE_LIB_SOURCES: Record<string, string> = {
|
|
|
142
143
|
'internal/events/abort_listener': INTERNAL_ABORT_LISTENER,
|
|
143
144
|
'internal/child_process/serialization': INTERNAL_CHILD_PROCESS_SERIALIZATION,
|
|
144
145
|
'internal/modules/customization_hooks': INTERNAL_MODULES_CUSTOMIZATION_HOOKS,
|
|
146
|
+
'internal/modules/typescript': INTERNAL_MODULES_TYPESCRIPT,
|
|
145
147
|
'events': EVENTS,
|
|
146
148
|
'internal/constants': INTERNAL_CONSTANTS,
|
|
147
149
|
'internal/mime': INTERNAL_MIME,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `.ts`, `.mts` or `.cts` module's types erased the way Node's loader erases
|
|
3
|
+
* them: `Module.prototype._compile` hands the source to
|
|
4
|
+
* `stripTypeScriptModuleTypes` of Node's own `internal/modules/typescript.js`
|
|
5
|
+
* (vendored, unmodified), which runs amaro in strip-only mode -- types become
|
|
6
|
+
* whitespace, so every line and column stays where the file has it -- refuses
|
|
7
|
+
* `enum`, `namespace` with values, parameter properties and the rest with
|
|
8
|
+
* `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`, and refuses any file under
|
|
9
|
+
* `node_modules` with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`.
|
|
10
|
+
*
|
|
11
|
+
* The file is the process's own, from the process's builtin graph: Node reads
|
|
12
|
+
* `--experimental-transform-types` once per process (`getLazy`) and emits its
|
|
13
|
+
* experimental warning on that process, which `--no-warnings` silences. The
|
|
14
|
+
* options a run was started with are answered for the length of the call;
|
|
15
|
+
* `--experimental-transform-types` implies `--enable-source-maps`, as Node's
|
|
16
|
+
* option table says.
|
|
17
|
+
*/
|
|
18
|
+
import { loadNodeLibFor } from './load';
|
|
19
|
+
import { withOptionValues } from './internals/runtime';
|
|
20
|
+
import { hostAmaro } from './internals/amaro';
|
|
21
|
+
|
|
22
|
+
interface TypeScriptModule { stripTypeScriptModuleTypes(source: string, filename: string): string }
|
|
23
|
+
type RunProcess = { execArgv?: readonly string[]; env?: Record<string, string | undefined> };
|
|
24
|
+
|
|
25
|
+
/** Whether this realm can erase types itself: amaro is loadable here. */
|
|
26
|
+
export function canStripTypes(): boolean {
|
|
27
|
+
return hostAmaro() !== null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether a process was started with an option, on its command line or in `NODE_OPTIONS` (both options here are allowed there). */
|
|
31
|
+
function startedWith(process: RunProcess, option: string): boolean {
|
|
32
|
+
if (Array.isArray(process.execArgv) && process.execArgv.includes(option)) return true;
|
|
33
|
+
const nodeOptions = process.env?.NODE_OPTIONS;
|
|
34
|
+
return typeof nodeOptions === 'string' && nodeOptions.split(/\s+/u).includes(option);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Whether a process transforms TypeScript-only syntax rather than refusing it. */
|
|
38
|
+
export function transformsTypes(process: RunProcess): boolean {
|
|
39
|
+
return startedWith(process, '--experimental-transform-types');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The module's source with its types erased (or, under `--experimental-transform-types`, transformed), for this process. */
|
|
43
|
+
export function stripModuleTypes(source: string, filename: string, process: object): string {
|
|
44
|
+
const typescript = loadNodeLibFor(process, 'internal/modules/typescript') as TypeScriptModule;
|
|
45
|
+
const transform = transformsTypes(process as RunProcess);
|
|
46
|
+
return withOptionValues({
|
|
47
|
+
'--experimental-transform-types': transform,
|
|
48
|
+
'--enable-source-maps': transform || startedWith(process as RunProcess, '--enable-source-maps'),
|
|
49
|
+
}, () => typescript.stripTypeScriptModuleTypes(source, filename));
|
|
50
|
+
}
|
package/src/process-tokens.ts
CHANGED
|
@@ -237,6 +237,12 @@ export function forgetRunPid(token: ProcessToken): void {
|
|
|
237
237
|
pidsOfRuns.delete(token);
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/** The named run a pid belongs to in this realm, for a signal sent by number (the shell's `kill`). */
|
|
241
|
+
export function tokenOfPid(pid: number): ProcessToken | null {
|
|
242
|
+
for (const [token, numbers] of pidsOfRuns) if (numbers.pid === pid) return token;
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
240
246
|
/** Whether a live run carries this number, which is what `kill(pid, 0)` asks. */
|
|
241
247
|
export function pidIsLive(pid: number): boolean {
|
|
242
248
|
return processRegistry.lookup(pid) !== undefined;
|
package/src/runtime.ts
CHANGED
|
@@ -66,8 +66,9 @@ import * as domainShim from './shims/domain';
|
|
|
66
66
|
import { createWasiModule, type WasiHostFs, type WasiModule } from './shims/wasi';
|
|
67
67
|
|
|
68
68
|
import { resolve as resolveExports, imports as resolveImports } from 'resolve.exports';
|
|
69
|
-
import { transformEsmToCjsSimple, setNodeLowering, __cjsExports, __substrateHasEsmSyntax } from './code-transforms';
|
|
69
|
+
import { transformEsmToCjsSimple, setNodeLowering, __cjsExports, __cjsObject, __substrateHasEsmSyntax } from './code-transforms';
|
|
70
70
|
import { applySourceEdits } from './source-edits';
|
|
71
|
+
import { canStripTypes, stripModuleTypes, transformsTypes } from './node-lib/typescript-module';
|
|
71
72
|
import * as acorn from 'acorn';
|
|
72
73
|
|
|
73
74
|
/**
|
|
@@ -114,6 +115,31 @@ function __substrateGuestConstructor(Constructor: unknown, process: Process): un
|
|
|
114
115
|
return __substrateFunctionScope(__substrateGuestGlobal(process), String(compiled));
|
|
115
116
|
} });
|
|
116
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* A `.ts`, `.mts` or `.cts` file runs with its types stripped, as Node runs
|
|
120
|
+
* one since 22.18, before the module's imports are read -- the entry a `node`
|
|
121
|
+
* command names and every module it loads alike. A page that loaded a
|
|
122
|
+
* stripper into the realm supplies it; elsewhere the engine erases the types
|
|
123
|
+
* itself, through Node's own `typescript.js` over amaro
|
|
124
|
+
* (`node-lib/typescript-module.ts`): strip-only, positions kept, the syntax
|
|
125
|
+
* Node refuses refused with Node's error. Before this, a realm with no stripper
|
|
126
|
+
* registered -- the engine under Node -- ran the file as JavaScript and died on
|
|
127
|
+
* its first type (`Unexpected identifier 'SlackReply'`), and an entry was never
|
|
128
|
+
* stripped anywhere. A realm that can load neither runs the file as it is.
|
|
129
|
+
*
|
|
130
|
+
* `format` is what a load hook named, and it decides instead of the extension
|
|
131
|
+
* where a hook gave one.
|
|
132
|
+
*/
|
|
133
|
+
function __substrateModuleTypes(code: string, resolvedPath: string, format: string | undefined, process: unknown): string {
|
|
134
|
+
const typescript = format === undefined
|
|
135
|
+
? /[.](?:ts|mts|cts)$/u.test(resolvedPath) && !resolvedPath.endsWith('.d.ts')
|
|
136
|
+
: format === 'typescript' || format === 'commonjs-typescript' || format === 'module-typescript';
|
|
137
|
+
if (!typescript) return code;
|
|
138
|
+
const registered = (globalThis as { __substrateStripTypes?: (code: string, filename: string) => string }).__substrateStripTypes;
|
|
139
|
+
if (typeof registered === 'function') return registered(code, resolvedPath);
|
|
140
|
+
if (!canStripTypes()) return code;
|
|
141
|
+
return stripModuleTypes(code, resolvedPath, process as object);
|
|
142
|
+
}
|
|
117
143
|
/**
|
|
118
144
|
* The name the script of a module's body carries.
|
|
119
145
|
*
|
|
@@ -263,7 +289,12 @@ function __substrateGuestGlobal(process: Process): Record<string, unknown> {
|
|
|
263
289
|
if (key === "fetch") { fetch = value; return true; }
|
|
264
290
|
if (key === "Buffer") { buffer = value; return true; }
|
|
265
291
|
if (shadowed(target, key)) return Reflect.set(target, key, value, target);
|
|
266
|
-
|
|
292
|
+
if (Reflect.set(host, key, value, host)) return true;
|
|
293
|
+
// A global the host holds read-only (a confined realm's \`WebSocket\`) is
|
|
294
|
+
// still the guest's to replace, as Node's globals are: undici's
|
|
295
|
+
// \`install()\` assigns \`globalThis.WebSocket\` and Pi died on the refusal.
|
|
296
|
+
// The value becomes the guest's own binding; the host's is untouched.
|
|
297
|
+
return Reflect.defineProperty(target, key, { value, writable: true, configurable: true, enumerable: true });
|
|
267
298
|
},
|
|
268
299
|
has(target, key) { return isLocalGlobal(key) ? Reflect.has(target, key) : key === "global" || shadowed(target, key) || key in host; },
|
|
269
300
|
ownKeys(target) {
|
|
@@ -448,7 +479,7 @@ function transformEsmToCjsAst(code: string, filename: string): string {
|
|
|
448
479
|
transformed = transformEsmToCjsSimple(transformed);
|
|
449
480
|
|
|
450
481
|
if (hasExportDecl) {
|
|
451
|
-
transformed =
|
|
482
|
+
transformed = `${__cjsObject()}.defineProperty(${__cjsExports()}, "__esModule", { value: true });\n` + transformed;
|
|
452
483
|
}
|
|
453
484
|
}
|
|
454
485
|
|
|
@@ -476,7 +507,7 @@ function transformEsmToCjsRegexFallback(code: string, filename: string): string
|
|
|
476
507
|
if (hasImport || hasExport) {
|
|
477
508
|
transformed = transformEsmToCjsSimple(transformed);
|
|
478
509
|
if (hasExport) {
|
|
479
|
-
transformed =
|
|
510
|
+
transformed = `${__cjsObject()}.defineProperty(${__cjsExports()}, "__esModule", { value: true });\n` + transformed;
|
|
480
511
|
}
|
|
481
512
|
}
|
|
482
513
|
|
|
@@ -1770,7 +1801,7 @@ function createRequire(
|
|
|
1770
1801
|
const prepareModuleCode = (rawCode: string, resolvedPath: string, format?: string): string => {
|
|
1771
1802
|
// Check processed code cache (useful for HMR when module cache is cleared but code hasn't changed)
|
|
1772
1803
|
// Use a simple hash of the content for cache key to handle content changes
|
|
1773
|
-
const codeCacheKey = `${resolvedPath}|${format ?? ''}|${simpleHash(rawCode)}`;
|
|
1804
|
+
const codeCacheKey = `${resolvedPath}|${format ?? ''}|${transformsTypes(process as { execArgv?: string[]; env?: Record<string, string> }) ? 'transform-types|' : ''}${simpleHash(rawCode)}`;
|
|
1774
1805
|
let code = processedCodeCache?.get(codeCacheKey);
|
|
1775
1806
|
|
|
1776
1807
|
if (!code) {
|
|
@@ -1781,16 +1812,7 @@ function createRequire(
|
|
|
1781
1812
|
code = code.slice(code.indexOf('\n') + 1);
|
|
1782
1813
|
}
|
|
1783
1814
|
|
|
1784
|
-
|
|
1785
|
-
// runs one since 22.18: the source goes to the type stripper the realm
|
|
1786
|
-
// loaded, amaro, before the module's imports are read. A realm without one
|
|
1787
|
-
// runs the file as it is.
|
|
1788
|
-
const __typescript = format === undefined
|
|
1789
|
-
? /[.](?:ts|mts|cts)$/u.test(resolvedPath) && !resolvedPath.endsWith('.d.ts')
|
|
1790
|
-
: format === 'typescript' || format === 'commonjs-typescript' || format === 'module-typescript';
|
|
1791
|
-
if (__typescript && typeof (globalThis as any).__substrateStripTypes === 'function') {
|
|
1792
|
-
code = (globalThis as any).__substrateStripTypes(code, resolvedPath) as string;
|
|
1793
|
-
}
|
|
1815
|
+
code = __substrateModuleTypes(code, resolvedPath, format, process);
|
|
1794
1816
|
|
|
1795
1817
|
// Transform ESM to CJS if needed (for .mjs files or ESM that wasn't pre-transformed)
|
|
1796
1818
|
// transformEsmToCjs uses AST to handle import/export, import.meta, and dynamic imports
|
|
@@ -1799,12 +1821,15 @@ function createRequire(
|
|
|
1799
1821
|
? true
|
|
1800
1822
|
: format === 'module' || format === 'module-typescript'
|
|
1801
1823
|
? false
|
|
1802
|
-
: resolvedPath.endsWith('.cjs');
|
|
1824
|
+
: resolvedPath.endsWith('.cjs') || resolvedPath.endsWith('.cts');
|
|
1803
1825
|
if (!__commonjs) {
|
|
1804
1826
|
// An ES module is strict wherever it runs; the mark says so to the
|
|
1805
1827
|
// compile below, which is otherwise sloppy as a CommonJS body is.
|
|
1828
|
+
// The directive shares the body's first line, as Node's one-line
|
|
1829
|
+
// wrapper does, so a frame of an ES module counts the file's own lines;
|
|
1830
|
+
// on a line of its own it put every frame one line below the file's.
|
|
1806
1831
|
const lowered = transformEsmToCjs(code, resolvedPath);
|
|
1807
|
-
code = lowered === code ? code : `${__substrateModuleMarker}"use strict"
|
|
1832
|
+
code = lowered === code ? code : `${__substrateModuleMarker}"use strict";${lowered}`;
|
|
1808
1833
|
} else {
|
|
1809
1834
|
// A `.cjs` module skips the ESM transform, and with it the rewrite of
|
|
1810
1835
|
// `import(...)` to the engine's dynamic import, so its
|
|
@@ -2369,10 +2394,13 @@ export class Runtime {
|
|
|
2369
2394
|
code = code.slice(code.indexOf('\n') + 1);
|
|
2370
2395
|
}
|
|
2371
2396
|
|
|
2397
|
+
code = __substrateModuleTypes(code, filename, undefined, this.process);
|
|
2398
|
+
|
|
2372
2399
|
// Transform ESM to CJS if needed (AST-based, handles import.meta and dynamic imports too)
|
|
2373
|
-
if (!filename.endsWith('.cjs')) {
|
|
2400
|
+
if (!filename.endsWith('.cjs') && !filename.endsWith('.cts')) {
|
|
2374
2401
|
const lowered = transformEsmToCjs(code, filename);
|
|
2375
|
-
|
|
2402
|
+
// The directive shares the body's first line, as `prepareModuleCode`'s does.
|
|
2403
|
+
code = lowered === code ? code : `${__substrateModuleMarker}"use strict";${lowered}`;
|
|
2376
2404
|
} else {
|
|
2377
2405
|
// A `.cjs` module skips the ESM transform, and with it the rewrite of
|
|
2378
2406
|
// `import(...)` to the engine's dynamic import, so its
|
|
@@ -2680,6 +2708,56 @@ forGuestRealm(() => {
|
|
|
2680
2708
|
}
|
|
2681
2709
|
});
|
|
2682
2710
|
|
|
2711
|
+
forGuestRealm(() => {
|
|
2712
|
+
// Node's `stream.finished()` on a web stream waits on a promise Node's own
|
|
2713
|
+
// web streams carry (`nodejs.webstream.isClosedPromise`), settled when the
|
|
2714
|
+
// stream closes or errors, and undici's fetch finalizes every response that
|
|
2715
|
+
// way. The worker's streams are the browser's and carry none, so a stream a
|
|
2716
|
+
// guest constructs is given one, settled by its own controller. The
|
|
2717
|
+
// constructor stays the browser's (a proxy of it), so a stream the platform
|
|
2718
|
+
// made is still `instanceof ReadableStream`.
|
|
2719
|
+
const kIsClosedPromise = Symbol.for('nodejs.webstream.isClosedPromise');
|
|
2720
|
+
const Native = globalThis.ReadableStream;
|
|
2721
|
+
if (typeof Native !== 'function' || (Native as unknown as { __substrateClosedPromise?: boolean }).__substrateClosedPromise) return;
|
|
2722
|
+
type Source = { start?: (controller: unknown) => unknown; pull?: (controller: unknown) => unknown; cancel?: (reason: unknown) => unknown };
|
|
2723
|
+
const Tracked: typeof ReadableStream = new Proxy(Native, {
|
|
2724
|
+
construct(target, args, newTarget): object {
|
|
2725
|
+
let resolve!: () => void;
|
|
2726
|
+
let reject!: (error: unknown) => void;
|
|
2727
|
+
const promise = new Promise<void>((yes, no) => { resolve = yes; reject = no; });
|
|
2728
|
+
promise.then(undefined, () => {});
|
|
2729
|
+
const source = args[0] as Source | null | undefined;
|
|
2730
|
+
const track = (controller: { close?: (...rest: unknown[]) => unknown; error?: (...rest: unknown[]) => unknown }): void => {
|
|
2731
|
+
const close = controller.close;
|
|
2732
|
+
const error = controller.error;
|
|
2733
|
+
if (typeof close === 'function') controller.close = function (this: unknown, ...rest: unknown[]) { const result = close.apply(this, rest); resolve(); return result; };
|
|
2734
|
+
if (typeof error === 'function') controller.error = function (this: unknown, ...rest: unknown[]) { reject(rest[0]); return error.apply(this, rest); };
|
|
2735
|
+
};
|
|
2736
|
+
const failed = (cause: unknown): never => { reject(cause); throw cause; };
|
|
2737
|
+
const wrapped: Source = Object.create(source ?? null);
|
|
2738
|
+
wrapped.start = (controller) => {
|
|
2739
|
+
track(controller as Parameters<typeof track>[0]);
|
|
2740
|
+
try { return source?.start?.call(source, controller); } catch (cause) { return failed(cause); }
|
|
2741
|
+
};
|
|
2742
|
+
if (typeof source?.pull === 'function') {
|
|
2743
|
+
const pull = source.pull;
|
|
2744
|
+
wrapped.pull = (controller) => {
|
|
2745
|
+
try { return Promise.resolve(pull.call(source, controller)).catch(failed); } catch (cause) { return failed(cause); }
|
|
2746
|
+
};
|
|
2747
|
+
}
|
|
2748
|
+
wrapped.cancel = (reason) => {
|
|
2749
|
+
resolve();
|
|
2750
|
+
return source?.cancel?.call(source, reason);
|
|
2751
|
+
};
|
|
2752
|
+
const stream: object = Reflect.construct(target, [wrapped, ...args.slice(1)], newTarget === Tracked ? target : newTarget);
|
|
2753
|
+
Object.defineProperty(stream, kIsClosedPromise, { value: { promise, resolve, reject }, configurable: true });
|
|
2754
|
+
return stream;
|
|
2755
|
+
},
|
|
2756
|
+
});
|
|
2757
|
+
Object.defineProperty(Tracked, '__substrateClosedPromise', { value: true });
|
|
2758
|
+
takeFromHost(globalThis, 'ReadableStream', Tracked);
|
|
2759
|
+
});
|
|
2760
|
+
|
|
2683
2761
|
// Polyfill Error.captureStackTrace/prepareStackTrace for Safari/WebKit
|
|
2684
2762
|
// (V8-specific API used by Express's depd and other npm packages)
|
|
2685
2763
|
forGuestRealm(__substrateStackTracePolyfill);
|
|
@@ -70,6 +70,8 @@ import { Runtime, pendingGuestTimers, stopGuestTimers, __substratePendingOf } fr
|
|
|
70
70
|
import { __nodeResolverFor } from '../node-resolver';
|
|
71
71
|
import { resolve as __resolvePath } from './path';
|
|
72
72
|
import { setSyncChildVfs, warmSyncChild } from './sync-child';
|
|
73
|
+
import { installShellJobs } from './shell-jobs';
|
|
74
|
+
import { unstreamedOutput } from './streamed-output';
|
|
73
75
|
import type { PackageJson } from '../types/package-json';
|
|
74
76
|
|
|
75
77
|
// Capture host capabilities before the guest's authority is restricted. These
|
|
@@ -936,8 +938,10 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
936
938
|
// engine's own registries. A run that still holds a handle or a timer
|
|
937
939
|
// is not that -- Node's loop runs while one is open, for as long as it
|
|
938
940
|
// is open -- and cutting it at a minute ended a forked child that was
|
|
939
|
-
// a server, which is every extension host and every pty host.
|
|
940
|
-
|
|
941
|
+
// a server, which is every extension host and every pty host. Work the
|
|
942
|
+
// host holds for it (a build, a pre-bundle) is seen work too: a dev
|
|
943
|
+
// server whose start passed a minute mid pre-bundle was cut with exit 0.
|
|
944
|
+
if (!isLongRunning && !stillWorking() && heldWork().count === 0 && Date.now() - startTime >= MAX_TOTAL_MS) break;
|
|
941
945
|
}
|
|
942
946
|
|
|
943
947
|
return { stdout, stderr, exitCode: exitCalled ? exitCode : (typeof proc !== 'undefined' && typeof proc.exitCode === 'number' ? proc.exitCode : 0) };
|
|
@@ -1034,6 +1038,9 @@ export function initChildProcess(vfs: VirtualFS): void {
|
|
|
1034
1038
|
},
|
|
1035
1039
|
customCommands: [nodeCommand, npmCommand],
|
|
1036
1040
|
});
|
|
1041
|
+
// A statement ending in `&` runs in the background, with `$!`, `wait` and
|
|
1042
|
+
// `kill` over the engine's process table (`shell-jobs.ts` says how).
|
|
1043
|
+
installShellJobs(bashInstance, { runTokenOf, runStreamsFor, registerRunStreams, releaseRunStreams });
|
|
1037
1044
|
shells.set(tree, { bash: bashInstance, adapter: vfsAdapter });
|
|
1038
1045
|
|
|
1039
1046
|
if (processRunnerInstalled) return;
|
|
@@ -1755,6 +1762,15 @@ function startChildRun(request: RunRequest): StartedRun {
|
|
|
1755
1762
|
const admittedNode = nodeProcessHostInstalled() && engineProgramFor(request.file, request.cwd) === 'node';
|
|
1756
1763
|
const hostTerminal = request.terminal !== undefined && hostExecutor() !== null && !admittedNode;
|
|
1757
1764
|
let wakeInput: (() => void) | undefined;
|
|
1765
|
+
/**
|
|
1766
|
+
* A piped fd 0 that stays open for a program the host runs (a program pack,
|
|
1767
|
+
* not a builtin of the engine's shell): what the parent writes reaches it
|
|
1768
|
+
* as it is written, through the same live stream a terminal gets. A
|
|
1769
|
+
* builtin reads its input once, before it runs; a server that speaks
|
|
1770
|
+
* JSON-RPC over stdio (`supercode harness serve`) read that one string,
|
|
1771
|
+
* saw EOF and exited before its client's first request.
|
|
1772
|
+
*/
|
|
1773
|
+
let liveInput = false;
|
|
1758
1774
|
const hostInput: AsyncIterable<Uint8Array> = {
|
|
1759
1775
|
async *[Symbol.asyncIterator]() {
|
|
1760
1776
|
while (!finished && !controller.signal.aborted) {
|
|
@@ -1794,11 +1810,11 @@ function startChildRun(request: RunRequest): StartedRun {
|
|
|
1794
1810
|
}
|
|
1795
1811
|
};
|
|
1796
1812
|
|
|
1797
|
-
|
|
1798
|
-
|
|
1813
|
+
const streamedOut: string[] = [];
|
|
1814
|
+
const streamedErr: string[] = [];
|
|
1799
1815
|
const streams: RunStreams = {
|
|
1800
|
-
onStdout: (data: string) => { streamedOut
|
|
1801
|
-
onStderr: (data: string) => { streamedErr
|
|
1816
|
+
onStdout: (data: string) => { streamedOut.push(data); request.stdout?.(data); },
|
|
1817
|
+
onStderr: (data: string) => { streamedErr.push(data); request.stderr?.(data); },
|
|
1802
1818
|
signal: controller.signal,
|
|
1803
1819
|
held: false,
|
|
1804
1820
|
stdinOpen: request.stdinIsPipe,
|
|
@@ -1854,6 +1870,12 @@ function startChildRun(request: RunRequest): StartedRun {
|
|
|
1854
1870
|
const begin = (): void => {
|
|
1855
1871
|
if (started || finished) return;
|
|
1856
1872
|
started = true;
|
|
1873
|
+
const engineFirst = !hostTerminal && programExists(request.file, request.cwd, request.env);
|
|
1874
|
+
liveInput = !hostTerminal && !admittedNode && request.stdinIsPipe && !engineFirst;
|
|
1875
|
+
if (liveInput) {
|
|
1876
|
+
pendingStdin.unshift(...initialStdin);
|
|
1877
|
+
initialStdin.length = 0;
|
|
1878
|
+
}
|
|
1857
1879
|
const stdin = initialStdin.length > 0 ? Buffer.concat(initialStdin).toString('utf8') : undefined;
|
|
1858
1880
|
void (async () => {
|
|
1859
1881
|
let outcome: CommandOutcome;
|
|
@@ -1867,11 +1889,11 @@ function startChildRun(request: RunRequest): StartedRun {
|
|
|
1867
1889
|
filesystem: currentVfs!, env, streams, ...(stdin !== undefined ? { stdin } : {}),
|
|
1868
1890
|
}) : await enterRun(token, () => routeCommand({
|
|
1869
1891
|
command: __substrateLineFor(request.file, request.args, request.cwd),
|
|
1870
|
-
engineFirst
|
|
1892
|
+
engineFirst,
|
|
1871
1893
|
cwd: request.cwd,
|
|
1872
1894
|
env: { ...request.env, [PROCESS_TOKEN_ENV]: token },
|
|
1873
1895
|
stdin,
|
|
1874
|
-
...(hostTerminal ? { stdinStream: hostInput, terminal: request.terminal } : {}),
|
|
1896
|
+
...(hostTerminal ? { stdinStream: hostInput, terminal: request.terminal } : liveInput ? { stdinStream: hostInput } : {}),
|
|
1875
1897
|
signal: controller.signal,
|
|
1876
1898
|
onStdout: streams.onStdout,
|
|
1877
1899
|
onStderr: streams.onStderr,
|
|
@@ -1882,9 +1904,12 @@ function startChildRun(request: RunRequest): StartedRun {
|
|
|
1882
1904
|
}
|
|
1883
1905
|
// A command the engine's shell ran to the end and did not stream --
|
|
1884
1906
|
// every builtin -- delivers its output here; the `node` command streamed
|
|
1885
|
-
// its own as it went, and only what it has not already sent is left
|
|
1886
|
-
|
|
1887
|
-
|
|
1907
|
+
// its own as it went, and only what it has not already sent is left
|
|
1908
|
+
// (`streamed-output.ts` says why that is not a cut at a length).
|
|
1909
|
+
const restOut = unstreamedOutput(outcome.stdout, streamedOut);
|
|
1910
|
+
const restErr = unstreamedOutput(outcome.stderr, streamedErr);
|
|
1911
|
+
if (restOut) request.stdout?.(restOut);
|
|
1912
|
+
if (restErr) request.stderr?.(restErr);
|
|
1888
1913
|
end(outcome.exitCode, outcome.signal ?? null);
|
|
1889
1914
|
})();
|
|
1890
1915
|
};
|
|
@@ -1912,18 +1937,20 @@ function startChildRun(request: RunRequest): StartedRun {
|
|
|
1912
1937
|
return 0;
|
|
1913
1938
|
},
|
|
1914
1939
|
writeStdin(bytes: Uint8Array): void | Promise<void> {
|
|
1915
|
-
if (hostTerminal) { pendingStdin.push(bytes); wakeInput?.(); wakeInput = undefined; return; }
|
|
1940
|
+
if (hostTerminal || liveInput) { pendingStdin.push(bytes); wakeInput?.(); wakeInput = undefined; return; }
|
|
1916
1941
|
if (!started) { initialStdin.push(bytes); return; }
|
|
1917
1942
|
pendingStdin.push(bytes);
|
|
1918
1943
|
return flushStdin();
|
|
1919
1944
|
},
|
|
1920
1945
|
endStdin(): void {
|
|
1921
|
-
if (hostTerminal) { pendingStdin.push(null); wakeInput?.(); wakeInput = undefined; return; }
|
|
1946
|
+
if (hostTerminal || liveInput) { pendingStdin.push(null); wakeInput?.(); wakeInput = undefined; return; }
|
|
1922
1947
|
if (!started) {
|
|
1923
1948
|
// The writer closed before the command began: the run's fd 0 is the
|
|
1924
1949
|
// string already collected and nothing more, so it is not held open.
|
|
1925
1950
|
streams.stdinOpen = false;
|
|
1926
1951
|
begin();
|
|
1952
|
+
// A live pipe closed before it began still ends where it was closed.
|
|
1953
|
+
if (liveInput) { pendingStdin.push(null); wakeInput?.(); wakeInput = undefined; }
|
|
1927
1954
|
return;
|
|
1928
1955
|
}
|
|
1929
1956
|
pendingStdin.push(null);
|
|
@@ -1951,3 +1978,4 @@ export default {
|
|
|
1951
1978
|
runCommand,
|
|
1952
1979
|
sendStdin,
|
|
1953
1980
|
};
|
|
1981
|
+
|
package/src/shims/esbuild.ts
CHANGED
|
@@ -398,6 +398,16 @@ export interface EsbuildHost {
|
|
|
398
398
|
}
|
|
399
399
|
let currentHost: EsbuildHost | null = null;
|
|
400
400
|
|
|
401
|
+
/**
|
|
402
|
+
* esbuild-wasm starts its compiler on a Worker unless told not to. A realm
|
|
403
|
+
* that runs guests may withhold `Worker` (the execution worker confines its
|
|
404
|
+
* guests that way), and there the compiler runs on the calling thread, which
|
|
405
|
+
* is esbuild's own `worker: false`.
|
|
406
|
+
*/
|
|
407
|
+
function realmCanStartWorker(): boolean {
|
|
408
|
+
return typeof (globalThis as { Worker?: unknown }).Worker === 'function';
|
|
409
|
+
}
|
|
410
|
+
|
|
401
411
|
let inlineEsbuild: Promise<typeof import('esbuild-wasm')> | null = null;
|
|
402
412
|
function ownEsbuild(): Promise<typeof import('esbuild-wasm')> {
|
|
403
413
|
// A build that brings plugins of its own, a bundler such as tsup does,
|
|
@@ -408,7 +418,7 @@ function ownEsbuild(): Promise<typeof import('esbuild-wasm')> {
|
|
|
408
418
|
/* @vite-ignore */
|
|
409
419
|
esbuildModuleURL
|
|
410
420
|
).then(async (esbuild) => {
|
|
411
|
-
await esbuild.initialize({ wasmURL });
|
|
421
|
+
await esbuild.initialize({ wasmURL, worker: realmCanStartWorker() });
|
|
412
422
|
return esbuild;
|
|
413
423
|
}).catch((cause) => {
|
|
414
424
|
// A load that failed, a page whose server was away for a moment, is not kept: the next build tries again.
|
|
@@ -527,6 +537,7 @@ export async function initialize(options?: { wasmURL?: string }): Promise<void>
|
|
|
527
537
|
|
|
528
538
|
await esbuild.initialize({
|
|
529
539
|
wasmURL: options?.wasmURL || wasmURL,
|
|
540
|
+
worker: realmCanStartWorker(),
|
|
530
541
|
});
|
|
531
542
|
|
|
532
543
|
esbuildInstance = esbuild;
|
package/src/shims/perf_hooks.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* perf_hooks shim - Performance measurement APIs
|
|
3
3
|
* Wraps browser Performance API
|
|
4
4
|
*/
|
|
5
|
+
import { forGuestRealm } from '../host-globals';
|
|
5
6
|
|
|
6
7
|
export const performance = globalThis.performance || {
|
|
7
8
|
now: () => Date.now(),
|
|
@@ -16,6 +17,23 @@ export const performance = globalThis.performance || {
|
|
|
16
17
|
clearResourceTimings: () => {},
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Node's `performance.markResourceTiming`, which a browser's `performance`
|
|
22
|
+
* does not have. undici's fetch reads it off the global `performance` at load
|
|
23
|
+
* and calls it after every response, so each fetch through an undici a
|
|
24
|
+
* program installed (Pi's does) threw once the body was read. Node records a
|
|
25
|
+
* PerformanceResourceTiming entry there; this engine keeps no resource
|
|
26
|
+
* timeline for a guest's own HTTP, so the call records nothing.
|
|
27
|
+
*/
|
|
28
|
+
forGuestRealm(() => {
|
|
29
|
+
const own = globalThis.performance as (Performance & { markResourceTiming?: unknown }) | undefined;
|
|
30
|
+
if (own && typeof own.markResourceTiming !== 'function') {
|
|
31
|
+
try {
|
|
32
|
+
Object.defineProperty(own, 'markResourceTiming', { value: () => undefined, writable: true, configurable: true, enumerable: false });
|
|
33
|
+
} catch { /* a performance object that refuses the name keeps its own shape */ }
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
19
37
|
export class PerformanceObserver {
|
|
20
38
|
private callback: (list: PerformanceObserverEntryList) => void;
|
|
21
39
|
private entryTypes: string[] = [];
|
package/src/shims/process.ts
CHANGED
|
@@ -363,8 +363,8 @@ function trailingCallback(args: readonly unknown[]): (() => void) | undefined {
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
/** The signal numbers a guest sees, and the signals a tab must ignore. */
|
|
366
|
-
const __substrateSignals: Record<string, number> = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGILL: 4, SIGTRAP: 5, SIGABRT: 6, SIGBUS: 7, SIGFPE: 8, SIGKILL: 9, SIGUSR1: 10, SIGSEGV: 11, SIGUSR2: 12, SIGPIPE: 13, SIGALRM: 14, SIGTERM: 15, SIGCHLD: 17, SIGCONT: 18, SIGSTOP: 19, SIGTSTP: 20, SIGTTIN: 21, SIGTTOU: 22, SIGURG: 23, SIGWINCH: 28 };
|
|
367
|
-
const __substrateSignalNames: Record<number, string> = Object.fromEntries(Object.entries(__substrateSignals).map(([name, number]) => [number, name]));
|
|
366
|
+
export const __substrateSignals: Record<string, number> = { SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGILL: 4, SIGTRAP: 5, SIGABRT: 6, SIGBUS: 7, SIGFPE: 8, SIGKILL: 9, SIGUSR1: 10, SIGSEGV: 11, SIGUSR2: 12, SIGPIPE: 13, SIGALRM: 14, SIGTERM: 15, SIGCHLD: 17, SIGCONT: 18, SIGSTOP: 19, SIGTSTP: 20, SIGTTIN: 21, SIGTTOU: 22, SIGURG: 23, SIGWINCH: 28 };
|
|
367
|
+
export const __substrateSignalNames: Record<number, string> = Object.fromEntries(Object.entries(__substrateSignals).map(([name, number]) => [number, name]));
|
|
368
368
|
// Node prints the `process.binding` deprecation once per process; the engine
|
|
369
369
|
// runs many guests in one realm, so the record is per guest, not per load.
|
|
370
370
|
const __substrateBindingWarned = new WeakSet<object>();
|