diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx
index ed2c5b1ec..a66b2ffd6 100644
--- a/docs/content/docs/limitations.mdx
+++ b/docs/content/docs/limitations.mdx
@@ -74,6 +74,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 93149c51c..a063052f3 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": "a1ff929d49885bd8c92e",
+ "artifactVersion": "7b345b86e2e5f616a7a0",
"rowCount": 3880
}
diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json
index 352c211b5..62eb8283e 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 fff784adc..c4ce78314 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": 1091,
- "verify-gap": 2238,
+ "audit-partial": 1098,
+ "verify-gap": 2231,
"replace-refusal": 40,
"classify": 2,
"implement": 85
@@ -58534,12 +58534,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"
+ ]
}
}
},
@@ -58559,12 +58563,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"
+ ]
}
}
},
@@ -58584,12 +58592,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"
+ ]
}
}
},
@@ -58609,12 +58621,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"
+ ]
}
}
},
@@ -58659,12 +58675,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"
+ ]
}
}
},
@@ -58684,12 +58704,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"
+ ]
}
}
},
@@ -58709,12 +58733,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 559bd48ca..58ffc3399 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
@@ -81908,8 +81908,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"
},
@@ -81931,8 +81936,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"
},
@@ -81954,8 +81964,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"
},
@@ -81977,8 +81992,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"
},
@@ -82000,8 +82020,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"
},
@@ -82046,8 +82071,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"
},
@@ -82069,8 +82099,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"
},
@@ -82092,8 +82127,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 d68011393..5332ddd4c 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -146,6 +146,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/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 d87172f61..2c53bb523 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -3643,6 +3643,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 50804bf5a..92dbaea76 100644
--- a/packages/compiler/src/backend/llvm/lib-dispatch.ts
+++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts
@@ -680,6 +680,8 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "events":
case "blob":
case "record":
+ 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 9232cbd03..700044da2 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -192,6 +192,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",
@@ -631,6 +632,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/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 08e12518b..88a4918d8 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -216,6 +216,7 @@ import {
} from "./class-prototypes.js";
import { isClassCallback } from "./class-callbacks.js";
import {
+ builtinConstantsObject,
builtinPrototypeMethod,
lowerArrayIsArrayValue,
lowerCheckedPredicateValue,
@@ -1164,6 +1165,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.
@@ -1206,6 +1218,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 };
@@ -1495,6 +1508,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 d18c4f547..b2f76cd24 100644
--- a/packages/compiler/src/frontend/lowering/lower-modules.ts
+++ b/packages/compiler/src/frontend/lowering/lower-modules.ts
@@ -2656,6 +2656,7 @@ export function lowerFileInit(
loc: loc0,
});
}
+ if (sf === lowerer.entry) header.push(...lowerer.punycodeLinkWarning());
for (const { stmt, dep } of orderedImportsOf(lowerer.program, sf)) {
const npm = lowerer.npmInitActions.get(sf)?.get(stmt);
if (npm !== undefined && npm.length > 0) {
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index 367c147f8..27f4169c1 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -1557,7 +1557,9 @@ export function lowerVarStatement(lowerer: Lowerer, stmt: ts.VariableStatement):
// 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 3217802ad..8e6f64143 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -128,6 +128,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";
@@ -3745,6 +3746,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
@@ -3754,6 +3814,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
@@ -11286,7 +11347,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 4c74e8b6a..72d6c09f8 100644
--- a/packages/compiler/src/frontend/lowering/surfaces.ts
+++ b/packages/compiler/src/frontend/lowering/surfaces.ts
@@ -1801,6 +1801,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
@@ -1930,6 +1946,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/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index 2381413f8..ee5ba8c7c 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -466,6 +466,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 5e219ecf6..e9f7778bc 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -559,6 +559,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. */
@@ -856,6 +860,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 80a9c9b57..ba97670ce 100644
--- a/packages/compiler/src/ir/runtime-features.ts
+++ b/packages/compiler/src/ir/runtime-features.ts
@@ -43,6 +43,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 63efde378..3ee171669 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",
@@ -2095,6 +2102,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/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 a391d2a26..f41949f6e 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 a14ae6e6a..7a27f16f3 100644
--- a/packages/runtime/src/scr_async_dyn.c
+++ b/packages/runtime/src/scr_async_dyn.c
@@ -813,6 +813,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 0bc50fb3f..0df57d2e1 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
@@ -1286,3 +1288,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 b40a8027a..6f8d9b616 100644
--- a/packages/runtime/src/scr_os.h
+++ b/packages/runtime/src/scr_os.h
@@ -34,6 +34,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 b6fdb9cd6..42a7ef607 100644
--- a/packages/runtime/src/scr_url.c
+++ b/packages/runtime/src/scr_url.c
@@ -1988,3 +1988,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/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"
+}