diff --git a/.gitignore b/.gitignore
index 7b7bc57f2b..fda9414aae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,8 @@ node_modules/
!tests/fixtures/npm-static/nested/inner/node_modules/
!tests/fixtures/npm-static/node_modules/*/node_modules/
!tests/fixtures/npm/store/*/node_modules/
+!tests/fixtures/npm/node_modules/*/node_modules/
+!tests/fixtures/npm/workspace/*/node_modules/
!tests/fixtures/npm/cases/*/node_modules/
!tests/library-mode/*/node_modules/
!tests/fixtures/provenance/node_modules/
@@ -49,3 +51,6 @@ tmp-*/
.env.local
.env*
.env*.local
+# the vendored fs-extra corpus package: its lib/output module and jsonfile's own universalify
+!tests/corpus/fs-extra-package/node_modules/fs-extra/lib/output/
+!tests/corpus/fs-extra-package/node_modules/jsonfile/node_modules/
diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx
index b16c14e7d7..85a232bb2a 100644
--- a/docs/content/docs/dependencies.mdx
+++ b/docs/content/docs/dependencies.mdx
@@ -73,7 +73,58 @@ Some packages your program imports directly ship no declarations, so their types
$ scriptc build app.ts --npm-static=three -o app >/dev/null
```
-Naming a package skips the declaration requirement above, but the package still has to compile. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
+Naming a package skips the declaration requirement above, but the package still has to compile. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=semver...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
+
+## Built-in packages
+
+scriptc implements a few packages itself and compiles that implementation in place of the package's published JavaScript. Which implementation compiles depends on the version your program has installed, so an import resolves the same way it does under Node.js: a package that isn't installed is still a missing package, and a version scriptc doesn't implement compiles from its own files as usual.
+
+`undici` 5.x and 7.x are built in. The implementation covers `request()`, `Dispatcher` (including subclasses that override `dispatch`), `Client`, `Pool` with its `stats`, `Agent` with per-origin `factory` functions and `connect` options, `buildConnector`, the `errors` classes and their `UND_ERR_*` codes, `setGlobalDispatcher` and `getGlobalDispatcher`, `fetch` and `Headers`, and the request events undici publishes on `node:diagnostics_channel`. Requests use HTTP/1.1 with keep-alive connection reuse and undici's headers, body and idle timeouts, and each version keeps its own behavior where 5.x and 7.x differ, such as the error an aborted request rejects with. The package's other exports aren't declared, so using one is a type error at build time. Known differences:
+
+- A response body supports the readable-stream members undici's body mixin uses: `on`, `once`, `pause`, `resume`, `destroy`, `text()`, `json()`, `arrayBuffer()`, `bytes()`, `blob()` and `dump()`. It can't be piped or iterated with `for await`.
+- Diagnostics channel messages carry the request but not the socket or connector.
+- `fetch` with a dispatcher returns a response whose `url` is empty and whose `redirected` is `false`, offers only `gzip, deflate` encodings, and sends string and byte bodies. Without a dispatcher it is the built-in `fetch`.
+- Request bodies are strings and bytes; stream, iterable and form-data bodies, `pipelining` above 1, `CONNECT` requests and Unix socket paths aren't supported.
+
+`fs-extra` 8.x through 11.x is built in, and its declarations replace `@types/fs-extra`. The default import, a namespace import and named imports all read the same exports. fs's members are re-exported, and fs-extra's wrapped methods follow its rule: with a callback as the last argument they call back, otherwise they return a promise. The extra methods cover `copy`, `copySync`, `move`, `moveSync`, `remove`, `removeSync`, `emptyDir`, `ensureDir`, `mkdirs`, `mkdirp`, `ensureFile`, `pathExists`, `outputFile`, `readJson`, `readJsonSync`, `writeJson`, `writeJsonSync`, `outputJson` and their aliases, with fs-extra's error messages. Known differences:
+
+- `ensureLink`, `ensureSymlink`, `createLink`, `createSymlink`, `gracefulify`, the promise forms of the descriptor-based methods, and the callback forms of `stat`, `lstat`, `readdir`, `mkdtemp`, `realpath` and `readJson` aren't declared, so using one is a type error at build time. Paths are strings, text is UTF-8, and write flags, string modes and `copyFile` mode flags aren't accepted.
+- graceful-fs's retry queue, which fs-extra uses under Node.js, isn't part of the implementation, so a program that runs out of file descriptors fails at the first `EMFILE` error instead of retrying.
+- An ES module that imports fs-extra can use any of its exports by name. Node.js only exposes the names its CommonJS lexer finds in fs-extra's source, so a name like `readFile` throws a `SyntaxError` there.
+
+Small polyfill packages whose module is exactly a Node.js built-in on Node.js 24 compile as that built-in, at the `require()` that loads them, from your program's CommonJS files, from compiled packages, and from esbuild bundles whose module paths name the package and its version (as pnpm installs do). The package's own files aren't compiled.
+
+
+
+ | Package | Versions | Compiles as |
+
+
+ isarray | 0.x, 1.x, 2.x | Array.isArray |
+ safe-buffer | 5.x | require("buffer") |
+ process-nextick-args | 2.x | process |
+ inherits | 2.x | require("util").inherits |
+ util-deprecate | 1.x | require("util").deprecate |
+ readable-stream | 2.x, 3.x, 4.x | require("stream") and its classes |
+
+
+
+`readable-stream` compiles as Node.js's built-in streams, which is its own `READABLE_STREAM=disable` mode for 2.x and 4.x. That can differ from its default behavior under Node.js; see [limitations](/docs/limitations#readable-stream).
+
+## Optional dependencies that never load
+
+Some packages load an optional dependency inside `try`/`catch` and work without it, such as `node-fetch` with `encoding`. To keep such a dependency out of the native build even when it is installed, mark it unloaded in your entry's `package.json`:
+
+```json:package.json
+{
+ "scriptc": {
+ "packages": {
+ "encoding": "unloaded"
+ }
+ }
+}
+```
+
+A `require()` of an unloaded package throws the same `MODULE_NOT_FOUND` error Node.js throws when the package isn't installed, and the package isn't compiled.
## CommonJS packages
diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx
index 823b7f0283..05f9d329ac 100644
--- a/docs/content/docs/limitations.mdx
+++ b/docs/content/docs/limitations.mdx
@@ -178,6 +178,10 @@ title: "Limitations"
../limitations/buffer-construction.mdx
+### readable-stream
+
+../limitations/readable-stream.mdx
+
### HTTP headers
../limitations/http-headers.mdx
diff --git a/docs/content/limitations/readable-stream.mdx b/docs/content/limitations/readable-stream.mdx
new file mode 100644
index 0000000000..80ab72f012
--- /dev/null
+++ b/docs/content/limitations/readable-stream.mdx
@@ -0,0 +1 @@
+`readable-stream` 2.x, 3.x and 4.x compile as Node.js's built-in `stream` module. For 2.x and 4.x, that is the package's own behavior when the `READABLE_STREAM` environment variable is `disable`. Without that variable, Node.js runs the package's JavaScript copy of the stream classes, so a native build can differ from Node.js wherever a program depends on that difference: `instanceof` checks against the built-in classes, class identity, error messages, and timing details. The package's `lib/_stream_*.js` files also compile as the matching built-in classes, although Node.js loads the package's own implementations through them in every environment. `readable-stream` 3.x is a copy of Node.js 10's streams. Its own `disable` mode throws on Node.js 24, so no setting makes Node.js run it against the built-in classes; a native build maps the members its entry exports (the stream classes, `Stream`, `finished` and `pipeline`) to the built-in ones.
diff --git a/internal/compatibility/README.md b/internal/compatibility/README.md
index 0b4fdb9575..51c7d72c5e 100644
--- a/internal/compatibility/README.md
+++ b/internal/compatibility/README.md
@@ -13,3 +13,7 @@ Statuses are evidence-aware: `supported` and `partial` require test evidence; `r
Run `pnpm node-compat:backlog` from the repository root for a summary. The filters `--tier`, `--action`, `--priority`, `--chapter`, and `--status` compose; use `--format=json` or `--format=tsv` for tooling.
Run `pnpm node-compat` from the repository root after changing the Node pin or the support manifest. `pnpm node-compat:check` is the offline drift/evidence gate; `pnpm --filter @internal/compatibility check:upstream` additionally verifies the pinned upstream inputs over the network.
+
+## npm packages that compile as builtins
+
+The ledger counts Node APIs, so npm packages that compile as a Node builtin have no rows. `packages/compiler/src/frontend/builtin-package-aliases.ts` lists them, each with the checked major versions and the reason its module is that builtin on Node 24. One entry is a documented divergence rather than an identity: `readable-stream` 2.x, 3.x and 4.x compile as `node:stream`; for 2.x and 4.x that is the package's `READABLE_STREAM=disable` behavior. Without that variable Node runs the package's own JavaScript streams. The `tests/corpus/readable-stream-disable` program pins the disabled behavior. 3.x, a copy of Node 10's streams, cannot run disabled on Node 24 (its disable branch throws), so `tests/corpus/readable-stream-3` pins only behavior its own streams share with the builtin.
diff --git a/packages/compiler/package.json b/packages/compiler/package.json
index 0c058c467a..70bf3b27aa 100644
--- a/packages/compiler/package.json
+++ b/packages/compiler/package.json
@@ -29,6 +29,7 @@
"files": [
"dist",
"ambient",
+ "shims",
"surface-manifest.json"
],
"publishConfig": {
diff --git a/packages/compiler/shims/fs-extra/core/api.ts b/packages/compiler/shims/fs-extra/core/api.ts
new file mode 100644
index 0000000000..7a59f17e29
--- /dev/null
+++ b/packages/compiler/shims/fs-extra/core/api.ts
@@ -0,0 +1,1192 @@
+// fs-extra's exports over node:fs.
+//
+// fs-extra re-exports every fs member and wraps the asynchronous ones with
+// universalify: a call whose last argument is a function takes the callback
+// form, any other call returns a promise. The wrapped methods below follow
+// that rule over fs.promises (callback forms settle through the promise);
+// fs's other members are fs's own. The extra methods (copy, remove, mkdirs,
+// the JSON helpers, ...) restate fs-extra's behavior and error messages.
+// graceful-fs's EMFILE retry queue, which fs-extra loads under Node, has no
+// counterpart: on Node 24 it only changes behavior when the process runs
+// out of file descriptors.
+//
+// What is not declared here is a compile-time error: ensureLink,
+// ensureSymlink, createLink, createSymlink and their sync forms,
+// gracefulify, the descriptor-based methods' promise forms (fstat, read,
+// write, ...), the callback forms of the methods that answer stats,
+// listings or paths (stat, lstat, readdir, mkdtemp, realpath, readJson),
+// Buffer and URL paths, and option shapes fs.promises does not compile
+// natively (copyFile mode flags, append options, write flags, string modes,
+// numeric utimes times). A readFile callback that fails receives an empty
+// value beside the error, where Node passes undefined.
+import * as fs from "node:fs";
+import * as fsp from "node:fs/promises";
+import * as path from "node:path";
+import type { Stats } from "node:fs";
+
+// fs's other members, as fs-extra re-exports them (fs-extra's own versions
+// of the wrapped methods below take their place).
+export {
+ appendFileSync,
+ accessSync,
+ chownSync,
+ chmodSync,
+ close,
+ closeSync,
+ copyFileSync,
+ cp,
+ cpSync,
+ createReadStream,
+ createWriteStream,
+ exists,
+ existsSync,
+ fchown,
+ fchownSync,
+ fchmod,
+ fchmodSync,
+ fdatasync,
+ fdatasyncSync,
+ fstat,
+ fstatSync,
+ fsync,
+ fsyncSync,
+ ftruncate,
+ ftruncateSync,
+ futimes,
+ futimesSync,
+ lchownSync,
+ linkSync,
+ lstatSync,
+ lutimes,
+ lutimesSync,
+ mkdirSync,
+ mkdtempSync,
+ open,
+ openSync,
+ readdirSync,
+ read,
+ readSync,
+ readv,
+ readvSync,
+ readFileSync,
+ readlinkSync,
+ realpathSync,
+ renameSync,
+ rmSync,
+ rmdirSync,
+ statfs,
+ statSync,
+ statfsSync,
+ symlinkSync,
+ truncateSync,
+ unwatchFile,
+ unlinkSync,
+ utimesSync,
+ watch,
+ watchFile,
+ writeFileSync,
+ write,
+ writeSync,
+ writev,
+ writevSync,
+ Dirent,
+ Stats,
+ ReadStream,
+ WriteStream,
+ Dir,
+ opendir,
+ opendirSync,
+ constants,
+ promises,
+} from "node:fs";
+
+/** A Node-style callback with a result. */
+export type Callback = (err: NodeJS.ErrnoException | null, value: T) => void;
+/** A Node-style callback without a result. */
+export type VoidCallback = (err: NodeJS.ErrnoException | null) => void;
+
+export type SymlinkType = "dir" | "file" | "junction";
+export type Utf8 = "utf8" | "utf-8";
+
+export interface WriteFileOptions {
+ encoding?: Utf8 | undefined;
+ mode?: number | undefined;
+}
+
+export interface ReadOptions {
+ throws?: boolean | undefined;
+ encoding?: Utf8 | undefined;
+}
+
+export interface WriteOptions {
+ encoding?: Utf8 | undefined;
+ mode?: number | undefined;
+ spaces?: number | undefined;
+ EOL?: string | undefined;
+ finalEOL?: boolean | undefined;
+}
+
+/** A copy filter: fs-extra accepts a boolean or a promise of one. */
+export type CopyFilter = (src: string, dest: string) => boolean | Promise;
+
+export interface CopyOptions {
+ dereference?: boolean | undefined;
+ overwrite?: boolean | undefined;
+ preserveTimestamps?: boolean | undefined;
+ errorOnExist?: boolean | undefined;
+ filter?: CopyFilter | undefined;
+}
+
+export interface CopyOptionsSync {
+ dereference?: boolean | undefined;
+ overwrite?: boolean | undefined;
+ preserveTimestamps?: boolean | undefined;
+ errorOnExist?: boolean | undefined;
+ filter?: ((src: string, dest: string) => boolean) | undefined;
+}
+
+export interface MoveOptions {
+ overwrite?: boolean | undefined;
+}
+
+export interface EnsureOptions {
+ mode?: number | undefined;
+}
+
+export interface MakeDirectoryOptions {
+ recursive?: boolean | undefined;
+ mode?: number | undefined;
+}
+
+export interface RmOptions {
+ force?: boolean | undefined;
+ recursive?: boolean | undefined;
+}
+
+function settleVoid(promise: Promise, callback: VoidCallback | undefined): Promise {
+ if (callback === undefined) return promise;
+ // The callback form's promise settles only through the callback (it
+ // never rejects); fs-extra answers undefined there.
+ return promise.then(
+ () => callback(null),
+ (err: NodeJS.ErrnoException) => callback(err),
+ );
+}
+
+/** The result of a callback-form call that settles through fs's own
+ * callback: callers ignore it (fs-extra answers undefined). */
+function idle(): Promise {
+ return new Promise(() => undefined);
+}
+
+function codeOf(err: unknown): string | undefined {
+ return err instanceof Error ? (err as NodeJS.ErrnoException).code : undefined;
+}
+
+/* ── fs methods in universalify's wrapper ─────────────────────────────── */
+
+export function access(path: string, mode?: number): Promise;
+export function access(path: string, callback: VoidCallback): void;
+export function access(path: string, mode: number, callback: VoidCallback): void;
+export function access(
+ path: string,
+ mode?: number | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof mode === "function") return settleVoid(fsp.access(path), mode);
+ return settleVoid(fsp.access(path, mode ?? fs.constants.F_OK), callback);
+}
+
+export function appendFile(path: string, data: string | Uint8Array): Promise;
+export function appendFile(path: string, data: string | Uint8Array, callback: VoidCallback): void;
+export function appendFile(
+ path: string,
+ data: string | Uint8Array,
+ callback?: VoidCallback,
+): Promise {
+ return settleVoid(fsp.appendFile(path, data), callback);
+}
+
+/** fs.writeFileSync(file, bytes, { mode }) as Node runs it: open with
+ * O_WRONLY | O_CREAT | O_TRUNC and the mode, write every byte, close. */
+function writeBytesSync(file: string, data: Uint8Array, mode: number): void {
+ const fd = fs.openSync(
+ file,
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC,
+ mode,
+ );
+ try {
+ let offset = 0;
+ while (offset < data.length) offset += fs.writeSync(fd, data, offset, data.length - offset);
+ } finally {
+ fs.closeSync(fd);
+ }
+}
+
+async function writeData(
+ file: string,
+ data: string | Uint8Array,
+ options: WriteFileOptions | Utf8 | undefined,
+): Promise {
+ if (typeof data !== "string") {
+ // fs.promises.writeFile compiles string data; bytes take the
+ // synchronous write after the same microtask boundary.
+ await Promise.resolve();
+ if (options === undefined || typeof options === "string") fs.writeFileSync(file, data);
+ else writeBytesSync(file, data, options.mode ?? 0o666);
+ return;
+ }
+ if (options === undefined || typeof options === "string")
+ return fsp.writeFile(file, data, "utf8");
+ return fsp.writeFile(file, data, { encoding: "utf8", mode: options.mode ?? 0o666 });
+}
+
+export function writeFile(
+ file: string,
+ data: string | Uint8Array,
+ options?: WriteFileOptions | Utf8,
+): Promise;
+export function writeFile(file: string, data: string | Uint8Array, callback: VoidCallback): void;
+export function writeFile(
+ file: string,
+ data: string | Uint8Array,
+ options: WriteFileOptions | Utf8,
+ callback: VoidCallback,
+): void;
+export function writeFile(
+ file: string,
+ data: string | Uint8Array,
+ options?: WriteFileOptions | Utf8 | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(writeData(file, data, undefined), options);
+ return settleVoid(writeData(file, data, options), callback);
+}
+
+export function chmod(path: string, mode: number): Promise;
+export function chmod(path: string, mode: number, callback: VoidCallback): void;
+export function chmod(path: string, mode: number, callback?: VoidCallback): Promise {
+ return settleVoid(fsp.chmod(path, mode), callback);
+}
+
+export function chown(path: string, uid: number, gid: number): Promise;
+export function chown(path: string, uid: number, gid: number, callback: VoidCallback): void;
+export function chown(
+ path: string,
+ uid: number,
+ gid: number,
+ callback?: VoidCallback,
+): Promise {
+ return settleVoid(fsp.chown(path, uid, gid), callback);
+}
+
+export function lchown(path: string, uid: number, gid: number): Promise;
+export function lchown(path: string, uid: number, gid: number, callback: VoidCallback): void;
+export function lchown(
+ path: string,
+ uid: number,
+ gid: number,
+ callback?: VoidCallback,
+): Promise {
+ return settleVoid(fsp.lchown(path, uid, gid), callback);
+}
+
+export function copyFile(src: string, dest: string): Promise;
+export function copyFile(src: string, dest: string, callback: VoidCallback): void;
+export function copyFile(src: string, dest: string, callback?: VoidCallback): Promise {
+ return settleVoid(fsp.copyFile(src, dest), callback);
+}
+
+export function link(existingPath: string, newPath: string): Promise;
+export function link(existingPath: string, newPath: string, callback: VoidCallback): void;
+export function link(
+ existingPath: string,
+ newPath: string,
+ callback?: VoidCallback,
+): Promise {
+ return settleVoid(fsp.link(existingPath, newPath), callback);
+}
+
+export function lstat(path: string): Promise {
+ return fsp.lstat(path);
+}
+
+export function stat(path: string): Promise {
+ return fsp.stat(path);
+}
+
+async function makeDirectory(
+ path: string,
+ options: number | MakeDirectoryOptions | undefined,
+): Promise {
+ if (options === undefined) return fsp.mkdir(path);
+ if (typeof options === "number") return fsp.mkdir(path, { mode: options });
+ const mode = options.mode ?? 0o777;
+ if (options.recursive === true) return fsp.mkdir(path, { recursive: true, mode });
+ return fsp.mkdir(path, { mode });
+}
+
+export function mkdir(path: string, options?: number | MakeDirectoryOptions): Promise;
+export function mkdir(path: string, callback: VoidCallback): void;
+export function mkdir(
+ path: string,
+ options: number | MakeDirectoryOptions,
+ callback: VoidCallback,
+): void;
+export function mkdir(
+ path: string,
+ options?: number | MakeDirectoryOptions | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(makeDirectory(path, undefined), options);
+ return settleVoid(makeDirectory(path, options), callback);
+}
+
+export function mkdtemp(prefix: string): Promise {
+ return fsp.mkdtemp(prefix);
+}
+
+export function readdir(path: string): Promise {
+ return fsp.readdir(path);
+}
+
+export type ReadFileOptions = Utf8 | { encoding: Utf8 };
+
+export function readFile(file: string): Promise;
+export function readFile(file: string, options: ReadFileOptions): Promise;
+export function readFile(file: string, callback: Callback): void;
+export function readFile(file: string, options: ReadFileOptions, callback: Callback): void;
+export function readFile(
+ file: string,
+ options?: ReadFileOptions | Callback,
+ callback?: Callback,
+): Promise {
+ if (typeof options === "function") {
+ fsp.readFile(file).then(
+ (data) => options(null, data),
+ (err: NodeJS.ErrnoException) => options(err, Buffer.alloc(0)),
+ );
+ return idle();
+ }
+ if (options === undefined) return fsp.readFile(file);
+ if (callback !== undefined) {
+ fsp.readFile(file, "utf8").then(
+ (text) => callback(null, text),
+ (err: NodeJS.ErrnoException) => callback(err, ""),
+ );
+ return idle();
+ }
+ return fsp.readFile(file, "utf8");
+}
+
+export function readlink(path: string): Promise;
+export function readlink(path: string, callback: Callback): void;
+export function readlink(path: string, callback?: Callback): Promise {
+ if (callback !== undefined) {
+ fs.readlink(path, (err, target) => callback(err, target));
+ return idle();
+ }
+ return fsp.readlink(path);
+}
+
+export function realpath(path: string): Promise {
+ return fsp.realpath(path);
+}
+
+export function rename(oldPath: string, newPath: string): Promise;
+export function rename(oldPath: string, newPath: string, callback: VoidCallback): void;
+export function rename(oldPath: string, newPath: string, callback?: VoidCallback): Promise {
+ return settleVoid(fsp.rename(oldPath, newPath), callback);
+}
+
+/** Removes a file or directory tree; a missing path is not an error
+ * (fs-extra's rimraf and fs.rm's force). */
+async function removeTree(target: string): Promise {
+ let entry: Stats;
+ try {
+ entry = await fsp.lstat(target);
+ } catch (err) {
+ if (codeOf(err) === "ENOENT") return;
+ throw err;
+ }
+ if (entry.isDirectory()) {
+ for (const item of await fsp.readdir(target)) await removeTree(path.join(target, item));
+ await fsp.rmdir(target);
+ return;
+ }
+ await fsp.unlink(target);
+}
+
+async function rmPath(target: string, options: RmOptions | undefined): Promise {
+ const recursive = options === undefined ? false : (options.recursive ?? false);
+ const force = options === undefined ? false : (options.force ?? false);
+ if (recursive && force) return removeTree(target);
+ if (!recursive && !force) return fsp.rm(target);
+ // The mixed forms: fs.rm's own checks, then the walk.
+ if (force && !fs.existsSync(target)) return;
+ const entry = await fsp.lstat(target);
+ if (entry.isDirectory() && !recursive) return fsp.rm(target);
+ return removeTree(target);
+}
+
+export function rm(path: string, options?: RmOptions): Promise;
+export function rm(path: string, callback: VoidCallback): void;
+export function rm(path: string, options: RmOptions, callback: VoidCallback): void;
+export function rm(
+ path: string,
+ options?: RmOptions | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(rmPath(path, undefined), options);
+ return settleVoid(rmPath(path, options), callback);
+}
+
+export function rmdir(path: string): Promise;
+export function rmdir(path: string, callback: VoidCallback): void;
+export function rmdir(path: string, callback?: VoidCallback): Promise {
+ return settleVoid(fsp.rmdir(path), callback);
+}
+
+export function symlink(target: string, path: string, type?: SymlinkType): Promise;
+export function symlink(target: string, path: string, callback: VoidCallback): void;
+export function symlink(
+ target: string,
+ path: string,
+ type: SymlinkType | undefined,
+ callback: VoidCallback,
+): void;
+export function symlink(
+ target: string,
+ path: string,
+ type?: SymlinkType | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof type === "function") return settleVoid(fsp.symlink(target, path), type);
+ return settleVoid(fsp.symlink(target, path, type ?? null), callback);
+}
+
+export function truncate(path: string, len?: number): Promise;
+export function truncate(path: string, callback: VoidCallback): void;
+export function truncate(path: string, len: number, callback: VoidCallback): void;
+export function truncate(
+ path: string,
+ len?: number | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof len === "function") return settleVoid(fsp.truncate(path, 0), len);
+ return settleVoid(fsp.truncate(path, len ?? 0), callback);
+}
+
+export function unlink(path: string): Promise;
+export function unlink(path: string, callback: VoidCallback): void;
+export function unlink(path: string, callback?: VoidCallback): Promise {
+ return settleVoid(fsp.unlink(path), callback);
+}
+
+export function utimes(path: string, atime: Date, mtime: Date): Promise;
+export function utimes(path: string, atime: Date, mtime: Date, callback: VoidCallback): void;
+export function utimes(
+ path: string,
+ atime: Date,
+ mtime: Date,
+ callback?: VoidCallback,
+): Promise {
+ return settleVoid(fsp.utimes(path, atime, mtime), callback);
+}
+
+/* ── mkdirs, ensureDir, mkdirp ────────────────────────────────────────── */
+
+function dirMode(options: EnsureOptions | number | undefined): number {
+ if (options === undefined) return 0o777;
+ if (typeof options === "number") return options;
+ return options.mode ?? 0o777;
+}
+
+function makeDir(dir: string, mode: number): Promise {
+ return fsp.mkdir(path.resolve(dir), { recursive: true, mode });
+}
+
+function makeDirSync(dir: string, mode: number): void {
+ fs.mkdirSync(path.resolve(dir), { recursive: true, mode });
+}
+
+export function ensureDir(dir: string, options?: EnsureOptions | number): Promise;
+export function ensureDir(dir: string, callback: VoidCallback): void;
+export function ensureDir(
+ dir: string,
+ options: EnsureOptions | number,
+ callback: VoidCallback,
+): void;
+export function ensureDir(
+ dir: string,
+ options?: EnsureOptions | number | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(makeDir(dir, 0o777), options);
+ return settleVoid(makeDir(dir, dirMode(options)), callback);
+}
+
+export function mkdirs(dir: string, options?: EnsureOptions | number): Promise;
+export function mkdirs(dir: string, callback: VoidCallback): void;
+export function mkdirs(
+ dir: string,
+ options?: EnsureOptions | number | VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(makeDir(dir, 0o777), options);
+ return makeDir(dir, dirMode(options));
+}
+
+export function mkdirp(dir: string, options?: EnsureOptions | number): Promise;
+export function mkdirp(dir: string, callback: VoidCallback): void;
+export function mkdirp(
+ dir: string,
+ options?: EnsureOptions | number | VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(makeDir(dir, 0o777), options);
+ return makeDir(dir, dirMode(options));
+}
+
+export function ensureDirSync(dir: string, options?: EnsureOptions | number): void {
+ makeDirSync(dir, dirMode(options));
+}
+
+export function mkdirsSync(dir: string, options?: EnsureOptions | number): void {
+ makeDirSync(dir, dirMode(options));
+}
+
+export function mkdirpSync(dir: string, options?: EnsureOptions | number): void {
+ makeDirSync(dir, dirMode(options));
+}
+
+/* ── remove, emptyDir ─────────────────────────────────────────────────── */
+
+export function remove(dir: string): Promise;
+export function remove(dir: string, callback: VoidCallback): void;
+export function remove(dir: string, callback?: VoidCallback): Promise {
+ return settleVoid(removeTree(dir), callback);
+}
+
+export function removeSync(dir: string): void {
+ fs.rmSync(dir, { recursive: true, force: true });
+}
+
+async function emptyDirAsync(dir: string): Promise {
+ let items: string[];
+ try {
+ items = await fsp.readdir(dir);
+ } catch {
+ await makeDir(dir, 0o777);
+ return;
+ }
+ while (items.length > 0) {
+ const item = items.pop();
+ if (item !== undefined) await removeTree(path.join(dir, item));
+ }
+}
+
+export function emptyDir(dir: string): Promise;
+export function emptyDir(dir: string, callback: VoidCallback): void;
+export function emptyDir(dir: string, callback?: VoidCallback): Promise {
+ return settleVoid(emptyDirAsync(dir), callback);
+}
+
+export function emptyDirSync(dir: string): void {
+ let items: string[];
+ try {
+ items = fs.readdirSync(dir);
+ } catch {
+ makeDirSync(dir, 0o777);
+ return;
+ }
+ for (const item of items) fs.rmSync(path.join(dir, item), { recursive: true, force: true });
+}
+
+/* ── pathExists, ensureFile, outputFile ───────────────────────────────── */
+
+async function pathExistsAsync(file: string): Promise {
+ try {
+ await fsp.access(file);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function pathExists(file: string): Promise;
+export function pathExists(file: string, callback: Callback): void;
+export function pathExists(file: string, callback?: Callback): Promise {
+ const found = pathExistsAsync(file);
+ if (callback === undefined) return found;
+ return found.then((exists) => {
+ callback(null, exists);
+ return exists;
+ });
+}
+
+export function pathExistsSync(file: string): boolean {
+ return fs.existsSync(file);
+}
+
+async function createFileAsync(file: string): Promise {
+ try {
+ if ((await fsp.stat(file)).isFile()) return;
+ } catch {
+ // Missing: created below.
+ }
+ const dir = path.dirname(file);
+ let dirIsDirectory = true;
+ try {
+ dirIsDirectory = (await fsp.stat(dir)).isDirectory();
+ } catch (err) {
+ if (codeOf(err) !== "ENOENT") throw err;
+ await makeDir(dir, 0o777);
+ }
+ // A parent that is not a directory fails with readdir's ENOTDIR.
+ if (!dirIsDirectory) await fsp.readdir(dir);
+ await fsp.writeFile(file, "", "utf8");
+}
+
+export function ensureFile(file: string): Promise;
+export function ensureFile(file: string, callback: VoidCallback): void;
+export function ensureFile(file: string, callback?: VoidCallback): Promise {
+ return settleVoid(createFileAsync(file), callback);
+}
+
+export function ensureFileSync(file: string): void {
+ try {
+ if (fs.statSync(file).isFile()) return;
+ } catch {
+ // Missing: created below.
+ }
+ const dir = path.dirname(file);
+ if (!fs.existsSync(dir)) makeDirSync(dir, 0o777);
+ fs.writeFileSync(file, "");
+}
+
+async function outputFileAsync(
+ file: string,
+ data: string | Uint8Array,
+ options: WriteFileOptions | Utf8 | undefined,
+): Promise {
+ const dir = path.dirname(file);
+ if (!(await pathExistsAsync(dir))) await makeDir(dir, 0o777);
+ await writeData(file, data, options);
+}
+
+export function outputFile(
+ file: string,
+ data: string | Uint8Array,
+ options?: WriteFileOptions | Utf8,
+): Promise;
+export function outputFile(file: string, data: string | Uint8Array, callback: VoidCallback): void;
+export function outputFile(
+ file: string,
+ data: string | Uint8Array,
+ options: WriteFileOptions | Utf8,
+ callback: VoidCallback,
+): void;
+export function outputFile(
+ file: string,
+ data: string | Uint8Array,
+ options?: WriteFileOptions | Utf8 | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function")
+ return settleVoid(outputFileAsync(file, data, undefined), options);
+ return settleVoid(outputFileAsync(file, data, options), callback);
+}
+
+export function outputFileSync(file: string, data: string | Uint8Array): void {
+ const dir = path.dirname(file);
+ if (!fs.existsSync(dir)) makeDirSync(dir, 0o777);
+ fs.writeFileSync(file, data);
+}
+
+/* ── JSON (jsonfile) ──────────────────────────────────────────────────── */
+
+function jsonText(value: unknown, options: WriteOptions | Utf8 | undefined): string {
+ const opts = options === undefined || typeof options === "string" ? undefined : options;
+ const eol = opts === undefined ? "\n" : (opts.EOL ?? "\n");
+ const finalEol = opts === undefined ? true : (opts.finalEOL ?? true);
+ const spaces = opts === undefined ? 0 : (opts.spaces ?? 0);
+ const text = JSON.stringify(value, null, spaces) ?? "undefined";
+ return text.replace(/\n/g, eol) + (finalEol ? eol : "");
+}
+
+function jsonWriteOptions(options: WriteOptions | Utf8 | undefined): WriteFileOptions | undefined {
+ if (options === undefined || typeof options === "string") return undefined;
+ return { mode: options.mode };
+}
+
+function jsonThrows(options: ReadOptions | Utf8 | undefined): boolean {
+ if (options === undefined || typeof options === "string") return true;
+ return options.throws ?? true;
+}
+
+function prefixed(file: string, err: unknown): Error {
+ const error = err instanceof Error ? err : new Error(String(err));
+ error.message = `${file}: ${error.message}`;
+ return error;
+}
+
+function parseJson(file: string, text: string, throws: boolean): any {
+ try {
+ return JSON.parse(text.replace(/^\uFEFF/, ""));
+ } catch (err) {
+ if (!throws) return null;
+ throw prefixed(file, err);
+ }
+}
+
+async function readJsonAsync(file: string, options: ReadOptions | Utf8 | undefined): Promise {
+ const text = await fsp.readFile(file, "utf8");
+ return parseJson(file, text, jsonThrows(options));
+}
+
+// readJson's callback form is not declared.
+export function readJson(file: string, options?: ReadOptions | Utf8): Promise {
+ return readJsonAsync(file, options);
+}
+
+export function readJsonSync(file: string, options?: ReadOptions | Utf8): any {
+ const throws = jsonThrows(options);
+ let text: string;
+ try {
+ text = fs.readFileSync(file, "utf8");
+ } catch (err) {
+ // jsonfile reads inside the same try as the parse.
+ if (!throws) return null;
+ throw prefixed(file, err);
+ }
+ return parseJson(file, text, throws);
+}
+
+async function writeJsonAsync(
+ file: string,
+ value: unknown,
+ options: WriteOptions | Utf8 | undefined,
+): Promise {
+ await writeData(file, jsonText(value, options), jsonWriteOptions(options));
+}
+
+export function writeJson(file: string, value: any, options?: WriteOptions | Utf8): Promise;
+export function writeJson(file: string, value: any, callback: VoidCallback): void;
+export function writeJson(
+ file: string,
+ value: any,
+ options: WriteOptions | Utf8,
+ callback: VoidCallback,
+): void;
+export function writeJson(
+ file: string,
+ value: any,
+ options?: WriteOptions | Utf8 | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function")
+ return settleVoid(writeJsonAsync(file, value, undefined), options);
+ return settleVoid(writeJsonAsync(file, value, options), callback);
+}
+
+export function writeJsonSync(file: string, value: any, options?: WriteOptions | Utf8): void {
+ const text = jsonText(value, options);
+ if (options === undefined || typeof options === "string") fs.writeFileSync(file, text, "utf8");
+ else fs.writeFileSync(file, text, { encoding: "utf8", mode: options.mode ?? 0o666 });
+}
+
+async function outputJsonAsync(
+ file: string,
+ value: unknown,
+ options: WriteOptions | Utf8 | undefined,
+): Promise {
+ const dir = path.dirname(file);
+ if (!(await pathExistsAsync(dir))) await makeDir(dir, 0o777);
+ await writeJsonAsync(file, value, options);
+}
+
+export function outputJson(file: string, value: any, options?: WriteOptions | Utf8): Promise;
+export function outputJson(file: string, value: any, callback: VoidCallback): void;
+export function outputJson(
+ file: string,
+ value: any,
+ options: WriteOptions | Utf8,
+ callback: VoidCallback,
+): void;
+export function outputJson(
+ file: string,
+ value: any,
+ options?: WriteOptions | Utf8 | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function")
+ return settleVoid(outputJsonAsync(file, value, undefined), options);
+ return settleVoid(outputJsonAsync(file, value, options), callback);
+}
+
+export function outputJsonSync(file: string, value: any, options?: WriteOptions | Utf8): void {
+ const dir = path.dirname(file);
+ if (!fs.existsSync(dir)) makeDirSync(dir, 0o777);
+ writeJsonSync(file, value, options);
+}
+
+/* ── copy, move ───────────────────────────────────────────────────────── */
+
+function isSrcSubdir(src: string, dest: string): boolean {
+ const srcParts = path
+ .resolve(src)
+ .split(path.sep)
+ .filter((part) => part !== "");
+ const destParts = path
+ .resolve(dest)
+ .split(path.sep)
+ .filter((part) => part !== "");
+ for (let i = 0; i < srcParts.length; i++) if (destParts[i] !== srcParts[i]) return false;
+ return true;
+}
+
+function subdirMessage(src: string, dest: string, verb: string): string {
+ return `Cannot ${verb} '${src}' to a subdirectory of itself, '${dest}'.`;
+}
+
+function sameNode(a: Stats, b: Stats): boolean {
+ return b.ino !== 0 && b.dev !== 0 && a.ino === b.ino && a.dev === b.dev;
+}
+
+async function statOrNull(file: string): Promise {
+ try {
+ return await fsp.stat(file);
+ } catch (err) {
+ if (codeOf(err) === "ENOENT") return null;
+ throw err;
+ }
+}
+
+/** fs-extra's checkPaths and checkParentPaths: the source exists, is not
+ * the destination, and a directory never copies or moves into itself.
+ * Answers whether the destination exists. */
+async function checkPaths(src: string, dest: string, verb: string): Promise {
+ const srcStat = await fsp.stat(src);
+ const destStat = await statOrNull(dest);
+ if (destStat !== null && sameNode(srcStat, destStat))
+ throw new Error("Source and destination must not be the same.");
+ if (srcStat.isDirectory() && isSrcSubdir(src, dest))
+ throw new Error(subdirMessage(src, dest, verb));
+ const srcParent = path.resolve(path.dirname(src));
+ let destParent = path.resolve(path.dirname(dest));
+ while (destParent !== srcParent && destParent !== path.parse(destParent).root) {
+ const parentStat = await statOrNull(destParent);
+ if (parentStat === null) break;
+ if (sameNode(srcStat, parentStat)) throw new Error(subdirMessage(src, dest, verb));
+ destParent = path.resolve(path.dirname(destParent));
+ }
+ return destStat !== null;
+}
+
+function statOrNullSync(file: string): Stats | null {
+ try {
+ return fs.statSync(file);
+ } catch (err) {
+ if (codeOf(err) === "ENOENT") return null;
+ throw err;
+ }
+}
+
+/** checkPaths for copySync and moveSync. Answers the destination's stats,
+ * or null when it does not exist. */
+function checkPathsSync(src: string, dest: string, verb: string): Stats | null {
+ const srcStat = fs.statSync(src);
+ const destStat = statOrNullSync(dest);
+ if (destStat !== null && sameNode(srcStat, destStat))
+ throw new Error("Source and destination must not be the same.");
+ if (srcStat.isDirectory() && isSrcSubdir(src, dest))
+ throw new Error(subdirMessage(src, dest, verb));
+ const srcParent = path.resolve(path.dirname(src));
+ let destParent = path.resolve(path.dirname(dest));
+ while (destParent !== srcParent && destParent !== path.parse(destParent).root) {
+ const parentStat = statOrNullSync(destParent);
+ if (parentStat === null) break;
+ if (sameNode(srcStat, parentStat)) throw new Error(subdirMessage(src, dest, verb));
+ destParent = path.resolve(path.dirname(destParent));
+ }
+ return destStat;
+}
+
+interface CopyState {
+ overwrite: boolean;
+ errorOnExist: boolean;
+ dereference: boolean;
+ preserveTimestamps: boolean;
+ filter: CopyFilter | undefined;
+}
+
+async function included(state: CopyState, src: string, dest: string): Promise {
+ const filter = state.filter;
+ if (filter === undefined) return true;
+ return await filter(src, dest);
+}
+
+/** fs-extra's onLink: a symlink source creates a symlink at `dest`. Only
+ * a destination that is itself a symlink is replaced; over a regular file
+ * or a directory, symlink fails with EEXIST, whatever `overwrite` says. */
+async function copyLink(
+ target: string,
+ dest: string,
+ destExists: boolean,
+ dereference: boolean,
+): Promise {
+ if (!destExists) return fsp.symlink(target, dest);
+ let destLinked: string;
+ try {
+ destLinked = await fsp.readlink(dest);
+ } catch (err) {
+ const code = codeOf(err);
+ if (code === "EINVAL" || code === "UNKNOWN") return fsp.symlink(target, dest);
+ throw err;
+ }
+ const destTarget = dereference ? path.resolve(process.cwd(), destLinked) : destLinked;
+ checkLinkTargets(target, destTarget, (await fsp.stat(dest)).isDirectory());
+ await fsp.unlink(dest);
+ await fsp.symlink(target, dest);
+}
+
+function copyLinkSync(
+ target: string,
+ dest: string,
+ destExists: boolean,
+ dereference: boolean,
+): void {
+ if (!destExists) {
+ fs.symlinkSync(target, dest);
+ return;
+ }
+ let destLinked: string;
+ try {
+ destLinked = fs.readlinkSync(dest);
+ } catch (err) {
+ const code = codeOf(err);
+ if (code !== "EINVAL" && code !== "UNKNOWN") throw err;
+ fs.symlinkSync(target, dest);
+ return;
+ }
+ const destTarget = dereference ? path.resolve(process.cwd(), destLinked) : destLinked;
+ checkLinkTargets(target, destTarget, fs.statSync(dest).isDirectory());
+ fs.unlinkSync(dest);
+ fs.symlinkSync(target, dest);
+}
+
+/** Replacing a destination symlink never points it into its own target,
+ * nor removes a directory link the source lives under. */
+function checkLinkTargets(target: string, destTarget: string, destIsDirectory: boolean): void {
+ if (isSrcSubdir(target, destTarget))
+ throw new Error(`Cannot copy '${target}' to a subdirectory of itself, '${destTarget}'.`);
+ if (destIsDirectory && isSrcSubdir(destTarget, target))
+ throw new Error(`Cannot overwrite '${destTarget}' with '${target}'.`);
+}
+
+async function copyEntry(
+ state: CopyState,
+ src: string,
+ dest: string,
+ destExists: boolean,
+ filtered: boolean,
+): Promise {
+ if (!filtered && !(await included(state, src, dest))) return;
+ const srcStat = state.dereference ? await fsp.stat(src) : await fsp.lstat(src);
+ if (srcStat.isDirectory()) {
+ if (destExists && !(await fsp.stat(dest)).isDirectory())
+ throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`);
+ if (!destExists) await fsp.mkdir(dest);
+ const items = await fsp.readdir(src);
+ while (items.length > 0) {
+ const item = items.pop();
+ if (item === undefined) break;
+ const srcItem = path.join(src, item);
+ const destItem = path.join(dest, item);
+ const destItemStat = await statOrNull(destItem);
+ if (destItemStat !== null && sameNode(await fsp.stat(srcItem), destItemStat))
+ throw new Error("Source and destination must not be the same.");
+ await copyEntry(state, srcItem, destItem, destItemStat !== null, false);
+ }
+ if (!destExists) await fsp.chmod(dest, srcStat.mode);
+ return;
+ }
+ if (srcStat.isSymbolicLink()) {
+ const linked = await fsp.readlink(src);
+ await copyLink(
+ state.dereference ? path.resolve(process.cwd(), linked) : linked,
+ dest,
+ destExists,
+ state.dereference,
+ );
+ return;
+ }
+ if (destExists) {
+ if (state.overwrite) await fsp.unlink(dest);
+ else if (state.errorOnExist) throw new Error(`'${dest}' already exists`);
+ else return;
+ }
+ await fsp.copyFile(src, dest);
+ if (state.preserveTimestamps) {
+ const updated = await fsp.stat(src);
+ if ((srcStat.mode & 0o200) === 0) await fsp.chmod(dest, srcStat.mode | 0o200);
+ await fsp.utimes(dest, updated.atime, updated.mtime);
+ }
+ await fsp.chmod(dest, srcStat.mode);
+}
+
+async function copyAsync(
+ src: string,
+ dest: string,
+ options: CopyOptions | undefined,
+): Promise {
+ const state: CopyState = {
+ overwrite: options === undefined ? true : (options.overwrite ?? true),
+ errorOnExist: options === undefined ? false : (options.errorOnExist ?? false),
+ dereference: options === undefined ? false : (options.dereference ?? false),
+ preserveTimestamps: options === undefined ? false : (options.preserveTimestamps ?? false),
+ filter: options === undefined ? undefined : options.filter,
+ };
+ const destExists = await checkPaths(src, dest, "copy");
+ if (!(await included(state, src, dest))) return;
+ const parent = path.dirname(dest);
+ if (!(await pathExistsAsync(parent))) await makeDir(parent, 0o777);
+ await copyEntry(state, src, dest, destExists, true);
+}
+
+export function copy(src: string, dest: string, options?: CopyOptions): Promise;
+export function copy(src: string, dest: string, callback: VoidCallback): void;
+export function copy(src: string, dest: string, options: CopyOptions, callback: VoidCallback): void;
+export function copy(
+ src: string,
+ dest: string,
+ options?: CopyOptions | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(copyAsync(src, dest, undefined), options);
+ return settleVoid(copyAsync(src, dest, options), callback);
+}
+
+function copyEntrySync(
+ state: CopyOptionsSync,
+ src: string,
+ dest: string,
+ destExists: boolean,
+ filtered: boolean,
+): void {
+ const filter = state.filter;
+ if (!filtered && filter !== undefined && !filter(src, dest)) return;
+ const srcStat = state.dereference === true ? fs.statSync(src) : fs.lstatSync(src);
+ if (srcStat.isDirectory()) {
+ if (destExists && !fs.statSync(dest).isDirectory())
+ throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`);
+ if (!destExists) fs.mkdirSync(dest);
+ for (const item of fs.readdirSync(src)) {
+ const srcItem = path.join(src, item);
+ const destItem = path.join(dest, item);
+ // fs-extra's copyDirItem checks every entry against its destination:
+ // a destination symlink back into the source tree must not let the
+ // overwrite below unlink the source's own file.
+ const destItemStat = statOrNullSync(destItem);
+ if (destItemStat !== null && sameNode(fs.statSync(srcItem), destItemStat))
+ throw new Error("Source and destination must not be the same.");
+ copyEntrySync(state, srcItem, destItem, destItemStat !== null, false);
+ }
+ if (!destExists) fs.chmodSync(dest, srcStat.mode);
+ return;
+ }
+ if (srcStat.isSymbolicLink()) {
+ const dereference = state.dereference === true;
+ const linked = fs.readlinkSync(src);
+ copyLinkSync(
+ dereference ? path.resolve(process.cwd(), linked) : linked,
+ dest,
+ destExists,
+ dereference,
+ );
+ return;
+ }
+ if (destExists) {
+ if (state.overwrite ?? true) fs.unlinkSync(dest);
+ else if (state.errorOnExist === true) throw new Error(`'${dest}' already exists`);
+ else return;
+ }
+ fs.copyFileSync(src, dest);
+ if (state.preserveTimestamps === true) {
+ const updated = fs.statSync(src);
+ if ((srcStat.mode & 0o200) === 0) fs.chmodSync(dest, srcStat.mode | 0o200);
+ fs.utimesSync(dest, updated.atime, updated.mtime);
+ }
+ fs.chmodSync(dest, srcStat.mode);
+}
+
+export function copySync(src: string, dest: string, options?: CopyOptionsSync): void {
+ const opts: CopyOptionsSync = options ?? {};
+ const destStat = checkPathsSync(src, dest, "copy");
+ const filter = opts.filter;
+ if (filter !== undefined && !filter(src, dest)) return;
+ const parent = path.dirname(dest);
+ if (!fs.existsSync(parent)) makeDirSync(parent, 0o777);
+ copyEntrySync(opts, src, dest, destStat !== null, true);
+}
+
+async function moveAsync(
+ src: string,
+ dest: string,
+ options: MoveOptions | undefined,
+): Promise {
+ const overwrite = options === undefined ? false : (options.overwrite ?? false);
+ await checkPaths(src, dest, "move");
+ await makeDir(path.dirname(dest), 0o777);
+ if (overwrite) await removeTree(dest);
+ else if (await pathExistsAsync(dest)) throw new Error("dest already exists.");
+ try {
+ await fsp.rename(src, dest);
+ } catch (err) {
+ if (codeOf(err) !== "EXDEV") throw err;
+ await copyAsync(src, dest, { overwrite, errorOnExist: true });
+ await removeTree(src);
+ }
+}
+
+export function move(src: string, dest: string, options?: MoveOptions): Promise;
+export function move(src: string, dest: string, callback: VoidCallback): void;
+export function move(src: string, dest: string, options: MoveOptions, callback: VoidCallback): void;
+export function move(
+ src: string,
+ dest: string,
+ options?: MoveOptions | VoidCallback,
+ callback?: VoidCallback,
+): Promise {
+ if (typeof options === "function") return settleVoid(moveAsync(src, dest, undefined), options);
+ return settleVoid(moveAsync(src, dest, options), callback);
+}
+
+export function moveSync(src: string, dest: string, options?: MoveOptions): void {
+ const overwrite = options === undefined ? false : (options.overwrite ?? false);
+ checkPathsSync(src, dest, "move");
+ makeDirSync(path.dirname(dest), 0o777);
+ if (overwrite) fs.rmSync(dest, { recursive: true, force: true });
+ else if (fs.existsSync(dest)) throw new Error("dest already exists.");
+ try {
+ fs.renameSync(src, dest);
+ } catch (err) {
+ if (codeOf(err) !== "EXDEV") throw err;
+ copySync(src, dest, { overwrite, errorOnExist: true });
+ fs.rmSync(src, { recursive: true, force: true });
+ }
+}
+
+/* ── fs-extra's aliases ───────────────────────────────────────────────── */
+
+export {
+ emptyDir as emptydir,
+ emptyDirSync as emptydirSync,
+ ensureFile as createFile,
+ ensureFileSync as createFileSync,
+ readJson as readJSON,
+ readJsonSync as readJSONSync,
+ writeJson as writeJSON,
+ writeJsonSync as writeJSONSync,
+ outputJson as outputJSON,
+ outputJsonSync as outputJSONSync,
+};
diff --git a/packages/compiler/shims/fs-extra/index.ts b/packages/compiler/shims/fs-extra/index.ts
new file mode 100644
index 0000000000..43debf2d43
--- /dev/null
+++ b/packages/compiler/shims/fs-extra/index.ts
@@ -0,0 +1,9 @@
+// scriptc's built-in implementation of the `fs-extra` package, 8.x-11.x API.
+//
+// Programs whose installed fs-extra is 8.x through 11.x compile this module
+// in its place (see packages/compiler/src/frontend/package-shims.ts), and its
+// declarations stand in for @types/fs-extra. The default import, the
+// namespace import and named imports all read the same exports, as they read
+// fs-extra's single CommonJS export object under Node.
+export * from "./core/api.ts";
+export * as default from "./core/api.ts";
diff --git a/packages/compiler/shims/undici/core/agent.ts b/packages/compiler/shims/undici/core/agent.ts
new file mode 100644
index 0000000000..c59f3cd1d6
--- /dev/null
+++ b/packages/compiler/shims/undici/core/agent.ts
@@ -0,0 +1,163 @@
+// undici's Agent (lib/dispatcher/agent.js): one dispatcher per origin,
+// created by `factory` (default: a Pool, or a Client when connections is 1).
+import { Client } from "./client.ts";
+import { Dispatcher, DispatcherBase } from "./dispatcher.ts";
+import { InvalidArgumentError } from "./errors.ts";
+import { Pool } from "./pool.ts";
+import type {
+ AgentOptions,
+ DispatchHandlers,
+ DispatchOptions,
+ PoolOptions,
+ Profile,
+} from "./types.ts";
+
+interface AgentEntry {
+ dispatcher: Dispatcher;
+ count: number;
+}
+
+export class Agent extends DispatcherBase {
+ __options: PoolOptions;
+ __factory: ((origin: string | URL, opts: object) => Dispatcher) | null;
+ __clients: Map;
+ __onDrain: (origin: URL, targets: Dispatcher[]) => void;
+ __onConnect: (origin: URL, targets: Dispatcher[]) => void;
+ __onDisconnect: (origin: URL, targets: Dispatcher[], err: Error) => void;
+ __onConnectionError: (origin: URL, targets: Dispatcher[], err: Error) => void;
+
+ constructor(options: AgentOptions | undefined, profile: Profile) {
+ super(profile);
+ const opts: AgentOptions = options ?? {};
+ const interceptors = opts.interceptors;
+ if (profile.major < 7 && interceptors !== undefined && interceptors.Agent !== undefined) {
+ this.__interceptors = interceptors.Agent;
+ }
+ this.__options = {
+ keepAliveTimeout: opts.keepAliveTimeout,
+ keepAliveMaxTimeout: opts.keepAliveMaxTimeout,
+ keepAliveTimeoutThreshold: opts.keepAliveTimeoutThreshold,
+ pipelining: opts.pipelining,
+ maxHeaderSize: opts.maxHeaderSize,
+ bodyTimeout: opts.bodyTimeout,
+ headersTimeout: opts.headersTimeout,
+ strictContentLength: opts.strictContentLength,
+ maxRequestsPerClient: opts.maxRequestsPerClient,
+ maxResponseSize: opts.maxResponseSize,
+ connectTimeout: opts.connectTimeout,
+ localAddress: opts.localAddress,
+ connections: opts.connections,
+ connect: opts.connect,
+ interceptors:
+ interceptors !== undefined
+ ? { Pool: interceptors.Pool, Client: interceptors.Client }
+ : undefined,
+ };
+ const factory = opts.factory;
+ this.__factory = factory !== undefined ? factory : null;
+ this.__clients = new Map();
+ this.__onDrain = (origin: URL, targets: Dispatcher[]): void => {
+ this.emit("drain", origin, [this as Dispatcher, ...targets]);
+ };
+ this.__onConnect = (origin: URL, targets: Dispatcher[]): void => {
+ this.emit("connect", origin, [this as Dispatcher, ...targets]);
+ };
+ this.__onDisconnect = (origin: URL, targets: Dispatcher[], err: Error): void => {
+ this.emit("disconnect", origin, [this as Dispatcher, ...targets], err);
+ };
+ this.__onConnectionError = (origin: URL, targets: Dispatcher[], err: Error): void => {
+ this.emit("connectionError", origin, [this as Dispatcher, ...targets], err);
+ };
+ }
+
+ __runningCount(): number {
+ let ret = 0;
+ for (const entry of this.__clients.values()) ret += entry.dispatcher.__runningCount();
+ return ret;
+ }
+
+ __dispatch(opts: DispatchOptions, handler: DispatchHandlers): boolean {
+ const origin = opts.origin;
+ if (origin === undefined || origin === "") {
+ throw new InvalidArgumentError("opts.origin must be a non-empty string or URL.");
+ }
+ const key = typeof origin === "string" ? origin : origin.href;
+ const existing = this.__clients.get(key);
+ let dispatcher: Dispatcher;
+ if (existing !== undefined) {
+ dispatcher = existing.dispatcher;
+ } else {
+ const factory = this.__factory;
+ dispatcher =
+ factory !== null
+ ? factory(origin, this.__options)
+ : this.__defaultFactory(origin, this.__options);
+ const entry: AgentEntry = { dispatcher, count: 0 };
+ if (this.__profile.major >= 7) {
+ // 7.x closes and forgets an origin's dispatcher once all of its
+ // connections are gone.
+ const closeIfUnused = (connected: boolean): void => {
+ const current = this.__clients.get(key);
+ if (current !== undefined) {
+ if (connected) current.count -= 1;
+ if (current.count <= 0) {
+ this.__clients.delete(key);
+ if (!current.dispatcher.__isDestroyed()) current.dispatcher.close();
+ }
+ }
+ };
+ dispatcher.on("drain", this.__onDrain);
+ dispatcher.on("connect", (o: URL, targets: Dispatcher[]) => {
+ const current = this.__clients.get(key);
+ if (current !== undefined) current.count += 1;
+ this.__onConnect(o, targets);
+ });
+ dispatcher.on("disconnect", (o: URL, targets: Dispatcher[], err: Error) => {
+ closeIfUnused(true);
+ this.__onDisconnect(o, targets, err);
+ });
+ dispatcher.on("connectionError", (o: URL, targets: Dispatcher[], err: Error) => {
+ closeIfUnused(false);
+ this.__onConnectionError(o, targets, err);
+ });
+ } else {
+ dispatcher.on("drain", this.__onDrain);
+ dispatcher.on("connect", this.__onConnect);
+ dispatcher.on("disconnect", this.__onDisconnect);
+ dispatcher.on("connectionError", this.__onConnectionError);
+ }
+ this.__clients.set(key, entry);
+ }
+ return dispatcher.dispatch(opts, handler);
+ }
+
+ /** The default factory: a Client when `connections` is 1, else a Pool, of
+ * the same undici version. */
+ __defaultFactory(origin: string | URL, opts: PoolOptions): Dispatcher {
+ return opts.connections === 1
+ ? this.__createClient(origin, opts)
+ : this.__createPool(origin, opts);
+ }
+
+ __createClient(origin: string | URL, opts: PoolOptions): Dispatcher {
+ return new Client(origin, opts, this.__profile);
+ }
+
+ __createPool(origin: string | URL, opts: PoolOptions): Dispatcher {
+ return new Pool(origin, opts, this.__profile);
+ }
+
+ __close(): Promise {
+ const closing: Promise[] = [];
+ for (const entry of this.__clients.values()) closing.push(entry.dispatcher.close());
+ if (this.__profile.major >= 7) this.__clients.clear();
+ return Promise.all(closing).then(() => {});
+ }
+
+ __destroy(err: Error): Promise {
+ const destroying: Promise[] = [];
+ for (const entry of this.__clients.values()) destroying.push(entry.dispatcher.destroy(err));
+ if (this.__profile.major >= 7) this.__clients.clear();
+ return Promise.all(destroying).then(() => {});
+ }
+}
diff --git a/packages/compiler/shims/undici/core/api.ts b/packages/compiler/shims/undici/core/api.ts
new file mode 100644
index 0000000000..b94d354ce8
--- /dev/null
+++ b/packages/compiler/shims/undici/core/api.ts
@@ -0,0 +1,93 @@
+// undici's top-level request() (index.js makeDispatcher over
+// lib/api/api-request.js) and the global dispatcher
+// (lib/global.js: one process-wide slot shared by every undici copy).
+import { requestThrough } from "./dispatcher.ts";
+import type { Dispatcher, RequestOptions, ResponseData } from "./dispatcher.ts";
+import { parseOrigin, parseURL } from "./util.ts";
+import type { IncomingHttpHeaders, Profile, RequestBody, UndiciHeaders } from "./types.ts";
+
+/** request(url, options)'s options: Dispatcher.RequestOptions without
+ * origin/path, an optional method, and the dispatcher to use. */
+export interface TopLevelRequestOptions {
+ dispatcher?: Dispatcher;
+ method?: string;
+ path?: string;
+ body?: RequestBody;
+ headers?: UndiciHeaders;
+ idempotent?: boolean;
+ blocking?: boolean;
+ headersTimeout?: number | null;
+ bodyTimeout?: number | null;
+ reset?: boolean;
+ throwOnError?: boolean;
+ opaque?: unknown;
+ signal?: AbortSignal | null;
+ onInfo?: (info: { statusCode: number; headers: IncomingHttpHeaders }) => void;
+ highWaterMark?: number;
+}
+
+let globalDispatcher: Dispatcher | null = null;
+let globalDispatcherSet = false;
+
+export function setGlobalDispatcher(dispatcher: Dispatcher): void {
+ globalDispatcher = dispatcher;
+ globalDispatcherSet = true;
+}
+
+/** The global dispatcher, created on first use by the asking version's
+ * Agent (undici stores whichever copy asked first). */
+export function getGlobalDispatcher(create: () => Dispatcher): Dispatcher {
+ let dispatcher = globalDispatcher;
+ if (dispatcher === null) {
+ dispatcher = create();
+ globalDispatcher = dispatcher;
+ }
+ return dispatcher;
+}
+
+/** True once a program installed its own global dispatcher. */
+export function hasCustomGlobalDispatcher(): boolean {
+ return globalDispatcherSet;
+}
+
+export function request(
+ url: string | URL,
+ options: TopLevelRequestOptions | undefined,
+ createGlobal: () => Dispatcher,
+ profile: Profile,
+): Promise {
+ // makeDispatcher validates synchronously: an invalid URL throws at the
+ // call rather than rejecting.
+ {
+ const opts: TopLevelRequestOptions = options ?? {};
+ let target: URL;
+ const path = opts.path;
+ if (path !== undefined) {
+ const prefixed = path.startsWith("/") ? path : `/${path}`;
+ target = new URL(parseOrigin(url, profile).origin + prefixed);
+ } else {
+ target = parseURL(url, profile);
+ }
+ const dispatcher = opts.dispatcher ?? getGlobalDispatcher(createGlobal);
+ const method =
+ opts.method !== undefined && opts.method !== "" ? opts.method : opts.body ? "PUT" : "GET";
+ const dispatchOptions: RequestOptions = {
+ origin: target.origin,
+ path: target.search ? `${target.pathname}${target.search}` : target.pathname,
+ method,
+ body: opts.body,
+ headers: opts.headers,
+ idempotent: opts.idempotent,
+ blocking: opts.blocking,
+ headersTimeout: opts.headersTimeout,
+ bodyTimeout: opts.bodyTimeout,
+ reset: opts.reset,
+ throwOnError: opts.throwOnError,
+ opaque: opts.opaque,
+ signal: opts.signal,
+ onInfo: opts.onInfo,
+ highWaterMark: opts.highWaterMark,
+ };
+ return requestThrough(dispatcher, dispatchOptions, profile);
+ }
+}
diff --git a/packages/compiler/shims/undici/core/client.ts b/packages/compiler/shims/undici/core/client.ts
new file mode 100644
index 0000000000..fb3404aab9
--- /dev/null
+++ b/packages/compiler/shims/undici/core/client.ts
@@ -0,0 +1,1169 @@
+// undici's Client (lib/dispatcher/client.js + client-h1.js): HTTP/1.1 over
+// one TCP/TLS connection at a time, with keep-alive reuse, a request queue,
+// headers/body/idle timeouts and the same request lifecycle callbacks.
+import type { Socket } from "node:net";
+import { buildConnector } from "./connector.ts";
+import { channels } from "./diagnostics.ts";
+import { DispatcherBase } from "./dispatcher.ts";
+import type { Dispatcher } from "./dispatcher.ts";
+import {
+ BodyTimeoutError,
+ HeadersOverflowError,
+ HeadersTimeoutError,
+ HTTPParserError,
+ InformationalError,
+ InvalidArgumentError,
+ NotSupportedError,
+ RequestAbortedError,
+ RequestContentLengthMismatchError,
+ ResponseContentLengthMismatchError,
+ ResponseExceededMaxSizeError,
+ SocketError,
+ UndiciError,
+} from "./errors.ts";
+import type { SocketInfo } from "./errors.ts";
+import { Request } from "./request.ts";
+import { FastTimer } from "./timers.ts";
+import { parseKeepAliveTimeout, parseOrigin } from "./util.ts";
+import type {
+ ClientOptions,
+ Connector,
+ ConnectorOptions,
+ DispatchHandlers,
+ DispatchOptions,
+ Profile,
+} from "./types.ts";
+
+const TIMEOUT_NONE = 0;
+const TIMEOUT_HEADERS = 1;
+const TIMEOUT_BODY = 2;
+const TIMEOUT_IDLE = 3;
+
+export class Client extends DispatcherBase {
+ __url: URL;
+ __connector: Connector;
+ __socket: Socket | null;
+ __connecting: boolean;
+ __queue: (Request | null)[];
+ __runningIdx: number;
+ __pendingIdx: number;
+ __pipelining: number;
+ __hostHeader: string;
+ __maxHeaderSize: number;
+ __keepAliveDefaultTimeout: number;
+ __keepAliveMaxTimeout: number;
+ __keepAliveTimeoutThreshold: number;
+ __keepAliveTimeoutValue: number;
+ __headersTimeout: number;
+ __bodyTimeout: number;
+ __strictContentLength: boolean;
+ __maxRequests: number;
+ __maxResponseSize: number;
+ __resuming: number;
+ __needDrain: number;
+ __closedResolve: (() => void) | null;
+ __serverName: string | null;
+ __localAddress: string | null;
+ // Per-connection state (one socket at a time).
+ __sockNoRef: boolean;
+ __sockReset: boolean;
+ __sockBlocking: boolean;
+ __sockCounter: number;
+ __parser: ResponseParser | null;
+ __sockError: Error | null;
+ // The parser timer, held as closures over its handle (a nullable timer
+ // field does not compile under @types/node).
+ __timerCancel: (() => void) | null;
+ __timerRefresh: (() => void) | null;
+ __timerType: number;
+
+ constructor(url: string | URL, options: ClientOptions | undefined, profile: Profile) {
+ super(profile);
+ const opts: ClientOptions = options ?? {};
+ const pipelining = opts.pipelining ?? 1;
+ if (!Number.isInteger(pipelining) || pipelining < 0) {
+ throw new InvalidArgumentError("invalid pipelining");
+ }
+ if (pipelining > 1) {
+ throw new NotSupportedError(
+ "scriptc's undici client does not pipeline more than one request yet",
+ );
+ }
+ const maxHeaderSize = opts.maxHeaderSize ?? null;
+ if (maxHeaderSize !== null && !Number.isFinite(maxHeaderSize)) {
+ throw new InvalidArgumentError("invalid maxHeaderSize");
+ }
+ const connectTimeout = opts.connectTimeout ?? null;
+ if (connectTimeout !== null && (!Number.isFinite(connectTimeout) || connectTimeout < 0)) {
+ throw new InvalidArgumentError("invalid connectTimeout");
+ }
+ const keepAliveTimeout = opts.keepAliveTimeout ?? null;
+ if (
+ keepAliveTimeout !== null &&
+ (!Number.isFinite(keepAliveTimeout) || keepAliveTimeout <= 0)
+ ) {
+ throw new InvalidArgumentError("invalid keepAliveTimeout");
+ }
+ const keepAliveMaxTimeout = opts.keepAliveMaxTimeout ?? null;
+ if (
+ keepAliveMaxTimeout !== null &&
+ (!Number.isFinite(keepAliveMaxTimeout) || keepAliveMaxTimeout <= 0)
+ ) {
+ throw new InvalidArgumentError("invalid keepAliveMaxTimeout");
+ }
+ const keepAliveTimeoutThreshold = opts.keepAliveTimeoutThreshold ?? null;
+ if (keepAliveTimeoutThreshold !== null && !Number.isFinite(keepAliveTimeoutThreshold)) {
+ throw new InvalidArgumentError("invalid keepAliveTimeoutThreshold");
+ }
+ const headersTimeout = opts.headersTimeout ?? null;
+ if (headersTimeout !== null && (!Number.isInteger(headersTimeout) || headersTimeout < 0)) {
+ throw new InvalidArgumentError("headersTimeout must be a positive integer or zero");
+ }
+ const bodyTimeout = opts.bodyTimeout ?? null;
+ if (bodyTimeout !== null && (!Number.isInteger(bodyTimeout) || bodyTimeout < 0)) {
+ throw new InvalidArgumentError("bodyTimeout must be a positive integer or zero");
+ }
+ const maxRequests = opts.maxRequestsPerClient ?? 0;
+ if (!Number.isInteger(maxRequests) || maxRequests < 0) {
+ throw new InvalidArgumentError("maxRequestsPerClient must be a positive number");
+ }
+ const maxResponseSize = opts.maxResponseSize ?? -1;
+ if (!Number.isInteger(maxResponseSize) || maxResponseSize < -1) {
+ throw new InvalidArgumentError("maxResponseSize must be a positive number");
+ }
+
+ const connect = opts.connect ?? null;
+ if (typeof connect === "function") {
+ this.__connector = connect;
+ } else {
+ this.__connector = buildConnector(
+ connect !== null
+ ? {
+ ...connect,
+ timeout: connect.timeout !== undefined ? connect.timeout : connectTimeout,
+ }
+ : { timeout: connectTimeout },
+ );
+ }
+ // 5.x composes `interceptors.Client` around dispatch; 7.x dropped the
+ // option and ignores it.
+ const interceptors = opts.interceptors;
+ if (profile.major < 7 && interceptors !== undefined && interceptors.Client !== undefined) {
+ this.__interceptors = interceptors.Client;
+ }
+
+ this.__url = parseOrigin(url, profile);
+ this.__socket = null;
+ this.__connecting = false;
+ this.__queue = [];
+ this.__runningIdx = 0;
+ this.__pendingIdx = 0;
+ this.__pipelining = pipelining;
+ this.__hostHeader = `host: ${this.__url.hostname}${this.__url.port ? `:${this.__url.port}` : ""}\r\n`;
+ this.__maxHeaderSize = maxHeaderSize !== null && maxHeaderSize !== 0 ? maxHeaderSize : 16384;
+ this.__keepAliveDefaultTimeout = keepAliveTimeout ?? 4e3;
+ this.__keepAliveMaxTimeout = keepAliveMaxTimeout ?? 600e3;
+ this.__keepAliveTimeoutThreshold =
+ keepAliveTimeoutThreshold ?? (profile.major >= 7 ? 2e3 : 1e3);
+ this.__keepAliveTimeoutValue = this.__keepAliveDefaultTimeout;
+ const defaultTimeout = profile.major >= 7 ? 300e3 : 30e3;
+ this.__headersTimeout = headersTimeout ?? defaultTimeout;
+ this.__bodyTimeout = bodyTimeout ?? defaultTimeout;
+ this.__strictContentLength = opts.strictContentLength ?? true;
+ this.__maxRequests = maxRequests;
+ this.__maxResponseSize = maxResponseSize;
+ this.__resuming = 0;
+ this.__needDrain = 0;
+ this.__closedResolve = null;
+ this.__serverName = null;
+ this.__localAddress = opts.localAddress ?? null;
+ this.__sockNoRef = false;
+ this.__sockReset = false;
+ this.__sockBlocking = false;
+ this.__sockCounter = 0;
+ this.__parser = null;
+ this.__sockError = null;
+ this.__timerCancel = null;
+ this.__timerRefresh = null;
+ this.__timerType = TIMEOUT_NONE;
+ }
+
+ get pipelining(): number {
+ return this.__pipelining;
+ }
+
+ __pendingCount(): number {
+ return this.__queue.length - this.__pendingIdx;
+ }
+
+ __runningCount(): number {
+ return this.__pendingIdx - this.__runningIdx;
+ }
+
+ __sizeCount(): number {
+ return this.__queue.length - this.__runningIdx;
+ }
+
+ __connectedFlag(): boolean {
+ const socket = this.__socket;
+ return socket !== null && !this.__connecting && !socket.destroyed;
+ }
+
+ __busy(): boolean {
+ return (
+ (this.__socket !== null && (this.__sockReset || this.__sockBlocking)) ||
+ this.__sizeCount() >= (this.__pipelining || 1) ||
+ this.__pendingCount() > 0
+ );
+ }
+
+ __dispatch(opts: DispatchOptions, handler: DispatchHandlers): boolean {
+ const originValue = opts.origin;
+ const origin =
+ originValue === undefined || originValue === ""
+ ? this.__url.origin
+ : typeof originValue === "string"
+ ? originValue
+ : originValue.origin;
+ const request = new Request(origin, opts, handler, this.__profile);
+ this.__queue.push(request);
+ if (this.__resuming === 0) this.__resume(true);
+ if (this.__resuming !== 0 && this.__needDrain !== 2 && this.__busy()) this.__needDrain = 2;
+ return this.__needDrain < 2;
+ }
+
+ __close(): Promise {
+ return new Promise((resolve) => {
+ if (this.__sizeCount() === 0) {
+ this.destroy().then(() => resolve());
+ } else {
+ this.__closedResolve = resolve;
+ }
+ });
+ }
+
+ __destroy(err: Error): Promise {
+ return new Promise((resolve) => {
+ const requests = this.__queue.splice(this.__pendingIdx);
+ for (const request of requests) if (request !== null) this.__errorRequest(request, err);
+ const done = (): void => {
+ const closed = this.__closedResolve;
+ if (closed !== null) {
+ this.__closedResolve = null;
+ closed();
+ }
+ resolve();
+ };
+ const socket = this.__socket;
+ if (socket === null) {
+ queueMicrotask(done);
+ } else {
+ socket.on("close", () => done());
+ this.__destroySocket(socket, err);
+ }
+ this.__resume(false);
+ });
+ }
+
+ /** util.destroy(socket, err): remember the first error for 'close'. */
+ __destroySocket(socket: Socket, err: Error): void {
+ if (socket === this.__socket && this.__sockError === null) this.__sockError = err;
+ socket.destroy();
+ }
+
+ __errorRequest(request: Request, err: Error): void {
+ try {
+ request.onError(err);
+ } catch (e) {
+ this.emit("error", e as Error);
+ }
+ }
+
+ __socketInfo(): SocketInfo {
+ const socket = this.__socket;
+ if (socket === null) return {};
+ return {
+ localAddress: socket.localAddress,
+ localPort: socket.localPort,
+ remoteAddress: socket.remoteAddress,
+ remotePort: socket.remotePort,
+ remoteFamily: socket.remoteFamily,
+ timeout: undefined,
+ bytesWritten: socket.bytesWritten,
+ bytesRead: socket.bytesRead,
+ };
+ }
+
+ /** The parser timer: 5.x uses native timers throughout; 7.x drives the
+ * headers and body timeouts from its fast timers. */
+ __setTimeout(delay: number, type: number): void {
+ this.__clearTimer();
+ this.__timerType = type;
+ if (delay > 0) {
+ if (this.__profile.major >= 7 && (type === TIMEOUT_HEADERS || type === TIMEOUT_BODY)) {
+ const fast = new FastTimer(() => this.__onTimeout(), delay);
+ this.__timerCancel = () => fast.clear();
+ this.__timerRefresh = () => fast.refresh();
+ } else {
+ const timer = setTimeout(() => this.__onTimeout(), delay);
+ timer.unref();
+ this.__timerCancel = () => clearTimeout(timer);
+ this.__timerRefresh = () => {
+ timer.refresh();
+ };
+ }
+ }
+ }
+
+ __clearTimer(): void {
+ const cancel = this.__timerCancel;
+ this.__timerCancel = null;
+ this.__timerRefresh = null;
+ if (cancel !== null) cancel();
+ }
+
+ __refreshTimer(): void {
+ const refresh = this.__timerRefresh;
+ if (refresh !== null) refresh();
+ }
+
+ __onTimeout(): void {
+ this.__timerCancel = null;
+ this.__timerRefresh = null;
+ const socket = this.__socket;
+ const parser = this.__parser;
+ if (socket === null || parser === null) return;
+ if (this.__timerType === TIMEOUT_HEADERS) {
+ this.__destroySocket(socket, new HeadersTimeoutError());
+ } else if (this.__timerType === TIMEOUT_BODY) {
+ if (!parser.paused) this.__destroySocket(socket, new BodyTimeoutError());
+ } else if (this.__timerType === TIMEOUT_IDLE) {
+ this.__destroySocket(socket, new InformationalError("socket idle timeout"));
+ }
+ }
+
+ __connect(): void {
+ const url = this.__url;
+ let hostname = url.hostname;
+ if (hostname.startsWith("[")) {
+ const idx = hostname.indexOf("]");
+ hostname = hostname.slice(1, idx);
+ }
+ this.__connecting = true;
+ const params: ConnectorOptions = {
+ host: url.host,
+ hostname,
+ protocol: url.protocol,
+ port: url.port,
+ servername: this.__serverName ?? undefined,
+ localAddress: this.__localAddress,
+ };
+ const connectParams = {
+ host: url.host,
+ hostname,
+ protocol: url.protocol,
+ port: url.port,
+ servername: this.__serverName,
+ localAddress: this.__localAddress,
+ };
+ if (channels.beforeConnect.hasSubscribers) {
+ channels.beforeConnect.publish({ connectParams });
+ }
+ let settled = false;
+ const onConnected = (err: Error | null, socket: Socket | null): void => {
+ if (settled) return;
+ settled = true;
+ // undici awaits the connector's promise: continue on a later tick.
+ queueMicrotask(() => {
+ if (err !== null || socket === null)
+ this.__onConnectError(err ?? new SocketError("connect failed"), connectParams);
+ else this.__onConnected(socket, connectParams);
+ });
+ };
+ try {
+ this.__connector(params, onConnected);
+ } catch (err) {
+ onConnected(err as Error, null);
+ }
+ }
+
+ __onConnected(
+ socket: Socket,
+ connectParams: {
+ host: string;
+ hostname: string;
+ protocol: string;
+ port: string;
+ servername: string | null;
+ localAddress: string | null;
+ },
+ ): void {
+ this.__connecting = false;
+ if (this.destroyed) {
+ socket.destroy();
+ this.__resume(false);
+ return;
+ }
+ this.__socket = socket;
+ this.__sockNoRef = false;
+ this.__sockReset = false;
+ this.__sockBlocking = false;
+ this.__sockCounter = 0;
+ this.__sockError = null;
+ const parser = new ResponseParser(this, socket);
+ this.__parser = parser;
+ socket.on("data", (chunk: Buffer) => {
+ if (this.__socket === socket) parser.execute(chunk);
+ });
+ socket.on("error", (err: Error) => {
+ if (this.__socket === socket) this.__onSocketError(socket, err);
+ });
+ socket.on("end", () => {
+ if (this.__socket === socket) this.__onSocketEnd(socket);
+ });
+ socket.on("close", () => {
+ if (this.__socket === socket) this.__onSocketClose(socket);
+ });
+ if (channels.connected.hasSubscribers) {
+ channels.connected.publish({ connectParams });
+ }
+ this.emit("connect", this.__url, [this as Dispatcher]);
+ this.__resume(false);
+ }
+
+ __onConnectError(
+ err: Error,
+ connectParams: {
+ host: string;
+ hostname: string;
+ protocol: string;
+ port: string;
+ servername: string | null;
+ localAddress: string | null;
+ },
+ ): void {
+ this.__connecting = false;
+ if (channels.connectError.hasSubscribers) {
+ channels.connectError.publish({ connectParams, error: err });
+ }
+ this.__onError(err);
+ this.emit("connectionError", this.__url, [this as Dispatcher], err);
+ this.__resume(false);
+ }
+
+ /** client.js onError: with nothing running, a connection-level failure
+ * fails every queued request. */
+ __onError(err: Error): void {
+ const code = errorCode(err);
+ if (this.__runningCount() === 0 && code !== "UND_ERR_INFO" && code !== "UND_ERR_SOCKET") {
+ const requests = this.__queue.splice(this.__runningIdx);
+ for (const request of requests) if (request !== null) this.__errorRequest(request, err);
+ }
+ }
+
+ __onSocketError(socket: Socket, err: Error): void {
+ const parser = this.__parser;
+ if (
+ errorCode(err) === "ECONNRESET" &&
+ parser !== null &&
+ parser.statusCode !== 0 &&
+ !parser.shouldKeepAlive
+ ) {
+ parser.finishOnEof();
+ return;
+ }
+ this.__destroySocket(socket, err);
+ this.__onError(err);
+ }
+
+ __onSocketEnd(socket: Socket): void {
+ const parser = this.__parser;
+ if (parser !== null && parser.statusCode !== 0 && !parser.shouldKeepAlive) {
+ parser.finishOnEof();
+ return;
+ }
+ this.__destroySocket(socket, new SocketError("other side closed", this.__socketInfo()));
+ }
+
+ __onSocketClose(_socket: Socket): void {
+ const parser = this.__parser;
+ const recorded = this.__sockError;
+ this.__sockError = null;
+ if (
+ recorded === null &&
+ parser !== null &&
+ parser.statusCode !== 0 &&
+ !parser.shouldKeepAlive
+ ) {
+ parser.finishOnEof();
+ }
+ const err = recorded ?? new SocketError("closed", this.__socketInfo());
+ this.__parser = null;
+ this.__socket = null;
+ this.__clearTimer();
+ this.__timerType = TIMEOUT_NONE;
+ if (this.destroyed) {
+ const requests = this.__queue.splice(this.__runningIdx);
+ for (const request of requests) if (request !== null) this.__errorRequest(request, err);
+ } else if (this.__runningCount() > 0 && errorCode(err) !== "UND_ERR_INFO") {
+ const request = this.__queue[this.__runningIdx];
+ this.__queue[this.__runningIdx++] = null;
+ if (request !== null && request !== undefined) this.__errorRequest(request, err);
+ }
+ this.__pendingIdx = this.__runningIdx;
+ this.emit("disconnect", this.__url, [this as Dispatcher], err);
+ this.__resume(false);
+ }
+
+ __emitDrain(): void {
+ this.__needDrain = 0;
+ this.emit("drain", this.__url, [this as Dispatcher]);
+ }
+
+ __resume(sync: boolean): void {
+ if (this.__resuming === 2) return;
+ this.__resuming = 2;
+ this.__resumeLoop(sync);
+ this.__resuming = 0;
+ if (this.__runningIdx > 256) {
+ this.__queue.splice(0, this.__runningIdx);
+ this.__pendingIdx -= this.__runningIdx;
+ this.__runningIdx = 0;
+ }
+ }
+
+ __resumeLoop(sync: boolean): void {
+ for (;;) {
+ if (this.destroyed) return;
+ if (this.closed && this.__sizeCount() === 0) {
+ this.destroy();
+ return;
+ }
+ const socket = this.__socket;
+ const parser = this.__parser;
+ if (socket !== null && parser !== null) {
+ if (this.__sizeCount() === 0) {
+ if (!this.__sockNoRef) {
+ socket.unref();
+ this.__sockNoRef = true;
+ }
+ } else if (this.__sockNoRef) {
+ socket.ref();
+ this.__sockNoRef = false;
+ }
+ if (this.__sizeCount() === 0) {
+ if (this.__timerType !== TIMEOUT_IDLE)
+ this.__setTimeout(this.__keepAliveTimeoutValue, TIMEOUT_IDLE);
+ } else if (this.__runningCount() > 0 && parser.statusCode < 200) {
+ if (this.__timerType !== TIMEOUT_HEADERS) {
+ const request = this.__queue[this.__runningIdx];
+ const headersTimeout =
+ request !== null && request !== undefined && request.headersTimeout !== null
+ ? request.headersTimeout
+ : this.__headersTimeout;
+ this.__setTimeout(headersTimeout, TIMEOUT_HEADERS);
+ }
+ }
+ }
+
+ if (this.__busy()) {
+ this.__needDrain = 2;
+ } else if (this.__needDrain === 2) {
+ if (sync) {
+ this.__needDrain = 1;
+ process.nextTick(() => this.__emitDrain());
+ } else {
+ this.__emitDrain();
+ }
+ continue;
+ }
+
+ if (this.__pendingCount() === 0) return;
+ if (this.__runningCount() >= (this.__pipelining || 1)) return;
+
+ const request = this.__queue[this.__pendingIdx]!;
+ if (this.__url.protocol === "https:" && this.__serverName !== request.servername) {
+ if (this.__runningCount() > 0) return;
+ this.__serverName = request.servername;
+ if (socket !== null) {
+ this.__destroySocket(socket, new InformationalError("servername changed"));
+ return;
+ }
+ }
+ if (this.__connecting) return;
+ if (socket === null) {
+ this.__connect();
+ continue;
+ }
+ if (socket.destroyed || this.__sockReset || this.__sockBlocking) return;
+ if (this.__runningCount() > 0 && !request.idempotent) return;
+
+ if (!request.aborted && this.__write(request, socket)) {
+ this.__pendingIdx++;
+ } else {
+ this.__queue.splice(this.__pendingIdx, 1);
+ }
+ }
+ }
+
+ __write(request: Request, socket: Socket): boolean {
+ const method = request.method;
+ const body = request.body;
+ const expectsPayload = method === "PUT" || method === "POST" || method === "PATCH";
+ // util.bodyLength: a missing body has length 0.
+ let contentLength: number | null = body !== null ? body.length : 0;
+ if (contentLength === 0 && !expectsPayload) contentLength = null;
+ const declared = request.contentLength;
+ const sendsLength =
+ method !== "GET" && method !== "HEAD" && method !== "OPTIONS" && method !== "TRACE";
+ const mismatch =
+ declared !== null &&
+ declared !== contentLength &&
+ (this.__profile.major < 7 || (sendsLength && contentLength !== null && contentLength > 0));
+ if (mismatch) {
+ if (this.__strictContentLength) {
+ this.__errorRequest(request, new RequestContentLengthMismatchError());
+ return false;
+ }
+ process.emitWarning(new RequestContentLengthMismatchError());
+ }
+
+ const abort = (err?: Error): void => {
+ if (request.aborted || request.completed) return;
+ this.__errorRequest(request, err ?? new RequestAbortedError());
+ this.__destroySocket(socket, new InformationalError("aborted"));
+ };
+ try {
+ request.onConnect(abort);
+ } catch (err) {
+ this.__errorRequest(request, err as Error);
+ }
+ if (request.aborted) return false;
+
+ if (method === "HEAD") this.__sockReset = true;
+ if (request.reset !== null) this.__sockReset = request.reset;
+ if (this.__maxRequests > 0 && this.__sockCounter++ >= this.__maxRequests)
+ this.__sockReset = true;
+ if (request.blocking) this.__sockBlocking = true;
+
+ let header = `${method} ${request.path} HTTP/1.1\r\n`;
+ header += request.host !== null ? `host: ${request.host}\r\n` : this.__hostHeader;
+ if (this.__pipelining !== 0 && !(this.__profile.major >= 7 && this.__sockReset)) {
+ header += "connection: keep-alive\r\n";
+ } else {
+ header += "connection: close\r\n";
+ }
+ header += request.headers;
+ if (channels.sendHeaders.hasSubscribers) {
+ channels.sendHeaders.publish({ request: request.diagnosticView(), headers: header });
+ }
+ if (body === null) {
+ if (contentLength === 0) socket.write(`${header}content-length: 0\r\n\r\n`);
+ else socket.write(`${header}\r\n`);
+ } else {
+ socket.write(`${header}content-length: ${contentLength}\r\n\r\n`);
+ socket.write(body);
+ request.onBodySent(body);
+ if (!expectsPayload && (this.__profile.major < 7 || request.reset !== false))
+ this.__sockReset = true;
+ }
+ request.onRequestSent();
+ return true;
+ }
+
+ // ResponseParser callbacks.
+
+ __onHeadersComplete(parser: ResponseParser, socket: Socket): number {
+ const request = this.__queue[this.__runningIdx];
+ if (request === null || request === undefined || socket.destroyed) return -1;
+ const statusCode = parser.statusCode;
+ if (statusCode === 100) {
+ this.__destroySocket(socket, new SocketError("bad response", this.__socketInfo()));
+ return -1;
+ }
+ if (statusCode >= 200) {
+ const bodyTimeout = request.bodyTimeout !== null ? request.bodyTimeout : this.__bodyTimeout;
+ this.__setTimeout(bodyTimeout, TIMEOUT_BODY);
+ }
+ if (parser.shouldKeepAlive && this.__pipelining !== 0) {
+ const hint =
+ parser.keepAliveHeader !== "" ? parseKeepAliveTimeout(parser.keepAliveHeader) : null;
+ if (hint !== null) {
+ const timeout = Math.min(
+ hint - this.__keepAliveTimeoutThreshold,
+ this.__keepAliveMaxTimeout,
+ );
+ if (timeout <= 0) this.__sockReset = true;
+ else this.__keepAliveTimeoutValue = timeout;
+ } else {
+ this.__keepAliveTimeoutValue = this.__keepAliveDefaultTimeout;
+ }
+ } else {
+ this.__sockReset = true;
+ }
+ let pause = false;
+ try {
+ pause =
+ request.onHeaders(statusCode, parser.headers, () => parser.resume(), parser.statusText) ===
+ false;
+ } catch (err) {
+ this.__destroySocket(socket, err as Error);
+ return -1;
+ }
+ if (request.method === "HEAD") return 1;
+ if (statusCode < 200) return 1;
+ if (this.__sockBlocking) {
+ this.__sockBlocking = false;
+ this.__resume(false);
+ }
+ return pause ? 2 : 0;
+ }
+
+ __onBody(parser: ResponseParser, socket: Socket, chunk: Buffer): boolean {
+ const request = this.__queue[this.__runningIdx];
+ if (request === null || request === undefined || socket.destroyed) return false;
+ if (this.__timerType === TIMEOUT_BODY) this.__refreshTimer();
+ if (this.__maxResponseSize > -1 && parser.bytesRead + chunk.length > this.__maxResponseSize) {
+ this.__destroySocket(socket, new ResponseExceededMaxSizeError());
+ return false;
+ }
+ parser.bytesRead += chunk.length;
+ try {
+ return request.onData(chunk) !== false;
+ } catch (err) {
+ this.__destroySocket(socket, err as Error);
+ return false;
+ }
+ }
+
+ /** Returns false when the socket was torn down (stop parsing). */
+ __onMessageComplete(parser: ResponseParser, socket: Socket, trailers: Buffer[]): boolean {
+ if (socket.destroyed && (parser.statusCode === 0 || parser.shouldKeepAlive)) return false;
+ const request = this.__queue[this.__runningIdx];
+ if (request === null || request === undefined) return false;
+ const statusCode = parser.statusCode;
+ const shouldKeepAlive = parser.shouldKeepAlive;
+ const contentLength = parser.contentLength;
+ const bytesRead = parser.bytesRead;
+ parser.reset();
+ if (statusCode < 200) return true;
+ if (request.method !== "HEAD" && contentLength >= 0 && bytesRead !== contentLength) {
+ this.__destroySocket(socket, new ResponseContentLengthMismatchError());
+ return false;
+ }
+ try {
+ request.onComplete(trailers);
+ } catch (err) {
+ this.__errorRequest(request, err as Error);
+ }
+ this.__queue[this.__runningIdx++] = null;
+ if (!shouldKeepAlive) {
+ this.__destroySocket(socket, new InformationalError("reset"));
+ return false;
+ }
+ if (this.__sockReset && this.__runningCount() === 0) {
+ this.__destroySocket(socket, new InformationalError("reset"));
+ return false;
+ }
+ if (this.__pipelining === 1) setImmediate(() => this.__resume(false));
+ else this.__resume(false);
+ return true;
+ }
+
+ __onParseError(socket: Socket, err: Error): void {
+ this.__destroySocket(socket, err);
+ }
+}
+
+function errorCode(err: Error): string {
+ if (err instanceof UndiciError) return err.code;
+ return (err as NodeJS.ErrnoException).code ?? "";
+}
+
+const CRLF = Buffer.from("\r\n");
+
+/** The value of an ASCII hex digit, or -1. */
+function hexValue(c: number): number {
+ if (c >= 0x30 && c <= 0x39) return c - 0x30;
+ if (c >= 0x41 && c <= 0x46) return c - 0x37;
+ if (c >= 0x61 && c <= 0x66) return c - 0x57;
+ return -1;
+}
+
+/** An incremental HTTP/1.1 response parser (the subset of llhttp undici
+ * relies on): status line, raw header pairs, Content-Length, chunked and
+ * read-until-close bodies, trailers, and pause/resume back-pressure. */
+class ResponseParser {
+ client: Client;
+ socket: Socket;
+ /** Unconsumed bytes of the current arrival, then the later arrivals in
+ * order. */
+ buf: Buffer;
+ queue: Buffer[];
+ state: number;
+ paused: boolean;
+ statusCode: number;
+ statusText: string;
+ headers: Buffer[];
+ headersSize: number;
+ versionMinor: number;
+ contentLength: number;
+ chunked: boolean;
+ connectionHeader: string;
+ keepAliveHeader: string;
+ shouldKeepAlive: boolean;
+ remaining: number;
+ bytesRead: number;
+ trailers: Buffer[];
+ skipBody: boolean;
+ /** The chunk-size line read so far: its value, its digit count, whether
+ * an extension or whitespace follows the digits, and whether its CR was
+ * read. Extensions are skipped as they arrive, so the line keeps no
+ * bytes. */
+ chunkSize: number;
+ chunkDigits: number;
+ chunkExtension: boolean;
+ chunkCR: boolean;
+
+ constructor(client: Client, socket: Socket) {
+ this.client = client;
+ this.socket = socket;
+ this.buf = Buffer.alloc(0);
+ this.queue = [];
+ this.state = 0;
+ this.paused = false;
+ this.statusCode = 0;
+ this.statusText = "";
+ this.headers = [];
+ this.headersSize = 0;
+ this.versionMinor = 1;
+ this.contentLength = -1;
+ this.chunked = false;
+ this.connectionHeader = "";
+ this.keepAliveHeader = "";
+ this.shouldKeepAlive = true;
+ this.remaining = 0;
+ this.bytesRead = 0;
+ this.trailers = [];
+ this.skipBody = false;
+ this.chunkSize = 0;
+ this.chunkDigits = 0;
+ this.chunkExtension = false;
+ this.chunkCR = false;
+ }
+
+ reset(): void {
+ this.state = 0;
+ this.statusCode = 0;
+ this.statusText = "";
+ this.headers = [];
+ this.headersSize = 0;
+ this.contentLength = -1;
+ this.chunked = false;
+ this.connectionHeader = "";
+ this.keepAliveHeader = "";
+ this.remaining = 0;
+ this.bytesRead = 0;
+ this.trailers = [];
+ this.skipBody = false;
+ }
+
+ execute(data: Buffer): void {
+ if (this.buf.length === 0 && this.queue.length === 0) this.buf = data;
+ else this.queue.push(data);
+ if (!this.paused) this.run();
+ }
+
+ /** Moves the next queued arrival into `buf`, after the bytes left in it;
+ * false when nothing is queued. Only a partial status, header or trailer
+ * line (bounded by maxHeaderSize) or a partial chunk-data CRLF is copied. */
+ more(): boolean {
+ const next = this.queue.shift();
+ if (next === undefined) return false;
+ this.buf = this.buf.length === 0 ? next : Buffer.concat([this.buf, next]);
+ return true;
+ }
+
+ resume(): void {
+ if (!this.paused) return;
+ this.paused = false;
+ this.socket.resume();
+ this.run();
+ }
+
+ pause(): void {
+ this.paused = true;
+ this.socket.pause();
+ }
+
+ /** EOF while reading a close-delimited body completes the message. */
+ finishOnEof(): void {
+ if (this.state === 6) {
+ this.client.__onMessageComplete(this, this.socket, []);
+ }
+ }
+
+ fail(message: string, code: string): void {
+ // undici 7.x wraps llhttp's reason and leaves the code unset (its
+ // createError indexes the error-name table with the error number);
+ // 5.x reports the reason and its HPE_ code.
+ const error =
+ this.client.__profile.major >= 7
+ ? new HTTPParserError(`Response does not match the HTTP/1.1 protocol (${message})`)
+ : new HTTPParserError(message, code, "");
+ this.client.__onParseError(this.socket, error);
+ this.state = 99;
+ }
+
+ line(): Buffer | null {
+ const idx = this.buf.indexOf(CRLF);
+ if (idx === -1) return null;
+ const out = this.buf.subarray(0, idx);
+ this.buf = this.buf.subarray(idx + 2);
+ return out;
+ }
+
+ run(): void {
+ while (!this.paused && this.state !== 99) {
+ if (this.state === 0) {
+ // Status line.
+ const line = this.line();
+ if (line === null) {
+ if (this.buf.length > this.client.__maxHeaderSize) {
+ this.client.__onParseError(this.socket, new HeadersOverflowError());
+ this.state = 99;
+ return;
+ }
+ if (this.more()) continue;
+ return;
+ }
+ if (line.length === 0) continue;
+ const text = line.toString("latin1");
+ const m = /^HTTP\/1\.([01]) (\d{3})(?: (.*))?$/.exec(text);
+ if (m === null) {
+ this.fail("Response does not match the HTTP/1.1 protocol", "INVALID_CONSTANT");
+ return;
+ }
+ this.versionMinor = Number(m[1]);
+ this.statusCode = Number(m[2]);
+ this.statusText = m[3] ?? "";
+ this.headersSize = line.length;
+ this.state = 1;
+ } else if (this.state === 1 || this.state === 5) {
+ // Header (1) or trailer (5) lines.
+ const line = this.line();
+ if (line === null) {
+ if (this.headersSize + this.buf.length > this.client.__maxHeaderSize) {
+ this.client.__onParseError(this.socket, new HeadersOverflowError());
+ this.state = 99;
+ return;
+ }
+ if (this.more()) continue;
+ return;
+ }
+ this.headersSize += line.length;
+ if (this.headersSize > this.client.__maxHeaderSize) {
+ this.client.__onParseError(this.socket, new HeadersOverflowError());
+ this.state = 99;
+ return;
+ }
+ if (line.length > 0) {
+ const colon = line.indexOf(0x3a);
+ if (colon <= 0) {
+ this.fail("Invalid header token", "INVALID_HEADER_TOKEN");
+ return;
+ }
+ const name = line.subarray(0, colon);
+ let start = colon + 1;
+ let end = line.length;
+ while (start < end && (line[start] === 0x20 || line[start] === 0x09)) start++;
+ while (end > start && (line[end - 1] === 0x20 || line[end - 1] === 0x09)) end--;
+ const value = line.subarray(start, end);
+ if (this.state === 5) {
+ this.trailers.push(Buffer.from(name), Buffer.from(value));
+ continue;
+ }
+ this.headers.push(Buffer.from(name), Buffer.from(value));
+ const lower = name.toString("latin1").toLowerCase();
+ if (lower === "content-length") {
+ const n = Number(value.toString("latin1"));
+ if (!Number.isInteger(n) || n < 0) {
+ this.fail("Invalid character in Content-Length", "INVALID_CONTENT_LENGTH");
+ return;
+ }
+ this.contentLength = n;
+ } else if (lower === "transfer-encoding") {
+ this.chunked = /(?:^|,)\s*chunked\s*$/i.test(value.toString("latin1"));
+ } else if (lower === "connection") {
+ this.connectionHeader = value.toString("latin1").toLowerCase();
+ } else if (lower === "keep-alive") {
+ this.keepAliveHeader = value.toString("latin1");
+ }
+ continue;
+ }
+ if (this.state === 5) {
+ if (!this.complete()) return;
+ continue;
+ }
+ // End of headers.
+ const noBody = this.statusCode < 200 || this.statusCode === 204 || this.statusCode === 304;
+ const closeRequested = /(?:^|,)\s*close\s*(?:,|$)/.test(this.connectionHeader);
+ const keepAliveRequested = /(?:^|,)\s*keep-alive\s*(?:,|$)/.test(this.connectionHeader);
+ this.shouldKeepAlive = this.versionMinor === 1 ? !closeRequested : keepAliveRequested;
+ if (!noBody && !this.chunked && this.contentLength < 0) this.shouldKeepAlive = false;
+ const ret = this.client.__onHeadersComplete(this, this.socket);
+ if (ret === -1) {
+ this.state = 99;
+ return;
+ }
+ this.skipBody = ret === 1 || noBody;
+ if (this.skipBody) {
+ if (!this.complete()) return;
+ continue;
+ }
+ if (this.chunked) {
+ this.state = 3;
+ } else if (this.contentLength >= 0) {
+ this.remaining = this.contentLength;
+ this.state = 2;
+ if (this.remaining === 0) {
+ if (ret === 2) this.pause();
+ if (!this.complete()) return;
+ continue;
+ }
+ } else {
+ this.state = 6;
+ }
+ if (ret === 2) this.pause();
+ } else if (this.state === 2) {
+ // Content-Length body.
+ if (this.buf.length === 0) {
+ if (this.more()) continue;
+ return;
+ }
+ const n = Math.min(this.remaining, this.buf.length);
+ const chunk = this.buf.subarray(0, n);
+ this.buf = this.buf.subarray(n);
+ this.remaining -= n;
+ const more = this.client.__onBody(this, this.socket, Buffer.from(chunk));
+ if (this.socket.destroyed) {
+ this.state = 99;
+ return;
+ }
+ if (!more) this.pause();
+ if (this.remaining === 0) {
+ if (!this.complete()) return;
+ }
+ } else if (this.state === 3) {
+ // Chunk size line: hex digits, then optional whitespace and
+ // extensions, then CRLF. It is read byte by byte as it arrives.
+ if (this.buf.length === 0) {
+ if (this.more()) continue;
+ return;
+ }
+ let i = 0;
+ let done = false;
+ for (; i < this.buf.length && !done; i++) {
+ const c = this.buf[i]!;
+ if (this.chunkCR) {
+ if (c !== 0x0a) {
+ this.fail("Expected LF after chunk size", "STRICT");
+ return;
+ }
+ done = true;
+ } else if (c === 0x0d) {
+ if (this.chunkDigits === 0) {
+ this.fail("Invalid character in chunk size", "INVALID_CHUNK_SIZE");
+ return;
+ }
+ this.chunkCR = true;
+ } else if (this.chunkExtension) {
+ continue;
+ } else if (hexValue(c) >= 0) {
+ this.chunkSize = this.chunkSize * 16 + hexValue(c);
+ this.chunkDigits++;
+ if (this.chunkSize > Number.MAX_SAFE_INTEGER) {
+ this.fail("Chunk size overflow", "INVALID_CHUNK_SIZE");
+ return;
+ }
+ } else if (this.chunkDigits > 0 && (c === 0x3b || c === 0x20 || c === 0x09)) {
+ this.chunkExtension = true;
+ } else {
+ this.fail("Invalid character in chunk size", "INVALID_CHUNK_SIZE");
+ return;
+ }
+ }
+ this.buf = this.buf.subarray(i);
+ if (!done) continue;
+ const size = this.chunkSize;
+ this.chunkSize = 0;
+ this.chunkDigits = 0;
+ this.chunkExtension = false;
+ this.chunkCR = false;
+ if (size === 0) {
+ this.state = 5;
+ } else {
+ this.remaining = size;
+ this.state = 4;
+ }
+ } else if (this.state === 4) {
+ // Chunk data, then its CRLF.
+ if (this.remaining > 0) {
+ if (this.buf.length === 0) {
+ if (this.more()) continue;
+ return;
+ }
+ const n = Math.min(this.remaining, this.buf.length);
+ const chunk = this.buf.subarray(0, n);
+ this.buf = this.buf.subarray(n);
+ this.remaining -= n;
+ const more = this.client.__onBody(this, this.socket, Buffer.from(chunk));
+ if (this.socket.destroyed) {
+ this.state = 99;
+ return;
+ }
+ if (!more) this.pause();
+ continue;
+ }
+ if (this.buf.length < 2) {
+ if (this.more()) continue;
+ return;
+ }
+ if (this.buf[0] !== 0x0d || this.buf[1] !== 0x0a) {
+ this.fail("Expected LF after chunk data", "STRICT");
+ return;
+ }
+ this.buf = this.buf.subarray(2);
+ this.state = 3;
+ } else if (this.state === 6) {
+ // Read until the connection closes.
+ if (this.buf.length === 0) {
+ if (this.more()) continue;
+ return;
+ }
+ const chunk = this.buf;
+ this.buf = Buffer.alloc(0);
+ const more = this.client.__onBody(this, this.socket, Buffer.from(chunk));
+ if (this.socket.destroyed) {
+ this.state = 99;
+ return;
+ }
+ if (!more) this.pause();
+ } else {
+ return;
+ }
+ }
+ }
+
+ /** Message complete: hand the trailers over and get ready for the next
+ * response on this connection. Returns false when parsing must stop. */
+ complete(): boolean {
+ const trailers = this.trailers;
+ const ok = this.client.__onMessageComplete(this, this.socket, trailers);
+ if (!ok) {
+ this.state = 99;
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/packages/compiler/shims/undici/core/connector.ts b/packages/compiler/shims/undici/core/connector.ts
new file mode 100644
index 0000000000..f264912ea5
--- /dev/null
+++ b/packages/compiler/shims/undici/core/connector.ts
@@ -0,0 +1,157 @@
+// undici's buildConnector (lib/core/connect.js) over node:net and node:tls.
+import * as net from "node:net";
+import * as tls from "node:tls";
+import { ConnectTimeoutError, InvalidArgumentError, NotSupportedError } from "./errors.ts";
+import type {
+ Connector,
+ ConnectorBuildOptions,
+ ConnectorCallback,
+ ConnectorOptions,
+ LookupAddress,
+} from "./types.ts";
+
+export function buildConnector(options: ConnectorBuildOptions): Connector {
+ const maxCachedSessions = options.maxCachedSessions;
+ if (
+ maxCachedSessions !== undefined &&
+ maxCachedSessions !== null &&
+ (!Number.isInteger(maxCachedSessions) || maxCachedSessions < 0)
+ ) {
+ throw new InvalidArgumentError("maxCachedSessions must be a positive integer or zero");
+ }
+ if (options.socketPath !== undefined && options.socketPath !== null) {
+ throw new NotSupportedError("scriptc's undici connector does not dial socketPath yet");
+ }
+ const timeout =
+ options.timeout === undefined || options.timeout === null ? 10e3 : options.timeout;
+ const lookup = options.lookup;
+ // The plain-TCP dial, with the caller's resolver when one is configured.
+ let dialTcp: (port: number, host: string) => net.Socket;
+ if (lookup !== undefined) {
+ // net.connect's lowered resolver answers every address at once; adapt a
+ // resolver that answers a single address string.
+ const userLookup: (
+ hostname: string,
+ options: unknown,
+ callback: (
+ err: NodeJS.ErrnoException | null,
+ address: string | LookupAddress[],
+ family?: number,
+ ) => void,
+ ) => void = lookup;
+ const resolver = (
+ hostname: string,
+ lookupOptions: unknown,
+ callback: (err: NodeJS.ErrnoException | null, addresses: LookupAddress[]) => void,
+ ): void => {
+ userLookup(hostname, lookupOptions, (err, address, family) => {
+ if (err !== null) callback(err, []);
+ else if (typeof address === "string") {
+ callback(null, [{ address, family: family ?? (net.isIPv6(address) ? 6 : 4) }]);
+ } else callback(null, address);
+ });
+ };
+ dialTcp = (port: number, host: string): net.Socket =>
+ net.connect({ port, host, autoSelectFamily: true, lookup: resolver });
+ } else {
+ dialTcp = (port: number, host: string): net.Socket => net.connect({ port, host });
+ }
+ const rejectUnauthorized = options.rejectUnauthorized;
+ const ca = options.ca;
+ const defaultServername = options.servername;
+
+ return (params: ConnectorOptions, callback: ConnectorCallback): net.Socket => {
+ let pending: ConnectorCallback | null = callback;
+ const https = params.protocol === "https:";
+ const port = params.port !== "" ? Number(params.port) : https ? 443 : 80;
+ let socket: net.Socket;
+ if (https) {
+ if (lookup !== undefined) {
+ throw new NotSupportedError(
+ "scriptc's undici connector does not apply a custom lookup to TLS connections yet",
+ );
+ }
+ const servername =
+ params.servername !== undefined && params.servername !== ""
+ ? params.servername
+ : defaultServername !== undefined
+ ? defaultServername
+ : serverNameOf(params.host ?? params.hostname);
+ socket = tls.connect({
+ port,
+ host: params.hostname,
+ servername,
+ rejectUnauthorized: rejectUnauthorized ?? true,
+ ...(ca !== undefined ? { ca } : {}),
+ });
+ } else {
+ socket = dialTcp(port, params.hostname);
+ }
+
+ let cancelTimer: (() => void) | null = null;
+ if (timeout !== 0) {
+ const timer = setTimeout(() => {
+ cancelTimer = null;
+ setImmediate(() => {
+ const err = new ConnectTimeoutError();
+ socket.destroy();
+ if (pending !== null) {
+ const cb = pending;
+ pending = null;
+ cb(err, null);
+ }
+ });
+ }, timeout);
+ cancelTimer = () => clearTimeout(timer);
+ }
+ const cancelTimeout = (): void => {
+ const cancel = cancelTimer;
+ cancelTimer = null;
+ if (cancel !== null) cancel();
+ };
+
+ socket.setNoDelay(true);
+ const onConnected = (): void => {
+ cancelTimeout();
+ if (pending !== null) {
+ const cb = pending;
+ pending = null;
+ cb(null, socket);
+ }
+ };
+ if (https) socket.once("secureConnect", onConnected);
+ else socket.once("connect", onConnected);
+ socket.on("error", (err: Error) => {
+ cancelTimeout();
+ if (pending !== null) {
+ const cb = pending;
+ pending = null;
+ cb(err, null);
+ }
+ });
+ return socket;
+ };
+}
+
+/** undici's util.getServerName: the host without its port, or null for an
+ * IP literal (no SNI). */
+function serverNameOf(host: string): string | undefined {
+ if (host === "") return undefined;
+ let servername = host;
+ if (servername.startsWith("[")) {
+ const idx = servername.indexOf("]");
+ servername = idx === -1 ? servername : servername.slice(1, idx);
+ } else {
+ const idx = servername.indexOf(":");
+ if (idx !== -1) servername = servername.slice(0, idx);
+ }
+ return net.isIP(servername) !== 0 ? undefined : servername;
+}
+
+/** The `buildConnector.*` type namespace of undici's types/connector.d.ts. */
+export declare namespace buildConnector {
+ type BuildOptions = ConnectorBuildOptions;
+ type Options = ConnectorOptions;
+ type Callback = ConnectorCallback;
+ type connector = Connector;
+}
diff --git a/packages/compiler/shims/undici/core/diagnostics-types.ts b/packages/compiler/shims/undici/core/diagnostics-types.ts
new file mode 100644
index 0000000000..3b84915ec8
--- /dev/null
+++ b/packages/compiler/shims/undici/core/diagnostics-types.ts
@@ -0,0 +1,68 @@
+// undici's DiagnosticsChannel message types (types/diagnostics-channel.d.ts).
+import type { Socket } from "node:net";
+import type { Connector } from "./types.ts";
+
+export declare namespace DiagnosticsChannel {
+ interface Request {
+ origin?: string | URL;
+ completed: boolean;
+ method?: string;
+ path: string;
+ headers: string;
+ addHeader(key: string, value: string): Request;
+ }
+ interface Response {
+ statusCode: number;
+ statusText: string;
+ headers: Array;
+ }
+ type Error = unknown;
+ interface ConnectParams {
+ host: string;
+ hostname: string;
+ protocol: string;
+ port: string;
+ servername: string | null;
+ }
+ type Connector = import("./types.ts").Connector;
+ interface RequestCreateMessage {
+ request: Request;
+ }
+ interface RequestBodySentMessage {
+ request: Request;
+ }
+ interface RequestHeadersMessage {
+ request: Request;
+ response: Response;
+ }
+ interface RequestTrailersMessage {
+ request: Request;
+ trailers: Array;
+ }
+ interface RequestErrorMessage {
+ request: Request;
+ error: Error;
+ }
+ interface ClientSendHeadersMessage {
+ request: Request;
+ headers: string;
+ socket: Socket;
+ }
+ interface ClientBeforeConnectMessage {
+ connectParams: ConnectParams;
+ connector: Connector;
+ }
+ interface ClientConnectedMessage {
+ socket: Socket;
+ connectParams: ConnectParams;
+ connector: Connector;
+ }
+ interface ClientConnectErrorMessage {
+ error: Error;
+ socket: Socket;
+ connectParams: ConnectParams;
+ connector: Connector;
+ }
+}
+
+export type { Connector };
diff --git a/packages/compiler/shims/undici/core/diagnostics.ts b/packages/compiler/shims/undici/core/diagnostics.ts
new file mode 100644
index 0000000000..4efd897f93
--- /dev/null
+++ b/packages/compiler/shims/undici/core/diagnostics.ts
@@ -0,0 +1,16 @@
+// undici's diagnostics_channel publications (lib/core/diagnostics.js). The
+// channel names and message shapes are undici's; subscribers see the same
+// request objects across one request's lifetime.
+import { channel } from "node:diagnostics_channel";
+
+export const channels = {
+ create: channel("undici:request:create"),
+ bodySent: channel("undici:request:bodySent"),
+ headers: channel("undici:request:headers"),
+ trailers: channel("undici:request:trailers"),
+ error: channel("undici:request:error"),
+ beforeConnect: channel("undici:client:beforeConnect"),
+ connected: channel("undici:client:connected"),
+ connectError: channel("undici:client:connectError"),
+ sendHeaders: channel("undici:client:sendHeaders"),
+};
diff --git a/packages/compiler/shims/undici/core/dispatcher.ts b/packages/compiler/shims/undici/core/dispatcher.ts
new file mode 100644
index 0000000000..cd195b5dad
--- /dev/null
+++ b/packages/compiler/shims/undici/core/dispatcher.ts
@@ -0,0 +1,568 @@
+// undici's Dispatcher (lib/dispatcher/dispatcher.js), DispatcherBase
+// (dispatcher-base.js) and the request() API's handler (lib/api/api-request.js).
+import { EventEmitter } from "node:events";
+import { BodyReadable } from "./readable.ts";
+import {
+ ClientClosedError,
+ ClientDestroyedError,
+ InvalidArgumentError,
+ RequestAbortedError,
+ ResponseStatusCodeError,
+} from "./errors.ts";
+import { parseHeaders, rawHeaderStrings } from "./util.ts";
+import type {
+ DispatchController,
+ DispatchFn,
+ DispatchHandlers,
+ DispatchInterceptor,
+ DispatchOptions,
+ IncomingHttpHeaders,
+ Profile,
+} from "./types.ts";
+
+/** Dispatcher.RequestOptions: the dispatch options (which carry the
+ * request members, see DispatchOptions). */
+export type RequestOptions = DispatchOptions;
+
+export interface ResponseData {
+ statusCode: number;
+ /** 7.x only. */
+ statusText?: string;
+ headers: IncomingHttpHeaders;
+ body: BodyReadable;
+ trailers: Record;
+ opaque: unknown;
+ context: object | undefined;
+}
+
+const defaultProfile: Profile = { major: 7 };
+
+export class Dispatcher extends EventEmitter {
+ dispatch(_options: DispatchOptions, _handler: DispatchHandlers): boolean {
+ throw new Error("not implemented");
+ }
+
+ close(): Promise {
+ return Promise.reject(new Error("not implemented"));
+ }
+
+ destroy(_err?: Error | null): Promise {
+ return Promise.reject(new Error("not implemented"));
+ }
+
+ /** Dispatcher.prototype.request: dispatches through this dispatcher and
+ * resolves with the response once its headers arrive. */
+ request(options: RequestOptions): Promise {
+ return requestThrough(this, options, this.__undiciProfile());
+ }
+
+ __undiciProfile(): Profile {
+ return defaultProfile;
+ }
+
+ // Pool statistics hooks (undici's kConnected/kPending/kRunning/kSize/
+ // kBusy); dispatchers that are not clients or pools count nothing.
+ __connectedFlag(): boolean {
+ return false;
+ }
+
+ __isDestroyed(): boolean {
+ return false;
+ }
+
+ __connectedCount(): number {
+ return 0;
+ }
+
+ __freeCount(): number {
+ return 0;
+ }
+
+ __pendingCount(): number {
+ return 0;
+ }
+
+ __queuedCount(): number {
+ return 0;
+ }
+
+ __runningCount(): number {
+ return 0;
+ }
+
+ __sizeCount(): number {
+ return 0;
+ }
+}
+
+export class DispatcherBase extends Dispatcher {
+ __profile: Profile;
+ __destroyed: boolean;
+ __closed: boolean;
+ __onDestroyed: (() => void)[] | null;
+ __onClosed: (() => void)[] | null;
+ __interceptors: readonly DispatchInterceptor[];
+ __interceptedDispatch: DispatchFn | null;
+
+ constructor(profile: Profile) {
+ super();
+ this.__profile = profile;
+ this.__destroyed = false;
+ this.__closed = false;
+ this.__onDestroyed = [];
+ this.__onClosed = [];
+ this.__interceptors = [];
+ this.__interceptedDispatch = null;
+ }
+
+ __undiciProfile(): Profile {
+ return this.__profile;
+ }
+
+ get destroyed(): boolean {
+ return this.__destroyed;
+ }
+
+ __isDestroyed(): boolean {
+ return this.__destroyed;
+ }
+
+ get closed(): boolean {
+ return this.__closed;
+ }
+
+ close(): Promise {
+ return new Promise((resolve, reject) => {
+ if (this.__destroyed) {
+ queueMicrotask(() => reject(new ClientDestroyedError()));
+ return;
+ }
+ if (this.__closed) {
+ const waiting = this.__onClosed;
+ if (waiting !== null) waiting.push(() => resolve());
+ else queueMicrotask(() => resolve());
+ return;
+ }
+ this.__closed = true;
+ this.__onClosed!.push(() => resolve());
+ this.__close()
+ .then(() => this.destroy())
+ .then(() => {
+ queueMicrotask(() => {
+ const callbacks = this.__onClosed;
+ this.__onClosed = null;
+ if (callbacks !== null) for (const cb of callbacks) cb();
+ });
+ });
+ });
+ }
+
+ destroy(err?: Error | null): Promise {
+ return new Promise((resolve) => {
+ if (this.__destroyed) {
+ const waiting = this.__onDestroyed;
+ if (waiting !== null) waiting.push(() => resolve());
+ else queueMicrotask(() => resolve());
+ return;
+ }
+ const error = err ?? new ClientDestroyedError();
+ this.__destroyed = true;
+ this.__onDestroyed!.push(() => resolve());
+ this.__destroy(error).then(() => {
+ queueMicrotask(() => {
+ const callbacks = this.__onDestroyed;
+ this.__onDestroyed = null;
+ if (callbacks !== null) for (const cb of callbacks) cb();
+ });
+ });
+ });
+ }
+
+ dispatch(options: DispatchOptions, handler: DispatchHandlers): boolean {
+ const target = this.__profile.major >= 7 ? unwrapHandler(handler, this.__profile) : handler;
+ try {
+ if (this.__destroyed) throw new ClientDestroyedError();
+ if (this.__closed) throw new ClientClosedError();
+ return this.__dispatchIntercepted(options, target);
+ } catch (err) {
+ if (target.onError === undefined) {
+ if (this.__profile.major >= 7) throw err;
+ throw new InvalidArgumentError("invalid onError method");
+ }
+ target.onError!(err as Error);
+ return false;
+ }
+ }
+
+ __dispatchIntercepted(options: DispatchOptions, handler: DispatchHandlers): boolean {
+ let dispatch = this.__interceptedDispatch;
+ if (dispatch === null) {
+ dispatch = (o: DispatchOptions, h: DispatchHandlers): boolean => this.__dispatch(o, h);
+ for (let i = this.__interceptors.length - 1; i >= 0; i--) {
+ dispatch = this.__interceptors[i]!(dispatch);
+ }
+ this.__interceptedDispatch = dispatch;
+ }
+ return dispatch(options, handler);
+ }
+
+ __dispatch(_options: DispatchOptions, _handler: DispatchHandlers): boolean {
+ throw new Error("not implemented");
+ }
+
+ __close(): Promise {
+ return Promise.resolve();
+ }
+
+ __destroy(_err: Error): Promise {
+ return Promise.resolve();
+ }
+}
+
+/** The request() API over a dispatcher (api-request.js's request with the
+ * promise callback). */
+export function requestThrough(
+ dispatcher: Dispatcher,
+ options: RequestOptions,
+ profile: Profile,
+): Promise {
+ return new Promise((resolve, reject) => {
+ const callback = (err: Error | null, data: ResponseData | null): void => {
+ if (err !== null) reject(err);
+ else resolve(data!);
+ };
+ try {
+ const handler = createRequestHandler(options, profile, callback);
+ dispatcher.dispatch(options, handler);
+ } catch (err) {
+ queueMicrotask(() => callback(err as Error, null));
+ }
+ });
+}
+
+/** api-request.js's RequestHandler, as a handler record over closure state. */
+function createRequestHandler(
+ opts: RequestOptions,
+ profile: Profile,
+ done: (err: Error | null, data: ResponseData | null) => void,
+): DispatchHandlers {
+ if (opts.method === "CONNECT") throw new InvalidArgumentError("invalid method");
+ if (
+ opts.highWaterMark !== undefined &&
+ (!Number.isFinite(opts.highWaterMark) || opts.highWaterMark < 0)
+ ) {
+ throw new InvalidArgumentError("invalid highWaterMark");
+ }
+ const v7 = profile.major >= 7;
+ const opaque: unknown = opts.opaque ?? null;
+ const onInfo = opts.onInfo ?? null;
+ const throwOnError = opts.throwOnError === true;
+ const trailers: Record = {};
+ let callback: ((err: Error | null, data: ResponseData | null) => void) | null = done;
+ let res: BodyReadable | null = null;
+ let abortFn: ((err?: Error) => void) | null = null;
+ let reason: Error | null = null;
+ let removeAbortListener: (() => void) | null = null;
+
+ const removeSignal = (): void => {
+ const remove = removeAbortListener;
+ removeAbortListener = null;
+ if (remove !== null) remove();
+ };
+
+ const onError = (err: Error): void => {
+ removeSignal();
+ const cb = callback;
+ if (cb !== null) {
+ callback = null;
+ queueMicrotask(() => cb(err, null));
+ }
+ const body = res;
+ if (body !== null) {
+ res = null;
+ queueMicrotask(() => {
+ body.on("error", () => {});
+ body.destroy(err);
+ });
+ }
+ };
+
+ // 5.x: abort() before the request reached a socket fails it at once.
+ const abortV5 = (): void => {
+ const abort = abortFn;
+ if (abort !== null) abort();
+ else onError(new RequestAbortedError());
+ };
+
+ const signal = opts.signal ?? null;
+ if (signal !== null) {
+ if (v7) {
+ if (signal.aborted) {
+ reason = abortReason(signal);
+ } else {
+ const listener = (): void => {
+ const why = abortReason(signal);
+ reason = why;
+ const body = res;
+ const abort = abortFn;
+ if (body !== null) {
+ body.on("error", () => {});
+ body.destroy(why);
+ } else if (abort !== null) {
+ abort(why);
+ }
+ };
+ signal.addEventListener("abort", listener);
+ removeAbortListener = () => signal.removeEventListener("abort", listener);
+ }
+ } else if (signal.aborted) {
+ abortV5();
+ } else {
+ const listener = (): void => abortV5();
+ signal.addEventListener("abort", listener);
+ removeAbortListener = () => signal.removeEventListener("abort", listener);
+ }
+ }
+
+ return {
+ onConnect: (abort: (err?: Error) => void): void => {
+ if (v7) {
+ if (reason !== null) {
+ abort(reason);
+ return;
+ }
+ } else if (callback === null) {
+ throw new RequestAbortedError();
+ }
+ abortFn = abort;
+ },
+ onHeaders: (
+ statusCode: number,
+ rawHeaders: Buffer[],
+ resume: () => void,
+ statusText: string,
+ ): boolean => {
+ const headers = parseHeaders(rawHeaderStrings(rawHeaders, profile), profile);
+ if (statusCode < 200) {
+ if (onInfo !== null) onInfo({ statusCode, headers });
+ return true;
+ }
+ const contentTypeValue = headers["content-type"];
+ const contentType = typeof contentTypeValue === "string" ? contentTypeValue : "";
+ const abort = abortFn;
+ const body = new BodyReadable(
+ resume,
+ (err: Error) => {
+ if (abort !== null) {
+ if (v7) abort(err);
+ else abort();
+ }
+ },
+ contentType,
+ profile,
+ );
+ if (v7 && removeAbortListener !== null) body.on("close", () => removeSignal());
+ const cb = callback;
+ callback = null;
+ res = body;
+ if (cb !== null) {
+ if (!v7 && throwOnError && statusCode >= 400) {
+ resolveErrorBody(cb, body, contentType, statusCode, statusText, headers);
+ return true;
+ }
+ const data: ResponseData = v7
+ ? { statusCode, statusText, headers, trailers, opaque, body, context: undefined }
+ : { statusCode, headers, trailers, opaque, body, context: undefined };
+ cb(null, data);
+ }
+ return true;
+ },
+ onData: (chunk: Buffer): boolean => {
+ const body = res;
+ return body !== null ? body.push(chunk) : true;
+ },
+ onComplete: (rawTrailers: string[] | null): void => {
+ if (!v7) removeSignal();
+ if (rawTrailers !== null) {
+ const parsed = parseHeaders(rawTrailers, profile);
+ for (const key of Object.keys(parsed)) {
+ const value = parsed[key];
+ if (typeof value === "string") trailers[key] = value;
+ else if (value !== undefined) trailers[key] = value.join(", ");
+ }
+ }
+ const body = res;
+ if (body !== null) body.push(null);
+ },
+ onError,
+ };
+}
+
+function abortReason(signal: AbortSignal): Error {
+ const reason: unknown = signal.reason;
+ return reason instanceof Error ? reason : new RequestAbortedError();
+}
+
+/** 5.x throwOnError: reject with ResponseStatusCodeError carrying the parsed
+ * JSON or text body. */
+function resolveErrorBody(
+ callback: (err: Error | null, data: ResponseData | null) => void,
+ body: BodyReadable,
+ contentType: string,
+ statusCode: number,
+ statusMessage: string,
+ headers: IncomingHttpHeaders,
+): void {
+ const message = `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ""}`;
+ const record: Record = {};
+ for (const key of Object.keys(headers)) {
+ const value = headers[key];
+ if (value !== undefined) record[key] = value;
+ }
+ if (statusCode === 204 || contentType === "") {
+ body.dump();
+ process.nextTick(() =>
+ callback(new ResponseStatusCodeError(message, statusCode, record), null),
+ );
+ return;
+ }
+ const finish = (payload: unknown): void => {
+ process.nextTick(() =>
+ callback(new ResponseStatusCodeError(message, statusCode, record, payload), null),
+ );
+ };
+ const fallback = (): void => {
+ body.dump();
+ process.nextTick(() =>
+ callback(new ResponseStatusCodeError(message, statusCode, record), null),
+ );
+ };
+ if (contentType.startsWith("application/json")) {
+ body.json().then((payload: unknown) => finish(payload), fallback);
+ } else if (contentType.startsWith("text/")) {
+ body.text().then((payload: string) => finish(payload), fallback);
+ } else {
+ fallback();
+ }
+}
+
+/** 7.x's UnwrapHandler: a controller-API handler (onRequestStart, ...)
+ * driven through the legacy callbacks the clients invoke. */
+function unwrapHandler(handler: DispatchHandlers, profile: Profile): DispatchHandlers {
+ if (handler.onRequestStart === undefined) return handler;
+ let controller: UnwrapController | null = null;
+ const controllerOf = (): UnwrapController => {
+ let c = controller;
+ if (c === null) {
+ c = new UnwrapController(() => {});
+ controller = c;
+ }
+ return c;
+ };
+ return {
+ onConnect: (abort: (err?: Error) => void): void => {
+ const c = new UnwrapController(abort);
+ controller = c;
+ if (handler.onRequestStart !== undefined) handler.onRequestStart!(c, undefined);
+ },
+ onHeaders: (
+ statusCode: number,
+ rawHeaders: Buffer[],
+ resume: () => void,
+ statusText: string,
+ ): boolean => {
+ const c = controllerOf();
+ c.__resume = resume;
+ if (handler.onResponseStart !== undefined) {
+ handler.onResponseStart!(
+ c,
+ statusCode,
+ parseHeaders(rawHeaderStrings(rawHeaders, profile), profile),
+ statusText,
+ );
+ }
+ return !c.paused;
+ },
+ onData: (chunk: Buffer): boolean => {
+ const c = controllerOf();
+ if (handler.onResponseData !== undefined) handler.onResponseData!(c, chunk);
+ return !c.paused;
+ },
+ onComplete: (trailers: string[] | null): void => {
+ if (handler.onResponseEnd !== undefined) {
+ handler.onResponseEnd!(controllerOf(), parseHeaders(trailers ?? [], profile));
+ }
+ },
+ onError: (err: Error): void => {
+ if (handler.onResponseError === undefined)
+ throw new InvalidArgumentError("invalid onError method");
+ handler.onResponseError!(controllerOf(), err);
+ },
+ };
+}
+
+/** 7.x's UnwrapController. */
+class UnwrapController implements DispatchController {
+ __paused: boolean;
+ __reason: Error | null;
+ __aborted: boolean;
+ __abort: (err?: Error) => void;
+ __resume: (() => void) | null;
+
+ constructor(abort: (err?: Error) => void) {
+ this.__paused = false;
+ this.__reason = null;
+ this.__aborted = false;
+ this.__abort = abort;
+ this.__resume = null;
+ }
+
+ pause(): void {
+ this.__paused = true;
+ }
+
+ resume(): void {
+ if (this.__paused) {
+ this.__paused = false;
+ const resume = this.__resume;
+ if (resume !== null) resume();
+ }
+ }
+
+ abort(reason: Error): void {
+ if (!this.__aborted) {
+ this.__aborted = true;
+ this.__reason = reason;
+ this.__abort(reason);
+ }
+ }
+
+ get aborted(): boolean {
+ return this.__aborted;
+ }
+
+ get reason(): Error | null {
+ return this.__reason;
+ }
+
+ get paused(): boolean {
+ return this.__paused;
+ }
+}
+
+/** The `Dispatcher.*` type namespace of undici's types/dispatcher.d.ts
+ * (5.x and 7.x spellings). */
+export declare namespace Dispatcher {
+ type DispatchOptions = import("./types.ts").DispatchOptions;
+ type DispatchHandlers = import("./types.ts").DispatchHandlers;
+ type DispatchHandler = import("./types.ts").DispatchHandlers;
+ type DispatchController = import("./types.ts").DispatchController;
+ type DispatchInterceptor = import("./types.ts").DispatchInterceptor;
+ type Dispatch = import("./types.ts").DispatchFn;
+ type HttpMethod = string;
+ type UndiciHeaders = import("./types.ts").UndiciHeaders;
+ // The 7.x spellings take an opaque type parameter; the opaque value is
+ // `unknown` here.
+ type RequestOptions<_TOpaque = null> = import("./dispatcher.ts").RequestOptions;
+ type ResponseData<_TOpaque = null> = import("./dispatcher.ts").ResponseData;
+}
diff --git a/packages/compiler/shims/undici/core/errors-v5.ts b/packages/compiler/shims/undici/core/errors-v5.ts
new file mode 100644
index 0000000000..781dc7ce1a
--- /dev/null
+++ b/packages/compiler/shims/undici/core/errors-v5.ts
@@ -0,0 +1,22 @@
+// undici 5.x's `errors` export (lib/core/errors.js module.exports).
+export {
+ HTTPParserError,
+ UndiciError,
+ HeadersTimeoutError,
+ HeadersOverflowError,
+ BodyTimeoutError,
+ RequestContentLengthMismatchError,
+ ConnectTimeoutError,
+ ResponseStatusCodeError,
+ InvalidArgumentError,
+ InvalidReturnValueError,
+ RequestAbortedError,
+ ClientDestroyedError,
+ ClientClosedError,
+ InformationalError,
+ SocketError,
+ NotSupportedError,
+ ResponseContentLengthMismatchError,
+ BalancedPoolMissingUpstreamError,
+ ResponseExceededMaxSizeError,
+} from "./errors.ts";
diff --git a/packages/compiler/shims/undici/core/errors-v7.ts b/packages/compiler/shims/undici/core/errors-v7.ts
new file mode 100644
index 0000000000..644a004907
--- /dev/null
+++ b/packages/compiler/shims/undici/core/errors-v7.ts
@@ -0,0 +1,25 @@
+// undici 7.x's `errors` export (the implemented classes of
+// lib/core/errors.js module.exports).
+export {
+ AbortError,
+ HTTPParserError,
+ UndiciError,
+ HeadersTimeoutError,
+ HeadersOverflowError,
+ BodyTimeoutError,
+ RequestContentLengthMismatchError,
+ ConnectTimeoutError,
+ InvalidArgumentError,
+ InvalidReturnValueError,
+ RequestAbortedError,
+ ClientDestroyedError,
+ ClientClosedError,
+ InformationalError,
+ SocketError,
+ NotSupportedError,
+ ResponseContentLengthMismatchError,
+ BalancedPoolMissingUpstreamError,
+ ResponseExceededMaxSizeError,
+ RequestRetryError,
+ ResponseError,
+} from "./errors.ts";
diff --git a/packages/compiler/shims/undici/core/errors.ts b/packages/compiler/shims/undici/core/errors.ts
new file mode 100644
index 0000000000..0af5835daf
--- /dev/null
+++ b/packages/compiler/shims/undici/core/errors.ts
@@ -0,0 +1,255 @@
+// undici's error classes (lib/core/errors.js), shared by the 5.x and 7.x
+// entries. Names, messages and codes are undici's; the 7.x-only classes are
+// exported by the 7.x entry alone.
+
+export interface SocketInfo {
+ localAddress?: string;
+ localPort?: number;
+ remoteAddress?: string;
+ remotePort?: number;
+ remoteFamily?: string;
+ timeout?: number;
+ bytesWritten?: number;
+ bytesRead?: number;
+}
+
+export class UndiciError extends Error {
+ code: string;
+ constructor(message?: string) {
+ super(message);
+ this.name = "UndiciError";
+ this.code = "UND_ERR";
+ }
+}
+
+export class ConnectTimeoutError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "ConnectTimeoutError";
+ this.message = message || "Connect Timeout Error";
+ this.code = "UND_ERR_CONNECT_TIMEOUT";
+ }
+}
+
+export class HeadersTimeoutError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "HeadersTimeoutError";
+ this.message = message || "Headers Timeout Error";
+ this.code = "UND_ERR_HEADERS_TIMEOUT";
+ }
+}
+
+export class HeadersOverflowError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "HeadersOverflowError";
+ this.message = message || "Headers Overflow Error";
+ this.code = "UND_ERR_HEADERS_OVERFLOW";
+ }
+}
+
+export class BodyTimeoutError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "BodyTimeoutError";
+ this.message = message || "Body Timeout Error";
+ this.code = "UND_ERR_BODY_TIMEOUT";
+ }
+}
+
+export class InvalidArgumentError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "InvalidArgumentError";
+ this.message = message || "Invalid Argument Error";
+ this.code = "UND_ERR_INVALID_ARG";
+ }
+}
+
+export class InvalidReturnValueError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "InvalidReturnValueError";
+ this.message = message || "Invalid Return Value Error";
+ this.code = "UND_ERR_INVALID_RETURN_VALUE";
+ }
+}
+
+/** 7.x's base of RequestAbortedError (5.x has no AbortError export; its
+ * RequestAbortedError extends UndiciError directly, which no 5.x program can
+ * observe through the exported classes). */
+export class AbortError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "AbortError";
+ this.message = message || "The operation was aborted";
+ this.code = "UND_ERR_ABORT";
+ }
+}
+
+export class RequestAbortedError extends AbortError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "AbortError";
+ this.message = message || "Request aborted";
+ this.code = "UND_ERR_ABORTED";
+ }
+}
+
+export class InformationalError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "InformationalError";
+ this.message = message || "Request information";
+ this.code = "UND_ERR_INFO";
+ }
+}
+
+export class RequestContentLengthMismatchError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "RequestContentLengthMismatchError";
+ this.message = message || "Request body length does not match content-length header";
+ this.code = "UND_ERR_REQ_CONTENT_LENGTH_MISMATCH";
+ }
+}
+
+export class ResponseContentLengthMismatchError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "ResponseContentLengthMismatchError";
+ this.message = message || "Response body length does not match content-length header";
+ this.code = "UND_ERR_RES_CONTENT_LENGTH_MISMATCH";
+ }
+}
+
+export class ClientDestroyedError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "ClientDestroyedError";
+ this.message = message || "The client is destroyed";
+ this.code = "UND_ERR_DESTROYED";
+ }
+}
+
+export class ClientClosedError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "ClientClosedError";
+ this.message = message || "The client is closed";
+ this.code = "UND_ERR_CLOSED";
+ }
+}
+
+export class SocketError extends UndiciError {
+ socket: SocketInfo | null;
+ constructor(message?: string, socket?: SocketInfo | null) {
+ super(message);
+ this.name = "SocketError";
+ this.message = message || "Socket error";
+ this.code = "UND_ERR_SOCKET";
+ this.socket = socket ?? null;
+ }
+}
+
+export class NotSupportedError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "NotSupportedError";
+ this.message = message || "Not supported error";
+ this.code = "UND_ERR_NOT_SUPPORTED";
+ }
+}
+
+export class BalancedPoolMissingUpstreamError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "MissingUpstreamError";
+ this.message = message || "No upstream has been added to the BalancedPool";
+ this.code = "UND_ERR_BPL_MISSING_UPSTREAM";
+ }
+}
+
+export class HTTPParserError extends Error {
+ code: string | undefined;
+ data: string | undefined;
+ constructor(message?: string, code?: string, data?: string) {
+ super(message);
+ this.name = "HTTPParserError";
+ this.code = code ? `HPE_${code}` : undefined;
+ this.data = data ? data : undefined;
+ }
+}
+
+export class ResponseExceededMaxSizeError extends UndiciError {
+ constructor(message?: string) {
+ super(message);
+ this.name = "ResponseExceededMaxSizeError";
+ this.message = message || "Response content exceeded max size";
+ this.code = "UND_ERR_RES_EXCEEDED_MAX_SIZE";
+ }
+}
+
+/** 5.x only (throwOnError). */
+export class ResponseStatusCodeError extends UndiciError {
+ body: unknown;
+ status: number;
+ statusCode: number;
+ headers: Record | null;
+ constructor(
+ message?: string,
+ statusCode?: number,
+ headers?: Record | null,
+ body?: unknown,
+ ) {
+ super(message);
+ this.name = "ResponseStatusCodeError";
+ this.message = message || "Response Status Code Error";
+ this.code = "UND_ERR_RESPONSE_STATUS_CODE";
+ this.body = body;
+ this.status = statusCode ?? 0;
+ this.statusCode = statusCode ?? 0;
+ this.headers = headers ?? null;
+ }
+}
+
+/** 7.x only. */
+export class RequestRetryError extends UndiciError {
+ statusCode: number;
+ data: { count: number };
+ headers: Record;
+ constructor(
+ message: string,
+ code: number,
+ options: { headers?: Record; data?: { count: number } },
+ ) {
+ super(message);
+ this.name = "RequestRetryError";
+ this.message = message || "Request retry error";
+ this.code = "UND_ERR_REQ_RETRY";
+ this.statusCode = code;
+ this.data = options.data ?? { count: 0 };
+ this.headers = options.headers ?? {};
+ }
+}
+
+/** 7.x only. */
+export class ResponseError extends UndiciError {
+ statusCode: number;
+ body: unknown;
+ headers: Record | null;
+ constructor(
+ message: string,
+ code: number,
+ options: { headers?: Record | null; body?: unknown },
+ ) {
+ super(message);
+ this.name = "ResponseError";
+ this.message = message || "Response error";
+ this.code = "UND_ERR_RESPONSE";
+ this.statusCode = code;
+ this.body = options.body;
+ this.headers = options.headers ?? null;
+ }
+}
diff --git a/packages/compiler/shims/undici/core/fetch.ts b/packages/compiler/shims/undici/core/fetch.ts
new file mode 100644
index 0000000000..9b287a7bd8
--- /dev/null
+++ b/packages/compiler/shims/undici/core/fetch.ts
@@ -0,0 +1,413 @@
+// undici's fetch(). Without a dispatcher (no init.dispatcher and no
+// program-installed global dispatcher) it is the runtime's own fetch, which
+// implements the same WHATWG algorithm. With one, the request goes through
+// that dispatcher (its connector, lookup and pool), following undici's
+// fetch for request headers, redirects and errors; the response body is
+// buffered into a native Response.
+//
+// Known gaps: a Response built here reports `url` as "" and `redirected` as
+// false (the runtime's Response has no settable URL yet), and https requests
+// offer `gzip, deflate` rather than undici's `br, gzip, deflate` (the runtime
+// has no Brotli decoder).
+import { gunzipSync, inflateSync } from "node:zlib";
+import type { Dispatcher } from "./dispatcher.ts";
+import { getGlobalDispatcher, hasCustomGlobalDispatcher } from "./api.ts";
+import type { DispatchHandlers, Profile } from "./types.ts";
+
+/** undici's RequestInit, restricted to the implemented members: string and
+ * byte bodies, and the init members the dispatcher path honors. */
+export interface UndiciRequestInit {
+ method?: string;
+ headers?: Headers | [string, string][] | Record;
+ body?: string | Uint8Array | null;
+ signal?: AbortSignal | null;
+ redirect?: "follow" | "manual" | "error";
+ dispatcher?: Dispatcher;
+}
+
+/** The fetch standard's blocked ports (undici lib/web/fetch/constants.js). */
+const badPorts = new Set([
+ "1",
+ "7",
+ "9",
+ "11",
+ "13",
+ "15",
+ "17",
+ "19",
+ "20",
+ "21",
+ "22",
+ "23",
+ "25",
+ "37",
+ "42",
+ "43",
+ "53",
+ "69",
+ "77",
+ "79",
+ "87",
+ "95",
+ "101",
+ "102",
+ "103",
+ "104",
+ "109",
+ "110",
+ "111",
+ "113",
+ "115",
+ "117",
+ "119",
+ "123",
+ "135",
+ "137",
+ "139",
+ "143",
+ "161",
+ "179",
+ "389",
+ "427",
+ "465",
+ "512",
+ "513",
+ "514",
+ "515",
+ "526",
+ "530",
+ "531",
+ "532",
+ "540",
+ "548",
+ "554",
+ "556",
+ "563",
+ "587",
+ "601",
+ "636",
+ "989",
+ "990",
+ "993",
+ "995",
+ "1719",
+ "1720",
+ "1723",
+ "2049",
+ "3659",
+ "4045",
+ "4190",
+ "5060",
+ "5061",
+ "6000",
+ "6566",
+ "6665",
+ "6666",
+ "6667",
+ "6668",
+ "6669",
+ "6679",
+ "6697",
+ "10080",
+]);
+
+interface RawResponse {
+ status: number;
+ statusText: string;
+ headers: string[];
+ body: Buffer;
+}
+
+const redirectStatuses = [301, 302, 303, 307, 308];
+const nullBodyStatuses = [101, 204, 205, 304];
+
+export function undiciFetch(
+ input: string | URL,
+ init: UndiciRequestInit | undefined,
+ createGlobal: () => Dispatcher,
+ _profile: Profile,
+): Promise {
+ const explicit = init !== undefined ? init.dispatcher : undefined;
+ if (explicit === undefined && !hasCustomGlobalDispatcher()) {
+ if (init === undefined) return fetch(input);
+ return fetch(input, {
+ method: init.method,
+ headers: init.headers,
+ body: init.body,
+ signal: init.signal ?? undefined,
+ redirect: init.redirect,
+ });
+ }
+ const dispatcher = explicit ?? getGlobalDispatcher(createGlobal);
+ return fetchThrough(dispatcher, input, init ?? {});
+}
+
+function normalizeMethod(method: string): string {
+ const upper = method.toUpperCase();
+ return ["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH"].includes(upper)
+ ? upper
+ : method;
+}
+
+async function fetchThrough(
+ dispatcher: Dispatcher,
+ input: string | URL,
+ init: UndiciRequestInit,
+): Promise {
+ let url = typeof input === "string" ? new URL(input) : new URL(input.href);
+ let method = normalizeMethod(init.method ?? "GET");
+ const signal = init.signal ?? null;
+ const redirectMode = init.redirect ?? "follow";
+
+ // Request headers: the caller's (as a Headers list: lower-cased, sorted,
+ // combined), then undici fetch's defaults.
+ const headers = headerListOf(init.headers);
+ let body: Buffer | null = null;
+ const rawBody = init.body ?? null;
+ if (rawBody !== null) {
+ if (typeof rawBody === "string") {
+ body = Buffer.from(rawBody);
+ if (!headers.has("content-type")) headers.set("content-type", "text/plain;charset=UTF-8");
+ } else {
+ body = Buffer.from(rawBody.buffer, rawBody.byteOffset, rawBody.byteLength);
+ }
+ }
+ if (!headers.has("accept")) headers.set("accept", "*/*");
+ if (!headers.has("accept-language")) headers.set("accept-language", "*");
+
+ for (let redirects = 0; ; redirects++) {
+ if (signal !== null && signal.aborted) throw abortReason(signal);
+ if (badPorts.has(url.port)) {
+ throw new TypeError("fetch failed", { cause: new Error("bad port") });
+ }
+ const requestHeaders = new HeaderList(headers);
+ if (body !== null) {
+ if (!requestHeaders.has("content-length"))
+ requestHeaders.set("content-length", String(body.length));
+ } else if ((method === "POST" || method === "PUT") && !requestHeaders.has("content-length")) {
+ requestHeaders.set("content-length", "0");
+ }
+ requestHeaders.set("sec-fetch-mode", "cors");
+ if (!requestHeaders.has("user-agent")) requestHeaders.set("user-agent", "undici");
+ // undici also offers br over https; the native runtime has no Brotli
+ // decoder, so only the codings it can decode are offered.
+ if (!requestHeaders.has("accept-encoding"))
+ requestHeaders.set("accept-encoding", "gzip, deflate");
+ const pairs = requestHeaders.pairs();
+
+ let raw: RawResponse;
+ try {
+ raw = await dispatchOnce(dispatcher, url, method, pairs, body, signal);
+ } catch (err) {
+ if (signal !== null && signal.aborted) throw abortReason(signal);
+ throw new TypeError("fetch failed", { cause: err });
+ }
+
+ const responseHeaders = new Headers();
+ for (let i = 0; i + 1 < raw.headers.length; i += 2) {
+ responseHeaders.append(raw.headers[i]!, raw.headers[i + 1]!);
+ }
+ const location = responseHeaders.get("location");
+ if (redirectStatuses.includes(raw.status) && location !== null && redirectMode !== "manual") {
+ if (redirectMode === "error") {
+ throw new TypeError("fetch failed", { cause: new Error("unexpected redirect") });
+ }
+ if (redirects >= 20) {
+ throw new TypeError("fetch failed", { cause: new Error("redirect count exceeded") });
+ }
+ const next = new URL(location, url);
+ if (
+ raw.status === 303 && method !== "GET" && method !== "HEAD"
+ ? true
+ : (raw.status === 301 || raw.status === 302) && method === "POST"
+ ) {
+ method = "GET";
+ body = null;
+ for (const name of [
+ "content-encoding",
+ "content-language",
+ "content-location",
+ "content-type",
+ "content-length",
+ ]) {
+ headers.delete(name);
+ }
+ }
+ if (next.origin !== url.origin) {
+ headers.delete("authorization");
+ headers.delete("proxy-authorization");
+ headers.delete("cookie");
+ headers.delete("host");
+ }
+ url = next;
+ continue;
+ }
+
+ const nullBody = nullBodyStatuses.includes(raw.status) || method === "HEAD";
+ return new Response(
+ nullBody ? null : decode(raw.body, responseHeaders.get("content-encoding")),
+ {
+ status: raw.status,
+ statusText: raw.statusText,
+ headers: responseHeaders,
+ },
+ );
+ }
+}
+
+/** A Fetch header list: lower-cased names, values combined with ", " in
+ * insertion order, serialized sorted by name (set-cookie values stay
+ * separate) — what a Headers object yields when iterated. */
+class HeaderList {
+ names: string[];
+ values: Map;
+
+ constructor(from: HeaderList | null) {
+ this.names = from !== null ? from.names.slice() : [];
+ this.values = new Map();
+ if (from !== null)
+ for (const name of from.names) this.values.set(name, from.values.get(name)!.slice());
+ }
+
+ has(name: string): boolean {
+ return this.values.has(name.toLowerCase());
+ }
+
+ append(name: string, value: string): void {
+ const key = name.toLowerCase();
+ const normalized = value.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "");
+ const existing = this.values.get(key);
+ if (existing !== undefined) existing.push(normalized);
+ else {
+ this.names.push(key);
+ this.values.set(key, [normalized]);
+ }
+ }
+
+ set(name: string, value: string): void {
+ this.delete(name);
+ this.append(name, value);
+ }
+
+ delete(name: string): void {
+ const key = name.toLowerCase();
+ if (!this.values.has(key)) return;
+ this.values.delete(key);
+ this.names = this.names.filter((n) => n !== key);
+ }
+
+ pairs(): string[] {
+ const out: string[] = [];
+ for (const name of this.names.slice().sort()) {
+ const list = this.values.get(name)!;
+ if (name === "set-cookie") for (const value of list) out.push(name, value);
+ else out.push(name, list.join(", "));
+ }
+ return out;
+ }
+}
+
+/** The caller's headers init as a header list. Records and pair arrays read
+ * directly; a Headers object is read through forEach. */
+function headerListOf(
+ init: Headers | [string, string][] | Record | undefined,
+): HeaderList {
+ const list = new HeaderList(null);
+ if (init === undefined) return list;
+ if (init instanceof Headers) {
+ init.forEach((value: string, name: string) => list.append(name, value));
+ } else if (Array.isArray(init)) {
+ for (const pair of init) list.append(pair[0], pair[1]);
+ } else {
+ for (const name of Object.keys(init)) list.append(name, init[name]!);
+ }
+ return list;
+}
+
+function decode(body: Buffer, encoding: string | null): Buffer {
+ if (encoding === null || body.length === 0) return body;
+ let out = body;
+ const codings = encoding
+ .toLowerCase()
+ .split(",")
+ .map((c) => c.trim())
+ .reverse();
+ for (const coding of codings) {
+ if (coding === "gzip" || coding === "x-gzip") out = gunzipSync(out);
+ else if (coding === "deflate") out = inflateSync(out);
+ else return body;
+ }
+ return out;
+}
+
+function abortReason(signal: AbortSignal): Error {
+ const reason: unknown = signal.reason;
+ return reason instanceof Error
+ ? reason
+ : new DOMException("This operation was aborted", "AbortError");
+}
+
+function dispatchOnce(
+ dispatcher: Dispatcher,
+ url: URL,
+ method: string,
+ headers: string[],
+ body: Buffer | null,
+ signal: AbortSignal | null,
+): Promise {
+ return new Promise((resolve, reject) => {
+ let status = 0;
+ let statusText = "";
+ let rawHeaders: string[] = [];
+ const chunks: Buffer[] = [];
+ let settled = false;
+ let abortFn: ((err?: Error) => void) | null = null;
+ const onAbort = (): void => {
+ const abort = abortFn;
+ if (abort !== null && signal !== null) abort(abortReason(signal));
+ };
+ if (signal !== null) signal.addEventListener("abort", onAbort);
+ const finish = (err: Error | null): void => {
+ if (settled) return;
+ settled = true;
+ if (signal !== null) signal.removeEventListener("abort", onAbort);
+ if (err !== null) reject(err);
+ else resolve({ status, statusText, headers: rawHeaders, body: Buffer.concat(chunks) });
+ };
+ const handler: DispatchHandlers = {
+ onConnect: (abort: (err?: Error) => void): void => {
+ abortFn = abort;
+ if (signal !== null && signal.aborted) abort(abortReason(signal));
+ },
+ onHeaders: (
+ statusCode: number,
+ raw: Buffer[],
+ _resume: () => void,
+ text: string,
+ ): boolean => {
+ if (statusCode < 200) return true;
+ status = statusCode;
+ statusText = text;
+ rawHeaders = [];
+ for (const b of raw) rawHeaders.push(b.toString("latin1"));
+ return true;
+ },
+ onData: (chunk: Buffer): boolean => {
+ chunks.push(chunk);
+ return true;
+ },
+ onComplete: (_trailers: string[] | null): void => finish(null),
+ onError: (err: Error): void => finish(err),
+ };
+ dispatcher.dispatch(
+ {
+ origin: url.origin,
+ path: url.pathname + url.search,
+ method,
+ headers,
+ body,
+ },
+ handler,
+ );
+ });
+}
diff --git a/packages/compiler/shims/undici/core/pool.ts b/packages/compiler/shims/undici/core/pool.ts
new file mode 100644
index 0000000000..101d5947b7
--- /dev/null
+++ b/packages/compiler/shims/undici/core/pool.ts
@@ -0,0 +1,280 @@
+// undici's Pool (lib/dispatcher/pool.js, pool-base.js, pool-stats.js): a
+// set of Clients for one origin, created on demand up to `connections`, with
+// a queue for requests no client can take.
+import { buildConnector } from "./connector.ts";
+import { Client } from "./client.ts";
+import { Dispatcher, DispatcherBase } from "./dispatcher.ts";
+import { InvalidArgumentError } from "./errors.ts";
+import { parseOrigin } from "./util.ts";
+import type {
+ ClientOptions,
+ DispatchHandlers,
+ DispatchOptions,
+ PoolOptions,
+ Profile,
+} from "./types.ts";
+
+interface QueuedDispatch {
+ opts: DispatchOptions;
+ handler: DispatchHandlers;
+}
+
+/** pool-stats.js: live counters over a pool's clients. */
+export class PoolStats {
+ __pool: PoolBase;
+
+ constructor(pool: PoolBase) {
+ this.__pool = pool;
+ }
+
+ get connected(): number {
+ return this.__pool.__connectedCount();
+ }
+
+ get free(): number {
+ return this.__pool.__freeCount();
+ }
+
+ get pending(): number {
+ return this.__pool.__pendingCount();
+ }
+
+ get queued(): number {
+ return this.__pool.__queuedCount();
+ }
+
+ get running(): number {
+ return this.__pool.__runningCount();
+ }
+
+ get size(): number {
+ return this.__pool.__sizeCount();
+ }
+}
+
+export class PoolBase extends DispatcherBase {
+ __clients: Dispatcher[];
+ __clientNeedDrain: Map;
+ __queue: QueuedDispatch[];
+ __queued: number;
+ __needDrain: boolean;
+ __closedResolve: (() => void) | null;
+ __stats: PoolStats;
+ __onDrain: (origin: URL, targets: Dispatcher[]) => void;
+ __onConnect: (origin: URL, targets: Dispatcher[]) => void;
+ __onDisconnect: (origin: URL, targets: Dispatcher[], err: Error) => void;
+ __onConnectionError: (origin: URL, targets: Dispatcher[], err: Error) => void;
+
+ constructor(profile: Profile) {
+ super(profile);
+ this.__clients = [];
+ this.__clientNeedDrain = new Map();
+ this.__queue = [];
+ this.__queued = 0;
+ this.__needDrain = false;
+ this.__closedResolve = null;
+ this.__stats = new PoolStats(this);
+ this.__onDrain = (origin: URL, targets: Dispatcher[]): void => {
+ const client = targets[0]!;
+ let needDrain = false;
+ while (!needDrain) {
+ const item = this.__queue.shift();
+ if (item === undefined) break;
+ this.__queued--;
+ needDrain = !client.dispatch(item.opts, item.handler);
+ }
+ this.__clientNeedDrain.set(client, needDrain);
+ if (!needDrain && this.__needDrain) {
+ this.__needDrain = false;
+ this.emit("drain", origin, [this as Dispatcher, ...targets]);
+ }
+ const closed = this.__closedResolve;
+ if (closed !== null && this.__queue.length === 0) {
+ Promise.all(this.__clients.map((c) => c.close())).then(() => closed());
+ }
+ };
+ this.__onConnect = (origin: URL, targets: Dispatcher[]): void => {
+ this.emit("connect", origin, [this as Dispatcher, ...targets]);
+ };
+ this.__onDisconnect = (origin: URL, targets: Dispatcher[], err: Error): void => {
+ this.emit("disconnect", origin, [this as Dispatcher, ...targets], err);
+ };
+ this.__onConnectionError = (origin: URL, targets: Dispatcher[], err: Error): void => {
+ this.emit("connectionError", origin, [this as Dispatcher, ...targets], err);
+ };
+ }
+
+ get stats(): PoolStats {
+ return this.__stats;
+ }
+
+ __busyFlag(): boolean {
+ return this.__needDrain;
+ }
+
+ __connectedCount(): number {
+ return this.__clients.filter((c) => c.__connectedFlag()).length;
+ }
+
+ __freeCount(): number {
+ return this.__clients.filter((c) => c.__connectedFlag() && !this.__clientNeedDrain.get(c))
+ .length;
+ }
+
+ __pendingCount(): number {
+ let ret = this.__queued;
+ for (const c of this.__clients) ret += c.__pendingCount();
+ return ret;
+ }
+
+ __queuedCount(): number {
+ return this.__queued;
+ }
+
+ __runningCount(): number {
+ let ret = 0;
+ for (const c of this.__clients) ret += c.__runningCount();
+ return ret;
+ }
+
+ __sizeCount(): number {
+ let ret = this.__queued;
+ for (const c of this.__clients) ret += c.__sizeCount();
+ return ret;
+ }
+
+ __close(): Promise {
+ if (this.__queue.length === 0) {
+ return Promise.all(this.__clients.map((c) => c.close())).then(() => {});
+ }
+ return new Promise((resolve) => {
+ this.__closedResolve = resolve;
+ });
+ }
+
+ __destroy(err: Error): Promise {
+ for (;;) {
+ const item = this.__queue.shift();
+ if (item === undefined) break;
+ if (item.handler.onError !== undefined) item.handler.onError!(err);
+ }
+ return Promise.all(this.__clients.map((c) => c.destroy(err))).then(() => {});
+ }
+
+ __dispatch(opts: DispatchOptions, handler: DispatchHandlers): boolean {
+ const dispatcher = this.__getDispatcher();
+ if (dispatcher === null) {
+ this.__needDrain = true;
+ this.__queue.push({ opts, handler });
+ this.__queued++;
+ } else if (!dispatcher.dispatch(opts, handler)) {
+ this.__clientNeedDrain.set(dispatcher, true);
+ this.__needDrain = this.__getDispatcher() === null;
+ }
+ return !this.__needDrain;
+ }
+
+ __getDispatcher(): Dispatcher | null {
+ return null;
+ }
+
+ __addClient(client: Dispatcher): void {
+ client.on("drain", this.__onDrain);
+ client.on("connect", this.__onConnect);
+ client.on("disconnect", this.__onDisconnect);
+ client.on("connectionError", this.__onConnectionError);
+ this.__clients.push(client);
+ this.__clientNeedDrain.set(client, false);
+ if (this.__needDrain) {
+ process.nextTick(() => {
+ if (this.__needDrain) this.__onDrain(clientUrl(client), [client, this as Dispatcher]);
+ });
+ }
+ }
+}
+
+function clientUrl(client: Dispatcher): URL {
+ return client instanceof Client ? client.__url : new URL("http://localhost");
+}
+
+export class Pool extends PoolBase {
+ __connections: number | null;
+ __url: URL;
+ __options: ClientOptions;
+ __factory: ((origin: URL, opts: object) => Dispatcher) | null;
+
+ constructor(origin: string | URL, options: PoolOptions | undefined, profile: Profile) {
+ super(profile);
+ const opts: PoolOptions = options ?? {};
+ const connections = opts.connections ?? null;
+ if (connections !== null && (!Number.isFinite(connections) || connections < 0)) {
+ throw new InvalidArgumentError("invalid connections");
+ }
+ const connect = opts.connect ?? null;
+ const connector =
+ typeof connect === "function"
+ ? connect
+ : buildConnector(
+ connect !== null
+ ? {
+ ...connect,
+ timeout:
+ connect.timeout !== undefined ? connect.timeout : (opts.connectTimeout ?? 10e3),
+ }
+ : { timeout: opts.connectTimeout ?? 10e3 },
+ );
+ const interceptors = opts.interceptors;
+ if (profile.major < 7 && interceptors !== undefined && interceptors.Pool !== undefined) {
+ this.__interceptors = interceptors.Pool;
+ }
+ this.__connections = connections !== null && connections !== 0 ? connections : null;
+ this.__url = parseOrigin(origin, profile);
+ this.__options = {
+ keepAliveTimeout: opts.keepAliveTimeout,
+ keepAliveMaxTimeout: opts.keepAliveMaxTimeout,
+ keepAliveTimeoutThreshold: opts.keepAliveTimeoutThreshold,
+ pipelining: opts.pipelining,
+ maxHeaderSize: opts.maxHeaderSize,
+ bodyTimeout: opts.bodyTimeout,
+ headersTimeout: opts.headersTimeout,
+ strictContentLength: opts.strictContentLength,
+ maxRequestsPerClient: opts.maxRequestsPerClient,
+ maxResponseSize: opts.maxResponseSize,
+ localAddress: opts.localAddress,
+ interceptors: interceptors !== undefined ? { Client: interceptors.Client } : undefined,
+ connect: connector,
+ };
+ const factory = opts.factory;
+ this.__factory = factory !== undefined ? factory : null;
+ if (profile.major >= 7) {
+ // 7.x drops a client whose connection attempt failed.
+ this.on("connectionError", (_origin: URL, targets: Dispatcher[], _err: Error) => {
+ for (const target of targets) {
+ const idx = this.__clients.indexOf(target);
+ if (idx !== -1) this.__clients.splice(idx, 1);
+ }
+ });
+ }
+ }
+
+ __getDispatcher(): Dispatcher | null {
+ for (const client of this.__clients) {
+ if (!this.__clientNeedDrain.get(client)) return client;
+ }
+ if (this.__connections === null || this.__clients.length < this.__connections) {
+ const factory = this.__factory;
+ const client =
+ factory !== null
+ ? factory(this.__url, this.__options)
+ : this.__createClient(this.__url, this.__options);
+ this.__addClient(client);
+ return client;
+ }
+ return null;
+ }
+
+ /** The default factory: a Client of the same undici version. */
+ __createClient(origin: URL, options: ClientOptions): Dispatcher {
+ return new Client(origin, options, this.__profile);
+ }
+}
diff --git a/packages/compiler/shims/undici/core/readable.ts b/packages/compiler/shims/undici/core/readable.ts
new file mode 100644
index 0000000000..53a7cc14ec
--- /dev/null
+++ b/packages/compiler/shims/undici/core/readable.ts
@@ -0,0 +1,268 @@
+// undici's response body (lib/api/readable.js): a readable byte stream fed
+// by the dispatch handler, with the fetch body mixin (text/json/
+// arrayBuffer/blob/bytes) and dump().
+//
+// It follows node:stream's Readable contract for the members it declares
+// (flowing on a 'data' listener or resume(), pause(), 'data'/'end'/'close'/
+// 'error' events and their ordering, destroy(err), back-pressure through
+// push()'s return value), built on EventEmitter: scriptc compiles program
+// classes extending EventEmitter under both its own Node declarations and
+// @types/node, which a node:stream subclass does not get.
+import { EventEmitter } from "node:events";
+import { NotSupportedError, RequestAbortedError } from "./errors.ts";
+import type { Profile } from "./types.ts";
+
+const HIGH_WATER_MARK = 64 * 1024;
+
+export class BodyReadable extends EventEmitter {
+ __resumeFn: () => void;
+ __abortFn: (err: Error) => void;
+ __contentType: string;
+ __consuming: boolean;
+ __profile: Profile;
+ __buffer: Buffer[];
+ __length: number;
+ __flowing: boolean;
+ __ended: boolean;
+ __endEmitted: boolean;
+ __destroyed: boolean;
+ __flowScheduled: boolean;
+ __needsResume: boolean;
+
+ constructor(
+ resume: () => void,
+ abort: (err: Error) => void,
+ contentType: string,
+ profile: Profile,
+ ) {
+ super();
+ this.__resumeFn = resume;
+ this.__abortFn = abort;
+ this.__contentType = contentType;
+ this.__consuming = false;
+ this.__profile = profile;
+ this.__buffer = [];
+ this.__length = 0;
+ this.__flowing = false;
+ this.__ended = false;
+ this.__endEmitted = false;
+ this.__destroyed = false;
+ this.__flowScheduled = false;
+ this.__needsResume = false;
+ // Readable.on('data') switches the stream to flowing mode.
+ this.on("newListener", (event: string) => {
+ if (event === "data" && !this.__flowing) this.resume();
+ });
+ }
+
+ get destroyed(): boolean {
+ return this.__destroyed;
+ }
+
+ get readableEnded(): boolean {
+ return this.__endEmitted;
+ }
+
+ get readableLength(): number {
+ return this.__length;
+ }
+
+ get readableFlowing(): boolean {
+ return this.__flowing;
+ }
+
+ /** Readable.push: queue a chunk (null ends the body). Returns false once
+ * the buffer reaches the high-water mark; the dispatcher then pauses the
+ * connection until the consumer drains it. */
+ push(chunk: Buffer | null): boolean {
+ if (this.__destroyed || this.__ended) return false;
+ if (chunk === null) {
+ this.__ended = true;
+ if (this.__length === 0) this.__scheduleEnd();
+ else this.__scheduleFlow();
+ return false;
+ }
+ if (this.__flowing && this.__length === 0 && this.listenerCount("data") > 0) {
+ this.emit("data", chunk);
+ return true;
+ }
+ this.__buffer.push(chunk);
+ this.__length += chunk.length;
+ if (this.__flowing) this.__scheduleFlow();
+ const below = this.__length < HIGH_WATER_MARK;
+ if (!below) this.__needsResume = true;
+ return below;
+ }
+
+ resume(): this {
+ if (!this.__flowing) {
+ this.__flowing = true;
+ this.__scheduleFlow();
+ }
+ return this;
+ }
+
+ pause(): this {
+ this.__flowing = false;
+ return this;
+ }
+
+ destroy(err?: Error): this {
+ if (this.__destroyed) return this;
+ this.__destroyed = true;
+ let error: Error | null = err ?? null;
+ if (error === null && !this.__endEmitted) error = new RequestAbortedError();
+ if (error !== null) this.__abortFn(error);
+ this.__buffer = [];
+ this.__length = 0;
+ const reported = error;
+ process.nextTick(() => {
+ if (reported !== null) this.emit("error", reported);
+ this.emit("close");
+ });
+ return this;
+ }
+
+ __scheduleFlow(): void {
+ if (this.__flowScheduled) return;
+ this.__flowScheduled = true;
+ process.nextTick(() => {
+ this.__flowScheduled = false;
+ this.__flow();
+ });
+ }
+
+ __flow(): void {
+ while (this.__flowing && !this.__destroyed && this.__buffer.length > 0) {
+ const chunk = this.__buffer.shift()!;
+ this.__length -= chunk.length;
+ this.emit("data", chunk);
+ }
+ if (this.__destroyed) return;
+ if (this.__needsResume && this.__length < HIGH_WATER_MARK) {
+ this.__needsResume = false;
+ this.__resumeFn();
+ }
+ if (this.__ended && this.__length === 0 && this.__flowing) this.__scheduleEnd();
+ }
+
+ __scheduleEnd(): void {
+ process.nextTick(() => {
+ if (this.__endEmitted || this.__destroyed || !this.__flowing || this.__length > 0) return;
+ this.__endEmitted = true;
+ this.emit("end");
+ // autoDestroy: a fully read body closes.
+ process.nextTick(() => {
+ if (this.__destroyed) return;
+ this.__destroyed = true;
+ this.emit("close");
+ });
+ });
+ }
+
+ /** True once text()/json()/arrayBuffer()/bytes()/blob()/dump() started
+ * consuming the body. */
+ get bodyUsed(): boolean {
+ return this.__consuming;
+ }
+
+ text(): Promise {
+ return this.__consume().then((bytes) => decodeText(bytes, this.__profile));
+ }
+
+ // undici declares json() as Promise: callers assign the parsed value
+ // to their own annotated types without a cast, exactly as with undici.
+ // oxlint-disable-next-line typescript/no-explicit-any
+ json(): Promise {
+ return this.__consume().then((bytes) => JSON.parse(decodeText(bytes, this.__profile)));
+ }
+
+ arrayBuffer(): Promise {
+ return this.__consume().then((bytes) => {
+ const out = new ArrayBuffer(bytes.length);
+ new Uint8Array(out).set(bytes);
+ return out;
+ });
+ }
+
+ bytes(): Promise {
+ return this.__consume().then((bytes) => new Uint8Array(bytes));
+ }
+
+ blob(): Promise {
+ return this.__consume().then((bytes) => new Blob([bytes], { type: this.__contentType }));
+ }
+
+ formData(): Promise {
+ return Promise.reject(new NotSupportedError());
+ }
+
+ /** Reads and discards up to `limit` bytes (default 256 KiB on 5.x, 128 KiB
+ * on 7.x), then resolves; read errors are swallowed like undici's. */
+ dump(opts?: { limit: number }): Promise {
+ let limit =
+ opts !== undefined && Number.isFinite(opts.limit)
+ ? opts.limit
+ : this.__profile.major >= 7
+ ? 131072
+ : 262144;
+ this.__consuming = true;
+ return new Promise((resolve) => {
+ if (this.__endEmitted || this.__destroyed) {
+ resolve();
+ return;
+ }
+ this.on("data", (chunk: Buffer) => {
+ limit -= chunk.length;
+ if (limit < 0) {
+ this.destroy();
+ resolve();
+ }
+ });
+ this.on("end", () => resolve());
+ this.on("error", () => resolve());
+ this.on("close", () => resolve());
+ });
+ }
+
+ __consume(): Promise {
+ if (this.bodyUsed) return Promise.reject(new TypeError("unusable"));
+ this.__consuming = true;
+ return new Promise((resolve, reject) => {
+ const chunks: Buffer[] = [];
+ let done = false;
+ this.on("data", (chunk: Buffer) => {
+ chunks.push(chunk);
+ });
+ this.on("end", () => {
+ if (done) return;
+ done = true;
+ resolve(Buffer.concat(chunks));
+ });
+ this.on("error", (err: Error) => {
+ if (done) return;
+ done = true;
+ reject(err);
+ });
+ this.on("close", () => {
+ if (done) return;
+ done = true;
+ reject(new RequestAbortedError());
+ });
+ });
+ }
+}
+
+/** 5.x decodes UTF-8 as-is; 7.x strips a leading byte-order mark first. */
+function decodeText(bytes: Buffer, profile: Profile): string {
+ if (
+ profile.major >= 7 &&
+ bytes.length >= 3 &&
+ bytes[0] === 0xef &&
+ bytes[1] === 0xbb &&
+ bytes[2] === 0xbf
+ ) {
+ return bytes.subarray(3).toString("utf8");
+ }
+ return bytes.toString("utf8");
+}
diff --git a/packages/compiler/shims/undici/core/request.ts b/packages/compiler/shims/undici/core/request.ts
new file mode 100644
index 0000000000..7f312481b5
--- /dev/null
+++ b/packages/compiler/shims/undici/core/request.ts
@@ -0,0 +1,287 @@
+// undici's per-dispatch request record (lib/core/request.js): option
+// validation, header processing, and the handler callbacks with their
+// diagnostics_channel publications.
+import { channels } from "./diagnostics.ts";
+import { InvalidArgumentError, NotSupportedError } from "./errors.ts";
+import type { DispatchHandlers, DispatchOptions, Profile } from "./types.ts";
+
+const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
+const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
+const invalidPathRegex = /[^!-ÿ]/;
+
+export class Request {
+ origin: string;
+ method: string;
+ path: string;
+ body: Buffer | null;
+ /** Header name/value pairs in insertion order (host and content-length
+ * are consumed into their own fields, like undici). */
+ headerPairs: string[];
+ host: string | null;
+ contentLength: number | null;
+ contentType: string | null;
+ completed: boolean;
+ aborted: boolean;
+ idempotent: boolean;
+ blocking: boolean;
+ reset: boolean | null;
+ headersTimeout: number | null;
+ bodyTimeout: number | null;
+ throwOnError: boolean;
+ servername: string | null;
+ error: Error | null;
+ abortFn: ((err?: Error) => void) | null;
+ handler: DispatchHandlers;
+ /** The request as diagnostics_channel subscribers see it: one boxed
+ * value per request, so its identity is stable across publications. */
+ diagnostic: unknown;
+
+ constructor(origin: string, opts: DispatchOptions, handler: DispatchHandlers, profile: Profile) {
+ const path = opts.path;
+ const method = opts.method;
+ if (typeof path !== "string") {
+ throw new InvalidArgumentError("path must be a string");
+ } else if (
+ path[0] !== "/" &&
+ !(path.startsWith("http://") || path.startsWith("https://")) &&
+ method !== "CONNECT"
+ ) {
+ throw new InvalidArgumentError("path must be an absolute URL or start with a slash");
+ } else if (invalidPathRegex.test(path)) {
+ throw new InvalidArgumentError("invalid request path");
+ }
+ if (typeof method !== "string") {
+ throw new InvalidArgumentError("method must be a string");
+ } else if (!tokenRegExp.test(method)) {
+ throw new InvalidArgumentError("invalid request method");
+ }
+ if (method === "CONNECT") {
+ throw new NotSupportedError(
+ "scriptc's undici client does not implement CONNECT requests yet",
+ );
+ }
+ const headersTimeout = opts.headersTimeout ?? null;
+ if (headersTimeout !== null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
+ throw new InvalidArgumentError("invalid headersTimeout");
+ }
+ const bodyTimeout = opts.bodyTimeout ?? null;
+ if (bodyTimeout !== null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
+ throw new InvalidArgumentError("invalid bodyTimeout");
+ }
+
+ this.origin = origin;
+ this.method = method;
+ this.path = path;
+ this.headersTimeout = headersTimeout;
+ this.bodyTimeout = bodyTimeout;
+ // 7.x dropped throwOnError (the responseError interceptor replaced it).
+ if (profile.major >= 7 && opts.throwOnError !== undefined) {
+ throw new InvalidArgumentError("invalid throwOnError");
+ }
+ this.throwOnError = opts.throwOnError === true;
+
+ const body = opts.body ?? null;
+ if (body === null) {
+ this.body = null;
+ } else if (typeof body === "string") {
+ this.body = body.length > 0 ? Buffer.from(body) : null;
+ } else {
+ this.body =
+ body.byteLength > 0 ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null;
+ }
+
+ this.completed = false;
+ this.aborted = false;
+ this.idempotent = opts.idempotent ?? (method === "HEAD" || method === "GET");
+ this.blocking = opts.blocking ?? (profile.major >= 7 ? method !== "HEAD" : false);
+ this.reset = opts.reset ?? null;
+ this.host = null;
+ this.contentLength = null;
+ this.contentType = null;
+ this.headerPairs = [];
+ this.error = null;
+ this.abortFn = null;
+
+ const headers = opts.headers ?? null;
+ if (headers !== null) {
+ if (Array.isArray(headers)) {
+ if (headers.length % 2 !== 0) throw new InvalidArgumentError("headers array must be even");
+ for (let i = 0; i < headers.length; i += 2) {
+ processHeader(this, headers[i]!, headers[i + 1]!, profile);
+ }
+ } else {
+ for (const key of Object.keys(headers)) {
+ const value = headers[key];
+ if (value === undefined) continue;
+ if (typeof value === "string") {
+ processHeader(this, key, value, profile);
+ } else if (profile.major >= 7) {
+ for (const item of value) processHeader(this, key, item, profile);
+ } else {
+ throw new InvalidArgumentError(`invalid ${key} header`);
+ }
+ }
+ }
+ }
+
+ this.servername = serverName(this.host);
+ this.handler = handler;
+ this.diagnostic = null;
+ if (channels.create.hasSubscribers) channels.create.publish({ request: this.diagnosticView() });
+ }
+
+ diagnosticView(): unknown {
+ let view = this.diagnostic;
+ if (view === null) {
+ view = {
+ origin: this.origin,
+ completed: this.completed,
+ method: this.method,
+ path: this.path,
+ headers: this.headers,
+ };
+ this.diagnostic = view;
+ }
+ return view;
+ }
+
+ /** The DiagnosticsChannel.Request view's raw header block. */
+ get headers(): string {
+ let out = "";
+ for (let i = 0; i < this.headerPairs.length; i += 2) {
+ out += `${this.headerPairs[i]}: ${this.headerPairs[i + 1]}\r\n`;
+ }
+ return out;
+ }
+
+ addHeader(key: string, value: string): Request {
+ this.headerPairs.push(key, value);
+ return this;
+ }
+
+ onBodySent(chunk: Buffer): void {
+ const cb = this.handler.onBodySent;
+ if (cb !== undefined) {
+ try {
+ this.handler.onBodySent!(chunk.length, chunk.length);
+ } catch (err) {
+ this.onError(err as Error);
+ }
+ }
+ }
+
+ onRequestSent(): void {
+ if (channels.bodySent.hasSubscribers)
+ channels.bodySent.publish({ request: this.diagnosticView() });
+ }
+
+ onConnect(abort: (err?: Error) => void): void {
+ if (this.error !== null) {
+ abort(this.error);
+ return;
+ }
+ this.abortFn = abort;
+ if (this.handler.onConnect !== undefined) this.handler.onConnect!(abort);
+ }
+
+ onHeaders(
+ statusCode: number,
+ headers: Buffer[],
+ resume: () => void,
+ statusText: string,
+ ): boolean {
+ if (channels.headers.hasSubscribers) {
+ channels.headers.publish({
+ request: this.diagnosticView(),
+ response: { statusCode, headers, statusText },
+ });
+ }
+ if (this.handler.onHeaders === undefined) return true;
+ return this.handler.onHeaders!(statusCode, headers, resume, statusText);
+ }
+
+ onData(chunk: Buffer): boolean {
+ if (this.handler.onData === undefined) return true;
+ return this.handler.onData!(chunk);
+ }
+
+ onComplete(trailers: Buffer[]): void {
+ this.completed = true;
+ if (channels.trailers.hasSubscribers)
+ channels.trailers.publish({ request: this.diagnosticView(), trailers });
+ if (this.handler.onComplete !== undefined) {
+ const raw: string[] = [];
+ for (const t of trailers) raw.push(t.toString("latin1"));
+ this.handler.onComplete!(raw);
+ }
+ }
+
+ onError(error: Error): void {
+ if (channels.error.hasSubscribers)
+ channels.error.publish({ request: this.diagnosticView(), error });
+ if (this.aborted) return;
+ this.aborted = true;
+ if (this.handler.onError !== undefined) this.handler.onError!(error);
+ }
+}
+
+function processHeader(request: Request, key: string, val: string, profile: Profile): void {
+ const lower = key.toLowerCase();
+ if (lower === "host") {
+ // The client writes the Host value into the request head as given.
+ if (headerCharRegex.test(val)) throw new InvalidArgumentError(`invalid ${key} header`);
+ if (request.host === null) {
+ request.host = val;
+ return;
+ }
+ if (profile.major >= 7) throw new InvalidArgumentError("duplicate host header");
+ } else if (lower === "content-length") {
+ if (request.contentLength === null) {
+ const n = parseInt(val, 10);
+ if (!Number.isFinite(n) || (profile.major >= 7 && !/^\d+$/.test(val))) {
+ throw new InvalidArgumentError("invalid content-length header");
+ }
+ request.contentLength = n;
+ return;
+ }
+ // The client writes the one Content-Length it validated or computed
+ // from the body; a second field would frame the request twice. Both
+ // profiles reject it with undici 7's error (5.x sends it as given).
+ throw new InvalidArgumentError("duplicate content-length header");
+ }
+ if (lower === "content-type" && request.contentType === null && !headerCharRegex.test(val)) {
+ request.contentType = val;
+ request.headerPairs.push(key, val);
+ return;
+ }
+ if (lower === "transfer-encoding" || lower === "keep-alive" || lower === "upgrade") {
+ throw new InvalidArgumentError(`invalid ${lower} header`);
+ }
+ if (lower === "connection") {
+ if (profile.major < 7) throw new InvalidArgumentError("invalid connection header");
+ for (const token of val.toLowerCase().split(",")) {
+ const trimmed = token.trim();
+ if (!tokenRegExp.test(trimmed)) throw new InvalidArgumentError("invalid connection header");
+ if (trimmed === "close") request.reset = true;
+ }
+ return;
+ }
+ if (lower === "expect") throw new NotSupportedError("expect header not supported");
+ if (!tokenRegExp.test(key)) throw new InvalidArgumentError("invalid header key");
+ if (headerCharRegex.test(val)) throw new InvalidArgumentError(`invalid ${key} header`);
+ request.headerPairs.push(key, val);
+}
+
+/** util.getServerName: the Host header without its port; null for IPs. */
+function serverName(host: string | null): string | null {
+ if (host === null || host === "") return null;
+ let name = host;
+ if (name.startsWith("[")) {
+ const idx = name.indexOf("]");
+ if (idx !== -1) name = name.slice(1, idx);
+ return null;
+ }
+ const idx = name.indexOf(":");
+ if (idx !== -1) name = name.slice(0, idx);
+ return /^\d+\.\d+\.\d+\.\d+$/.test(name) ? null : name;
+}
diff --git a/packages/compiler/shims/undici/core/timers.ts b/packages/compiler/shims/undici/core/timers.ts
new file mode 100644
index 0000000000..d5028b6ebc
--- /dev/null
+++ b/packages/compiler/shims/undici/core/timers.ts
@@ -0,0 +1,78 @@
+// undici 7.x's fast timers (lib/util/timers.js): one unref'd native timer
+// ticking every 499 ms drives every headers/body timeout, so those fire on
+// the first tick at least `delay` after the timer (re)started.
+const RESOLUTION_MS = 1e3;
+const TICK_MS = (RESOLUTION_MS >> 1) - 1;
+const NOT_IN_LIST = -2;
+const TO_BE_CLEARED = -1;
+const PENDING = 0;
+const ACTIVE = 1;
+
+let fastNow = 0;
+// The ticking native timer, held as its refresh closure.
+let refreshTick: (() => void) | null = null;
+const fastTimers: FastTimer[] = [];
+
+function onTick(): void {
+ fastNow += TICK_MS;
+ let idx = 0;
+ let len = fastTimers.length;
+ while (idx < len) {
+ const timer = fastTimers[idx]!;
+ if (timer.state === PENDING) {
+ timer.idleStart = fastNow - TICK_MS;
+ timer.state = ACTIVE;
+ } else if (timer.state === ACTIVE && fastNow >= timer.idleStart + timer.idleTimeout) {
+ timer.state = TO_BE_CLEARED;
+ timer.idleStart = -1;
+ timer.onTimeout();
+ }
+ if (timer.state === TO_BE_CLEARED) {
+ timer.state = NOT_IN_LIST;
+ if (--len !== 0) fastTimers[idx] = fastTimers[len]!;
+ } else {
+ ++idx;
+ }
+ }
+ fastTimers.length = len;
+ if (fastTimers.length !== 0) refreshTimeout();
+}
+
+function refreshTimeout(): void {
+ const refresh = refreshTick;
+ if (refresh !== null) {
+ refresh();
+ } else {
+ const timeout = setTimeout(onTick, TICK_MS);
+ timeout.unref();
+ refreshTick = () => {
+ timeout.refresh();
+ };
+ }
+}
+
+export class FastTimer {
+ state: number;
+ idleTimeout: number;
+ idleStart: number;
+ onTimeout: () => void;
+
+ constructor(callback: () => void, delay: number) {
+ this.state = NOT_IN_LIST;
+ this.idleTimeout = delay;
+ this.idleStart = -1;
+ this.onTimeout = callback;
+ this.refresh();
+ }
+
+ refresh(): void {
+ if (this.state === NOT_IN_LIST) fastTimers.push(this);
+ if (refreshTick === null || fastTimers.length === 1) refreshTimeout();
+ this.state = PENDING;
+ }
+
+ clear(): void {
+ this.state = TO_BE_CLEARED;
+ this.idleStart = -1;
+ }
+}
diff --git a/packages/compiler/shims/undici/core/types.ts b/packages/compiler/shims/undici/core/types.ts
new file mode 100644
index 0000000000..652918afac
--- /dev/null
+++ b/packages/compiler/shims/undici/core/types.ts
@@ -0,0 +1,167 @@
+// Types shared by the undici entries. They restate the subset of undici's
+// own declarations (types/dispatcher.d.ts, connector.d.ts, ...) that the
+// shim implements; test/package-shims/undici-types.test.ts checks them
+// against the installed 5.x and 7.x declarations.
+import type { Socket } from "node:net";
+import type { Dispatcher } from "./dispatcher.ts";
+
+/** Response and request header records (undici's IncomingHttpHeaders). */
+export type IncomingHttpHeaders = Record;
+
+/** What a request's `headers` option accepts here: a record or a flat
+ * [name, value, ...] array. */
+export type UndiciHeaders = Record | string[] | null;
+
+/** The payloads the shim writes. Streams, iterables, Blob and FormData
+ * bodies are outside the implemented subset. */
+export type RequestBody = string | Buffer | Uint8Array | null;
+
+export interface DispatchOptions {
+ origin?: string | URL;
+ path: string;
+ method: string;
+ body?: RequestBody;
+ headers?: UndiciHeaders;
+ idempotent?: boolean;
+ blocking?: boolean;
+ headersTimeout?: number | null;
+ bodyTimeout?: number | null;
+ reset?: boolean;
+ throwOnError?: boolean;
+ // Dispatcher.RequestOptions' members live here too: a record passed to
+ // dispatch() is copied to this shape, and interceptors read them back
+ // through a `params as Dispatcher.RequestOptions` cast, as with undici.
+ opaque?: unknown;
+ signal?: AbortSignal | null;
+ onInfo?: (info: { statusCode: number; headers: IncomingHttpHeaders }) => void;
+ highWaterMark?: number;
+}
+
+/** undici's dispatch-controller (7.x handler API). */
+export interface DispatchController {
+ readonly aborted: boolean;
+ readonly paused: boolean;
+ readonly reason: Error | null;
+ abort(reason: Error): void;
+ pause(): void;
+ resume(): void;
+}
+
+/** Dispatch handlers: the 5.x callbacks, plus 7.x's controller callbacks. */
+export interface DispatchHandlers {
+ onConnect?(abort: (err?: Error) => void): void;
+ onError?(err: Error): void;
+ onHeaders?(
+ statusCode: number,
+ headers: Buffer[],
+ resume: () => void,
+ statusText: string,
+ ): boolean;
+ onData?(chunk: Buffer): boolean;
+ onComplete?(trailers: string[] | null): void;
+ onBodySent?(chunkSize: number, totalBytesSent: number): void;
+ onRequestStart?(controller: DispatchController, context: unknown): void;
+ onResponseStart?(
+ controller: DispatchController,
+ statusCode: number,
+ headers: IncomingHttpHeaders,
+ statusMessage?: string,
+ ): void;
+ onResponseData?(controller: DispatchController, chunk: Buffer): void;
+ onResponseEnd?(controller: DispatchController, trailers: IncomingHttpHeaders): void;
+ onResponseError?(controller: DispatchController, error: Error): void;
+}
+
+export type DispatchFn = (options: DispatchOptions, handler: DispatchHandlers) => boolean;
+
+export type DispatchInterceptor = (dispatch: DispatchFn) => DispatchFn;
+
+/** buildConnector's per-connection parameters. */
+export interface ConnectorOptions {
+ hostname: string;
+ host?: string;
+ protocol: string;
+ port: string;
+ servername?: string;
+ localAddress?: string | null;
+}
+
+export type ConnectorCallback = (err: Error | null, socket: Socket | null) => void;
+
+/** A connector: dials, reports through the callback, and returns the socket
+ * (undici 5 and 7 both return it although their types declare void). */
+export type Connector = (options: ConnectorOptions, callback: ConnectorCallback) => Socket;
+
+/** net/tls connect options a connector forwards (the implemented subset). */
+export interface ConnectorBuildOptions {
+ timeout?: number | null;
+ maxCachedSessions?: number | null;
+ socketPath?: string | null;
+ port?: number;
+ rejectUnauthorized?: boolean;
+ ca?: string | Buffer | (string | Buffer)[];
+ servername?: string;
+ /** net.connect's resolver hook: @types/node's LookupFunction is
+ * assignable (method syntax; the callback takes one address or all). */
+ lookup?(
+ hostname: string,
+ options: unknown,
+ callback: (
+ err: NodeJS.ErrnoException | null,
+ address: string | LookupAddress[],
+ family?: number,
+ ) => void,
+ ): void;
+ keepAlive?: boolean;
+ keepAliveInitialDelay?: number;
+ noDelay?: boolean;
+ autoSelectFamily?: boolean;
+ autoSelectFamilyAttemptTimeout?: number;
+}
+
+export interface LookupAddress {
+ address: string;
+ family: number;
+}
+
+export interface ClientOptions {
+ keepAliveTimeout?: number | null;
+ keepAliveMaxTimeout?: number | null;
+ keepAliveTimeoutThreshold?: number | null;
+ pipelining?: number | null;
+ connect?: ConnectorBuildOptions | Connector | null;
+ maxHeaderSize?: number | null;
+ bodyTimeout?: number | null;
+ headersTimeout?: number | null;
+ strictContentLength?: boolean;
+ maxRequestsPerClient?: number;
+ maxResponseSize?: number | null;
+ connectTimeout?: number | null;
+ localAddress?: string;
+ /** 5.x applies `Client` interceptors; 7.x ignores the option. */
+ interceptors?: { Client?: readonly DispatchInterceptor[] | undefined };
+}
+
+export interface PoolOptions extends ClientOptions {
+ factory?(origin: URL, opts: object): Dispatcher;
+ connections?: number | null;
+ interceptors?: {
+ Pool?: readonly DispatchInterceptor[] | undefined;
+ Client?: readonly DispatchInterceptor[] | undefined;
+ };
+}
+
+export interface AgentOptions extends PoolOptions {
+ factory?(origin: string | URL, opts: object): Dispatcher;
+ interceptors?: {
+ Agent?: readonly DispatchInterceptor[] | undefined;
+ Pool?: readonly DispatchInterceptor[] | undefined;
+ Client?: readonly DispatchInterceptor[] | undefined;
+ };
+}
+
+/** Version-dependent behavior, selected by the importing package's installed
+ * undici (the 5.x or 7.x entry). */
+export interface Profile {
+ major: number;
+}
diff --git a/packages/compiler/shims/undici/core/util.ts b/packages/compiler/shims/undici/core/util.ts
new file mode 100644
index 0000000000..2fc9ffbf68
--- /dev/null
+++ b/packages/compiler/shims/undici/core/util.ts
@@ -0,0 +1,67 @@
+// Helpers from undici's lib/core/util.js.
+import { InvalidArgumentError } from "./errors.ts";
+import type { IncomingHttpHeaders, Profile } from "./types.ts";
+
+/** util.parseHeaders over raw [name, value, ...] pairs: lower-cased names,
+ * repeated names collected into arrays. 5.x decodes values as UTF-8 and
+ * treats an empty first value as absent; 7.x decodes latin1. */
+export function parseHeaders(raw: readonly string[], profile: Profile): IncomingHttpHeaders {
+ const obj: IncomingHttpHeaders = {};
+ for (let i = 0; i + 1 < raw.length; i += 2) {
+ const key = raw[i]!.toLowerCase();
+ const value = raw[i + 1]!;
+ const existing = obj[key];
+ const absent = profile.major >= 7 ? existing === undefined : !existing;
+ if (absent) {
+ obj[key] = value;
+ } else if (typeof existing === "string") {
+ obj[key] = [existing, value];
+ } else if (existing !== undefined) {
+ existing.push(value);
+ }
+ }
+ return obj;
+}
+
+/** Raw header Buffers as strings (latin1 on 7.x, UTF-8 on 5.x). */
+export function rawHeaderStrings(raw: readonly Buffer[], profile: Profile): string[] {
+ const out: string[] = [];
+ for (let i = 0; i < raw.length; i++) {
+ // Names are ASCII tokens; values follow the version's decoding.
+ out.push(
+ i % 2 === 0 || profile.major >= 7 ? raw[i]!.toString("latin1") : raw[i]!.toString("utf8"),
+ );
+ }
+ return out;
+}
+
+/** util.parseOrigin: an http(s) origin URL. */
+export function parseOrigin(url: string | URL, profile: Profile): URL {
+ const parsed = parseURL(url, profile);
+ if (parsed.pathname !== "/" || parsed.search !== "" || parsed.hash !== "") {
+ throw new InvalidArgumentError("invalid url");
+ }
+ return parsed;
+}
+
+/** util.parseURL for the string/URL forms. */
+export function parseURL(url: string | URL, profile: Profile): URL {
+ const parsed = typeof url === "string" ? new URL(url) : url;
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
+ // 7.x checks only string inputs; 5.x checks every input.
+ if (profile.major < 7) throw new InvalidArgumentError("invalid protocol");
+ if (typeof url === "string") {
+ throw new InvalidArgumentError(
+ "Invalid URL protocol: the URL must start with `http:` or `https:`.",
+ );
+ }
+ }
+ return parsed;
+}
+
+/** util.parseKeepAliveTimeout: the server's Keep-Alive `timeout=` hint in
+ * milliseconds, or null. */
+export function parseKeepAliveTimeout(value: string): number | null {
+ const m = /timeout=(\d+)/.exec(value);
+ return m !== null ? parseInt(m[1]!, 10) * 1000 : null;
+}
diff --git a/packages/compiler/shims/undici/v5.ts b/packages/compiler/shims/undici/v5.ts
new file mode 100644
index 0000000000..b97c65dfa6
--- /dev/null
+++ b/packages/compiler/shims/undici/v5.ts
@@ -0,0 +1,119 @@
+// scriptc's built-in implementation of the `undici` package, 5.x API.
+//
+// Programs whose installed undici is 5.x compile this module in its place
+// (see packages/compiler/src/frontend/package-shims.ts). It implements the
+// HTTP/1.1 client subset over node:net / node:tls: Dispatcher, Client, Pool,
+// Agent, request(), buildConnector, errors, the global dispatcher, fetch and
+// the diagnostics_channel publications. Anything else undici exports is not
+// declared here, so using it is a compile-time error.
+import { Agent as CoreAgent } from "./core/agent.ts";
+import {
+ getGlobalDispatcher as coreGetGlobalDispatcher,
+ request as coreRequest,
+ setGlobalDispatcher as coreSetGlobalDispatcher,
+} from "./core/api.ts";
+import type { TopLevelRequestOptions } from "./core/api.ts";
+import { Client as CoreClient } from "./core/client.ts";
+import { buildConnector as coreBuildConnector } from "./core/connector.ts";
+import { Dispatcher } from "./core/dispatcher.ts";
+import type { ResponseData } from "./core/dispatcher.ts";
+import { undiciFetch } from "./core/fetch.ts";
+import type { UndiciRequestInit } from "./core/fetch.ts";
+import { Pool as CorePool, PoolStats as CorePoolStats } from "./core/pool.ts";
+import type {
+ AgentOptions,
+ ClientOptions,
+ Connector,
+ ConnectorBuildOptions,
+ ConnectorCallback,
+ ConnectorOptions,
+ DispatchOptions,
+ PoolOptions,
+ Profile,
+} from "./core/types.ts";
+import type { SocketInfo as CoreSocketInfo } from "./core/errors.ts";
+
+const profile: Profile = { major: 5 };
+
+export { Dispatcher };
+export type { DiagnosticsChannel } from "./core/diagnostics-types.ts";
+export * as errors from "./core/errors-v5.ts";
+
+export class Client extends CoreClient {
+ constructor(url: string | URL, options?: Client.Options) {
+ super(url, options, profile);
+ }
+}
+
+export declare namespace Client {
+ type Options = ClientOptions;
+ type SocketInfo = CoreSocketInfo;
+}
+
+export class Pool extends CorePool {
+ constructor(url: string | URL, options?: Pool.Options) {
+ super(url, options, profile);
+ }
+
+ __createClient(origin: URL, options: ClientOptions): Dispatcher {
+ return new Client(origin, options);
+ }
+}
+
+export declare namespace Pool {
+ type Options = PoolOptions;
+ type PoolStats = CorePoolStats;
+}
+
+export class Agent extends CoreAgent {
+ constructor(options?: Agent.Options) {
+ super(options, profile);
+ }
+
+ __createClient(origin: string | URL, options: PoolOptions): Dispatcher {
+ return new Client(origin, options);
+ }
+
+ __createPool(origin: string | URL, options: PoolOptions): Dispatcher {
+ return new Pool(origin, options);
+ }
+}
+
+export declare namespace Agent {
+ type Options = AgentOptions;
+ type DispatchOptions = import("./core/types.ts").DispatchOptions;
+}
+
+export function buildConnector(options?: buildConnector.BuildOptions): buildConnector.connector {
+ return coreBuildConnector(options ?? {});
+}
+
+export declare namespace buildConnector {
+ type BuildOptions = ConnectorBuildOptions;
+ type Options = ConnectorOptions;
+ type Callback = ConnectorCallback;
+ type connector = Connector;
+}
+
+export function setGlobalDispatcher(dispatcher: Dispatcher): void {
+ coreSetGlobalDispatcher(dispatcher);
+}
+
+export function getGlobalDispatcher(): Dispatcher {
+ return coreGetGlobalDispatcher(() => new Agent());
+}
+
+export function request(
+ url: string | URL,
+ options?: TopLevelRequestOptions,
+): Promise {
+ return coreRequest(url, options, () => new Agent(), profile);
+}
+
+export function fetch(input: string | URL, init?: UndiciRequestInit): Promise {
+ return undiciFetch(input, init, () => new Agent(), profile);
+}
+
+// undici's Headers is the WHATWG class the runtime implements natively.
+export import Headers = globalThis.Headers;
+export type { DispatchOptions };
diff --git a/packages/compiler/shims/undici/v7.ts b/packages/compiler/shims/undici/v7.ts
new file mode 100644
index 0000000000..63b1b1f649
--- /dev/null
+++ b/packages/compiler/shims/undici/v7.ts
@@ -0,0 +1,119 @@
+// scriptc's built-in implementation of the `undici` package, 7.x API.
+//
+// Programs whose installed undici is 7.x compile this module in its place
+// (see packages/compiler/src/frontend/package-shims.ts). It implements the
+// HTTP/1.1 client subset over node:net / node:tls: Dispatcher, Client, Pool,
+// Agent, request(), buildConnector, errors, the global dispatcher, fetch and
+// the diagnostics_channel publications. Anything else undici exports is not
+// declared here, so using it is a compile-time error.
+import { Agent as CoreAgent } from "./core/agent.ts";
+import {
+ getGlobalDispatcher as coreGetGlobalDispatcher,
+ request as coreRequest,
+ setGlobalDispatcher as coreSetGlobalDispatcher,
+} from "./core/api.ts";
+import type { TopLevelRequestOptions } from "./core/api.ts";
+import { Client as CoreClient } from "./core/client.ts";
+import { buildConnector as coreBuildConnector } from "./core/connector.ts";
+import { Dispatcher } from "./core/dispatcher.ts";
+import type { ResponseData } from "./core/dispatcher.ts";
+import { undiciFetch } from "./core/fetch.ts";
+import type { UndiciRequestInit } from "./core/fetch.ts";
+import { Pool as CorePool, PoolStats as CorePoolStats } from "./core/pool.ts";
+import type {
+ AgentOptions,
+ ClientOptions,
+ Connector,
+ ConnectorBuildOptions,
+ ConnectorCallback,
+ ConnectorOptions,
+ DispatchOptions,
+ PoolOptions,
+ Profile,
+} from "./core/types.ts";
+import type { SocketInfo as CoreSocketInfo } from "./core/errors.ts";
+
+const profile: Profile = { major: 7 };
+
+export { Dispatcher };
+export type { DiagnosticsChannel } from "./core/diagnostics-types.ts";
+export * as errors from "./core/errors-v7.ts";
+
+export class Client extends CoreClient {
+ constructor(url: string | URL, options?: Client.Options) {
+ super(url, options, profile);
+ }
+}
+
+export declare namespace Client {
+ type Options = ClientOptions;
+ type SocketInfo = CoreSocketInfo;
+}
+
+export class Pool extends CorePool {
+ constructor(url: string | URL, options?: Pool.Options) {
+ super(url, options, profile);
+ }
+
+ __createClient(origin: URL, options: ClientOptions): Dispatcher {
+ return new Client(origin, options);
+ }
+}
+
+export declare namespace Pool {
+ type Options = PoolOptions;
+ type PoolStats = CorePoolStats;
+}
+
+export class Agent extends CoreAgent {
+ constructor(options?: Agent.Options) {
+ super(options, profile);
+ }
+
+ __createClient(origin: string | URL, options: PoolOptions): Dispatcher {
+ return new Client(origin, options);
+ }
+
+ __createPool(origin: string | URL, options: PoolOptions): Dispatcher {
+ return new Pool(origin, options);
+ }
+}
+
+export declare namespace Agent {
+ type Options = AgentOptions;
+ type DispatchOptions = import("./core/types.ts").DispatchOptions;
+}
+
+export function buildConnector(options?: buildConnector.BuildOptions): buildConnector.connector {
+ return coreBuildConnector(options ?? {});
+}
+
+export declare namespace buildConnector {
+ type BuildOptions = ConnectorBuildOptions;
+ type Options = ConnectorOptions;
+ type Callback = ConnectorCallback;
+ type connector = Connector;
+}
+
+export function setGlobalDispatcher(dispatcher: Dispatcher): void {
+ coreSetGlobalDispatcher(dispatcher);
+}
+
+export function getGlobalDispatcher(): Dispatcher {
+ return coreGetGlobalDispatcher(() => new Agent());
+}
+
+export function request(
+ url: string | URL,
+ options?: TopLevelRequestOptions,
+): Promise {
+ return coreRequest(url, options, () => new Agent(), profile);
+}
+
+export function fetch(input: string | URL, init?: UndiciRequestInit): Promise {
+ return undiciFetch(input, init, () => new Agent(), profile);
+}
+
+// undici's Headers is the WHATWG class the runtime implements natively.
+export import Headers = globalThis.Headers;
+export type { DispatchOptions };
diff --git a/packages/compiler/src/frontend/builtin-package-aliases.test.ts b/packages/compiler/src/frontend/builtin-package-aliases.test.ts
new file mode 100644
index 0000000000..2d1001bc24
--- /dev/null
+++ b/packages/compiler/src/frontend/builtin-package-aliases.test.ts
@@ -0,0 +1,70 @@
+import { expect, test } from "vitest";
+import { builtinAliasOf, versionedPackageOfKey } from "./builtin-package-aliases.js";
+
+test("aliases name the builtin each checked major's module is on Node 24", () => {
+ expect(builtinAliasOf("isarray", "1.0.0", "")).toEqual({ whole: "Array.isArray" });
+ expect(builtinAliasOf("isarray", "0.0.1", "index.js")).toEqual({ whole: "Array.isArray" });
+ expect(builtinAliasOf("isarray", "3.0.0", "")).toBeNull();
+ expect(builtinAliasOf("safe-buffer", "5.1.2", "")).toEqual({ whole: 'require("buffer")' });
+ expect(builtinAliasOf("process-nextick-args", "2.0.1", "")).toEqual({ whole: "process" });
+ expect(builtinAliasOf("process-nextick-args", "1.0.7", "")).toBeNull();
+ expect(builtinAliasOf("inherits", "2.0.4", "inherits.js")).toEqual({
+ whole: 'require("util").inherits',
+ });
+ // The browser file is the package's own implementation.
+ expect(builtinAliasOf("inherits", "2.0.4", "inherits_browser.js")).toBeNull();
+ expect(builtinAliasOf("util-deprecate", "1.0.2", "node.js")).toEqual({
+ whole: 'require("util").deprecate',
+ });
+ expect(builtinAliasOf("util-deprecate", "1.0.2", "browser.js")).toBeNull();
+});
+
+test("readable-stream aliases follow each major's layout", () => {
+ const rs2 = builtinAliasOf("readable-stream", "2.3.8", "readable.js");
+ expect(rs2?.whole).toBe('require("stream").Readable');
+ expect([...(rs2?.members?.names ?? [])].sort()).toEqual(
+ ["Duplex", "PassThrough", "Readable", "Stream", "Transform", "Writable"].sort(),
+ );
+ expect(builtinAliasOf("readable-stream", "2.3.8", "writable.js")?.whole).toBe(
+ 'require("stream").Writable',
+ );
+ expect(builtinAliasOf("readable-stream", "2.3.8", "lib/_stream_duplex.js")?.whole).toBe(
+ 'require("stream").Duplex',
+ );
+ expect(builtinAliasOf("readable-stream", "2.3.8", "lib/internal/streams/destroy.js")).toBeNull();
+ // 3.x has no root writable.js; its entry also carries finished and pipeline.
+ const rs3 = builtinAliasOf("readable-stream", "3.6.2", "");
+ expect(rs3?.whole).toBe('require("stream").Readable');
+ expect([...(rs3?.members?.names ?? [])]).toContain("pipeline");
+ expect(builtinAliasOf("readable-stream", "3.6.2", "lib/_stream_writable.js")?.whole).toBe(
+ 'require("stream").Writable',
+ );
+ expect(builtinAliasOf("readable-stream", "3.6.2", "writable.js")).toBeNull();
+ expect(builtinAliasOf("readable-stream", "3.6.2", "lib/internal/streams/pipeline.js")).toBeNull();
+ const rs4 = builtinAliasOf("readable-stream", "4.7.0", "lib/ours/index.js");
+ expect(rs4?.whole).toBeNull();
+ expect(rs4?.members?.names.has("pipeline")).toBe(true);
+ expect(rs4?.members?.names.has("promises")).toBe(false);
+ expect(builtinAliasOf("readable-stream", "4.7.0", "transform.js")).toBeNull();
+ expect(builtinAliasOf("readable-stream", "4.7.0", "lib/_stream_transform.js")?.whole).toBe(
+ 'require("stream").Transform',
+ );
+});
+
+test("pnpm module keys name the package, version and file", () => {
+ expect(
+ versionedPackageOfKey("../node_modules/.pnpm/isarray@1.0.0/node_modules/isarray/index.js"),
+ ).toEqual({ name: "isarray", version: "1.0.0", subpath: "index.js" });
+ expect(
+ versionedPackageOfKey(
+ "node_modules/.pnpm/@scope+pkg@2.1.0_peer@1.0.0/node_modules/@scope/pkg/lib/a.js",
+ ),
+ ).toEqual({ name: "@scope/pkg", version: "2.1.0", subpath: "lib/a.js" });
+ // A store entry's own dependency links name other packages.
+ expect(
+ versionedPackageOfKey(
+ "node_modules/.pnpm/from2@2.3.0/node_modules/readable-stream/readable.js",
+ ),
+ ).toBeNull();
+ expect(versionedPackageOfKey("node_modules/isarray/index.js")).toBeNull();
+});
diff --git a/packages/compiler/src/frontend/builtin-package-aliases.ts b/packages/compiler/src/frontend/builtin-package-aliases.ts
new file mode 100644
index 0000000000..ccb8dc5a3a
--- /dev/null
+++ b/packages/compiler/src/frontend/builtin-package-aliases.ts
@@ -0,0 +1,150 @@
+/* Polyfill packages whose behavior on Node 24 IS a Node builtin. The
+ * package-shim registry (package-shims.ts) consults this table: a require
+ * of such a package, from an installed copy's importer or through a bundled
+ * __commonJS chunk whose path names the package, compiles as the builtin
+ * expression its module evaluates to on Node 24, at the require's position
+ * (loading a builtin runs no code, and none of these packages has an
+ * observable side effect at load).
+ *
+ * Each entry names the major versions whose entry modules were checked:
+ *
+ * - isarray 0.x, 1.x, 2.x: `module.exports = Array.isArray || function …`;
+ * Node always has Array.isArray, so the export is Array.isArray itself.
+ * - safe-buffer 5.x: `module.exports = buffer` when Buffer.from, alloc,
+ * allocUnsafe and allocUnsafeSlow exist (Node 6 and later); the rest of
+ * the module only builds its unexported SafeBuffer fallback.
+ * - process-nextick-args 2.x: `module.exports = process` unless
+ * process.version is v0.x or a pre-1.8 v1.x.
+ * - inherits 2.x: inherits.js exports require("util").inherits (2.0.1
+ * directly, 2.0.3 and later whenever it is a function, always on Node).
+ * - util-deprecate 1.x: node.js (its Node entry) is
+ * `module.exports = require("util").deprecate`.
+ * - readable-stream 2.x, 3.x, 4.x: a documented divergence. The package
+ * ships a JavaScript copy of Node's streams; its READABLE_STREAM=disable
+ * behavior hands out Node's own stream classes instead, and that is what
+ * compiles:
+ * - 2.x readable.js exports stream.Readable carrying Readable, Writable,
+ * Duplex, Transform, PassThrough and Stream; writable.js exports
+ * stream.Writable and duplex/transform/passthrough.js read the entry;
+ * - 3.x (a copy of Node 10's streams) readable.js exports the same
+ * members plus finished and pipeline. Its own disable branch throws on
+ * Node 24 (it Object.assigns every stream export onto stream.Readable,
+ * which inherits the getter-only `promises` accessor), so 3.x maps the
+ * members its default branch exports onto the builtin's;
+ * - 4.x lib/ours/index.js exports a plain object of named stream exports.
+ * The lib/_stream_*.js implementation files map to the matching builtin
+ * classes as well (Node itself loads the package's JavaScript
+ * implementations through them, whatever the environment). Without the
+ * variable Node runs the package's own JavaScript streams. */
+
+export interface BuiltinAlias {
+ /** The expression the module's export evaluates to, or null when only
+ * its member reads have a builtin spelling. */
+ whole: string | null;
+ /** For an export that carries another object's members (readable-stream's
+ * entry): member reads of these names read `object`'s members. */
+ members?: { object: string; names: ReadonlySet };
+}
+
+const STREAM_CLASSES = ["Readable", "Writable", "Duplex", "Transform", "PassThrough"];
+
+/** The members readable-stream 4.x's entry copies from `stream` under
+ * READABLE_STREAM=disable. */
+const RS4_ENTRY_EXPORTS = [
+ "_uint8ArrayToBuffer",
+ "_isUint8Array",
+ "isDisturbed",
+ "isErrored",
+ "isReadable",
+ ...STREAM_CLASSES,
+ "addAbortSignal",
+ "finished",
+ "destroy",
+ "pipeline",
+ "compose",
+ "Stream",
+];
+
+function majorOf(version: string): number {
+ return Number(/^(\d+)\./.exec(version)?.[1] ?? Number.NaN);
+}
+
+function stripJs(subpath: string): string {
+ return subpath.replace(/^\.\//, "").replace(/\.js$/, "");
+}
+
+/** The builtin a package module is on Node 24, or null. `subpath` is the
+ * file inside the package ("" for its main entry). */
+export function builtinAliasOf(pkg: string, version: string, subpath: string): BuiltinAlias | null {
+ const major = majorOf(version);
+ const file = stripJs(subpath);
+ switch (pkg) {
+ case "isarray":
+ return (major === 0 || major === 1 || major === 2) && (file === "" || file === "index")
+ ? { whole: "Array.isArray" }
+ : null;
+ case "safe-buffer":
+ return major === 5 && (file === "" || file === "index")
+ ? { whole: 'require("buffer")' }
+ : null;
+ case "process-nextick-args":
+ return major === 2 && (file === "" || file === "index") ? { whole: "process" } : null;
+ case "inherits":
+ return major === 2 && (file === "" || file === "inherits")
+ ? { whole: 'require("util").inherits' }
+ : null;
+ case "util-deprecate":
+ return major === 1 && (file === "" || file === "node")
+ ? { whole: 'require("util").deprecate' }
+ : null;
+ case "readable-stream": {
+ if (major !== 2 && major !== 3 && major !== 4) return null;
+ const stream = 'require("stream")';
+ const entry = major === 4 ? file === "lib/ours/index" : file === "readable";
+ if (file === "" || entry) {
+ if (major === 4)
+ return { whole: null, members: { object: stream, names: new Set(RS4_ENTRY_EXPORTS) } };
+ const names = new Set([
+ ...STREAM_CLASSES,
+ "Stream",
+ ...(major === 3 ? ["finished", "pipeline"] : []),
+ ]);
+ return { whole: `${stream}.Readable`, members: { object: stream, names } };
+ }
+ for (const cls of STREAM_CLASSES) {
+ const lower = cls.toLowerCase();
+ if (file === `lib/_stream_${lower}` || (major === 2 && file === lower))
+ return { whole: `${stream}.${cls}` };
+ }
+ return null;
+ }
+ default:
+ return null;
+ }
+}
+
+/** Package names the table can alias (a cheap pre-filter). */
+export const BUILTIN_ALIAS_PACKAGES: ReadonlySet = new Set([
+ "isarray",
+ "safe-buffer",
+ "process-nextick-args",
+ "inherits",
+ "util-deprecate",
+ "readable-stream",
+]);
+
+/** The package, version and in-package file a bundler's module key names
+ * when it carries a version (pnpm's `node_modules/.pnpm/@/
+ * node_modules//`), or null. */
+export function versionedPackageOfKey(
+ key: string,
+): { name: string; version: string; subpath: string } | null {
+ const m =
+ /(?:^|\/)node_modules\/\.pnpm\/((?:@[^/+]+\+)?[^/@]+)@([^/_]+)[^/]*\/node_modules\/((?:@[^/]+\/)?[^/]+)\/(.+)$/.exec(
+ key,
+ );
+ if (m === null) return null;
+ const [, storeName, version, name, subpath] = m;
+ if (storeName!.replace("+", "/") !== name) return null;
+ return { name: name!, version: version!, subpath: subpath! };
+}
diff --git a/packages/compiler/src/frontend/dts-paths.ts b/packages/compiler/src/frontend/dts-paths.ts
index 064cb9b857..3b2f32413a 100644
--- a/packages/compiler/src/frontend/dts-paths.ts
+++ b/packages/compiler/src/frontend/dts-paths.ts
@@ -1,5 +1,5 @@
import { createRequire } from "node:module";
-import { join } from "node:path";
+import { dirname, join } from "node:path";
import { npmPackageNameOf } from "./workspace-registry.js";
import { tsgoPath } from "./ts7/session-path.js";
@@ -25,6 +25,15 @@ function declarationPath(name: string): string {
);
}
+/** Directory of the shipped package shims (packages/compiler/shims): the
+ * implementation modules package-shims.ts maps npm packages to. A native
+ * distribution ships them beside its declarations. */
+export function packageShimRoot(): string {
+ return installedDeclarationRoot === null
+ ? join(dirname(require.resolve("@scriptc/compiler/scriptc.d.ts")), "..", "shims")
+ : join(installedDeclarationRoot, "shims");
+}
+
/** Path of the shipped ambient declarations — the always-shipped CORE
* (comptime, setTimeout). Part of EVERY program scriptc
* builds, the project-world preflight program included. */
diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts
index 7230d18cda..2d531663f3 100644
--- a/packages/compiler/src/frontend/lowering/builtins/timers.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts
@@ -156,7 +156,11 @@ function isTimerHandleTyped(
const sym = t.getAliasSymbol() ?? t.getSymbol();
if (sym === undefined || ts.symbolName(sym) !== name) return false;
return sym.declarations.some(
- (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
+ (d) =>
+ // An interface in the fallback and @types/node 22+, a class in
+ // @types/node 20 and older.
+ (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
+ lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
);
}
function isTimeoutTyped(lowerer: Lowerer, node: ts.Expression): boolean {
diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts
index 30030b18fe..ae8cce4caf 100644
--- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts
+++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts
@@ -25,6 +25,7 @@ import {
cjsExportDiscardReason,
cjsExportTargetLiteral,
isCjsJsFile,
+ isJsSourceFile,
isModuleExportsAccess,
isRequireStatement,
locOf,
@@ -367,6 +368,50 @@ function symbolIsThisFreeFunction(lowerer: Lowerer, sym: ts.Symbol | undefined):
);
}
+/** The ES-module half (a TypeScript module required from JavaScript, such
+ * as a package shim): require() answers its namespace, so a call through
+ * it binds `this` to the namespace. True when the export is a function
+ * that never reads `this` (every declaration of an overload set), a
+ * builtin module's member re-exported from its declarations, or reached
+ * through an `export *` of a module where that holds. */
+function esModuleExportIgnoresThis(
+ lowerer: Lowerer,
+ sf: ts.SourceFile,
+ name: string,
+ depth: number,
+): boolean {
+ const moduleSym = lowerer.checker.getSymbolAtLocation(sf);
+ const exported = moduleSym?.getExports().get(name as ts.__String);
+ if (exported !== undefined) {
+ const target =
+ exported.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(exported) : exported;
+ const decls = target.declarations;
+ if (decls.length === 0) return false;
+ return decls.every(
+ (d) =>
+ (ts.isFunctionDeclaration(d) && !functionUsesThis(d)) ||
+ (ts.getSourceFileOfNode(d).isDeclarationFile && ts.isFunctionDeclaration(d)),
+ );
+ }
+ for (const stmt of sf.statements.nodes) {
+ if (
+ !ts.isExportDeclaration(stmt) ||
+ stmt.exportClause !== undefined ||
+ stmt.moduleSpecifier === undefined ||
+ !ts.isStringLiteral(stmt.moduleSpecifier)
+ )
+ continue;
+ const dep = resolveImport(lowerer.program, sf, stmt.moduleSpecifier.text);
+ if (
+ dep !== null &&
+ !dep.isDeclarationFile &&
+ esModuleExportIgnoresThis(lowerer, dep, name, depth + 1)
+ )
+ return true;
+ }
+ return false;
+}
+
/** True when every value the module assigns to its export `name` is a
* function that never reads `this`. A call through the exports object
* (`exports.f(x)`) binds `this` to that object, which the static export
@@ -378,6 +423,7 @@ export function cjsExportCallIgnoresThis(
depth = 0,
): boolean {
if (depth > 16) return false;
+ if (!isJsSourceFile(sf)) return esModuleExportIgnoresThis(lowerer, sf, name, depth);
const values: ts.Expression[] = [];
ts.walkPreorder(sf, (node) => {
if (
diff --git a/packages/compiler/src/frontend/lowering/class-to-primitive.ts b/packages/compiler/src/frontend/lowering/class-to-primitive.ts
index 516a62f3c8..f3a6b23fa9 100644
--- a/packages/compiler/src/frontend/lowering/class-to-primitive.ts
+++ b/packages/compiler/src/frontend/lowering/class-to-primitive.ts
@@ -270,8 +270,8 @@ function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string):
}
/** ToString(ToPrimitive(value, hint)) of a union whose arms are program
- * class instances and primitives (`Box | undefined`): each arm converts as
- * it would alone. Null for other unions. */
+ * class instances, URLs and primitives (`Box | undefined`, `string | URL`):
+ * each arm converts as it would alone. Null for other unions. */
export function classUnionToString(
lowerer: Lowerer,
value: IrExpr,
@@ -281,9 +281,11 @@ export function classUnionToString(
if (value.type.kind !== "union") return null;
const unionId = value.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms;
- if (!arms || !arms.some((arm) => arm.kind === "object")) return null;
+ if (!arms || !arms.some((arm) => arm.kind === "object" || arm.kind === "url")) return null;
const convertible = (arm: IrType): boolean => {
if (PRIMITIVE_KINDS.has(arm.kind)) return true;
+ // A URL arm converts through its href, like String(url).
+ if (arm.kind === "url") return true;
if (arm.kind !== "object") return false;
const info = lowerer.classes.get(arm.className);
return info !== undefined && plainProgramClass(info);
diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
index e3a24ea1ae..7fe5061aea 100644
--- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
@@ -1,4 +1,5 @@
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
+import { staticallySelected } from "../static-typeof.js";
import * as ts from "../../ts7/adapter.js";
import { lowerHistogramProperty } from "../builtins/performance.js";
import { staticFunctionNameRead } from "../static-function-names.js";
@@ -478,7 +479,10 @@ export function lowerPropertyAccessExpr(
// undefined value so stored probes compose with optional calls.
if (
stdlibGlobalNameOf(lowerer, expr.expression) === "globalThis" &&
- !lowerer.checker.getPropertyOfType(lowerer.typeOf(expr.expression), expr.name.text)
+ !lowerer.checker.getPropertyOfType(
+ lowerer.typeOf(staticallySelected(lowerer, expr.expression)),
+ expr.name.text,
+ )
) {
return dynUndefinedExpr(loc);
}
diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts
index 46005e2281..cc3ac06fb4 100644
--- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts
@@ -171,6 +171,21 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
}
if (constructorGlobalTypeof(lowerer, tested) !== null)
return { kind: "strLit", value: "function", type: STRING, loc };
+ // `typeof global.setTimeout` / `typeof globalThis.queueMicrotask`: a
+ // callable standard-library global read through the global object
+ // answers like its bare spelling (JavaScript sources represent the
+ // value as an identity token, so the operand cannot answer).
+ if (ts.isPropertyAccessExpression(tested) && !tested.questionDotToken) {
+ const name = stdlibGlobalNameOf(lowerer, tested);
+ if (name !== null && name !== "globalThis") {
+ const t = lowerer.typeOf(tested);
+ if (
+ lowerer.checker.getCallSignatures(t).length > 0 ||
+ lowerer.checker.getConstructSignatures(t).length > 0
+ )
+ return { kind: "strLit", value: "function", type: STRING, loc };
+ }
+ }
// `typeof globalThis.AbortSignal.any` (ky's feature probe): a static
// member of a standard-library global, declared callable, whose value
// has no representation. The read has no effects and Node's answer
diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index 70cb732cc8..0c6287cc71 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -8,6 +8,7 @@ import {
} from "./constructor-returns.js";
import { checkedIterableSpread } from "./checked-iterable-spread.js";
import { lowerReadlineUiNew } from "./builtins/readline.js";
+import { builtinMemberRequireDecl } from "./builtins/module-bindings.js";
import { checkedClassUnionAssertion } from "./class-assertions.js";
import { collectClassMethodMutations } from "./class-method-mutations.js";
import { InternalCompilerError } from "../../errors.js";
@@ -1036,11 +1037,16 @@ export function builtinStreamInfoOf(
// member to the stdlib class symbol. The declaration itself is
// alias plumbing (streamClassAliasDecl — both declaration walks
// skip it).
+ // A `var`/`let` binding of the inline require member
+ // (`var Transform = require("stream").Transform`) is the same alias
+ // plumbing as builtinMemberRequireDecl's: its declaration owns no
+ // storage either way.
const decl = symbol.valueDeclaration;
if (
decl &&
ts.isVariableDeclaration(decl) &&
- (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 &&
+ ((ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 ||
+ builtinMemberRequireDecl(decl.name, decl.initializer)) &&
decl.initializer !== undefined &&
ts.isPropertyAccessExpression(decl.initializer) &&
!decl.initializer.questionDotToken &&
@@ -1399,7 +1405,8 @@ export function builtinEmitterInfoOf(
if (
decl !== undefined &&
ts.isVariableDeclaration(decl) &&
- (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 &&
+ ((ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0 ||
+ builtinMemberRequireDecl(decl.name, decl.initializer)) &&
decl.initializer !== undefined &&
ts.isPropertyAccessExpression(decl.initializer) &&
!decl.initializer.questionDotToken &&
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index 3e52f0cdc9..4fb7350a05 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -11,6 +11,7 @@ import { InternalCompilerError } from "../../errors.js";
* (FieldTarget). */
import * as ts from "../ts7/adapter.js";
import { checkedPrivateReceiver, localPrivateBrandClass } from "./private-brands.js";
+import { staticCondition } from "./static-typeof.js";
import { constituentTypes } from "../ts7/checker.js";
import type { Lowerer } from "./lowerer.js";
import { lowerMaybePromiseAwait, maybePromiseUnionOf } from "./maybe-promise.js";
@@ -1096,6 +1097,11 @@ export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr {
});
if (presence !== null) return presence;
}
+ // Global detection (`typeof window < "u"`, `typeof self !==
+ // "undefined" && …`): a condition whose typeof answers are static
+ // folds, so the untaken arm never lowers (static-typeof.ts).
+ const folded = staticCondition(lowerer, e);
+ if (folded !== null) return { kind: "boolLit", value: folded, type: BOOL, loc: locOf(e) };
if (ts.isBinaryExpression(e)) {
const op = e.operatorToken.kind;
if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index 68dbce996f..6682ffb23b 100644
--- a/packages/compiler/src/frontend/lowering/lower-modules.ts
+++ b/packages/compiler/src/frontend/lowering/lower-modules.ts
@@ -157,6 +157,49 @@ export interface FileParts {
topStmts: ts.Statement[];
}
+/** Preflight's module graph (LoadResult.moduleEdges), for the walks that
+ * append modules outside the startup order, and the set collecting the
+ * inline require() calls that close a cycle there. */
+export interface AppendGraph {
+ edges?: ReadonlyMap | undefined;
+ inlineCycleCalls?: Set;
+}
+
+/** An appended module's edges: its import and export declarations, then
+ * the require() edges preflight resolved — a CommonJS file's only edges
+ * (the compiled CommonJS package a workspace module loaded on demand
+ * imports, and the sibling packages and files IT requires). */
+function appendedEdgesOf(
+ program: ts.Program,
+ graph: AppendGraph | undefined,
+): (sf: ts.SourceFile) => CycleEdge[] {
+ const cache = new Map();
+ return (sf) => {
+ let out = cache.get(sf);
+ if (out !== undefined) return out;
+ out = orderedImportsOf(program, sf).flatMap(({ stmt, dep }) =>
+ dep !== null && dep !== sf
+ ? [{ dep, stmt: stmt as ts.ImportDeclaration | ts.ExportDeclaration }]
+ : [],
+ );
+ for (const e of graph?.edges?.get(sf) ?? [])
+ if (e.stmt === undefined && e.dep !== sf) out.push(e);
+ cache.set(sf, out);
+ return out;
+ };
+}
+
+/** Lazy inline requires run after their module's top level: their modules
+ * join the order afterwards, as in preflight's walk. */
+function visitLazyEdges(
+ sf: ts.SourceFile,
+ edgesOf: (sf: ts.SourceFile) => CycleEdge[],
+ state: ReadonlyMap,
+ visit: (sf: ts.SourceFile) => void,
+): void {
+ for (const e of edgesOf(sf)) if (e.lazy === true && state.get(e.dep) === undefined) visit(e.dep);
+}
+
/** Program modules selected by statically resolved child_process.fork()
* calls join the compiled graph without becoming startup dependencies of
* the parent entry. Their guarded init functions are selected by %main only
@@ -167,8 +210,9 @@ export function appendForkModules(
order: ts.SourceFile[],
targets: ts.SourceFile[],
onCycle: (cycle: string, reason: string) => void,
+ graph?: AppendGraph,
): void {
- appendEntryModules(program, order, targets, forkTargetPaths(program, order), onCycle);
+ appendEntryModules(program, order, targets, forkTargetPaths(program, order), onCycle, graph);
}
export function appendWorkerModules(
@@ -176,8 +220,9 @@ export function appendWorkerModules(
order: ts.SourceFile[],
targets: ts.SourceFile[],
onCycle: (cycle: string, reason: string) => void,
+ graph?: AppendGraph,
): void {
- appendEntryModules(program, order, targets, workerTargetPaths(program, order), onCycle);
+ appendEntryModules(program, order, targets, workerTargetPaths(program, order), onCycle, graph);
}
function appendEntryModules(
@@ -186,6 +231,7 @@ function appendEntryModules(
targets: ts.SourceFile[],
paths: readonly string[],
onCycle: (cycle: string, reason: string) => void,
+ graph?: AppendGraph,
): void {
if (order.length === 0) return;
const byPath = new Map(
@@ -195,22 +241,24 @@ function appendEntryModules(
for (const sf of order) state.set(sf, "done");
const added: ts.SourceFile[] = [];
const stack: string[] = [];
- const staticEdgesOf = (sf: ts.SourceFile): CycleEdge[] =>
- orderedImportsOf(program, sf).flatMap(({ stmt, dep }) =>
- dep !== null && dep !== sf
- ? [{ dep, stmt: stmt as ts.ImportDeclaration | ts.ExportDeclaration }]
- : [],
- );
- const cycleAdmissionReason = makeCycleAdmission(program, staticEdgesOf);
+ const staticEdgesOf = appendedEdgesOf(program, graph);
+ const cycleAdmissionReason = makeCycleAdmission(program, (sf) =>
+ staticEdgesOf(sf).filter((e) => e.call === undefined),
+ );
const visit = (sf: ts.SourceFile): void => {
const prior = state.get(sf);
if (prior === "done") return;
state.set(sf, "visiting");
stack.push(sf.fileName);
for (const edge of staticEdgesOf(sf)) {
+ if (edge.lazy === true) continue; // visited after this module completes
const edgeState = state.get(edge.dep);
if (edgeState === "done") continue;
if (edgeState === "visiting") {
+ if (edge.call !== undefined) {
+ graph?.inlineCycleCalls?.add(edge.call);
+ continue;
+ }
const reason = cycleAdmissionReason(sf, edge);
if (reason !== null) {
const cycleStart = stack.indexOf(edge.dep.fileName);
@@ -223,6 +271,7 @@ function appendEntryModules(
stack.pop();
state.set(sf, "done");
added.push(sf);
+ visitLazyEdges(sf, staticEdgesOf, state, visit);
};
const knownTargets = new Set(targets);
@@ -318,27 +367,30 @@ export function appendDynamicImportModules(
program: ts.Program,
order: ts.SourceFile[],
onCycle: (cycle: string, reason: string) => void,
+ graph?: AppendGraph,
): void {
if (order.length === 0) return; // no preflight order — sites keep their fences
const state = new Map();
for (const sf of order) state.set(sf, "done");
const added: ts.SourceFile[] = [];
const stack: string[] = [];
- const staticEdgesOf = (sf: ts.SourceFile): CycleEdge[] =>
- orderedImportsOf(program, sf).flatMap(({ stmt, dep }) =>
- dep !== null && dep !== sf
- ? [{ dep, stmt: stmt as ts.ImportDeclaration | ts.ExportDeclaration }]
- : [],
- );
- const cycleAdmissionReason = makeCycleAdmission(program, staticEdgesOf);
+ const staticEdgesOf = appendedEdgesOf(program, graph);
+ const cycleAdmissionReason = makeCycleAdmission(program, (sf) =>
+ staticEdgesOf(sf).filter((e) => e.call === undefined),
+ );
const visit = (sf: ts.SourceFile): void => {
if (state.get(sf) !== undefined) return;
state.set(sf, "visiting");
stack.push(sf.fileName);
for (const e of staticEdgesOf(sf)) {
+ if (e.lazy === true) continue; // visited after this module completes
const s = state.get(e.dep);
if (s === "done") continue;
if (s === "visiting") {
+ if (e.call !== undefined) {
+ graph?.inlineCycleCalls?.add(e.call);
+ continue;
+ }
// A back edge: Node answers it from the cache. Same admission
// question as preflight's static walk, same fence when refused.
const reason = cycleAdmissionReason(sf, e);
@@ -353,6 +405,7 @@ export function appendDynamicImportModules(
stack.pop();
state.set(sf, "done");
added.push(sf);
+ visitLazyEdges(sf, staticEdgesOf, state, visit);
};
const scanned = new Set();
for (;;) {
diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts
index 9997eb7d94..dca835d779 100644
--- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts
+++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts
@@ -37,6 +37,7 @@
* statement is not modeled — same as top-level const reads through early
* calls). */
import * as ts from "../ts7/adapter.js";
+import { packageShimFor } from "../package-shims.js";
import type { Lowerer } from "./lowerer.js";
import { boundIdentifiersOf } from "./lowerer.js";
import type { FileParts } from "./lower-modules.js";
@@ -253,6 +254,10 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile
sym = lowerer.checker.getSymbolAtLocation(e.name);
}
if (!sym) return null;
+ // The import binding's own declaration stays in view: a package scriptc
+ // implements itself (package-shims.ts) types through a declaration
+ // file the alias resolves to, while the import compiles the shim.
+ const bindingDecls = sym.declarations;
if (sym.flags & ts.SymbolFlags.Alias) sym = lowerer.checker.getAliasedSymbol(sym);
for (const d of sym.declarations) {
if (
@@ -284,6 +289,20 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile
}
}
}
+ for (const d of bindingDecls) {
+ if (!ts.isNamespaceImport(d)) continue;
+ const stmt = d.parent?.parent;
+ if (
+ stmt === undefined ||
+ !ts.isImportDeclaration(stmt) ||
+ !ts.isStringLiteral(stmt.moduleSpecifier)
+ )
+ continue;
+ const from = ts.getSourceFileOfNode(d);
+ if (packageShimFor(from.fileName, stmt.moduleSpecifier.text) === null) continue;
+ const dep = resolveImport(lowerer.program, from, stmt.moduleSpecifier.text);
+ if (dep && !dep.isDeclarationFile && lowerer.fileTag.has(dep)) return dep;
+ }
return null;
}
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index 2d814b4578..efa2813f1f 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -14,7 +14,8 @@ import {
matchAllRowsReadByElements,
storedMatchAllRowsReadByElements,
} from "./containers/match-row-uses.js";
-import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js";
+import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js";
+import { declaresHoistedBinding, staticCondition } from "./static-typeof.js";
import { lowerCollectionSpread } from "./containers/collection-methods.js";
import { lowerEventObjectPropertyWrite } from "./lower-event-target.js";
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
@@ -1546,6 +1547,20 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
}
if (ts.isIfStatement(stmt)) {
const cond = lowerer.lowerCondition(stmt.expression);
+ // A global-detection condition (static-typeof.ts) folds; its untaken
+ // arm is code Node never runs and does not lower, unless it declares a
+ // hoisted binding.
+ const decided = staticCondition(lowerer, stmt.expression);
+ if (decided !== null) {
+ const taken = decided ? stmt.thenStatement : stmt.elseStatement;
+ const untaken = decided ? stmt.elseStatement : stmt.thenStatement;
+ if (untaken === undefined || !declaresHoistedBinding(untaken)) {
+ const arm = taken === undefined ? [] : lowerer.lowerScopedBlock(taken);
+ return decided
+ ? { kind: "if", cond, then: arm, else_: null, loc: locOf(stmt) }
+ : { kind: "if", cond, then: [], else_: arm, loc: locOf(stmt) };
+ }
+ }
// Aliased-typeof narrows (ms's `var type = typeof val` guard): the
// then-branch lowers under what a TRUE condition proves, the
// else-branch under a FALSE one — the var/let alias narrowing the
@@ -7094,16 +7109,12 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
if (!isRelativeSpecifier(spec) && canonicalBuiltinModule(spec) === null) {
const refusal = probeNodeRequireRefusal(sf.fileName, spec);
if (refusal !== null) {
+ // The same throw as the expression form (cjs-inline-require):
+ // an Error carrying Node's MODULE_NOT_FOUND code.
const loc = locOf(expr);
return {
- kind: "throw",
- value: {
- kind: "libCall",
- fn: "error.new",
- args: [{ kind: "strLit", value: refusal.message, type: STRING, loc }],
- type: { kind: "object", className: "%Error" },
- loc,
- },
+ kind: "exprStmt",
+ expr: nodeThrowExpr(0, "MODULE_NOT_FOUND", refusal.message, VOID, loc),
loc,
};
}
@@ -11509,18 +11520,23 @@ export function lowerForStatement(lowerer: Lowerer, stmt: ts.ForStatement): IrSt
}
}
-/** safe-buffer's `var Buffer = buffer.Buffer` at file scope: a `var` alias
- * of the global Buffer is the same alias plumbing as the const spelling
- * when nothing reassigns it and no code that runs before the declaration
- * can read it (the require-binding early-reference analysis), so the
- * hoisted undefined is unobservable. */
+/** safe-buffer's `var Buffer = buffer.Buffer` (or `var pna = process`, the
+ * process-nextick-args binding once it compiles as process) at file scope:
+ * a `var` alias of a surfaced global other than the global object itself
+ * is the same alias plumbing as the const spelling when nothing reassigns
+ * it and no code that runs before the declaration can read it (the
+ * require-binding early-reference analysis), so the hoisted undefined is
+ * unobservable. */
export function stdlibBufferVarAlias(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
if (
!ts.isIdentifier(decl.name) ||
!ts.isVariableDeclarationList(decl.parent) ||
(decl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0 ||
decl.initializer === undefined ||
- !bufferNamespaceMember(lowerer, decl.initializer)
+ !(
+ bufferNamespaceMember(lowerer, decl.initializer) ||
+ (stdlibGlobalAliasNameOf(lowerer, decl.initializer) ?? "globalThis") !== "globalThis"
+ )
)
return false;
const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index 8fae8a939b..3915ee9e1e 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -171,6 +171,7 @@ import { resolve } from "node:path";
import { bindingSource } from "../binding-source.js";
import { tsgoPath } from "../dts-paths.js";
import * as ts from "../ts7/adapter.js";
+import { isBundledShimForwarder } from "../package-shims.js";
import { erasedTypeOnlyImport } from "../type-only-imports.js";
import { literalValues } from "../literal-values.js";
import { discriminantOwners, literalUnionArm, type UnionLiteral } from "../union-discriminants.js";
@@ -255,6 +256,7 @@ import {
requireSpecOf,
resolveImport,
workspacePackageOfPath,
+ type CycleEdge,
} from "../program.js";
import {
containsRecord,
@@ -807,6 +809,8 @@ export interface LowerOptions {
startupCrash?: StartupCrash | null;
/** LoadResult.inlineRequireCycleCalls. */
inlineRequireCycleCalls?: ReadonlySet | undefined;
+ /** LoadResult.moduleEdges. */
+ moduleEdges?: ReadonlyMap | undefined;
/** LIBRARY mode's reachability roots: the profile-mapped exports of the
* entry module. Executable builds root at the entry's top level alone
* (an unreferenced export dead-strips); a library's exports are called from
@@ -1004,11 +1008,15 @@ export function lowerToIr(
),
);
};
+ // Inline require() calls closing a cycle inside the appended subgraphs
+ // join preflight's set (the same run-time refusal by name).
+ const inlineRequireCycleCalls = new Set(options.inlineRequireCycleCalls ?? []);
+ const graph = { edges: options.moduleEdges, inlineCycleCalls: inlineRequireCycleCalls };
for (let pass = 0; pass < 32; pass++) {
const before = moduleOrder.length;
- appendDynamicImportModules(program, moduleOrder, onExtraCycle);
- appendForkModules(program, moduleOrder, forkTargets, onExtraCycle);
- appendWorkerModules(program, moduleOrder, workerTargets, onExtraCycle);
+ appendDynamicImportModules(program, moduleOrder, onExtraCycle, graph);
+ appendForkModules(program, moduleOrder, forkTargets, onExtraCycle, graph);
+ appendWorkerModules(program, moduleOrder, workerTargets, onExtraCycle, graph);
if (moduleOrder.length === before) break;
if (pass === 31) throw new Error("dynamic import and fork module discovery did not converge");
}
@@ -1023,7 +1031,7 @@ export function lowerToIr(
projectCheckJs: options.projectCheckJs ?? false,
targetPlatform,
startupCrash,
- inlineRequireCycleCalls: options.inlineRequireCycleCalls,
+ inlineRequireCycleCalls,
ffiImports,
libraryCallbacks,
forkTargets,
@@ -1047,7 +1055,7 @@ export function lowerToIr(
projectCheckJs: options.projectCheckJs ?? false,
targetPlatform,
startupCrash,
- inlineRequireCycleCalls: options.inlineRequireCycleCalls,
+ inlineRequireCycleCalls,
ffiImports,
libraryCallbacks,
forkTargets,
@@ -1079,7 +1087,7 @@ export function lowerToIr(
projectCheckJs: options.projectCheckJs ?? false,
targetPlatform,
startupCrash,
- inlineRequireCycleCalls: options.inlineRequireCycleCalls,
+ inlineRequireCycleCalls,
ffiImports,
libraryCallbacks,
forkTargets,
@@ -1123,7 +1131,7 @@ export function lowerToIr(
reachable,
targetPlatform,
startupCrash,
- inlineRequireCycleCalls: options.inlineRequireCycleCalls,
+ inlineRequireCycleCalls,
ffiImports,
libraryCallbacks,
forkTargets,
@@ -1220,7 +1228,7 @@ export function lowerToIr(
reachable,
remainder: true,
alreadyFlushed: resultLowerer.flushedSymbols,
- inlineRequireCycleCalls: options.inlineRequireCycleCalls,
+ inlineRequireCycleCalls,
targetPlatform,
ffiImports,
libraryCallbacks,
@@ -11984,7 +11992,9 @@ export class Lowerer {
readonly isNpmFile = (sf: ts.SourceFile): boolean =>
sf.isDeclarationFile &&
(sf.fileName.includes("/node_modules/") || workspacePackageOfPath(sf.fileName) !== null) &&
- !this.isStdlibFile(sf);
+ !this.isStdlibFile(sf) &&
+ // A bundled copy's forwarder to a package shim types compiled code.
+ !isBundledShimForwarder(sf.fileName);
npmPackageOf(type: ts.Type): string | null {
return npmPackageOf(this, type);
diff --git a/packages/compiler/src/frontend/lowering/static-typeof.ts b/packages/compiler/src/frontend/lowering/static-typeof.ts
new file mode 100644
index 0000000000..54f2fdfa04
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/static-typeof.ts
@@ -0,0 +1,125 @@
+import * as ts from "../ts7/adapter.js";
+import type { Lowerer } from "./lowerer.js";
+
+/** The answer `typeof ` gives without evaluating anything, for
+ * the global-detection idioms (`typeof window < "u"`, `typeof global !==
+ * "undefined"`): a name nothing declares answers "undefined" (Node's
+ * answer for an undeclared global such as window or self, and typeof never
+ * throws for one), and the Node global object and process answer
+ * "object". Null for every other operand. */
+export function staticTypeofAnswer(lowerer: Lowerer, operand: ts.Expression): string | null {
+ let e = operand;
+ while (ts.isParenthesizedExpression(e)) e = e.expression;
+ if (!ts.isIdentifier(e)) return null;
+ if (lowerer.checker.getSymbolAtLocation(e) === undefined) return "undefined";
+ for (const name of ["global", "globalThis", "process"])
+ if (lowerer.isStdlibGlobal(e, name)) return "object";
+ return null;
+}
+
+/** A comparison of a statically answered typeof against a string literal
+ * (`typeof window < "u"`, esbuild's minified undefined test, or
+ * `typeof self !== "undefined"`), folded: the operand read has no effects,
+ * so only the comparison's value remains. Null for other shapes. */
+export function staticTypeofComparison(lowerer: Lowerer, e: ts.BinaryExpression): boolean | null {
+ const unwrap = (x: ts.Expression): ts.Expression => {
+ while (ts.isParenthesizedExpression(x)) x = x.expression;
+ return x;
+ };
+ const left = unwrap(e.left);
+ const right = unwrap(e.right);
+ const literal = (x: ts.Expression): string | null =>
+ ts.isStringLiteral(x) || ts.isNoSubstitutionTemplateLiteral(x) ? x.text : null;
+ let answer: string | null = null;
+ let text: string | null = null;
+ let flipped = false;
+ if (ts.isTypeOfExpression(left) && literal(right) !== null) {
+ answer = staticTypeofAnswer(lowerer, left.expression);
+ text = literal(right);
+ } else if (ts.isTypeOfExpression(right) && literal(left) !== null) {
+ answer = staticTypeofAnswer(lowerer, right.expression);
+ text = literal(left);
+ flipped = true;
+ }
+ if (answer === null || text === null) return null;
+ const [a, b] = flipped ? [text, answer] : [answer, text];
+ switch (e.operatorToken.kind) {
+ case ts.SyntaxKind.EqualsEqualsEqualsToken:
+ case ts.SyntaxKind.EqualsEqualsToken:
+ return a === b;
+ case ts.SyntaxKind.ExclamationEqualsEqualsToken:
+ case ts.SyntaxKind.ExclamationEqualsToken:
+ return a !== b;
+ case ts.SyntaxKind.LessThanToken:
+ return a < b;
+ case ts.SyntaxKind.LessThanEqualsToken:
+ return a <= b;
+ case ts.SyntaxKind.GreaterThanToken:
+ return a > b;
+ case ts.SyntaxKind.GreaterThanEqualsToken:
+ return a >= b;
+ default:
+ return null;
+ }
+}
+
+/** The value of a condition built from statically answered typeof
+ * comparisons, `!`, `&&` and `||` (the global-detection chains), or null
+ * when any operand needs evaluation. */
+export function staticCondition(lowerer: Lowerer, expr: ts.Expression): boolean | null {
+ let e = expr;
+ while (ts.isParenthesizedExpression(e)) e = e.expression;
+ if (ts.isPrefixUnaryExpression(e) && e.operator === ts.SyntaxKind.ExclamationToken) {
+ const inner = staticCondition(lowerer, e.operand);
+ return inner === null ? null : !inner;
+ }
+ if (!ts.isBinaryExpression(e)) return null;
+ const op = e.operatorToken.kind;
+ if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken) {
+ const isAnd = op === ts.SyntaxKind.AmpersandAmpersandToken;
+ const left = staticCondition(lowerer, e.left);
+ if (left === null) return null;
+ if (left !== isAnd) return left;
+ return staticCondition(lowerer, e.right);
+ }
+ return staticTypeofComparison(lowerer, e);
+}
+
+/** The expression a value reduces to once statically decided conditions
+ * select their arms (`(typeof global < "u" ? global : window)` → `global`),
+ * through parentheses. */
+export function staticallySelected(lowerer: Lowerer, expr: ts.Expression): ts.Expression {
+ let e = expr;
+ for (;;) {
+ while (ts.isParenthesizedExpression(e)) e = e.expression;
+ if (!ts.isConditionalExpression(e)) return e;
+ const taken = staticCondition(lowerer, e.condition);
+ if (taken === null) return e;
+ e = taken ? e.whenTrue : e.whenFalse;
+ }
+}
+
+/** True when `stmt` declares a binding that is visible outside it: a `var`
+ * or a function declaration, outside nested functions. Such an arm still
+ * lowers when its condition is decided, for the hoisted binding. */
+export function declaresHoistedBinding(stmt: ts.Statement): boolean {
+ let found = false;
+ const visit = (n: ts.Node): void => {
+ if (found) return;
+ if (ts.isFunctionDeclaration(n)) {
+ found = true;
+ return;
+ }
+ if (ts.isFunctionLike(n) || ts.isClassDeclaration(n) || ts.isClassExpression(n)) return;
+ if (
+ ts.isVariableDeclarationList(n) &&
+ (n.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) === 0
+ ) {
+ found = true;
+ return;
+ }
+ ts.forEachChild(n, visit);
+ };
+ visit(stmt);
+ return found;
+}
diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index a884b39e16..b418ebc204 100644
--- a/packages/compiler/src/frontend/lowering/surfaces.ts
+++ b/packages/compiler/src/frontend/lowering/surfaces.ts
@@ -1,5 +1,11 @@
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
+import { staticCondition, staticallySelected } from "./static-typeof.js";
+
+function unparenthesized(e: ts.Expression): ts.Expression {
+ while (ts.isParenthesizedExpression(e)) e = e.expression;
+ return e;
+}
import { UNSUPPORTED } from "../../diagnostics/diagnostic.js";
import {
BOOL,
@@ -3343,6 +3349,14 @@ export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined):
* builtinClassAliasOf). */
export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): string | null {
if (ts.isParenthesizedExpression(expr)) return stdlibGlobalNameOf(lowerer, expr.expression);
+ // The global-object detection chain (`typeof global < "u" ? global :
+ // typeof window < "u" ? window : {}`): a condition with static typeof
+ // answers selects one arm, and only that arm ever evaluates.
+ if (ts.isConditionalExpression(expr)) {
+ const taken = staticCondition(lowerer, expr.condition);
+ if (taken === null) return null;
+ return stdlibGlobalNameOf(lowerer, taken ? expr.whenTrue : expr.whenFalse);
+ }
if (ts.isIdentifier(expr)) {
const symbol = lowerer.checker.getSymbolAtLocation(expr);
// `globalThis` itself: a reserved intrinsic — tsc rejects user
@@ -3393,7 +3407,14 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin
}
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null;
- const symbol = lowerer.checker.getSymbolAtLocation(expr.name);
+ // Through a detection chain the checker types the receiver as the
+ // union of every arm; the selected arm (the global object) owns the
+ // member.
+ const selected = staticallySelected(lowerer, expr.expression);
+ const symbol =
+ selected === expr.expression || ts.isIdentifier(unparenthesized(expr.expression))
+ ? lowerer.checker.getSymbolAtLocation(expr.name)
+ : lowerer.checker.getPropertyOfType(lowerer.typeOf(selected), expr.name.text);
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
return ts.symbolName(symbol) === "global" ? "globalThis" : ts.symbolName(symbol);
}
diff --git a/packages/compiler/src/frontend/npm-static-builtin-aliases.ts b/packages/compiler/src/frontend/npm-static-builtin-aliases.ts
new file mode 100644
index 0000000000..f1e3d53d3e
--- /dev/null
+++ b/packages/compiler/src/frontend/npm-static-builtin-aliases.ts
@@ -0,0 +1,68 @@
+/* Requires of polyfill packages that ARE a Node builtin on Node 24
+ * (builtin-package-aliases.ts) compile as the builtin expression, at the
+ * require's position. Two spellings reach here:
+ *
+ * - an installed package: `require("isarray")` (or a subpath such as
+ * `require("readable-stream/transform")`) in an opted-in package's file,
+ * matched against the version installed where Node's resolution from
+ * that file would find it;
+ * - a bundled __commonJS chunk whose path key names the package and its
+ * version (npm-static-bundled-chunks.ts): each `require_x()` call.
+ *
+ * For a module whose export carries another object's members
+ * (readable-stream's entry is stream.Readable with Stream's classes copied
+ * onto it), the replacement depends on how the result is used: member reads
+ * of the carried names read the builtin module's members, value uses read
+ * the class itself, and a mixture keeps the package's own JavaScript. */
+import * as ts from "./ts7/syntax.js";
+import { tokenStartOfNode } from "./ts7/node-functions.js";
+import {
+ aliasReplacement,
+ applyEdits,
+ countDeclarations,
+ unparen,
+ type Edit,
+} from "./npm-static-bundled-chunks.js";
+import { BUILTIN_ALIAS_PACKAGES, type BuiltinAlias } from "./builtin-package-aliases.js";
+import { packageNameOfSpecifier } from "./workspace-registry.js";
+
+/** True when the text may require an aliased package (a cheap probe). */
+export function mayRequireBuiltinAlias(source: string): boolean {
+ if (!source.includes("require(")) return false;
+ for (const name of BUILTIN_ALIAS_PACKAGES) if (source.includes(name)) return true;
+ return false;
+}
+
+/** The module text with each `require("[/subpath]")`
+ * respelled as its builtin, or null when none is. `aliasFor` answers the
+ * alias for a specifier as resolved from this file. */
+export function rewriteBuiltinAliasRequires(
+ sf: ts.SourceFile,
+ aliasFor: (specifier: string) => BuiltinAlias | null,
+): string | null {
+ const edits: Edit[] = [];
+ const declared = countDeclarations(sf, "require") > 0;
+ if (declared) return null;
+ ts.walkPreorder(sf, (n) => {
+ if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode)
+ return "skip";
+ if (
+ !ts.isCallExpression(n) ||
+ !ts.isIdentifier(n.expression) ||
+ n.expression.text !== "require" ||
+ n.arguments.nodes.length !== 1
+ )
+ return undefined;
+ const arg = unparen(n.arguments.nodes[0]!);
+ if (!ts.isStringLiteral(arg) && !ts.isNoSubstitutionTemplateLiteral(arg)) return undefined;
+ const spec = arg.text;
+ if (!BUILTIN_ALIAS_PACKAGES.has(packageNameOfSpecifier(spec))) return undefined;
+ const alias = aliasFor(spec);
+ if (alias === null) return undefined;
+ const text = aliasReplacement(n, alias);
+ if (text !== null) edits.push({ start: tokenStartOfNode(n), end: n.end, text });
+ return undefined;
+ });
+ if (edits.length === 0) return null;
+ return applyEdits(sf.text, 0, sf.text.length, edits);
+}
diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts
index e6dfe9a56b..0779db3ced 100644
--- a/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts
+++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts
@@ -316,3 +316,126 @@ console.log(typeof require_stream().PassThrough, require_user().same);
expect(answer.chunks.map((c) => basename(c.path))).toEqual(["index.js.require_user.cjs"]);
expect(answer.text).toContain('typeof require("stream").PassThrough');
});
+
+test("chunks of polyfill packages that are Node builtins become the builtins", () => {
+ const key = (name: string, version: string, file: string) =>
+ `node_modules/.pnpm/${name}@${version}/node_modules/${name}/${file}`;
+ const answer = expectSameBehavior(`${keyedHelper}var require_isarray = __commonJS({
+ "${key("isarray", "1.0.0", "index.js")}"(exports2, module2) {
+ var toString = {}.toString;
+ module2.exports = Array.isArray || function(arr) {
+ return toString.call(arr) == "[object Array]";
+ };
+ }
+});
+var require_inherits = __commonJS({
+ "${key("inherits", "2.0.4", "inherits.js")}"(exports2, module2) {
+ module2.exports = require("util").inherits;
+ }
+});
+var require_old = __commonJS({
+ "${key("isarray", "3.0.0", "index.js")}"(exports2, module2) {
+ module2.exports = function isArray() { return "own"; };
+ }
+});
+var require_user = __commonJS({
+ "lib/user.js"(exports2) {
+ var isArray = require_isarray();
+ var inherits = require_inherits();
+ exports2.same = isArray === Array.isArray && inherits === require("util").inherits;
+ }
+});
+console.log(require_user().same, require_isarray()([1]), require_old()());
+`);
+ expect(answer.chunks.map((c) => basename(c.path))).toEqual([
+ "index.js.require_old.cjs",
+ "index.js.require_user.cjs",
+ ]);
+ expect(answer.text).toContain("Array.isArray([1])");
+ const user = answer.chunks.find((c) => c.path.endsWith("require_user.cjs"))!;
+ expect(user.source).toContain("var isArray = Array.isArray;");
+ expect(user.source).toContain('var inherits = require("util").inherits;');
+});
+
+test("a polyfill chunk keeps its module when the bundle declares the builtin's global", () => {
+ const key =
+ "node_modules/.pnpm/process-nextick-args@2.0.1/node_modules/process-nextick-args/index.js";
+ const answer = expectSameBehavior(`${keyedHelper}var require_pna = __commonJS({
+ "${key}"(exports2, module2) {
+ module2.exports = process;
+ }
+});
+var require_user = __commonJS({
+ "lib/user.js"(exports2) {
+ var process = { local: true };
+ var pna = require_pna();
+ exports2.same = pna === globalThis.process && process.local;
+ }
+});
+console.log(require_user().same);
+`);
+ expect(answer.chunks.map((c) => basename(c.path))).toEqual([
+ "index.js.require_pna.cjs",
+ "index.js.require_user.cjs",
+ ]);
+ const user = answer.chunks.find((c) => c.path.endsWith("require_user.cjs"))!;
+ expect(user.source).not.toContain("var pna = process;");
+});
+
+test("a readable-stream 2.x entry chunk follows its uses", () => {
+ const key = (file: string) =>
+ `node_modules/.pnpm/readable-stream@2.3.8/node_modules/readable-stream/${file}`;
+ const chunk = (name: string, file: string, body: string) =>
+ `var ${name} = __commonJS({\n "${key(file)}"(exports2, module2) {\n ${body}\n }\n});\n`;
+ const result =
+ split(`${keyedHelper}${chunk("require_readable", "readable.js", 'module2.exports = require("stream").Readable;')}${chunk("require_transform", "transform.js", "module2.exports = require_readable().Transform;")}
+var members = require_readable();
+console.log(members.Readable, members.Writable);
+var value = require_readable();
+console.log(value.from);
+var { Duplex } = require_readable();
+console.log(require_transform());
+`);
+ const answer = result as BundledChunkSplit;
+ expect(answer.chunks).toEqual([]);
+ expect(answer.text).toContain('var members = require("stream");');
+ expect(answer.text).toContain('var value = require("stream").Readable;');
+ expect(answer.text).toContain('var { Duplex } = require("stream");');
+ expect(answer.text).toContain('console.log(require("stream").Transform);');
+ // A use the alias cannot answer keeps the package's own module for
+ // every call site of that chunk.
+ const mixed =
+ split(`${keyedHelper}${chunk("require_readable", "readable.js", 'module2.exports = require("stream").Readable;')}
+var members = require_readable();
+console.log(members.Readable);
+var mixed = require_readable();
+console.log(mixed.Readable, mixed.from);
+`) as BundledChunkSplit;
+ expect(mixed.chunks.map((c) => basename(c.path))).toEqual(["index.js.require_readable.cjs"]);
+ expect(mixed.text).toContain('var members = require("./index.js.require_readable.cjs");');
+ expect(mixed.text).toContain('var mixed = require("./index.js.require_readable.cjs");');
+});
+
+test("a bundled copy of a shimmed package's main file requires the package shim", () => {
+ const key = (version: string, file: string) =>
+ `node_modules/.pnpm/fs-extra@${version}/node_modules/fs-extra/${file}`;
+ const result = split(`${keyedHelper}var require_lib = __commonJS({
+ "${key("9.0.0", "lib/index.js")}"(exports2, module2) {
+ module2.exports = { readFile() { return "bundled"; } };
+ }
+});
+var require_old = __commonJS({
+ "${key("7.0.1", "lib/index.js")}"(exports2, module2) {
+ module2.exports = { readFile() { return "old"; } };
+ }
+});
+console.log(require_lib().readFile(), require_old().readFile());
+`) as BundledChunkSplit;
+ const forwarder = result.chunks.find((c) => c.path.endsWith(".shim.d.ts"));
+ expect(forwarder?.path).toBe("/pkg/dist/index.js.require_lib.shim.d.ts");
+ expect(forwarder?.source).toMatch(/export \* from ".*shims\/fs-extra\/index\.ts";/);
+ expect(forwarder?.source).toMatch(/export \{ default \} from ".*shims\/fs-extra\/index\.ts";/);
+ expect(result.text).toContain('require("./index.js.require_lib.shim").readFile()');
+ // fs-extra 7 has no shim: its chunk stays a module.
+ expect(result.chunks.map((c) => basename(c.path))).toContain("index.js.require_old.cjs");
+});
diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts
index b625c456d1..37a74f2bec 100644
--- a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts
+++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts
@@ -25,6 +25,12 @@ import * as ts from "./ts7/syntax.js";
import { helperTokens } from "./helper-tokens.js";
import { canonicalBuiltinModule } from "./builtin-modules.js";
import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js";
+import { bundledShimForwarder, packageShimForBundledFile } from "./package-shims.js";
+import {
+ builtinAliasOf,
+ versionedPackageOfKey,
+ type BuiltinAlias,
+} from "./builtin-package-aliases.js";
export interface BundledChunkSplit {
/** The bundle with chunk declarations blanked and calls respelled. */
@@ -464,6 +470,8 @@ interface Chunk {
body: ts.Block;
path: string;
spec: string;
+ /** The keyed helper form's module key (the bundled file's path). */
+ key: string | null;
}
interface TopLevel {
@@ -584,12 +592,14 @@ export function splitBundledChunks(
)
return { degrade: CHUNK_SHAPE_DEGRADE };
let factory: ts.Node = call.arguments.nodes[0]!;
+ let key: string | null = null;
if (form === "keyed") {
if (!ts.isObjectLiteralExpression(factory) || factory.properties.nodes.length !== 1)
return { degrade: CHUNK_SHAPE_DEGRADE };
factory = factory.properties.nodes[0]!;
if (!ts.isMethodDeclaration(factory) || ts.isComputedPropertyName(factory.name))
return { degrade: CHUNK_SHAPE_DEGRADE };
+ if (ts.isStringLiteral(factory.name)) key = factory.name.text;
} else {
factory = unparen(factory as ts.Expression);
if (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory))
@@ -618,6 +628,7 @@ export function splitBundledChunks(
body: fn.body,
path: filePath.slice(0, filePath.length - base.length) + file,
spec: `./${file}`,
+ key,
});
}
if (chunks.size === 0) return null;
@@ -625,9 +636,12 @@ export function splitBundledChunks(
// Every factory reference is a direct zero-argument call. Calls inside a
// chunk body belong to that chunk's text, the rest to the bundle's.
const callEdits: Edit[] = [];
+ const callNames: string[] = [];
+ const unaliased = new Set();
for (const r of allRefs) {
const chunk = chunks.get(r.text);
if (chunk === undefined) continue;
+ callNames.push(chunk.name);
const call = r.parent;
if (
call === undefined ||
@@ -639,13 +653,30 @@ export function splitBundledChunks(
return {
degrade: `its bundled CommonJS module factory '${r.text}' is used other than as a direct call`,
};
- const forwarded = forwarderSpec(chunk);
+ const alias = builtinAliasOfChunk(chunk);
+ const aliased = alias === null ? null : aliasReplacement(call, alias);
+ if (alias !== null && aliased === null) unaliased.add(chunk.name);
+ const forwarded =
+ forwarderSpec(chunk) ?? (shimOfChunk(chunk) !== null ? shimSpec(chunk) : null);
callEdits.push({
start: tokenStartOfNode(call),
end: call.end,
- text: `require(${JSON.stringify(forwarded ?? chunk.spec)})`,
+ text: aliased ?? `require(${JSON.stringify(forwarded ?? chunk.spec)})`,
});
}
+ // A chunk keeps its module when any call site's use left the alias
+ // undetermined; every call site then requires the module.
+ if (unaliased.size > 0) {
+ for (let i = 0; i < callEdits.length; i++) {
+ const name = callNames[i]!;
+ if (!unaliased.has(name)) continue;
+ const chunk = chunks.get(name)!;
+ callEdits[i] = {
+ ...callEdits[i]!,
+ text: `require(${JSON.stringify(forwarderSpec(chunk) ?? chunk.spec)})`,
+ };
+ }
+ }
for (const name of chunks.keys()) {
if (declaredAnywhere.has(name) && countDeclarations(sf, name) > 1)
return { degrade: CHUNK_SHAPE_DEGRADE };
@@ -655,6 +686,23 @@ export function splitBundledChunks(
const out: BundledChunkSplit["chunks"] = [];
const bundleEdits: Edit[] = [...callEdits];
for (const chunk of chunks.values()) {
+ if (!unaliased.has(chunk.name) && builtinAliasOfChunk(chunk) !== null) {
+ // A chunk of a package that IS a builtin on Node 24 is that builtin.
+ bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" });
+ continue;
+ }
+ const shim = shimOfChunk(chunk);
+ if (shim !== null) {
+ // A bundled copy of a package scriptc implements itself is the
+ // package's shim (package-shims.ts), reached through a forwarder
+ // declaration the checker follows.
+ out.push({
+ path: shimDeclPath(chunk),
+ source: bundledShimForwarder(shimDeclPath(chunk), shim),
+ });
+ bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" });
+ continue;
+ }
if (forwarderSpec(chunk) !== null) {
// A chunk that only re-exports a builtin module is that module.
bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" });
@@ -678,7 +726,7 @@ export function splitBundledChunks(
return { text: applyEdits(source, 0, source.length, bundleEdits), chunks: out };
}
-export function countDeclarations(sf: ts.SourceFile, name: string): number {
+export function countDeclarations(sf: ts.Node, name: string): number {
let n = 0;
ts.walkPreorder(sf, (node) => {
if (
@@ -720,6 +768,132 @@ export function applyEdits(source: string, from: number, to: number, edits: Edit
return text + source.slice(at, to);
}
+/* Use analysis for a require of a builtin-aliased module
+ * (npm-static-builtin-aliases.ts). */
+
+function isAssignmentTarget(node: ts.Node): boolean {
+ const p = node.parent;
+ return (
+ p !== undefined &&
+ ts.isBinaryExpression(p) &&
+ p.left === node &&
+ p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
+ p.operatorToken.kind <= ts.SyntaxKind.LastAssignment
+ );
+}
+
+/** The outermost expression the call's value flows through unchanged
+ * (parentheses). */
+function valueNode(call: ts.Expression): ts.Expression {
+ let e: ts.Expression = call;
+ while (e.parent !== undefined && ts.isParenthesizedExpression(e.parent)) e = e.parent;
+ return e;
+}
+
+type Use = "member" | "value" | "invalid";
+
+/** How one occurrence of the module value is used: a read of a carried
+ * member, any other value use (member reads of other names included, which
+ * the class itself answers), or a write that keeps the package. */
+function useOf(node: ts.Expression, names: ReadonlySet): Use {
+ const p = node.parent;
+ if (p !== undefined && ts.isPropertyAccessExpression(p) && p.expression === node) {
+ if (isAssignmentTarget(p) || p.questionDotToken !== undefined) return "invalid";
+ return names.has(p.name.text) ? "member" : "value";
+ }
+ if (p !== undefined && ts.isElementAccessExpression(p) && p.expression === node) return "invalid";
+ return "value";
+}
+
+function scopeRoot(node: ts.Node): ts.Node {
+ for (let p = node.parent; p !== undefined; p = p.parent) {
+ if (ts.isFunctionLike(p) || ts.isSourceFile(p)) return p;
+ }
+ return node;
+}
+
+/** The text replacing a require of an aliased module, or null when its uses
+ * do not determine one or when the file declares the global the text
+ * starts with (`process`, `Array`): the text would read that binding
+ * instead of the builtin. `require` roots are the require the call site
+ * already uses. */
+export function aliasReplacement(call: ts.CallExpression, alias: BuiltinAlias): string | null {
+ const text = aliasUseReplacement(call, alias);
+ if (text === null) return null;
+ const root = /^[A-Za-z_$][\w$]*/.exec(text)?.[0];
+ if (
+ root !== undefined &&
+ root !== "require" &&
+ countDeclarations(ts.getSourceFileOfNode(call), root) > 0
+ )
+ return null;
+ return text;
+}
+
+function aliasUseReplacement(call: ts.CallExpression, alias: BuiltinAlias): string | null {
+ const members = alias.members;
+ if (members === undefined) return alias.whole;
+ const whole = alias.whole;
+ const node = valueNode(call);
+ const p = node.parent;
+ let uses: Use[];
+ if (p !== undefined && ts.isVariableDeclaration(p) && p.initializer === node) {
+ if (ts.isObjectBindingPattern(p.name)) {
+ const ok = p.name.elements.nodes.every((el) => {
+ if (el.dotDotDotToken !== undefined) return false;
+ const key = el.propertyName ?? el.name;
+ return ts.isIdentifier(key) && members.names.has(key.text);
+ });
+ return ok ? members.object : null;
+ }
+ if (!ts.isIdentifier(p.name)) return null;
+ const name = p.name.text;
+ const root = scopeRoot(p);
+ if (countDeclarations(root as ts.SourceFile, name) !== 1) return null;
+ const list = p.parent;
+ // `var` hoisting: a read before the declaration sees undefined.
+ if (list === undefined || !ts.isVariableDeclarationList(list)) return null;
+ uses = [];
+ for (const ref of references(root)) {
+ if (ref.text !== name) continue;
+ if (isWritten(ref) || ref.pos < p.end) return null;
+ uses.push(useOf(ref, members.names));
+ }
+ } else {
+ uses = [useOf(node, members.names)];
+ }
+ if (uses.includes("invalid")) return null;
+ if (uses.length > 0 && uses.every((u) => u === "member")) return members.object;
+ if (uses.every((u) => u === "value")) return whole;
+ return null;
+}
+
+/** The builtin a chunk's module is on Node 24, when its key names an
+ * aliased package at a checked version (builtin-package-aliases.ts). */
+function builtinAliasOfChunk(chunk: Chunk): BuiltinAlias | null {
+ if (chunk.key === null) return null;
+ const pkg = versionedPackageOfKey(chunk.key);
+ return pkg === null ? null : builtinAliasOf(pkg.name, pkg.version, pkg.subpath);
+}
+
+/** The shim module standing in for a chunk that is a bundled copy of a
+ * shimmed package's main file at a supported version, or null. */
+function shimOfChunk(chunk: Chunk): string | null {
+ if (chunk.key === null) return null;
+ const pkg = versionedPackageOfKey(chunk.key);
+ return pkg === null ? null : packageShimForBundledFile(pkg.name, pkg.version, pkg.subpath);
+}
+
+/** The forwarder declaration beside the bundle for a shimmed chunk, and
+ * the specifier its call sites require. */
+function shimDeclPath(chunk: Chunk): string {
+ return chunk.path.replace(/\.cjs$/, ".shim.d.ts");
+}
+
+function shimSpec(chunk: Chunk): string {
+ return chunk.spec.replace(/\.cjs$/, ".shim");
+}
+
/** The builtin module a chunk forwards, or null: a body that is exactly
* `module.exports = require("")` (readable-stream's
* internal/streams/stream.js). Requiring the chunk is then requiring the
diff --git a/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts b/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts
index 381538087f..c5b5568214 100644
--- a/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts
+++ b/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts
@@ -228,3 +228,140 @@ test("files without __esm initializers and ES-module bundles are left alone", ()
lower(`${helpers}var init_a = __esm({ "a.ts"() {} });\ninit_a();\nexport {};\n`),
).toBeNull();
});
+
+test("a class an initializer defines into a module binding becomes a class declaration", () => {
+ const lowered = expectSameBehavior(`"use strict";
+${helpers}var import_stream, Shout, Loud, Plain;
+var init_shout = __esm({
+ "src/shout.ts"() {
+ "use strict";
+ import_stream = require("stream");
+ console.log("shout init");
+ Shout = class extends import_stream.Transform {
+ _transform(chunk, encoding, callback) {
+ callback(null, String(chunk).toUpperCase());
+ }
+ };
+ Loud = class Loud extends Shout {
+ static of() { return new Loud(); }
+ }, Plain = class {
+ label() { return "plain"; }
+ };
+ console.log("after", typeof Shout, Loud.name, new Plain().label());
+ }
+});
+function main() {
+ init_shout();
+ const s = Loud.of();
+ s.on("data", (c) => console.log("data", String(c)));
+ s.end("hi");
+}
+console.log("start");
+main();
+`);
+ expect(lowered).toContain('class Shout extends require("stream").Transform');
+ expect(lowered).toContain("class Loud extends Shout");
+ expect(lowered).toContain("class Plain ");
+ // The bindings' declarators are gone with the interop binding's.
+ expect(lowered).not.toMatch(/var [^;=]*\b(?:import_stream|Shout|Loud|Plain)\b/);
+});
+
+test("classes whose definition or binding is observable stay assignments", () => {
+ const lowered = expectSameBehavior(`${helpers}var A, B, C, D;
+var init_m = __esm({
+ "src/m.ts"() {
+ A = class { static made = console.log("static field runs at definition") };
+ B = class extends globalThis.Array {};
+ console.log(typeof D);
+ D = class {};
+ C = class {};
+ }
+});
+console.log(typeof C);
+init_m();
+console.log(typeof A, typeof B, typeof C, typeof D);
+`);
+ expect(lowered).toContain("A = class");
+ expect(lowered).toContain("B = class");
+ expect(lowered).toContain("C = class");
+ expect(lowered).toContain("D = class");
+});
+
+test("an interop binding a new expression constructs through stays the callee", () => {
+ const lowered = expectSameBehavior(`${helpers}function make() {
+ return new import_events.EventEmitter();
+}
+var import_events;
+var init_m = __esm({
+ "src/m.ts"() {
+ import_events = require("events");
+ }
+});
+init_m();
+console.log(typeof make().on);
+`);
+ expect(lowered).toContain('new (require("events")).EventEmitter()');
+});
+
+test("a hoisted class may extend a global constructor, past esbuild's export annotation", () => {
+ const lowered = expectSameBehavior(`${helpers}var AppError;
+var init_errors = __esm({
+ "src/errors.ts"() {
+ AppError = class extends Error {
+ code;
+ constructor(message, code) {
+ super(message);
+ this.code = code;
+ }
+ };
+ }
+});
+function fail() {
+ init_errors();
+ throw new AppError("failed", "E1");
+}
+function main() {
+ try {
+ fail();
+ } catch (err) {
+ console.log(err instanceof AppError, err instanceof Error, err.code, err.message);
+ }
+}
+main();
+0 && (module.exports = { AppError });
+`);
+ expect(lowered).toContain("class AppError extends Error");
+});
+
+test("an initializer declared last in a shared var statement still hoists its classes", () => {
+ const lowered = expectSameBehavior(`"use strict";
+${helpers}var BuildError, hints, init_errors = __esm({
+ "src/errors.ts"() {
+ "use strict";
+ BuildError = class extends Error {
+ constructor(code) {
+ super("failed: " + code);
+ this.code = code;
+ }
+ }, hints = { retry: "try again" };
+ }
+});
+function run() {
+ init_errors();
+ try {
+ throw new BuildError("E2");
+ } catch (err) {
+ console.log(err instanceof BuildError, err.code, err.message, hints.retry);
+ }
+}
+run();
+`);
+ expect(lowered).toContain("class BuildError extends Error");
+});
+
+test("a minified initializer that ends with its class still hoists it", () => {
+ const lowered = expectSameBehavior(
+ `${helpers}var Counter,init_counter=__esm({"src/counter.ts"(){console.log("counter init"),Counter=class{n=0;next(){return++this.n}}}});function run(){init_counter();const c=new Counter;c.next();console.log(c.next())}run();`,
+ );
+ expect(lowered).toContain("class Counter{");
+});
diff --git a/packages/compiler/src/frontend/npm-static-bundled-esm.ts b/packages/compiler/src/frontend/npm-static-bundled-esm.ts
index ac572000d7..83cb1c1e11 100644
--- a/packages/compiler/src/frontend/npm-static-bundled-esm.ts
+++ b/packages/compiler/src/frontend/npm-static-bundled-esm.ts
@@ -29,6 +29,7 @@
import * as ts from "./ts7/syntax.js";
import { bareRequireSpecOf } from "./cjs-syntax.js";
import { helperTokens } from "./helper-tokens.js";
+import { canonicalBuiltinModule } from "./builtin-modules.js";
import {
applyEdits,
countDeclarations,
@@ -37,6 +38,7 @@ import {
helperBase,
isWritten,
references,
+ strictPrologue,
unparen,
type Edit,
} from "./npm-static-bundled-chunks.js";
@@ -46,6 +48,19 @@ export interface EsmInitHost {
requireTargetEsModuleStamped(fromFile: string, specifier: string): boolean;
}
+/** A top-level binding of the bundle: its statement and declaration. */
+interface Top {
+ stmt: ts.Statement;
+ node: ts.VariableDeclaration | ts.FunctionDeclaration;
+}
+
+/** A lazy initializer: its declaration's `__esm(...)` call and body. */
+interface InitBody {
+ name: string;
+ call: ts.CallExpression;
+ body: ts.Block;
+}
+
const ESM_SHAPE_DEGRADE =
"its shipped JS initializes bundled ES modules through the __esm helper in a shape the " +
"recognizer cannot verify";
@@ -134,6 +149,17 @@ function removeDeclarators(sf: ts.SourceFile, dead: ReadonlySet();
const duplicate = new Set();
for (const stmt of sf.statements.nodes) {
@@ -311,12 +333,7 @@ export function lowerBundledEsmInits(
}
// The initializers: `NAME = HELPER(factory)` declarators.
- interface Init {
- name: string;
- call: ts.CallExpression;
- body: ts.Block;
- }
- const inits = new Map();
+ const inits = new Map();
for (const [name, t] of top) {
if (!ts.isVariableDeclaration(t.node) || t.node.initializer === undefined) continue;
const call = unparen(t.node.initializer);
@@ -383,6 +400,7 @@ export function lowerBundledEsmInits(
else list.push(r);
}
const deadDeclarators = new Set();
+ const interopSpecs = new Map();
for (const init of inits.values()) {
for (const stmt of init.body.statements.nodes) {
if (!ts.isExpressionStatement(stmt)) continue;
@@ -444,14 +462,27 @@ export function lowerBundledEsmInits(
}
if (defaultIsModule && access.name.text === "default")
uses.push({ start: tokenStartOfNode(access), end: access.end, text: `(${req})` });
- else uses.push({ start: tokenStartOfNode(r), end: r.end, text: req });
+ // `new import_x.C()` must stay `new (require("x")).C()`: a bare
+ // `new require("x").C()` constructs the require call itself.
+ else
+ uses.push({
+ start: tokenStartOfNode(r),
+ end: r.end,
+ text: inNewCallee(r) ? `(${req})` : req,
+ });
}
if (!ok) continue;
edits.push({ start: tokenStartOfNode(el), end: el.end, text: req }, ...uses);
deadDeclarators.add(decl);
+ interopSpecs.set(name, spec);
}
}
}
+ const hoisted = hoistInitClasses(sf, inits, top, duplicate, writes, refsByName, interopSpecs);
+ for (const h of hoisted.values()) {
+ edits.push(...h.edits);
+ for (const d of h.declarators) deadDeclarators.add(d);
+ }
/* Namespaces of lazily initialized modules read through members. */
let mainTarget: string | null = null;
@@ -813,14 +844,231 @@ export function lowerBundledEsmInits(
{
start: tokenStartOfNode(call),
end: tokenStartOfNode(body) + 1,
- text: "(() => { let __scriptc_done = false; function __scriptc_init() {",
+ text: `(() => { let __scriptc_done = false; function __scriptc_init() {`,
},
{
start: body.end - 1,
end: call.end,
- text: "} return function () { if (__scriptc_done) return; __scriptc_done = true; __scriptc_init(); }; })()",
+ // A body split around hoisted classes ends in its last tail function.
+ text: hoisted.has(init.name) ? "}" : `} ${RUN_ONCE_TAIL}`,
},
);
}
return applyEdits(source, 0, source.length, edits);
}
+
+/** Global constructors a hoisted class may extend: reading one runs no
+ * code. */
+const GLOBAL_BASES: ReadonlySet = new Set([
+ "Error",
+ "TypeError",
+ "RangeError",
+ "SyntaxError",
+ "ReferenceError",
+ "EvalError",
+ "URIError",
+ "AggregateError",
+ "Array",
+ "Map",
+ "Set",
+ "Promise",
+ "Object",
+]);
+
+/** The run-once closure's closing text after the initializer function. */
+const RUN_ONCE_TAIL =
+ "return function () { if (__scriptc_done) return; __scriptc_done = true; __scriptc_init(); }; })()";
+
+/* Classes an initializer defines into a module binding,
+ *
+ * var Multi;
+ * var init_m = __esm({ "src/m.ts"() { Multi = class extends import_stream.Readable { … }; } });
+ *
+ * become class declarations in the bundle's scope, at the same text
+ * position: the initializer's body splits around each class into run-once
+ * pieces that call one another in order (`… __scriptc_rest_init_m_1(); }
+ * return …; })(); class Multi extends … { … } function
+ * __scriptc_rest_init_m_1() { … }`), so the class's own text stays where
+ * it was and only its binding changes. A stored class value with a
+ * runtime-provided base has no static representation, while a declared
+ * class does.
+ *
+ * Exact where defining the class earlier is unobservable: its heritage is
+ * absent, a builtin module member, or a class hoisted before it; it has no
+ * static field initializers, static blocks or computed member names, so
+ * evaluating the definition runs no code; the binding is declared once,
+ * assigned only there, and read only inside functions (which esbuild's
+ * code reaches only after the initializer ran, the assumption the interop
+ * respelling makes too); and the body declares nothing, so its statements
+ * can move between functions. */
+function hoistInitClasses(
+ sf: ts.SourceFile,
+ inits: ReadonlyMap,
+ top: ReadonlyMap,
+ duplicate: ReadonlySet,
+ writes: ReadonlyMap,
+ refsByName: ReadonlyMap,
+ interopSpecs: ReadonlyMap,
+): Map {
+ const out = new Map();
+ const hoistedNames = new Set();
+ /** esbuild's `0 && (module.exports = { … })` annotation for Node's
+ * CommonJS lexer: code that never runs. */
+ const inDeadAnnotation = (n: ts.Node): boolean => {
+ for (let child: ts.Node = n, p = n.parent; p !== undefined; child = p, p = p.parent) {
+ if (
+ ts.isBinaryExpression(p) &&
+ p.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken &&
+ p.right === child &&
+ ts.isNumericLiteral(unparen(p.left)) &&
+ (unparen(p.left) as ts.NumericLiteral).text === "0"
+ )
+ return true;
+ }
+ return false;
+ };
+ const insideFunction = (n: ts.Node): boolean => {
+ for (let p = n.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent)
+ if (ts.isFunctionLike(p)) return true;
+ return false;
+ };
+ const heritageOk = (cls: ts.ClassExpression): boolean => {
+ const clause = cls.heritageClauses?.nodes.find((c) => c.token === ts.SyntaxKind.ExtendsKeyword);
+ if (clause === undefined) return true;
+ const type = clause.types.nodes[0];
+ if (type === undefined || clause.types.nodes.length !== 1 || type.typeArguments !== undefined)
+ return false;
+ let root: ts.Expression = type.expression;
+ while (ts.isPropertyAccessExpression(root) && !root.questionDotToken) root = root.expression;
+ if (ts.isIdentifier(root)) {
+ if (root === type.expression)
+ return (
+ hoistedNames.has(root.text) ||
+ // A standard global constructor the bundle never rebinds
+ // (`class extends Error`, an error base class).
+ (GLOBAL_BASES.has(root.text) && countDeclarations(sf, root.text) === 0)
+ );
+ const spec = interopSpecs.get(root.text);
+ return spec !== undefined && canonicalBuiltinModule(spec) !== null;
+ }
+ const spec = bareRequireSpecOf(root);
+ return spec !== null && canonicalBuiltinModule(spec) !== null && root !== type.expression;
+ };
+ const definitionInert = (cls: ts.ClassExpression): boolean =>
+ (cls.modifiers?.nodes.length ?? 0) === 0 &&
+ cls.members.nodes.every((m) => {
+ if (ts.isClassStaticBlockDeclaration(m)) return false;
+ if (m.name !== undefined && ts.isComputedPropertyName(m.name)) return false;
+ if (m.modifiers?.nodes.some((x) => x.kind === ts.SyntaxKind.Decorator)) return false;
+ const isStatic = m.modifiers?.nodes.some((x) => x.kind === ts.SyntaxKind.StaticKeyword);
+ return !(isStatic && ts.isPropertyDeclaration(m));
+ });
+
+ for (const init of inits.values()) {
+ const stmts = init.body.statements.nodes;
+ if (stmts.some((st) => !ts.isExpressionStatement(st) && !ts.isEmptyStatement(st))) continue;
+ // The initializer's declarator is its statement's last one.
+ const declarator = init.call.parent;
+ const list = declarator?.parent;
+ if (
+ declarator === undefined ||
+ !ts.isVariableDeclaration(declarator) ||
+ list === undefined ||
+ !ts.isVariableDeclarationList(list) ||
+ // The initializer ends its statement: the split pieces follow it
+ // (`var A, B, init_x = __esm(…)` keeps A and B declared before).
+ list.declarations.nodes[list.declarations.nodes.length - 1] !== declarator ||
+ unparen(declarator.initializer!) !== init.call
+ )
+ continue;
+ const strict = strictPrologue(stmts) ? '"use strict"; ' : "";
+ const edits: Edit[] = [];
+ const declarators: ts.VariableDeclaration[] = [];
+ let piece = 0;
+ for (const stmt of stmts) {
+ if (!ts.isExpressionStatement(stmt)) continue;
+ const els = commaElements(stmt.expression);
+ const hoistedEls = new Set();
+ for (let i = 0; i < els.length; i++) {
+ const el = els[i]!;
+ if (
+ !ts.isBinaryExpression(el) ||
+ el.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
+ !ts.isIdentifier(el.left) ||
+ !ts.isClassExpression(el.right)
+ )
+ continue;
+ const name = el.left.text;
+ const cls = el.right;
+ const target = el.left;
+ const decl = top.get(name)?.node;
+ if (
+ decl === undefined ||
+ !ts.isVariableDeclaration(decl) ||
+ decl.initializer !== undefined ||
+ duplicate.has(name) ||
+ writes.get(name) !== 1 ||
+ (cls.name !== undefined && cls.name.text !== name) ||
+ // The binding's declarator (and the class's own name, if spelled).
+ countDeclarations(sf, name) !== (cls.name === undefined ? 1 : 2) ||
+ !definitionInert(cls) ||
+ !heritageOk(cls) ||
+ !(refsByName.get(name) ?? []).every(
+ (r) =>
+ r === target ||
+ // The class's own methods name the class itself.
+ (tokenStartOfNode(r) >= tokenStartOfNode(cls) && r.end <= cls.end) ||
+ inDeadAnnotation(r) ||
+ (insideFunction(r) &&
+ // The initializer's own earlier code reads the unassigned binding.
+ !(
+ tokenStartOfNode(r) >= tokenStartOfNode(init.body) && tokenStartOfNode(r) < el.end
+ )),
+ )
+ )
+ continue;
+ piece++;
+ const rest = `__scriptc_rest_${init.name}_${piece}`;
+ const close = piece === 1 ? `} ${RUN_ONCE_TAIL};` : "}";
+ // `a, X = class …, b`: the elements around the class end and begin
+ // the neighboring pieces.
+ // After a hoisted neighbor the separating comma is already gone.
+ const fresh = i === 0 || hoistedEls.has(els[i - 1]!);
+ const from =
+ i === 0 ? tokenStartOfNode(stmt) : fresh ? tokenStartOfNode(el) : els[i - 1]!.end;
+ const sep = fresh ? "" : ";";
+ hoistedEls.add(el);
+ edits.push({
+ start: from,
+ end: tokenStartOfNode(cls),
+ text: `${sep} ${rest}(); ${close} `,
+ });
+ const next = els[i + 1];
+ // The class's own closing brace carries the next piece's opener: an
+ // empty insertion at its end would share its position with the
+ // initializer body's closing edit in minified text (`…}}})`).
+ edits.push(
+ next === undefined
+ ? {
+ start: cls.end - 1,
+ end: cls.end,
+ text: `} function ${rest}() { ${strict}`,
+ }
+ : {
+ start: cls.end,
+ end: tokenStartOfNode(next),
+ text: ` function ${rest}() { ${strict}`,
+ },
+ );
+ if (cls.name === undefined) {
+ const keyword = tokenStartOfNode(cls);
+ edits.push({ start: keyword, end: keyword + "class".length, text: `class ${name}` });
+ }
+ declarators.push(decl);
+ hoistedNames.add(name);
+ }
+ }
+ if (piece > 0) out.set(init.name, { edits, declarators });
+ }
+ return out;
+}
diff --git a/packages/compiler/src/frontend/npm-static-enum-iife.test.ts b/packages/compiler/src/frontend/npm-static-enum-iife.test.ts
new file mode 100644
index 0000000000..b27c53345c
--- /dev/null
+++ b/packages/compiler/src/frontend/npm-static-enum-iife.test.ts
@@ -0,0 +1,135 @@
+import { spawnSync } from "node:child_process";
+import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterAll, expect, test } from "vitest";
+import { nodeFrontendServices } from "./services-node.js";
+
+const dirs: string[] = [];
+afterAll(() => {
+ for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
+});
+
+function rewrite(source: string): string | null {
+ return nodeFrontendServices().enumIifes(source, "/pkg/index.js");
+}
+
+function run(file: string): string {
+ const result = spawnSync(process.execPath, [file], { encoding: "utf8" });
+ expect(result.status).toBe(0);
+ return result.stdout;
+}
+
+const probe = `
+for (const name of Object.keys(module.exports)) {
+ const v = module.exports[name];
+ console.log(name, JSON.stringify(v), Object.keys(v ?? {}).join(","));
+}
+`;
+
+/** The rewritten module exports what the original exports, keys in the
+ * same order. */
+function expectSameExports(source: string): string {
+ const result = rewrite(source);
+ expect(typeof result).toBe("string");
+ const dir = mkdtempSync(join(tmpdir(), "scriptc-enum-iife-"));
+ dirs.push(dir);
+ const original = join(dir, "original.cjs");
+ const rewritten = join(dir, "rewritten.cjs");
+ writeFileSync(original, source + probe);
+ writeFileSync(rewritten, result! + probe);
+ expect(run(rewritten)).toBe(run(original));
+ // Line positions survive the respelling.
+ expect(result!.split("\n").length).toBe(source.split("\n").length);
+ return result!;
+}
+
+test("a TypeScript 5 exported string enum becomes its object literal", () => {
+ const out = expectSameExports(`"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.Kind = void 0;
+var Kind;
+(function (Kind) {
+ Kind["Identifier"] = "Identifier";
+ Kind["Literal"] = "Literal";
+})(Kind || (exports.Kind = Kind = {}));
+`);
+ expect(out).toContain(`var Kind = { "Identifier": "Identifier", "Literal": "Literal" };`);
+ expect(out).toContain("exports.Kind = Kind;");
+});
+
+test("numeric enums keep their reverse mapping and Node's key order", () => {
+ const out = expectSameExports(`"use strict";
+var Level;
+(function (Level) {
+ Level[Level["High"] = 2] = "High";
+ Level[Level["Low"] = 0] = "Low";
+ Level[Level["Below"] = -1] = "Below";
+ Level[Level["Also"] = 2] = "Also";
+})(Level = exports.Level || (exports.Level = {}));
+`);
+ expect(out).toContain(
+ `var Level = { 0: "Low", 2: "Also", "High": 2, "Low": 0, "Below": -1, "-1": "Below", "Also": 2 };`,
+ );
+});
+
+test("an ES module's exported enum keeps its export", () => {
+ const source = `export var Kind;
+(function (Kind) {
+ Kind["Identifier"] = "Identifier";
+ Kind[Kind["Zero"] = 0] = "Zero";
+})(Kind || (Kind = {}));
+`;
+ const out = rewrite(source);
+ expect(out).toContain(`export var Kind = { 0: "Zero", "Identifier": "Identifier", "Zero": 0 };`);
+ const dir = mkdtempSync(join(tmpdir(), "scriptc-enum-iife-"));
+ dirs.push(dir);
+ writeFileSync(join(dir, "original.mjs"), source);
+ writeFileSync(join(dir, "rewritten.mjs"), out!);
+ const reader = (file: string) =>
+ `import { Kind } from "./${file}"; console.log(JSON.stringify(Kind), Object.keys(Kind).join(","));`;
+ writeFileSync(join(dir, "read-original.mjs"), reader("original.mjs"));
+ writeFileSync(join(dir, "read-rewritten.mjs"), reader("rewritten.mjs"));
+ expect(run(join(dir, "read-rewritten.mjs"))).toBe(run(join(dir, "read-original.mjs")));
+ expect(out!.split("\n").length).toBe(source.split("\n").length);
+});
+
+test("an enum the module does not export becomes a local object", () => {
+ const out = expectSameExports(`"use strict";
+var Mode;
+(function (Mode) {
+ Mode.Fast = "fast";
+})(Mode || (Mode = {}));
+exports.mode = Mode.Fast;
+`);
+ expect(out).toContain(`var Mode = { "Fast": "fast" };`);
+});
+
+test("merged, computed and otherwise written enums keep their text", () => {
+ // A second declaration merges into the first.
+ expect(
+ rewrite(`var E;
+(function (E) { E["A"] = "a"; })(E || (E = {}));
+(function (E) { E["B"] = "b"; })(E || (E = {}));
+`),
+ ).toBeNull();
+ // A member referencing another member.
+ expect(
+ rewrite(`var F;
+(function (F) { F[F["A"] = 1] = "A"; F[F["B"] = F.A << 1] = "B"; })(F || (exports.F = F = {}));
+`),
+ ).toBeNull();
+ // A statement other than a member write.
+ expect(
+ rewrite(`var G;
+(function (G) { G["A"] = "a"; console.log("side effect"); })(G || (exports.G = G = {}));
+`),
+ ).toBeNull();
+ // The variable is reassigned later.
+ expect(
+ rewrite(`var H;
+(function (H) { H["A"] = "a"; })(H || (exports.H = H = {}));
+H = null;
+`),
+ ).toBeNull();
+});
diff --git a/packages/compiler/src/frontend/npm-static-enum-iife.ts b/packages/compiler/src/frontend/npm-static-enum-iife.ts
new file mode 100644
index 0000000000..e1b9220a91
--- /dev/null
+++ b/packages/compiler/src/frontend/npm-static-enum-iife.ts
@@ -0,0 +1,285 @@
+/* TypeScript's enum emit in a compiled package: a bare `var` filled by an
+ * immediately invoked function that writes each member onto its argument.
+ *
+ * var Kind;
+ * (function (Kind) {
+ * Kind["Identifier"] = "Identifier";
+ * Kind[Kind["Zero"] = 0] = "Zero";
+ * })(Kind || (exports.Kind = Kind = {}));
+ *
+ * Inference types the variable from the empty object the argument creates,
+ * so every member read lands on `{}`. When every member value is a literal,
+ * the enum is that object literal: the declaration becomes
+ * `var Kind = { ... };` and the call becomes the export assignment it
+ * performs. Keys keep Node's own-property order (integer keys ascending,
+ * then the rest in insertion order, a later write replacing an earlier
+ * value in place), so enumeration observes the same object.
+ *
+ * Accepted argument spellings: `Kind || (exports.Kind = Kind = {})`
+ * (TypeScript 5), `Kind = exports.Kind || (exports.Kind = {})` (earlier
+ * releases) and `Kind || (Kind = {})` (an enum the module does not
+ * export). Merged declarations, computed or referencing members, and any
+ * other write to the variable keep the original text. */
+import * as ts from "./ts7/syntax.js";
+import { tokenStartOfNode } from "./ts7/node-functions.js";
+import { applyEdits, unparen, type Edit } from "./npm-static-bundled-chunks.js";
+
+/** True when the text may hold an enum IIFE (a cheap probe). */
+export function mayHoldEnumIife(source: string): boolean {
+ return source.includes("(function (") && source.includes("|| (");
+}
+
+/** The module text with each literal-valued enum IIFE respelled as an
+ * object literal, or null when there is none. */
+export function rewriteEnumIifes(sf: ts.SourceFile): string | null {
+ const stmts = sf.statements.nodes;
+ const edits: Edit[] = [];
+ for (let i = 1; i < stmts.length; i++) {
+ const decl = bareVarName(stmts[i - 1]!);
+ if (decl === null) continue;
+ const iife = enumIife(stmts[i]!, decl);
+ if (iife === null || writtenElsewhere(sf, decl, stmts[i]!)) continue;
+ // Modifiers before `var` stay: `export var Kind;` (an ES module's
+ // enum) keeps exporting the binding.
+ const declaration = stmts[i - 1] as ts.VariableStatement;
+ const modifiers = sf.text.slice(
+ tokenStartOfNode(declaration),
+ tokenStartOfNode(declaration.declarationList),
+ );
+ edits.push({
+ start: tokenStartOfNode(declaration),
+ end: declaration.end,
+ text: `${modifiers}var ${decl} = ${objectLiteral(iife.members)};`,
+ });
+ edits.push({
+ start: tokenStartOfNode(stmts[i]!),
+ end: stmts[i]!.end,
+ text: iife.exportName === null ? "" : `exports.${iife.exportName} = ${decl};`,
+ });
+ }
+ if (edits.length === 0) return null;
+ return applyEdits(sf.text, 0, sf.text.length, edits);
+}
+
+/** `var Name;`: one declarator, no initializer. */
+function bareVarName(stmt: ts.Statement): string | null {
+ if (!ts.isVariableStatement(stmt)) return null;
+ const list = stmt.declarationList;
+ if ((list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0) return null;
+ const decls = list.declarations.nodes;
+ if (decls.length !== 1) return null;
+ const d = decls[0]!;
+ if (d.initializer !== undefined || !ts.isIdentifier(d.name)) return null;
+ return d.name.text;
+}
+
+type Members = Map;
+
+function enumIife(
+ stmt: ts.Statement,
+ name: string,
+): { members: Members; exportName: string | null } | null {
+ if (!ts.isExpressionStatement(stmt)) return null;
+ const call = unparen(stmt.expression);
+ if (!ts.isCallExpression(call) || call.arguments.nodes.length !== 1) return null;
+ const fn = unparen(call.expression);
+ if (!ts.isFunctionExpression(fn) || fn.name !== undefined || fn.asteriskToken !== undefined)
+ return null;
+ if (fn.modifiers !== undefined && fn.modifiers.nodes.length > 0) return null;
+ const params = fn.parameters.nodes;
+ if (params.length !== 1) return null;
+ const param = params[0]!;
+ if (!ts.isIdentifier(param.name) || param.initializer !== undefined) return null;
+ if (param.dotDotDotToken !== undefined) return null;
+ const exportName = enumArgument(call.arguments.nodes[0]!, name);
+ if (exportName === undefined) return null;
+ const members = enumMembers(fn.body.statements.nodes, param.name.text);
+ return members === null ? null : { members, exportName };
+}
+
+/** The export name the argument assigns (null: not exported), or
+ * undefined when it is not one of the enum spellings. */
+function enumArgument(arg: ts.Expression, name: string): string | null | undefined {
+ const e = unparen(arg);
+ if (!ts.isBinaryExpression(e)) return undefined;
+ // `Kind = exports.Kind || (exports.Kind = {})`
+ if (e.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
+ if (!isName(e.left, name)) return undefined;
+ const or = unparen(e.right);
+ if (!ts.isBinaryExpression(or) || or.operatorToken.kind !== ts.SyntaxKind.BarBarToken)
+ return undefined;
+ const exported = exportsMember(or.left);
+ if (exported === null) return undefined;
+ const init = unparen(or.right);
+ if (
+ !ts.isBinaryExpression(init) ||
+ init.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
+ exportsMember(init.left) !== exported ||
+ !isEmptyObject(init.right)
+ )
+ return undefined;
+ return exported;
+ }
+ if (e.operatorToken.kind !== ts.SyntaxKind.BarBarToken || !isName(e.left, name)) return undefined;
+ const init = unparen(e.right);
+ if (!ts.isBinaryExpression(init) || init.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
+ return undefined;
+ // `Kind || (Kind = {})`
+ if (isName(init.left, name)) return isEmptyObject(init.right) ? null : undefined;
+ // `Kind || (exports.Kind = Kind = {})`
+ const exported = exportsMember(init.left);
+ const inner = unparen(init.right);
+ if (
+ exported === null ||
+ !ts.isBinaryExpression(inner) ||
+ inner.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
+ !isName(inner.left, name) ||
+ !isEmptyObject(inner.right)
+ )
+ return undefined;
+ return exported;
+}
+
+/** The members the body writes, or null when a statement is anything but
+ * a literal member write (`E["K"] = v`, `E.K = v`, or the reverse-mapped
+ * `E[E["K"] = v] = "K"`). */
+function enumMembers(body: readonly ts.Statement[], param: string): Members | null {
+ const members: Members = new Map();
+ for (const stmt of body) {
+ if (!ts.isExpressionStatement(stmt)) return null;
+ const e = stmt.expression;
+ if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
+ return null;
+ const value = literalValue(e.right);
+ if (value === null) return null;
+ const key = memberKey(e.left, param);
+ if (key !== null) {
+ members.set(key, value);
+ continue;
+ }
+ // E[E["K"] = v] = "K"
+ const target = e.left;
+ if (!ts.isElementAccessExpression(target) || !isName(target.expression, param)) return null;
+ const inner = unparen(target.argumentExpression);
+ if (!ts.isBinaryExpression(inner) || inner.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
+ return null;
+ const innerKey = memberKey(inner.left, param);
+ const innerValue = literalValue(inner.right);
+ if (innerKey === null || innerValue === null) return null;
+ members.set(innerKey, innerValue);
+ members.set(propertyKey(innerValue), value);
+ }
+ return members.size > 0 ? members : null;
+}
+
+function memberKey(target: ts.Expression, param: string): string | null {
+ if (ts.isElementAccessExpression(target) && isName(target.expression, param)) {
+ const k = unparen(target.argumentExpression);
+ return ts.isStringLiteral(k) || ts.isNoSubstitutionTemplateLiteral(k) ? k.text : null;
+ }
+ if (
+ ts.isPropertyAccessExpression(target) &&
+ isName(target.expression, param) &&
+ ts.isIdentifier(target.name)
+ )
+ return target.name.text;
+ return null;
+}
+
+function literalValue(expr: ts.Expression): string | number | null {
+ const e = unparen(expr);
+ if (ts.isStringLiteral(e) || ts.isNoSubstitutionTemplateLiteral(e)) return e.text;
+ if (ts.isNumericLiteral(e)) return numberOf(e.text);
+ if (
+ ts.isPrefixUnaryExpression(e) &&
+ e.operator === ts.SyntaxKind.MinusToken &&
+ ts.isNumericLiteral(e.operand)
+ ) {
+ const n = numberOf(e.operand.text);
+ return n === null ? null : -n;
+ }
+ return null;
+}
+
+function numberOf(text: string): number | null {
+ if (!/^(?:\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|0[xX][0-9a-fA-F]+)$/.test(text)) return null;
+ const n = Number(text);
+ return Number.isFinite(n) ? n : null;
+}
+
+/** ToPropertyKey of a member value. */
+function propertyKey(value: string | number): string {
+ return typeof value === "string" ? value : String(value);
+}
+
+/** The canonical array-index keys JavaScript enumerates first. */
+function isIndexKey(key: string): boolean {
+ return /^(?:0|[1-9]\d*)$/.test(key) && Number(key) < 4294967295;
+}
+
+function objectLiteral(members: Members): string {
+ const keys = [...members.keys()];
+ const ordered = [
+ ...keys.filter(isIndexKey).sort((a, b) => Number(a) - Number(b)),
+ ...keys.filter((k) => !isIndexKey(k)),
+ ];
+ const parts = ordered.map((k) => {
+ const v = members.get(k)!;
+ const value = typeof v === "string" ? JSON.stringify(v) : Object.is(v, -0) ? "-0" : String(v);
+ return `${isIndexKey(k) ? k : JSON.stringify(k)}: ${value}`;
+ });
+ return `{ ${parts.join(", ")} }`;
+}
+
+/** True when the module writes `name` anywhere outside the enum call. */
+function writtenElsewhere(sf: ts.SourceFile, name: string, iife: ts.Statement): boolean {
+ const start = tokenStartOfNode(iife);
+ const end = iife.end;
+ let written = false;
+ ts.walkPreorder(sf, (n) => {
+ if (written) return "stop";
+ if (n.pos >= start && n.end <= end) return "skip";
+ if (ts.isBinaryExpression(n) && isAssignmentOperator(n.operatorToken.kind)) {
+ if (isName(n.left, name)) written = true;
+ } else if (
+ (ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) &&
+ (n.operator === ts.SyntaxKind.PlusPlusToken ||
+ n.operator === ts.SyntaxKind.MinusMinusToken) &&
+ isName(n.operand, name)
+ ) {
+ written = true;
+ } else if (
+ (ts.isFunctionDeclaration(n) || ts.isClassDeclaration(n)) &&
+ n.name !== undefined &&
+ n.name.text === name
+ ) {
+ written = true;
+ }
+ return undefined;
+ });
+ return written;
+}
+
+function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
+ return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
+}
+
+function isName(e: ts.Expression, name: string): boolean {
+ const u = unparen(e);
+ return ts.isIdentifier(u) && u.text === name;
+}
+
+function exportsMember(e: ts.Expression): string | null {
+ const u = unparen(e);
+ return ts.isPropertyAccessExpression(u) &&
+ ts.isIdentifier(u.expression) &&
+ u.expression.text === "exports" &&
+ ts.isIdentifier(u.name)
+ ? u.name.text
+ : null;
+}
+
+function isEmptyObject(e: ts.Expression): boolean {
+ const u = unparen(e);
+ return ts.isObjectLiteralExpression(u) && u.properties.nodes.length === 0;
+}
diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts
index ae8a4a8f4c..1bf84e734d 100644
--- a/packages/compiler/src/frontend/npm-static.ts
+++ b/packages/compiler/src/frontend/npm-static.ts
@@ -4,6 +4,7 @@
* Eligibility failures and package preflight errors become explicit refusals. */
import { dirname } from "node:path";
import type { FrontendServices } from "./services.js";
+import { isBundledShimForwarder } from "./package-shims.js";
import type {
NpmStaticDeclarationOverloads,
NpmStaticDeclarationProperties,
@@ -451,16 +452,28 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
} else if (esm !== null) {
original = esm;
}
+ // Polyfill packages that ARE a Node builtin compile as the builtin.
+ const aliased = original === null ? null : services.builtinAliasRequires(original, path);
+ if (aliased !== null) original = aliased;
// A module exporting one of its functions reaches its members
// through that function.
const fnExports = original === null ? null : services.cjsFunctionExports(original, path);
if (fnExports !== null) original = fnExports;
+ // TypeScript's enum IIFEs with literal members become the object
+ // literals they build.
+ const enums = original === null ? null : services.enumIifes(original, path);
+ if (enums !== null) original = enums;
// A module that names its export object through a variable reaches
// its members through `exports`.
const exportsVar = original === null ? null : services.cjsExportsVar(original, path);
if (exportsVar !== null) original = exportsVar;
const respelled =
- split !== null || esm !== null || fnExports !== null || exportsVar !== null;
+ split !== null ||
+ esm !== null ||
+ aliased !== null ||
+ fnExports !== null ||
+ enums !== null ||
+ exportsVar !== null;
if (original !== null) {
const namepaths = services.jsDocNamepaths(path, original);
const source = namepaths ?? original;
@@ -510,12 +523,43 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
hideFile: (path) => {
const target = shadowTargetOf(path);
if (!target) return false;
+ if (isBundledShimForwarder(path)) return false;
if (target.viaTypes) return true; // the @types twin hides whole
return isDeclarationFileName(path) || isUnloadableInstalledTsSource(path);
},
};
}
+const programAliasCache = new Map();
+
+/** The always-on program-file half of the builtin aliases: a CommonJS
+ * program file's `require("")` compiles as the builtin
+ * the package is on Node 24 (npm-static-builtin-aliases.ts), as in an
+ * opted-in package's files. Undefined for every other path, so TypeScript
+ * programs and JavaScript without such a require read the file as is. */
+export function programBuiltinAliasSource(
+ services: FrontendServices,
+ path: string,
+): string | undefined {
+ if (!/\.c?js$/.test(path) || isNodeModulesPathNorm(path)) return undefined;
+ const hit = programAliasCache.get(path);
+ if (hit !== undefined) return hit ?? undefined;
+ let rewritten: string | null = null;
+ try {
+ const source = trackedReadFile(path);
+ if (source !== null) rewritten = services.builtinAliasRequires(source, path);
+ } catch {
+ rewritten = null;
+ }
+ programAliasCache.set(path, rewritten);
+ return rewritten ?? undefined;
+}
+
+/** Resets the program-file alias cache (once per program load). */
+export function clearProgramBuiltinAliasCache(): void {
+ programAliasCache.clear();
+}
+
/* Only a bundler RUNTIME disqualifies now: webpack's module registry has
* no static story (the graph lives in numbered closures). The tsc/babel
* __esModule stamp and esbuild's helper preambles used to disqualify too,
diff --git a/packages/compiler/src/frontend/npm.ts b/packages/compiler/src/frontend/npm.ts
index 0afd1a59a2..6cb1d3b5eb 100644
--- a/packages/compiler/src/frontend/npm.ts
+++ b/packages/compiler/src/frontend/npm.ts
@@ -7,6 +7,7 @@ import {
trackedReadFile,
trackedRealpath,
} from "./input-tracker.js";
+import { isUnloadedPackage } from "./package-shims.js";
import { resolveExports, resolvePackageImports } from "./resolve.js";
const NODE_IMPORT_CONDITIONS = new Set(["import", "node", "default"]);
@@ -66,6 +67,13 @@ export function probeNodeImportRefusal(
): NodeImportRefusal | null {
if (specifier.startsWith("#") || specifier.startsWith("node:")) return null;
const name = packageNameOf(specifier);
+ // A package the project marks never loaded resolves like a missing one.
+ if (isUnloadedPackage(specifier)) {
+ return {
+ code: "ERR_MODULE_NOT_FOUND",
+ message: `Cannot find package '${name}' imported from ${resolve(fromFile)}`,
+ };
+ }
const parts = specifier.split("/");
const subparts = specifier.startsWith("@") ? parts.slice(2) : parts.slice(1);
const subpath = subparts.length > 0 ? `./${subparts.join("/")}` : ".";
@@ -152,6 +160,11 @@ export function probeNodeRequireRefusal(
}
const name = packageNameOf(specifier);
const importer = resolve(fromFile);
+ // A package the project marks never loaded (package-shims.ts) throws
+ // exactly what Node throws when it is not installed.
+ if (isUnloadedPackage(specifier)) {
+ return { message: `Cannot find module '${specifier}'\nRequire stack:\n- ${importer}` };
+ }
for (let dir = dirname(importer); ;) {
// A self-scope whose "name" matches: Node's require honors package
// self-reference — conservative null (resolvable or its own story).
diff --git a/packages/compiler/src/frontend/package-shims.ts b/packages/compiler/src/frontend/package-shims.ts
new file mode 100644
index 0000000000..86a41d3a68
--- /dev/null
+++ b/packages/compiler/src/frontend/package-shims.ts
@@ -0,0 +1,323 @@
+/** Package shims: npm packages that compile from an implementation module
+ * shipped with scriptc instead of from their own published JavaScript, and
+ * packages a project marks as never loaded.
+ *
+ * Two kinds of entries:
+ *
+ * - implementation: a built-in TypeScript module per supported major version
+ * (packages/compiler/shims//v.ts). The importing package's
+ * INSTALLED version selects the entry, so a program with undici 5.x in one
+ * workspace package and 7.x in another compiles both, each against its own
+ * version's behavior. A package that is not installed, or whose major
+ * version has no entry, is not shimmed and resolves exactly as before.
+ *
+ * The module edge goes through resolveProjectImport (preflight, module
+ * order, the CommonJS link) and resolveNpmImport7 / the pipeline's npm-edge
+ * walk skip the package. The checker sees the same module through the fs
+ * shadow: the installed package's types entry (index.d.ts) reads as a
+ * re-export of the shim, so types resolve per importer like the real
+ * package's declarations (tsconfig `paths` would map every importer to one
+ * file).
+ *
+ * - unloaded: a package the project declares never loaded
+ * (`"scriptc": { "packages": { "encoding": "unloaded" } }` in the entry's
+ * package.json). `require()` of it throws Node's MODULE_NOT_FOUND exactly
+ * as when the package is not installed, so the optional-dependency pattern
+ * `try { require("encoding") } catch {}` takes its absent branch, and the
+ * package never joins the program.
+ *
+ * - builtin alias: a polyfill package whose module IS a Node builtin on
+ * Node 24 at its installed major version (builtin-package-aliases.ts).
+ * A require of it compiles as the builtin expression at the require's
+ * position (npm-static-builtin-aliases.ts), so the package never joins
+ * the program.
+ *
+ * A configured stand-in (for example a no-op tracer for dd-trace) is an
+ * implementation entry whose module the project supplies. Today a tsconfig
+ * `paths` mapping already does that: resolveProjectImport honors project
+ * paths for module edges and the checker uses the same map. A config
+ * spelling (`"dd-trace": "./native-shims/dd-trace.ts"`) would add the path
+ * as a project-owned implementation entry here.
+ */
+import { dirname, join, resolve } from "node:path";
+import { packageShimRoot, tsgoPath } from "./dts-paths.js";
+import { trackedDirectoryExists, trackedReadFile, trackedRealpath } from "./input-tracker.js";
+import { packageNameOfSpecifier } from "./workspace-registry.js";
+import {
+ BUILTIN_ALIAS_PACKAGES,
+ builtinAliasOf,
+ type BuiltinAlias,
+} from "./builtin-package-aliases.js";
+
+interface ImplementationShim {
+ /** npm package name. */
+ name: string;
+ /** Supported major versions and their entry modules, relative to the
+ * shim root. */
+ versions: ReadonlyMap;
+ /** The entry has a default export (a CommonJS package's whole export
+ * object, which a default import reads). */
+ defaultExport?: boolean;
+ /** The package's main file in every supported version: a bundler's copy
+ * of it (a __commonJS chunk whose key names the package, its version
+ * and this file) is the package itself. */
+ mainFile: string;
+}
+
+const IMPLEMENTATION_SHIMS: ReadonlyMap = new Map([
+ [
+ "fs-extra",
+ {
+ name: "fs-extra",
+ defaultExport: true,
+ mainFile: "lib/index.js",
+ versions: new Map([
+ [8, "fs-extra/index.ts"],
+ [9, "fs-extra/index.ts"],
+ [10, "fs-extra/index.ts"],
+ [11, "fs-extra/index.ts"],
+ ]),
+ },
+ ],
+ [
+ "undici",
+ {
+ name: "undici",
+ mainFile: "index.js",
+ versions: new Map([
+ [5, "undici/v5.ts"],
+ [7, "undici/v7.ts"],
+ ]),
+ },
+ ],
+]);
+
+/** The shim module that stands in for a bundled copy of a package's main
+ * file (a __commonJS chunk keyed `…/@/…//`), or
+ * null when the package, version or file is not one the registry serves. */
+export function packageShimForBundledFile(
+ name: string,
+ version: string,
+ file: string,
+): string | null {
+ const shim = IMPLEMENTATION_SHIMS.get(name);
+ if (shim === undefined || file.replace(/^\.\//, "") !== shim.mainFile) return null;
+ const major = Number(/^(\d+)\./.exec(version)?.[1] ?? NaN);
+ const entry = shim.versions.get(major);
+ return entry === undefined ? null : join(packageShimRoot(), entry);
+}
+
+/** Bundled-copy forwarders: a virtual declaration module beside a bundle
+ * (`..shim.d.ts`) that re-exports the shim standing in
+ * for a bundled package, so the checker loads the shim through it, and
+ * module resolution answers the shim itself for its path. */
+const bundledForwarders = new Map();
+
+/** The forwarder declaration text for a bundled copy of a shimmed package
+ * and records its path; the bundle's call sites require the forwarder's
+ * extensionless path. */
+export function bundledShimForwarder(declPath: string, shimPath: string): string {
+ bundledForwarders.set(declPath.split("\\").join("/"), shimPath);
+ const target = JSON.stringify(tsgoPath(shimPath));
+ const shim = [...IMPLEMENTATION_SHIMS.values()].find((entry) =>
+ [...entry.versions.values()].some((v) => join(packageShimRoot(), v) === shimPath),
+ );
+ const defaultLine = shim?.defaultExport === true ? `export { default } from ${target};\n` : "";
+ return `// scriptc: this bundled package compiles from its built-in implementation.\nexport * from ${target};\n${defaultLine}`;
+}
+
+/** True for a bundled-copy forwarder's path (the --npm-static shadow keeps
+ * it visible where it hides the package's other declaration files). */
+export function isBundledShimForwarder(path: string): boolean {
+ return bundledForwarders.has(path.split("\\").join("/"));
+}
+
+/** The shim a resolved module path forwards to, or the path itself. */
+export function throughBundledShimForwarder(resolved: string): string {
+ return bundledForwarders.get(resolved.split("\\").join("/")) ?? resolved;
+}
+
+/** Every implementation module the registry ships (absolute paths), for
+ * the guard test that compiles each one. */
+export function packageShimEntries(): string[] {
+ const entries = new Set();
+ for (const shim of IMPLEMENTATION_SHIMS.values())
+ for (const entry of shim.versions.values()) entries.add(join(packageShimRoot(), entry));
+ return [...entries].sort();
+}
+
+/** Package names the project marks never loaded. */
+let unloadedPackages: ReadonlySet = new Set();
+
+/** Reads the project's package-shim configuration: the `scriptc.packages`
+ * map of the entry's nearest package.json. Called once per program load. */
+export function configurePackageShims(entryPath: string): void {
+ const packages = new Set();
+ for (let dir = dirname(resolve(entryPath)); ;) {
+ const text = trackedReadFile(join(dir, "package.json"));
+ if (text !== null) {
+ let parsed: unknown = null;
+ try {
+ parsed = JSON.parse(text);
+ } catch {
+ parsed = null;
+ }
+ const config =
+ parsed !== null && typeof parsed === "object"
+ ? (parsed as Record)["scriptc"]
+ : undefined;
+ const map =
+ config !== null && typeof config === "object"
+ ? (config as Record)["packages"]
+ : undefined;
+ if (map !== null && typeof map === "object") {
+ for (const [name, kind] of Object.entries(map as Record)) {
+ if (kind === "unloaded") packages.add(name);
+ }
+ }
+ break;
+ }
+ const parent = dirname(dir);
+ if (parent === dir) break;
+ dir = parent;
+ }
+ unloadedPackages = packages;
+}
+
+/** True for a package name the registry implements or the project marks
+ * never loaded: such a package never compiles its published JavaScript. */
+export function isPackageShimName(name: string): boolean {
+ return IMPLEMENTATION_SHIMS.has(name) || unloadedPackages.has(name);
+}
+
+/** True when `specifier` names a package the project marks never loaded. */
+export function isUnloadedPackage(specifier: string): boolean {
+ if (unloadedPackages.size === 0) return false;
+ if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("#")) {
+ return false;
+ }
+ return unloadedPackages.has(packageNameOfSpecifier(specifier));
+}
+
+const installedCache = new Map();
+
+/** The installed package directory Node's resolution would reach for
+ * `name` from `fromFile` (node_modules walked upward, realpath'd). */
+function installedPackageDir(fromFile: string, name: string): string | null {
+ const start = dirname(resolve(fromFile));
+ const key = `${start}\0${name}`;
+ const cached = installedCache.get(key);
+ if (cached !== undefined) return cached;
+ let found: string | null = null;
+ for (let dir = start; ;) {
+ const candidate = join(dir, "node_modules", name);
+ if (trackedDirectoryExists(candidate)) {
+ found = trackedRealpath(candidate) ?? candidate;
+ break;
+ }
+ const parent = dirname(dir);
+ if (parent === dir) break;
+ dir = parent;
+ }
+ installedCache.set(key, found);
+ return found;
+}
+
+/** The shim entry for an installed package directory, or null when the
+ * directory is not a registered package at a supported major version. */
+function shimEntryForPackageDir(pkgDir: string, expectedName: string | null): string | null {
+ const text = trackedReadFile(join(pkgDir, "package.json"));
+ if (text === null) return null;
+ let pkg: { name?: unknown; version?: unknown };
+ try {
+ pkg = JSON.parse(text) as { name?: unknown; version?: unknown };
+ } catch {
+ return null;
+ }
+ if (typeof pkg.name !== "string" || typeof pkg.version !== "string") return null;
+ if (expectedName !== null && pkg.name !== expectedName) return null;
+ const shim = IMPLEMENTATION_SHIMS.get(pkg.name);
+ if (shim === undefined) return null;
+ const major = Number(/^(\d+)\./.exec(pkg.version)?.[1] ?? NaN);
+ const entry = shim.versions.get(major);
+ return entry === undefined ? null : join(packageShimRoot(), entry);
+}
+
+/** The shim module `specifier` resolves to from `fromFile`, or null. Only
+ * the package's main entry is shimmed; subpath imports resolve as before. */
+export function packageShimFor(fromFile: string, specifier: string): string | null {
+ if (!IMPLEMENTATION_SHIMS.has(specifier)) return null;
+ const dir = installedPackageDir(fromFile, specifier);
+ return dir === null ? null : shimEntryForPackageDir(dir, specifier);
+}
+
+/** The builtin a bare specifier's module is on Node 24, as installed where
+ * Node's resolution from `fromFile` finds it, or null. A package the
+ * project marks unloaded has no alias: its require throws MODULE_NOT_FOUND. */
+export function builtinPackageAliasFor(fromFile: string, specifier: string): BuiltinAlias | null {
+ if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("#"))
+ return null;
+ if (isUnloadedPackage(specifier)) return null;
+ const name = packageNameOfSpecifier(specifier);
+ if (!BUILTIN_ALIAS_PACKAGES.has(name)) return null;
+ const dir = installedPackageDir(fromFile, name);
+ if (dir === null) return null;
+ const text = trackedReadFile(join(dir, "package.json"));
+ if (text === null) return null;
+ let pkg: { name?: unknown; version?: unknown };
+ try {
+ pkg = JSON.parse(text) as { name?: unknown; version?: unknown };
+ } catch {
+ return null;
+ }
+ if (pkg.name !== name || typeof pkg.version !== "string") return null;
+ return builtinAliasOf(name, pkg.version, specifier.slice(name.length).replace(/^\//, ""));
+}
+
+/** True when a bare specifier is served by a shim or marked unloaded: it is
+ * not an npm import for the npm-static machinery. */
+export function isShimmedImport(fromFile: string, specifier: string): boolean {
+ return isUnloadedPackage(specifier) || packageShimFor(fromFile, specifier) !== null;
+}
+
+/** The fs-shadow answer for a shimmed package's declaration entry: the
+ * installed `index.d.ts` reads as a re-export of the version's shim module,
+ * so the checker types each importer against its own version. Undefined
+ * for every other path. */
+export function packageShimDeclarationText(path: string): string | undefined {
+ if (!path.endsWith("/index.d.ts")) return undefined;
+ const pkgDir = dirname(path);
+ const name = pkgDir.slice(pkgDir.lastIndexOf("/") + 1);
+ const shim = IMPLEMENTATION_SHIMS.get(name);
+ if (shim === undefined) return undefined;
+ // A DefinitelyTyped twin (`@types/fs-extra`) types a package that ships no
+ // declarations. Nothing at its path names the installed runtime version,
+ // so it reads as the shim only when every supported version shares one
+ // module.
+ const viaTypes = dirname(pkgDir).endsWith("/@types");
+ const entries = new Set(shim.versions.values());
+ const entry = viaTypes
+ ? entries.size === 1
+ ? join(packageShimRoot(), [...entries][0]!)
+ : null
+ : shimEntryForPackageDir(pkgDir, name);
+ if (entry === null) return undefined;
+ const target = JSON.stringify(tsgoPath(entry));
+ const defaultLine = shim.defaultExport === true ? `export { default } from ${target};\n` : "";
+ return `// scriptc: this package compiles from its built-in implementation.\nexport * from ${target};\n${defaultLine}`;
+}
+
+/** True for a file of the shipped package shims. They are compiler-owned
+ * implementations, type-checked by scriptc's own tests: like library
+ * declarations under skipLibCheck, a project's lib and @types/node choice
+ * must not turn their internals into the project's type errors. */
+export function isPackageShimFile(fileName: string): boolean {
+ const root = tsgoPath(packageShimRoot());
+ return fileName.startsWith(root.endsWith("/") ? root : `${root}/`);
+}
+
+/** Resets per-compile caches. */
+export function clearPackageShimCaches(): void {
+ installedCache.clear();
+ bundledForwarders.clear();
+}
diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts
index 16c96247f7..193e8adc09 100644
--- a/packages/compiler/src/frontend/pipeline.ts
+++ b/packages/compiler/src/frontend/pipeline.ts
@@ -11,8 +11,10 @@ import {
checkPreflight,
isNodeTypesPath,
locOf,
+ npmStaticDepSf7,
npmStaticLoadableFiles,
requiresOf,
+ resolveImport,
resolveNpmImport,
type LoadResult,
} from "./program.js";
@@ -24,6 +26,7 @@ import {
npmStaticPackageOfPath,
} from "./npm-static.js";
import { clearResolveCaches, resolveBareModule, resolveWorkspaceSourceModule } from "./resolve.js";
+import { isShimmedImport } from "./package-shims.js";
import { provenanceEntryFor } from "./provenance-registry.js";
import { isJsSourceFileName } from "./tsc-codes.js";
import { isRelativeSpecifier, packageNameOfSpecifier } from "./workspace-registry.js";
@@ -36,6 +39,7 @@ import {
isStringLiteral,
isStringLiteralLike,
SyntaxKind,
+ type SourceFile,
} from "./ts7/adapter.js";
import { moduleSourceCandidates } from "./module-source-candidates.js";
import { CONTRACT_TYPES_MODULE } from "./contract-types.js";
@@ -113,6 +117,65 @@ export type FrontendFactory = (
libraryNpmStatic?: readonly string[],
) => Frontend;
+/** Adds the program modules a literal `import()` reaches to `scanned`,
+ * with their own static import graphs: they load at the call, outside the
+ * startup module order, and their npm imports are dependencies all the
+ * same (a workspace package loaded on demand, the vercel.ts config parser
+ * shape). An opted-in package's JavaScript reached that way joins too,
+ * with every file its literal require() and import edges load: those
+ * files are outside the module order as well, and the packages THEY
+ * require (a compiled CommonJS dist requiring a sibling package) are
+ * dependencies the selection must see. */
+function addDynamicImportGraph(load: LoadResult, scanned: Set): void {
+ const queue = [...scanned];
+ const added = new Set();
+ while (queue.length > 0) {
+ const sf = queue.pop()!;
+ const packageJs = sf.fileName.includes("/node_modules/");
+ if (packageJs && !added.has(sf)) continue;
+ const specs: { spec: string; kind: "import" | "require" }[] = [];
+ // Every literal import() in the file, read from the syntax: a comment
+ // can stand between the keyword and its parenthesis. A file without
+ // the keyword has none. Package JavaScript loads its literal require()
+ // calls the same way, nested ones included.
+ const calls =
+ packageJs || sf.text.includes("import") ? moduleSourceCandidates(load.program, sf).calls : [];
+ for (const call of calls) {
+ const arg = call.arguments.nodes[0];
+ if (arg === undefined || !isStringLiteralLike(arg)) continue;
+ if (call.expression.kind === SyntaxKind.ImportKeyword)
+ specs.push({ spec: arg.text, kind: "import" });
+ else if (packageJs && isIdentifier(call.expression) && call.expression.text === "require")
+ specs.push({ spec: arg.text, kind: "require" });
+ }
+ // The startup graph's static edges are already in the module order.
+ if (added.has(sf)) {
+ for (const stmt of sf.statements.nodes) {
+ if (
+ (isImportDeclaration(stmt) || isExportDeclaration(stmt)) &&
+ stmt.moduleSpecifier &&
+ isStringLiteral(stmt.moduleSpecifier) &&
+ !(isImportDeclaration(stmt) ? erasedTypeOnlyImport(stmt) : stmt.isTypeOnly)
+ )
+ specs.push({ spec: stmt.moduleSpecifier.text, kind: "import" });
+ }
+ }
+ for (const { spec, kind } of specs) {
+ if (canonicalBuiltinModule(spec) !== null) continue;
+ // Only the JavaScript of a package the selection already opted in:
+ // a package's declarations load nothing.
+ const dep =
+ resolveImport(load.program, sf, spec, kind) ??
+ npmStaticDepSf7(load.program, sf, spec, kind);
+ if (dep === null || scanned.has(dep)) continue;
+ if (dep.fileName.includes("/node_modules/") && !isJsSourceFileName(dep.fileName)) continue;
+ scanned.add(dep);
+ added.add(dep);
+ queue.push(dep);
+ }
+ }
+}
+
function detectAutoPackages(
load: LoadResult,
statuses: NpmStaticStatus[],
@@ -135,6 +198,7 @@ function detectAutoPackages(
// nested require() calls the graph walk does not model) still name its
// dependencies.
const scanned = new Set([...load.moduleOrder, load.entry]);
+ addDynamicImportGraph(load, scanned);
const transitiveFiles =
transitive.size === 0
? []
@@ -192,6 +256,9 @@ function detectAutoPackages(
// Attested entries already resolve to program sources, so the
// published package's declaration eligibility does not apply.
if (provenanceEntryFor(spec) !== null) continue;
+ // Packages scriptc implements itself, or that the project marks never
+ // loaded, are not npm dependencies (package-shims.ts).
+ if (isShimmedImport(sf.fileName, spec)) continue;
if (resolveWorkspaceSourceModule(sf.fileName, spec) !== null) continue;
if (isRelativeSpecifier(spec) || spec.startsWith("node:") || spec.startsWith("#")) continue;
// Bare builtin names ("fs", "path") are the builtin machinery's
@@ -624,6 +691,7 @@ function loadFrontend(
frontendServices: finalLoad.services,
startupCrash: finalLoad.startupCrash ?? null,
inlineRequireCycleCalls: finalLoad.inlineRequireCycleCalls,
+ moduleEdges: finalLoad.moduleEdges,
projectCheckJs: finalLoad.typeWorld?.projectCheckJs ?? false,
externalTypes: finalLoad.externalTypes,
externalTypeSpecifiersByFile: finalLoad.externalTypeSpecifiersByFile,
diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts
index 2e75542d7d..a9e926a1a3 100644
--- a/packages/compiler/src/frontend/program.ts
+++ b/packages/compiler/src/frontend/program.ts
@@ -68,6 +68,8 @@ import {
isNpmStaticPackage,
npmStaticActive,
npmStaticFsShadow,
+ programBuiltinAliasSource,
+ clearProgramBuiltinAliasCache,
npmStaticLexerVisibleNames,
npmStaticLiveExportTable,
npmStaticPackageOfPath,
@@ -89,6 +91,12 @@ import type {
NpmStaticDeclarationProperties,
NpmStaticOverloadSignature,
} from "./npm-static-declaration-syntax.js";
+import {
+ configurePackageShims,
+ isPackageShimFile,
+ isPackageShimName,
+ isShimmedImport,
+} from "./package-shims.js";
import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js";
import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js";
import {
@@ -348,6 +356,11 @@ export interface LoadResult {
* at run time instead of refusing the whole program. Filled by
* checkPreflight. */
inlineRequireCycleCalls?: ReadonlySet;
+ /** Every program file's module edges as preflight resolved them,
+ * require() edges included: the lowering orders the modules only a
+ * literal import(), fork() or Worker reaches with them. Filled by
+ * checkPreflight. */
+ moduleEdges?: ReadonlyMap | undefined;
/** Node's startup refusal, when the graph carries an import edge Node
* rejects before ANY module evaluates — a RESOLUTION refusal (a bare
* specifier nothing installed resolves, an exports/imports map that
@@ -914,7 +927,13 @@ export function loadProgram(
}
externalTypes.set(specifier, declarationPath);
}
- const npmStaticPackages = [...new Set(opts?.npmStatic ?? [])];
+ // Packages scriptc implements itself or the project marks never loaded
+ // (package-shims.ts) never compile their published JavaScript, even when
+ // named.
+ configurePackageShims(entryPath);
+ const npmStaticPackages = [...new Set(opts?.npmStatic ?? [])].filter(
+ (name) => !isPackageShimName(name),
+ );
// Resolve authored declaration entries before enabling the JS-only
// package resolver. Safe overload metadata binds through the runtime
// entry to one exact implementation file; it never contributes values
@@ -1037,8 +1056,10 @@ export function loadProgram(
// the checker, so its resolution lands on the JS Node actually loads;
// resolve.ts answers the same sibling for scriptc's own edges).
const npmShadow = npmStaticFsShadow(services);
+ clearProgramBuiltinAliasCache();
const fsShadow = {
- readFile: (path: string) => npmShadow?.readFile(path),
+ readFile: (path: string) =>
+ npmShadow?.readFile(path) ?? programBuiltinAliasSource(services, path),
hideFile: (path: string) =>
(npmShadow?.hideFile(path) ?? false) || projectDtsRuntimeSibling(path) !== null,
};
@@ -1088,10 +1109,12 @@ export function loadProgram(
* preflight diagnostics. Preserve the caller's complete load shape when
* mutating it, including its owned services and disposal callback. */
export function checkPreflight(load: T): ScrDiagnostic[] {
- const { diags, moduleOrder, startupCrash, inlineRequireCycleCalls } = preflight7(load);
+ const { diags, moduleOrder, startupCrash, inlineRequireCycleCalls, moduleEdges } =
+ preflight7(load);
load.moduleOrder = moduleOrder;
load.startupCrash = startupCrash;
load.inlineRequireCycleCalls = inlineRequireCycleCalls;
+ load.moduleEdges = moduleEdges;
return diags;
}
@@ -4316,6 +4339,10 @@ function resolveNpmImport7(
// its attested source compiles as program modules (resolveProjectModule
if (provenanceEntryFor(specifier) !== null) return null;
+ // Shimmed and never-loaded packages (package-shims.ts) are not npm
+ // imports either: the shim compiles as a program module, and an unloaded
+ // package's require() is Node's MODULE_NOT_FOUND.
+ if (isShimmedImport(fromFileName, specifier)) return null;
const resolved = resolveBareModule(fromFileName, specifier, undefined, resolutionKind);
if (!resolved) return null;
if (!isNodeModulesPath(resolved.typesFile)) {
@@ -4428,6 +4455,7 @@ function preflight7(load: LoadResult): {
moduleOrder: ts.SourceFile[];
startupCrash: StartupCrash | null;
inlineRequireCycleCalls: ReadonlySet;
+ moduleEdges?: ReadonlyMap;
} {
const { program, entry } = load;
const diags: ScrDiagnostic[] = [...load.configDiags];
@@ -4564,6 +4592,25 @@ function preflight7(load: LoadResult): {
return d.code === 2300 && commentDup.has(`${d.fileName}:${sf.text.slice(d.pos, d.end)}`);
};
+ /* `typeof window` / `typeof self` in a JavaScript file: the operand of
+ * typeof may name a global nothing declares (Node answers "undefined"
+ * and never throws), the feature-detection idiom every isomorphic
+ * package uses, and the guarded uses of that name (`typeof window < "u"
+ * ? window : …`) are code Node never runs. A name the file feature-tests
+ * this way is therefore not a type error in JavaScript; a reached use
+ * still meets the lowering's located refusal. TypeScript files keep the
+ * error and its hint. */
+ const typeofTestedNameSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => {
+ if (d.code !== 2304 && d.code !== 2552 && d.code !== 2584) return false;
+ if (d.fileName === undefined || !isJsSourceFileName(d.fileName)) return false;
+ if (d.pos === undefined || d.end === undefined) return false;
+ const sf = p.getSourceFile(d.fileName);
+ if (!sf) return false;
+ const name = sf.text.slice(d.pos, d.end);
+ if (!/^[A-Za-z_$][\w$]*$/.test(name)) return false;
+ return new RegExp(`\\btypeof\\s*\\(?\\s*${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(sf.text);
+ };
+
const workspaceImplicitAnySuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => {
if (d.code !== 7016 || d.fileName === undefined || d.pos === undefined || d.end === undefined)
return false;
@@ -4604,10 +4651,12 @@ function preflight7(load: LoadResult): {
(!suppressedJsStrictness7(d) || enforcedInCheckedJs(p, d)) &&
!npmStaticFileSuppressed(d) &&
!nodeModulesJsSuppressed(d) &&
+ (d.fileName === undefined || !isPackageShimFile(d.fileName)) &&
!namespaceCalleeSuppressed(p, d) &&
!inferredJsDiagnosticSuppressed(p, d) &&
!isNpmStaticSubclassArgument(p, d) &&
!workspaceImplicitAnySuppressed(p, d) &&
+ !typeofTestedNameSuppressed(p, d) &&
!jsdocTypeSuppressed(p, d, commentDup),
);
};
@@ -5523,6 +5572,7 @@ function preflight7(load: LoadResult): {
moduleOrder: order,
startupCrash: resolveCrash ?? linkCrash,
inlineRequireCycleCalls,
+ moduleEdges: edges,
};
}
diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts
index 36470a6435..3b32dad3a2 100644
--- a/packages/compiler/src/frontend/resolve.ts
+++ b/packages/compiler/src/frontend/resolve.ts
@@ -22,6 +22,11 @@ import {
npmStaticPackageOfPath,
npmStaticTransformPkgJson,
} from "./npm-static.js";
+import {
+ clearPackageShimCaches,
+ packageShimFor,
+ throughBundledShimForwarder,
+} from "./package-shims.js";
import { provenanceEntryFor } from "./provenance-registry.js";
import {
trackedAccessibleEntries,
@@ -609,6 +614,10 @@ export function resolveProjectImport(fromFile: string, specifier: string): strin
if (provenance !== null) return normalizeResolvedPath(provenance);
const viaPaths = resolveViaProjectPaths(specifier);
if (viaPaths !== null) return normalizeResolvedPath(viaPaths);
+ // A package scriptc implements itself (package-shims.ts): the installed
+ // version's shim module compiles in the package's place.
+ const shim = packageShimFor(fromFile, specifier);
+ if (shim !== null) return normalizeResolvedPath(shim);
const pkgDir = nearestPkgDir(dirname(resolve(fromFile)));
if (pkgDir === null) return null;
const pkg = pkgJsonOf(pkgDir);
@@ -662,6 +671,11 @@ export function resolveProjectImport(fromFile: string, specifier: string): strin
* answer null for their dedicated callers. */
export function resolveProjectModule(fromFile: string, specifier: string): string | null {
if (isRelativeSpecifier(specifier) || isAbsolute(specifier)) {
+ // A bundled copy's forwarder (a virtual declaration module) answers
+ // the shim it forwards to.
+ const forwarder = join(dirname(resolve(fromFile)), `${specifier}.d.ts`);
+ const forwarded = throughBundledShimForwarder(forwarder);
+ if (forwarded !== forwarder) return forwarded;
return resolveRelativeModule(fromFile, specifier);
}
if (specifier.startsWith("node:")) return null;
@@ -1051,6 +1065,7 @@ export function resolveTypeDirective(name: string, fromFile: string): string | n
/** Resolver metadata and the active project realm are shared within one
* compile, never across compiles in a long-lived process. */
export function clearResolveCaches(): void {
+ clearPackageShimCaches();
pkgJsonCache.clear();
workspaceMembersCache.clear();
projectRealmPkgJson = null;
diff --git a/packages/compiler/src/frontend/services.ts b/packages/compiler/src/frontend/services.ts
index 07bc8ca69c..be3881e137 100644
--- a/packages/compiler/src/frontend/services.ts
+++ b/packages/compiler/src/frontend/services.ts
@@ -8,6 +8,12 @@ import {
splitBundledChunks,
type BundledChunkSplit,
} from "./npm-static-bundled-chunks.js";
+import {
+ mayRequireBuiltinAlias,
+ rewriteBuiltinAliasRequires,
+} from "./npm-static-builtin-aliases.js";
+import { mayHoldEnumIife, rewriteEnumIifes } from "./npm-static-enum-iife.js";
+import { builtinPackageAliasFor } from "./package-shims.js";
import * as syntax from "./npm-static-declaration-syntax.js";
import { normalizeCjsExportsVar } from "./npm-static-cjs-exports-var.js";
import type { SourceFile } from "./ts7/ast-types.js";
@@ -100,6 +106,22 @@ export class FrontendServices {
if (!source.includes("module.exports")) return null;
return normalizeCjsFunctionExports(this.parse(path, source, "js"));
}
+ /** A package module with its literal-valued TypeScript enum IIFEs
+ * respelled as object literals (null: none). */
+ enumIifes(source: string, path: string): string | null {
+ this.ensureOpen();
+ if (!mayHoldEnumIife(source)) return null;
+ return rewriteEnumIifes(this.parse(path, source, "js"));
+ }
+ /** A package module with its requires of builtin-aliased polyfill
+ * packages respelled as the builtins (null: none). */
+ builtinAliasRequires(source: string, path: string): string | null {
+ this.ensureOpen();
+ if (!mayRequireBuiltinAlias(source)) return null;
+ return rewriteBuiltinAliasRequires(this.parse(path, source, "js"), (specifier) =>
+ builtinPackageAliasFor(path, specifier),
+ );
+ }
/** A CommonJS module that names its export object through a variable
* (`var ip = exports`), its member accesses respelled through `exports`
* (null: none). */
diff --git a/packages/compiler/src/frontend/ts7/program-host.ts b/packages/compiler/src/frontend/ts7/program-host.ts
index dbc0804d5c..928b2bb583 100644
--- a/packages/compiler/src/frontend/ts7/program-host.ts
+++ b/packages/compiler/src/frontend/ts7/program-host.ts
@@ -40,6 +40,7 @@ import {
trackedReadFile,
trackedRealpath,
} from "../input-tracker.js";
+import { packageShimDeclarationText } from "../package-shims.js";
/** The compiler options our createProgram accepts: TS7's CompilerOptions
* shape (numeric enums for target/module/moduleResolution — the enums module
@@ -154,6 +155,11 @@ export class Ts7Host {
const replacement = shadow.readFile(fileName);
if (replacement !== undefined) return stripSourceBom(replacement);
}
+ // A package scriptc implements itself (package-shims.ts): its
+ // installed declaration entry re-exports the shim module, in every
+ // program the host creates.
+ const shim = packageShimDeclarationText(fileName);
+ if (shim !== undefined) return shim;
const source = trackedReadFile(fileName);
return source === null ? null : stripSourceBom(source);
},
diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts
index c9a1cc9df5..d16cd6ef06 100644
--- a/packages/compiler/src/frontend/type-mapper.ts
+++ b/packages/compiler/src/frontend/type-mapper.ts
@@ -2471,6 +2471,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
psym.declarations.some(
(d) => ts.isInterfaceDeclaration(d) && ctx.isStdlibFile(ts.getSourceFileOfNode(d)),
);
+ // The timer handles: an interface in the fallback and in @types/node 22+,
+ // a class (`class Timeout implements Timer`) in @types/node 20 and older.
+ const isStdlibTimerHandle = (name: string): boolean =>
+ psym !== undefined &&
+ ts.symbolName(psym) === name &&
+ psym.declarations.some(
+ (d) =>
+ (ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
+ ctx.isStdlibFile(ts.getSourceFileOfNode(d)),
+ );
// The builtin Error classes: references to the lib's Error interfaces map to the runtime-provided class
// hierarchy (provenance, not the name — a user's own `class Error`
// resolved through the class-instance branch above, and a user
@@ -3004,11 +3014,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// methods lower over that handle (loop-liveness bookkeeping); `Timeout |
// null` becomes `number | null`, and clearTimeout/clearInterval take the
// handle. Provenance-checked like the other named interfaces.
- if (isStdlibInterface("Timeout")) return F64;
+ if (isStdlibTimerHandle("Timeout")) return F64;
// NodeJS.Immediate / the fallback `Immediate` (setImmediate's return):
// the same numeric-handle story over the check-phase queue — its own id
// space, so clearTimeout of an Immediate no-ops like Node.
- if (isStdlibInterface("Immediate")) return F64;
+ if (isStdlibTimerHandle("Immediate")) return F64;
if (
isStdlibInterface("ArrayBuffer") ||
isStdlibInterface("SharedArrayBuffer") ||
diff --git a/packages/compiler/test/package-shim-guard.test.ts b/packages/compiler/test/package-shim-guard.test.ts
new file mode 100644
index 0000000000..ff01acbb8c
--- /dev/null
+++ b/packages/compiler/test/package-shim-guard.test.ts
@@ -0,0 +1,64 @@
+/* Every package shim (packages/compiler/shims, registered in
+ * src/frontend/package-shims.ts) compiles without a lowering refusal or a
+ * runtime fence anywhere in its own files.
+ *
+ * Shim files suppress TypeScript errors (like library declarations under
+ * skipLibCheck), but never lowering diagnostics: a refusal in code the
+ * program reaches fails its build, located in the shim, and one in code it
+ * does not reach would fail a later program. This test reaches all of it:
+ * the analysis lowers the unreached remainder too, so every function body
+ * of every shim module is checked, under both type worlds a project can
+ * bring (scriptc's bundled Node declarations, and @types/node).
+ */
+import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterAll, describe, expect, test } from "vitest";
+import { analyze } from "../src/index.js";
+import { packageShimEntries } from "../src/frontend/package-shims.js";
+import type { ScrDiagnostic } from "../src/diagnostics/diagnostic.js";
+
+const repoRoot = join(import.meta.dirname, "../../..");
+const shimRoot = join(repoRoot, "packages/compiler/shims");
+const scratch = mkdtempSync(join(tmpdir(), "scriptc-shim-guard-"));
+afterAll(() => rmSync(scratch, { recursive: true, force: true }));
+
+function shimFindings(entry: string, world: "bundled" | "types-node"): string[] {
+ const dir = join(scratch, `${world}-${entry.slice(shimRoot.length + 1).replace(/[/.]/g, "_")}`);
+ mkdirSync(dir, { recursive: true });
+ if (world === "types-node") {
+ mkdirSync(join(dir, "node_modules/@types"), { recursive: true });
+ symlinkSync(join(repoRoot, "node_modules/@types/node"), join(dir, "node_modules/@types/node"));
+ }
+ const main = join(dir, "main.ts");
+ writeFileSync(main, `import ${JSON.stringify(entry)};\nconsole.log("loaded");\n`);
+ const { coverage } = analyze(main);
+ expect(coverage.preflightFailed, "the shim type-checks for the analysis").toBe(false);
+ const all: ScrDiagnostic[] = [
+ ...coverage.diagnostics,
+ ...(coverage.runtimeFences ?? []),
+ ...(coverage.unreached?.diagnostics ?? []),
+ ...(coverage.unreached?.runtimeFences ?? []),
+ ];
+ return all
+ .filter((d) => d.loc.file.startsWith(shimRoot))
+ .map((d) => `${d.loc.file.slice(shimRoot.length + 1)}: ${d.code} ${d.message}`);
+}
+
+describe("package shims compile without refusals", () => {
+ const entries = packageShimEntries();
+ test("the registry lists its shim modules", () => {
+ expect(entries.map((e) => e.slice(shimRoot.length + 1))).toEqual([
+ "fs-extra/index.ts",
+ "undici/v5.ts",
+ "undici/v7.ts",
+ ]);
+ });
+ for (const entry of entries) {
+ for (const world of ["bundled", "types-node"] as const) {
+ test(`${entry.slice(shimRoot.length + 1)} (${world})`, () => {
+ expect(shimFindings(entry, world)).toEqual([]);
+ }, 300_000);
+ }
+ }
+});
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6250-string-of-string-or-url.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6250-string-of-string-or-url.ts.json
new file mode 100644
index 0000000000..437bbfcb92
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6250-string-of-string-or-url.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6250-string-of-string-or-url.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-extra-package/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-extra-package/main.ts.json
new file mode 100644
index 0000000000..d48b63a869
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/fs-extra-package/main.ts.json
@@ -0,0 +1,8 @@
+{
+ "order": [
+ "/packages/compiler/shims/fs-extra/core/api.ts",
+ "/packages/compiler/shims/fs-extra/index.ts",
+ "/tests/corpus/fs-extra-package/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/global-detection-typeof.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/global-detection-typeof.cjs.json
new file mode 100644
index 0000000000..93ea8e3a72
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/global-detection-typeof.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/global-detection-typeof.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readable-stream-3/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readable-stream-3/main.cjs.json
new file mode 100644
index 0000000000..8a308528a1
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readable-stream-3/main.cjs.json
@@ -0,0 +1,27 @@
+{
+ "order": [
+ "/tests/corpus/readable-stream-3/main.cjs"
+ ],
+ "diags": [
+ {
+ "code": "SC0001",
+ "message": "Could not find a declaration file for module 'readable-stream'. '/tests/corpus/readable-stream-3/node_modules/readable-stream/readable.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream';`",
+ "loc": {
+ "file": "/tests/corpus/readable-stream-3/main.cjs",
+ "start": 554,
+ "end": 571
+ }
+ },
+ {
+ "code": "SC1010",
+ "message": "the 'readable-stream' module is not supported yet",
+ "loc": {
+ "file": "/tests/corpus/readable-stream-3/main.cjs",
+ "start": 541,
+ "end": 572
+ },
+ "milestone": "M4",
+ "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
+ }
+ ]
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readable-stream-disable/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readable-stream-disable/main.cjs.json
new file mode 100644
index 0000000000..21f85bd016
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/readable-stream-disable/main.cjs.json
@@ -0,0 +1,87 @@
+{
+ "order": [
+ "/tests/corpus/readable-stream-disable/main.cjs"
+ ],
+ "diags": [
+ {
+ "code": "SC0001",
+ "message": "Could not find a declaration file for module 'readable-stream'. '/tests/corpus/readable-stream-disable/node_modules/readable-stream/readable.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream';`",
+ "loc": {
+ "file": "/tests/corpus/readable-stream-disable/main.cjs",
+ "start": 581,
+ "end": 598
+ }
+ },
+ {
+ "code": "SC0001",
+ "message": "Could not find a declaration file for module 'readable-stream/transform'. '/tests/corpus/readable-stream-disable/node_modules/readable-stream/transform.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream/transform';`",
+ "loc": {
+ "file": "/tests/corpus/readable-stream-disable/main.cjs",
+ "start": 625,
+ "end": 652
+ }
+ },
+ {
+ "code": "SC0001",
+ "message": "Could not find a declaration file for module 'readable-stream/passthrough'. '/tests/corpus/readable-stream-disable/node_modules/readable-stream/passthrough.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream/passthrough';`",
+ "loc": {
+ "file": "/tests/corpus/readable-stream-disable/main.cjs",
+ "start": 681,
+ "end": 710
+ }
+ },
+ {
+ "code": "SC0001",
+ "message": "Could not find a declaration file for module 'readable-stream/writable'. '/tests/corpus/readable-stream-disable/node_modules/readable-stream/writable.js' implicitly has an 'any' type.\n Try `npm i --save-dev @types/readable-stream` if it exists or add a new declaration (.d.ts) file containing `declare module 'readable-stream/writable';`",
+ "loc": {
+ "file": "/tests/corpus/readable-stream-disable/main.cjs",
+ "start": 736,
+ "end": 762
+ }
+ },
+ {
+ "code": "SC1010",
+ "message": "the 'readable-stream' module is not supported yet",
+ "loc": {
+ "file": "/tests/corpus/readable-stream-disable/main.cjs",
+ "start": 568,
+ "end": 599
+ },
+ "milestone": "M4",
+ "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
+ },
+ {
+ "code": "SC1010",
+ "message": "the 'readable-stream/transform' module is not supported yet",
+ "loc": {
+ "file": "/tests/corpus/readable-stream-disable/main.cjs",
+ "start": 605,
+ "end": 653
+ },
+ "milestone": "M4",
+ "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
+ },
+ {
+ "code": "SC1010",
+ "message": "the 'readable-stream/passthrough' module is not supported yet",
+ "loc": {
+ "file": "/tests/corpus/readable-stream-disable/main.cjs",
+ "start": 659,
+ "end": 711
+ },
+ "milestone": "M4",
+ "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
+ },
+ {
+ "code": "SC1010",
+ "message": "the 'readable-stream/writable' module is not supported yet",
+ "loc": {
+ "file": "/tests/corpus/readable-stream-disable/main.cjs",
+ "start": 717,
+ "end": 763
+ },
+ "milestone": "M4",
+ "hint": "relative imports (./file, ../dir/file), package.json-mediated project imports (#alias via the imports field, self-name references via exports), statically compiled npm packages, and the built-in fs, fs/promises, path, os, url, crypto, zlib, child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
+ }
+ ]
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2585-builtin-polyfill-aliases/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2585-builtin-polyfill-aliases/main.ts.json
new file mode 100644
index 0000000000..6ccd6c6573
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2585-builtin-polyfill-aliases/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/npm/cases/2585-builtin-polyfill-aliases/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2586-readable-stream-builtin/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2586-readable-stream-builtin/main.ts.json
new file mode 100644
index 0000000000..db2264f1a2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2586-readable-stream-builtin/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/fixtures/npm/cases/2586-readable-stream-builtin/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2587-bundled-polyfill-aliases/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2587-bundled-polyfill-aliases/main.ts.json
new file mode 100644
index 0000000000..f8fcef2303
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/2587-bundled-polyfill-aliases/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "