diff --git a/test-parity/node-suite/module/EVIDENCE.md b/test-parity/node-suite/module/EVIDENCE.md new file mode 100644 index 0000000000..b9784f6224 --- /dev/null +++ b/test-parity/node-suite/module/EVIDENCE.md @@ -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 +bun run +``` + +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`. diff --git a/test-parity/node-suite/module/README.md b/test-parity/node-suite/module/README.md new file mode 100644 index 0000000000..cd443764df --- /dev/null +++ b/test-parity/node-suite/module/README.md @@ -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 + `` 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. diff --git a/test-parity/node-suite/module/commonjs/constructor-prototype.ts b/test-parity/node-suite/module/commonjs/constructor-prototype.ts new file mode 100644 index 0000000000..8818ea9f84 --- /dev/null +++ b/test-parity/node-suite/module/commonjs/constructor-prototype.ts @@ -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, +); diff --git a/test-parity/node-suite/module/commonjs/prototype-descriptors.ts b/test-parity/node-suite/module/commonjs/prototype-descriptors.ts new file mode 100644 index 0000000000..6aff10607f --- /dev/null +++ b/test-parity/node-suite/module/commonjs/prototype-descriptors.ts @@ -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, + }), + ); +} diff --git a/test-parity/node-suite/module/commonjs/wrap-wrapper.ts b/test-parity/node-suite/module/commonjs/wrap-wrapper.ts new file mode 100644 index 0000000000..aea6d0f22f --- /dev/null +++ b/test-parity/node-suite/module/commonjs/wrap-wrapper.ts @@ -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, +); diff --git a/test-parity/node-suite/module/exports/builtin-modules.ts b/test-parity/node-suite/module/exports/builtin-modules.ts new file mode 100644 index 0000000000..ea74770265 --- /dev/null +++ b/test-parity/node-suite/module/exports/builtin-modules.ts @@ -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")), +); diff --git a/test-parity/node-suite/module/exports/constants.ts b/test-parity/node-suite/module/exports/constants.ts new file mode 100644 index 0000000000..40e6e12f04 --- /dev/null +++ b/test-parity/node-suite/module/exports/constants.ts @@ -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)), +); diff --git a/test-parity/node-suite/module/exports/default-static-identity.ts b/test-parity/node-suite/module/exports/default-static-identity.ts new file mode 100644 index 0000000000..251774d1a4 --- /dev/null +++ b/test-parity/node-suite/module/exports/default-static-identity.ts @@ -0,0 +1,49 @@ +import ModuleDefault, * as moduleNS from "node:module"; + +const keys = [ + "Module", + "SourceMap", + "_cache", + "_extensions", + "_findPath", + "_initPaths", + "_load", + "_nodeModulePaths", + "_pathCache", + "_preloadModules", + "_resolveFilename", + "_resolveLookupPaths", + "builtinModules", + "constants", + "createRequire", + "enableCompileCache", + "findPackageJSON", + "findSourceMap", + "flushCompileCache", + "getCompileCacheDir", + "getSourceMapsSupport", + "globalPaths", + "isBuiltin", + "register", + "registerHooks", + "runMain", + "setSourceMapsSupport", + "stripTypeScriptTypes", + "syncBuiltinESMExports", +] as const; + +console.log( + "all identities:", + keys.every((key) => (ModuleDefault as any)[key] === moduleNS[key]), +); +console.log( + "default extras:", + typeof ModuleDefault.wrap, + Array.isArray(ModuleDefault.wrapper), +); +console.log( + "own names include extras:", + ["wrap", "wrapper", "_readPackage", "_stat"].every((key) => + Object.prototype.hasOwnProperty.call(ModuleDefault, key) + ), +); diff --git a/test-parity/node-suite/module/exports/descriptors.ts b/test-parity/node-suite/module/exports/descriptors.ts new file mode 100644 index 0000000000..5361f13160 --- /dev/null +++ b/test-parity/node-suite/module/exports/descriptors.ts @@ -0,0 +1,30 @@ +import * as moduleNS from "node:module"; + +for ( + const key of [ + "Module", + "builtinModules", + "constants", + "createRequire", + "default", + ] +) { + const descriptor = Object.getOwnPropertyDescriptor(moduleNS, key)!; + console.log( + key, + JSON.stringify({ + enumerable: descriptor.enumerable, + writable: descriptor.writable, + configurable: descriptor.configurable, + getter: typeof descriptor.get, + setter: typeof descriptor.set, + }), + ); +} + +console.log( + "namespace frozen/sealed:", + Object.isFrozen(moduleNS), + Object.isSealed(moduleNS), +); +console.log("symbol tag:", moduleNS[Symbol.toStringTag]); diff --git a/test-parity/node-suite/module/exports/public-surface.ts b/test-parity/node-suite/module/exports/public-surface.ts new file mode 100644 index 0000000000..89db3a707d --- /dev/null +++ b/test-parity/node-suite/module/exports/public-surface.ts @@ -0,0 +1,47 @@ +import ModuleDefault, * as moduleNS from "node:module"; + +const expected = [ + "Module", + "SourceMap", + "_cache", + "_extensions", + "_findPath", + "_initPaths", + "_load", + "_nodeModulePaths", + "_pathCache", + "_preloadModules", + "_resolveFilename", + "_resolveLookupPaths", + "builtinModules", + "constants", + "createRequire", + "default", + "enableCompileCache", + "findPackageJSON", + "findSourceMap", + "flushCompileCache", + "getCompileCacheDir", + "getSourceMapsSupport", + "globalPaths", + "isBuiltin", + "register", + "registerHooks", + "runMain", + "setSourceMapsSupport", + "stripTypeScriptTypes", + "syncBuiltinESMExports", +]; + +console.log("keys:", JSON.stringify(Object.keys(moduleNS).sort())); +console.log( + "exact:", + JSON.stringify(Object.keys(moduleNS).sort()) === JSON.stringify(expected), +); +console.log( + "identity:", + ModuleDefault === moduleNS.Module, + moduleNS.default === moduleNS.Module, +); +console.log("tag:", Object.prototype.toString.call(moduleNS)); +console.log("extensible:", Object.isExtensible(moduleNS)); diff --git a/test-parity/node-suite/module/helpers/compile-cache-disabled.ts b/test-parity/node-suite/module/helpers/compile-cache-disabled.ts new file mode 100644 index 0000000000..c093d3a79d --- /dev/null +++ b/test-parity/node-suite/module/helpers/compile-cache-disabled.ts @@ -0,0 +1,5 @@ +import { flushCompileCache, getCompileCacheDir } from "node:module"; + +console.log("directory before enable:", String(getCompileCacheDir())); +console.log("flush before enable:", String(flushCompileCache())); +console.log("directory after flush:", String(getCompileCacheDir())); diff --git a/test-parity/node-suite/module/helpers/compile-cache.ts b/test-parity/node-suite/module/helpers/compile-cache.ts index 5011779864..f1dcf1eace 100644 --- a/test-parity/node-suite/module/helpers/compile-cache.ts +++ b/test-parity/node-suite/module/helpers/compile-cache.ts @@ -1,35 +1,49 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import * as Module from "node:module"; const status = Module.constants.compileCacheStatus; +const root = mkdtempSync(join(tmpdir(), "perry-node-module-cache-probe-")); +const firstDirectory = join(root, "first"); +const secondDirectory = join(root, "second"); -console.log("status keys:", Object.keys(status).sort().join(",")); -console.log( - "status values:", - Object.keys(status) - .sort() - .map((key) => `${key}=${status[key]}`) - .join(","), -); -console.log("cache before:", String(Module.getCompileCacheDir())); +try { + console.log("status keys:", Object.keys(status).sort().join(",")); + console.log( + "status values:", + Object.keys(status) + .sort() + .map((key) => `${key}=${status[key]}`) + .join(","), + ); + console.log("cache before:", String(Module.getCompileCacheDir())); -const first = Module.enableCompileCache("/tmp/perry-node-module-cache-probe"); -console.log("first keys:", Object.keys(first).sort().join(",")); -console.log("first enabled:", first.status === status.ENABLED); -console.log( - "first directory contains:", - String(first.directory).includes("perry-node-module-cache-probe"), -); -console.log( - "cache after contains:", - String(Module.getCompileCacheDir()).includes("perry-node-module-cache-probe"), -); + const first = Module.enableCompileCache(firstDirectory); + console.log("first keys:", Object.keys(first).sort().join(",")); + console.log("first enabled:", first.status === status.ENABLED); + console.log( + "first directory contains:", + String(first.directory).includes("perry-node-module-cache-probe"), + ); + console.log( + "cache after contains:", + String(Module.getCompileCacheDir()).includes( + "perry-node-module-cache-probe", + ), + ); -const capturedGetCompileCacheDir = Module.getCompileCacheDir; -console.log( - "captured cache contains:", - String(capturedGetCompileCacheDir()).includes("perry-node-module-cache-probe"), -); + const capturedGetCompileCacheDir = Module.getCompileCacheDir; + console.log( + "captured cache contains:", + String(capturedGetCompileCacheDir()).includes( + "perry-node-module-cache-probe", + ), + ); -const second = Module.enableCompileCache("/tmp/perry-node-module-cache-probe-2"); -console.log("second already:", second.status === status.ALREADY_ENABLED); -console.log("flush:", String(Module.flushCompileCache())); + const second = Module.enableCompileCache(secondDirectory); + console.log("second already:", second.status === status.ALREADY_ENABLED); + console.log("flush:", String(Module.flushCompileCache())); +} finally { + rmSync(root, { recursive: true, force: true }); +} diff --git a/test-parity/node-suite/module/helpers/strip-options-validation.ts b/test-parity/node-suite/module/helpers/strip-options-validation.ts new file mode 100644 index 0000000000..87d7c8ef82 --- /dev/null +++ b/test-parity/node-suite/module/helpers/strip-options-validation.ts @@ -0,0 +1,20 @@ +process.emitWarning = () => false; +import { stripTypeScriptTypes } from "node:module"; + +for ( + const [label, options] of [ + ["null", null], + ["number", 1], + ["bad mode", { mode: "erase" }], + ["strip map", { mode: "strip", sourceMap: true }], + ["map string", { mode: "transform", sourceMap: "yes" }], + ["url number", { mode: "transform", sourceUrl: 1 }], + ] as const +) { + try { + const result = stripTypeScriptTypes("const x: number = 1;", options as any); + console.log(label, "ok", JSON.stringify(result)); + } catch (error) { + console.log(label, (error as any).name, (error as any).code ?? "no-code"); + } +} diff --git a/test-parity/node-suite/module/helpers/strip-syntax.ts b/test-parity/node-suite/module/helpers/strip-syntax.ts new file mode 100644 index 0000000000..410c3f35c4 --- /dev/null +++ b/test-parity/node-suite/module/helpers/strip-syntax.ts @@ -0,0 +1,26 @@ +process.emitWarning = () => false; +import { stripTypeScriptTypes } from "node:module"; + +const source = [ + "type Pair = [T, T];", + "interface Box { value: T }", + "const pair: Pair = [1, 2];", + "const box = { value: pair[0] } satisfies Box;", + "const asserted = box.value as number;", +].join("\n"); +const output = stripTypeScriptTypes(source); +console.log("line count:", output.split("\n").length); +console.log("length preserved:", output.length === source.length); +console.log( + "runtime preserved:", + output.includes("const pair"), + output.includes("const box"), + output.includes("const asserted"), +); +console.log( + "types absent:", + !output.includes("interface Box"), + !output.includes("satisfies"), + !output.includes("as number"), +); +console.log("prefix:", JSON.stringify(output.split("\n").slice(0, 3))); diff --git a/test-parity/node-suite/module/imports/cjs-dynamic-identity.ts b/test-parity/node-suite/module/imports/cjs-dynamic-identity.ts new file mode 100644 index 0000000000..f78282b411 --- /dev/null +++ b/test-parity/node-suite/module/imports/cjs-dynamic-identity.ts @@ -0,0 +1,17 @@ +import { createRequire } from "node:module"; + +const req = createRequire(import.meta.url); +const required = req("./fixtures/cjs-object.cjs"); +const first = await import("./fixtures/cjs-object.cjs"); +const second = await import("./fixtures/cjs-object.cjs"); +console.log("namespace cached:", first === second); +console.log( + "default/marker:", + first.default === required, + first["module.exports"] === required, +); +console.log("named/shared:", first.named, first.shared === required.shared); +console.log( + "require cache:", + req.cache[req.resolve("./fixtures/cjs-object.cjs")]!.exports === required, +); diff --git a/test-parity/node-suite/module/imports/cjs-namespace.ts b/test-parity/node-suite/module/imports/cjs-namespace.ts new file mode 100644 index 0000000000..a4665ef42b --- /dev/null +++ b/test-parity/node-suite/module/imports/cjs-namespace.ts @@ -0,0 +1,26 @@ +import cjsDefault, * as namespace from "./fixtures/cjs-object.cjs"; +import { named, shared } from "./fixtures/cjs-object.cjs"; + +console.log( + "values:", + cjsDefault.named, + named, + namespace.named, + namespace.extra, +); +console.log( + "identity:", + namespace.default === cjsDefault, + namespace.shared === shared, + shared === cjsDefault.shared, +); +console.log("keys:", Object.keys(namespace).sort().join(",")); +console.log( + "module exports marker:", + (namespace as any)["module.exports"] === cjsDefault, +); +console.log( + "namespace tag/extensible:", + Object.prototype.toString.call(namespace), + Object.isExtensible(namespace), +); diff --git a/test-parity/node-suite/module/imports/fixtures/cjs-object.cjs b/test-parity/node-suite/module/imports/fixtures/cjs-object.cjs new file mode 100644 index 0000000000..0db6316a07 --- /dev/null +++ b/test-parity/node-suite/module/imports/fixtures/cjs-object.cjs @@ -0,0 +1,3 @@ +exports.named = 17; +exports.shared = { marker: "shared" }; +module.exports.extra = "extra"; diff --git a/test-parity/node-suite/module/imports/json-attribute.ts b/test-parity/node-suite/module/imports/json-attribute.ts new file mode 100644 index 0000000000..ff5a9c9415 --- /dev/null +++ b/test-parity/node-suite/module/imports/json-attribute.ts @@ -0,0 +1,8 @@ +import data from "../require/fixtures/data.json" with { type: "json" }; + +console.log("json:", data.name, data.count, data.nested.ok); +console.log( + "frozen/extensible:", + Object.isFrozen(data), + Object.isExtensible(data), +); diff --git a/test-parity/node-suite/module/imports/json-require-identity.ts b/test-parity/node-suite/module/imports/json-require-identity.ts new file mode 100644 index 0000000000..3968ea5a6c --- /dev/null +++ b/test-parity/node-suite/module/imports/json-require-identity.ts @@ -0,0 +1,8 @@ +import data from "../require/fixtures/data.json" with { type: "json" }; +import { createRequire } from "node:module"; + +const req = createRequire(import.meta.url); +const required = req("../require/fixtures/data.json"); +const cache = req.cache[req.resolve("../require/fixtures/data.json")]; +console.log("identity:", data === required, cache!.exports === data); +console.log("values:", required.name, required.count, required.nested.ok); diff --git a/test-parity/node-suite/module/imports/package-conditions.ts b/test-parity/node-suite/module/imports/package-conditions.ts new file mode 100644 index 0000000000..bb1254d442 --- /dev/null +++ b/test-parity/node-suite/module/imports/package-conditions.ts @@ -0,0 +1,15 @@ +import imported, { mode as importedMode } from "parity-conditions"; +import { createRequire } from "node:module"; + +const req = createRequire(import.meta.url); +const slash = (value: string) => value.replaceAll("\\", "/"); +const required = req("parity-conditions"); +console.log("import:", importedMode, imported.mode, imported.marker); +console.log("require:", required.mode, required.marker); +console.log("distinct:", imported !== required); +console.log( + "resolved branch:", + slash(req.resolve("parity-conditions")).endsWith( + "/parity-conditions/require.cjs", + ), +); diff --git a/test-parity/node-suite/module/loader/register-hooks-order.ts b/test-parity/node-suite/module/loader/register-hooks-order.ts new file mode 100644 index 0000000000..2e5319cd0f --- /dev/null +++ b/test-parity/node-suite/module/loader/register-hooks-order.ts @@ -0,0 +1,38 @@ +import { registerHooks } from "node:module"; + +const calls: string[] = []; +const first = registerHooks({ + resolve(specifier, context, nextResolve) { + calls.push("first:resolve:before"); + const result = nextResolve(specifier, context); + calls.push("first:resolve:after"); + return result; + }, + load(url, context, nextLoad) { + calls.push("first:load:before"); + const result = nextLoad(url, context); + calls.push("first:load:after"); + return result; + }, +}); +const second = registerHooks({ + resolve(specifier, context, nextResolve) { + calls.push("second:resolve:before"); + const result = nextResolve(specifier, context); + calls.push("second:resolve:after"); + return result; + }, + load(url, context, nextLoad) { + calls.push("second:load:before"); + const result = nextLoad(url, context); + calls.push("second:load:after"); + return result; + }, +}); +try { + await import("./fixtures/loader-hook-first.ts"); + console.log("order:", calls.join("|")); +} finally { + second.deregister(); + first.deregister(); +} diff --git a/test-parity/node-suite/module/methods/find-package-json.ts b/test-parity/node-suite/module/methods/find-package-json.ts new file mode 100644 index 0000000000..7e0bb03a1d --- /dev/null +++ b/test-parity/node-suite/module/methods/find-package-json.ts @@ -0,0 +1,40 @@ +import { findPackageJSON } from "node:module"; + +const normalize = (value: unknown) => + String(value).replace(process.cwd(), ""); +for ( + const [specifier, base] of [ + [ + "./require/fixtures/find-package/nested/probe.cjs", + new URL("../entry.ts", import.meta.url).href, + ], + ["parity-exports", new URL("../require/entry.ts", import.meta.url).href], + ["node:fs", import.meta.url], + ] as const +) { + try { + console.log(specifier, normalize(findPackageJSON(specifier, base))); + } catch (error) { + console.log( + specifier, + (error as any).name, + (error as any).code ?? "no-code", + ); + } +} +for (const value of [undefined, null, 1, {}, ""] as any[]) { + try { + console.log( + "bad", + Object.prototype.toString.call(value), + normalize(findPackageJSON(value, import.meta.url)), + ); + } catch (error) { + console.log( + "bad", + Object.prototype.toString.call(value), + (error as any).name, + (error as any).code ?? "no-code", + ); + } +} diff --git a/test-parity/node-suite/module/methods/function-shapes.ts b/test-parity/node-suite/module/methods/function-shapes.ts new file mode 100644 index 0000000000..1c4df232d6 --- /dev/null +++ b/test-parity/node-suite/module/methods/function-shapes.ts @@ -0,0 +1,29 @@ +import * as moduleNS from "node:module"; + +for ( + const key of [ + "createRequire", + "enableCompileCache", + "findPackageJSON", + "findSourceMap", + "flushCompileCache", + "getCompileCacheDir", + "getSourceMapsSupport", + "isBuiltin", + "register", + "registerHooks", + "runMain", + "setSourceMapsSupport", + "stripTypeScriptTypes", + "syncBuiltinESMExports", + ] as const +) { + const fn = moduleNS[key] as Function; + console.log( + key, + typeof fn, + fn.name, + fn.length, + Object.prototype.hasOwnProperty.call(fn, "prototype"), + ); +} diff --git a/test-parity/node-suite/module/methods/is-builtin-validation.ts b/test-parity/node-suite/module/methods/is-builtin-validation.ts new file mode 100644 index 0000000000..1406f025f3 --- /dev/null +++ b/test-parity/node-suite/module/methods/is-builtin-validation.ts @@ -0,0 +1,23 @@ +import { isBuiltin } from "node:module"; + +for ( + const value of [undefined, null, true, 0, 1n, Symbol("x"), {}, [], () => {}] +) { + let result: unknown; + try { + result = isBuiltin(value as any); + } catch (error) { + result = `${(error as any).name}:${(error as any).code ?? "no-code"}`; + } + console.log( + typeof value, + Object.prototype.toString.call(value), + String(result), + ); +} + +console.log( + "receiver ignored:", + isBuiltin.call(null, "fs"), + isBuiltin.call({ fake: true }, "node:path"), +); diff --git a/test-parity/node-suite/module/methods/sync-builtin-exports.ts b/test-parity/node-suite/module/methods/sync-builtin-exports.ts new file mode 100644 index 0000000000..1740d90b2e --- /dev/null +++ b/test-parity/node-suite/module/methods/sync-builtin-exports.ts @@ -0,0 +1,18 @@ +import fsDefault, { readFile } from "node:fs"; +import { createRequire, syncBuiltinESMExports } from "node:module"; + +const req = createRequire(import.meta.url); +const cjsFs = req("node:fs"); +const original = cjsFs.readFile; +const replacement = function parityReadFile() {}; +try { + console.log("initial identity:", fsDefault === cjsFs, readFile === original); + cjsFs.readFile = replacement; + console.log("before sync:", readFile === original, readFile === replacement); + console.log("return:", String(syncBuiltinESMExports())); + console.log("after sync:", readFile === original, readFile === replacement); +} finally { + cjsFs.readFile = original; + syncBuiltinESMExports(); +} +console.log("restored:", readFile === original, cjsFs.readFile === original); diff --git a/test-parity/node-suite/module/node_modules/parity-conditions/import.mjs b/test-parity/node-suite/module/node_modules/parity-conditions/import.mjs new file mode 100644 index 0000000000..846204ee02 --- /dev/null +++ b/test-parity/node-suite/module/node_modules/parity-conditions/import.mjs @@ -0,0 +1,2 @@ +export const mode = "import"; +export default { mode, marker: "esm" }; diff --git a/test-parity/node-suite/module/node_modules/parity-conditions/package.json b/test-parity/node-suite/module/node_modules/parity-conditions/package.json new file mode 100644 index 0000000000..30463bcf3e --- /dev/null +++ b/test-parity/node-suite/module/node_modules/parity-conditions/package.json @@ -0,0 +1,10 @@ +{ + "name": "parity-conditions", + "type": "module", + "exports": { + ".": { + "import": "./import.mjs", + "require": "./require.cjs" + } + } +} diff --git a/test-parity/node-suite/module/node_modules/parity-conditions/require.cjs b/test-parity/node-suite/module/node_modules/parity-conditions/require.cjs new file mode 100644 index 0000000000..5d726d341d --- /dev/null +++ b/test-parity/node-suite/module/node_modules/parity-conditions/require.cjs @@ -0,0 +1 @@ +module.exports = { mode: "require", marker: "cjs" }; diff --git a/test-parity/node-suite/module/require/cache-identity-deletion.ts b/test-parity/node-suite/module/require/cache-identity-deletion.ts new file mode 100644 index 0000000000..f77c4877d2 --- /dev/null +++ b/test-parity/node-suite/module/require/cache-identity-deletion.ts @@ -0,0 +1,31 @@ +import { createRequire } from "node:module"; + +const reqA = createRequire(import.meta.url); +const reqB = createRequire(new URL("./other-base.cjs", import.meta.url)); +const resolved = reqA.resolve("./fixtures/value.cjs"); +delete reqA.cache[resolved]; +const first = reqA("./fixtures/value.cjs"); +const second = reqB("./fixtures/value.cjs"); +console.log( + "shared identity/cache:", + first === second, + reqA.cache === reqB.cache, + reqA.cache[resolved]!.exports === first, +); +console.log( + "cache metadata:", + reqA.cache[resolved]!.loaded, + reqA.cache[resolved]!.filename === resolved, + Array.isArray(reqA.cache[resolved]!.children), +); +delete reqA.cache[resolved]; +const third = reqA("./fixtures/value.cjs"); +console.log( + "delete reload:", + third !== first, + first.loads, + third.loads, + reqA.cache[resolved]!.exports === third, +); +delete reqA.cache[resolved]; +delete (globalThis as any).__moduleValueLoads; diff --git a/test-parity/node-suite/module/require/create-require-overloads.ts b/test-parity/node-suite/module/require/create-require-overloads.ts new file mode 100644 index 0000000000..1526a84776 --- /dev/null +++ b/test-parity/node-suite/module/require/create-require-overloads.ts @@ -0,0 +1,32 @@ +import { createRequire } from "node:module"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const filename = fileURLToPath(import.meta.url); +const slash = (value: string) => value.replaceAll("\\", "/"); +const bases = [ + ["url object", pathToFileURL(filename)], + ["url string", pathToFileURL(filename).href], + ["absolute", filename], +] as const; +for (const [label, base] of bases) { + const req = createRequire(base); + console.log( + label, + slash(req.resolve("./fixtures/value.cjs")).endsWith("/fixtures/value.cjs"), + ); +} + +for ( + const value of ["./relative.js", "https://example.test/a.js", {}, null, 1] +) { + try { + createRequire(value as any); + console.log(Object.prototype.toString.call(value), "no throw"); + } catch (error) { + console.log( + Object.prototype.toString.call(value), + (error as any).name, + (error as any).code ?? "no-code", + ); + } +} diff --git a/test-parity/node-suite/module/require/cycles.ts b/test-parity/node-suite/module/require/cycles.ts new file mode 100644 index 0000000000..22dad5846e --- /dev/null +++ b/test-parity/node-suite/module/require/cycles.ts @@ -0,0 +1,31 @@ +import { createRequire } from "node:module"; + +const req = createRequire(import.meta.url); +const aPath = req.resolve("./fixtures/cycle-a.cjs"); +const bPath = req.resolve("./fixtures/cycle-b.cjs"); +delete req.cache[aPath]; +delete req.cache[bPath]; +const a = req("./fixtures/cycle-a.cjs"); +const b = req("./fixtures/cycle-b.cjs"); +console.log( + "exports:", + a.name, + a.sawB, + a.bSawAReady, + a.ready, + b.name, + b.sawA, + b.sawAReady, +); +console.log( + "cache loaded:", + req.cache[aPath]!.loaded, + req.cache[bPath]!.loaded, +); +console.log( + "cycle children:", + req.cache[aPath]!.children[0] === req.cache[bPath], + req.cache[bPath]!.children[0] === req.cache[aPath], +); +delete req.cache[aPath]; +delete req.cache[bPath]; diff --git a/test-parity/node-suite/module/require/error-fields.ts b/test-parity/node-suite/module/require/error-fields.ts new file mode 100644 index 0000000000..c67ddf53e8 --- /dev/null +++ b/test-parity/node-suite/module/require/error-fields.ts @@ -0,0 +1,26 @@ +import { createRequire } from "node:module"; + +const req = createRequire(import.meta.url); +for ( + const [label, action] of [ + ["missing local", () => req("./fixtures/does-not-exist.cjs")], + ["missing package", () => req("parity-definitely-missing")], + ["missing export", () => req("parity-exports/private.cjs")], + ] as const +) { + try { + action(); + console.log(label, "no throw"); + } catch (error) { + const value = error as any; + console.log( + label, + value.name, + value.code, + Array.isArray(value.requireStack), + value.requireStack?.length ?? -1, + typeof value.path, + typeof value.request, + ); + } +} diff --git a/test-parity/node-suite/module/require/exports-alias.ts b/test-parity/node-suite/module/require/exports-alias.ts new file mode 100644 index 0000000000..0ce360f431 --- /dev/null +++ b/test-parity/node-suite/module/require/exports-alias.ts @@ -0,0 +1,7 @@ +import { createRequire } from "node:module"; + +const req = createRequire(import.meta.url); +const value = req("./fixtures/exports-alias.cjs"); +console.log("keys:", Object.keys(value).sort().join(",")); +console.log("replacement:", value.replacement, value.before, value.after); +console.log("self reference:", value.final === value); diff --git a/test-parity/node-suite/module/require/extensions.ts b/test-parity/node-suite/module/require/extensions.ts new file mode 100644 index 0000000000..4e02a31d4e --- /dev/null +++ b/test-parity/node-suite/module/require/extensions.ts @@ -0,0 +1,26 @@ +import { createRequire, Module } from "node:module"; + +const req = createRequire(import.meta.url); +console.log("shared:", req.extensions === Module._extensions); +console.log("keys:", Object.keys(req.extensions).sort().join(",")); +const customPath = req.resolve("./fixtures/custom.ext"); +delete req.cache[customPath]; +const previous = req.extensions[".ext"]; +let calls = 0; +req.extensions[".ext"] = (module, filename) => { + calls++; + req.extensions[".js"]!(module, filename); +}; +try { + console.log( + "custom:", + req("./fixtures/custom.ext").extension, + calls, + req.cache[customPath]!.loaded, + ); +} finally { + delete req.cache[customPath]; + if (previous) req.extensions[".ext"] = previous; + else delete req.extensions[".ext"]; +} +console.log("restored:", req.extensions[".ext"] === previous); diff --git a/test-parity/node-suite/module/require/fixtures/child.cjs b/test-parity/node-suite/module/require/fixtures/child.cjs new file mode 100644 index 0000000000..7a5e09d6fa --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/child.cjs @@ -0,0 +1 @@ +module.exports = { child: true, parentId: module.parent && module.parent.id }; diff --git a/test-parity/node-suite/module/require/fixtures/custom.ext b/test-parity/node-suite/module/require/fixtures/custom.ext new file mode 100644 index 0000000000..8553889b1b --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/custom.ext @@ -0,0 +1 @@ +module.exports = { extension: "custom" }; diff --git a/test-parity/node-suite/module/require/fixtures/cycle-a.cjs b/test-parity/node-suite/module/require/fixtures/cycle-a.cjs new file mode 100644 index 0000000000..92715e4f1e --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/cycle-a.cjs @@ -0,0 +1,6 @@ +exports.name = "a"; +exports.before = true; +const b = require("./cycle-b.cjs"); +exports.sawB = b.name; +exports.bSawAReady = b.sawAReady; +exports.ready = true; diff --git a/test-parity/node-suite/module/require/fixtures/cycle-b.cjs b/test-parity/node-suite/module/require/fixtures/cycle-b.cjs new file mode 100644 index 0000000000..08760d1830 --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/cycle-b.cjs @@ -0,0 +1,4 @@ +exports.name = "b"; +const a = require("./cycle-a.cjs"); +exports.sawA = a.name; +exports.sawAReady = a.ready; diff --git a/test-parity/node-suite/module/require/fixtures/data.json b/test-parity/node-suite/module/require/fixtures/data.json new file mode 100644 index 0000000000..4745dc8fc4 --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/data.json @@ -0,0 +1 @@ +{ "name": "parity", "count": 3, "nested": { "ok": true } } diff --git a/test-parity/node-suite/module/require/fixtures/exports-alias.cjs b/test-parity/node-suite/module/require/fixtures/exports-alias.cjs new file mode 100644 index 0000000000..07e7a341ca --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/exports-alias.cjs @@ -0,0 +1,4 @@ +exports.before = "discarded"; +module.exports = { replacement: true }; +exports.after = "also-discarded"; +module.exports.final = module.exports; diff --git a/test-parity/node-suite/module/require/fixtures/find-package/nested/probe.cjs b/test-parity/node-suite/module/require/fixtures/find-package/nested/probe.cjs new file mode 100644 index 0000000000..bd816eaba4 --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/find-package/nested/probe.cjs @@ -0,0 +1 @@ +module.exports = 1; diff --git a/test-parity/node-suite/module/require/fixtures/find-package/package.json b/test-parity/node-suite/module/require/fixtures/find-package/package.json new file mode 100644 index 0000000000..f5f42c4884 --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/find-package/package.json @@ -0,0 +1 @@ +{ "name": "find-package-fixture", "type": "commonjs" } diff --git a/test-parity/node-suite/module/require/fixtures/metadata.cjs b/test-parity/node-suite/module/require/fixtures/metadata.cjs new file mode 100644 index 0000000000..984c05ab4d --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/metadata.cjs @@ -0,0 +1,7 @@ +exports.id = module.id; +exports.filename = module.filename; +exports.path = module.path; +exports.loadedDuringEvaluation = module.loaded; +exports.paths = module.paths; +exports.requireIdentity = module.require === require; +exports.parentId = module.parent && module.parent.id; diff --git a/test-parity/node-suite/module/require/fixtures/parent.cjs b/test-parity/node-suite/module/require/fixtures/parent.cjs new file mode 100644 index 0000000000..42d3b9cf16 --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/parent.cjs @@ -0,0 +1,3 @@ +exports.loadedBefore = module.loaded; +exports.child = require("./child.cjs"); +exports.children = module.children.map((child) => child.filename); diff --git a/test-parity/node-suite/module/require/fixtures/pkg-main/lib/entry.cjs b/test-parity/node-suite/module/require/fixtures/pkg-main/lib/entry.cjs new file mode 100644 index 0000000000..09060854ac --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/pkg-main/lib/entry.cjs @@ -0,0 +1 @@ +module.exports = { kind: "main", filename: __filename }; diff --git a/test-parity/node-suite/module/require/fixtures/pkg-main/package.json b/test-parity/node-suite/module/require/fixtures/pkg-main/package.json new file mode 100644 index 0000000000..9f0cfe4e99 --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/pkg-main/package.json @@ -0,0 +1 @@ +{ "name": "pkg-main", "main": "lib/entry.cjs" } diff --git a/test-parity/node-suite/module/require/fixtures/value.cjs b/test-parity/node-suite/module/require/fixtures/value.cjs new file mode 100644 index 0000000000..454ad6e701 --- /dev/null +++ b/test-parity/node-suite/module/require/fixtures/value.cjs @@ -0,0 +1,2 @@ +globalThis.__moduleValueLoads = (globalThis.__moduleValueLoads || 0) + 1; +module.exports = { loads: globalThis.__moduleValueLoads }; diff --git a/test-parity/node-suite/module/require/function-descriptors.ts b/test-parity/node-suite/module/require/function-descriptors.ts new file mode 100644 index 0000000000..6ed898899d --- /dev/null +++ b/test-parity/node-suite/module/require/function-descriptors.ts @@ -0,0 +1,32 @@ +import { createRequire } from "node:module"; + +const req = createRequire(import.meta.url); +for ( + const [label, object] of [["require", req], ["resolve", req.resolve]] as const +) { + console.log( + label, + object.name, + object.length, + Object.getOwnPropertyNames(object).sort().join(","), + ); + for (const key of Object.getOwnPropertyNames(object).sort()) { + if (["arguments", "caller", "prototype"].includes(key)) continue; + const descriptor = Object.getOwnPropertyDescriptor(object, key)!; + console.log( + label, + key, + JSON.stringify({ + enumerable: descriptor.enumerable, + writable: descriptor.writable, + configurable: descriptor.configurable, + type: typeof descriptor.value, + }), + ); + } +} +console.log( + "shared globals:", + req.cache === createRequire(new URL("./other.cjs", import.meta.url)).cache, + req.extensions === createRequire(import.meta.url).extensions, +); diff --git a/test-parity/node-suite/module/require/json-cache.ts b/test-parity/node-suite/module/require/json-cache.ts new file mode 100644 index 0000000000..b64940565e --- /dev/null +++ b/test-parity/node-suite/module/require/json-cache.ts @@ -0,0 +1,19 @@ +import { createRequire } from "node:module"; + +const req = createRequire(import.meta.url); +const resolved = req.resolve("./fixtures/data.json"); +delete req.cache[resolved]; +const first = req("./fixtures/data.json"); +const second = req("./fixtures/data.json"); +console.log("shape:", first.name, first.count, first.nested.ok); +console.log( + "identity/cache:", + first === second, + req.cache[resolved]!.exports === first, + req.cache[resolved]!.loaded, +); +first.count = 9; +console.log("mutation visible:", req("./fixtures/data.json").count); +delete req.cache[resolved]; +console.log("delete reparses:", req("./fixtures/data.json").count); +delete req.cache[resolved]; diff --git a/test-parity/node-suite/module/require/module-load.ts b/test-parity/node-suite/module/require/module-load.ts new file mode 100644 index 0000000000..e880ac7c42 --- /dev/null +++ b/test-parity/node-suite/module/require/module-load.ts @@ -0,0 +1,35 @@ +import { createRequire, Module } from "node:module"; +import { fileURLToPath } from "node:url"; + +const req = createRequire(import.meta.url); +const filename = req.resolve("./fixtures/metadata.cjs"); +const instance = new Module("manual-id"); +instance.filename = filename; +instance.paths = Module._nodeModulePaths( + fileURLToPath(new URL("./fixtures/", import.meta.url)), +); +console.log( + "before:", + instance.loaded, + Object.keys(instance.exports).length, + instance.children.length, +); +console.log("return:", String(instance.load(filename))); +console.log( + "after:", + instance.loaded, + instance.id, + instance.filename === filename, + (instance.exports as any).loadedDuringEvaluation, +); +console.log("cache independent:", req.cache[filename] === undefined); +try { + instance.load(filename); + console.log("second load: no throw"); +} catch (error) { + console.log( + "second load:", + (error as any).name, + (error as any).code ?? "no-code", + ); +} diff --git a/test-parity/node-suite/module/require/module-metadata.ts b/test-parity/node-suite/module/require/module-metadata.ts new file mode 100644 index 0000000000..183331e355 --- /dev/null +++ b/test-parity/node-suite/module/require/module-metadata.ts @@ -0,0 +1,28 @@ +import { createRequire } from "node:module"; + +const req = createRequire(import.meta.url); +const slash = (value: string) => value.replaceAll("\\", "/"); +const resolved = req.resolve("./fixtures/metadata.cjs"); +delete req.cache[resolved]; +const value = req("./fixtures/metadata.cjs"); +const record = req.cache[resolved]!; +console.log( + "identity:", + value.id === resolved, + value.filename === resolved, + record.id === resolved, + record.filename === resolved, +); +console.log( + "directory/path:", + slash(value.path).endsWith("/require/fixtures"), + slash(value.paths[0]).endsWith("/require/fixtures/node_modules"), +); +console.log("evaluation/loaded:", value.loadedDuringEvaluation, record.loaded); +console.log( + "require/parent:", + value.requireIdentity, + typeof value.parentId, + record.parent === undefined || typeof record.parent.id === "string", +); +delete req.cache[resolved]; diff --git a/test-parity/node-suite/module/require/node_modules/parity-exports/feature.cjs b/test-parity/node-suite/module/require/node_modules/parity-exports/feature.cjs new file mode 100644 index 0000000000..81c274adce --- /dev/null +++ b/test-parity/node-suite/module/require/node_modules/parity-exports/feature.cjs @@ -0,0 +1 @@ +module.exports = { feature: 42 }; diff --git a/test-parity/node-suite/module/require/node_modules/parity-exports/internal.cjs b/test-parity/node-suite/module/require/node_modules/parity-exports/internal.cjs new file mode 100644 index 0000000000..0a713b26f7 --- /dev/null +++ b/test-parity/node-suite/module/require/node_modules/parity-exports/internal.cjs @@ -0,0 +1 @@ +module.exports = "internal-import"; diff --git a/test-parity/node-suite/module/require/node_modules/parity-exports/legacy.cjs b/test-parity/node-suite/module/require/node_modules/parity-exports/legacy.cjs new file mode 100644 index 0000000000..90339a2673 --- /dev/null +++ b/test-parity/node-suite/module/require/node_modules/parity-exports/legacy.cjs @@ -0,0 +1 @@ +module.exports = { entry: "legacy" }; diff --git a/test-parity/node-suite/module/require/node_modules/parity-exports/package.json b/test-parity/node-suite/module/require/node_modules/parity-exports/package.json new file mode 100644 index 0000000000..047e1f2e37 --- /dev/null +++ b/test-parity/node-suite/module/require/node_modules/parity-exports/package.json @@ -0,0 +1,12 @@ +{ + "name": "parity-exports", + "type": "commonjs", + "main": "./legacy.cjs", + "exports": { + ".": "./root.cjs", + "./feature": "./feature.cjs" + }, + "imports": { + "#internal": "./internal.cjs" + } +} diff --git a/test-parity/node-suite/module/require/node_modules/parity-exports/root.cjs b/test-parity/node-suite/module/require/node_modules/parity-exports/root.cjs new file mode 100644 index 0000000000..7af19e7de3 --- /dev/null +++ b/test-parity/node-suite/module/require/node_modules/parity-exports/root.cjs @@ -0,0 +1 @@ +module.exports = { entry: "exports", internal: require("#internal") }; diff --git a/test-parity/node-suite/module/require/package-boundaries.ts b/test-parity/node-suite/module/require/package-boundaries.ts new file mode 100644 index 0000000000..d0706f3351 --- /dev/null +++ b/test-parity/node-suite/module/require/package-boundaries.ts @@ -0,0 +1,30 @@ +import { createRequire } from "node:module"; + +const req = createRequire(import.meta.url); +const slash = (value: string) => value.replaceAll("\\", "/"); +console.log( + "main:", + req("./fixtures/pkg-main").kind, + slash(req.resolve("./fixtures/pkg-main")).endsWith("/pkg-main/lib/entry.cjs"), +); +const root = req("parity-exports"); +console.log("exports root:", root.entry, root.internal); +console.log("exports feature:", req("parity-exports/feature").feature); +for ( + const specifier of [ + "parity-exports/legacy.cjs", + "parity-exports/package.json", + "parity-exports/missing", + ] +) { + try { + req(specifier); + console.log(specifier, "no throw"); + } catch (error) { + console.log( + specifier, + (error as any).name, + (error as any).code ?? "no-code", + ); + } +} diff --git a/test-parity/node-suite/module/require/parent-children.ts b/test-parity/node-suite/module/require/parent-children.ts new file mode 100644 index 0000000000..887562f404 --- /dev/null +++ b/test-parity/node-suite/module/require/parent-children.ts @@ -0,0 +1,31 @@ +import { createRequire } from "node:module"; + +const req = createRequire(import.meta.url); +const parentPath = req.resolve("./fixtures/parent.cjs"); +const childPath = req.resolve("./fixtures/child.cjs"); +delete req.cache[parentPath]; +delete req.cache[childPath]; +const parent = req("./fixtures/parent.cjs"); +const parentRecord = req.cache[parentPath]!; +const childRecord = req.cache[childPath]!; +console.log( + "loaded states:", + parent.loadedBefore, + parentRecord.loaded, + childRecord.loaded, +); +console.log( + "children:", + parent.children.length, + parent.children[0] === childPath, + parentRecord.children[0] === childRecord, +); +console.log( + "parent:", + childRecord.parent === parentRecord, + parent.child.parentId === parentPath, +); +req("./fixtures/child.cjs"); +console.log("no duplicate child:", parentRecord.children.length); +delete req.cache[parentPath]; +delete req.cache[childPath]; diff --git a/test-parity/node-suite/module/require/resolve-paths.ts b/test-parity/node-suite/module/require/resolve-paths.ts new file mode 100644 index 0000000000..23deb1e73f --- /dev/null +++ b/test-parity/node-suite/module/require/resolve-paths.ts @@ -0,0 +1,48 @@ +import { createRequire } from "node:module"; + +const req = createRequire(import.meta.url); +const slash = (value: string) => value.replaceAll("\\", "/"); +const normalize = (value: string) => + slash(value.replace(process.cwd(), "")); +for ( + const specifier of [ + "fs", + "node:fs", + "./fixtures/value", + "./fixtures/value.cjs", + "./fixtures/pkg-main", + "parity-exports", + "parity-exports/feature", + ] +) { + try { + console.log("resolve", specifier, normalize(req.resolve(specifier))); + } catch (error) { + console.log( + "resolve", + specifier, + (error as any).name, + (error as any).code ?? "no-code", + ); + } +} +for ( + const specifier of [ + "fs", + "node:fs", + "./fixtures/value.cjs", + "parity-exports", + "missing-parity-package", + ] +) { + const paths = req.resolve.paths(specifier); + console.log( + "paths", + specifier, + paths === null + ? "null" + : `${Array.isArray(paths)}:${ + paths!.some((path) => slash(path).endsWith("/require/node_modules")) + }`, + ); +} diff --git a/test-parity/node-suite/module/source-map/constructor-payload.ts b/test-parity/node-suite/module/source-map/constructor-payload.ts new file mode 100644 index 0000000000..bd1ea92975 --- /dev/null +++ b/test-parity/node-suite/module/source-map/constructor-payload.ts @@ -0,0 +1,38 @@ +import { SourceMap } from "node:module"; + +const payload = { + version: 3, + file: "out.js", + sourceRoot: "../src/", + sources: ["input.ts"], + sourcesContent: ["export const value = 1;"], + names: ["value"], + mappings: "AAAAA", +}; +const lengths = [12, 0, 7]; +const map = new SourceMap(payload, { lineLengths: lengths }); +console.log("instance:", map instanceof SourceMap); +console.log( + "payload equal/not same:", + JSON.stringify(map.payload) === JSON.stringify(payload), + map.payload !== payload, +); +console.log( + "nested cloned:", + map.payload.sources !== payload.sources, + map.payload.names !== payload.names, + map.payload.sourcesContent !== payload.sourcesContent, +); +console.log( + "line lengths:", + JSON.stringify(map.lineLengths), + map.lineLengths !== lengths, +); +payload.sources[0] = "mutated.ts"; +lengths[0] = 999; +console.log( + "payload isolated/lengths shared:", + map.payload.sources[0], + map.lineLengths[0], +); +console.log("entry:", JSON.stringify(map.findEntry(0, 0))); diff --git a/test-parity/node-suite/module/source-map/descriptors-validation.ts b/test-parity/node-suite/module/source-map/descriptors-validation.ts new file mode 100644 index 0000000000..d3701c8e8c --- /dev/null +++ b/test-parity/node-suite/module/source-map/descriptors-validation.ts @@ -0,0 +1,34 @@ +import { SourceMap } from "node:module"; + +console.log( + "shape:", + SourceMap.name, + SourceMap.length, + Object.getOwnPropertyNames(SourceMap.prototype).sort().join(","), +); +for (const key of Object.getOwnPropertyNames(SourceMap.prototype).sort()) { + const descriptor = Object.getOwnPropertyDescriptor(SourceMap.prototype, key)!; + console.log( + key, + JSON.stringify({ + enumerable: descriptor.enumerable, + configurable: descriptor.configurable, + writable: descriptor.writable, + get: typeof descriptor.get, + value: typeof descriptor.value, + }), + ); +} + +for (const value of [undefined, null, 1, true, "map", []]) { + try { + new SourceMap(value as any); + console.log(typeof value, "no throw"); + } catch (error) { + console.log( + typeof value, + (error as any).name, + (error as any).code ?? "no-code", + ); + } +} diff --git a/test-parity/node-suite/module/source-map/find-inline.ts b/test-parity/node-suite/module/source-map/find-inline.ts new file mode 100644 index 0000000000..21ac58c8c7 --- /dev/null +++ b/test-parity/node-suite/module/source-map/find-inline.ts @@ -0,0 +1,34 @@ +import { + createRequire, + findSourceMap, + getSourceMapsSupport, + setSourceMapsSupport, +} from "node:module"; + +const req = createRequire(import.meta.url); +const filename = req.resolve("./fixtures/inline-map.cjs"); +const previous = getSourceMapsSupport(); +const normalize = (value: unknown) => + JSON.stringify(value).replaceAll(process.cwd(), ""); +try { + setSourceMapsSupport(true, { nodeModules: true, generatedCode: true }); + req("./fixtures/inline-map.cjs"); + const map = findSourceMap(filename); + console.log("found:", map !== undefined); + console.log( + "payload:", + map!.payload.version, + map!.payload.file, + normalize(map!.payload.sources[0]), + map!.payload.names[0], + ); + console.log("entry:", normalize(map!.findEntry(0, 0))); + console.log("same lookup:", findSourceMap(filename) === map); + console.log("missing:", String(findSourceMap(filename + ".missing"))); +} finally { + setSourceMapsSupport(previous.enabled, { + nodeModules: previous.nodeModules, + generatedCode: previous.generatedCode, + }); + delete req.cache[filename]; +} diff --git a/test-parity/node-suite/module/source-map/fixtures/inline-map.cjs b/test-parity/node-suite/module/source-map/fixtures/inline-map.cjs new file mode 100644 index 0000000000..1332f12063 --- /dev/null +++ b/test-parity/node-suite/module/source-map/fixtures/inline-map.cjs @@ -0,0 +1,2 @@ +exports.answer = 42; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5saW5lLW1hcC5janMiLCJzb3VyY2VzIjpbImlubGluZS1vcmlnaW5hbC50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgY29uc3QgYW5zd2VyID0gNDI7Il0sIm5hbWVzIjpbImFuc3dlciJdLCJtYXBwaW5ncyI6IkFBQUFBIiwic291cmNlUm9vdCI6Ii4vIn0= diff --git a/test-parity/node-suite/module/source-map/getter-semantics.ts b/test-parity/node-suite/module/source-map/getter-semantics.ts new file mode 100644 index 0000000000..eb434cee6c --- /dev/null +++ b/test-parity/node-suite/module/source-map/getter-semantics.ts @@ -0,0 +1,25 @@ +import { SourceMap } from "node:module"; + +const payload = { version: 3, sources: ["a.ts"], names: [], mappings: "AAAA" }; +const lengths = [4, 8]; +const map = new SourceMap(payload, { lineLengths: lengths }); +const firstPayload = map.payload; +const secondPayload = map.payload; +const firstLengths = map.lineLengths; +const secondLengths = map.lineLengths; +console.log( + "getter identity:", + firstPayload === secondPayload, + firstLengths === secondLengths, +); +firstPayload.sources[0] = "changed.ts"; +firstLengths[0] = 99; +console.log("mutation visibility:", map.payload.sources[0], map.lineLengths[0]); +console.log( + "input links:", + map.payload === payload, + map.lineLengths === lengths, +); + +const defaults = new SourceMap(payload); +console.log("default lengths:", String(defaults.lineLengths)); diff --git a/test-parity/node-suite/module/source-map/lookup-boundaries.ts b/test-parity/node-suite/module/source-map/lookup-boundaries.ts new file mode 100644 index 0000000000..c87184a454 --- /dev/null +++ b/test-parity/node-suite/module/source-map/lookup-boundaries.ts @@ -0,0 +1,33 @@ +import { SourceMap } from "node:module"; + +const map = new SourceMap({ + version: 3, + sources: ["a.ts", "b.ts"], + names: ["first", "second"], + mappings: "AAAAA,KAAKC;ACCL", +}); + +for ( + const [line, column] of [[0, -1], [0, 0], [0, 4], [0, 5], [0, 100], [1, 0], [ + 2, + 0, + ]] +) { + console.log( + `entry ${line},${column}:`, + JSON.stringify(map.findEntry(line, column)), + ); +} +for ( + const args of [[1, 1], ["a.ts", 1, 1], ["b.ts", 2, 1], [ + "missing.ts", + 1, + 1, + ]] as any[] +) { + console.log( + "origin", + JSON.stringify(args), + JSON.stringify(map.findOrigin(...args)), + ); +} diff --git a/test-parity/node-suite/module/source-map/malformed-indexed.ts b/test-parity/node-suite/module/source-map/malformed-indexed.ts new file mode 100644 index 0000000000..ede6a907e7 --- /dev/null +++ b/test-parity/node-suite/module/source-map/malformed-indexed.ts @@ -0,0 +1,31 @@ +import { SourceMap } from "node:module"; + +for (const mappings of ["", ";;;;;", "!", "AAAA,", ",AAAA"]) { + const map = new SourceMap({ + version: 3, + sources: ["a.ts"], + names: [], + mappings, + }); + console.log( + JSON.stringify(mappings), + JSON.stringify(map.findEntry(0, 5)), + JSON.stringify(map.findOrigin(1, 6)), + ); +} + +const indexed = new SourceMap({ + version: 3, + sections: [ + { + offset: { line: 0, column: 0 }, + map: { version: 3, sources: ["a.ts"], names: [], mappings: "AAAA" }, + }, + { + offset: { line: 2, column: 3 }, + map: { version: 3, sources: ["b.ts"], names: [], mappings: "AAAA" }, + }, + ], +}); +console.log("indexed first:", JSON.stringify(indexed.findEntry(0, 0))); +console.log("indexed second:", JSON.stringify(indexed.findEntry(2, 3))); diff --git a/test-parity/node-suite/module/source-map/receiver-validation.ts b/test-parity/node-suite/module/source-map/receiver-validation.ts new file mode 100644 index 0000000000..dd084c23bc --- /dev/null +++ b/test-parity/node-suite/module/source-map/receiver-validation.ts @@ -0,0 +1,54 @@ +import { SourceMap } from "node:module"; + +const map = new SourceMap({ + version: 3, + sources: ["a.ts"], + names: [], + mappings: "AAAA", +}); +const payloadGetter = Object.getOwnPropertyDescriptor( + SourceMap.prototype, + "payload", +)!.get!; +const lengthsGetter = Object.getOwnPropertyDescriptor( + SourceMap.prototype, + "lineLengths", +)!.get!; +for ( + const [label, action] of [ + ["payload", () => payloadGetter.call({})], + ["lengths", () => lengthsGetter.call(null)], + ["entry", () => map.findEntry.call({}, 0, 0)], + ["origin", () => map.findOrigin.call({}, 1, 1)], + ] as const +) { + try { + action(); + console.log(label, "no throw"); + } catch (error) { + console.log(label, (error as any).name, (error as any).code ?? "no-code"); + } +} +for ( + const [label, args] of [ + ["undefined", [undefined, undefined]], + ["strings", ["0", "0"]], + ["non-finite", [NaN, Infinity]], + ["negative", [-1, 0]], + ] as any[] +) { + try { + console.log( + "entry args", + label, + JSON.stringify(map.findEntry(...args)), + ); + } catch (error) { + console.log( + "entry args", + label, + (error as any).name, + (error as any).code ?? "no-code", + ); + } +} diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index 56a4fa4e88..831c26914d 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -98,8 +98,8 @@ "total": 30 }, "module": { - "pass": 28, - "total": 28 + "pass": 29, + "total": 69 }, "net": { "pass": 12,