@volter/tabnode 0.5.17 → 0.5.19
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-DCIwIWHJ.js → runtime-worker-ChOZScpd.js} +613 -123
- package/dist/index.mjs +574 -103
- 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/process.d.ts +3 -0
- package/dist/shims/shell-jobs.d.ts +12 -0
- package/dist/shims/streamed-output.d.ts +14 -0
- package/package.json +2 -1
- 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 +16 -1
- 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 +44 -16
- package/src/shims/child_process.ts +15 -7
- package/src/shims/process.ts +2 -2
- package/src/shims/shell-jobs.ts +451 -0
- package/src/shims/streamed-output.ts +25 -0
|
@@ -0,0 +1,31 @@
|
|
|
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>) => {
|
|
21
|
+
code: string;
|
|
22
|
+
map?: string;
|
|
23
|
+
};
|
|
24
|
+
interface Amaro {
|
|
25
|
+
transformSync: AmaroTransform;
|
|
26
|
+
}
|
|
27
|
+
/** amaro, loaded once from beside this engine, or null where the realm cannot load it. */
|
|
28
|
+
export declare function hostAmaro(): Amaro | null;
|
|
29
|
+
/** The module Node's `typescript.js` requires; absent, the error Node raises when built without amaro. */
|
|
30
|
+
export declare function internalAmaro(): Amaro;
|
|
31
|
+
export {};
|
|
@@ -107,6 +107,7 @@ export declare const internalEventTarget: {
|
|
|
107
107
|
kNewListener: symbol;
|
|
108
108
|
kRemoveListener: symbol;
|
|
109
109
|
};
|
|
110
|
+
export declare function withOptionValues<T>(overrides: Record<string, unknown>, fn: () => T): T;
|
|
110
111
|
export declare const internalOptions: {
|
|
111
112
|
getOptionValue: (name: string) => unknown;
|
|
112
113
|
getEmbedderOptions: () => {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type RunProcess = {
|
|
2
|
+
execArgv?: readonly string[];
|
|
3
|
+
env?: Record<string, string | undefined>;
|
|
4
|
+
};
|
|
5
|
+
/** Whether this realm can erase types itself: amaro is loadable here. */
|
|
6
|
+
export declare function canStripTypes(): boolean;
|
|
7
|
+
/** Whether a process transforms TypeScript-only syntax rather than refusing it. */
|
|
8
|
+
export declare function transformsTypes(process: RunProcess): boolean;
|
|
9
|
+
/** The module's source with its types erased (or, under `--experimental-transform-types`, transformed), for this process. */
|
|
10
|
+
export declare function stripModuleTypes(source: string, filename: string, process: object): string;
|
|
11
|
+
export {};
|
package/dist/process-tokens.d.ts
CHANGED
|
@@ -96,6 +96,8 @@ export declare function runPid(token: ProcessToken | null | undefined): {
|
|
|
96
96
|
} | undefined;
|
|
97
97
|
/** A run that has ended is no longer a process; its number is nobody's. */
|
|
98
98
|
export declare function forgetRunPid(token: ProcessToken): void;
|
|
99
|
+
/** The named run a pid belongs to in this realm, for a signal sent by number (the shell's `kill`). */
|
|
100
|
+
export declare function tokenOfPid(pid: number): ProcessToken | null;
|
|
99
101
|
/** Whether a live run carries this number, which is what `kill(pid, 0)` asks. */
|
|
100
102
|
export declare function pidIsLive(pid: number): boolean;
|
|
101
103
|
/** `kill(pid, signal)` to another realm's live process; whether one took it. */
|
package/dist/shims/process.d.ts
CHANGED
|
@@ -177,6 +177,9 @@ export interface Process {
|
|
|
177
177
|
send?: (message: unknown, callback?: (error: Error | null) => void) => boolean;
|
|
178
178
|
connected?: boolean;
|
|
179
179
|
}
|
|
180
|
+
/** The signal numbers a guest sees, and the signals a tab must ignore. */
|
|
181
|
+
export declare const __substrateSignals: Record<string, number>;
|
|
182
|
+
export declare const __substrateSignalNames: Record<number, string>;
|
|
180
183
|
/**
|
|
181
184
|
* The guest's children are processes it can see. `process.kill(pid, 0)`
|
|
182
185
|
* answered ESRCH for every pid but the guest's own, so a program that keeps its
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Bash, CommandContext } from 'just-bash';
|
|
2
|
+
import { type ProcessToken } from '../process-tokens';
|
|
3
|
+
import type { RunStreams } from './child_process';
|
|
4
|
+
/** What the engine's process model gives this module. */
|
|
5
|
+
export interface ShellJobsHost {
|
|
6
|
+
runTokenOf(ctx: CommandContext): ProcessToken | null;
|
|
7
|
+
runStreamsFor(token: ProcessToken): RunStreams | undefined;
|
|
8
|
+
registerRunStreams(token: ProcessToken, streams: RunStreams): void;
|
|
9
|
+
releaseRunStreams(token: ProcessToken): void;
|
|
10
|
+
}
|
|
11
|
+
/** The engine's shell, given background jobs. */
|
|
12
|
+
export declare function installShellJobs(bash: Bash, host: ShellJobsHost): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What of a command's collected output was not already streamed.
|
|
3
|
+
*
|
|
4
|
+
* The engine's shell collects a command line's output as one text at its end;
|
|
5
|
+
* a `node` in it (or a background job) also streams its own as it writes. A
|
|
6
|
+
* caller that forwarded the streamed pieces sends the rest at the end. Cutting
|
|
7
|
+
* the collected text at the streamed length assumed the streamed pieces came
|
|
8
|
+
* first: `echo "pid=1"; node -e "console.log('fg out')"; echo end`, spawned by
|
|
9
|
+
* a guest, reached it as `fg out\ng out\nend\n` -- the builtin's line lost and
|
|
10
|
+
* the node's half repeated. Each streamed piece is taken out where it stands in
|
|
11
|
+
* the collected text, in order, and what is left is the rest; a piece the text
|
|
12
|
+
* does not hold is passed over.
|
|
13
|
+
*/
|
|
14
|
+
export declare function unstreamedOutput(total: string, streamed: readonly string[]): string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/tabnode",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.19",
|
|
4
4
|
"description": "Node's own library, in a browser tab.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@noble/hashes": "2.3.0",
|
|
55
55
|
"acorn": "^8.15.0",
|
|
56
|
+
"amaro": "1.1.0",
|
|
56
57
|
"brotli": "^1.3.3",
|
|
57
58
|
"brotli-wasm": "^3.0.1",
|
|
58
59
|
"buffer": "6.0.3",
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
ObjectPrototypeHasOwnProperty,
|
|
5
|
+
} = primordials;
|
|
6
|
+
const {
|
|
7
|
+
validateBoolean,
|
|
8
|
+
validateOneOf,
|
|
9
|
+
validateObject,
|
|
10
|
+
validateString,
|
|
11
|
+
} = require('internal/validators');
|
|
12
|
+
const { assertTypeScript,
|
|
13
|
+
emitExperimentalWarning,
|
|
14
|
+
getLazy,
|
|
15
|
+
isUnderNodeModules,
|
|
16
|
+
kEmptyObject } = require('internal/util');
|
|
17
|
+
const {
|
|
18
|
+
ERR_INVALID_TYPESCRIPT_SYNTAX,
|
|
19
|
+
ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING,
|
|
20
|
+
ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX,
|
|
21
|
+
} = require('internal/errors').codes;
|
|
22
|
+
const { getOptionValue } = require('internal/options');
|
|
23
|
+
const assert = require('internal/assert');
|
|
24
|
+
const { Buffer } = require('buffer');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The TypeScript parsing mode, either 'strip-only' or 'transform'.
|
|
28
|
+
* @type {function(): TypeScriptMode}
|
|
29
|
+
*/
|
|
30
|
+
const getTypeScriptParsingMode = getLazy(() =>
|
|
31
|
+
(getOptionValue('--experimental-transform-types') ?
|
|
32
|
+
(emitExperimentalWarning('Transform Types'), 'transform') : 'strip-only'),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Load the TypeScript parser.
|
|
37
|
+
* and returns an object with a `code` property.
|
|
38
|
+
* @returns {Function} The TypeScript parser function.
|
|
39
|
+
*/
|
|
40
|
+
const loadTypeScriptParser = getLazy(() => {
|
|
41
|
+
assertTypeScript();
|
|
42
|
+
const amaro = require('internal/deps/amaro/dist/index');
|
|
43
|
+
return amaro.transformSync;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
*
|
|
48
|
+
* @param {string} source the source code
|
|
49
|
+
* @param {object} options the options to pass to the parser
|
|
50
|
+
* @returns {TransformOutput} an object with a `code` property.
|
|
51
|
+
*/
|
|
52
|
+
function parseTypeScript(source, options) {
|
|
53
|
+
const parse = loadTypeScriptParser();
|
|
54
|
+
try {
|
|
55
|
+
return parse(source, options);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
/**
|
|
58
|
+
* Amaro v0.3.0 (from SWC v1.10.7) throws an object with `message` and `code` properties.
|
|
59
|
+
* It allows us to distinguish between invalid syntax and unsupported syntax.
|
|
60
|
+
*/
|
|
61
|
+
switch (error?.code) {
|
|
62
|
+
case 'UnsupportedSyntax': {
|
|
63
|
+
const unsupportedSyntaxError = new ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX(error.message);
|
|
64
|
+
throw decorateErrorWithSnippet(unsupportedSyntaxError, error); /* node-do-not-add-exception-line */
|
|
65
|
+
}
|
|
66
|
+
case 'InvalidSyntax': {
|
|
67
|
+
const invalidSyntaxError = new ERR_INVALID_TYPESCRIPT_SYNTAX(error.message);
|
|
68
|
+
throw decorateErrorWithSnippet(invalidSyntaxError, error); /* node-do-not-add-exception-line */
|
|
69
|
+
}
|
|
70
|
+
default:
|
|
71
|
+
// SWC may throw strings when something goes wrong.
|
|
72
|
+
if (typeof error === 'string') { assert.fail(error); }
|
|
73
|
+
assert(error != null && ObjectPrototypeHasOwnProperty(error, 'message'));
|
|
74
|
+
assert.fail(error.message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
*
|
|
81
|
+
* @param {Error} error the error to decorate: ERR_INVALID_TYPESCRIPT_SYNTAX, ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX
|
|
82
|
+
* @param {object} amaroError the error object from amaro
|
|
83
|
+
* @returns {Error} the decorated error
|
|
84
|
+
*/
|
|
85
|
+
function decorateErrorWithSnippet(error, amaroError) {
|
|
86
|
+
const errorHints = `${amaroError.filename}:${amaroError.startLine}\n${amaroError.snippet}`;
|
|
87
|
+
error.stack = `${errorHints}\n${error.stack}`;
|
|
88
|
+
return error;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Performs type-stripping to TypeScript source code.
|
|
93
|
+
* @param {string} code TypeScript code to parse.
|
|
94
|
+
* @param {TransformOptions} options The configuration for type stripping.
|
|
95
|
+
* @returns {string} The stripped TypeScript code.
|
|
96
|
+
*/
|
|
97
|
+
function stripTypeScriptTypes(code, options = kEmptyObject) {
|
|
98
|
+
emitExperimentalWarning('stripTypeScriptTypes');
|
|
99
|
+
validateString(code, 'code');
|
|
100
|
+
validateObject(options, 'options');
|
|
101
|
+
|
|
102
|
+
const {
|
|
103
|
+
sourceMap = false,
|
|
104
|
+
sourceUrl = '',
|
|
105
|
+
} = options;
|
|
106
|
+
let { mode = 'strip' } = options;
|
|
107
|
+
validateOneOf(mode, 'options.mode', ['strip', 'transform']);
|
|
108
|
+
validateBoolean(sourceMap, 'options.sourceMap');
|
|
109
|
+
validateString(sourceUrl, 'options.sourceUrl');
|
|
110
|
+
if (mode === 'strip') {
|
|
111
|
+
validateOneOf(sourceMap, 'options.sourceMap', [false, undefined]);
|
|
112
|
+
// Rename mode from 'strip' to 'strip-only'.
|
|
113
|
+
// The reason is to match `process.features.typescript` which returns `strip`,
|
|
114
|
+
// but the parser expects `strip-only`.
|
|
115
|
+
mode = 'strip-only';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return processTypeScriptCode(code, {
|
|
119
|
+
mode,
|
|
120
|
+
sourceMap,
|
|
121
|
+
filename: sourceUrl,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Processes TypeScript code by stripping types or transforming.
|
|
127
|
+
* Handles source maps if needed.
|
|
128
|
+
* @param {string} code TypeScript code to process.
|
|
129
|
+
* @param {object} options The configuration object.
|
|
130
|
+
* @returns {string} The processed code.
|
|
131
|
+
*/
|
|
132
|
+
function processTypeScriptCode(code, options) {
|
|
133
|
+
const { code: transformedCode, map } = parseTypeScript(code, options);
|
|
134
|
+
|
|
135
|
+
if (map) {
|
|
136
|
+
return addSourceMap(transformedCode, map);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (options.filename) {
|
|
140
|
+
return `${transformedCode}\n\n//# sourceURL=${options.filename}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return transformedCode;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Performs type-stripping to TypeScript source code internally.
|
|
148
|
+
* It is used by internal loaders.
|
|
149
|
+
* @param {string} source TypeScript code to parse.
|
|
150
|
+
* @param {string} filename The filename of the source code.
|
|
151
|
+
* @returns {TransformOutput} The stripped TypeScript code.
|
|
152
|
+
*/
|
|
153
|
+
function stripTypeScriptModuleTypes(source, filename) {
|
|
154
|
+
assert(typeof source === 'string');
|
|
155
|
+
if (isUnderNodeModules(filename)) {
|
|
156
|
+
throw new ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING(filename);
|
|
157
|
+
}
|
|
158
|
+
const options = {
|
|
159
|
+
mode: getTypeScriptParsingMode(),
|
|
160
|
+
sourceMap: getOptionValue('--enable-source-maps'),
|
|
161
|
+
filename,
|
|
162
|
+
};
|
|
163
|
+
return processTypeScriptCode(source, options);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
*
|
|
168
|
+
* @param {string} code The compiled code.
|
|
169
|
+
* @param {string} sourceMap The source map.
|
|
170
|
+
* @returns {string} The code with the source map attached.
|
|
171
|
+
*/
|
|
172
|
+
function addSourceMap(code, sourceMap) {
|
|
173
|
+
// The base64 encoding should be https://datatracker.ietf.org/doc/html/rfc4648#section-4,
|
|
174
|
+
// not base64url https://datatracker.ietf.org/doc/html/rfc4648#section-5. See data url
|
|
175
|
+
// spec https://tools.ietf.org/html/rfc2397#section-2.
|
|
176
|
+
const base64SourceMap = Buffer.from(sourceMap).toString('base64');
|
|
177
|
+
return `${code}\n\n//# sourceMappingURL=data:application/json;base64,${base64SourceMap}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
stripTypeScriptModuleTypes,
|
|
182
|
+
stripTypeScriptTypes,
|
|
183
|
+
};
|
|
@@ -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
|
};;
|
|
@@ -235,10 +235,25 @@ const optionValues: Record<string, unknown> = {
|
|
|
235
235
|
'--max-http-header-size': 16 * 1024,
|
|
236
236
|
'--insecure-http-parser': false,
|
|
237
237
|
'--enable-source-maps': false,
|
|
238
|
+
'--experimental-transform-types': false,
|
|
238
239
|
};
|
|
239
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
|
+
|
|
240
255
|
export const internalOptions = {
|
|
241
|
-
getOptionValue: (name: string): unknown => optionValues[name],
|
|
256
|
+
getOptionValue: (name: string): unknown => (optionOverrides !== null && name in optionOverrides ? optionOverrides[name] : optionValues[name]),
|
|
242
257
|
getEmbedderOptions: () => ({ shouldNotRegisterESMLoader: false, noGlobalSearchPaths: false, noBrowserGlobals: false }),
|
|
243
258
|
};
|
|
244
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
|
@@ -68,6 +68,7 @@ import { createWasiModule, type WasiHostFs, type WasiModule } from './shims/wasi
|
|
|
68
68
|
import { resolve as resolveExports, imports as resolveImports } from 'resolve.exports';
|
|
69
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
|
*
|
|
@@ -794,9 +820,14 @@ function __substratePackageIdentity(vfs: VirtualFS, file: string): string | unde
|
|
|
794
820
|
}
|
|
795
821
|
|
|
796
822
|
function __webStreamsModule(): Record<string, unknown> {
|
|
823
|
+
// Read when used, not when the engine loads: the engine later wraps the
|
|
824
|
+
// global ReadableStream (Node's closed-promise tracking), and a copy taken
|
|
825
|
+
// here was the unwrapped one, so require('stream/web').ReadableStream was
|
|
826
|
+
// not globalThis.ReadableStream, as it is in Node.
|
|
797
827
|
const module: Record<string, unknown> = {};
|
|
798
828
|
for (const name of ["ReadableStream", "ReadableStreamDefaultReader", "ReadableStreamBYOBReader", "ReadableStreamBYOBRequest", "ReadableByteStreamController", "ReadableStreamDefaultController", "TransformStream", "TransformStreamDefaultController", "WritableStream", "WritableStreamDefaultWriter", "WritableStreamDefaultController", "ByteLengthQueuingStrategy", "CountQueuingStrategy", "TextEncoderStream", "TextDecoderStream", "CompressionStream", "DecompressionStream"]) {
|
|
799
|
-
if (typeof (globalThis as Record<string, unknown>)[name]
|
|
829
|
+
if (typeof (globalThis as Record<string, unknown>)[name] === "undefined") continue;
|
|
830
|
+
Object.defineProperty(module, name, { enumerable: true, configurable: true, get: () => (globalThis as Record<string, unknown>)[name] });
|
|
800
831
|
}
|
|
801
832
|
return module;
|
|
802
833
|
}
|
|
@@ -1775,7 +1806,7 @@ function createRequire(
|
|
|
1775
1806
|
const prepareModuleCode = (rawCode: string, resolvedPath: string, format?: string): string => {
|
|
1776
1807
|
// Check processed code cache (useful for HMR when module cache is cleared but code hasn't changed)
|
|
1777
1808
|
// Use a simple hash of the content for cache key to handle content changes
|
|
1778
|
-
const codeCacheKey = `${resolvedPath}|${format ?? ''}|${simpleHash(rawCode)}`;
|
|
1809
|
+
const codeCacheKey = `${resolvedPath}|${format ?? ''}|${transformsTypes(process as { execArgv?: string[]; env?: Record<string, string> }) ? 'transform-types|' : ''}${simpleHash(rawCode)}`;
|
|
1779
1810
|
let code = processedCodeCache?.get(codeCacheKey);
|
|
1780
1811
|
|
|
1781
1812
|
if (!code) {
|
|
@@ -1786,16 +1817,7 @@ function createRequire(
|
|
|
1786
1817
|
code = code.slice(code.indexOf('\n') + 1);
|
|
1787
1818
|
}
|
|
1788
1819
|
|
|
1789
|
-
|
|
1790
|
-
// runs one since 22.18: the source goes to the type stripper the realm
|
|
1791
|
-
// loaded, amaro, before the module's imports are read. A realm without one
|
|
1792
|
-
// runs the file as it is.
|
|
1793
|
-
const __typescript = format === undefined
|
|
1794
|
-
? /[.](?:ts|mts|cts)$/u.test(resolvedPath) && !resolvedPath.endsWith('.d.ts')
|
|
1795
|
-
: format === 'typescript' || format === 'commonjs-typescript' || format === 'module-typescript';
|
|
1796
|
-
if (__typescript && typeof (globalThis as any).__substrateStripTypes === 'function') {
|
|
1797
|
-
code = (globalThis as any).__substrateStripTypes(code, resolvedPath) as string;
|
|
1798
|
-
}
|
|
1820
|
+
code = __substrateModuleTypes(code, resolvedPath, format, process);
|
|
1799
1821
|
|
|
1800
1822
|
// Transform ESM to CJS if needed (for .mjs files or ESM that wasn't pre-transformed)
|
|
1801
1823
|
// transformEsmToCjs uses AST to handle import/export, import.meta, and dynamic imports
|
|
@@ -1804,12 +1826,15 @@ function createRequire(
|
|
|
1804
1826
|
? true
|
|
1805
1827
|
: format === 'module' || format === 'module-typescript'
|
|
1806
1828
|
? false
|
|
1807
|
-
: resolvedPath.endsWith('.cjs');
|
|
1829
|
+
: resolvedPath.endsWith('.cjs') || resolvedPath.endsWith('.cts');
|
|
1808
1830
|
if (!__commonjs) {
|
|
1809
1831
|
// An ES module is strict wherever it runs; the mark says so to the
|
|
1810
1832
|
// compile below, which is otherwise sloppy as a CommonJS body is.
|
|
1833
|
+
// The directive shares the body's first line, as Node's one-line
|
|
1834
|
+
// wrapper does, so a frame of an ES module counts the file's own lines;
|
|
1835
|
+
// on a line of its own it put every frame one line below the file's.
|
|
1811
1836
|
const lowered = transformEsmToCjs(code, resolvedPath);
|
|
1812
|
-
code = lowered === code ? code : `${__substrateModuleMarker}"use strict"
|
|
1837
|
+
code = lowered === code ? code : `${__substrateModuleMarker}"use strict";${lowered}`;
|
|
1813
1838
|
} else {
|
|
1814
1839
|
// A `.cjs` module skips the ESM transform, and with it the rewrite of
|
|
1815
1840
|
// `import(...)` to the engine's dynamic import, so its
|
|
@@ -2374,10 +2399,13 @@ export class Runtime {
|
|
|
2374
2399
|
code = code.slice(code.indexOf('\n') + 1);
|
|
2375
2400
|
}
|
|
2376
2401
|
|
|
2402
|
+
code = __substrateModuleTypes(code, filename, undefined, this.process);
|
|
2403
|
+
|
|
2377
2404
|
// Transform ESM to CJS if needed (AST-based, handles import.meta and dynamic imports too)
|
|
2378
|
-
if (!filename.endsWith('.cjs')) {
|
|
2405
|
+
if (!filename.endsWith('.cjs') && !filename.endsWith('.cts')) {
|
|
2379
2406
|
const lowered = transformEsmToCjs(code, filename);
|
|
2380
|
-
|
|
2407
|
+
// The directive shares the body's first line, as `prepareModuleCode`'s does.
|
|
2408
|
+
code = lowered === code ? code : `${__substrateModuleMarker}"use strict";${lowered}`;
|
|
2381
2409
|
} else {
|
|
2382
2410
|
// A `.cjs` module skips the ESM transform, and with it the rewrite of
|
|
2383
2411
|
// `import(...)` to the engine's dynamic import, so its
|