@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.
@@ -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: () => {
@@ -23,4 +23,5 @@ export declare function nodeVersions(node?: string): {
23
23
  uv: string;
24
24
  webcontainer: string;
25
25
  openssl: string;
26
+ amaro: string;
26
27
  };
@@ -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 {};
@@ -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. */
@@ -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.17",
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
- return { node, v8: "11.3.244.8", uv: "1.44.2", webcontainer: "1", openssl: "3.0.15" };
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
  }
@@ -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
+ }
@@ -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] !== "undefined") module[name] = (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
- // A `.ts`, `.mts` or `.cts` file runs with its types stripped, as Node
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";\n${lowered}`;
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
- code = lowered === code ? code : `${__substrateModuleMarker}"use strict";\n${lowered}`;
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