mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Compile polyfill packages that are Node builtins as the builtins (#1458)
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -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.
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Package</th><th>Versions</th><th>Compiles as</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>isarray</code></td><td>0.x, 1.x, 2.x</td><td><code>Array.isArray</code></td></tr>
|
||||
<tr><td><code>safe-buffer</code></td><td>5.x</td><td><code>require("buffer")</code></td></tr>
|
||||
<tr><td><code>process-nextick-args</code></td><td>2.x</td><td><code>process</code></td></tr>
|
||||
<tr><td><code>inherits</code></td><td>2.x</td><td><code>require("util").inherits</code></td></tr>
|
||||
<tr><td><code>util-deprecate</code></td><td>1.x</td><td><code>require("util").deprecate</code></td></tr>
|
||||
<tr><td><code>readable-stream</code></td><td>2.x, 3.x, 4.x</td><td><code>require("stream")</code> and its classes</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
`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
|
||||
|
||||
|
||||
@@ -178,6 +178,10 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/buffer-construction.mdx</include>
|
||||
|
||||
### readable-stream
|
||||
|
||||
<include>../limitations/readable-stream.mdx</include>
|
||||
|
||||
### HTTP headers
|
||||
|
||||
<include>../limitations/http-headers.mdx</include>
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"files": [
|
||||
"dist",
|
||||
"ambient",
|
||||
"shims",
|
||||
"surface-manifest.json"
|
||||
],
|
||||
"publishConfig": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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";
|
||||
@@ -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<string, AgentEntry>;
|
||||
__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<string, AgentEntry>();
|
||||
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<void> {
|
||||
const closing: Promise<void>[] = [];
|
||||
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<void> {
|
||||
const destroying: Promise<void>[] = [];
|
||||
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(() => {});
|
||||
}
|
||||
}
|
||||
@@ -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<ResponseData> {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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<Buffer>;
|
||||
}
|
||||
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<Buffer>;
|
||||
}
|
||||
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 };
|
||||
@@ -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"),
|
||||
};
|
||||
@@ -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<string, string>;
|
||||
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<void> {
|
||||
return Promise.reject(new Error("not implemented"));
|
||||
}
|
||||
|
||||
destroy(_err?: Error | null): Promise<void> {
|
||||
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<ResponseData> {
|
||||
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<void> {
|
||||
return new Promise<void>((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<void> {
|
||||
return new Promise<void>((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<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
__destroy(_err: Error): Promise<void> {
|
||||
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<ResponseData> {
|
||||
return new Promise<ResponseData>((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<string, string> = {};
|
||||
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<string, string | string[]> = {};
|
||||
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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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";
|
||||
@@ -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<string, string | string[]> | null;
|
||||
constructor(
|
||||
message?: string,
|
||||
statusCode?: number,
|
||||
headers?: Record<string, string | string[]> | 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<string, string | string[]>;
|
||||
constructor(
|
||||
message: string,
|
||||
code: number,
|
||||
options: { headers?: Record<string, string | string[]>; 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<string, string | string[]> | null;
|
||||
constructor(
|
||||
message: string,
|
||||
code: number,
|
||||
options: { headers?: Record<string, string | string[]> | 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;
|
||||
}
|
||||
}
|
||||
@@ -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<string, string>;
|
||||
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<Response> {
|
||||
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<Response> {
|
||||
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<string, string[]>;
|
||||
|
||||
constructor(from: HeaderList | null) {
|
||||
this.names = from !== null ? from.names.slice() : [];
|
||||
this.values = new Map<string, string[]>();
|
||||
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<string, string> | 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<RawResponse> {
|
||||
return new Promise<RawResponse>((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,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -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<Dispatcher, boolean>;
|
||||
__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<Dispatcher, boolean>();
|
||||
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<void> {
|
||||
if (this.__queue.length === 0) {
|
||||
return Promise.all(this.__clients.map((c) => c.close())).then(() => {});
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
this.__closedResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
__destroy(err: Error): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<string> {
|
||||
return this.__consume().then((bytes) => decodeText(bytes, this.__profile));
|
||||
}
|
||||
|
||||
// undici declares json() as Promise<any>: 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<any> {
|
||||
return this.__consume().then((bytes) => JSON.parse(decodeText(bytes, this.__profile)));
|
||||
}
|
||||
|
||||
arrayBuffer(): Promise<ArrayBuffer> {
|
||||
return this.__consume().then((bytes) => {
|
||||
const out = new ArrayBuffer(bytes.length);
|
||||
new Uint8Array(out).set(bytes);
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
bytes(): Promise<Uint8Array> {
|
||||
return this.__consume().then((bytes) => new Uint8Array(bytes));
|
||||
}
|
||||
|
||||
blob(): Promise<Blob> {
|
||||
return this.__consume().then((bytes) => new Blob([bytes], { type: this.__contentType }));
|
||||
}
|
||||
|
||||
formData(): Promise<never> {
|
||||
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<void> {
|
||||
let limit =
|
||||
opts !== undefined && Number.isFinite(opts.limit)
|
||||
? opts.limit
|
||||
: this.__profile.major >= 7
|
||||
? 131072
|
||||
: 262144;
|
||||
this.__consuming = true;
|
||||
return new Promise<void>((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<Buffer> {
|
||||
if (this.bodyUsed) return Promise.reject(new TypeError("unusable"));
|
||||
this.__consuming = true;
|
||||
return new Promise<Buffer>((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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<string, string | string[] | undefined>;
|
||||
|
||||
/** What a request's `headers` option accepts here: a record or a flat
|
||||
* [name, value, ...] array. */
|
||||
export type UndiciHeaders = Record<string, string | string[] | undefined> | 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;
|
||||
}
|
||||
@@ -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=<s>` 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;
|
||||
}
|
||||
@@ -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<ResponseData> {
|
||||
return coreRequest(url, options, () => new Agent(), profile);
|
||||
}
|
||||
|
||||
export function fetch(input: string | URL, init?: UndiciRequestInit): Promise<Response> {
|
||||
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 };
|
||||
@@ -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<ResponseData> {
|
||||
return coreRequest(url, options, () => new Agent(), profile);
|
||||
}
|
||||
|
||||
export function fetch(input: string | URL, init?: UndiciRequestInit): Promise<Response> {
|
||||
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 };
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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<string> };
|
||||
}
|
||||
|
||||
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<string> = 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/<name>@<version>/
|
||||
* node_modules/<name>/<file>`), 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! };
|
||||
}
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
@@ -479,7 +480,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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<ts.SourceFile, readonly CycleEdge[]> | undefined;
|
||||
inlineCycleCalls?: Set<ts.Node>;
|
||||
}
|
||||
|
||||
/** 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<ts.SourceFile, CycleEdge[]>();
|
||||
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<ts.SourceFile, "visiting" | "done">,
|
||||
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<ts.SourceFile, "visiting" | "done">();
|
||||
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<ts.SourceFile>();
|
||||
for (;;) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<ts.Node> | undefined;
|
||||
/** LoadResult.moduleEdges. */
|
||||
moduleEdges?: ReadonlyMap<ts.SourceFile, readonly CycleEdge[]> | 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<ts.Node>(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,
|
||||
@@ -11987,7 +11995,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);
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
/** The answer `typeof <operand>` 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;
|
||||
}
|
||||
@@ -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,
|
||||
@@ -3346,6 +3352,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
|
||||
@@ -3396,7 +3410,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);
|
||||
}
|
||||
|
||||
@@ -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("<aliased package>[/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);
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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<string>();
|
||||
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<string>): 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("<builtin>")` (readable-stream's
|
||||
* internal/streams/stream.js). Requiring the chunk is then requiring the
|
||||
|
||||
@@ -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{");
|
||||
});
|
||||
|
||||
@@ -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<ts.VariableDecla
|
||||
return edits;
|
||||
}
|
||||
|
||||
/** True when `id` roots the member chain a `new` expression constructs. */
|
||||
function inNewCallee(id: ts.Identifier): boolean {
|
||||
let e: ts.Node = id;
|
||||
for (let p = e.parent; p !== undefined; e = p, p = p.parent) {
|
||||
if ((ts.isPropertyAccessExpression(p) || ts.isElementAccessExpression(p)) && p.expression === e)
|
||||
continue;
|
||||
return ts.isNewExpression(p) && p.expression === e;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The elements of an expression statement's comma chain, in order. */
|
||||
function commaElements(e: ts.Expression): ts.Expression[] {
|
||||
const out: ts.Expression[] = [];
|
||||
@@ -264,10 +290,6 @@ export function lowerBundledEsmInits(
|
||||
if (!source.includes("__esm")) return null;
|
||||
if (!filePath.endsWith(".cjs") && ts.isExternalModule(sf)) return null;
|
||||
|
||||
interface Top {
|
||||
stmt: ts.Statement;
|
||||
node: ts.VariableDeclaration | ts.FunctionDeclaration;
|
||||
}
|
||||
const top = new Map<string, Top>();
|
||||
const duplicate = new Set<string>();
|
||||
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<string, Init>();
|
||||
const inits = new Map<string, InitBody>();
|
||||
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<ts.VariableDeclaration>();
|
||||
const interopSpecs = new Map<string, string>();
|
||||
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<string> = 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<string, InitBody>,
|
||||
top: ReadonlyMap<string, Top>,
|
||||
duplicate: ReadonlySet<string>,
|
||||
writes: ReadonlyMap<string, number>,
|
||||
refsByName: ReadonlyMap<string, ts.Identifier[]>,
|
||||
interopSpecs: ReadonlyMap<string, string>,
|
||||
): Map<string, { edits: Edit[]; declarators: ts.VariableDeclaration[] }> {
|
||||
const out = new Map<string, { edits: Edit[]; declarators: ts.VariableDeclaration[] }>();
|
||||
const hoistedNames = new Set<string>();
|
||||
/** 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<ts.Expression>();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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<string, string | number>;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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<string, string | null>();
|
||||
|
||||
/** The always-on program-file half of the builtin aliases: a CommonJS
|
||||
* program file's `require("<polyfill package>")` 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,
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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/<name>/v<major>.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<number, string>;
|
||||
/** 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<string, ImplementationShim> = 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 `…/<name>@<version>/…/<name>/<file>`), 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
|
||||
* (`<bundle>.<binding>.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<string, string>();
|
||||
|
||||
/** 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<string>();
|
||||
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<string> = 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<string>();
|
||||
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<string, unknown>)["scriptc"]
|
||||
: undefined;
|
||||
const map =
|
||||
config !== null && typeof config === "object"
|
||||
? (config as Record<string, unknown>)["packages"]
|
||||
: undefined;
|
||||
if (map !== null && typeof map === "object") {
|
||||
for (const [name, kind] of Object.entries(map as Record<string, unknown>)) {
|
||||
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<string, string | null>();
|
||||
|
||||
/** 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();
|
||||
}
|
||||
@@ -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<SourceFile>): void {
|
||||
const queue = [...scanned];
|
||||
const added = new Set<SourceFile>();
|
||||
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,
|
||||
|
||||
@@ -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<ts.Node>;
|
||||
/** 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<ts.SourceFile, readonly CycleEdge[]> | 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<T extends LoadResult>(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<ts.Node>;
|
||||
moduleEdges?: ReadonlyMap<ts.SourceFile, readonly CycleEdge[]>;
|
||||
} {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -2473,6 +2473,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
|
||||
@@ -3006,11 +3016,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") ||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6250-string-of-string-or-url.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/packages/compiler/shims/fs-extra/core/api.ts",
|
||||
"<repo>/packages/compiler/shims/fs-extra/index.ts",
|
||||
"<repo>/tests/corpus/fs-extra-package/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/global-detection-typeof.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/readable-stream-3/main.cjs"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Could not find a declaration file for module 'readable-stream'. '<repo>/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": "<repo>/tests/corpus/readable-stream-3/main.cjs",
|
||||
"start": 554,
|
||||
"end": 571
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC1010",
|
||||
"message": "the 'readable-stream' module is not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/readable-stream-disable/main.cjs"
|
||||
],
|
||||
"diags": [
|
||||
{
|
||||
"code": "SC0001",
|
||||
"message": "Could not find a declaration file for module 'readable-stream'. '<repo>/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": "<repo>/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'. '<repo>/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": "<repo>/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'. '<repo>/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": "<repo>/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'. '<repo>/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": "<repo>/tests/corpus/readable-stream-disable/main.cjs",
|
||||
"start": 736,
|
||||
"end": 762
|
||||
}
|
||||
},
|
||||
{
|
||||
"code": "SC1010",
|
||||
"message": "the 'readable-stream' module is not supported yet",
|
||||
"loc": {
|
||||
"file": "<repo>/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": "<repo>/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": "<repo>/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": "<repo>/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"
|
||||
}
|
||||
]
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/2585-builtin-polyfill-aliases/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/2586-readable-stream-builtin/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/2587-bundled-polyfill-aliases/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/2588-bundled-esm-classes/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/2589-bundled-error-subclass/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/2590-bundled-fs-extra/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/packages/compiler/shims/undici/core/errors.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/connector.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/diagnostics.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/readable.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/util.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/dispatcher.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/request.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/timers.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/client.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/pool.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/agent.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/api.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/fetch.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/errors-v5.ts",
|
||||
"<repo>/packages/compiler/shims/undici/v5.ts",
|
||||
"<repo>/tests/fixtures/npm/cases/undici5/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/packages/compiler/shims/undici/core/errors.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/connector.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/diagnostics.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/readable.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/util.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/dispatcher.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/request.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/timers.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/client.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/pool.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/agent.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/api.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/fetch.ts",
|
||||
"<repo>/packages/compiler/shims/undici/core/errors-v7.ts",
|
||||
"<repo>/packages/compiler/shims/undici/v7.ts",
|
||||
"<repo>/tests/fixtures/npm/cases/undici7/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/unloaded-optional-dependency/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/workspace-dynamic-import-deps/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/workspace-dynamic-import/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
resolveTypeDirective,
|
||||
setProjectPathMappings,
|
||||
} from "../../src/frontend/resolve.js";
|
||||
import { packageShimFor } from "../../src/frontend/package-shims.js";
|
||||
import { isRelativeSpecifier } from "../../src/frontend/workspace-registry.js";
|
||||
import { options5 } from "./harness.js";
|
||||
|
||||
@@ -138,14 +139,21 @@ test("relative and bare specifiers across the whole test tree resolve identicall
|
||||
// the resolver follows Node — corpus 2124 pins the behavior
|
||||
// differentially, so parity has nothing to check here.
|
||||
if (spec.startsWith("#/")) continue;
|
||||
// A package scriptc implements itself (package-shims.ts) resolves
|
||||
// to its built-in module by design; 5.9.3 answers the installed
|
||||
// package.
|
||||
if (packageShimFor(file, spec) !== null) continue;
|
||||
bare++;
|
||||
// PROJECT imports (#alias / self-name): ts5's NON-external answers
|
||||
// for bare specifiers are exactly the package.json-mediated
|
||||
// resolutions inside the project — resolveProjectImport must agree.
|
||||
// A package scriptc implements itself (package-shims.ts) answers its
|
||||
// shim module where 5.9.3 answers the installed package, so only the
|
||||
// external-library answer below is compared for it.
|
||||
const oursProj = resolveProjectImport(file, spec);
|
||||
const theirsProj =
|
||||
reference && !reference.isExternalLibraryImport ? reference.resolvedFileName : null;
|
||||
if (oursProj !== theirsProj) {
|
||||
if (oursProj !== theirsProj && packageShimFor(file, spec) === null) {
|
||||
failures.push(`${file} '${spec}' (project): ours=${oursProj} ts5=${theirsProj}`);
|
||||
}
|
||||
const ours = resolveBareModule(file, spec);
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/* Message framing in the undici shim (packages/compiler/shims/undici),
|
||||
* against the real undici 5.12 and 7.30 the undici5/undici7 npm fixtures
|
||||
* vendor:
|
||||
*
|
||||
* - a chunk-size line is read as it arrives: an extension streamed in many
|
||||
* pieces is skipped without keeping its bytes, and an oversized chunk
|
||||
* size fails the response;
|
||||
* - a request with two Content-Length fields is rejected in both profiles
|
||||
* before anything is written.
|
||||
*/
|
||||
import { createServer, type Server, type Socket } from "node:net";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
||||
import * as shim5 from "../shims/undici/v5.ts";
|
||||
import * as shim7 from "../shims/undici/v7.ts";
|
||||
|
||||
const repoRoot = join(import.meta.dirname, "../../..");
|
||||
const real7 = (await import(
|
||||
join(repoRoot, "tests/fixtures/npm/cases/undici7/node_modules/undici/index.js")
|
||||
)) as typeof shim7;
|
||||
const real5 = (await import(
|
||||
join(repoRoot, "tests/fixtures/npm/cases/undici5/node_modules/undici/index.js")
|
||||
)) as typeof shim7;
|
||||
|
||||
type Respond = (socket: Socket) => void;
|
||||
let respond: Respond = () => {};
|
||||
let server: Server;
|
||||
let origin = "";
|
||||
|
||||
beforeAll(async () => {
|
||||
server = createServer((socket) => {
|
||||
socket.once("data", () => respond(socket));
|
||||
socket.on("error", () => {});
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
if (address === null || typeof address === "string") throw new Error("no port");
|
||||
origin = `http://127.0.0.1:${address.port}`;
|
||||
});
|
||||
|
||||
afterAll(() => new Promise<void>((resolve) => server.close(() => resolve())));
|
||||
|
||||
/** The body text, or the error's name, code and message. */
|
||||
async function outcome(undici: typeof shim7, path: string): Promise<string> {
|
||||
const agent = new undici.Agent();
|
||||
try {
|
||||
const res = await undici.request(`${origin}${path}`, { dispatcher: agent });
|
||||
return `body ${await res.body.text()}`;
|
||||
} catch (err) {
|
||||
const e = err as Error & { code?: string };
|
||||
return `${e.name} ${e.code ?? ""} ${e.message}`;
|
||||
} finally {
|
||||
await agent.close();
|
||||
}
|
||||
}
|
||||
|
||||
const HEAD = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n";
|
||||
|
||||
describe("undici shim chunked framing", () => {
|
||||
test("a chunk extension streamed in many pieces is skipped", async () => {
|
||||
// The bytes the shim's parser holds while the extension streams in.
|
||||
let parser: { buf: Buffer; queue: Buffer[] } | null = null;
|
||||
let retained = 0;
|
||||
respond = (socket) => {
|
||||
socket.write(HEAD + "5;name=");
|
||||
let left = 2048;
|
||||
const piece = "x".repeat(1024);
|
||||
const next = (): void => {
|
||||
if (parser !== null) {
|
||||
const held = parser.buf.length + parser.queue.reduce((n, b) => n + b.length, 0);
|
||||
retained = Math.max(retained, held);
|
||||
}
|
||||
if (left-- > 0) {
|
||||
socket.write(piece);
|
||||
setImmediate(next);
|
||||
} else {
|
||||
socket.end("\r\nhello\r\n0\r\n\r\n");
|
||||
}
|
||||
};
|
||||
next();
|
||||
};
|
||||
const expected = await outcome(real7, "/extension");
|
||||
expect(expected).toBe("body hello");
|
||||
|
||||
const client = new shim7.Client(origin) as unknown as {
|
||||
__parser: { buf: Buffer; queue: Buffer[] } | null;
|
||||
request(options: object): Promise<{ body: { text(): Promise<string> } }>;
|
||||
close(): Promise<void>;
|
||||
};
|
||||
const pending = client.request({ path: "/extension", method: "GET" });
|
||||
const watch = setInterval(() => {
|
||||
if (client.__parser !== null) parser = client.__parser;
|
||||
}, 0);
|
||||
const res = await pending;
|
||||
expect(await res.body.text()).toBe("hello");
|
||||
clearInterval(watch);
|
||||
await client.close();
|
||||
expect(parser).not.toBeNull();
|
||||
expect(retained).toBeLessThan(64 * 1024);
|
||||
}, 60_000);
|
||||
|
||||
for (const [name, real, undici] of [
|
||||
["5.x", real5, shim5 as unknown as typeof shim7],
|
||||
["7.x", real7, shim7],
|
||||
] as const) {
|
||||
test(`${name}: an oversized chunk size fails the response`, async () => {
|
||||
respond = (socket) => socket.end(HEAD + "fffffffffffffffffff\r\n");
|
||||
const expected = await outcome(real, "/overflow");
|
||||
expect(expected).toContain("Chunk size overflow");
|
||||
expect(await outcome(undici, "/overflow")).toBe(expected);
|
||||
});
|
||||
|
||||
test(`${name}: an invalid chunk-size character fails the response`, async () => {
|
||||
respond = (socket) => socket.end(HEAD + "5z\r\nhello\r\n0\r\n\r\n");
|
||||
const expected = await outcome(real, "/invalid");
|
||||
expect(expected).toContain("Invalid character in chunk size");
|
||||
expect(await outcome(undici, "/invalid")).toBe(expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("undici shim Content-Length", () => {
|
||||
const headers = { "content-length": "5", "Content-Length": "5" };
|
||||
for (const [name, undici] of [
|
||||
["5.x", shim5 as unknown as typeof shim7],
|
||||
["7.x", shim7],
|
||||
] as const) {
|
||||
test(`${name} rejects a duplicate Content-Length`, async () => {
|
||||
let wrote = false;
|
||||
respond = (socket) => {
|
||||
wrote = true;
|
||||
socket.end("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n");
|
||||
};
|
||||
const agent = new undici.Agent();
|
||||
const err = await undici
|
||||
.request(`${origin}/duplicate`, {
|
||||
dispatcher: agent,
|
||||
method: "POST",
|
||||
headers,
|
||||
body: "hello",
|
||||
})
|
||||
.then(
|
||||
() => null,
|
||||
(e: Error & { code?: string }) => e,
|
||||
);
|
||||
await agent.close();
|
||||
expect(err?.message).toBe("duplicate content-length header");
|
||||
expect(err?.code).toBe("UND_ERR_INVALID_ARG");
|
||||
expect(wrote).toBe(false);
|
||||
});
|
||||
}
|
||||
|
||||
test("real undici 7.x rejects it with the same error", async () => {
|
||||
const agent = new real7.Agent();
|
||||
const err = await real7
|
||||
.request(`${origin}/duplicate`, { dispatcher: agent, method: "POST", headers, body: "hello" })
|
||||
.then(
|
||||
() => null,
|
||||
(e: Error & { code?: string }) => e,
|
||||
);
|
||||
await agent.close();
|
||||
expect(err?.message).toBe("duplicate content-length header");
|
||||
expect(err?.code).toBe("UND_ERR_INVALID_ARG");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,331 @@
|
||||
/* The undici shim's declarations against the real package's (5.12 and 7.30,
|
||||
* vendored by the npm fixture cases undici5/undici7).
|
||||
*
|
||||
* The shim restates the subset of undici's types it implements. Two checks
|
||||
* keep the restatement honest:
|
||||
*
|
||||
* - usage conformance: one program written against undici's API (the
|
||||
* transport-style Agent subclass, request(), responses, errors, pool
|
||||
* stats, buildConnector, diagnostics message types, fetch through a
|
||||
* dispatcher) type-checks without diagnostics against the real
|
||||
* declarations AND against the shim, so code written for the implemented
|
||||
* subset keeps compiling when scriptc swaps the module;
|
||||
* - shape: every value the shim exports, every `errors` class, and every
|
||||
* member of the response/stats/message records exists in the real
|
||||
* declarations with an assignable type.
|
||||
*
|
||||
* The shim modules themselves must type-check under @types/node too (the
|
||||
* program the check builds includes them), since real projects compile
|
||||
* them with their own Node types.
|
||||
*/
|
||||
import { join } from "node:path";
|
||||
import ts5 from "typescript5";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
const repoRoot = join(import.meta.dirname, "../../..");
|
||||
const shimRoot = join(repoRoot, "packages/compiler/shims/undici");
|
||||
|
||||
function realEntry(major: number): string {
|
||||
return join(repoRoot, `tests/fixtures/npm/cases/undici${major}/node_modules/undici`);
|
||||
}
|
||||
|
||||
/** Drops the shim's implementation members (`__`-prefixed) at every
|
||||
* level, so instance types compare by their public API. */
|
||||
const helpers = `
|
||||
type Public<T> = T extends (...args: never[]) => unknown
|
||||
? T
|
||||
: T extends object
|
||||
? { [K in keyof T as K extends \`__\${string}\` ? never : K]: T[K] }
|
||||
: T;
|
||||
type Assert<T extends true> = T;
|
||||
type ExpectNever<T extends never> = T;
|
||||
type Sub<A, B> = [A] extends [B] ? true : false;
|
||||
`;
|
||||
|
||||
function usage(major: number): string {
|
||||
return `
|
||||
import type { Socket } from "node:net";
|
||||
import { channel } from "node:diagnostics_channel";
|
||||
import {
|
||||
Agent,
|
||||
Client,
|
||||
Headers,
|
||||
Pool,
|
||||
buildConnector,
|
||||
errors,
|
||||
fetch,
|
||||
getGlobalDispatcher,
|
||||
request,
|
||||
setGlobalDispatcher,
|
||||
} from "UNDICI";
|
||||
import type { DiagnosticsChannel, Dispatcher } from "UNDICI";
|
||||
|
||||
interface TransportOptions {
|
||||
connections?: number;
|
||||
keepAliveTimeout?: number;
|
||||
connect?: buildConnector.BuildOptions;
|
||||
}
|
||||
|
||||
function wrap(options?: buildConnector.BuildOptions) {
|
||||
const connect = buildConnector(options ?? {}) as (
|
||||
options: buildConnector.Options,
|
||||
callback: buildConnector.Callback,
|
||||
) => Socket;
|
||||
return (params: buildConnector.Options, callback: buildConnector.Callback) => {
|
||||
const socket = connect(params, (error, connected) => {
|
||||
if (error) callback(error, null);
|
||||
else callback(null, connected);
|
||||
});
|
||||
socket.once("connect", () => {});
|
||||
return socket;
|
||||
};
|
||||
}
|
||||
|
||||
class Transport extends Agent {
|
||||
private readonly pools: Map<string, Pool>;
|
||||
constructor(options: TransportOptions = {}) {
|
||||
const pools = new Map<string, Pool>();
|
||||
super({
|
||||
factory(origin) {
|
||||
const pool = new Pool(origin, {
|
||||
connect: wrap(options.connect),
|
||||
connections: options.connections,
|
||||
keepAliveTimeout: options.keepAliveTimeout,
|
||||
pipelining: 1,
|
||||
interceptors: {
|
||||
Client: [
|
||||
(dispatch) => (params, handler) => {
|
||||
const opaque = (params as Dispatcher.RequestOptions).opaque;
|
||||
return opaque === undefined ? dispatch(params, handler) : dispatch(params, handler);
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
pools.set(String(origin), pool);
|
||||
return pool;
|
||||
},
|
||||
});
|
||||
this.pools = pools;
|
||||
}
|
||||
counts(origin: string): number {
|
||||
const stats = this.pools.get(origin)?.stats;
|
||||
return (stats?.connected ?? 0) + (stats?.free ?? 0) + (stats?.pending ?? 0) +
|
||||
(stats?.queued ?? 0) + (stats?.running ?? 0) + (stats?.size ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
class Counting extends Agent {
|
||||
count = 0;
|
||||
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.${major === 7 ? "DispatchHandler" : "DispatchHandlers"}): boolean {
|
||||
this.count++;
|
||||
return super.dispatch(options, handler);
|
||||
}
|
||||
}
|
||||
|
||||
export async function run(url: string, lookup: import("node:net").LookupFunction): Promise<string> {
|
||||
const transport = new Transport({ connections: 2 });
|
||||
const response: Dispatcher.ResponseData${major === 7 ? "<unknown>" : ""} = await request(url, {
|
||||
dispatcher: transport,
|
||||
opaque: { label: "x" },
|
||||
body: "payload",
|
||||
bodyTimeout: 1000,
|
||||
headers: { "x-test": "1", authorization: "Ticket t" },
|
||||
headersTimeout: 1000,
|
||||
method: "POST",
|
||||
signal: AbortSignal.timeout(1000),
|
||||
});
|
||||
const value = response.headers["content-type"];
|
||||
const header = Array.isArray(value) ? value.join(", ") : value === undefined ? null : String(value);
|
||||
const status: number = response.statusCode;
|
||||
const text: string = await response.body.text();
|
||||
const parsed = (await response.body.json()) as { ok: boolean };
|
||||
const bytes: ArrayBuffer = await response.body.arrayBuffer();
|
||||
response.body.on("error", () => {});
|
||||
response.body.destroy();
|
||||
const counting = new Counting();
|
||||
await counting.close();
|
||||
const client = new Client(url, { keepAliveTimeout: 1000, headersTimeout: 1000 });
|
||||
await client.destroy();
|
||||
const pool = new Pool(url, { connections: 4 });
|
||||
const viaPool = await pool.request({ path: "/x", method: "GET", opaque: null });
|
||||
const safe = new Agent({ connect: { lookup } });
|
||||
const fetched = await fetch(url, {
|
||||
dispatcher: safe,
|
||||
method: "GET",
|
||||
headers: { a: "b" },
|
||||
redirect: "manual",
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
const fetchedText: string = await fetched.text();
|
||||
setGlobalDispatcher(transport);
|
||||
const global = getGlobalDispatcher();
|
||||
let code = "";
|
||||
try {
|
||||
await request(url);
|
||||
} catch (err) {
|
||||
if (err instanceof errors.HeadersTimeoutError || err instanceof errors.BodyTimeoutError) code = err.code;
|
||||
if (err instanceof errors.UndiciError || err instanceof errors.SocketError) code += "?";
|
||||
if (err instanceof errors.RequestAbortedError || err instanceof errors.InvalidArgumentError) code += ".";
|
||||
}
|
||||
channel("undici:request:headers").subscribe((message) => {
|
||||
const m = message as DiagnosticsChannel.RequestHeadersMessage;
|
||||
code += m.response.statusCode;
|
||||
});
|
||||
channel("undici:client:sendHeaders").subscribe((message) => {
|
||||
const m = message as DiagnosticsChannel.ClientSendHeadersMessage;
|
||||
code += m.headers;
|
||||
});
|
||||
return [header, status, text, parsed.ok, bytes.byteLength, viaPool.statusCode, fetched.status,
|
||||
fetchedText, global === transport, code, transport.counts(url)].join("${major}");
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
/** Names the shim declares beyond a version's published declarations,
|
||||
* each because undici's runtime has it although its .d.ts does not (5.12's
|
||||
* declarations predate several runtime exports and options) or because the
|
||||
* shim shares one declaration between the 5.x and 7.x entries. */
|
||||
const tolerated: Record<number, Record<string, string[]>> = {
|
||||
5: {
|
||||
// Exported by lib/core/errors.js 5.12, missing from types/errors.d.ts.
|
||||
errors: [
|
||||
"ConnectTimeoutError",
|
||||
"HeadersOverflowError",
|
||||
"InvalidReturnValueError",
|
||||
"ResponseContentLengthMismatchError",
|
||||
"HTTPParserError",
|
||||
"BalancedPoolMissingUpstreamError",
|
||||
],
|
||||
// 7.x request options in the shared DispatchOptions; 5.12 ignores them.
|
||||
// The RequestOptions members ride in DispatchOptions (see core/types.ts).
|
||||
dispatch: ["reset", "blocking", "opaque", "signal", "onInfo", "highWaterMark"],
|
||||
// Accepted by the 5.12 Client/Pool constructors, undeclared in 5.12.
|
||||
options: ["localAddress", "connectTimeout"],
|
||||
// 5.12 declares the connector's port as a number; it passes the URL's
|
||||
// port string (7.x declares string).
|
||||
connector: ["port"],
|
||||
},
|
||||
7: {
|
||||
// lib/core/errors.js 7.30 exports AbortError; types/errors.d.ts omits it.
|
||||
errors: ["AbortError"],
|
||||
dispatch: ["opaque", "signal", "onInfo", "highWaterMark"],
|
||||
options: [],
|
||||
connector: [],
|
||||
},
|
||||
};
|
||||
|
||||
const union = (names: string[]): string =>
|
||||
names.length === 0 ? "never" : names.map((n) => JSON.stringify(n)).join(" | ");
|
||||
|
||||
function shape(major: number): string {
|
||||
const t = tolerated[major]!;
|
||||
return `
|
||||
import * as real from "REAL";
|
||||
import * as shim from "SHIM";
|
||||
import type { Dispatcher as RealDispatcher, DiagnosticsChannel as RealDC } from "REAL";
|
||||
import type { Dispatcher as ShimDispatcher, DiagnosticsChannel as ShimDC } from "SHIM";
|
||||
|
||||
type _values = ExpectNever<Exclude<keyof typeof shim, keyof typeof real>>;
|
||||
type _errors = ExpectNever<Exclude<keyof typeof shim.errors, keyof typeof real.errors | ${union(t.errors!)}>>;
|
||||
type SharedErrors = Extract<keyof typeof shim.errors, keyof typeof real.errors>;
|
||||
type Instance<C> = C extends abstract new (...args: never[]) => infer I ? I : never;
|
||||
type ErrorFields<I> = Pick<I, Extract<keyof I, "name" | "message" | "code">>;
|
||||
// The fields the real class declares (5.12 declares no UndiciError.code).
|
||||
type _errorInstances = ExpectNever<{
|
||||
[K in SharedErrors]: Sub<
|
||||
ErrorFields<Instance<(typeof real.errors)[K]>>,
|
||||
Pick<Instance<(typeof shim.errors)[K]>, keyof ErrorFields<Instance<(typeof real.errors)[K]>>>
|
||||
> extends true
|
||||
? never
|
||||
: K;
|
||||
}[SharedErrors]>;
|
||||
|
||||
// Records the shim hands to programs: each member of the real record is
|
||||
// assignable to the shim's.
|
||||
type RealResponse = RealDispatcher.ResponseData;
|
||||
type ShimResponse = ShimDispatcher.ResponseData;
|
||||
type ResponseKeys = Exclude<keyof RealResponse, "body">;
|
||||
type _responseKeys = ExpectNever<Exclude<ResponseKeys, keyof ShimResponse>>;
|
||||
type _response = ExpectNever<{
|
||||
[K in Extract<ResponseKeys, keyof ShimResponse>]: Sub<RealResponse[K], ShimResponse[K]> extends true ? never : K;
|
||||
}[Extract<ResponseKeys, keyof ShimResponse>]>;
|
||||
// The body mixin (the Readable half is node:stream's on both sides).
|
||||
type BodyApi = "text" | "json" | "arrayBuffer" | "blob" | "bodyUsed" | "dump";
|
||||
type RealBody = RealResponse["body"];
|
||||
type ShimBody = ShimResponse["body"];
|
||||
type _bodyKeys = ExpectNever<Exclude<BodyApi, keyof ShimBody>>;
|
||||
type _body = ExpectNever<{
|
||||
[K in BodyApi]: Sub<RealBody[K], ShimBody[K]> extends true ? never : K;
|
||||
}[BodyApi]>;
|
||||
type StatsApi = "connected" | "free" | "pending" | "queued" | "running" | "size";
|
||||
type _stats = Assert<Sub<Pick<real.Pool["stats"], StatsApi>, Pick<shim.Pool["stats"], StatsApi>>>;
|
||||
type _headersMessage = Assert<
|
||||
Sub<RealDC.RequestHeadersMessage["response"], ShimDC.RequestHeadersMessage["response"]>
|
||||
>;
|
||||
type ConnectorKeys = Exclude<keyof real.buildConnector.Options, ${union(t.connector!)}>;
|
||||
type SharedConnectorKeys = Extract<ConnectorKeys, keyof shim.buildConnector.Options>;
|
||||
type _connectorOptions = ExpectNever<{
|
||||
[K in SharedConnectorKeys]: Sub<real.buildConnector.Options[K], shim.buildConnector.Options[K]> extends true
|
||||
? never
|
||||
: K;
|
||||
}[SharedConnectorKeys]>;
|
||||
// Option records programs pass in: the shim's accepted options are a subset.
|
||||
type _dispatchKeys = ExpectNever<
|
||||
Exclude<keyof ShimDispatcher.DispatchOptions, keyof RealDispatcher.DispatchOptions | ${union(t.dispatch!)}>
|
||||
>;
|
||||
type _clientKeys = ExpectNever<Exclude<keyof shim.Client.Options, keyof real.Client.Options | ${union(t.options!)}>>;
|
||||
type _poolKeys = ExpectNever<Exclude<keyof shim.Pool.Options, keyof real.Pool.Options | ${union(t.options!)}>>;
|
||||
// Class APIs: every public member the shim declares exists on the real class.
|
||||
type _agentApi = ExpectNever<Exclude<keyof Public<shim.Agent>, keyof real.Agent>>;
|
||||
type _poolApi = ExpectNever<Exclude<keyof Public<shim.Pool>, keyof real.Pool>>;
|
||||
type _clientApi = ExpectNever<Exclude<keyof Public<shim.Client>, keyof real.Client>>;
|
||||
`;
|
||||
}
|
||||
|
||||
function check(files: Record<string, string>): string[] {
|
||||
const options: ts5.CompilerOptions = {
|
||||
strict: true,
|
||||
noEmit: true,
|
||||
skipLibCheck: true,
|
||||
allowImportingTsExtensions: true,
|
||||
module: ts5.ModuleKind.ESNext,
|
||||
moduleResolution: ts5.ModuleResolutionKind.Bundler,
|
||||
target: ts5.ScriptTarget.ES2022,
|
||||
lib: ["lib.es2023.d.ts"],
|
||||
types: ["node"],
|
||||
typeRoots: [join(repoRoot, "node_modules/@types")],
|
||||
};
|
||||
const host = ts5.createCompilerHost(options);
|
||||
const readFile = host.readFile.bind(host);
|
||||
const fileExists = host.fileExists.bind(host);
|
||||
host.readFile = (name) => files[name] ?? readFile(name);
|
||||
host.fileExists = (name) => name in files || fileExists(name);
|
||||
const program = ts5.createProgram(Object.keys(files), options, host);
|
||||
return ts5.getPreEmitDiagnostics(program).map((d) => {
|
||||
const where = d.file
|
||||
? `${d.file.fileName}:${d.file.getLineAndCharacterOfPosition(d.start ?? 0).line + 1}`
|
||||
: "";
|
||||
return `${where} ${ts5.flattenDiagnosticMessageText(d.messageText, "\n")}`;
|
||||
});
|
||||
}
|
||||
|
||||
describe.each([5, 7])("undici %i.x shim types", (major) => {
|
||||
const real = realEntry(major);
|
||||
const shim = join(shimRoot, `v${major}.ts`);
|
||||
|
||||
test("a program written against undici type-checks against the real declarations", () => {
|
||||
const file = join(repoRoot, `.undici-usage-real-${major}.ts`);
|
||||
expect(check({ [file]: helpers + usage(major).replace(/UNDICI/g, real) })).toEqual([]);
|
||||
});
|
||||
|
||||
test("the same program type-checks against the shim", () => {
|
||||
const file = join(repoRoot, `.undici-usage-shim-${major}.ts`);
|
||||
expect(check({ [file]: helpers + usage(major).replace(/UNDICI/g, shim) })).toEqual([]);
|
||||
});
|
||||
|
||||
test("the shim's exports and records are a compatible subset", () => {
|
||||
const file = join(repoRoot, `.undici-shape-${major}.ts`);
|
||||
const text = helpers + shape(major).replace(/REAL/g, real).replace(/SHIM/g, shim);
|
||||
expect(check({ [file]: text })).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -74,6 +74,10 @@ export function stageNativeCliAssets(options: {
|
||||
for (const name of ["scriptc.d.ts", "scriptc-overrides.d.ts", "scriptc-node-fallback.d.ts"]) {
|
||||
cpSync(join(repository, "packages/compiler/ambient", name), join(declarations, name));
|
||||
}
|
||||
// Package shims (packageShimRoot): the built-in npm package implementations.
|
||||
cpSync(join(repository, "packages/compiler/shims"), join(declarations, "shims"), {
|
||||
recursive: true,
|
||||
});
|
||||
// This optional Node host runs the built WASI module, never the compiler.
|
||||
const wasi = join(lib, "wasi");
|
||||
mkdirSync(join(wasi, "cli"), { recursive: true });
|
||||
|
||||
@@ -30,6 +30,7 @@ const ALLOWED_TYPESCRIPT5_IMPORTS = new Set([
|
||||
"packages/compiler/src/library/semantic-source.ts",
|
||||
"packages/compiler/test/ts7/harness.ts",
|
||||
"packages/compiler/test/ts7/resolver-parity.test.ts",
|
||||
"packages/compiler/test/undici-shim-types.test.ts",
|
||||
]);
|
||||
|
||||
function sourceFilesUnder(dir) {
|
||||
|
||||
@@ -4,6 +4,6 @@ try {
|
||||
require("scriptc-test-definitely-not-installed");
|
||||
status = "installed";
|
||||
} catch (e) {
|
||||
status = "missing: " + String(e.message).split("\n")[0];
|
||||
status = "missing: " + e.code + " " + String(e.message).split("\n")[0];
|
||||
}
|
||||
exports.probe = function probe() { return status; };
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// String(), template literals and concatenation over a `string | URL`
|
||||
// union: the string arm passes through and the URL arm converts through its
|
||||
// href, exactly as each would alone (an origin URL keeps its trailing
|
||||
// slash, the origin string does not).
|
||||
function key(origin: string | URL): string {
|
||||
return String(origin);
|
||||
}
|
||||
|
||||
function label(origin: string | URL | undefined): string {
|
||||
return `[${origin}]`;
|
||||
}
|
||||
|
||||
const url = new URL("http://127.0.0.1:8080");
|
||||
console.log(key("http://127.0.0.1:8080"));
|
||||
console.log(key(url));
|
||||
console.log(key(new URL("https://example.com/a/b?x=1#h")));
|
||||
console.log(label(url), label("plain"), label(undefined));
|
||||
const both: (string | URL)[] = ["a", url];
|
||||
console.log(both.map((o) => "" + o).join(" "));
|
||||
const map = new Map<string, number>();
|
||||
map.set(key(url.origin), 1);
|
||||
console.log(map.get(String(url.origin)), map.get(String(url)));
|
||||
@@ -0,0 +1,182 @@
|
||||
// fs-extra compiles from scriptc's built-in implementation; Node runs the
|
||||
// real package. node_modules vendors fs-extra 9.0.0 with its dependencies
|
||||
// (graceful-fs, jsonfile, universalify, at-least-node) and @types/fs-extra
|
||||
// 9.0.13, unmodified (MIT). The program reads fs-extra through its default
|
||||
// import, a namespace import and named imports: fs's re-exported members in
|
||||
// promise and callback form, fs.promises, streams, and the extras (mkdirs,
|
||||
// copy, remove, pathExists, the JSON helpers, outputFile, emptyDir, move),
|
||||
// the modes of binary and JSON writes, and the sync copy and move checks.
|
||||
import fs from "fs-extra";
|
||||
import * as fsx from "fs-extra";
|
||||
// Named imports use the names Node detects in fs-extra's CommonJS source.
|
||||
import { copy, outputFile, pathExists, remove } from "fs-extra";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
|
||||
function shown(root: string, text: string): string {
|
||||
return text.split(root).join("<root>");
|
||||
}
|
||||
|
||||
async function readStream(file: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let text = "";
|
||||
const stream = fs.createReadStream(file, { encoding: "utf8" });
|
||||
stream.on("data", (chunk) => {
|
||||
text += String(chunk);
|
||||
});
|
||||
stream.on("end", () => resolve(text));
|
||||
stream.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "scriptc-fs-extra-"));
|
||||
const file = path.join(root, "a.txt");
|
||||
|
||||
await fs.writeFile(file, "hello\n");
|
||||
await fs.appendFile(file, "world\n");
|
||||
console.log(JSON.stringify(await fs.readFile(file, "utf8")));
|
||||
console.log((await fs.readFile(file)).length, fs.existsSync(file), fs.existsSync(root + "-missing"));
|
||||
console.log((await fs.stat(file)).size, (await fs.lstat(file)).isFile(), fs.statSync(root).isDirectory());
|
||||
await fs.chmod(file, 0o640);
|
||||
console.log(((await fs.stat(file)).mode & 0o777).toString(8));
|
||||
|
||||
const link = path.join(root, "link.txt");
|
||||
await fs.symlink(file, link);
|
||||
console.log(shown(root, await fs.readlink(link)), (await fs.lstat(link)).isSymbolicLink());
|
||||
|
||||
await fs.ensureDir(path.join(root, "deep", "er"));
|
||||
await fs.mkdirp(path.join(root, "made", "by", "mkdirp"));
|
||||
await fs.writeFile(path.join(root, "deep", "er", "leaf.txt"), "leaf");
|
||||
console.log((await fs.readdir(root)).sort().join(","));
|
||||
|
||||
const data = path.join(root, "data.json");
|
||||
await fs.writeJson(data, { a: 1, list: [true, null, "x"] }, { spaces: 2 });
|
||||
console.log(JSON.stringify(await fs.readFile(data, "utf8")));
|
||||
console.log(JSON.stringify(await fs.readJson(data)));
|
||||
await fs.outputJson(path.join(root, "out", "nested", "o.json"), { ok: true });
|
||||
console.log(JSON.stringify(fs.readJSONSync(path.join(root, "out", "nested", "o.json"))));
|
||||
await outputFile(path.join(root, "out2", "z.txt"), "zed");
|
||||
console.log(await fs.readFile(path.join(root, "out2", "z.txt"), "utf8"));
|
||||
console.log(fs.readJsonSync(file, { throws: false }));
|
||||
fs.writeFileSync(path.join(root, "bom.json"), '\uFEFF{"bom":true}');
|
||||
console.log(JSON.stringify(fs.readJsonSync(path.join(root, "bom.json"))));
|
||||
|
||||
await fs.copy(path.join(root, "deep"), path.join(root, "copied"));
|
||||
await copy(file, path.join(root, "copied", "a-copy.txt"));
|
||||
await fsx.copy(file, path.join(root, "copied", "b-copy.txt"));
|
||||
console.log((await fs.readdir(path.join(root, "copied"))).sort().join(","));
|
||||
console.log(await fs.readFile(path.join(root, "copied", "er", "leaf.txt"), "utf8"));
|
||||
console.log(await pathExists(path.join(root, "copied", "er")), await fs.pathExists(root + "-missing"));
|
||||
try {
|
||||
await fs.copy(path.join(root, "deep"), path.join(root, "deep", "er", "inner"));
|
||||
} catch (err) {
|
||||
console.log(shown(root, (err as Error).message));
|
||||
}
|
||||
try {
|
||||
await fs.copy(file, file);
|
||||
} catch (err) {
|
||||
console.log((err as Error).message);
|
||||
}
|
||||
|
||||
await fs.move(path.join(root, "out2"), path.join(root, "moved"));
|
||||
console.log(await fs.pathExists(path.join(root, "out2")), await fs.pathExists(path.join(root, "moved", "z.txt")));
|
||||
try {
|
||||
await fs.move(path.join(root, "moved"), path.join(root, "copied"));
|
||||
} catch (err) {
|
||||
console.log((err as Error).message);
|
||||
}
|
||||
await fs.emptyDir(path.join(root, "copied"));
|
||||
console.log((await fs.readdir(path.join(root, "copied"))).length);
|
||||
|
||||
// Modes of binary and JSON writes, and the sync copy and move checks.
|
||||
await fs.writeFile(path.join(root, "bytes.bin"), Buffer.from([1, 2, 3]), { mode: 0o600 });
|
||||
await fs.outputFile(path.join(root, "out3", "bytes.bin"), new Uint8Array([4, 5]), { mode: 0o600 });
|
||||
fs.writeJsonSync(path.join(root, "private.json"), { a: 1 }, { mode: 0o600 });
|
||||
fs.outputJsonSync(path.join(root, "out4", "private.json"), { b: 2 }, { mode: 0o600 });
|
||||
for (const name of ["bytes.bin", "out3/bytes.bin", "private.json", "out4/private.json"])
|
||||
console.log(name, (fs.statSync(path.join(root, name)).mode & 0o777).toString(8));
|
||||
const selfLink = path.join(root, "self-link");
|
||||
fs.symlinkSync(path.join(root, "deep"), selfLink);
|
||||
try {
|
||||
fs.copySync(path.join(root, "deep"), path.join(selfLink, "inner"));
|
||||
} catch (err) {
|
||||
console.log(shown(root, (err as Error).message));
|
||||
}
|
||||
try {
|
||||
fs.moveSync(path.join(root, "deep"), path.join(selfLink, "inner"));
|
||||
} catch (err) {
|
||||
console.log(shown(root, (err as Error).message));
|
||||
}
|
||||
try {
|
||||
fs.moveSync(file, file, { overwrite: true });
|
||||
} catch (err) {
|
||||
console.log((err as Error).message);
|
||||
}
|
||||
console.log(fs.existsSync(file), fs.existsSync(path.join(root, "deep", "inner")));
|
||||
|
||||
// A symlink source over an existing destination entry: a regular file is
|
||||
// kept and the copy fails with EEXIST; a destination symlink is replaced.
|
||||
const linkSrc = path.join(root, "link-src");
|
||||
fs.mkdirSync(linkSrc);
|
||||
fs.symlinkSync(file, path.join(linkSrc, "entry"));
|
||||
for (const sync of [true, false]) {
|
||||
const linkDest = path.join(root, sync ? "link-dest-sync" : "link-dest-async");
|
||||
fs.mkdirSync(linkDest);
|
||||
fs.writeFileSync(path.join(linkDest, "entry"), "kept");
|
||||
try {
|
||||
if (sync) fs.copySync(linkSrc, linkDest, { overwrite: false });
|
||||
else await fs.copy(linkSrc, linkDest, { overwrite: false });
|
||||
} catch (err) {
|
||||
console.log((err as NodeJS.ErrnoException).code, shown(root, (err as Error).message));
|
||||
}
|
||||
const entry = path.join(linkDest, "entry");
|
||||
console.log(fs.readFileSync(entry, "utf8"), fs.lstatSync(entry).isSymbolicLink());
|
||||
}
|
||||
const relinkDest = path.join(root, "relink-dest");
|
||||
fs.mkdirSync(relinkDest);
|
||||
fs.symlinkSync(data, path.join(relinkDest, "entry"));
|
||||
fs.copySync(linkSrc, relinkDest);
|
||||
console.log(shown(root, fs.readlinkSync(path.join(relinkDest, "entry"))));
|
||||
|
||||
// A destination directory symlink back into the source tree: each entry
|
||||
// is checked against its destination, so the copy stops there instead
|
||||
// of replacing the source's own file.
|
||||
const loopSrc = path.join(root, "loop-src");
|
||||
fs.mkdirSync(path.join(loopSrc, "sub"), { recursive: true });
|
||||
fs.writeFileSync(path.join(loopSrc, "sub", "keep.txt"), "source");
|
||||
const loopDest = path.join(root, "loop-dest");
|
||||
fs.mkdirSync(loopDest);
|
||||
fs.symlinkSync(path.join(loopSrc, "sub"), path.join(loopDest, "sub"));
|
||||
for (const sync of [true, false]) {
|
||||
try {
|
||||
if (sync) fs.copySync(loopSrc, loopDest);
|
||||
else await fs.copy(loopSrc, loopDest);
|
||||
} catch (err) {
|
||||
console.log(shown(root, (err as Error).message));
|
||||
}
|
||||
console.log(fs.readFileSync(path.join(loopSrc, "sub", "keep.txt"), "utf8"));
|
||||
}
|
||||
|
||||
console.log(await readStream(file));
|
||||
console.log((await fs.promises.readFile(file, "utf8")).length);
|
||||
console.log(fs.readFileSync(file, "utf8").length);
|
||||
|
||||
try {
|
||||
await fs.readFile(path.join(root, "missing.txt"), "utf8");
|
||||
} catch (err) {
|
||||
console.log((err as NodeJS.ErrnoException).code);
|
||||
}
|
||||
|
||||
fs.readFile(file, "utf8", (err, text) => {
|
||||
console.log("callback", err === null, text.length);
|
||||
fs.ensureDir(path.join(root, "from-callback"), (dirErr) => {
|
||||
console.log("callback ensureDir", dirErr === null, fs.existsSync(path.join(root, "from-callback")));
|
||||
void remove(root).then(async () => {
|
||||
console.log("removed", await fs.pathExists(root));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
||||
+563
@@ -0,0 +1,563 @@
|
||||
// Type definitions for fs-extra 9.0
|
||||
// Project: https://github.com/jprichardson/node-fs-extra
|
||||
// Definitions by: Alan Agius <https://github.com/alan-agius4>,
|
||||
// midknight41 <https://github.com/midknight41>,
|
||||
// Brendan Forster <https://github.com/shiftkey>,
|
||||
// Mees van Dijk <https://github.com/mees->,
|
||||
// Justin Rockwood <https://github.com/jrockwood>,
|
||||
// Sang Dang <https://github.com/sangdth>,
|
||||
// Florian Keller <https://github.com/ffflorian>
|
||||
// Piotr Błażejewicz <https://github.com/peterblazejewicz>
|
||||
// Tiger Oakes <https://github.com/NotWoods>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// Minimum TypeScript Version: 3.9
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
import * as fs from 'fs';
|
||||
import Stats = fs.Stats;
|
||||
import PathLike = fs.PathLike;
|
||||
|
||||
export * from 'fs';
|
||||
|
||||
export function copy(src: string, dest: string, options?: CopyOptions): Promise<void>;
|
||||
export function copy(src: string, dest: string, callback: (err: Error) => void): void;
|
||||
export function copy(src: string, dest: string, options: CopyOptions, callback: (err: Error) => void): void;
|
||||
export function copySync(src: string, dest: string, options?: CopyOptionsSync): void;
|
||||
|
||||
export function copyFile(src: string, dest: string, flags?: number): Promise<void>;
|
||||
export function copyFile(src: string, dest: string, callback: (err: Error) => void): void;
|
||||
export function copyFile(src: string, dest: string, flags: number, callback: (err: Error) => void): void;
|
||||
|
||||
export function move(src: string, dest: string, options?: MoveOptions): Promise<void>;
|
||||
export function move(src: string, dest: string, callback: (err: Error) => void): void;
|
||||
export function move(src: string, dest: string, options: MoveOptions, callback: (err: Error) => void): void;
|
||||
export function moveSync(src: string, dest: string, options?: MoveOptions): void;
|
||||
|
||||
export function createFile(file: string): Promise<void>;
|
||||
export function createFile(file: string, callback: (err: Error) => void): void;
|
||||
export function createFileSync(file: string): void;
|
||||
|
||||
export function createSymlink(src: string, dest: string, type: SymlinkType): Promise<void>;
|
||||
export function createSymlink(src: string, dest: string, type: SymlinkType, callback?: (err: Error) => void): void;
|
||||
export function createSymlinkSync(src: string, dest: string, type: SymlinkType): void;
|
||||
|
||||
export function ensureDir(path: string, options?: EnsureOptions | number): Promise<void>;
|
||||
export function ensureDir(path: string, callback?: (err: Error) => void): void;
|
||||
export function ensureDir(path: string, options?: EnsureOptions | number, callback?: (err: Error) => void): void;
|
||||
export function ensureDirSync(path: string, options?: EnsureOptions | number): void;
|
||||
|
||||
export function mkdirs(dir: string): Promise<void>;
|
||||
export function mkdirs(dir: string, callback: (err: Error) => void): void;
|
||||
export function mkdirp(dir: string): Promise<void>;
|
||||
export function mkdirp(dir: string, callback: (err: Error) => void): void;
|
||||
export function mkdirsSync(dir: string): void;
|
||||
export function mkdirpSync(dir: string): void;
|
||||
|
||||
export function outputFile(
|
||||
file: string,
|
||||
data: any,
|
||||
options?: WriteFileOptions | BufferEncoding | string,
|
||||
): Promise<void>;
|
||||
export function outputFile(file: string, data: any, callback: (err: Error) => void): void;
|
||||
export function outputFile(
|
||||
file: string,
|
||||
data: any,
|
||||
options: WriteFileOptions | string,
|
||||
callback: (err: Error) => void,
|
||||
): void;
|
||||
export function outputFileSync(file: string, data: any, options?: WriteFileOptions | BufferEncoding | string): void;
|
||||
|
||||
export function readJson(file: string, options?: ReadOptions | BufferEncoding | string): Promise<any>;
|
||||
export function readJson(file: string, callback: (err: Error, jsonObject: any) => void): void;
|
||||
export function readJson(
|
||||
file: string,
|
||||
options: ReadOptions | BufferEncoding | string,
|
||||
callback: (err: Error, jsonObject: any) => void,
|
||||
): void;
|
||||
export function readJSON(file: string, options?: ReadOptions | BufferEncoding | string): Promise<any>;
|
||||
export function readJSON(file: string, callback: (err: Error, jsonObject: any) => void): void;
|
||||
export function readJSON(
|
||||
file: string,
|
||||
options: ReadOptions | BufferEncoding | string,
|
||||
callback: (err: Error, jsonObject: any) => void,
|
||||
): void;
|
||||
|
||||
export function readJsonSync(file: string, options?: ReadOptions | BufferEncoding | string): any;
|
||||
export function readJSONSync(file: string, options?: ReadOptions | BufferEncoding | string): any;
|
||||
|
||||
export function remove(dir: string, callback: (err: Error) => void): void;
|
||||
export function remove(dir: string, callback?: (err: Error) => void): Promise<void>;
|
||||
export function removeSync(dir: string): void;
|
||||
|
||||
export function outputJSON(file: string, data: any, options?: WriteOptions | BufferEncoding | string): Promise<void>;
|
||||
export function outputJSON(
|
||||
file: string,
|
||||
data: any,
|
||||
options: WriteOptions | BufferEncoding | string,
|
||||
callback: (err: Error) => void,
|
||||
): void;
|
||||
export function outputJSON(file: string, data: any, callback: (err: Error) => void): void;
|
||||
export function outputJson(file: string, data: any, options?: WriteOptions | BufferEncoding | string): Promise<void>;
|
||||
export function outputJson(
|
||||
file: string,
|
||||
data: any,
|
||||
options: WriteOptions | BufferEncoding | string,
|
||||
callback: (err: Error) => void,
|
||||
): void;
|
||||
export function outputJson(file: string, data: any, callback: (err: Error) => void): void;
|
||||
export function outputJsonSync(file: string, data: any, options?: WriteOptions | BufferEncoding | string): void;
|
||||
export function outputJSONSync(file: string, data: any, options?: WriteOptions | BufferEncoding | string): void;
|
||||
|
||||
export function writeJSON(file: string, object: any, options?: WriteOptions | BufferEncoding | string): Promise<void>;
|
||||
export function writeJSON(file: string, object: any, callback: (err: Error) => void): void;
|
||||
export function writeJSON(
|
||||
file: string,
|
||||
object: any,
|
||||
options: WriteOptions | BufferEncoding | string,
|
||||
callback: (err: Error) => void,
|
||||
): void;
|
||||
export function writeJson(file: string, object: any, options?: WriteOptions | BufferEncoding | string): Promise<void>;
|
||||
export function writeJson(file: string, object: any, callback: (err: Error) => void): void;
|
||||
export function writeJson(
|
||||
file: string,
|
||||
object: any,
|
||||
options: WriteOptions | BufferEncoding | string,
|
||||
callback: (err: Error) => void,
|
||||
): void;
|
||||
|
||||
export function writeJsonSync(file: string, object: any, options?: WriteOptions | BufferEncoding | string): void;
|
||||
export function writeJSONSync(file: string, object: any, options?: WriteOptions | BufferEncoding | string): void;
|
||||
|
||||
export function ensureFile(path: string): Promise<void>;
|
||||
export function ensureFile(path: string, callback: (err: Error) => void): void;
|
||||
export function ensureFileSync(path: string): void;
|
||||
|
||||
export function ensureLink(src: string, dest: string): Promise<void>;
|
||||
export function ensureLink(src: string, dest: string, callback: (err: Error) => void): void;
|
||||
// alias for ensureLink
|
||||
export const createLink: typeof ensureLink;
|
||||
export function ensureLinkSync(src: string, dest: string): void;
|
||||
// aliased as
|
||||
export const createLinkSync: typeof ensureLinkSync;
|
||||
|
||||
export function ensureSymlink(src: string, dest: string, type?: SymlinkType): Promise<void>;
|
||||
export function ensureSymlink(src: string, dest: string, type: SymlinkType, callback: (err: Error) => void): void;
|
||||
export function ensureSymlink(src: string, dest: string, callback: (err: Error) => void): void;
|
||||
export function ensureSymlinkSync(src: string, dest: string, type?: SymlinkType): void;
|
||||
|
||||
export function emptyDir(path: string): Promise<void>;
|
||||
export function emptyDir(path: string, callback: (err: Error) => void): void;
|
||||
export const emptydir: typeof emptyDir;
|
||||
|
||||
export function emptyDirSync(path: string): void;
|
||||
export const emptydirSync: typeof emptyDirSync;
|
||||
|
||||
export function pathExists(path: string): Promise<boolean>;
|
||||
export function pathExists(path: string, callback: (err: Error, exists: boolean) => void): void;
|
||||
export function pathExistsSync(path: string): boolean;
|
||||
|
||||
// fs async methods
|
||||
// copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/v6/index.d.ts
|
||||
|
||||
export function access(path: PathLike, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function access(path: PathLike, mode: number, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function access(path: PathLike, mode?: number): Promise<void>;
|
||||
|
||||
export function appendFile(
|
||||
file: PathLike | number,
|
||||
data: any,
|
||||
options: {
|
||||
encoding?: BufferEncoding | string | undefined;
|
||||
mode?: number | string | undefined;
|
||||
flag?: string | undefined;
|
||||
},
|
||||
callback: (err: NodeJS.ErrnoException) => void,
|
||||
): void;
|
||||
export function appendFile(file: PathLike | number, data: any, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function appendFile(
|
||||
file: PathLike | number,
|
||||
data: any,
|
||||
options?:
|
||||
| {
|
||||
encoding?: BufferEncoding | string | undefined;
|
||||
mode?: number | string | undefined;
|
||||
flag?: string | undefined;
|
||||
}
|
||||
| BufferEncoding
|
||||
| string,
|
||||
): Promise<void>;
|
||||
|
||||
export function chmod(path: PathLike, mode: Mode, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function chmod(path: PathLike, mode: Mode): Promise<void>;
|
||||
|
||||
export function chown(path: PathLike, uid: number, gid: number): Promise<void>;
|
||||
export function chown(path: PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
|
||||
export function close(fd: number, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function close(fd: number): Promise<void>;
|
||||
|
||||
export function fchmod(fd: number, mode: Mode, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function fchmod(fd: number, mode: Mode): Promise<void>;
|
||||
|
||||
export function fchown(fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function fchown(fd: number, uid: number, gid: number): Promise<void>;
|
||||
|
||||
export function fdatasync(fd: number, callback: () => void): void;
|
||||
export function fdatasync(fd: number): Promise<void>;
|
||||
|
||||
export function fstat(fd: number, callback: (err: NodeJS.ErrnoException, stats: Stats) => any): void;
|
||||
export function fstat(fd: number): Promise<Stats>;
|
||||
|
||||
export function fsync(fd: number, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function fsync(fd: number): Promise<void>;
|
||||
|
||||
export function ftruncate(fd: number, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function ftruncate(fd: number, len: number, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function ftruncate(fd: number, len?: number): Promise<void>;
|
||||
|
||||
export function futimes(fd: number, atime: number, mtime: number, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function futimes(fd: number, atime: Date, mtime: Date, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function futimes(fd: number, atime: number, mtime: number): Promise<void>;
|
||||
export function futimes(fd: number, atime: Date, mtime: Date): Promise<void>;
|
||||
|
||||
export function lchown(path: PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function lchown(path: PathLike, uid: number, gid: number): Promise<void>;
|
||||
|
||||
export function link(existingPath: PathLike, newPath: PathLike, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function link(existingPath: PathLike, newPath: PathLike): Promise<void>;
|
||||
|
||||
export function lstat(path: PathLike, callback: (err: NodeJS.ErrnoException, stats: Stats) => any): void;
|
||||
export function lstat(path: PathLike): Promise<Stats>;
|
||||
|
||||
/**
|
||||
* Asynchronous mkdir - creates the directory specified in {path}. Parameter {mode} defaults to 0777.
|
||||
*
|
||||
* @param callback No arguments other than a possible exception are given to the completion callback.
|
||||
*/
|
||||
export function mkdir(path: PathLike, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
/**
|
||||
* Asynchronous mkdir - creates the directory specified in {path}. Parameter {mode} defaults to 0777.
|
||||
*
|
||||
* @param callback No arguments other than a possible exception are given to the completion callback.
|
||||
*/
|
||||
export function mkdir(
|
||||
path: PathLike,
|
||||
options: Mode | fs.MakeDirectoryOptions | null,
|
||||
callback: (err: NodeJS.ErrnoException) => void,
|
||||
): void;
|
||||
export function mkdir(path: PathLike, options?: Mode | fs.MakeDirectoryOptions | null): Promise<void>;
|
||||
export function mkdirSync(path: PathLike, options?: Mode | fs.MakeDirectoryOptions | null): void;
|
||||
|
||||
export function open(
|
||||
path: PathLike,
|
||||
flags: string | number,
|
||||
callback: (err: NodeJS.ErrnoException, fd: number) => void,
|
||||
): void;
|
||||
export function open(
|
||||
path: PathLike,
|
||||
flags: string | number,
|
||||
mode: Mode,
|
||||
callback: (err: NodeJS.ErrnoException, fd: number) => void,
|
||||
): void;
|
||||
export function open(path: PathLike, flags: string | number, mode?: Mode | null): Promise<number>;
|
||||
|
||||
export function opendir(path: string, cb: (err: NodeJS.ErrnoException | null, dir: fs.Dir) => void): void;
|
||||
export function opendir(
|
||||
path: string,
|
||||
options: fs.OpenDirOptions,
|
||||
cb: (err: NodeJS.ErrnoException | null, dir: fs.Dir) => void,
|
||||
): void;
|
||||
export function opendir(path: string, options?: fs.OpenDirOptions): Promise<fs.Dir>;
|
||||
|
||||
export function read<TBuffer extends ArrayBufferView>(
|
||||
fd: number,
|
||||
buffer: TBuffer,
|
||||
offset: number,
|
||||
length: number,
|
||||
position: number | null,
|
||||
callback: (err: NodeJS.ErrnoException, bytesRead: number, buffer: TBuffer) => void,
|
||||
): void;
|
||||
export function read<TBuffer extends ArrayBufferView>(
|
||||
fd: number,
|
||||
buffer: TBuffer,
|
||||
offset: number,
|
||||
length: number,
|
||||
position: number | null,
|
||||
): Promise<{ bytesRead: number; buffer: TBuffer }>;
|
||||
|
||||
export function readFile(file: PathLike | number, callback: (err: NodeJS.ErrnoException, data: Buffer) => void): void;
|
||||
export function readFile(
|
||||
file: PathLike | number,
|
||||
encoding: BufferEncoding | string,
|
||||
callback: (err: NodeJS.ErrnoException, data: string) => void,
|
||||
): void;
|
||||
export function readFile(
|
||||
file: PathLike | number,
|
||||
options: { flag?: string | undefined } | { encoding: BufferEncoding | string; flag?: string | undefined },
|
||||
callback: (err: NodeJS.ErrnoException, data: Buffer) => void,
|
||||
): void;
|
||||
export function readFile(
|
||||
file: PathLike | number,
|
||||
options: { flag?: string | undefined } | { encoding: BufferEncoding | string; flag?: string | undefined },
|
||||
): Promise<string>;
|
||||
// tslint:disable-next-line:unified-signatures
|
||||
export function readFile(file: PathLike | number, encoding: BufferEncoding | string): Promise<string>;
|
||||
export function readFile(file: PathLike | number): Promise<Buffer>;
|
||||
|
||||
export function readdir(path: PathLike, callback: (err: NodeJS.ErrnoException, files: string[]) => void): void;
|
||||
export function readdir(
|
||||
path: PathLike,
|
||||
options: 'buffer' | { encoding: 'buffer'; withFileTypes?: false | undefined },
|
||||
): Promise<Buffer[]>;
|
||||
export function readdir(
|
||||
path: PathLike,
|
||||
options?:
|
||||
| { encoding: BufferEncoding | string | null; withFileTypes?: false | undefined }
|
||||
| BufferEncoding
|
||||
| string
|
||||
| null,
|
||||
): Promise<string[]>;
|
||||
export function readdir(
|
||||
path: PathLike,
|
||||
options?: { encoding?: BufferEncoding | string | null | undefined; withFileTypes?: false | undefined },
|
||||
): Promise<string[] | Buffer[]>;
|
||||
export function readdir(
|
||||
path: PathLike,
|
||||
options: { encoding?: BufferEncoding | string | null | undefined; withFileTypes: true },
|
||||
): Promise<fs.Dirent[]>;
|
||||
|
||||
export function readlink(path: PathLike, callback: (err: NodeJS.ErrnoException, linkString: string) => any): void;
|
||||
export function readlink(path: PathLike): Promise<string>;
|
||||
|
||||
export function realpath(path: PathLike, callback: (err: NodeJS.ErrnoException, resolvedPath: string) => any): void;
|
||||
export function realpath(
|
||||
path: PathLike,
|
||||
cache: { [path: string]: string },
|
||||
callback: (err: NodeJS.ErrnoException, resolvedPath: string) => any,
|
||||
): void;
|
||||
export function realpath(path: PathLike, cache?: { [path: string]: string }): Promise<string>;
|
||||
|
||||
/* tslint:disable:unified-signatures */
|
||||
export namespace realpath {
|
||||
const native: {
|
||||
(path: PathLike, options: { encoding: 'buffer' } | 'buffer'): Promise<Buffer>;
|
||||
(
|
||||
path: PathLike,
|
||||
options: { encoding: BufferEncoding | string | null } | BufferEncoding | string | undefined | null,
|
||||
): Promise<string>;
|
||||
(path: PathLike, options: { encoding: BufferEncoding | string | null } | string | undefined | null): Promise<
|
||||
string | Buffer
|
||||
>;
|
||||
(path: PathLike): Promise<string>;
|
||||
} & typeof fs.realpath.native;
|
||||
}
|
||||
/* tslint:enable:unified-signatures */
|
||||
|
||||
export function rename(oldPath: PathLike, newPath: PathLike, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function rename(oldPath: PathLike, newPath: PathLike): Promise<void>;
|
||||
|
||||
/**
|
||||
* Asynchronously removes files and directories (modeled on the standard POSIX
|
||||
* `rm` utility).
|
||||
*
|
||||
* Only available in node >= v14.14.0
|
||||
*/
|
||||
export function rm(
|
||||
path: PathLike,
|
||||
options?: {
|
||||
force?: boolean | undefined;
|
||||
maxRetries?: number | undefined;
|
||||
recursive?: boolean | undefined;
|
||||
retryDelay?: number | undefined;
|
||||
},
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Asynchronous rmdir - removes the directory specified in {path}
|
||||
*
|
||||
* @param callback No arguments other than a possible exception are given to the completion callback.
|
||||
*/
|
||||
export function rmdir(path: PathLike, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function rmdir(path: PathLike, options?: fs.RmDirOptions): Promise<void>;
|
||||
|
||||
export function stat(path: PathLike, callback: (err: NodeJS.ErrnoException, stats: Stats) => any): void;
|
||||
export function stat(path: PathLike): Promise<Stats>;
|
||||
|
||||
export function symlink(
|
||||
target: PathLike,
|
||||
path: PathLike,
|
||||
type: SymlinkType | undefined,
|
||||
callback: (err: NodeJS.ErrnoException) => void,
|
||||
): void;
|
||||
export function symlink(target: PathLike, path: PathLike, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function symlink(target: PathLike, path: PathLike, type?: SymlinkType): Promise<void>;
|
||||
|
||||
export function truncate(path: PathLike, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function truncate(path: PathLike, len: number, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function truncate(path: PathLike, len?: number): Promise<void>;
|
||||
|
||||
/**
|
||||
* Asynchronous unlink - deletes the file specified in {path}
|
||||
*
|
||||
* @param callback No arguments other than a possible exception are given to the completion callback.
|
||||
*/
|
||||
export function unlink(path: PathLike, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function unlink(path: PathLike): Promise<void>;
|
||||
|
||||
export function utimes(
|
||||
path: PathLike,
|
||||
atime: number,
|
||||
mtime: number,
|
||||
callback: (err: NodeJS.ErrnoException) => void,
|
||||
): void;
|
||||
export function utimes(path: PathLike, atime: Date, mtime: Date, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function utimes(path: PathLike, atime: number, mtime: number): Promise<void>;
|
||||
export function utimes(path: PathLike, atime: Date, mtime: Date): Promise<void>;
|
||||
|
||||
export function write<TBuffer extends ArrayBufferView>(
|
||||
fd: number,
|
||||
buffer: TBuffer,
|
||||
offset: number,
|
||||
length: number,
|
||||
position: number | null,
|
||||
callback: (err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void,
|
||||
): void;
|
||||
export function write<TBuffer extends ArrayBufferView>(
|
||||
fd: number,
|
||||
buffer: TBuffer,
|
||||
offset: number,
|
||||
length: number,
|
||||
callback: (err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void,
|
||||
): void;
|
||||
export function write(
|
||||
fd: number,
|
||||
data: any,
|
||||
callback: (err: NodeJS.ErrnoException, written: number, str: string) => void,
|
||||
): void;
|
||||
export function write(
|
||||
fd: number,
|
||||
data: any,
|
||||
offset: number,
|
||||
callback: (err: NodeJS.ErrnoException, written: number, str: string) => void,
|
||||
): void;
|
||||
export function write(
|
||||
fd: number,
|
||||
data: any,
|
||||
offset: number,
|
||||
encoding: BufferEncoding | string,
|
||||
callback: (err: NodeJS.ErrnoException, written: number, str: string) => void,
|
||||
): void;
|
||||
export function write<TBuffer extends ArrayBufferView>(
|
||||
fd: number,
|
||||
buffer: TBuffer,
|
||||
offset?: number,
|
||||
length?: number,
|
||||
position?: number | null,
|
||||
): Promise<{ bytesWritten: number; buffer: TBuffer }>;
|
||||
export function write(
|
||||
fd: number,
|
||||
data: any,
|
||||
offset?: number,
|
||||
encoding?: BufferEncoding | string,
|
||||
): Promise<{ bytesWritten: number; buffer: string }>;
|
||||
|
||||
export function writeFile(file: PathLike | number, data: any, callback: (err: NodeJS.ErrnoException) => void): void;
|
||||
export function writeFile(
|
||||
file: PathLike | number,
|
||||
data: any,
|
||||
options?: WriteFileOptions | BufferEncoding | string,
|
||||
): Promise<void>;
|
||||
export function writeFile(
|
||||
file: PathLike | number,
|
||||
data: any,
|
||||
options: WriteFileOptions | BufferEncoding | string,
|
||||
callback: (err: NodeJS.ErrnoException) => void,
|
||||
): void;
|
||||
|
||||
export function writev(
|
||||
fd: number,
|
||||
buffers: NodeJS.ArrayBufferView[],
|
||||
position: number,
|
||||
cb: (err: NodeJS.ErrnoException | null, bytesWritten: number, buffers: NodeJS.ArrayBufferView[]) => void,
|
||||
): void;
|
||||
export function writev(
|
||||
fd: number,
|
||||
buffers: NodeJS.ArrayBufferView[],
|
||||
cb: (err: NodeJS.ErrnoException | null, bytesWritten: number, buffers: NodeJS.ArrayBufferView[]) => void,
|
||||
): void;
|
||||
export function writev(fd: number, buffers: NodeJS.ArrayBufferView[], position?: number): Promise<WritevResult>;
|
||||
|
||||
/**
|
||||
* Asynchronous mkdtemp - Creates a unique temporary directory. Generates six random characters to be appended behind a required prefix to create a unique temporary directory.
|
||||
*
|
||||
* @param callback The created folder path is passed as a string to the callback's second parameter.
|
||||
*/
|
||||
export function mkdtemp(prefix: string): Promise<string>;
|
||||
export function mkdtemp(prefix: string, callback: (err: NodeJS.ErrnoException, folder: string) => void): void;
|
||||
|
||||
export interface PathEntry {
|
||||
path: string;
|
||||
stats: Stats;
|
||||
}
|
||||
|
||||
export interface PathEntryStream {
|
||||
read(): PathEntry | null;
|
||||
}
|
||||
|
||||
export type CopyFilterSync = (src: string, dest: string) => boolean;
|
||||
export type CopyFilterAsync = (src: string, dest: string) => Promise<boolean>;
|
||||
|
||||
export type SymlinkType = 'dir' | 'file' | 'junction';
|
||||
|
||||
export type Mode = string | number;
|
||||
|
||||
export type ArrayBufferView = NodeJS.TypedArray | DataView;
|
||||
|
||||
export interface CopyOptions {
|
||||
dereference?: boolean | undefined;
|
||||
overwrite?: boolean | undefined;
|
||||
preserveTimestamps?: boolean | undefined;
|
||||
errorOnExist?: boolean | undefined;
|
||||
filter?: CopyFilterSync | CopyFilterAsync | undefined;
|
||||
recursive?: boolean | undefined;
|
||||
}
|
||||
|
||||
export interface CopyOptionsSync extends CopyOptions {
|
||||
filter?: CopyFilterSync | undefined;
|
||||
}
|
||||
|
||||
export interface EnsureOptions {
|
||||
mode?: number | undefined;
|
||||
}
|
||||
|
||||
export interface MoveOptions {
|
||||
overwrite?: boolean | undefined;
|
||||
limit?: number | undefined;
|
||||
}
|
||||
|
||||
export interface ReadOptions {
|
||||
throws?: boolean | undefined;
|
||||
fs?: object | undefined;
|
||||
reviver?: any;
|
||||
encoding?: BufferEncoding | string | undefined;
|
||||
flag?: string | undefined;
|
||||
}
|
||||
|
||||
export interface WriteFileOptions {
|
||||
encoding?: BufferEncoding | string | null | undefined;
|
||||
flag?: string | undefined;
|
||||
mode?: number | undefined;
|
||||
}
|
||||
|
||||
export interface WriteOptions extends WriteFileOptions {
|
||||
fs?: object | undefined;
|
||||
replacer?: any;
|
||||
spaces?: number | string | undefined;
|
||||
EOL?: string | undefined;
|
||||
}
|
||||
|
||||
export interface WritevResult {
|
||||
bytesWritten: number;
|
||||
buffers: ArrayBufferView[];
|
||||
}
|
||||
Generated
Vendored
Executable
+67
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "@types/fs-extra",
|
||||
"version": "9.0.13",
|
||||
"description": "TypeScript definitions for fs-extra",
|
||||
"homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/fs-extra",
|
||||
"license": "MIT",
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Alan Agius",
|
||||
"url": "https://github.com/alan-agius4",
|
||||
"githubUsername": "alan-agius4"
|
||||
},
|
||||
{
|
||||
"name": "midknight41",
|
||||
"url": "https://github.com/midknight41",
|
||||
"githubUsername": "midknight41"
|
||||
},
|
||||
{
|
||||
"name": "Brendan Forster",
|
||||
"url": "https://github.com/shiftkey",
|
||||
"githubUsername": "shiftkey"
|
||||
},
|
||||
{
|
||||
"name": "Mees van Dijk",
|
||||
"url": "https://github.com/mees-",
|
||||
"githubUsername": "mees-"
|
||||
},
|
||||
{
|
||||
"name": "Justin Rockwood",
|
||||
"url": "https://github.com/jrockwood",
|
||||
"githubUsername": "jrockwood"
|
||||
},
|
||||
{
|
||||
"name": "Sang Dang",
|
||||
"url": "https://github.com/sangdth",
|
||||
"githubUsername": "sangdth"
|
||||
},
|
||||
{
|
||||
"name": "Florian Keller",
|
||||
"url": "https://github.com/ffflorian",
|
||||
"githubUsername": "ffflorian"
|
||||
},
|
||||
{
|
||||
"name": "Piotr Błażejewicz",
|
||||
"url": "https://github.com/peterblazejewicz",
|
||||
"githubUsername": "peterblazejewicz"
|
||||
},
|
||||
{
|
||||
"name": "Tiger Oakes",
|
||||
"url": "https://github.com/NotWoods",
|
||||
"githubUsername": "NotWoods"
|
||||
}
|
||||
],
|
||||
"main": "",
|
||||
"types": "index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git",
|
||||
"directory": "types/fs-extra"
|
||||
},
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
},
|
||||
"typesPublisherContentHash": "75e10d16a18d936a50e54414024604aa9484e8345b37c2027921869f254523ee",
|
||||
"typeScriptVersion": "3.9"
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
The ISC License
|
||||
Copyright (c) 2020 Ryan Zimmerman <opensrc@ryanzim.com>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
module.exports = r => {
|
||||
const n = process.versions.node.split('.').map(x => parseInt(x, 10))
|
||||
r = r.split('.').map(x => parseInt(x, 10))
|
||||
return n[0] > r[0] || (n[0] === r[0] && (n[1] > r[1] || (n[1] === r[1] && n[2] >= r[2])))
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "at-least-node",
|
||||
"version": "1.0.0",
|
||||
"description": "Lightweight Node.js version sniffing/comparison",
|
||||
"keywords": [
|
||||
"semver",
|
||||
"feature"
|
||||
],
|
||||
"homepage": "https://github.com/RyanZim/at-least-node#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/RyanZim/at-least-node/issues"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/RyanZim/at-least-node.git"
|
||||
},
|
||||
"license": "ISC",
|
||||
"author": "Ryan Zimmerman <opensrc@ryanzim.com>",
|
||||
"main": "index.js",
|
||||
"files": [],
|
||||
"scripts": {
|
||||
"test": "ava"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ava": "^3.1.0",
|
||||
"rewire": "^4.0.1",
|
||||
"semver": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2011-2017 JP Richardson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
|
||||
(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
|
||||
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
||||
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
'use strict'
|
||||
|
||||
const fs = require('graceful-fs')
|
||||
const path = require('path')
|
||||
const mkdirsSync = require('../mkdirs').mkdirsSync
|
||||
const utimesMillisSync = require('../util/utimes').utimesMillisSync
|
||||
const stat = require('../util/stat')
|
||||
|
||||
function copySync (src, dest, opts) {
|
||||
if (typeof opts === 'function') {
|
||||
opts = { filter: opts }
|
||||
}
|
||||
|
||||
opts = opts || {}
|
||||
opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
|
||||
opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber
|
||||
|
||||
// Warn about using preserveTimestamps on 32-bit node
|
||||
if (opts.preserveTimestamps && process.arch === 'ia32') {
|
||||
console.warn(`fs-extra: Using the preserveTimestamps option in 32-bit node is not recommended;\n
|
||||
see https://github.com/jprichardson/node-fs-extra/issues/269`)
|
||||
}
|
||||
|
||||
const { srcStat, destStat } = stat.checkPathsSync(src, dest, 'copy')
|
||||
stat.checkParentPathsSync(src, srcStat, dest, 'copy')
|
||||
return handleFilterAndCopy(destStat, src, dest, opts)
|
||||
}
|
||||
|
||||
function handleFilterAndCopy (destStat, src, dest, opts) {
|
||||
if (opts.filter && !opts.filter(src, dest)) return
|
||||
const destParent = path.dirname(dest)
|
||||
if (!fs.existsSync(destParent)) mkdirsSync(destParent)
|
||||
return startCopy(destStat, src, dest, opts)
|
||||
}
|
||||
|
||||
function startCopy (destStat, src, dest, opts) {
|
||||
if (opts.filter && !opts.filter(src, dest)) return
|
||||
return getStats(destStat, src, dest, opts)
|
||||
}
|
||||
|
||||
function getStats (destStat, src, dest, opts) {
|
||||
const statSync = opts.dereference ? fs.statSync : fs.lstatSync
|
||||
const srcStat = statSync(src)
|
||||
|
||||
if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts)
|
||||
else if (srcStat.isFile() ||
|
||||
srcStat.isCharacterDevice() ||
|
||||
srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts)
|
||||
else if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts)
|
||||
}
|
||||
|
||||
function onFile (srcStat, destStat, src, dest, opts) {
|
||||
if (!destStat) return copyFile(srcStat, src, dest, opts)
|
||||
return mayCopyFile(srcStat, src, dest, opts)
|
||||
}
|
||||
|
||||
function mayCopyFile (srcStat, src, dest, opts) {
|
||||
if (opts.overwrite) {
|
||||
fs.unlinkSync(dest)
|
||||
return copyFile(srcStat, src, dest, opts)
|
||||
} else if (opts.errorOnExist) {
|
||||
throw new Error(`'${dest}' already exists`)
|
||||
}
|
||||
}
|
||||
|
||||
function copyFile (srcStat, src, dest, opts) {
|
||||
fs.copyFileSync(src, dest)
|
||||
if (opts.preserveTimestamps) handleTimestamps(srcStat.mode, src, dest)
|
||||
return setDestMode(dest, srcStat.mode)
|
||||
}
|
||||
|
||||
function handleTimestamps (srcMode, src, dest) {
|
||||
// Make sure the file is writable before setting the timestamp
|
||||
// otherwise open fails with EPERM when invoked with 'r+'
|
||||
// (through utimes call)
|
||||
if (fileIsNotWritable(srcMode)) makeFileWritable(dest, srcMode)
|
||||
return setDestTimestamps(src, dest)
|
||||
}
|
||||
|
||||
function fileIsNotWritable (srcMode) {
|
||||
return (srcMode & 0o200) === 0
|
||||
}
|
||||
|
||||
function makeFileWritable (dest, srcMode) {
|
||||
return setDestMode(dest, srcMode | 0o200)
|
||||
}
|
||||
|
||||
function setDestMode (dest, srcMode) {
|
||||
return fs.chmodSync(dest, srcMode)
|
||||
}
|
||||
|
||||
function setDestTimestamps (src, dest) {
|
||||
// The initial srcStat.atime cannot be trusted
|
||||
// because it is modified by the read(2) system call
|
||||
// (See https://nodejs.org/api/fs.html#fs_stat_time_values)
|
||||
const updatedSrcStat = fs.statSync(src)
|
||||
return utimesMillisSync(dest, updatedSrcStat.atime, updatedSrcStat.mtime)
|
||||
}
|
||||
|
||||
function onDir (srcStat, destStat, src, dest, opts) {
|
||||
if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts)
|
||||
if (destStat && !destStat.isDirectory()) {
|
||||
throw new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`)
|
||||
}
|
||||
return copyDir(src, dest, opts)
|
||||
}
|
||||
|
||||
function mkDirAndCopy (srcMode, src, dest, opts) {
|
||||
fs.mkdirSync(dest)
|
||||
copyDir(src, dest, opts)
|
||||
return setDestMode(dest, srcMode)
|
||||
}
|
||||
|
||||
function copyDir (src, dest, opts) {
|
||||
fs.readdirSync(src).forEach(item => copyDirItem(item, src, dest, opts))
|
||||
}
|
||||
|
||||
function copyDirItem (item, src, dest, opts) {
|
||||
const srcItem = path.join(src, item)
|
||||
const destItem = path.join(dest, item)
|
||||
const { destStat } = stat.checkPathsSync(srcItem, destItem, 'copy')
|
||||
return startCopy(destStat, srcItem, destItem, opts)
|
||||
}
|
||||
|
||||
function onLink (destStat, src, dest, opts) {
|
||||
let resolvedSrc = fs.readlinkSync(src)
|
||||
if (opts.dereference) {
|
||||
resolvedSrc = path.resolve(process.cwd(), resolvedSrc)
|
||||
}
|
||||
|
||||
if (!destStat) {
|
||||
return fs.symlinkSync(resolvedSrc, dest)
|
||||
} else {
|
||||
let resolvedDest
|
||||
try {
|
||||
resolvedDest = fs.readlinkSync(dest)
|
||||
} catch (err) {
|
||||
// dest exists and is a regular file or directory,
|
||||
// Windows may throw UNKNOWN error. If dest already exists,
|
||||
// fs throws error anyway, so no need to guard against it here.
|
||||
if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return fs.symlinkSync(resolvedSrc, dest)
|
||||
throw err
|
||||
}
|
||||
if (opts.dereference) {
|
||||
resolvedDest = path.resolve(process.cwd(), resolvedDest)
|
||||
}
|
||||
if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) {
|
||||
throw new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`)
|
||||
}
|
||||
|
||||
// prevent copy if src is a subdir of dest since unlinking
|
||||
// dest in this case would result in removing src contents
|
||||
// and therefore a broken symlink would be created.
|
||||
if (fs.statSync(dest).isDirectory() && stat.isSrcSubdir(resolvedDest, resolvedSrc)) {
|
||||
throw new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`)
|
||||
}
|
||||
return copyLink(resolvedSrc, dest)
|
||||
}
|
||||
}
|
||||
|
||||
function copyLink (resolvedSrc, dest) {
|
||||
fs.unlinkSync(dest)
|
||||
return fs.symlinkSync(resolvedSrc, dest)
|
||||
}
|
||||
|
||||
module.exports = copySync
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
copySync: require('./copy-sync')
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
'use strict'
|
||||
|
||||
const fs = require('graceful-fs')
|
||||
const path = require('path')
|
||||
const mkdirs = require('../mkdirs').mkdirs
|
||||
const pathExists = require('../path-exists').pathExists
|
||||
const utimesMillis = require('../util/utimes').utimesMillis
|
||||
const stat = require('../util/stat')
|
||||
|
||||
function copy (src, dest, opts, cb) {
|
||||
if (typeof opts === 'function' && !cb) {
|
||||
cb = opts
|
||||
opts = {}
|
||||
} else if (typeof opts === 'function') {
|
||||
opts = { filter: opts }
|
||||
}
|
||||
|
||||
cb = cb || function () {}
|
||||
opts = opts || {}
|
||||
|
||||
opts.clobber = 'clobber' in opts ? !!opts.clobber : true // default to true for now
|
||||
opts.overwrite = 'overwrite' in opts ? !!opts.overwrite : opts.clobber // overwrite falls back to clobber
|
||||
|
||||
// Warn about using preserveTimestamps on 32-bit node
|
||||
if (opts.preserveTimestamps && process.arch === 'ia32') {
|
||||
console.warn(`fs-extra: Using the preserveTimestamps option in 32-bit node is not recommended;\n
|
||||
see https://github.com/jprichardson/node-fs-extra/issues/269`)
|
||||
}
|
||||
|
||||
stat.checkPaths(src, dest, 'copy', (err, stats) => {
|
||||
if (err) return cb(err)
|
||||
const { srcStat, destStat } = stats
|
||||
stat.checkParentPaths(src, srcStat, dest, 'copy', err => {
|
||||
if (err) return cb(err)
|
||||
if (opts.filter) return handleFilter(checkParentDir, destStat, src, dest, opts, cb)
|
||||
return checkParentDir(destStat, src, dest, opts, cb)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function checkParentDir (destStat, src, dest, opts, cb) {
|
||||
const destParent = path.dirname(dest)
|
||||
pathExists(destParent, (err, dirExists) => {
|
||||
if (err) return cb(err)
|
||||
if (dirExists) return startCopy(destStat, src, dest, opts, cb)
|
||||
mkdirs(destParent, err => {
|
||||
if (err) return cb(err)
|
||||
return startCopy(destStat, src, dest, opts, cb)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function handleFilter (onInclude, destStat, src, dest, opts, cb) {
|
||||
Promise.resolve(opts.filter(src, dest)).then(include => {
|
||||
if (include) return onInclude(destStat, src, dest, opts, cb)
|
||||
return cb()
|
||||
}, error => cb(error))
|
||||
}
|
||||
|
||||
function startCopy (destStat, src, dest, opts, cb) {
|
||||
if (opts.filter) return handleFilter(getStats, destStat, src, dest, opts, cb)
|
||||
return getStats(destStat, src, dest, opts, cb)
|
||||
}
|
||||
|
||||
function getStats (destStat, src, dest, opts, cb) {
|
||||
const stat = opts.dereference ? fs.stat : fs.lstat
|
||||
stat(src, (err, srcStat) => {
|
||||
if (err) return cb(err)
|
||||
|
||||
if (srcStat.isDirectory()) return onDir(srcStat, destStat, src, dest, opts, cb)
|
||||
else if (srcStat.isFile() ||
|
||||
srcStat.isCharacterDevice() ||
|
||||
srcStat.isBlockDevice()) return onFile(srcStat, destStat, src, dest, opts, cb)
|
||||
else if (srcStat.isSymbolicLink()) return onLink(destStat, src, dest, opts, cb)
|
||||
})
|
||||
}
|
||||
|
||||
function onFile (srcStat, destStat, src, dest, opts, cb) {
|
||||
if (!destStat) return copyFile(srcStat, src, dest, opts, cb)
|
||||
return mayCopyFile(srcStat, src, dest, opts, cb)
|
||||
}
|
||||
|
||||
function mayCopyFile (srcStat, src, dest, opts, cb) {
|
||||
if (opts.overwrite) {
|
||||
fs.unlink(dest, err => {
|
||||
if (err) return cb(err)
|
||||
return copyFile(srcStat, src, dest, opts, cb)
|
||||
})
|
||||
} else if (opts.errorOnExist) {
|
||||
return cb(new Error(`'${dest}' already exists`))
|
||||
} else return cb()
|
||||
}
|
||||
|
||||
function copyFile (srcStat, src, dest, opts, cb) {
|
||||
fs.copyFile(src, dest, err => {
|
||||
if (err) return cb(err)
|
||||
if (opts.preserveTimestamps) return handleTimestampsAndMode(srcStat.mode, src, dest, cb)
|
||||
return setDestMode(dest, srcStat.mode, cb)
|
||||
})
|
||||
}
|
||||
|
||||
function handleTimestampsAndMode (srcMode, src, dest, cb) {
|
||||
// Make sure the file is writable before setting the timestamp
|
||||
// otherwise open fails with EPERM when invoked with 'r+'
|
||||
// (through utimes call)
|
||||
if (fileIsNotWritable(srcMode)) {
|
||||
return makeFileWritable(dest, srcMode, err => {
|
||||
if (err) return cb(err)
|
||||
return setDestTimestampsAndMode(srcMode, src, dest, cb)
|
||||
})
|
||||
}
|
||||
return setDestTimestampsAndMode(srcMode, src, dest, cb)
|
||||
}
|
||||
|
||||
function fileIsNotWritable (srcMode) {
|
||||
return (srcMode & 0o200) === 0
|
||||
}
|
||||
|
||||
function makeFileWritable (dest, srcMode, cb) {
|
||||
return setDestMode(dest, srcMode | 0o200, cb)
|
||||
}
|
||||
|
||||
function setDestTimestampsAndMode (srcMode, src, dest, cb) {
|
||||
setDestTimestamps(src, dest, err => {
|
||||
if (err) return cb(err)
|
||||
return setDestMode(dest, srcMode, cb)
|
||||
})
|
||||
}
|
||||
|
||||
function setDestMode (dest, srcMode, cb) {
|
||||
return fs.chmod(dest, srcMode, cb)
|
||||
}
|
||||
|
||||
function setDestTimestamps (src, dest, cb) {
|
||||
// The initial srcStat.atime cannot be trusted
|
||||
// because it is modified by the read(2) system call
|
||||
// (See https://nodejs.org/api/fs.html#fs_stat_time_values)
|
||||
fs.stat(src, (err, updatedSrcStat) => {
|
||||
if (err) return cb(err)
|
||||
return utimesMillis(dest, updatedSrcStat.atime, updatedSrcStat.mtime, cb)
|
||||
})
|
||||
}
|
||||
|
||||
function onDir (srcStat, destStat, src, dest, opts, cb) {
|
||||
if (!destStat) return mkDirAndCopy(srcStat.mode, src, dest, opts, cb)
|
||||
if (destStat && !destStat.isDirectory()) {
|
||||
return cb(new Error(`Cannot overwrite non-directory '${dest}' with directory '${src}'.`))
|
||||
}
|
||||
return copyDir(src, dest, opts, cb)
|
||||
}
|
||||
|
||||
function mkDirAndCopy (srcMode, src, dest, opts, cb) {
|
||||
fs.mkdir(dest, err => {
|
||||
if (err) return cb(err)
|
||||
copyDir(src, dest, opts, err => {
|
||||
if (err) return cb(err)
|
||||
return setDestMode(dest, srcMode, cb)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function copyDir (src, dest, opts, cb) {
|
||||
fs.readdir(src, (err, items) => {
|
||||
if (err) return cb(err)
|
||||
return copyDirItems(items, src, dest, opts, cb)
|
||||
})
|
||||
}
|
||||
|
||||
function copyDirItems (items, src, dest, opts, cb) {
|
||||
const item = items.pop()
|
||||
if (!item) return cb()
|
||||
return copyDirItem(items, item, src, dest, opts, cb)
|
||||
}
|
||||
|
||||
function copyDirItem (items, item, src, dest, opts, cb) {
|
||||
const srcItem = path.join(src, item)
|
||||
const destItem = path.join(dest, item)
|
||||
stat.checkPaths(srcItem, destItem, 'copy', (err, stats) => {
|
||||
if (err) return cb(err)
|
||||
const { destStat } = stats
|
||||
startCopy(destStat, srcItem, destItem, opts, err => {
|
||||
if (err) return cb(err)
|
||||
return copyDirItems(items, src, dest, opts, cb)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function onLink (destStat, src, dest, opts, cb) {
|
||||
fs.readlink(src, (err, resolvedSrc) => {
|
||||
if (err) return cb(err)
|
||||
if (opts.dereference) {
|
||||
resolvedSrc = path.resolve(process.cwd(), resolvedSrc)
|
||||
}
|
||||
|
||||
if (!destStat) {
|
||||
return fs.symlink(resolvedSrc, dest, cb)
|
||||
} else {
|
||||
fs.readlink(dest, (err, resolvedDest) => {
|
||||
if (err) {
|
||||
// dest exists and is a regular file or directory,
|
||||
// Windows may throw UNKNOWN error. If dest already exists,
|
||||
// fs throws error anyway, so no need to guard against it here.
|
||||
if (err.code === 'EINVAL' || err.code === 'UNKNOWN') return fs.symlink(resolvedSrc, dest, cb)
|
||||
return cb(err)
|
||||
}
|
||||
if (opts.dereference) {
|
||||
resolvedDest = path.resolve(process.cwd(), resolvedDest)
|
||||
}
|
||||
if (stat.isSrcSubdir(resolvedSrc, resolvedDest)) {
|
||||
return cb(new Error(`Cannot copy '${resolvedSrc}' to a subdirectory of itself, '${resolvedDest}'.`))
|
||||
}
|
||||
|
||||
// do not copy if src is a subdir of dest since unlinking
|
||||
// dest in this case would result in removing src contents
|
||||
// and therefore a broken symlink would be created.
|
||||
if (destStat.isDirectory() && stat.isSrcSubdir(resolvedDest, resolvedSrc)) {
|
||||
return cb(new Error(`Cannot overwrite '${resolvedDest}' with '${resolvedSrc}'.`))
|
||||
}
|
||||
return copyLink(resolvedSrc, dest, cb)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function copyLink (resolvedSrc, dest, cb) {
|
||||
fs.unlink(dest, err => {
|
||||
if (err) return cb(err)
|
||||
return fs.symlink(resolvedSrc, dest, cb)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = copy
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const u = require('universalify').fromCallback
|
||||
module.exports = {
|
||||
copy: u(require('./copy'))
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
'use strict'
|
||||
|
||||
const u = require('universalify').fromCallback
|
||||
const fs = require('graceful-fs')
|
||||
const path = require('path')
|
||||
const mkdir = require('../mkdirs')
|
||||
const remove = require('../remove')
|
||||
|
||||
const emptyDir = u(function emptyDir (dir, callback) {
|
||||
callback = callback || function () {}
|
||||
fs.readdir(dir, (err, items) => {
|
||||
if (err) return mkdir.mkdirs(dir, callback)
|
||||
|
||||
items = items.map(item => path.join(dir, item))
|
||||
|
||||
deleteItem()
|
||||
|
||||
function deleteItem () {
|
||||
const item = items.pop()
|
||||
if (!item) return callback()
|
||||
remove.remove(item, err => {
|
||||
if (err) return callback(err)
|
||||
deleteItem()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function emptyDirSync (dir) {
|
||||
let items
|
||||
try {
|
||||
items = fs.readdirSync(dir)
|
||||
} catch {
|
||||
return mkdir.mkdirsSync(dir)
|
||||
}
|
||||
|
||||
items.forEach(item => {
|
||||
item = path.join(dir, item)
|
||||
remove.removeSync(item)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
emptyDirSync,
|
||||
emptydirSync: emptyDirSync,
|
||||
emptyDir,
|
||||
emptydir: emptyDir
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
'use strict'
|
||||
|
||||
const u = require('universalify').fromCallback
|
||||
const path = require('path')
|
||||
const fs = require('graceful-fs')
|
||||
const mkdir = require('../mkdirs')
|
||||
|
||||
function createFile (file, callback) {
|
||||
function makeFile () {
|
||||
fs.writeFile(file, '', err => {
|
||||
if (err) return callback(err)
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
fs.stat(file, (err, stats) => { // eslint-disable-line handle-callback-err
|
||||
if (!err && stats.isFile()) return callback()
|
||||
const dir = path.dirname(file)
|
||||
fs.stat(dir, (err, stats) => {
|
||||
if (err) {
|
||||
// if the directory doesn't exist, make it
|
||||
if (err.code === 'ENOENT') {
|
||||
return mkdir.mkdirs(dir, err => {
|
||||
if (err) return callback(err)
|
||||
makeFile()
|
||||
})
|
||||
}
|
||||
return callback(err)
|
||||
}
|
||||
|
||||
if (stats.isDirectory()) makeFile()
|
||||
else {
|
||||
// parent is not a directory
|
||||
// This is just to cause an internal ENOTDIR error to be thrown
|
||||
fs.readdir(dir, err => {
|
||||
if (err) return callback(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function createFileSync (file) {
|
||||
let stats
|
||||
try {
|
||||
stats = fs.statSync(file)
|
||||
} catch {}
|
||||
if (stats && stats.isFile()) return
|
||||
|
||||
const dir = path.dirname(file)
|
||||
try {
|
||||
if (!fs.statSync(dir).isDirectory()) {
|
||||
// parent is not a directory
|
||||
// This is just to cause an internal ENOTDIR error to be thrown
|
||||
fs.readdirSync(dir)
|
||||
}
|
||||
} catch (err) {
|
||||
// If the stat call above failed because the directory doesn't exist, create it
|
||||
if (err && err.code === 'ENOENT') mkdir.mkdirsSync(dir)
|
||||
else throw err
|
||||
}
|
||||
|
||||
fs.writeFileSync(file, '')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createFile: u(createFile),
|
||||
createFileSync
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
'use strict'
|
||||
|
||||
const file = require('./file')
|
||||
const link = require('./link')
|
||||
const symlink = require('./symlink')
|
||||
|
||||
module.exports = {
|
||||
// file
|
||||
createFile: file.createFile,
|
||||
createFileSync: file.createFileSync,
|
||||
ensureFile: file.createFile,
|
||||
ensureFileSync: file.createFileSync,
|
||||
// link
|
||||
createLink: link.createLink,
|
||||
createLinkSync: link.createLinkSync,
|
||||
ensureLink: link.createLink,
|
||||
ensureLinkSync: link.createLinkSync,
|
||||
// symlink
|
||||
createSymlink: symlink.createSymlink,
|
||||
createSymlinkSync: symlink.createSymlinkSync,
|
||||
ensureSymlink: symlink.createSymlink,
|
||||
ensureSymlinkSync: symlink.createSymlinkSync
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
'use strict'
|
||||
|
||||
const u = require('universalify').fromCallback
|
||||
const path = require('path')
|
||||
const fs = require('graceful-fs')
|
||||
const mkdir = require('../mkdirs')
|
||||
const pathExists = require('../path-exists').pathExists
|
||||
|
||||
function createLink (srcpath, dstpath, callback) {
|
||||
function makeLink (srcpath, dstpath) {
|
||||
fs.link(srcpath, dstpath, err => {
|
||||
if (err) return callback(err)
|
||||
callback(null)
|
||||
})
|
||||
}
|
||||
|
||||
pathExists(dstpath, (err, destinationExists) => {
|
||||
if (err) return callback(err)
|
||||
if (destinationExists) return callback(null)
|
||||
fs.lstat(srcpath, (err) => {
|
||||
if (err) {
|
||||
err.message = err.message.replace('lstat', 'ensureLink')
|
||||
return callback(err)
|
||||
}
|
||||
|
||||
const dir = path.dirname(dstpath)
|
||||
pathExists(dir, (err, dirExists) => {
|
||||
if (err) return callback(err)
|
||||
if (dirExists) return makeLink(srcpath, dstpath)
|
||||
mkdir.mkdirs(dir, err => {
|
||||
if (err) return callback(err)
|
||||
makeLink(srcpath, dstpath)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function createLinkSync (srcpath, dstpath) {
|
||||
const destinationExists = fs.existsSync(dstpath)
|
||||
if (destinationExists) return undefined
|
||||
|
||||
try {
|
||||
fs.lstatSync(srcpath)
|
||||
} catch (err) {
|
||||
err.message = err.message.replace('lstat', 'ensureLink')
|
||||
throw err
|
||||
}
|
||||
|
||||
const dir = path.dirname(dstpath)
|
||||
const dirExists = fs.existsSync(dir)
|
||||
if (dirExists) return fs.linkSync(srcpath, dstpath)
|
||||
mkdir.mkdirsSync(dir)
|
||||
|
||||
return fs.linkSync(srcpath, dstpath)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createLink: u(createLink),
|
||||
createLinkSync
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
'use strict'
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('graceful-fs')
|
||||
const pathExists = require('../path-exists').pathExists
|
||||
|
||||
/**
|
||||
* Function that returns two types of paths, one relative to symlink, and one
|
||||
* relative to the current working directory. Checks if path is absolute or
|
||||
* relative. If the path is relative, this function checks if the path is
|
||||
* relative to symlink or relative to current working directory. This is an
|
||||
* initiative to find a smarter `srcpath` to supply when building symlinks.
|
||||
* This allows you to determine which path to use out of one of three possible
|
||||
* types of source paths. The first is an absolute path. This is detected by
|
||||
* `path.isAbsolute()`. When an absolute path is provided, it is checked to
|
||||
* see if it exists. If it does it's used, if not an error is returned
|
||||
* (callback)/ thrown (sync). The other two options for `srcpath` are a
|
||||
* relative url. By default Node's `fs.symlink` works by creating a symlink
|
||||
* using `dstpath` and expects the `srcpath` to be relative to the newly
|
||||
* created symlink. If you provide a `srcpath` that does not exist on the file
|
||||
* system it results in a broken symlink. To minimize this, the function
|
||||
* checks to see if the 'relative to symlink' source file exists, and if it
|
||||
* does it will use it. If it does not, it checks if there's a file that
|
||||
* exists that is relative to the current working directory, if does its used.
|
||||
* This preserves the expectations of the original fs.symlink spec and adds
|
||||
* the ability to pass in `relative to current working direcotry` paths.
|
||||
*/
|
||||
|
||||
function symlinkPaths (srcpath, dstpath, callback) {
|
||||
if (path.isAbsolute(srcpath)) {
|
||||
return fs.lstat(srcpath, (err) => {
|
||||
if (err) {
|
||||
err.message = err.message.replace('lstat', 'ensureSymlink')
|
||||
return callback(err)
|
||||
}
|
||||
return callback(null, {
|
||||
toCwd: srcpath,
|
||||
toDst: srcpath
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const dstdir = path.dirname(dstpath)
|
||||
const relativeToDst = path.join(dstdir, srcpath)
|
||||
return pathExists(relativeToDst, (err, exists) => {
|
||||
if (err) return callback(err)
|
||||
if (exists) {
|
||||
return callback(null, {
|
||||
toCwd: relativeToDst,
|
||||
toDst: srcpath
|
||||
})
|
||||
} else {
|
||||
return fs.lstat(srcpath, (err) => {
|
||||
if (err) {
|
||||
err.message = err.message.replace('lstat', 'ensureSymlink')
|
||||
return callback(err)
|
||||
}
|
||||
return callback(null, {
|
||||
toCwd: srcpath,
|
||||
toDst: path.relative(dstdir, srcpath)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function symlinkPathsSync (srcpath, dstpath) {
|
||||
let exists
|
||||
if (path.isAbsolute(srcpath)) {
|
||||
exists = fs.existsSync(srcpath)
|
||||
if (!exists) throw new Error('absolute srcpath does not exist')
|
||||
return {
|
||||
toCwd: srcpath,
|
||||
toDst: srcpath
|
||||
}
|
||||
} else {
|
||||
const dstdir = path.dirname(dstpath)
|
||||
const relativeToDst = path.join(dstdir, srcpath)
|
||||
exists = fs.existsSync(relativeToDst)
|
||||
if (exists) {
|
||||
return {
|
||||
toCwd: relativeToDst,
|
||||
toDst: srcpath
|
||||
}
|
||||
} else {
|
||||
exists = fs.existsSync(srcpath)
|
||||
if (!exists) throw new Error('relative srcpath does not exist')
|
||||
return {
|
||||
toCwd: srcpath,
|
||||
toDst: path.relative(dstdir, srcpath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
symlinkPaths,
|
||||
symlinkPathsSync
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user