Unblock common npm packages: type-only imports, @types packages, punycode and constants (#1285)

This commit is contained in:
Chris Tate
2026-10-18 16:25:22 -05:00
committed by GitHub
74 changed files with 1750 additions and 181 deletions
+1 -1
View File
@@ -119,7 +119,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
<dd>On Linux, when the program calls <code>process.exit()</code>, hand the release of its memory to a helper process so the parent sees the exit sooner. The kernel otherwise frees the whole address space before the exit is reported, which takes tens of milliseconds per gigabyte. Opt in only where the trade-offs are acceptable: for up to about 100 ms after the exit, the helper keeps the executable mapped (overwriting the binary in place fails with "text file busy"), its memory stays charged to the cgroup, and the teardown CPU time no longer appears in the parent's resource usage. Valid only for executable builds; it has no effect on other platforms.</dd>
<dt><code>--npm-static &lt;pkg[,pkg…]|auto&gt;</code></dt>
<dd>Name npm packages to compile even if they don't ship their own type declarations (repeatable). <code>auto</code> uses the default automatic selection. Packages that can't compile produce diagnostics. See <a href="/docs/dependencies#explicit-package-selection">npm Dependencies</a>.</dd>
<dd>Name npm packages to compile even if they don't ship their own type declarations (repeatable). A trailing <code>...</code>, as in <code>fs-extra...</code>, also names every package that package depends on. <code>auto</code> uses the default automatic selection. Packages that can't compile produce diagnostics. See <a href="/docs/dependencies#explicit-package-selection">npm Dependencies</a>.</dd>
<dt><code>--provenance-sources</code></dt>
<dd>Experimental. Compile npm dependencies from the source at the commit their provenance attestation names. Packages without a usable attestation compile from their installed files.</dd>
+4 -2
View File
@@ -46,6 +46,8 @@ Your callback and the commander implementation both compile to native code. No e
scriptc finds packages in `node_modules` with Node.js's resolution rules, including the export conditions in `package.json`, and compiles their dependencies too. TypeScript workspace packages compile as part of your program, including `.js` import specifiers that resolve to `.ts` sources.
Type-only imports follow Node.js's type stripping. `import type` and `export type … from` statements are erased, so a package or module named only by them supplies types and isn't compiled or loaded; it can even be a package with no JavaScript, such as `type-fest`. A clause whose specifiers are all marked `type`, such as `import { type Options } from "./config.ts"`, still loads its module, as it does under Node.js.
## Which packages compile
scriptc compiles a package automatically when it:
@@ -53,7 +55,7 @@ scriptc compiles a package automatically when it:
- ships its own type declarations, and
- ships readable, unminified JavaScript.
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself.
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself. A package whose declarations come from a separate `@types/` package doesn't ship its own; its diagnostic names the package and the `--npm-static` option that compiles it anyway.
<span id="value-conversion" />
<span id="static-package-compilation-experimental" />
@@ -67,7 +69,7 @@ Some packages rely on separately installed declarations, such as an `@types/` pa
$ 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. `--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. The option selects only the packages it names: a dependency of a named package that doesn't ship its own declarations is still refused. 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.
## CommonJS packages
+2
View File
@@ -76,6 +76,8 @@ title: "Limitations"
<include>../limitations/builtin-apis/legacy-url-api.mdx</include>
<include>../limitations/builtin-apis/deprecated-modules.mdx</include>
<include>../limitations/builtin-apis/filesystem-flags.mdx</include>
<include>../limitations/builtin-apis/copy.mdx</include>
@@ -0,0 +1 @@
- **Deprecated `punycode` and `constants` modules:** `punycode.encode()`, `decode()`, `toASCII()`, `toUnicode()`, `ucs2.decode()`, `ucs2.encode()`, and `version` compile, with the `DEP0040` warning Node.js reports when the module loads. A `require("punycode")` made from inside `node_modules` doesn't warn; a `require()` used inside a larger expression, such as `require("punycode").toASCII(name)`, doesn't warn either. The `punycode.ucs2` object itself is unsupported as a value. The `constants` module is the frozen object Node.js builds from `os.constants` and `fs.constants`, plus the `crypto.constants` of Node.js 24.22.0.
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "a610bdd1ae56e1c4cd38",
"artifactVersion": "4421a7b808191ac7f410",
"rowCount": 3880
}
+24 -24
View File
@@ -584,9 +584,9 @@
"apiEntries": 9,
"static": {
"supported": 0,
"partial": 0,
"partial": 8,
"refused": 0,
"not-implemented": 9,
"not-implemented": 1,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -58489,7 +58489,7 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"status": "partial",
"detail": "Derived from the exact API rows in this chapter.",
"verification": "derived"
}
@@ -58512,9 +58512,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -58535,9 +58535,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -58558,9 +58558,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -58581,9 +58581,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -58627,9 +58627,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -58650,9 +58650,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -58673,9 +58673,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -7,8 +7,8 @@
"tierItemCount": 3456,
"tiers": {
"static": {
"audit-partial": 1155,
"verify-gap": 2174,
"audit-partial": 1162,
"verify-gap": 2167,
"replace-refusal": 40,
"classify": 2,
"implement": 85
@@ -58559,12 +58559,16 @@
"priority": "low",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:punycode",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.punycode.decode",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
}
}
},
@@ -58584,12 +58588,16 @@
"priority": "low",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:punycode",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.punycode.encode",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
}
}
},
@@ -58609,12 +58617,16 @@
"priority": "low",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:punycode",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.punycode.toASCII",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
}
}
},
@@ -58634,12 +58646,16 @@
"priority": "low",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:punycode",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.punycode.toUnicode",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
}
}
},
@@ -58684,12 +58700,16 @@
"priority": "low",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:punycode",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.punycode.ucs2.decode",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
}
}
},
@@ -58709,12 +58729,16 @@
"priority": "low",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:punycode",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.punycode.ucs2.encode",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
}
}
},
@@ -58734,12 +58758,16 @@
"priority": "low",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:punycode",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.punycode.version",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
}
}
},
@@ -611,9 +611,9 @@
"apiEntries": 9,
"static": {
"supported": 0,
"partial": 0,
"partial": 8,
"refused": 0,
"not-implemented": 9,
"not-implemented": 1,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -81945,8 +81945,13 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "derived:descendants"
"status": "partial",
"evidence": "derived:descendants",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
},
"anchorSource": "chapter"
},
@@ -81968,8 +81973,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:punycode"
"status": "partial",
"evidence": "surface-manifest:node-builtin.punycode.decode",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
},
"anchorSource": "exact"
},
@@ -81991,8 +82001,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:punycode"
"status": "partial",
"evidence": "surface-manifest:node-builtin.punycode.encode",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
},
"anchorSource": "exact"
},
@@ -82014,8 +82029,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:punycode"
"status": "partial",
"evidence": "surface-manifest:node-builtin.punycode.toASCII",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
},
"anchorSource": "exact"
},
@@ -82037,8 +82057,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:punycode"
"status": "partial",
"evidence": "surface-manifest:node-builtin.punycode.toUnicode",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
},
"anchorSource": "exact"
},
@@ -82083,8 +82108,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:punycode"
"status": "partial",
"evidence": "surface-manifest:node-builtin.punycode.ucs2.decode",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
},
"anchorSource": "exact"
},
@@ -82106,8 +82136,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:punycode"
"status": "partial",
"evidence": "surface-manifest:node-builtin.punycode.ucs2.encode",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
},
"anchorSource": "exact"
},
@@ -82129,8 +82164,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:punycode"
"status": "partial",
"evidence": "surface-manifest:node-builtin.punycode.version",
"tests": [
"tests/corpus/6122-punycode-conversions.ts",
"tests/corpus/6124-punycode-require-warnings.cjs",
"tests/corpus/6126-punycode-dependency/main.cjs"
]
},
"anchorSource": "chapter"
},
@@ -147,6 +147,7 @@
"node-builtin.process.rows": ["tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"],
"node-builtin.process.columns": ["tests/corpus/1448-stream-columns.ts", "tests/harness/terminal-geometry.test.ts"],
"node-builtin.process.exitCode": ["tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"],
"node-builtin.punycode": ["tests/corpus/6122-punycode-conversions.ts", "tests/corpus/6124-punycode-require-warnings.cjs", "tests/corpus/6126-punycode-dependency/main.cjs"],
"node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"],
"node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"],
"node-builtin.stream": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1746-stream-for-await.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/2629-stream-consumers.ts", "tests/corpus/2861-readable-iterator-close.ts"],
+33
View File
@@ -3762,6 +3762,39 @@ declare module "node:querystring" {
export * from "querystring";
}
/* node:punycode (deprecated, DEP0040) — punycode.js 2.1.0: decode and
* toUnicode throw its RangeErrors on malformed input; ucs2 converts
* between strings and UTF-16 code point arrays. */
declare module "punycode" {
export function decode(string: string): string;
export function encode(string: string): string;
export function toUnicode(domain: string): string;
export function toASCII(domain: string): string;
export namespace ucs2 {
function decode(string: string): number[];
function encode(codePoints: readonly number[]): string;
}
export const version: string;
}
declare module "node:punycode" {
export * from "punycode";
}
/* node:constants (deprecated, DEP0008) — one frozen object: os.constants'
* dlopen, errno, priority and signals groups, fs.constants, and
* crypto.constants. A platform's missing constant reads as undefined. */
declare module "constants" {
const constants: { readonly [name: string]: number } & {
readonly defaultCoreCipherList: string;
readonly defaultCipherList: string;
};
export = constants;
}
declare module "node:constants" {
import constants = require("constants");
export = constants;
}
/* node:readline — the question/close slice: createInterface over exactly
* { input: process.stdin, output: process.stdout }, question(query, cb)
* (the query writes to stdout, the callback gets the next line's text),
@@ -774,6 +774,8 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "blob":
case "record":
case "disposable":
case "punycode":
case "constants":
return host.emitGenericLibCall(e);
case "test":
return emitTestLibCall(host, e);
@@ -194,6 +194,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"os.cpusModel": "scr_os_cpus_model",
"os.cpusField": "scr_os_cpus_field",
"os.constants": "scr_os_constants",
"constants.object": "scr_node_constants",
"os.release": "scr_os_release",
"os.userName": "scr_os_user_name",
"os.userShell": "scr_os_user_shell",
@@ -638,6 +639,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
"url.legacyFormat": "scr_url_legacy_format",
"url.legacyResolve": "scr_url_legacy_resolve",
"url.domainToASCII": "scr_url_domain_to_ascii",
"punycode.decode": "scr_punycode_decode",
"punycode.encode": "scr_punycode_encode",
"punycode.toASCII": "scr_punycode_to_ascii",
"punycode.toUnicode": "scr_punycode_to_unicode",
"punycode.ucs2Decode": "scr_punycode_ucs2_decode",
"punycode.ucs2Encode": "scr_str_from_code_point",
"punycode.warn": "scr_punycode_warn",
"url.domainToUnicode": "scr_url_domain_to_unicode",
"url.pathToFileURL": "scr_url_from_path",
"url.pathToFileURLPlatform": "scr_url_from_path_platform",
+2 -1
View File
@@ -57,7 +57,8 @@ Options:
compile the named npm packages' shipped JS statically as
program modules (repeatable; "auto" opts in every eligible
direct import: own .d.ts, unminified JS, no build-transform
markers). Packages that fail preflight are refused with a
markers; "pkg..." also names every package pkg depends
on). Packages that fail preflight are refused with a
diagnostic. Eligible dependencies compile automatically.
--provenance-sources
EXPERIMENTAL: compile npm dependencies from their
+2
View File
@@ -67,6 +67,8 @@ export interface NpmStaticStatus {
status: "static" | "refused";
/** The first refusal reason for a refused package. */
detail?: string;
/** How to act on the refusal, when there is a specific remedy. */
hint?: string;
}
const GREEN = "\x1b[32m";
@@ -877,13 +877,20 @@ export function unsupportedPackageValueDiag(pkg: string, loc: SrcLoc): ScrDiagno
);
}
export function npmStaticRefusalDiag(pkg: string, detail: string, loc: SrcLoc): ScrDiagnostic {
export function npmStaticRefusalDiag(
pkg: string,
detail: string,
loc: SrcLoc,
hint?: string,
): ScrDiagnostic {
return {
code: "SC2013",
message: `cannot compile npm package '${pkg}' statically: ${detail}`,
loc,
milestone: "later",
hint: "use a dependency with supported source, or replace the unsupported operation; scriptc has no interpreter fallback",
hint:
hint ??
"use a dependency with supported source, or replace the unsupported operation; scriptc has no interpreter fallback",
};
}
@@ -59,6 +59,7 @@ export function prepareExecutableModule(
entry.package,
entry.detail ?? "its static compilation was refused",
fe.npmImportSites.get(entry.package) ?? { file: entryPath, start: 0, end: 0 },
entry.hint,
),
),
);
@@ -58,6 +58,7 @@ export function analyzeWithFrontend(
entry.package,
entry.detail ?? "its static compilation was refused",
fe.npmImportSites.get(entry.package) ?? { file: entryPath, start: 0, end: 0 },
entry.hint,
),
),
];
@@ -42,6 +42,8 @@ export const SUPPORTED_BUILTIN_MODULES = [
"module",
"console",
"process",
"punycode",
"constants",
] as const;
/** Builtins Node itself serves ONLY under the node: prefix —
@@ -23,6 +23,7 @@ import {
builtinModulesArrayLit,
stdlibGlobalNameOf,
} from "./surfaces.js";
import { CRYPTO_CONSTANTS } from "./crypto-tables.js";
const PROTOTYPE_METHODS: Record<string, readonly string[]> = {
Date: ["getTime", "valueOf", "toISOString", "toJSON"],
@@ -680,3 +681,34 @@ export function lowerBuiltinLoaderValue(lowerer: Lowerer, loc: SrcLoc): IrExpr {
loc,
};
}
/** The deprecated node:constants module object: the runtime merges
* os.constants' groups and fs.constants with crypto.constants, which a
* lifted function answers from the baked table the first time the object
* is built. */
export function builtinConstantsObject(lowerer: Lowerer, loc: SrcLoc): IrExpr {
const name = "%builtin.constants.crypto";
if (!lowerer.builtinCallableValueFns.has(name)) {
lowerer.builtinCallableValueFns.set(name, name);
const fields = Object.entries(CRYPTO_CONSTANTS).map(([key, value]) => ({
key: strLit(key, loc),
value: box(builtinConstLit(value, loc)),
}));
lowerer.liftedFns.push({
name,
params: [],
returnType: DYN,
locals: [],
body: [{ kind: "return", value: { kind: "dynObjLit", fields, type: DYN, loc }, loc }],
loc,
});
}
const table: IrExpr = {
kind: "closure",
fnName: name,
captures: [],
type: { kind: "func", params: [], ret: DYN },
loc,
};
return { kind: "libCall", fn: "constants.object", args: [box(table)], type: DYN, loc };
}
@@ -230,6 +230,7 @@ import {
} from "./class-prototypes.js";
import { isClassCallback } from "./class-callbacks.js";
import {
builtinConstantsObject,
builtinPrototypeMethod,
lowerArrayIsArrayValue,
lowerCheckedPredicateValue,
@@ -1256,6 +1257,17 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (bi.module === "os" && bi.member === "constants") {
return { kind: "libCall", fn: "os.constants", args: [], type: DYN, loc };
}
// `import { O_RDONLY } from "constants"`: the member of the frozen
// module object.
if (bi.module === "constants") {
return {
kind: "dynKeyGet",
value: builtinConstantsObject(lowerer, loc),
key: { kind: "strLit", value: bi.member, type: STRING, loc },
type: DYN,
loc,
};
}
// tls.rootCertificates: a runtime-valued module constant (the
// cached bundled-CA array) — the one member read that lowers
// to a libCall instead of a baked literal.
@@ -1298,6 +1310,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// their access sites only. The CommonJS namespace binding
// (`const lib = require("./lib.js")`) gets the same fence.
const builtinNamespace = lowerer.builtinNamespaceModuleOf(expr);
if (builtinNamespace === "constants") return builtinConstantsObject(lowerer, loc);
if (builtinNamespace !== null) {
const type: IrType = { kind: "moduleNs", moduleId: `builtin:${builtinNamespace}` };
return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc };
@@ -1587,6 +1600,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
return { kind: "moduleNsRef", moduleId: moduleNs.moduleId, type: moduleNs, loc };
}
const builtinNs = lowerer.builtinNamespaceModuleOf(expr);
if (builtinNs === "constants") return builtinConstantsObject(lowerer, loc);
if (builtinNs !== null) {
const type: IrType = { kind: "moduleNs", moduleId: `builtin:${builtinNs}` };
return { kind: "moduleNsRef", moduleId: type.moduleId, type, loc };
@@ -2664,6 +2664,7 @@ export function lowerFileInit(
loc: loc0,
});
}
if (sf === lowerer.entry) header.push(...lowerer.punycodeLinkWarning());
// Top-level `this` of a CommonJS module: its object exists before the
// module's first statement (lowerCjsTopLevelThis).
const exportsThis = lowerer.cjsExportsThis.get(sf);
@@ -1586,7 +1586,9 @@ function lowerVarDeclarator(
// Relative requires keep the fence: their module-init-at-position
// semantics needs storage this model doesn't represent.
if (canonicalBuiltinModule(spec) !== null) {
return [];
return canonicalBuiltinModule(spec) === "punycode"
? [lowerer.punycodeRequireWarning(decl)]
: [];
}
lowerer.unsupported(
"SC1090",
@@ -142,6 +142,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 { erasedTypeOnlyImport } from "../type-only-imports.js";
import { literalValues } from "../literal-values.js";
import { discriminantOwners, literalUnionArm, type UnionLiteral } from "../union-discriminants.js";
import type { ScrDiagnostic } from "../../diagnostics/diagnostic.js";
@@ -3901,6 +3902,65 @@ export class Lowerer {
for (const fp of parts) if (!indexOf.has(fp.sf)) visit(fp.sf);
}
/** node:punycode's load at a require() call: the module body reports
* DEP0040 when it first runs, unless isInsideNodeModules() finds the
* requiring frame in a dependency, and process.emitWarning delivers the
* report on the next tick. Later loads are cache hits either way. */
punycodeRequireWarning(node: ts.Node): IrStmt {
const loc = locOf(node);
const fromDependency = /[\\/]node_modules[\\/]/.test(node.getSourceFile().fileName);
return {
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "punycode.warn",
args: [
{ kind: "boolLit", value: false, type: BOOL, loc },
{ kind: "boolLit", value: !fromDependency, type: BOOL, loc },
],
type: VOID,
loc,
},
loc,
};
}
/** node:punycode's DEP0040 for a static ES import anywhere in the module
* graph: Node links builtins before any module evaluates, so the report
* comes first, before the program can register a 'warning' listener —
* whichever file imports it (a dependency's ES import has no requiring
* frame either). Prepended to the entry's %init. */
punycodeLinkWarning(): IrStmt[] {
const imports = this.moduleOrder.some((sf) =>
sf.statements.some(
(stmt) =>
((ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) ||
(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly)) &&
stmt.moduleSpecifier !== undefined &&
ts.isStringLiteral(stmt.moduleSpecifier) &&
canonicalBuiltinModule(stmt.moduleSpecifier.text) === "punycode",
),
);
if (!imports) return [];
const loc: SrcLoc = { file: this.entry.fileName, start: 0, end: 0 };
return [
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "punycode.warn",
args: [
{ kind: "boolLit", value: true, type: BOOL, loc },
{ kind: "boolLit", value: true, type: BOOL, loc },
],
type: VOID,
loc,
},
loc,
},
];
}
/** The lowering of a CommonJS `require("./local")` occurrence: a call of
* the required module's run-once %init at exactly this statement's
* position — Node's inline evaluation, with the guard supplying the
@@ -3910,6 +3970,7 @@ export class Lowerer {
* their external dependencies by name). Null for everything else
* (builtins load nothing; the rest kept its preflight fence). */
requireInitStmt(spec: string, node: ts.Node): IrStmt | null {
if (canonicalBuiltinModule(spec) === "punycode") return this.punycodeRequireWarning(node);
// Project requires resolve within the program; another bare require can
// be a program-module edge when it names an opted-in --npm-static package
// (one package requiring another — the resolution answered its shipped
@@ -11731,7 +11792,23 @@ export class Lowerer {
return { module: "timers/promises", member: `scheduler.${access.name.text}` };
}
}
// punycode.ucs2.decode / encode: methods of the module's ucs2 object,
// dispatched as module members.
if (access.name.text === "decode" || access.name.text === "encode") {
const owner = access.expression;
const imported = ts.isIdentifier(owner)
? this.builtinImportOf(owner)
: ts.isPropertyAccessExpression(owner)
? this.builtinMemberOf(owner)
: null;
if (imported?.module === "punycode" && imported.member === "ucs2") {
return { module: "punycode", member: `ucs2.${access.name.text}` };
}
}
const module = this.builtinNamespaceModuleOf(access.expression);
// node:constants is one checked-dynamic object: its members read
// through the object itself (builtinConstantsObject), not the tables.
if (module === "constants") return null;
if (module !== null) return { module, member: access.name.text };
// The events module object IS the EventEmitter class, so the class's
// defaultMaxListeners is the module property.
@@ -1822,6 +1822,22 @@ export const BUILTIN_MODULE_FNS: Record<
valueParams: [{ mode: "required", type: STRING }],
},
},
// node:punycode (deprecated): punycode.js 2.1.0's conversions over
// strings (scr_url.c). ucs2.decode/encode are members of the module's
// ucs2 object, dispatched as module members (builtinMemberOf). The
// DEP0040 report rides the module's load sites (punycodeLoadWarning).
punycode: {
decode: { fn: "punycode.decode", params: [STRING], result: STRING },
encode: { fn: "punycode.encode", params: [STRING], result: STRING },
toASCII: { fn: "punycode.toASCII", params: [STRING], result: STRING },
toUnicode: { fn: "punycode.toUnicode", params: [STRING], result: STRING },
"ucs2.decode": { fn: "punycode.ucs2Decode", params: [STRING], result: arrayOf(F64) },
"ucs2.encode": { fn: "punycode.ucs2Encode", params: [arrayOf(F64)], result: STRING },
},
// node:constants (deprecated) is one frozen object (Object.assign of
// os.constants' groups, fs.constants and crypto.constants): bindings of
// the module read it as a checked-dynamic value (builtinConstantsObject).
constants: {},
// node:readline: createInterface's options are entirely special-cased
// (exactly { input: process.stdin, output?: process.stdout } — see
// lowerBuiltinModuleCall); the entry carries the canonical shape. The
@@ -1951,6 +1967,7 @@ export const BUILTIN_MODULE_CONSTS: Record<
os: { EOL: "\n", devNull: "/dev/null" },
worker_threads: { isMainThread: true, threadId: 0 },
cluster: { isPrimary: true, isMaster: true, isWorker: false },
punycode: { version: "2.1.0" },
// zlib's module-level copies of its constants (every member outside
// BROTLI_*, Node's own loop over zlib.constants).
zlib: Object.fromEntries(
@@ -10,6 +10,7 @@ import { nearestPackageType, resolveBareModule } from "./resolve.js";
import { npmStaticPackageOfPath } from "./npm-static.js";
import { trackedReadFile } from "./input-tracker.js";
import { npmPackageNameOf } from "./workspace-registry.js";
import { typeOnlyNamedImport } from "./type-only-imports.js";
export function isPrunedNpmReexport(program: ts.Program, stmt: ts.ExportDeclaration): boolean {
return program.analysis.prunedNpmReexports.has(stmt);
@@ -257,7 +258,9 @@ export function planNpmStaticReexports(
(clause.namedBindings !== undefined &&
(ts.isNamespaceImport(clause.namedBindings) ||
(ts.isNamedImports(clause.namedBindings) &&
clause.namedBindings.elements.length === 0)))
clause.namedBindings.elements.length === 0))) ||
// `import { type A }` still loads the module, like `import {}`.
typeOnlyNamedImport(stmt)
) {
request(dep, null);
} else {
@@ -269,12 +272,10 @@ export function planNpmStaticReexports(
}
}
} else if (ts.isExportDeclaration(stmt)) {
// `export { type A } from` still loads the module under Node's
// type stripping; only `export type … from` erases.
if (
stmt.isTypeOnly ||
(stmt.exportClause !== undefined &&
ts.isNamedExports(stmt.exportClause) &&
stmt.exportClause.elements.length > 0 &&
stmt.exportClause.elements.every((element) => element.isTypeOnly)) ||
stmt.moduleSpecifier === undefined ||
!ts.isStringLiteral(stmt.moduleSpecifier)
)
+121 -20
View File
@@ -25,9 +25,18 @@ import { isJsSourceFileName } from "./tsc-codes.js";
import { isRelativeSpecifier, packageNameOfSpecifier } from "./workspace-registry.js";
import { lowerToIr, type LowerOptions, type LowerResult } from "./lowering/lowerer.js";
import type { FrontendServices } from "./services.js";
import { isImportDeclaration, isExportDeclaration, isStringLiteral } from "./ts7/adapter.js";
import {
isIdentifier,
isImportDeclaration,
isExportDeclaration,
isStringLiteral,
isStringLiteralLike,
SyntaxKind,
} from "./ts7/adapter.js";
import { moduleSourceCandidates } from "./module-source-candidates.js";
import { CONTRACT_TYPES_MODULE } from "./contract-types.js";
import { THREADS_MODULE } from "./threads-import.js";
import { erasedTypeOnlyImport } from "./type-only-imports.js";
import { EXPERIMENTAL_MODULE } from "./experimental-import.js";
export interface ProgramLoadOptions {
@@ -91,15 +100,36 @@ function detectAutoPackages(
mode: "auto" | "lib" = "auto",
judged?: Set<string>,
sites?: Map<string, SrcLoc>,
transitive: Set<string> = new Set(),
): string[] {
const chosen: string[] = [];
// package → the resolved types file AND the file whose import found it:
// the runtime-JS probe below must resolve from the SAME importing file,
// or a package visible only to a nested package.json realm (a pnpm
// monorepo's packages/*/node_modules, unreachable from the entry's own
// walk-up) answers "no runtime JS" for perfectly ordinary installs.
const seen = new Map<string, { typesFile: string; fromFile: string; spec: string }>();
for (const sf of [...load.moduleOrder, load.entry]) {
const seen = new Map<
string,
{ typesFile: string; fromFile: string; spec: string; owner: string | null }
>();
// A transitive package's files outside the module order (reached through
// nested require() calls the graph walk does not model) still name its
// dependencies.
const scanned = new Set([...load.moduleOrder, load.entry]);
const transitiveFiles =
transitive.size === 0
? []
: load.program.getImplementationSourceFiles().filter((sf) => {
const pkg = npmStaticPackageOfPath(sf.fileName);
return !scanned.has(sf) && pkg !== null && transitive.has(pkg);
});
for (const sf of [...scanned, ...transitiveFiles]) {
if (mode === "auto" && sf.fileName.includes("/node_modules/")) continue;
// `--npm-static=pkg...` opts pkg's whole dependency closure in: every
// package its compiled files import is selected the same explicit way,
// without the declaration eligibility bar.
const owner = npmStaticPackageOfPath(sf.fileName);
const viaTransitive = owner !== null && transitive.has(owner);
const edges: { spec: string; loc: SrcLoc }[] = [];
for (const stmt of sf.statements) {
if (
@@ -107,13 +137,31 @@ function detectAutoPackages(
stmt.moduleSpecifier &&
isStringLiteral(stmt.moduleSpecifier)
) {
// `import type` and `export type … from` erase under Node's type
// stripping: Node never loads the package, so it is no dependency
// to compile (its declarations still type the program).
if (isImportDeclaration(stmt) ? erasedTypeOnlyImport(stmt) : stmt.isTypeOnly) continue;
edges.push({ spec: stmt.moduleSpecifier.text, loc: locOf(stmt) });
} else if (mode === "lib") {
} else if (mode === "lib" && !viaTransitive) {
// CJS packages spell their dep edges as top-level requires; the
// import-statement scan alone would miss every one of them.
for (const req of requiresOf(stmt)) edges.push({ spec: req.spec, loc: locOf(req.node) });
}
}
// Transitive selection counts every literal require() and import()
// anywhere in the package, nested ones included.
if (viaTransitive) {
for (const call of moduleSourceCandidates(load.program, sf).calls) {
const arg = call.arguments[0];
if (
arg !== undefined &&
isStringLiteralLike(arg) &&
(call.expression.kind === SyntaxKind.ImportKeyword ||
(isIdentifier(call.expression) && call.expression.text === "require"))
)
edges.push({ spec: arg.text, loc: locOf(call) });
}
}
for (const { spec, loc } of edges) {
if (load.externalTypes?.has(spec)) continue;
// scriptc's own packages are not npm dependencies: contract types are
@@ -137,28 +185,51 @@ function detectAutoPackages(
if (judged?.has(packageNameOfSpecifier(spec))) continue;
const npm = resolveNpmImport(sf.fileName, spec);
if (npm !== null && isNodeTypesPath(npm.typesFile)) continue;
if (npm === null) {
if (mode !== "lib") continue;
const js = resolveBareModule(sf.fileName, spec, "js-only");
if (js === null || judged!.has(js.packageName)) continue;
judged!.add(js.packageName);
sites!.set(js.packageName, loc);
// The runtime package Node loads. Types-first resolution answers an
// @types twin's declarations for a package that ships none, but
// selection and every refusal name the package that has the code.
const js =
mode === "lib" && (npm === null || npm.packageName.startsWith("@types/"))
? resolveBareModule(sf.fileName, spec, "js-only")
: null;
const runtimePkg = js !== null && js.packageName !== npm?.packageName ? js.packageName : null;
if (npm === null || runtimePkg !== null) {
if (mode !== "lib" || runtimePkg === null || judged!.has(runtimePkg)) continue;
judged!.add(runtimePkg);
sites!.set(runtimePkg, loc);
if (viaTransitive) {
transitive.add(runtimePkg);
chosen.push(runtimePkg);
continue;
}
statuses.push({
package: js.packageName,
package: runtimePkg,
status: "refused",
detail: "it ships no own .d.ts declaration surface",
detail:
npm === null
? "it ships no own .d.ts declaration surface"
: `its declared types come from the separate ${npm.packageName} package, not the package itself`,
hint: npmStaticOptInHint(runtimePkg, owner),
});
continue;
}
if (judged?.has(npm.packageName)) continue;
// An @types twin with no runtime package installed has nothing to
// compile (Node cannot load it either).
if (viaTransitive && !npm.packageName.startsWith("@types/")) {
judged!.add(npm.packageName);
sites!.set(npm.packageName, loc);
transitive.add(npm.packageName);
chosen.push(npm.packageName);
continue;
}
if (!seen.has(npm.packageName)) {
seen.set(npm.packageName, { typesFile: npm.typesFile, fromFile: sf.fileName, spec });
seen.set(npm.packageName, { typesFile: npm.typesFile, fromFile: sf.fileName, spec, owner });
sites?.set(npm.packageName, loc);
}
}
}
const chosen: string[] = [];
for (const [pkg, { typesFile, fromFile, spec }] of seen) {
for (const [pkg, { typesFile, fromFile, spec, owner }] of seen) {
judged?.add(pkg);
// A package without a root entry (no "main", "exports" or index file)
// can still serve the subpath the program imports.
@@ -175,11 +246,23 @@ function detectAutoPackages(
package: pkg,
status: "refused",
detail: mode === "lib" ? reason : `auto: ${reason}`,
// Naming a package bypasses the declaration and shipped-JS
// heuristics; it cannot supply JavaScript that does not resolve.
...(mode === "lib" && jsEntry !== null ? { hint: npmStaticOptInHint(pkg, owner) } : {}),
});
}
return chosen;
}
/** How to compile a package the declaration bar refused anyway: name it,
* or (for a dependency of an opted-in package) opt in that package's
* whole dependency closure. */
function npmStaticOptInHint(pkg: string, importer: string | null): string {
return importer === null
? `to compile it anyway, name it with --npm-static=${pkg} (or --npm-static=${pkg}... to include its dependencies)`
: `to compile it anyway, name it with --npm-static=${pkg}, or use --npm-static=${importer}... to include all of ${importer}'s dependencies`;
}
/** The opted-in packages a consumer-anchored tsc message NAMES: module
* specifiers in `Module '"spec"'` phrasings, and resolved file paths in
* `import("…")` type spellings — the two ways the checker points at an
@@ -227,9 +310,23 @@ export function runFrontend(
// selections bypass eligibility heuristics, but never compilation checks.
const explicit: readonly string[] =
npmStatic !== undefined && typeof npmStatic !== "string" ? npmStatic : [];
return loadFrontend(entryPath, trackedLoader, "lib", externalTypes, [
...new Set([...libraryNpmStatic, ...explicit]),
]);
// A trailing "..." (`--npm-static=fs-extra...`) selects the package and
// its whole dependency closure, which the library fixpoint discovers.
const transitive = new Set<string>();
const names = [...libraryNpmStatic, ...explicit].map((name) => {
if (!name.endsWith("...") || name.length === 3) return name;
const base = name.slice(0, -3);
transitive.add(base);
return base;
});
return loadFrontend(
entryPath,
trackedLoader,
"lib",
externalTypes,
[...new Set(names)],
transitive,
);
} catch (error) {
for (const load of active) {
// Preserve the original failure and attempt every remaining cleanup.
@@ -249,6 +346,7 @@ function loadFrontend(
npmStatic?: readonly string[] | "auto" | "lib",
externalTypes?: Readonly<Record<string, string>>,
libraryNpmStatic: readonly string[] = [],
transitive: Set<string> = new Set(),
): Frontend {
// Resolver package/workspace metadata is intentionally shared across the
// several load attempts of ONE auto-detection fixpoint, but never across
@@ -270,7 +368,10 @@ function loadFrontend(
const scoutPreflight = checkPreflight(scout);
requested =
npmStatic === "lib"
? [...libraryNpmStatic, ...detectAutoPackages(scout, statuses, "lib", judged, npmSites)]
? [
...libraryNpmStatic,
...detectAutoPackages(scout, statuses, "lib", judged, npmSites, transitive),
]
: detectAutoPackages(scout, statuses);
// With no package to opt in, the scout already IS the final frontend:
// same roots, resolution posture, preflight, and module order. Retain it
@@ -328,7 +429,7 @@ function loadFrontend(
// at least one new package for good).
if (npmStatic === "lib") {
for (;;) {
const grown = detectAutoPackages(load, statuses, "lib", judged, npmSites);
const grown = detectAutoPackages(load, statuses, "lib", judged, npmSites, transitive);
if (grown.length === 0) break;
requested = [...requested, ...grown];
load.dispose();
+34 -49
View File
@@ -119,6 +119,11 @@ import {
import { trackedFileExists, trackedReadFile, trackedRealpath } from "./input-tracker.js";
import { forkTargetPaths } from "./fork-target.js";
import { workerTargetPaths } from "./worker-target.js";
import {
erasedTypeOnlyImport,
typeOnlyNamedImport,
typeOnlyReachedFiles,
} from "./type-only-imports.js";
import { moduleSourceCandidates } from "./module-source-candidates.js";
import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js";
@@ -2146,7 +2151,7 @@ function insideFunctionLike7(node: ts.Node): boolean {
function nodeEsmSyntaxMarker7(sf: ts.SourceFile): ts.Node | null {
for (const stmt of sf.statements) {
if (ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) return stmt;
if (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly && !erasedTypeOnlyReexport(stmt)) {
if (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly) {
return stmt;
}
if (ts.isExportAssignment(stmt)) return stmt;
@@ -2829,6 +2834,7 @@ export function makeCycleAdmission(
const clause = e.stmt.importClause;
if (
clause === undefined ||
typeOnlyNamedImport(e.stmt) ||
(clause.name === undefined &&
(clause.namedBindings === undefined ||
(ts.isNamespaceImport(clause.namedBindings) &&
@@ -4299,21 +4305,41 @@ function preflight7(load: LoadResult): {
npmStaticPackageOfPath(sf.fileName) !== null) &&
!unselectedPackageJsFile(sf.fileName),
);
// Files reached only through `import type` / `export type … from` are
// checker input, never program modules: Node does not load them, so
// their imports and syntax must not fence the program.
const resolveRuntimeEdge = (
sf: ts.SourceFile,
spec: string,
resolutionKind?: "import" | "require",
): ts.SourceFile | null =>
resolveImport7(program, sf, spec, resolutionKind) ??
npmStaticDepSf7(program, sf, spec, resolutionKind);
const typeOnlyFiles = typeOnlyReachedFiles(
program,
entry,
programFiles,
() => [
...forkTargetPaths(program, program.getImplementationSourceFiles()),
...workerTargetPaths(program, program.getImplementationSourceFiles()),
],
resolveRuntimeEdge,
);
const runtimeFiles =
typeOnlyFiles.size === 0 ? programFiles : programFiles.filter((sf) => !typeOnlyFiles.has(sf));
const userFiles = npmStaticActive()
? planNpmStaticReexports(
program,
entry,
programFiles,
runtimeFiles,
[
...createRequireProgramRoots7(program),
...forkTargetPaths(program, program.getImplementationSourceFiles()),
...workerTargetPaths(program, program.getImplementationSourceFiles()),
],
(sf, spec, resolutionKind) =>
resolveImport7(program, sf, spec, resolutionKind) ??
npmStaticDepSf7(program, sf, spec, resolutionKind),
resolveRuntimeEdge,
)
: programFiles;
: runtimeFiles;
program.getTypeChecker().prefetchSourceFileStructures(userFiles);
// Unselected node_modules JS is not native program source even when
@@ -4438,8 +4464,7 @@ function preflight7(load: LoadResult): {
continue;
}
if (ts.isExportDeclaration(stmt)) {
if (stmt.isTypeOnly || erasedTypeOnlyReexport(stmt) || isPrunedNpmReexport(program, stmt))
continue;
if (stmt.isTypeOnly || isPrunedNpmReexport(program, stmt)) continue;
if (!stmt.moduleSpecifier) continue;
const fromSpec = ts.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : "";
if (load.externalTypes.has(fromSpec)) {
@@ -5708,42 +5733,6 @@ export function isCheckedJsSourceFile(sf: ts.SourceFile, projectCheckJs: boolean
return checked;
}
/** `import type …` declarations and named clauses whose every element is
* `type`-qualified are ERASED by tsc at emit — Node's module graph has no
* edge there, so module order, cycle detection, init calls, and the
* startup-refusal probe must not count them either (zod v4's core modules
* are "cyclic" only through `import type * as ns` namespaces — 27 phantom
* SC1016 sites against 1 real value-level cycle; and a type-only import
* of a module Node cannot resolve is NOT a startup crash — Node never
* resolves it). Originated as the provenance-sources pilot's flag-gated
* prototype (SCRIPTC_TYPE_ONLY_EDGES), promoted to always-on with the
* full-suite differential green. Side-effect imports (no clause) stay
* real edges. */
function erasedTypeOnlyImport(stmt: ts.ImportDeclaration): boolean {
const clause = stmt.importClause;
if (clause === undefined) return false; // side-effect import: a real edge
if (clause.phaseModifier === ts.SyntaxKind.TypeKeyword) return true;
return (
clause.name === undefined &&
clause.namedBindings !== undefined &&
ts.isNamedImports(clause.namedBindings) &&
clause.namedBindings.elements.length > 0 &&
clause.namedBindings.elements.every((e) => e.isTypeOnly)
);
}
/** The re-export twin: `export { type X } from "./m"` with every element
* `type`-qualified erases at emit exactly like `export type { X } from`
* (which stmt.isTypeOnly already covers) — no module edge. */
function erasedTypeOnlyReexport(stmt: ts.ExportDeclaration): boolean {
return (
stmt.exportClause !== undefined &&
ts.isNamedExports(stmt.exportClause) &&
stmt.exportClause.elements.length > 0 &&
stmt.exportClause.elements.every((e) => e.isTypeOnly)
);
}
export function orderedImportsOf(
program: ts.Program,
sf: ts.SourceFile,
@@ -5751,11 +5740,7 @@ export function orderedImportsOf(
const out: { stmt: ts.Statement; dep: ts.SourceFile | null }[] = [];
for (const stmt of sf.statements) {
if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue;
if (
ts.isExportDeclaration(stmt) &&
(stmt.isTypeOnly || erasedTypeOnlyReexport(stmt) || !stmt.moduleSpecifier)
)
continue;
if (ts.isExportDeclaration(stmt) && (stmt.isTypeOnly || !stmt.moduleSpecifier)) continue;
if (!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) continue;
if (ts.isImportDeclaration(stmt) && erasedTypeOnlyImport(stmt)) continue;
if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
+47 -13
View File
@@ -1701,16 +1701,38 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// Declaration-only package types have no compiled implementation.
// Primitive and container types retain their structural mappings; types
// owned by the standard library or explicit host mappings are handled
// by their dedicated native rules.
const npmSym = widened.getAliasSymbol() ?? widened.getSymbol();
const npmDecls = npmSym ? checker.declarationsOf(npmSym) : undefined;
// by their dedicated native rules. Pure type shapes — interfaces, type
// aliases, and object or mapped type literals with no value declaration
// merged in — describe data, not an implementation: a program value of
// such a type is one the program built itself (`import type` from a
// types-only package), so it maps structurally like a local declaration.
// Values the package itself would produce stay refused at their import.
const packageOnly = (sym: ts.Symbol | undefined): readonly ts.Node[] | null => {
const decls = sym ? checker.declarationsOf(sym) : undefined;
return decls &&
decls.length > 0 &&
decls.every((d) => {
const sf = d.getSourceFile();
return sf.isDeclarationFile && !ctx.isStdlibFile(sf) && !ctx.isExternalTypeFile(sf);
})
? decls
: null;
};
const pureTypeShape = (decls: readonly ts.Node[] | null): boolean =>
decls === null ||
decls.every(
(d) =>
packageTypeFile(d.getSourceFile(), ctx) &&
(ts.isInterfaceDeclaration(d) ||
ts.isTypeAliasDeclaration(d) ||
ts.isTypeLiteralNode(d) ||
ts.isMappedTypeNode(d)),
);
const aliasDecls = packageOnly(widened.getAliasSymbol());
const ownDecls = packageOnly(widened.getSymbol());
if (
npmDecls &&
npmDecls.length > 0 &&
npmDecls.every((d) => {
const sf = d.getSourceFile();
return sf.isDeclarationFile && !ctx.isStdlibFile(sf) && !ctx.isExternalTypeFile(sf);
})
(aliasDecls !== null || (widened.getAliasSymbol() === undefined && ownDecls !== null)) &&
!(pureTypeShape(aliasDecls) && pureTypeShape(ownDecls))
) {
return null;
}
@@ -5421,6 +5443,17 @@ function isMappedShape(t: ts.Type): boolean {
);
}
/** A declaration file an installed package ships (its own or an @types
* twin), outside the standard library and @types/node: the type surface
* of a package the program does not compile. Its pure type shapes —
* interfaces, aliases, type literals — describe data a program can build
* itself (`import type` from a types-only package such as type-fest). */
function packageTypeFile(sf: ts.SourceFile, ctx: Pick<TypeMapperCtx, "isStdlibFile">): boolean {
return (
sf.isDeclarationFile && /[\\/]node_modules[\\/]/.test(sf.fileName) && !ctx.isStdlibFile(sf)
);
}
/** The record path's provenance fence. Declared shapes (object literals,
* interfaces, type literals) must come from user code or an explicitly
* mapped external type surface, never an arbitrary .d.ts — the empty
@@ -5459,13 +5492,14 @@ function recordProvenanceOk(t: ts.Type, ctx: TypeMapperCtx): boolean {
.every(
(decl) =>
!decl.getSourceFile().isDeclarationFile ||
ctx.isExternalTypeFile(decl.getSourceFile()),
ctx.isExternalTypeFile(decl.getSourceFile()) ||
packageTypeFile(decl.getSourceFile(), ctx),
),
);
}
return !decls.some((d) => {
const sf = d.getSourceFile();
return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf);
return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx);
});
}
@@ -5922,7 +5956,7 @@ function mapRecordTypeInner(
computed &&
checker.declarationsOf(p).some((d) => {
const sf = d.getSourceFile();
return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf);
return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx);
})
) {
return null;
@@ -6249,7 +6283,7 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx
computed &&
checker.declarationsOf(p).some((d) => {
const sf = d.getSourceFile();
return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf);
return sf.isDeclarationFile && !ctx.isExternalTypeFile(sf) && !packageTypeFile(sf, ctx);
})
) {
return null;
@@ -0,0 +1,143 @@
/** Type-only module edges under Node's type stripping.
*
* Node runs TypeScript by erasing types in place, with the semantics of
* `verbatimModuleSyntax`: `import type …` and `export type … from` vanish,
* but `import { type A } from "./m"` and `export { type A } from "./m"`
* keep an empty clause (`import {} from "./m"`) and still load and
* evaluate `./m`. Only the erased forms are absent from Node's module
* graph. */
import * as ts from "./ts7/adapter.js";
import { moduleSourceCandidates } from "./module-source-candidates.js";
/** `import type …` (default, named, or namespace): erased by Node, so
* module order, cycle detection, init calls, package selection, and the
* startup-refusal probe must not count it (zod v4's core modules are
* "cyclic" only through `import type * as ns` namespaces, and a
* type-only import of a module Node cannot resolve is not a startup
* crash). Side-effect imports and inline `type` specifiers stay real
* edges. */
export function erasedTypeOnlyImport(stmt: ts.ImportDeclaration): boolean {
return stmt.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword;
}
/** `import { type A, type B } from "./m"`: a real module edge that binds
* nothing at runtime, exactly like a side-effect import. */
export function typeOnlyNamedImport(stmt: ts.ImportDeclaration): boolean {
const clause = stmt.importClause;
return (
clause !== undefined &&
clause.phaseModifier !== ts.SyntaxKind.TypeKeyword &&
clause.name === undefined &&
clause.namedBindings !== undefined &&
ts.isNamedImports(clause.namedBindings) &&
clause.namedBindings.elements.length > 0 &&
clause.namedBindings.elements.every((e) => e.isTypeOnly)
);
}
/** One module reference of a program file: its specifier and whether
* Node erases it. */
interface ModuleReference {
spec: string;
erased: boolean;
kind?: "import" | "require";
}
function declarationReferencesOf(sf: ts.SourceFile): ModuleReference[] {
const refs: ModuleReference[] = [];
for (const stmt of sf.statements) {
if (ts.isImportDeclaration(stmt)) {
if (ts.isStringLiteral(stmt.moduleSpecifier))
refs.push({ spec: stmt.moduleSpecifier.text, erased: erasedTypeOnlyImport(stmt) });
} else if (ts.isExportDeclaration(stmt)) {
if (stmt.moduleSpecifier !== undefined && ts.isStringLiteral(stmt.moduleSpecifier))
refs.push({ spec: stmt.moduleSpecifier.text, erased: stmt.isTypeOnly });
} else if (
ts.isImportEqualsDeclaration(stmt) &&
ts.isExternalModuleReference(stmt.moduleReference) &&
ts.isStringLiteralLike(stmt.moduleReference.expression)
) {
refs.push({
spec: stmt.moduleReference.expression.text,
erased: stmt.isTypeOnly,
kind: "require",
});
}
}
return refs;
}
/** Every call with a leading string literal counts as a loading edge —
* require(), import(), createRequire'd require functions, and loader
* helpers alike. Over-counting only keeps a file in the graph. */
function callReferencesOf(program: ts.Program, sf: ts.SourceFile): ModuleReference[] {
const refs: ModuleReference[] = [];
for (const node of moduleSourceCandidates(program, sf).calls) {
const arg = node.arguments[0];
if (arg === undefined || !ts.isStringLiteralLike(arg)) continue;
refs.push({
spec: arg.text,
erased: false,
kind: node.expression.kind === ts.SyntaxKind.ImportKeyword ? "import" : "require",
});
}
return refs;
}
/** The program files Node never loads because the only references that
* reach them are erased type-only forms (directly or through other such
* files). A file referenced by nothing at all (a program root, or a file
* reached through a reference this walk does not model, such as an
* `import("./m")` type query) keeps its place, as do `extraRoots` (fork
* and worker targets). Empty when no erased reference resolves to a
* program file — the common case pays one statement scan per file. */
export function typeOnlyReachedFiles(
program: ts.Program,
entry: ts.SourceFile,
files: readonly ts.SourceFile[],
extraRoots: () => readonly string[],
resolveEdge: (
from: ts.SourceFile,
spec: string,
resolutionKind?: "import" | "require",
) => ts.SourceFile | null,
): Set<ts.SourceFile> {
const available = new Set(files);
const declarationRefs = new Map(files.map((sf) => [sf, declarationReferencesOf(sf)] as const));
const target = (sf: ts.SourceFile, ref: ModuleReference): ts.SourceFile | null => {
const dep = resolveEdge(sf, ref.spec, ref.kind);
return dep === null || dep === sf || !available.has(dep) ? null : dep;
};
if (
!files.some((sf) =>
declarationRefs.get(sf)!.some((ref) => ref.erased && target(sf, ref) !== null),
)
)
return new Set();
const valueEdges = new Map<ts.SourceFile, ts.SourceFile[]>();
const referenced = new Set<ts.SourceFile>();
for (const sf of files) {
const deps: ts.SourceFile[] = [];
valueEdges.set(sf, deps);
for (const ref of [...declarationRefs.get(sf)!, ...callReferencesOf(program, sf)]) {
const dep = target(sf, ref);
if (dep === null) continue;
referenced.add(dep);
if (!ref.erased) deps.push(dep);
}
}
const roots = files.filter((sf) => sf === entry || !referenced.has(sf));
for (const path of extraRoots()) {
const sf = program.getSourceFile(path);
if (sf !== undefined && available.has(sf)) roots.push(sf);
}
const reached = new Set<ts.SourceFile>();
const queue = [...roots];
while (queue.length > 0) {
const sf = queue.pop()!;
if (reached.has(sf)) continue;
reached.add(sf);
for (const dep of valueEdges.get(sf) ?? []) if (!reached.has(dep)) queue.push(dep);
}
return new Set(files.filter((sf) => !reached.has(sf)));
}
@@ -489,6 +489,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"str.encodeUri",
"qs.escape",
"qs.stringify",
// node:punycode's RangeErrors (encode/toASCII only past 32-bit deltas)
// and String.fromCodePoint's for ucs2.encode.
"punycode.decode",
"punycode.encode",
"punycode.toASCII",
"punycode.toUnicode",
"punycode.ucs2Encode",
// The base64 globals: atob/btoa throw the catchable DOMException
// InvalidCharacterError on malformed input; the zero-argument form
// always throws Node's TypeError [ERR_MISSING_ARGS].
@@ -563,6 +563,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** os.constants: the per-thread null-prototype constants object (dlopen,
* errno, signals, priority, UV_UDP_REUSEADDR). Never throws. */
"os.constants": { argTypes: [], result: DYN },
/** The deprecated node:constants object (scr_os.c): the os groups and
* fs.constants merged with the borrowed baked crypto.constants table,
* frozen, one per thread. */
"constants.object": { argTypes: [DYN], result: DYN },
/** umask(2): arg < 0 reads without setting (umask has no read-only form
* — set 0, restore); otherwise sets and answers the PREVIOUS mask.
* Never throws. */
@@ -861,6 +865,19 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"url.legacyResolve": { argTypes: [DYN, DYN, BOOL], result: STRING },
"url.domainToASCII": { argTypes: [DYN, BOOL], result: STRING },
"url.domainToUnicode": { argTypes: [DYN, BOOL], result: STRING },
/** node:punycode (scr_url.c): punycode.js 2.1.0's string conversions;
* decode/toUnicode (and, past 32-bit deltas, encode/toASCII) throw the
* module's RangeErrors, ucs2Encode String.fromCodePoint's. warn marks the
* module's first load (scr_async_dyn.c) and reports DEP0040 then unless
* the second argument is false: at once for a static ES import, on the
* next tick for a require() call. */
"punycode.decode": { argTypes: [STRING], result: STRING },
"punycode.encode": { argTypes: [STRING], result: STRING },
"punycode.toASCII": { argTypes: [STRING], result: STRING },
"punycode.toUnicode": { argTypes: [STRING], result: STRING },
"punycode.ucs2Decode": { argTypes: [STRING], result: arrayOf(F64) },
"punycode.ucs2Encode": { argTypes: [arrayOf(F64)], result: STRING },
"punycode.warn": { argTypes: [BOOL, BOOL], result: VOID },
"url.fileURLToPathBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 },
"url.pathToFileURLChecked": { argTypes: [DYN, DYN], result: URL_T },
"url.fileURLToPathStr": { argTypes: [STRING], result: STRING },
@@ -46,6 +46,8 @@ export interface RuntimeFeatures {
}
const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// DEP0040 rides the dyn-async unit's warning machinery.
"punycode.warn",
"fs.callbackValue",
"fs.callbackCall",
"fs.callbackGlob",
+57
View File
@@ -466,6 +466,13 @@
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.constants",
"kind": "node-builtin",
"name": "constants",
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.crypto",
"kind": "node-builtin",
@@ -2107,6 +2114,56 @@
"status": "static",
"note": "shared version dictionary containing node and openssl; other components are absent unless defined by the program"
},
{
"id": "node-builtin.punycode",
"kind": "node-builtin",
"name": "punycode",
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
{
"id": "node-builtin.punycode.decode",
"kind": "node-builtin",
"name": "punycode.decode",
"status": "static"
},
{
"id": "node-builtin.punycode.encode",
"kind": "node-builtin",
"name": "punycode.encode",
"status": "static"
},
{
"id": "node-builtin.punycode.toASCII",
"kind": "node-builtin",
"name": "punycode.toASCII",
"status": "static"
},
{
"id": "node-builtin.punycode.toUnicode",
"kind": "node-builtin",
"name": "punycode.toUnicode",
"status": "static"
},
{
"id": "node-builtin.punycode.ucs2.decode",
"kind": "node-builtin",
"name": "punycode.ucs2.decode",
"status": "static"
},
{
"id": "node-builtin.punycode.ucs2.encode",
"kind": "node-builtin",
"name": "punycode.ucs2.encode",
"status": "static"
},
{
"id": "node-builtin.punycode.version",
"kind": "node-builtin",
"name": "punycode.version",
"status": "static",
"note": "constant value read"
},
{
"id": "node-builtin.querystring",
"kind": "node-builtin",
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/corpus/6120-type-only-import-graph/mixed.ts",
"<repo>/tests/corpus/6120-type-only-import-graph/flags.ts",
"<repo>/tests/corpus/6120-type-only-import-graph/main.ts"
],
"diags": []
}
@@ -0,0 +1,10 @@
{
"order": [
"<repo>/tests/corpus/6121-type-specifier-edges/config.ts",
"<repo>/tests/corpus/6121-type-specifier-edges/config-barrel.ts",
"<repo>/tests/corpus/6121-type-specifier-edges/b.ts",
"<repo>/tests/corpus/6121-type-specifier-edges/a.ts",
"<repo>/tests/corpus/6121-type-specifier-edges/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6122-punycode-conversions.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6124-punycode-require-warnings.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6125-constants-module.cjs"
],
"diags": []
}
@@ -0,0 +1,18 @@
{
"order": [
"<repo>/tests/corpus/6126-punycode-dependency/main.cjs"
],
"diags": [
{
"code": "SC1010",
"message": "the 'puny-dep' module is not supported yet",
"loc": {
"file": "<repo>/tests/corpus/6126-punycode-dependency/main.cjs",
"start": 347,
"end": 376
},
"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, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
}
]
}
+5
View File
@@ -212,6 +212,11 @@ void scr_process_on_warning(ScrDyn *fn);
void scr_process_off_warning(ScrDyn *fn);
void scr_process_emit_warning(ScrDyn *args);
void scr_emit_warning(const char *name, const char *code, ScrStr *message);
/* node:punycode's first load (scr_async_dyn.c): later calls are module
* cache hits. The first reports DEP0040 when `report` (false for a require
* made from node_modules) — at once for a static ES import, which Node
* links before any module evaluates, otherwise on the next tick. */
void scr_punycode_warn(bool immediate, bool report);
/* scr_emit_warning once a 'warning' listener exists (scr_lib.c; set by the
* listener registration): units that cannot link the dyn-async unit route
* their warnings through it, and print the default report otherwise. */
+21
View File
@@ -904,6 +904,27 @@ void scr_warning_emit_deferred(ScrDyn *w) {
scr_warning_defer(w, scr_warning_tick);
}
static SCR_TL bool scr_punycode_loaded = false;
void scr_punycode_warn(bool immediate, bool report) {
if (scr_punycode_loaded) return;
scr_punycode_loaded = true;
if (!report) return;
static const char text[] =
"The `punycode` module is deprecated. Please use a userland alternative instead.";
ScrStr *message = scr_str_new(text, sizeof text - 1);
ScrStr *code = scr_str_new("DEP0040", 7);
ScrDyn *w = scr_warning_new(message, "DeprecationWarning", code);
if (immediate) {
if (!scr_process_deprecation_flag_on(0)) scr_warning_dispatch(w);
} else {
scr_warning_emit_deferred(w);
}
scr_dyn_release(w);
scr_str_release(code);
scr_str_release(message);
}
/* The console members' warnings. Node's emitWarning emits on
* process.nextTick, so these queue in order and each rides one raw tick
* on the shared tick queue; any left at exit are dropped with it. */
+194
View File
@@ -27,6 +27,8 @@
#include <sys/utsname.h>
#include <unistd.h>
#endif
#include <fcntl.h>
#include <sys/stat.h>
#ifdef __linux__
#include <sys/sysinfo.h>
#endif
@@ -1332,3 +1334,195 @@ ScrDyn *scr_os_constants(void) {
scr_atexit(scr_os_constants_clear);
return scr_dyn_retain(os);
}
/* ── node:constants (deprecated, DEP0008) ──────────────────────────────
* Node's lib/constants.js: Object.assign({}, os.constants.dlopen, errno,
* priority, signals, fs.constants, crypto.constants), frozen, with an
* ordinary prototype. The fs group follows node_constants.cc's
* DefineFsConstants order and guards (libuv's UV_* values are fixed on
* POSIX); crypto_table (borrowed) is a compiler-emitted function that
* answers the baked crypto.constants table, called only on first use. One
* object per thread. */
#define SCR_FS_CONSTANT(o, name) scr_os_constant((o), #name, (double)(name))
static void scr_fs_constants(ScrDyn *o) {
scr_os_constant(o, "UV_FS_SYMLINK_DIR", 1);
scr_os_constant(o, "UV_FS_SYMLINK_JUNCTION", 2);
#ifdef O_RDONLY
SCR_FS_CONSTANT(o, O_RDONLY);
#endif
#ifdef O_WRONLY
SCR_FS_CONSTANT(o, O_WRONLY);
#endif
#ifdef O_RDWR
SCR_FS_CONSTANT(o, O_RDWR);
#endif
scr_os_constant(o, "UV_DIRENT_UNKNOWN", 0);
scr_os_constant(o, "UV_DIRENT_FILE", 1);
scr_os_constant(o, "UV_DIRENT_DIR", 2);
scr_os_constant(o, "UV_DIRENT_LINK", 3);
scr_os_constant(o, "UV_DIRENT_FIFO", 4);
scr_os_constant(o, "UV_DIRENT_SOCKET", 5);
scr_os_constant(o, "UV_DIRENT_CHAR", 6);
scr_os_constant(o, "UV_DIRENT_BLOCK", 7);
#ifdef S_IFMT
SCR_FS_CONSTANT(o, S_IFMT);
#endif
#ifdef S_IFREG
SCR_FS_CONSTANT(o, S_IFREG);
#endif
#ifdef S_IFDIR
SCR_FS_CONSTANT(o, S_IFDIR);
#endif
#ifdef S_IFCHR
SCR_FS_CONSTANT(o, S_IFCHR);
#endif
#ifdef S_IFBLK
SCR_FS_CONSTANT(o, S_IFBLK);
#endif
#ifdef S_IFIFO
SCR_FS_CONSTANT(o, S_IFIFO);
#endif
#ifdef S_IFLNK
SCR_FS_CONSTANT(o, S_IFLNK);
#endif
#ifdef S_IFSOCK
SCR_FS_CONSTANT(o, S_IFSOCK);
#endif
#ifdef O_CREAT
SCR_FS_CONSTANT(o, O_CREAT);
#endif
#ifdef O_EXCL
SCR_FS_CONSTANT(o, O_EXCL);
#endif
/* libuv's Windows-only open flags are 0 everywhere else. */
scr_os_constant(o, "UV_FS_O_FILEMAP", 0);
scr_os_constant(o, "UV_FS_O_TEMPORARY", 0);
scr_os_constant(o, "UV_FS_O_SHORT_LIVED", 0);
scr_os_constant(o, "UV_FS_O_SEQUENTIAL", 0);
scr_os_constant(o, "UV_FS_O_RANDOM", 0);
#ifdef O_NOCTTY
SCR_FS_CONSTANT(o, O_NOCTTY);
#endif
#ifdef O_TRUNC
SCR_FS_CONSTANT(o, O_TRUNC);
#endif
#ifdef O_APPEND
SCR_FS_CONSTANT(o, O_APPEND);
#endif
#ifdef O_DIRECTORY
SCR_FS_CONSTANT(o, O_DIRECTORY);
#endif
#ifdef O_NOATIME
SCR_FS_CONSTANT(o, O_NOATIME);
#endif
#ifdef O_NOFOLLOW
SCR_FS_CONSTANT(o, O_NOFOLLOW);
#endif
#ifdef O_SYNC
SCR_FS_CONSTANT(o, O_SYNC);
#endif
#ifdef O_DSYNC
SCR_FS_CONSTANT(o, O_DSYNC);
#endif
#ifdef O_SYMLINK
SCR_FS_CONSTANT(o, O_SYMLINK);
#endif
#ifdef O_DIRECT
SCR_FS_CONSTANT(o, O_DIRECT);
#endif
#ifdef O_NONBLOCK
SCR_FS_CONSTANT(o, O_NONBLOCK);
#endif
#ifdef S_IRWXU
SCR_FS_CONSTANT(o, S_IRWXU);
#endif
#ifdef S_IRUSR
SCR_FS_CONSTANT(o, S_IRUSR);
#endif
#ifdef S_IWUSR
SCR_FS_CONSTANT(o, S_IWUSR);
#endif
#ifdef S_IXUSR
SCR_FS_CONSTANT(o, S_IXUSR);
#endif
#ifdef S_IRWXG
SCR_FS_CONSTANT(o, S_IRWXG);
#endif
#ifdef S_IRGRP
SCR_FS_CONSTANT(o, S_IRGRP);
#endif
#ifdef S_IWGRP
SCR_FS_CONSTANT(o, S_IWGRP);
#endif
#ifdef S_IXGRP
SCR_FS_CONSTANT(o, S_IXGRP);
#endif
#ifdef S_IRWXO
SCR_FS_CONSTANT(o, S_IRWXO);
#endif
#ifdef S_IROTH
SCR_FS_CONSTANT(o, S_IROTH);
#endif
#ifdef S_IWOTH
SCR_FS_CONSTANT(o, S_IWOTH);
#endif
#ifdef S_IXOTH
SCR_FS_CONSTANT(o, S_IXOTH);
#endif
#ifdef F_OK
SCR_FS_CONSTANT(o, F_OK);
#endif
#ifdef R_OK
SCR_FS_CONSTANT(o, R_OK);
#endif
#ifdef W_OK
SCR_FS_CONSTANT(o, W_OK);
#endif
#ifdef X_OK
SCR_FS_CONSTANT(o, X_OK);
#endif
scr_os_constant(o, "UV_FS_COPYFILE_EXCL", 1);
scr_os_constant(o, "COPYFILE_EXCL", 1);
scr_os_constant(o, "UV_FS_COPYFILE_FICLONE", 2);
scr_os_constant(o, "COPYFILE_FICLONE", 2);
scr_os_constant(o, "UV_FS_COPYFILE_FICLONE_FORCE", 4);
scr_os_constant(o, "COPYFILE_FICLONE_FORCE", 4);
}
#undef SCR_FS_CONSTANT
static SCR_TL ScrDyn *scr_node_constants_value;
static void scr_node_constants_clear(void) {
ScrDyn *value = scr_node_constants_value;
scr_node_constants_value = NULL;
scr_dyn_release(value);
}
ScrDyn *scr_node_constants(const ScrDyn *crypto_table) {
if (scr_node_constants_value) return scr_dyn_retain(scr_node_constants_value);
ScrDyn *crypto = scr_dyn_call(crypto_table, NULL, 0, "constants");
if (!crypto) return NULL;
ScrDyn *o = scr_dyn_new_obj();
scr_os_dlopen_constants(o);
scr_os_errno_constants(o);
scr_os_constant(o, "PRIORITY_LOW", SCR_OS_PRIORITY_LOW);
scr_os_constant(o, "PRIORITY_BELOW_NORMAL", SCR_OS_PRIORITY_BELOW_NORMAL);
scr_os_constant(o, "PRIORITY_NORMAL", SCR_OS_PRIORITY_NORMAL);
scr_os_constant(o, "PRIORITY_ABOVE_NORMAL", SCR_OS_PRIORITY_ABOVE_NORMAL);
scr_os_constant(o, "PRIORITY_HIGH", SCR_OS_PRIORITY_HIGH);
scr_os_constant(o, "PRIORITY_HIGHEST", SCR_OS_PRIORITY_HIGHEST);
scr_os_signal_constants(o);
scr_fs_constants(o);
if (crypto && crypto->kind == SCR_DYN_OBJ) {
for (size_t i = 0; i < crypto->v.obj.len; i++) {
const ScrDynEntry *entry = &crypto->v.obj.entries[i];
if (entry->accessor || !entry->value) continue;
scr_dyn_obj_set(o, entry->key, entry->key_len, scr_dyn_retain(entry->value));
}
}
scr_dyn_release(crypto);
scr_dyn_release(scr_dyn_freeze(o));
scr_node_constants_value = o;
scr_atexit(scr_node_constants_clear);
return scr_dyn_retain(o);
}
+4
View File
@@ -38,6 +38,10 @@ ScrStr *scr_os_cpus_model(double i);
double scr_os_cpus_field(double i, double field);
/* os.constants: the per-thread null-prototype constants object (+1). */
ScrDyn *scr_os_constants(void);
/* The deprecated node:constants module object (scr_os.c): the os groups,
* fs.constants and the crypto table the borrowed function answers, merged
* and frozen (+1). */
ScrDyn *scr_node_constants(const ScrDyn *crypto_table);
double scr_os_available_parallelism(void); /* uv_available_parallelism: CPUs this process may use, >= 1 */
/* os.userInfo()'s field trio (pw_name / pw_shell / pw_dir — the passwd
* home, not the $HOME cascade). +1 fresh; abort on lookup failure. */
+339
View File
@@ -1989,3 +1989,342 @@ ScrBytes *scr_url_checked_to_path_buffer(const ScrDyn *value, const ScrDyn *opti
scr_url_release(url);
return result;
}
/* ── node:punycode (deprecated, DEP0040) ───────────────────────────────
* A port of the punycode.js 2.1.0 that Node 24 bundles as lib/punycode.js.
* It works on UTF-16 code units: ucs2.decode pairs surrogates and keeps
* lone ones, which is exactly a code-point walk over WTF-8 storage.
* Errors throw the module's RangeErrors ("Overflow: ...", "Illegal input
* >= 0x80 ...", "Invalid input"); decoded code points past U+10FFFF throw
* String.fromCodePoint's "Invalid code point N". All inputs are borrowed;
* results are +1, or NULL with the error pending. The DEP0040 report is
* scr_punycode_warn (scr_async_dyn.c). */
enum {
PC_MAX_INT = 2147483647,
PC_BASE = 36,
PC_TMIN = 1,
PC_TMAX = 26,
PC_SKEW = 38,
PC_DAMP = 700,
PC_INITIAL_BIAS = 72,
PC_INITIAL_N = 128
};
static void pc_error(const char *message) {
scr_throw_error_msg(SCR_ERR_RANGE, message, strlen(message));
}
#define PC_OVERFLOW "Overflow: input needs wider integers to process"
#define PC_NOT_BASIC "Illegal input >= 0x80 (not a basic code point)"
#define PC_INVALID "Invalid input"
typedef struct {
uint32_t *data;
size_t len, cap;
} PcPoints;
static void pc_push(PcPoints *p, uint32_t cp) {
if (p->len == p->cap) {
size_t cap = p->cap ? p->cap * 2 : 16;
uint32_t *grown = realloc(p->data, cap * sizeof *grown);
if (!grown) scr_trap("scriptc: out of memory\n");
p->data = grown;
p->cap = cap;
}
p->data[p->len++] = cp;
}
/* ucs2decode over WTF-8 storage: a pair is one 4-byte sequence, a lone
* surrogate one 3-byte sequence carrying its own value. */
static void pc_code_points(const char *s, size_t n, PcPoints *out) {
const unsigned char *b = (const unsigned char *)s;
for (size_t i = 0; i < n;) {
uint32_t c = b[i];
if (c < 0x80) {
i += 1;
} else if (c < 0xE0 && i + 1 < n) {
c = ((c & 0x1F) << 6) | (b[i + 1] & 0x3F);
i += 2;
} else if (c < 0xF0 && i + 2 < n) {
c = ((c & 0x0F) << 12) | ((uint32_t)(b[i + 1] & 0x3F) << 6) | (b[i + 2] & 0x3F);
i += 3;
} else if (i + 3 < n) {
c = ((c & 0x07) << 18) | ((uint32_t)(b[i + 1] & 0x3F) << 12) |
((uint32_t)(b[i + 2] & 0x3F) << 6) | (b[i + 3] & 0x3F);
i += 4;
} else {
i += 1;
}
pc_push(out, c);
}
}
static int64_t pc_adapt(int64_t delta, int64_t points, bool first) {
int64_t k = 0;
delta = first ? delta / PC_DAMP : delta >> 1;
delta += delta / points;
for (; delta > ((PC_BASE - PC_TMIN) * PC_TMAX) >> 1; k += PC_BASE) delta /= PC_BASE - PC_TMIN;
return k + (PC_BASE - PC_TMIN + 1) * delta / (delta + PC_SKEW);
}
static int64_t pc_basic_to_digit(unsigned char c) {
if (c >= 0x30 && c < 0x3A) return 26 + (c - 0x30);
if (c >= 0x41 && c < 0x5B) return c - 0x41;
if (c >= 0x61 && c < 0x7B) return c - 0x61;
return PC_BASE;
}
static char pc_digit_to_basic(int64_t digit) {
return (char)(digit + 22 + 75 * (digit < 26));
}
/* String.fromCodePoint(...points). */
static ScrStr *pc_from_points(const PcPoints *p) {
for (size_t i = 0; i < p->len; i++) {
if (p->data[i] > 0x10FFFF) {
char message[48];
int size = snprintf(message, sizeof message, "Invalid code point %u", (unsigned)p->data[i]);
scr_throw_error_msg(SCR_ERR_RANGE, message, (size_t)size);
return NULL;
}
}
char *out = malloc(p->len * 4 + 1);
if (!out) scr_trap("scriptc: out of memory\n");
size_t offset = 0;
for (size_t i = 0; i < p->len; i++) {
uint32_t point = p->data[i];
if (point >= 0xD800 && point <= 0xDBFF && i + 1 < p->len && p->data[i + 1] >= 0xDC00 &&
p->data[i + 1] <= 0xDFFF) {
point = 0x10000 + ((point - 0xD800) << 10) + (p->data[i + 1] - 0xDC00);
i++;
}
offset += scr_wtf8_put(out + offset, point);
}
ScrStr *result = scr_str_new(out, offset);
free(out);
return result;
}
/* punycode.decode over the bytes of one label. Any non-ASCII byte fails
* where its first code unit would: before the last delimiter it is not
* basic, after it it is no digit — so byte positions stand in for code
* unit positions until the first error. */
static ScrStr *pc_decode(const char *s, size_t n) {
PcPoints output = {0};
size_t basic = 0;
for (size_t k = n; k > 0; k--) {
if (s[k - 1] == '-') {
basic = k - 1;
break;
}
}
ScrStr *result = NULL;
for (size_t j = 0; j < basic; j++) {
if ((unsigned char)s[j] >= 0x80) {
pc_error(PC_NOT_BASIC);
goto done;
}
pc_push(&output, (unsigned char)s[j]);
}
int64_t i = 0, cp = PC_INITIAL_N, bias = PC_INITIAL_BIAS;
for (size_t index = basic > 0 ? basic + 1 : 0; index < n;) {
int64_t old = i;
for (int64_t w = 1, k = PC_BASE;; k += PC_BASE) {
if (index >= n) {
pc_error(PC_INVALID);
goto done;
}
int64_t digit = pc_basic_to_digit((unsigned char)s[index++]);
if (digit >= PC_BASE) {
pc_error(PC_INVALID);
goto done;
}
if (digit > (PC_MAX_INT - i) / w) {
pc_error(PC_OVERFLOW);
goto done;
}
i += digit * w;
int64_t t = k <= bias ? PC_TMIN : (k >= bias + PC_TMAX ? PC_TMAX : k - bias);
if (digit < t) break;
if (w > PC_MAX_INT / (PC_BASE - t)) {
pc_error(PC_OVERFLOW);
goto done;
}
w *= PC_BASE - t;
}
int64_t out = (int64_t)output.len + 1;
bias = pc_adapt(i - old, out, old == 0);
if (i / out > PC_MAX_INT - cp) {
pc_error(PC_OVERFLOW);
goto done;
}
cp += i / out;
i %= out;
pc_push(&output, 0);
memmove(output.data + i + 1, output.data + i, (output.len - 1 - (size_t)i) * sizeof *output.data);
output.data[i++] = (uint32_t)cp;
}
result = pc_from_points(&output);
done:
free(output.data);
return result;
}
/* punycode.encode of one label's code points, appended to buf. */
static bool pc_encode(const PcPoints *input, ScrJsonBuf *buf) {
int64_t cp = PC_INITIAL_N, delta = 0, bias = PC_INITIAL_BIAS;
size_t basic_length = 0;
for (size_t k = 0; k < input->len; k++) {
if (input->data[k] < 0x80) {
scr_jb_putc(buf, (char)input->data[k]);
basic_length++;
}
}
size_t handled = basic_length;
if (basic_length) scr_jb_putc(buf, '-');
while (handled < input->len) {
int64_t m = PC_MAX_INT;
for (size_t k = 0; k < input->len; k++)
if (input->data[k] >= cp && input->data[k] < m) m = input->data[k];
int64_t handled_plus_one = (int64_t)handled + 1;
if (m - cp > (PC_MAX_INT - delta) / handled_plus_one) {
pc_error(PC_OVERFLOW);
return false;
}
delta += (m - cp) * handled_plus_one;
cp = m;
for (size_t k = 0; k < input->len; k++) {
int64_t current = input->data[k];
if (current < cp && ++delta > PC_MAX_INT) {
pc_error(PC_OVERFLOW);
return false;
}
if (current == cp) {
int64_t q = delta;
for (int64_t j = PC_BASE;; j += PC_BASE) {
int64_t t = j <= bias ? PC_TMIN : (j >= bias + PC_TMAX ? PC_TMAX : j - bias);
if (q < t) break;
scr_jb_putc(buf, pc_digit_to_basic(t + (q - t) % (PC_BASE - t)));
q = (q - t) / (PC_BASE - t);
}
scr_jb_putc(buf, pc_digit_to_basic(q));
bias = pc_adapt(delta, handled_plus_one, handled == basic_length);
delta = 0;
handled++;
}
}
delta++;
cp++;
}
return true;
}
ScrStr *scr_punycode_decode(const ScrStr *input) { return pc_decode(input->data, input->len); }
ScrStr *scr_punycode_encode(const ScrStr *input) {
PcPoints points = {0};
pc_code_points(input->data, input->len, &points);
ScrJsonBuf buf;
scr_jb_init(&buf);
bool ok = pc_encode(&points, &buf);
free(points.data);
ScrStr *result = scr_jb_finish(&buf);
if (ok) return result;
scr_str_release(result);
return NULL;
}
ScrArr *scr_punycode_ucs2_decode(const ScrStr *input) {
PcPoints points = {0};
pc_code_points(input->data, input->len, &points);
ScrArr *out = scr_arr_new(SCR_ELEM_F64, points.len);
for (size_t i = 0; i < points.len; i++) scr_arr_push_f64(out, (double)points.data[i]);
free(points.data);
return out;
}
/* mapDomain: only the part after the first '@' maps (and only up to a
* second one — punycode.js keeps parts[0] and parts[1]); the RFC 3490
* separators U+002E, U+3002, U+FF0E and U+FF61 all split labels, and the
* labels rejoin with '.'. */
static ScrStr *pc_map_domain(const ScrStr *input, bool to_ascii) {
const char *s = input->data;
size_t n = input->len;
ScrJsonBuf buf;
scr_jb_init(&buf);
const char *at = memchr(s, '@', n);
if (at) {
scr_jb_write(&buf, s, (size_t)(at - s) + 1);
const char *rest = at + 1;
const char *second = memchr(rest, '@', n - (size_t)(rest - s));
n = (second ? (size_t)(second - rest) : n - (size_t)(rest - s));
s = rest;
}
size_t start = 0;
bool ok = true;
for (size_t i = 0; ok && i <= n;) {
size_t sep = 0;
if (i == n) sep = 1;
else if (s[i] == '.') sep = 1;
else if (i + 2 < n && (unsigned char)s[i] == 0xE3 && (unsigned char)s[i + 1] == 0x80 &&
(unsigned char)s[i + 2] == 0x82)
sep = 3;
else if (i + 2 < n && (unsigned char)s[i] == 0xEF &&
(((unsigned char)s[i + 1] == 0xBC && (unsigned char)s[i + 2] == 0x8E) ||
((unsigned char)s[i + 1] == 0xBD && (unsigned char)s[i + 2] == 0xA1)))
sep = 3;
if (!sep) {
i++;
continue;
}
const char *label = s + start;
size_t len = i - start;
bool ascii = true;
for (size_t k = 0; k < len; k++) ascii = ascii && (unsigned char)label[k] < 0x80;
if (to_ascii && !ascii) {
PcPoints points = {0};
pc_code_points(label, len, &points);
scr_jb_puts(&buf, "xn--");
ok = pc_encode(&points, &buf);
free(points.data);
} else if (!to_ascii && len >= 4 && memcmp(label, "xn--", 4) == 0) {
/* string.slice(4).toLowerCase(): ASCII letters fold; the one other
* code point whose lowercase is ASCII is KELVIN SIGN (U+212A → k).
* Any other non-ASCII stays non-ASCII and fails in decode either
* way, at the same position. */
ScrJsonBuf lower;
scr_jb_init(&lower);
for (size_t k = 4; k < len; k++) {
unsigned char c = (unsigned char)label[k];
if (c == 0xE2 && k + 2 < len && (unsigned char)label[k + 1] == 0x84 &&
(unsigned char)label[k + 2] == 0xAA) {
scr_jb_putc(&lower, 'k');
k += 2;
} else {
scr_jb_putc(&lower, (char)(c >= 'A' && c <= 'Z' ? c + 32 : c));
}
}
ScrStr *folded = scr_jb_finish(&lower);
ScrStr *decoded = pc_decode(folded->data, folded->len);
scr_str_release(folded);
if (decoded) {
scr_jb_put_str(&buf, decoded);
scr_str_release(decoded);
} else {
ok = false;
}
} else {
scr_jb_write(&buf, label, len);
}
if (ok && i < n) scr_jb_putc(&buf, '.');
i += sep;
start = i;
}
ScrStr *result = scr_jb_finish(&buf);
if (ok) return result;
scr_str_release(result);
return NULL;
}
ScrStr *scr_punycode_to_ascii(const ScrStr *input) { return pc_map_domain(input, true); }
ScrStr *scr_punycode_to_unicode(const ScrStr *input) { return pc_map_domain(input, false); }
+9
View File
@@ -53,6 +53,15 @@ ScrDyn *scr_url_legacy_parse(const ScrDyn *url, const ScrDyn *parse_query, const
ScrStr *scr_url_legacy_format(const ScrDyn *object, const ScrDyn *options, bool warn);
ScrStr *scr_url_legacy_resolve(const ScrDyn *from, const ScrDyn *to, bool warn);
ScrStr *scr_url_domain_to_ascii(const ScrDyn *domain, bool missing);
/* node:punycode (scr_url.c): punycode.js 2.1.0's encode/decode, toASCII/
* toUnicode (mapDomain over RFC 3490 separators) and ucs2.decode (UTF-16
* code points as an f64 array). Borrowed input; +1, or NULL with the
* module's RangeError pending. */
ScrStr *scr_punycode_decode(const ScrStr *input);
ScrStr *scr_punycode_encode(const ScrStr *input);
ScrStr *scr_punycode_to_ascii(const ScrStr *input);
ScrStr *scr_punycode_to_unicode(const ScrStr *input);
ScrArr *scr_punycode_ucs2_decode(const ScrStr *input);
ScrStr *scr_url_domain_to_unicode(const ScrDyn *domain, bool missing);
ScrStr *scr_url_to_path(ScrUrl *u); /* +1, or throws */
ScrStr *scr_url_str_to_path(ScrStr *s); /* +1, or throws */
@@ -0,0 +1,2 @@
export type Flag = boolean;
console.log("flags evaluated");
@@ -0,0 +1,21 @@
// Node's type stripping erases `import type` and `export type … from`, so
// the modules and packages they name are never loaded: a package with no
// runtime entry, a module importing a builtin scriptc does not compile, and
// modules with load-time output. An inline `type` specifier keeps an empty
// import clause and still evaluates its module, as does a mixed clause.
import type { Frozen, Shape } from "types-only-pkg";
import type { Label } from "./uses-vm.ts";
import type * as Models from "./models.ts";
import type { Reexported } from "./reexports.ts";
import { type Mode, describe } from "./mixed.ts";
import { type Flag } from "./flags.ts";
const shape: Shape = { width: 3, height: 4 };
const frozen: Frozen<Shape> = { width: 5, height: 6 };
const label: Label = "area";
const model: Models.Model = { id: 1 };
const payload: Reexported = { ok: true };
const mode: Mode = "fast";
const flag: Flag = true;
console.log(label, shape.width * shape.height, model.id, payload.ok);
console.log(describe(mode), flag, frozen.width + frozen.height);
@@ -0,0 +1,8 @@
export type { Payload } from "./payload.ts";
export type Mode = "fast" | "slow";
export function describe(mode: Mode): string {
return `mode=${mode}`;
}
console.log("mixed evaluated");
@@ -0,0 +1,4 @@
export interface Model {
id: number;
}
console.log("models evaluated");
@@ -0,0 +1,6 @@
export interface Shape {
width: number;
height: number;
}
export type Frozen<T> = { readonly [K in keyof T]: T[K] };
@@ -0,0 +1,5 @@
{
"name": "types-only-pkg",
"version": "1.0.0",
"types": "./index.d.ts"
}
@@ -0,0 +1,4 @@
export interface Payload {
ok: boolean;
}
console.log("payload evaluated");
@@ -0,0 +1,2 @@
export type { Payload as Reexported } from "./payload.ts";
console.log("reexports evaluated");
@@ -0,0 +1,5 @@
{
"compilerOptions": {
"strict": true
}
}
@@ -0,0 +1,4 @@
import * as vm from "node:vm";
export type Label = string;
console.log("uses-vm evaluated", vm.runInThisContext("1 + 1"));
@@ -0,0 +1,10 @@
import { b } from "./b.ts";
export interface AInfo {
tag: string;
}
export function start(): string {
return `a+${b()}`;
}
console.log("a evaluated");
@@ -0,0 +1,7 @@
import { type AInfo } from "./a.ts";
export function b(): string {
const info: AInfo = { tag: "b" };
return info.tag;
}
console.log("b evaluated");
@@ -0,0 +1,2 @@
export { type Config } from "./config.ts";
console.log("barrel evaluated");
@@ -0,0 +1,4 @@
export interface Config {
name: string;
}
console.log("config evaluated");
@@ -0,0 +1,10 @@
// An inline `type` specifier keeps its module request under Node's type
// stripping: `export { type X } from` and `import { type X } from` both
// evaluate the named module, once, in source order. A cycle closed only by
// such an import binds nothing at runtime and runs like a side-effect
// import.
import { type Config } from "./config-barrel.ts";
import { start } from "./a.ts";
const config: Config = { name: "demo" };
console.log("main", config.name, start());
+45
View File
@@ -0,0 +1,45 @@
// @exit: 1
// @no-node-shims
// node:punycode's conversions (punycode.js 2.1.0): encode/decode of bare
// labels, toASCII/toUnicode over domains and email addresses (only the part
// after '@' maps; every RFC 3490 separator splits labels), ucs2 code point
// arrays with astral pairs and lone surrogates, and the RangeErrors of
// malformed input. A static import links the module before anything runs,
// so its DEP0040 report precedes the program and no 'warning' listener the
// program registers sees it (the Node side runs without the preload shim,
// which would delay that report past the program's first turn). The report
// carries the process id, so the @exit directive keeps stderr out of the
// comparison.
import * as punycode from "node:punycode";
process.on("warning", (w) => console.log("listener", w.name));
const show = (label: string, f: () => unknown): void => {
try {
console.log(label, JSON.stringify(f()));
} catch (e) {
const err = e as Error;
console.log(label, "threw", err instanceof RangeError, err.name, err.message);
}
};
console.log(punycode.version);
for (const s of ["mañana", "bücher", "ü", "abc", "", "-> $1.00 <-", "ليهمابتكلموشعربي؟", "他们为什么不说中文", "😀x", "MaJiでKoiする5秒前"]) {
show(`encode ${s}`, () => punycode.encode(s));
show(`roundtrip ${s}`, () => punycode.decode(punycode.encode(s)));
}
for (const s of ["maana-pta", "bcher-kva", "tda", "abc-", "-abc", "ABC-kva", "a-b-c-kva", "ü-kva", "kva-ü", "99999999999", "zzzzzzzzzzzzz", "a-", "-"]) {
show(`decode ${s}`, () => punycode.decode(s));
}
for (const d of ["mañana.com", "bücher.de", "example.com", "", "a..b", "ex@mple@bücher.de", "user@bücher.de", "日本。jp", "a.b。c", "ÄÖÜ.test", "a.b."]) {
show(`toASCII ${d}`, () => punycode.toASCII(d));
}
for (const d of ["xn--maana-pta.com", "xn--bcher-kva.de", "XN--bcher-kva.de", "xn--BCHER-KVA.de", "example.com", "user@xn--bcher-kva.de", "xn--.x", "xn--ü.x", "xn--zzzzzzzzzzzzz.x", "xn--a-Kva.x"]) {
show(`toUnicode ${d}`, () => punycode.toUnicode(d));
}
show("ucs2.decode", () => punycode.ucs2.decode("aé😀\ud800b\udc00"));
show("ucs2.encode", () => punycode.ucs2.encode([97, 0xe9, 0x1f600, 0xd800, 0xdc00, 98]));
show("ucs2.encode join", () => punycode.ucs2.encode([0xd83d, 0xde00]).length);
show("ucs2.encode range", () => punycode.ucs2.encode([0x110000]));
show("ucs2 roundtrip", () => punycode.ucs2.encode(punycode.ucs2.decode("x😀\udfffy")) === "x😀\udfffy");
process.exitCode = 1;
@@ -0,0 +1,32 @@
// DEP0040 for require("punycode"): reported once, on the next tick after
// the requiring turn, to the stderr report and the 'warning' listeners;
// require("node:punycode") is the same module and does not report again.
// process.noDeprecation drops the report and the listener call. The program
// re-runs itself per case and prints each child's output with process ids
// normalized.
"use strict";
const { spawnSync } = require("child_process");
const mode = process.argv[2];
if (mode === "direct") {
process.on("warning", (w) => console.log("listener", w.name, w.code, w.message));
process.nextTick(() => console.log("earlier tick"));
console.log("before require");
const direct = require("punycode");
console.log("after require", direct.toASCII("bücher.de"));
const prefixed = require("node:punycode");
console.log("prefixed", prefixed.toUnicode("xn--bcher-kva.de"));
setTimeout(() => console.log("timer"), 0);
} else if (mode === "silenced") {
process.on("warning", (w) => console.log("listener", w.code));
process.noDeprecation = true;
const silencedModule = require("punycode");
console.log("silenced", silencedModule.encode("ü"));
} else {
for (const child of ["direct", "silenced"]) {
const run = spawnSync(process.execPath, [process.argv[1], child], { encoding: "utf8" });
console.log(`== ${child}: status ${run.status}`);
console.log(run.stdout);
console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:<pid>)"));
}
}
+26
View File
@@ -0,0 +1,26 @@
// node:constants (deprecated in documentation only, so no warning): one
// frozen object merging os.constants' dlopen, errno, priority and signals
// groups with fs.constants and crypto.constants, in that order. Members
// read like the owning modules' constants, and a constant the platform
// lacks is absent (graceful-fs probes O_SYMLINK with hasOwnProperty).
const constants = require("constants");
const nodeConstants = require("node:constants");
const os = require("os");
console.log(typeof constants, Object.isFrozen(constants), constants === nodeConstants);
console.log(constants.O_RDONLY, constants.O_WRONLY, constants.O_RDWR);
console.log(constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC);
console.log(constants.S_IFMT, constants.S_IFREG, constants.S_IFDIR, constants.S_IRWXU);
console.log(constants.F_OK, constants.R_OK, constants.W_OK, constants.X_OK);
console.log(constants.COPYFILE_EXCL, constants.UV_FS_COPYFILE_FICLONE, constants.UV_DIRENT_DIR);
console.log(constants.SIGTERM === os.constants.signals.SIGTERM, constants.SIGINT);
console.log(constants.ENOENT === os.constants.errno.ENOENT, constants.EACCES);
console.log(constants.PRIORITY_HIGH, constants.PRIORITY_LOW, constants.RTLD_LAZY);
console.log(constants.TLS1_2_VERSION, constants.RSA_PKCS1_PADDING, typeof constants.defaultCoreCipherList);
console.log(constants.hasOwnProperty("O_SYMLINK") === ("O_SYMLINK" in constants));
console.log(constants.hasOwnProperty("O_RDWR"), "SIGKILL" in constants, "UV_UDP_REUSEADDR" in constants);
console.log(constants.hasOwnProperty("NOT_A_CONSTANT"), "dlopen" in constants, "errno" in constants);
const keys = Object.keys(constants);
console.log(keys.indexOf("RTLD_LAZY") < keys.indexOf("E2BIG"), keys.indexOf("E2BIG") < keys.indexOf("PRIORITY_LOW"));
console.log(keys.indexOf("PRIORITY_LOW") < keys.indexOf("SIGHUP"), keys.indexOf("SIGHUP") < keys.indexOf("O_RDONLY"));
console.log(keys.indexOf("O_RDONLY") < keys.indexOf("OPENSSL_VERSION_NUMBER"), keys.includes("POINT_CONVERSION_HYBRID"));
@@ -0,0 +1,12 @@
// A dependency under node_modules that requires node:punycode: Node's
// isInsideNodeModules() rule skips DEP0040 for it, so nothing reaches stderr
// or the 'warning' listeners, and the program's own later require of the
// cached module does not report either.
"use strict";
process.on("warning", (w) => console.log("listener", w.code));
const toAscii = require("puny-dep");
const punycode = require("punycode");
console.log(toAscii("mañana.com"), toAscii("example.org"));
console.log(punycode.toUnicode("xn--maana-pta.com"));
setTimeout(() => console.log("timer"), 0);
@@ -0,0 +1,2 @@
declare function toAscii(domain: string): string;
export = toAscii;
+8
View File
@@ -0,0 +1,8 @@
// A dependency requiring node:punycode: Node's isInsideNodeModules() rule
// skips DEP0040 for it.
"use strict";
const punycode = require("punycode");
module.exports = function toAscii(domain) {
return punycode.toASCII(domain);
};
@@ -0,0 +1,5 @@
{
"name": "puny-dep",
"version": "1.0.0",
"main": "index.js"
}
+5
View File
@@ -0,0 +1,5 @@
export interface Greeting {
text: string;
}
export default function wait(text: string): string;
+5
View File
@@ -0,0 +1,5 @@
{
"name": "@types/twinned",
"version": "1.0.0",
"types": "index.d.ts"
}
+9
View File
@@ -0,0 +1,9 @@
// A CommonJS package without its own declarations: its types live in the
// separate @types/twinned package, and it depends on the untyped ms.
'use strict';
const ms = require('ms');
module.exports = function wait(text) {
return 'twinned:' + text + ':' + ms(90000);
};
+5
View File
@@ -0,0 +1,5 @@
{
"name": "twinned",
"version": "1.0.0",
"main": "index.js"
}
+5
View File
@@ -0,0 +1,5 @@
// twinned's declarations come from @types/twinned; the package itself and
// its untyped ms dependency ship JavaScript only.
import wait from "twinned";
console.log(wait("ready"));
+7
View File
@@ -0,0 +1,7 @@
// `import type` erases under Node's type stripping: twinned is never
// loaded, so no package is selected or refused, and its declared
// interface types a value the program builds itself.
import type { Greeting } from "twinned";
const greeting: Greeting = { text: "hello" };
console.log(greeting.text);
+62
View File
@@ -821,10 +821,71 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
package: "ms",
status: "refused",
detail: "it ships no own .d.ts declaration surface",
hint: "to compile it anyway, name it with --npm-static=ms (or --npm-static=ms... to include its dependencies)",
},
]);
}, 120_000);
// Types-first resolution answers @types/twinned, but the refusal names the
// package that has the code and the flag that compiles it.
test("an @types-typed package is refused under its runtime name with the opt-in remedy", () => {
const { coverage } = analyze(join(pilotRoot, "twinned-cli.ts"));
expect(coverage.npmStatic).toEqual([
{
package: "twinned",
status: "refused",
detail:
"its declared types come from the separate @types/twinned package, not the package itself",
hint: "to compile it anyway, name it with --npm-static=twinned (or --npm-static=twinned... to include its dependencies)",
},
]);
const diagnostic = coverage.diagnostics.find((d) => d.code === "SC2013");
expect(diagnostic?.message).toContain("'twinned'");
expect(diagnostic?.hint).toContain("--npm-static=twinned");
}, 120_000);
test("a named package's untyped dependency points at the transitive opt-in", () => {
const { coverage } = analyze(join(pilotRoot, "twinned-cli.ts"), { npmStatic: ["twinned"] });
expect(coverage.npmStatic).toContainEqual({
package: "ms",
status: "refused",
detail: "it ships no own .d.ts declaration surface",
hint: "to compile it anyway, name it with --npm-static=ms, or use --npm-static=twinned... to include all of twinned's dependencies",
});
}, 120_000);
test("--npm-static=pkg... compiles the package's dependency closure", async () => {
const entry = join(pilotRoot, "twinned-cli.ts");
const { coverage } = analyze(entry, { npmStatic: ["twinned..."] });
expect(coverage.npmStatic).toEqual([
{ package: "twinned", status: "static" },
{ package: "ms", status: "static" },
]);
expect(coverage.preflightFailed).toBe(false);
const binary = await buildStatic(entry, ["twinned..."]);
const [nodeRes, nativeRes] = await Promise.all([
runBinary("node", [entry]),
runBinary(binary, []),
]);
expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
expect(nativeRes.exitCode).toBe(nodeRes.exitCode);
}, 180_000);
test("type-only imports select no package", async () => {
const entry = join(pilotRoot, "type-only-cli.ts");
const { coverage } = analyze(entry);
expect(coverage.npmStatic ?? []).toEqual([]);
expect(coverage.preflightFailed).toBe(false);
expect(coverage.diagnostics).toEqual([]);
const binary = await buildStatic(entry, []);
const [nodeRes, nativeRes] = await Promise.all([
runBinary("node", [entry]),
runBinary(binary, []),
]);
expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
expect(nativeRes.exitCode).toBe(nodeRes.exitCode);
}, 180_000);
test("ms supports invalid duration strings without runtime fences", () => {
const { coverage } = analyze(join(pilotRoot, "ms-invalid-cli.ts"), { npmStatic: ["ms"] });
expect(coverage.npmStatic).toEqual([{ package: "ms", status: "static" }]);
@@ -974,6 +1035,7 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
package: "typegapped",
status: "refused",
detail: "it ships no own .d.ts declaration surface",
hint: "to compile it anyway, name it with --npm-static=typegapped (or --npm-static=typegapped... to include its dependencies)",
},
{ package: "escape-string-regexp", status: "static" },
]);