diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx
index e8596fc3d..bbe1da3c6 100644
--- a/docs/content/docs/cli.mdx
+++ b/docs/content/docs/cli.mdx
@@ -119,7 +119,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
On Linux, when the program calls process.exit(), 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.
--npm-static <pkg[,pkg…]|auto>
- Name npm packages to compile even if they don't ship their own type declarations (repeatable). auto uses the default automatic selection. Packages that can't compile produce diagnostics. See npm Dependencies.
+ Name npm packages to compile even if they don't ship their own type declarations (repeatable). A trailing ..., as in fs-extra..., also names every package that package depends on. auto uses the default automatic selection. Packages that can't compile produce diagnostics. See npm Dependencies.
--provenance-sources
Experimental. Compile npm dependencies from the source at the commit their provenance attestation names. Packages without a usable attestation compile from their installed files.
diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx
index 5ecc144c6..3c6958841 100644
--- a/docs/content/docs/dependencies.mdx
+++ b/docs/content/docs/dependencies.mdx
@@ -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.
@@ -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
diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx
index 544bd2b2d..6f21e2ce3 100644
--- a/docs/content/docs/limitations.mdx
+++ b/docs/content/docs/limitations.mdx
@@ -76,6 +76,8 @@ title: "Limitations"
../limitations/builtin-apis/legacy-url-api.mdx
+../limitations/builtin-apis/deprecated-modules.mdx
+
../limitations/builtin-apis/filesystem-flags.mdx
../limitations/builtin-apis/copy.mdx
diff --git a/docs/content/limitations/builtin-apis/deprecated-modules.mdx b/docs/content/limitations/builtin-apis/deprecated-modules.mdx
new file mode 100644
index 000000000..bb630b8c3
--- /dev/null
+++ b/docs/content/limitations/builtin-apis/deprecated-modules.mdx
@@ -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.
diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json
index a64169933..f03a39e85 100644
--- a/docs/src/generated/node-v24-compatibility-meta.json
+++ b/docs/src/generated/node-v24-compatibility-meta.json
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
- "artifactVersion": "a610bdd1ae56e1c4cd38",
+ "artifactVersion": "4421a7b808191ac7f410",
"rowCount": 3880
}
diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json
index d4b6839e7..7a9e0e2a2 100644
--- a/docs/src/generated/node-v24-compatibility.json
+++ b/docs/src/generated/node-v24-compatibility.json
@@ -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"
}
},
{
diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json
index f36c865cf..0fba7781e 100644
--- a/internal/compatibility/generated/node-v24-backlog.json
+++ b/internal/compatibility/generated/node-v24-backlog.json
@@ -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"
+ ]
}
}
},
diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json
index 514282a0a..b755a95e8 100644
--- a/internal/compatibility/generated/node-v24-internal.json
+++ b/internal/compatibility/generated/node-v24-internal.json
@@ -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"
},
diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json
index 45a946925..0adeb9882 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -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"],
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index d72ca2dcd..cc5de935c 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -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),
diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts
index fdfe723d2..68d943ea1 100644
--- a/packages/compiler/src/backend/llvm/lib-dispatch.ts
+++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts
@@ -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);
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index 02bf37192..6b2f739a8 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -194,6 +194,7 @@ export const LIB_FN_SYMS: Record = {
"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 = {
"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",
diff --git a/packages/compiler/src/cli/usage.ts b/packages/compiler/src/cli/usage.ts
index 97c56c274..b5dcec332 100644
--- a/packages/compiler/src/cli/usage.ts
+++ b/packages/compiler/src/cli/usage.ts
@@ -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
diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts
index 730704f6f..8b03f221d 100644
--- a/packages/compiler/src/coverage/report.ts
+++ b/packages/compiler/src/coverage/report.ts
@@ -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";
diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts
index eaae1b531..e6ed50ae1 100644
--- a/packages/compiler/src/diagnostics/diagnostic.ts
+++ b/packages/compiler/src/diagnostics/diagnostic.ts
@@ -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",
};
}
diff --git a/packages/compiler/src/executable/prepare.ts b/packages/compiler/src/executable/prepare.ts
index 7becd2358..d97497c70 100644
--- a/packages/compiler/src/executable/prepare.ts
+++ b/packages/compiler/src/executable/prepare.ts
@@ -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,
),
),
);
diff --git a/packages/compiler/src/frontend/analysis.ts b/packages/compiler/src/frontend/analysis.ts
index 87c7d22ca..f17f6af6c 100644
--- a/packages/compiler/src/frontend/analysis.ts
+++ b/packages/compiler/src/frontend/analysis.ts
@@ -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,
),
),
];
diff --git a/packages/compiler/src/frontend/builtin-modules.ts b/packages/compiler/src/frontend/builtin-modules.ts
index b45dce6d7..3a1acb934 100644
--- a/packages/compiler/src/frontend/builtin-modules.ts
+++ b/packages/compiler/src/frontend/builtin-modules.ts
@@ -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 —
diff --git a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts
index a9df9e662..c05430ad3 100644
--- a/packages/compiler/src/frontend/lowering/lower-builtin-values.ts
+++ b/packages/compiler/src/frontend/lowering/lower-builtin-values.ts
@@ -23,6 +23,7 @@ import {
builtinModulesArrayLit,
stdlibGlobalNameOf,
} from "./surfaces.js";
+import { CRYPTO_CONSTANTS } from "./crypto-tables.js";
const PROTOTYPE_METHODS: Record = {
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 };
+}
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index 09296bdfe..149c25248 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -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 };
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index da70cd53a..3c035cd45 100644
--- a/packages/compiler/src/frontend/lowering/lower-modules.ts
+++ b/packages/compiler/src/frontend/lowering/lower-modules.ts
@@ -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);
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index 12544979c..57e6c2311 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -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",
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index 145fb3ae6..a1ded6aee 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -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.
diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index 73cc58c45..44ed37e3c 100644
--- a/packages/compiler/src/frontend/lowering/surfaces.ts
+++ b/packages/compiler/src/frontend/lowering/surfaces.ts
@@ -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(
diff --git a/packages/compiler/src/frontend/npm-static-prune.ts b/packages/compiler/src/frontend/npm-static-prune.ts
index a420b6b7c..b71d6ca33 100644
--- a/packages/compiler/src/frontend/npm-static-prune.ts
+++ b/packages/compiler/src/frontend/npm-static-prune.ts
@@ -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)
)
diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts
index 2ec9553f6..c769ed99e 100644
--- a/packages/compiler/src/frontend/pipeline.ts
+++ b/packages/compiler/src/frontend/pipeline.ts
@@ -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,
sites?: Map,
+ transitive: Set = 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();
- 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();
+ 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>,
libraryNpmStatic: readonly string[] = [],
+ transitive: Set = 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();
diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts
index df0cb673e..dcdaf9040 100644
--- a/packages/compiler/src/frontend/program.ts
+++ b/packages/compiler/src/frontend/program.ts
@@ -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;
diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts
index f37703fd8..7ecf44659 100644
--- a/packages/compiler/src/frontend/type-mapper.ts
+++ b/packages/compiler/src/frontend/type-mapper.ts
@@ -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): 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;
diff --git a/packages/compiler/src/frontend/type-only-imports.ts b/packages/compiler/src/frontend/type-only-imports.ts
new file mode 100644
index 000000000..003d4ccf7
--- /dev/null
+++ b/packages/compiler/src/frontend/type-only-imports.ts
@@ -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 {
+ 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();
+ const referenced = new Set();
+ 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();
+ 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)));
+}
diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index 1bb9cbd14..155c097d5 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -489,6 +489,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = 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].
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index 08c493ae4..8015c104a 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -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 },
diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts
index d16edfabd..7df85c738 100644
--- a/packages/compiler/src/ir/runtime-features.ts
+++ b/packages/compiler/src/ir/runtime-features.ts
@@ -46,6 +46,8 @@ export interface RuntimeFeatures {
}
const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([
+ // DEP0040 rides the dyn-async unit's warning machinery.
+ "punycode.warn",
"fs.callbackValue",
"fs.callbackCall",
"fs.callbackGlob",
diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json
index f5313f12d..6dc871976 100644
--- a/packages/compiler/surface-manifest.json
+++ b/packages/compiler/surface-manifest.json
@@ -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",
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6120-type-only-import-graph/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6120-type-only-import-graph/main.ts.json
new file mode 100644
index 000000000..979391c1c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6120-type-only-import-graph/main.ts.json
@@ -0,0 +1,8 @@
+{
+ "order": [
+ "/tests/corpus/6120-type-only-import-graph/mixed.ts",
+ "/tests/corpus/6120-type-only-import-graph/flags.ts",
+ "/tests/corpus/6120-type-only-import-graph/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6121-type-specifier-edges/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6121-type-specifier-edges/main.ts.json
new file mode 100644
index 000000000..c0f3f653c
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6121-type-specifier-edges/main.ts.json
@@ -0,0 +1,10 @@
+{
+ "order": [
+ "/tests/corpus/6121-type-specifier-edges/config.ts",
+ "/tests/corpus/6121-type-specifier-edges/config-barrel.ts",
+ "/tests/corpus/6121-type-specifier-edges/b.ts",
+ "/tests/corpus/6121-type-specifier-edges/a.ts",
+ "/tests/corpus/6121-type-specifier-edges/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6122-punycode-conversions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6122-punycode-conversions.ts.json
new file mode 100644
index 000000000..b15e4fe13
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6122-punycode-conversions.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6122-punycode-conversions.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6124-punycode-require-warnings.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6124-punycode-require-warnings.cjs.json
new file mode 100644
index 000000000..dfd4aa912
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6124-punycode-require-warnings.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6124-punycode-require-warnings.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6125-constants-module.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6125-constants-module.cjs.json
new file mode 100644
index 000000000..ef8c7f2bf
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6125-constants-module.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6125-constants-module.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json
new file mode 100644
index 000000000..5de83361a
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json
@@ -0,0 +1,18 @@
+{
+ "order": [
+ "/tests/corpus/6126-punycode-dependency/main.cjs"
+ ],
+ "diags": [
+ {
+ "code": "SC1010",
+ "message": "the 'puny-dep' module is not supported yet",
+ "loc": {
+ "file": "/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"
+ }
+ ]
+}
diff --git a/packages/runtime/src/scr_async.h b/packages/runtime/src/scr_async.h
index 7f294ca29..efa98e942 100644
--- a/packages/runtime/src/scr_async.h
+++ b/packages/runtime/src/scr_async.h
@@ -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. */
diff --git a/packages/runtime/src/scr_async_dyn.c b/packages/runtime/src/scr_async_dyn.c
index b7387c13e..95771d457 100644
--- a/packages/runtime/src/scr_async_dyn.c
+++ b/packages/runtime/src/scr_async_dyn.c
@@ -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. */
diff --git a/packages/runtime/src/scr_os.c b/packages/runtime/src/scr_os.c
index 76f69588e..2e70d0d85 100644
--- a/packages/runtime/src/scr_os.c
+++ b/packages/runtime/src/scr_os.c
@@ -27,6 +27,8 @@
#include
#include
#endif
+#include
+#include
#ifdef __linux__
#include
#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);
+}
diff --git a/packages/runtime/src/scr_os.h b/packages/runtime/src/scr_os.h
index 3c5009fc5..219726660 100644
--- a/packages/runtime/src/scr_os.h
+++ b/packages/runtime/src/scr_os.h
@@ -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. */
diff --git a/packages/runtime/src/scr_url.c b/packages/runtime/src/scr_url.c
index eeadba98f..67de82471 100644
--- a/packages/runtime/src/scr_url.c
+++ b/packages/runtime/src/scr_url.c
@@ -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); }
diff --git a/packages/runtime/src/scr_url.h b/packages/runtime/src/scr_url.h
index 134893054..05e5702bb 100644
--- a/packages/runtime/src/scr_url.h
+++ b/packages/runtime/src/scr_url.h
@@ -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 */
diff --git a/tests/corpus/6120-type-only-import-graph/flags.ts b/tests/corpus/6120-type-only-import-graph/flags.ts
new file mode 100644
index 000000000..5af639c0a
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/flags.ts
@@ -0,0 +1,2 @@
+export type Flag = boolean;
+console.log("flags evaluated");
diff --git a/tests/corpus/6120-type-only-import-graph/main.ts b/tests/corpus/6120-type-only-import-graph/main.ts
new file mode 100644
index 000000000..251ce6809
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/main.ts
@@ -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 = { 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);
diff --git a/tests/corpus/6120-type-only-import-graph/mixed.ts b/tests/corpus/6120-type-only-import-graph/mixed.ts
new file mode 100644
index 000000000..ec5a08bbb
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/mixed.ts
@@ -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");
diff --git a/tests/corpus/6120-type-only-import-graph/models.ts b/tests/corpus/6120-type-only-import-graph/models.ts
new file mode 100644
index 000000000..195cf9a17
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/models.ts
@@ -0,0 +1,4 @@
+export interface Model {
+ id: number;
+}
+console.log("models evaluated");
diff --git a/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/index.d.ts b/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/index.d.ts
new file mode 100644
index 000000000..e9c1b5497
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/index.d.ts
@@ -0,0 +1,6 @@
+export interface Shape {
+ width: number;
+ height: number;
+}
+
+export type Frozen = { readonly [K in keyof T]: T[K] };
diff --git a/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/package.json b/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/package.json
new file mode 100644
index 000000000..c2be952b9
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/node_modules/types-only-pkg/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "types-only-pkg",
+ "version": "1.0.0",
+ "types": "./index.d.ts"
+}
diff --git a/tests/corpus/6120-type-only-import-graph/payload.ts b/tests/corpus/6120-type-only-import-graph/payload.ts
new file mode 100644
index 000000000..beb97c5ac
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/payload.ts
@@ -0,0 +1,4 @@
+export interface Payload {
+ ok: boolean;
+}
+console.log("payload evaluated");
diff --git a/tests/corpus/6120-type-only-import-graph/reexports.ts b/tests/corpus/6120-type-only-import-graph/reexports.ts
new file mode 100644
index 000000000..52e86f294
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/reexports.ts
@@ -0,0 +1,2 @@
+export type { Payload as Reexported } from "./payload.ts";
+console.log("reexports evaluated");
diff --git a/tests/corpus/6120-type-only-import-graph/tsconfig.json b/tests/corpus/6120-type-only-import-graph/tsconfig.json
new file mode 100644
index 000000000..aee0ec940
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "compilerOptions": {
+ "strict": true
+ }
+}
diff --git a/tests/corpus/6120-type-only-import-graph/uses-vm.ts b/tests/corpus/6120-type-only-import-graph/uses-vm.ts
new file mode 100644
index 000000000..bed6646bf
--- /dev/null
+++ b/tests/corpus/6120-type-only-import-graph/uses-vm.ts
@@ -0,0 +1,4 @@
+import * as vm from "node:vm";
+
+export type Label = string;
+console.log("uses-vm evaluated", vm.runInThisContext("1 + 1"));
diff --git a/tests/corpus/6121-type-specifier-edges/a.ts b/tests/corpus/6121-type-specifier-edges/a.ts
new file mode 100644
index 000000000..59d6385da
--- /dev/null
+++ b/tests/corpus/6121-type-specifier-edges/a.ts
@@ -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");
diff --git a/tests/corpus/6121-type-specifier-edges/b.ts b/tests/corpus/6121-type-specifier-edges/b.ts
new file mode 100644
index 000000000..a71f62def
--- /dev/null
+++ b/tests/corpus/6121-type-specifier-edges/b.ts
@@ -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");
diff --git a/tests/corpus/6121-type-specifier-edges/config-barrel.ts b/tests/corpus/6121-type-specifier-edges/config-barrel.ts
new file mode 100644
index 000000000..dc461f654
--- /dev/null
+++ b/tests/corpus/6121-type-specifier-edges/config-barrel.ts
@@ -0,0 +1,2 @@
+export { type Config } from "./config.ts";
+console.log("barrel evaluated");
diff --git a/tests/corpus/6121-type-specifier-edges/config.ts b/tests/corpus/6121-type-specifier-edges/config.ts
new file mode 100644
index 000000000..1a42a9093
--- /dev/null
+++ b/tests/corpus/6121-type-specifier-edges/config.ts
@@ -0,0 +1,4 @@
+export interface Config {
+ name: string;
+}
+console.log("config evaluated");
diff --git a/tests/corpus/6121-type-specifier-edges/main.ts b/tests/corpus/6121-type-specifier-edges/main.ts
new file mode 100644
index 000000000..23855b470
--- /dev/null
+++ b/tests/corpus/6121-type-specifier-edges/main.ts
@@ -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());
diff --git a/tests/corpus/6122-punycode-conversions.ts b/tests/corpus/6122-punycode-conversions.ts
new file mode 100644
index 000000000..061987a78
--- /dev/null
+++ b/tests/corpus/6122-punycode-conversions.ts
@@ -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;
diff --git a/tests/corpus/6124-punycode-require-warnings.cjs b/tests/corpus/6124-punycode-require-warnings.cjs
new file mode 100644
index 000000000..c3255e6b7
--- /dev/null
+++ b/tests/corpus/6124-punycode-require-warnings.cjs
@@ -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:)"));
+ }
+}
diff --git a/tests/corpus/6125-constants-module.cjs b/tests/corpus/6125-constants-module.cjs
new file mode 100644
index 000000000..d7a547ac5
--- /dev/null
+++ b/tests/corpus/6125-constants-module.cjs
@@ -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"));
diff --git a/tests/corpus/6126-punycode-dependency/main.cjs b/tests/corpus/6126-punycode-dependency/main.cjs
new file mode 100644
index 000000000..530503a71
--- /dev/null
+++ b/tests/corpus/6126-punycode-dependency/main.cjs
@@ -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);
diff --git a/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.d.ts b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.d.ts
new file mode 100644
index 000000000..89b712210
--- /dev/null
+++ b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.d.ts
@@ -0,0 +1,2 @@
+declare function toAscii(domain: string): string;
+export = toAscii;
diff --git a/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.js b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.js
new file mode 100644
index 000000000..417c44889
--- /dev/null
+++ b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/index.js
@@ -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);
+};
diff --git a/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/package.json b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/package.json
new file mode 100644
index 000000000..71edeb25c
--- /dev/null
+++ b/tests/corpus/6126-punycode-dependency/node_modules/puny-dep/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "puny-dep",
+ "version": "1.0.0",
+ "main": "index.js"
+}
diff --git a/tests/fixtures/npm-static/node_modules/@types/twinned/index.d.ts b/tests/fixtures/npm-static/node_modules/@types/twinned/index.d.ts
new file mode 100644
index 000000000..ef7c0d6c6
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/@types/twinned/index.d.ts
@@ -0,0 +1,5 @@
+export interface Greeting {
+ text: string;
+}
+
+export default function wait(text: string): string;
diff --git a/tests/fixtures/npm-static/node_modules/@types/twinned/package.json b/tests/fixtures/npm-static/node_modules/@types/twinned/package.json
new file mode 100644
index 000000000..2bdbbfa6e
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/@types/twinned/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "@types/twinned",
+ "version": "1.0.0",
+ "types": "index.d.ts"
+}
diff --git a/tests/fixtures/npm-static/node_modules/twinned/index.js b/tests/fixtures/npm-static/node_modules/twinned/index.js
new file mode 100644
index 000000000..0aa0a79f0
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/twinned/index.js
@@ -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);
+};
diff --git a/tests/fixtures/npm-static/node_modules/twinned/package.json b/tests/fixtures/npm-static/node_modules/twinned/package.json
new file mode 100644
index 000000000..14cce5ab2
--- /dev/null
+++ b/tests/fixtures/npm-static/node_modules/twinned/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "twinned",
+ "version": "1.0.0",
+ "main": "index.js"
+}
diff --git a/tests/fixtures/npm-static/twinned-cli.ts b/tests/fixtures/npm-static/twinned-cli.ts
new file mode 100644
index 000000000..e24383184
--- /dev/null
+++ b/tests/fixtures/npm-static/twinned-cli.ts
@@ -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"));
diff --git a/tests/fixtures/npm-static/type-only-cli.ts b/tests/fixtures/npm-static/type-only-cli.ts
new file mode 100644
index 000000000..9fd332331
--- /dev/null
+++ b/tests/fixtures/npm-static/type-only-cli.ts
@@ -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);
diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts
index c5021fd06..ba716076c 100644
--- a/tests/harness/npm-static.test.ts
+++ b/tests/harness/npm-static.test.ts
@@ -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" },
]);