Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions test-parity/node-suite/module/EVIDENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# `node:module` evidence matrix

This file makes the focused audit reproducible at the per-entry level. Source
paths refer to the pinned upstream trees and commits listed in `README.md`.
“match” means stdout and exit code exactly match Node 26.5.0; “ok-diff” means
the runtime exits successfully with intentionally different stdout; `exit-1`
means the API/contract is unavailable or rejects the probe. Perry statuses use
the suite runner buckets.

## Reproduction

```sh
NODE_BIN="$HOME/.nvm/versions/node/v26.5.0/bin/node" \
python3 scripts/node_suite_run.py "$PWD/target/release/perry" "$PWD" module
# Run each staged-added .ts entry twice with Node, then once with:
deno run --allow-all <entry>
bun run <entry>
```

Two complete Node runs, including file headers, were byte-identical at
`85ffb7464d6c8e860fb2741cc635c2d59711636d1dfafb5a742a8355c95ab1a2`. Two focused
runner reports were byte-identical at
`1a8fee9fa4a5f8267e262d3623a6830a4226858bbb22ea315db724abaafb6eb4`; two per-file
Perry classifications were byte-identical at
`3eddc8e08d9e49fffe8633061efd8bb7e566e3f41e664a2e3e291d5bcda30ef6`.

## Added-entry matrix

| Entry | Primary Node contract | Perry | Deno 2.9.2 | Bun 1.2.18 |
| -------------------------------------- | ------------------------------------------------------------------------------------------ | -------------- | ---------- | ---------- |
| `commonjs/constructor-prototype.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-wrap.js` / `test-module-children.js` | `diff` | `ok-diff` | `ok-diff` |
| `commonjs/prototype-descriptors.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-wrap.js` / `test-module-children.js` | `diff` | `ok-diff` | `ok-diff` |
| `commonjs/wrap-wrapper.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-wrap.js` / `test-module-children.js` | `diff` | `match` | `ok-diff` |
| `exports/builtin-modules.ts` | `lib/module.js`; `test-module-builtin.js`; `test-module-isBuiltin.js` | `diff` | `ok-diff` | `ok-diff` |
| `exports/constants.ts` | `lib/module.js`; `test-module-builtin.js`; `test-module-isBuiltin.js` | `diff` | `exit-1` | `ok-diff` |
| `exports/default-static-identity.ts` | `lib/module.js`; `test-module-builtin.js`; `test-module-isBuiltin.js` | `diff` | `ok-diff` | `ok-diff` |
| `exports/descriptors.ts` | `lib/module.js`; `test-module-builtin.js`; `test-module-isBuiltin.js` | `diff` | `exit-1` | `match` |
| `exports/public-surface.ts` | `lib/module.js`; `test-module-builtin.js`; `test-module-isBuiltin.js` | `diff` | `ok-diff` | `ok-diff` |
| `helpers/compile-cache-disabled.ts` | compile-cache exports in `lib/module.js`; `lib/internal/modules/helpers.js` | `pass` | `exit-1` | `exit-1` |
| `helpers/strip-options-validation.ts` | `test-module-strip-types.js`; `lib/internal/modules/typescript.js` | `pass` | `ok-diff` | `exit-1` |
| `helpers/strip-syntax.ts` | `test-module-strip-types.js`; `lib/internal/modules/typescript.js` | `diff` | `match` | `exit-1` |
| `imports/cjs-dynamic-identity.ts` | `lib/internal/modules/esm/translators.js`; package/JSON cache tests under `test/es-module` | `diff` | `match` | `ok-diff` |
| `imports/cjs-namespace.ts` | `lib/internal/modules/esm/translators.js`; package/JSON cache tests under `test/es-module` | `diff` | `match` | `ok-diff` |
| `imports/json-attribute.ts` | `lib/internal/modules/esm/translators.js`; package/JSON cache tests under `test/es-module` | `pass` | `match` | `match` |
| `imports/json-require-identity.ts` | `lib/internal/modules/esm/translators.js`; package/JSON cache tests under `test/es-module` | `diff` | `ok-diff` | `ok-diff` |
| `imports/package-conditions.ts` | `lib/internal/modules/esm/translators.js`; package/JSON cache tests under `test/es-module` | `compile_fail` | `match` | `match` |
| `loader/register-hooks-order.ts` | `lib/internal/modules/customization_hooks.js`; Deno `module_register_hooks*` specs | `diff` | `match` | `exit-1` |
| `methods/find-package-json.ts` | `lib/internal/modules/package_json_reader.js`; `findPackageJSON` in `lib/module.js` | `diff` | `ok-diff` | `exit-1` |
| `methods/function-shapes.ts` | `lib/module.js`; `test-module-isBuiltin.js` | `diff` | `exit-1` | `exit-1` |
| `methods/is-builtin-validation.ts` | `lib/module.js`; `test-module-isBuiltin.js` | `diff` | `match` | `match` |
| `methods/sync-builtin-exports.ts` | `lib/internal/modules/esm/translators.js`; builtin live-binding tests | `diff` | `ok-diff` | `ok-diff` |
| `require/cache-identity-deletion.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-create-require.js` | `diff` | `match` | `match` |
| `require/create-require-overloads.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-create-require.js` | `diff` | `match` | `match` |
| `require/cycles.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-cache.js` / `test-module-children.js` | `diff` | `match` | `match` |
| `require/error-fields.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-create-require.js` | `compile_fail` | `match` | `ok-diff` |
| `require/exports-alias.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-create-require.js` | `diff` | `match` | `match` |
| `require/extensions.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-create-require.js` | `diff` | `exit-1` | `ok-diff` |
| `require/function-descriptors.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-create-require.js` | `diff` | `ok-diff` | `ok-diff` |
| `require/json-cache.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-create-require.js` | `diff` | `match` | `match` |
| `require/module-load.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-cache.js` / `test-module-children.js` | `diff` | `ok-diff` | `exit-1` |
| `require/module-metadata.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-cache.js` / `test-module-children.js` | `diff` | `match` | `ok-diff` |
| `require/package-boundaries.ts` | CJS resolver in `loader.js`; package exports/imports fixtures | `compile_fail` | `match` | `ok-diff` |
| `require/parent-children.ts` | `lib/internal/modules/cjs/loader.js`; `test-module-cache.js` / `test-module-children.js` | `diff` | `match` | `ok-diff` |
| `require/resolve-paths.ts` | CJS resolver in `loader.js`; package exports/imports fixtures | `diff` | `ok-diff` | `ok-diff` |
| `source-map/constructor-payload.ts` | `test-source-map-api.js`; `lib/internal/source_map/source_map.js` | `diff` | `ok-diff` | `exit-1` |
| `source-map/descriptors-validation.ts` | `test-source-map-api.js`; `lib/internal/source_map/source_map.js` | `diff` | `ok-diff` | `exit-1` |
| `source-map/find-inline.ts` | `test-source-map-api.js`; `lib/internal/source_map/source_map.js` | `diff` | `exit-1` | `exit-1` |
| `source-map/getter-semantics.ts` | `test-source-map-api.js`; `lib/internal/source_map/source_map.js` | `diff` | `ok-diff` | `exit-1` |
| `source-map/lookup-boundaries.ts` | `test-source-map-api.js`; `lib/internal/source_map/source_map.js` | `diff` | `ok-diff` | `exit-1` |
| `source-map/malformed-indexed.ts` | `test-source-map-api.js`; `lib/internal/source_map/source_map.js` | `diff` | `ok-diff` | `exit-1` |
| `source-map/receiver-validation.ts` | `test-source-map-api.js`; `lib/internal/source_map/source_map.js` | `diff` | `ok-diff` | `exit-1` |

## Rejected or separate-lane candidates

- `stripTypeScriptTypes(..., { mode: "transform" })`: the exact Node 26.5.0
build rejects the mode; only the supported strip and validation contracts
remain.
- Compile-cache `FAILED`/`DISABLED` production: forcing them requires
permissions, read-only/global host paths, or process flags. Constants,
disabled-state reads, enabling, already-enabled state, directory selection,
flushing, and cleanup are covered without fabricating failure state.
- Custom generated loader source and runtime-only virtual modules: these
conflate loader semantics with unsupported AOT dynamic-code evaluation.
Pass-through registration, validation, hook order, dynamic import invocation,
and deregistration are covered.
- Symlink/realpath matrices, global search paths, workers, policies/permissions,
native addons, SEA/snapshots, coverage/inspector, watch mode, races, and
stress remain in isolated platform/process lanes described in `README.md`.
97 changes: 97 additions & 0 deletions test-parity/node-suite/module/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# `node:module` granular parity

This lane treats Node.js **26.5.0** as the oracle. Each TypeScript entry point
prints one deterministic contract; controlled CommonJS, ESM, JSON, package, and
source-map fixtures live below this directory and never depend on a registry,
the user home, or global `node_modules`.

## Inventory

The expansion adds **41** entry points, taking the runner-visible lane from **28
to 69**. The 69 entries are grouped by directory as follows:

| Area | Added | Total | Contracts |
| ------------ | ----: | ----: | ------------------------------------------------------------- |
| `commonjs` | 3 | 5 | constructor/prototype shape, wrapper, resolver helpers |
| `exports` | 5 | 5 | public names, descriptors, constants, default/static identity |
| `helpers` | 3 | 6 | compile cache, source-map support, TypeScript stripping |
| `imports` | 5 | 6 | CJS namespace identity, JSON, conditional exports |
| `loader` | 1 | 19 | hook chaining plus existing registration/dynamic-import cases |
| `methods` | 4 | 6 | function shapes, builtins, package JSON, builtin ESM sync |
| `require` | 13 | 15 | overloads, resolution, cache, cycles, metadata, packages |
| `source-map` | 7 | 7 | payloads, getters, mappings, origins, validation |

`loader/fixtures/**/*.ts` contains ten pre-existing executable fixture modules.
The generic runner intentionally counts every `.ts` file recursively, so they
remain represented in the total and baseline rather than being silently
subtracted in this document.

## Primary evidence

The audit is pinned to primary sources, not to latest-branch behavior:

[`EVIDENCE.md`](./EVIDENCE.md) records the per-entry source basis,
Perry/Deno/Bun classification, reproduction commands, and hashes from repeated
runs.

- Node 26.5.0 (`bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb`):
[`lib/module.js`](https://github.com/nodejs/node/blob/v26.5.0/lib/module.js),
[`lib/internal/modules`](https://github.com/nodejs/node/tree/v26.5.0/lib/internal/modules),
and the upstream
[`test-module-*` / `test-source-map-*` tests](https://github.com/nodejs/node/tree/v26.5.0/test/parallel).
- Deno 2.9.2 (`356c132ed60e679b34535a287a493193aa8bb6a4`):
[`ext/node/polyfills/01_require.js`](https://github.com/denoland/deno/blob/v2.9.2/ext/node/polyfills/01_require.js),
[`ext/node/ops/module.rs`](https://github.com/denoland/deno/blob/v2.9.2/ext/node/ops/module.rs),
and its pinned
[`node/module` specifications](https://github.com/denoland/deno/tree/v2.9.2/tests/specs/node).
- Bun 1.2.18 (`0d4089ea7c48d339e87cc48f1871aeee745d8112`):
[`NodeModuleModule`](https://github.com/oven-sh/bun/tree/bun-v1.2.18/src/bun.js/modules)
and the pinned
[`node:module` tests](https://github.com/oven-sh/bun/tree/bun-v1.2.18/test/js/node/module).

Repeated execution of all 69 entries under Node 26.5.0 produced byte-identical
stdout. The focused Perry classification is **29 pass / 69 total**: 37 stable
output differences and three stable compile failures
(`imports/package-conditions.ts`, `require/error-fields.ts`, and
`require/package-boundaries.ts`), with no runtime timeout, compile timeout, or
signal bucket. The floor is measured from those results; failures are retained
to expose claimed-surface gaps rather than weakening the Node assertions.

For the 41 additions, Deno exactly matches 17, completes with an intentional or
implementation-specific output difference on 18, and exits non-zero on 6. Bun
exactly matches 9, completes with a differing result on 18, and exits non-zero
on 14. Node remains authoritative where builtin inventories, namespace
properties, source-map APIs, compile-cache helpers, or error details diverge.

## Isolation rules

- Paths and file URLs printed by fixtures are reduced to semantic suffixes or
`<cwd>` markers.
- Cache mutations are deleted before exit. Builtin mutations are restored in a
`finally` block and followed by `syncBuiltinESMExports()`.
- Loader hooks run in their own process and are deregistered.
- Compile-cache probes use a uniquely-created OS temporary directory and remove
it recursively in `finally`.
- Source-map tests use only controlled inline payloads and normalize resolved
source URLs. No raw stack or warning text participates in stdout.
- Package fixtures are local, minimal, and cover `main`, `exports`, `imports`,
condition selection, and hidden-subpath errors without registry access.

## Deliberate stopping boundary

The deterministic public and legacy-public surface claimed by this lane is
covered through exports/descriptors, `Module`, `createRequire`, CommonJS cache
and graph semantics, CJS/ESM/JSON interop, package boundaries, source maps,
loader registration, compile-cache state, TypeScript stripping, and builtin ESM
synchronization. Node 26.5.0 on this build exposes `stripTypeScriptTypes()` but
rejects `mode: "transform"`; the lane records that boundary instead of inventing
transform output from a different build.

Custom loader source generation, permission/policy state, workers, symlink and
realpath matrices, native addons, SEA/snapshots, coverage/inspector coupling,
watch mode, huge graphs, races, resource exhaustion, and platform-specific
global search paths remain separate lanes. Runtime-generated modules and eval
are not used as proxies for AOT module support. These exclusions either require
process flags or host state, duplicate another granular suite, are inherently
platform-sensitive, or cannot distinguish a module parity gap from an AOT
dynamic-code limitation.
43 changes: 43 additions & 0 deletions test-parity/node-suite/module/commonjs/constructor-prototype.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Module } from "node:module";

console.log(
"constructor:",
Module.name,
Module.length,
Module.Module === Module,
);
console.log(
"prototype names:",
Object.getOwnPropertyNames(Module.prototype).sort().join(","),
);
console.log(
"prototype chain:",
Object.getPrototypeOf(Module.prototype) === Object.prototype,
);

const parent = new Module("parent-id");
const child = new Module("child-id", parent);
console.log(
"parent defaults:",
parent.id,
JSON.stringify(parent.path),
JSON.stringify(parent.filename),
parent.loaded,
parent.children.length,
Array.isArray(parent.paths),
);
console.log(
"child relationship:",
child.parent === parent,
parent.children[0] === child,
parent.children.length,
);
console.log(
"child exports prototype:",
Object.getPrototypeOf(child.exports) === Object.prototype,
);
console.log(
"instanceof:",
child instanceof Module,
child.constructor === Module,
);
19 changes: 19 additions & 0 deletions test-parity/node-suite/module/commonjs/prototype-descriptors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Module } from "node:module";

for (const key of Object.getOwnPropertyNames(Module.prototype).sort()) {
const descriptor = Object.getOwnPropertyDescriptor(Module.prototype, key)!;
const value = "value" in descriptor ? descriptor.value : undefined;
console.log(
key,
JSON.stringify({
type: typeof value,
name: typeof value === "function" ? value.name : "",
length: typeof value === "function" ? value.length : -1,
enumerable: descriptor.enumerable,
writable: descriptor.writable,
configurable: descriptor.configurable,
get: typeof descriptor.get,
set: typeof descriptor.set,
}),
);
}
20 changes: 20 additions & 0 deletions test-parity/node-suite/module/commonjs/wrap-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ModuleDefault from "node:module";

const source = "exports.answer = 42;\n";
const wrapped = ModuleDefault.wrap(source);
console.log(
"wrapper array:",
Array.isArray(ModuleDefault.wrapper),
ModuleDefault.wrapper.length,
);
console.log("wrapper values:", JSON.stringify(ModuleDefault.wrapper));
console.log(
"wrap exact:",
wrapped === ModuleDefault.wrapper[0] + source + ModuleDefault.wrapper[1],
);
console.log("wrapped value:", JSON.stringify(wrapped));
console.log(
"function shapes:",
ModuleDefault.wrap.name,
ModuleDefault.wrap.length,
);
38 changes: 38 additions & 0 deletions test-parity/node-suite/module/exports/builtin-modules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { builtinModules, isBuiltin } from "node:module";

const sorted = [...builtinModules].sort();
console.log("array:", Array.isArray(builtinModules));
console.log("sorted unique:", sorted.length === new Set(sorted).size);
console.log(
"already sorted:",
JSON.stringify(sorted) === JSON.stringify(builtinModules),
);
console.log(
"contains core:",
["assert", "fs", "fs/promises", "module", "sys"].every((name) =>
builtinModules.includes(name)
),
);
console.log(
"prefixed entries:",
JSON.stringify(builtinModules.filter((name) => name.startsWith("node:"))),
);
console.log(
"excludes internals:",
builtinModules.every((name) => !name.startsWith("internal/")),
);
console.log(
"prefix-only builtins:",
isBuiltin("node:test"),
isBuiltin("test"),
isBuiltin("node:sea"),
isBuiltin("sea"),
);
console.log(
"inventory agrees:",
builtinModules.every((name) => isBuiltin(name)),
);
console.log(
"descriptor:",
JSON.stringify(Object.getOwnPropertyDescriptor(builtinModules, "length")),
);
20 changes: 20 additions & 0 deletions test-parity/node-suite/module/exports/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { constants } from "node:module";

const status = constants.compileCacheStatus;
console.log("constant keys:", Object.keys(constants).sort().join(","));
console.log("status keys:", Object.keys(status).sort().join(","));
console.log(
"status values:",
Object.entries(status).sort().map(([key, value]) => `${key}:${value}`).join(
"|",
),
);
console.log(
"objects frozen:",
Object.isFrozen(constants),
Object.isFrozen(status),
);
console.log(
"descriptors:",
JSON.stringify(Object.getOwnPropertyDescriptors(status)),
);
Loading
Loading