| name | node-module-contracts |
| description | Generate a new Node.js module interface contract or update an existing one from pinned official Node.js 24 LTS documentation, including intrinsic adapters, manual validation, and support docs. |
| tier | standard |
| applyTo | scripts/nodeContracts/**,src/JavaScriptRuntime/Node/**,src/JavaScriptRuntime/IJavaScript*.cs,tests/Jroc.NodeContracts.Tests/**,tests/Jroc.Tests/Node/**,docs/nodejs/**,package.json,CHANGELOG.md |
Node Module Contracts
Use this skill when adding a generated contract for a Node module that does not
have one yet, or when changing an existing generated Node module contract.
When contract work is triggered by adding a new Node runtime module or
implementing a public member on an existing module, also follow the
node-module-implementation skill. Runtime implementation and generated
contract follow-through belong in the same change.
Goals
- Generate the complete public module surface from the official Node.js 24 LTS
documentation.
- Keep the public contract independent from JROC's current implementation
coverage.
- Bind implemented intrinsic members directly at compile time.
- Make unavailable intrinsic members fail explicitly with
NotImplementedException.
- Keep generation deterministic, reviewable, and manually validated.
Non-Negotiable Rules
- The source of truth is the official public Node.js 24 LTS documentation.
Use the machine-readable JSON published by Node.js for an exact pinned
v24.x.y release.
- Never use
docs/nodejs as contract-generation input. Those files describe
JROC support and may intentionally be incomplete.
- Do not derive the public contract from JROC runtime classes. Runtime classes
describe current implementation, not the complete Node API.
- Do not manually edit
*.Generated.cs files.
- Do not omit a public Node member because JROC does not implement it.
- Generated contracts expose public ABI abstractions, not concrete runtime
implementations:
- arrays use
IJavaScriptArray, not JavaScriptRuntime.Array;
- promises use
IJavaScriptPromise, not JavaScriptRuntime.Promise;
- generated Node classes and stable shapes use public contract interfaces
when that support exists;
- host/CLR implementation types must not leak into public contracts.
- Generated intrinsic adapters use statically emitted calls. Do not introduce
reflection, late-bound method discovery, or a generic invocation helper.
- Contract generation and contract tests are manual. Do not add them to the
normal solution or every-run CI unless repository policy explicitly changes.
- A shared generator change changes its SHA-256 provenance and normally requires
regenerating every contract produced by that generator.
Current Layout
| Purpose | Location |
|---|
| Shared generator | scripts/nodeContracts/generateNodeModuleInterface.js |
| Pin and drift locks | scripts/nodeContracts/*.node24.lock.json |
| Parsing/runtime implementation overrides | scripts/nodeContracts/*.node24.overrides.json |
| Generated interfaces | src/JavaScriptRuntime/Node/Contracts/I*Module.Generated.cs |
| Generated intrinsic adapters | src/JavaScriptRuntime/Node/*.I*Module.Generated.cs |
| Runtime intrinsic implementations | src/JavaScriptRuntime/Node/*.cs |
| Manual contract tests | tests/Jroc.NodeContracts.Tests |
| Manual workflow documentation | scripts/nodeContracts/README.md |
| JROC support documentation | docs/nodejs |
The current generator manifest has explicit modes for:
fs;
fs/promises;
console;
path;
child_process;
perf_hooks;
process;
buffer;
events;
os;
stream;
stream/promises;
util;
util/types;
zlib;
string_decoder;
timers;
timers/promises;
url;
querystring;
net;
tls;
http;
https;
crypto.
Extend the shared generator for another module. Do not copy it into a
module-specific generator.
Decide Which Workflow Applies
Generate a new module contract
Use the new-contract workflow when no attributed interface exists in
src/JavaScriptRuntime/Node/Contracts.
Update an existing module contract
Use the update workflow when changing any of:
- the pinned Node.js patch version or source document;
- the upstream public API surface;
- JavaScript-to-.NET type mappings;
- optional/rest/overload normalization;
- generated member metadata;
- intrinsic implementation mappings;
- generated adapter invocation rules;
- the shared generator itself.
An intrinsic implementation change that does not affect the contract may only
need an override-map, adapter, runtime, and test update. Still run the contract
check for that module.
Phase 1: Establish the Official Input
- Determine the canonical module specifier without the
node: prefix, for
example path or fs/promises.
- Identify the official Node documentation JSON containing that module:
https://nodejs.org/docs/v24.18.1/api/<document>.json for the current pin.
A submodule can live in its parent document; fs/promises is in fs.json.
- Use the same exact Node 24 LTS patch release as the other contracts unless
the task explicitly advances the repository-wide pin.
- Download the JSON and calculate its SHA-256.
- Inspect its module/class/section structure and count the public members the
generator will consume.
- Record the exact version, URL, hash, and drift-detection counts in a lock
file.
Example:
mkdir -p artifacts/nodeContracts
curl -fsSLo artifacts/nodeContracts/path.node24.json \
https://nodejs.org/docs/v24.18.1/api/path.json
sha256sum artifacts/nodeContracts/path.node24.json
The generator accepts --input <file> for reproducible local generation, but
the input must match the checked-in lock hash. artifacts/ is gitignored.
Do not change a hash or expected count merely to silence a failure. First
inspect the upstream API change and decide how it affects the normalized
contract.
For faster iteration, download each official document once and reuse it with
--input across every contract sourced from that document. Stabilize new
module-specific generation and tests before running the aggregate generator;
because the generator hash changes every generated contract, regenerating all
contracts after each exploratory edit creates avoidable churn.
For larger batches, inventory every document with one cached download and one
topology report, then group modules by normalized-api, documented-api, or
an existing specialized shape. A future scaffolder should produce manifest,
lock, override, package-script, and test skeletons from that inventory while
leaving public-roster and semantic decisions for review.
Top-level exports are not always stored directly on the documentation module.
For example, child_process stores its seven exported functions in nested
asynchronous and synchronous sections. Inspect the full JSON section tree and
extract the documented public sections explicitly; do not assume
module.methods is the complete surface.
buffer.json, for example, stores its module functions and legacy constants
under the nested `node:buffer`_module_apis section while constructor
exports are documented as classes elsewhere in the document. Drift-check both
the containing document and selected API section, then add only the missing
top-level constructor/object exports as cited overrides.
The JSON module name can also differ from the canonical specifier.
perf_hooks.json, for example, names its root module
performance_measurement_apis. Record the exact JSON name in the lock instead
of assuming it matches the require specifier.
Structured JSON can omit contract-critical metadata or an explicit export
roster. Use the surrounding official API narrative first. When a pinned
official Node source file is needed to disambiguate which documented classes
are actually exported, record only those missing exports as cited overrides;
do not derive method semantics or replace the documentation contract with the
runtime implementation. A missing structured return type, such as
perf_hooks.timerify(), also requires a narrow cited override rather than
silently mapping the return to void.
Likewise, when prose and examples document an omitted argument but the
structured signature lacks optional brackets, record the exact optional
parameter as a cited override rather than treating every defaulted parameter as
optional globally.
Some API documents describe a module-shaped global rather than placing the
contract under modules. process.json, for example, stores the public
Process object under globals even though the same object is exported by
node:process. Select the documented root by its actual JSON category and
retain the canonical module specifier in [NodeModuleInterface].
Some CommonJS modules export a callable constructor carrying static members.
events assigns EventEmitter itself to module.exports; its methods,
properties, and EventEmitterAsyncResource constructor therefore form the
module's top-level named contract even though their documentation is split
between root methods, malformed root properties, and class records. Confirm
the export roster from the pinned Node source and do not include narrative
Web API classes that are not assigned to the module export.
Promise submodules can be documented inside a narrative section whose method
records use the parent module prefix. stream/promises, for example, uses the
three promise-returning stream.pipeline and stream.finished records under
types_of_streams; keep the canonical contract identity
stream/promises while preserving those official signatures.
The stream JSON also hoists nested class statics and even example calls into
the root method array. Select only exact stream.<member> signatures for the
top-level contract, drift-check the excluded records, and obtain
stream.duplexPair from its consumer subsection.
Some submodules are aliases of a documented namespace property rather than a
separate module record. util/types is the 43-method util.types property in
util.json. The runtime object returned for the alias must itself implement
the generated contract; otherwise RequireObject<T> fails even if a separate
attributed intrinsic class exists.
Use cited normalizedMethods overrides for signatures whose structured JSON
is incomplete or whose JavaScript variadic shape cannot be represented by the
ordinary overload expander. Keep the selected official member roster and
drift counts in generator code and locks; normalized methods are not a license
to redefine the module around the runtime implementation.
Normalized methods support two compact overload forms:
minimumParameterCount expands trailing optional parameters from one full
parameter list;
overloads represents non-prefix call forms and overload-specific return
types while sharing the official signatures and source citation.
Prefer the shared normalized-api extraction kind when every selected method
needs normalization. Prefer documented-api when a root or nested API section
is mostly structured and only selected methods need normalization, signature
repair, or return overrides.
url.json splits top-level methods between the WHATWG and legacy sections,
and its root has no methods or properties. Select both sections, normalize the
11 records, and cite the pinned lib/url.js export roster for URL,
URLSearchParams, and URLPattern.
querystring.json has six root method records, but all six have incomplete
structured signatures. decode and encode are callable aliases of parse
and stringify; normalize their full call forms and statically target the
existing intrinsic methods rather than adding reflection.
Networking call forms are not reliably represented by one structured record.
net.connect, net.createConnection, and tls.connect need reviewed
overloads for options, IPC paths, ports, hosts, and callbacks. The TLS JSON
root is named tls_(ssl), not tls. Preserve the canonical module identity
while locking the actual JSON root name.
The HTTP and HTTPS JSON renderers duplicate get and request records and can
attach the wrong descriptor list to the options-only record. Normalize the
combined URL/options/callback call forms. Cross-check documented constructor
exports against pinned source; in Node 24.18.1, http.WebSocket is a real
top-level export.
The crypto top-level API lives under the nested
`node:crypto`_module_methods_and_properties section. Its structured JSON
contains malformed parameter records and callback-sensitive methods whose
return type differs between synchronous and callback forms. Drift-check those
malformed records, use narrow signature overrides where one return type
applies, and normalized overloads where return types differ.
Constructor exports remain object? unless the exported value itself has a
documented stable instance contract. For such nested types, add a cited
nestedContracts entry to the module override so the shared generator emits a
[NodeModuleType] interface and static intrinsic adapter; do not substitute a
concrete runtime type in the public ABI.
zlib.json has root methods but omits return metadata for constructors and
convenience methods. Pin all 34 root methods, use cited return overrides for
the 11 stream factories and 11 synchronous Buffer helpers, and cross-check the
documented classes against the pinned lib/zlib.js export roster:
ZlibBase is documented but is not a top-level constructor export. The JSON
documents 39 non-Brotli constants that remain deprecated top-level exports;
extract and drift-check those names from the official constant prose while
keeping constants, codes, and the 11 constructors as cited properties.
Constructor-only modules can have no root methods or properties.
string_decoder.json documents a single StringDecoder class, so the
top-level contract is the cited constructor property. When its instance API is
needed, use the same cited nested-contract configuration and retain its class
method counts as drift detectors.
timers.json splits the six node:timers functions between
scheduling_timers and cancelling_timers. Its timers_promises_api section
contains three top-level functions plus flattened
timersPromises.scheduler.wait/yield records. Exclude those dotted nested
methods, expose one cited scheduler property, and normalize setInterval
because its structured signature has no parameters or return type. Both
contracts should share the same timers lock and downloaded input.
Documentation nesting can also be flattened by the JSON renderer.
process.features.* entries appear in the parent Process.properties array
without their features. prefix. Normalize these back to one top-level
features object and record the excluded nested names and counts as
drift-checked overrides. Do not accidentally expose nested properties as
top-level module members.
Nested method records can likewise appear in a parent method array. Until
nested contracts are supported, exclude dotted methods such as
process.hrtime.bigint() from the top-level interface with drift-checked
counts. A dotted [NodeModuleMember] on the root contract is not an equivalent
representation and can incorrectly bind a nonexistent top-level property.
Phase 2A: Add a New Contract
1. Add module configuration
Add one entry to the contractDefinitions manifest in
generateNodeModuleInterface.js. The configuration must identify:
- canonical module specifier;
- documentation prefix used in signatures;
- interface name;
- intrinsic class name;
- display name such as
node:path;
- generated output stem;
- lock and override stems;
- official documentation module/section used in generated provenance;
- generated C# contract alias.
Use the existing data-driven configuration and shared extraction kinds. Do not
reintroduce parallel mode-selection conditionals or create a second generation
pipeline.
The manifest centralizes command-line validation, contract metadata, aliases,
documentation names, output paths, and stale-file diagnostics. Add a new
extraction branch only when the official JSON shape cannot use an existing
shared extraction kind.
Unknown flags are rejected. Verify that the new command reports the intended
generated paths and provenance before accepting output.
2. Add a lock file
Add <module>.node24.lock.json with:
- canonical documentation module;
- exact Node
24.x.y version;
- immutable official source URL;
- SHA-256 of the raw JSON;
- expected method, property, class, and relevant subsection counts.
Counts are drift detectors. They should correspond to the exact sections read
by the generator.
3. Add a narrow override file
Add <module>.node24.overrides.json.
Use overrides only for:
- documented public members that the structured JSON does not expose in the
section being normalized, with an exact official source citation;
- a narrowly reviewed parsing/type clarification;
- intrinsic implementation metadata.
Do not use overrides to redefine the public contract around JROC's current
method signatures.
intrinsicImplementations maps a JavaScript member to its statically bound
runtime implementation:
{
"intrinsicImplementations": {
"join": { "style": "direct" },
"readFile": { "style": "argument-array" },
"debug": { "style": "direct", "target": "InvokeContractDebug" },
"stat": { "style": "direct", "argumentCount": 1 },
"table": { "style": "direct", "parameterCounts": [1] }
}
}
Supported concepts:
direct: call a known runtime method/property directly;
argument-array: generate an object[] bridge for a legacy intrinsic method
that already accepts one argument array;
target: use a different known CLR member name;
argumentCount: intentionally pass only a supported leading subset;
minimumArgumentCount: append null arguments required by a runtime
overload;
parameterCounts: only selected generated overloads are implemented.
returnsVoid: the statically bound runtime target itself returns void
rather than returning a value that the adapter discards.
getterOnly: bind a documented read/write property directly to a runtime
getter while keeping its unavailable setter as an explicit
NotImplementedException.
Every use must correspond to a concrete compile-time-resolvable runtime member.
Unmapped generated members receive an explicit NotImplementedException
adapter.
4. Normalize the complete public surface
Add module-specific extraction only where the official JSON shape requires it.
Assert the lock counts before rendering.
The normalized output must include all selected public:
- methods and documented call forms;
- properties and access semantics;
- deprecated members;
- experimental members where present in Node 24 LTS;
- JavaScript member names through
[NodeModuleMember].
Generation must fail with an actionable error when a public signature is
missing structured metadata or cannot be mapped safely.
5. Add or prepare the intrinsic class
The generated adapter expects a partial intrinsic class under
JavaScriptRuntime.Node:
[NodeModule("example")]
public sealed partial class Example
{
}
If the runtime module does not exist, add its intrinsic registration and
implementation separately from generated code.
The generated partial class implements the public contract. Implemented
members call existing intrinsic members directly. Unavailable members throw:
The intrinsic node:<module> module does not implement '<prefix><member>'.
Do not add placeholder behavior that looks successful.
6. Add package scripts
Add consistent manual commands:
"generate:node-contract-<name>": "node scripts/nodeContracts/generateNodeModuleInterface.js --<mode>",
"check:node-contract-<name>": "node scripts/nodeContracts/generateNodeModuleInterface.js --<mode> --check",
"test:node-contract-<name>": "dotnet test tests/Jroc.NodeContracts.Tests/Jroc.NodeContracts.Tests.csproj --nologo --filter FullyQualifiedName~<TestClass>"
Document the commands in scripts/nodeContracts/README.md.
7. Add manual contract tests
Add one focused test class under tests/Jroc.NodeContracts.Tests. Cover:
[NodeModuleInterface] canonical identity;
[GeneratedCode] tool and sha256:<64 lowercase hex> version;
- representative required, optional, union, overload, and rest mappings;
[NodeModuleMember] on every public generated member;
- expected distinct method and property counts;
- absence of concrete
Array and Promise ABI types;
- the intrinsic class implements the generated interface;
- representative direct intrinsic delegation;
- no
InvokeContractMember or reflection bridge;
- representative unavailable method/property throws
NotImplementedException with the expected message.
Keep this project outside the regular solution/CI workflow.
Phase 2B: Update an Existing Contract
- Identify why the generated output must change.
- Read the existing lock, override file, generator mode, generated interface,
generated adapter, and manual tests before editing.
- If advancing Node:
- download the new exact
v24.x.y JSON;
- compare the old and new official surfaces;
- update the lock URL, hash, version, and reviewed counts;
- update parsing/type exceptions only when justified by the upstream change.
- If changing type mappings:
- update the shared mapping in the generator;
- preserve ABI interfaces instead of concrete runtime classes;
- regenerate every contract affected by the shared mapping.
- If adding runtime support for an existing public member:
- implement the intrinsic behavior;
- add an
intrinsicImplementations entry with a direct, statically
resolvable invocation;
- regenerate the adapter;
- add runtime and manual contract coverage.
- If removing runtime support, remove the implementation mapping and
regenerate so the adapter throws explicitly. Do not remove the member from
the public contract.
- If the shared generator changes, regenerate and check all generated
contracts because the normalized generator SHA appears in every generated
[GeneratedCode] attribute.
Normative ABI Type Mappings
Use the mappings established by issue #1659:
| Node/JavaScript type | Contract type |
|---|
number, integer, fd, mode, byte count | double |
bigint | System.Numerics.BigInteger |
required boolean | bool |
required string or string-literal union | string |
symbol | JavaScriptRuntime.Symbol |
method result only undefined | void |
value undefined, any, unknown, unconstrained value | object? |
guaranteed JavaScript null | JavaScriptRuntime.JsNull |
| unshaped object/options object | object? |
| array or tuple | IJavaScriptArray |
Promise<T> | IJavaScriptPromise |
| iterator/iterable | IJavaScriptIterator |
| async iterator/iterable | IJavaScriptAsyncIterator |
| function/callback/listener | System.Delegate |
Buffer | JavaScriptRuntime.Node.Buffer |
ArrayBuffer | JavaScriptRuntime.ArrayBuffer |
SharedArrayBuffer | JavaScriptRuntime.SharedArrayBuffer |
DataView | JavaScriptRuntime.DataView |
| JavaScript typed array | corresponding public runtime typed-array type |
Date | JavaScriptRuntime.Date |
Union rules:
- If every union member maps to one CLR type, use that type.
- If union members have incompatible CLR representations, use
object? unless
one generated contract safely represents the union.
- Optional parameters and
T | undefined normally use object? so omission,
undefined, null, coercible values, and invalid values remain distinguishable
to runtime semantics.
- Do not use nullable value types such as
double? merely for optional Node
parameters.
- Rest parameters use
params object?[].
- Callbacks use
Delegate, not invented Action/Func signatures.
Nested stable shapes use the shared #1660 architecture: generated interfaces
carry [NodeModuleType] identity, generated contract hosts retain the original
JavaScript value through IJavaScriptValueHost, and promise/callback/iterator
payloads use NodeModuleResultContractAttribute. Keep heterogeneous JavaScript
parameters as object? and annotate their documented shapes with
NodeModuleParameterContractAttribute; this preserves JavaScript property
semantics without reflection.
Generation and Validation
Generate
Run the module's generation command. If the shared generator changed, run all
contract generators with the aggregate command:
npm run generate:node-contracts
npm run check:node-contracts
The aggregate commands execute every manifest entry. Add and document the
module-specific generate/check/test scripts as well.
Check determinism
Run the corresponding check:* commands after generation. Run generation a
second time and confirm it produces no diff.
The generated header must include:
- exact Node.js version;
- official source URL;
- official document SHA-256.
GeneratedCodeAttribute.Version is the normalized shared generator source
SHA-256, not the Node version. Node provenance remains in the generated header
and lock.
Run manual tests
Run the module-specific test:node-contract-* command. If shared ABI or
generator behavior changed, run the entire manual project:
dotnet test tests/Jroc.NodeContracts.Tests/Jroc.NodeContracts.Tests.csproj --nologo
Run a normal build because generated contracts ship in the runtime/package:
dotnet build --no-restore
If intrinsic runtime behavior changed, also run focused execution/generator
tests under tests/Jroc.Tests/Node/<Module>.
Review generated output
Before committing, inspect the diff and verify:
- complete public Node surface for the selected official sections;
- stable ordering and formatting;
- correct canonical
[NodeModuleInterface];
[NodeModuleMember] preserves exact JavaScript names;
- no concrete runtime
Array/Promise leakage;
- no reflection or runtime method discovery;
- direct calls resolve to the intended overload;
- unsupported members throw explicitly;
- lock counts and hashes changed only for understood reasons;
--check passes;
git diff --check reports no hand-authored whitespace errors.
Generated decompiler/snapshot files may preserve tool-produced trailing
whitespace; do not hand-edit generated output merely to satisfy
git diff --check.
Documentation Follow-Through
Contract generation and JROC support documentation are separate concerns:
- The contract comes from official Node.js 24 LTS docs.
docs/nodejs/<module>.json records what JROC currently implements.
For a new module, add its JSON support document following
docs/nodejs/ModuleDoc.schema.json. For an existing module, update the JSON
only when runtime support changed.
Regenerate documentation:
npm run generate:node-modules
Update CHANGELOG.md when adding a contract, changing public ABI mappings,
advancing the Node pin, or adding meaningful runtime support.
Common Failure Modes
Documentation hash mismatch
The downloaded bytes do not match the lock. Confirm the exact versioned URL.
If intentionally advancing Node, inspect the upstream diff before updating the
lock.
Count mismatch
The official public surface changed or the wrong section is being read. Review
the JSON structure and update normalization plus tests intentionally.
Generated files are stale
Run the generator named by the diagnostic. If the shared generator changed,
regenerate every mode.
Cannot map a type
Do not silently omit the member. Add a public runtime ABI abstraction or a
narrow, cited mapping clarification. Do not expose an intrinsic implementation
class.
Direct adapter call does not compile
The override metadata does not match a concrete intrinsic signature. Adjust
the runtime API or use the existing static bridge options (target,
argument-array, or reviewed argument counts). Do not fall back to reflection.
Runtime member is unavailable
Leave the public contract member generated and omit its intrinsic mapping. The
generated adapter must throw NotImplementedException.
Pull Request Checklist
- Branch from current
master.
- Keep lock, generator/overrides, generated files, tests, runtime changes, and
directly related documentation in one reviewable PR.
- Explain the official Node source/version and whether this is a new contract,
upstream refresh, ABI mapping change, or runtime implementation update.
- Call out generated files and the manual commands run.
- Link #1659 for top-level contract generation and #1660 when nested contracts
are involved.
- Do not claim full runtime support merely because the complete interface was
generated.