feat: add Node.js compatibility matrix (#284)

* feat: add Node.js compatibility matrix

- Add generated Node.js v24 compatibility inventory with static and dynamic support classifications.
- Publish the interactive /compatibility matrix and wire manifest generation into runtime and docs checks.

* fix: correct compatibility matrix generation

* fix: address PR 284 review findings

* fix: address PR 284 review findings

* test: exercise stream web loader through npm graph

* fix: correct dynamic global availability

* fix: keep compatibility module mappings precise
This commit is contained in:
Chris Tate
2026-09-02 16:03:36 -05:00
committed by GitHub
parent c41f1a8be9
commit 8faad2f639
40 changed files with 340274 additions and 226 deletions
+2
View File
@@ -1,4 +1,5 @@
node_modules/
.pnpm-store/
# test-fixture packages are TEST DATA, committed on purpose
!tests/diagnostics/npm-imports/node_modules/
!tests/fixtures/commander-calc/node_modules/
@@ -40,3 +41,4 @@ tmp-*/
.vercel/
.env.local
.env*
.env*.local
+145 -8
View File
@@ -1,6 +1,10 @@
# Agent Guide
Guidance for agents (and humans) working on this repository. These conventions apply repo-wide; the docs site under `docs/` additionally has its own conventions in `docs/AGENTS.md`.
This is the only agent guide for the repository. Its rules apply everywhere, including `docs/` and `internal/compatibility/`.
## Markdown style
Write prose paragraphs and list items as single source lines and let editors and renderers wrap them naturally. Do not hard-wrap Markdown at a fixed column; preserve separate lines only where Markdown syntax or intentional formatting requires them, such as headings, lists, tables, blockquotes, and code blocks.
## Build and test
@@ -41,9 +45,7 @@ SCRIPTC_TEST_WORKERS=4 pnpm test # plain lane
SCRIPTC_TEST_WORKERS=4 SCRIPTC_SAN=1 pnpm test # sanitized lane
```
`SCRIPTC_TEST_WORKERS` caps the vitest worker pool so concurrent agents don't
contend for cores; full local suites also queue behind an advisory lock per
lane.
`SCRIPTC_TEST_WORKERS` caps the vitest worker pool so concurrent agents don't contend for cores; full local suites also queue behind an advisory lock per lane.
Corpus programs are differential tests against Node: every program runs under Node and as a compiled native binary, and stdout, stderr, and exit codes must match byte-for-byte. A new feature lands with corpus programs that pin its behavior both ways.
@@ -59,12 +61,147 @@ new tests should follow this convention.
## Where things live
- `packages/compiler` — the frontend (tsc API to IR), the typed IR with validator and serializer, and the LLVM and C backends.
- `packages/compiler` — the frontend (tsc API to IR), typed IR, validator, serializer, and LLVM and C backends.
- `packages/runtime` — the C runtime compiled into every scriptc binary.
- `packages/cli` — `scriptc build | run | coverage`.
- `tests/` — the differential corpus, diagnostics snapshots, and the harness.
- `docs/` — the documentation site (standalone pnpm workspace); see `docs/AGENTS.md`.
- `scripts/` — repo tooling, including the release version stamp.
- `internal/compatibility` — generated Node.js parity inventory, implementation-owned compatibility manifests, and engineering backlog.
- `tests/` — the differential corpus, diagnostics snapshots, and harness.
- `docs/` — the standalone Next.js App Router + MDX documentation workspace.
- `scripts/` — repository tooling, including the release version stamp.
## Node.js compatibility
The Node.js compatibility system is an implementation inventory and planning tool. Keep its claims narrower than the evidence. Do not change a status merely to make the public matrix look correct.
### Sources of truth
- `internal/compatibility/node-v24.json` pins the Node release, commit, official API inputs, and documentation URLs. The pinned Node API is the denominator.
- Node's official documentation defines the public API hierarchy and stability metadata. Consult Node source when the docs do not fully specify behavior.
- Running the pinned Node release is the behavior oracle for differential tests.
- Compiler source and `packages/compiler/surface-manifest.json`, augmented by `internal/compatibility/static-support.json`, define static-native support.
- `internal/compatibility/dynamic-support.json` defines dynamic-island modules, exports, members, explicit stubs, globals, and policies. It also generates the runtime builtin registry.
- Test evidence determines positive behavioral claims. Source manifests alone do not prove Node-compatible behavior.
Treat static-native and dynamic-island compatibility independently. Support in one tier says nothing about the other, and `--dynamic` does not turn an unsupported typed call into a supported static call.
### Status contract
- `supported` — broadly implemented. This requires test evidence.
- `partial` — a useful implemented subset. This requires test evidence, but evidence may cover an API family rather than every documented overload.
- `refused` — a named compiler diagnostic or explicit dynamic throwing or rejecting stub exists. Never infer this status from absence.
- `not-implemented` — no implementation claim. Its verification basis decides whether it is ready to implement or must first be investigated.
- `by-design` — intentionally outside a scriptc execution model. This requires an explicit implementation-owned architectural policy.
- `unreviewed` — no compatibility subsystem owns a reliable classification. Do not treat this as unsupported or as an implementation ticket.
- `not-applicable` — documentation, configuration, executable behavior, or an embedding model that does not map to the execution tier.
Chapter rows are generated summaries of their descendants, never independent support claims.
### Verification basis and backlog actions
Read a row's verification basis before acting:
- `test-backed` — mapped to implementation and relevant tests. Audit the exact overload or behavior before broadening a `partial` claim.
- `explicit-refusal` — source-verified refusal boundary.
- `verified-absence` — a runtime presence test confirms absence.
- `declared-gap` — an implementation-owned manifest explicitly declares the gap; it is ready for implementation.
- `registry-gap` — no implementation registry entry matched. Verify the gap before implementing: it may be an incomplete manifest or symbol match.
- `architectural-policy` — explicit intentional exclusion.
- `unreviewed`, `not-applicable`, and `derived` retain the meanings above.
`pnpm node-compat:backlog` emits tier-specific work. Its actions are:
- `verify-gap` — investigate a registry gap first; this is not yet an implementation ticket.
- `implement` — implement a declared or verified gap.
- `replace-refusal` — add support where an explicit refusal currently exists.
- `audit-partial` — identify and implement missing behavior in a tested subset, or tighten an overbroad mapping.
- `classify` — establish ownership and evidence for an unreviewed row.
Backlog priority is a heuristic based on Node stability: stable APIs are raised, release-candidate or unrated APIs remain normal, and experimental, deprecated, and legacy APIs are lowered. It is not a product roadmap.
Rows are not necessarily independent work items. One implementation can close a class, methods, overloads, aliases, and nested rows. Plan work by coherent API family and inspect the regenerated diff instead of creating one ticket per row.
Useful queue filters compose:
```bash
pnpm node-compat:backlog --action=implement --priority=high
pnpm node-compat:backlog --action=verify-gap --chapter=assert
pnpm node-compat:backlog --tier=dynamic --format=tsv
```
### Generated files
Never hand-edit these files:
- `packages/compiler/surface-manifest.json`
- `internal/compatibility/generated/node-v24-internal.json`
- `internal/compatibility/generated/node-v24-backlog.json`
- `docs/src/generated/node-v24-compatibility.json`
- `docs/src/generated/node-v24-compatibility-meta.json`
- `packages/runtime/src/scr_island_manifest.h`
Change compiler decision sources or the implementation-owned compatibility manifests, then regenerate. Use `pnpm manifest` when compiler decision tables change and `pnpm node-compat` whenever the Node pin, compatibility manifests, compiler surface manifest, or dynamic registry changes.
The public docs artifact must not expose repository test paths, internal source identifiers, engineering evidence bookkeeping, documentation/metadata headings, command-line or configuration-only entries, or rows that are N/A in both tiers. Those remain in the complete internal census under `internal/compatibility/generated/`.
### Landing compatibility work
For each API family:
1. Start from the backlog action and verification basis. Verify every `registry-gap` before assuming code is missing.
2. Read the pinned Node documentation, stability metadata, relevant Node source when needed, and existing scriptc implementation paths.
3. Decide static-native and dynamic-island scope separately.
4. Implement the smallest coherent behavior family. Preserve explicit refusals for unimplemented forms rather than silently diverging.
5. Add differential evidence that executes under Node and as a compiled binary. Cover stdout, stderr, exit status, success behavior, and important error shapes. Dynamic-island work needs island-executed differential fixtures; a load-only export check does not prove behavior.
6. Update the implementation-owned source or manifest. Positive `supported` and `partial` mappings must name existing test evidence.
7. Run `pnpm manifest` if compiler surface tables changed, then run `pnpm node-compat`. Never patch generated JSON or headers.
8. Inspect the internal ledger, public artifact, and backlog diff. Confirm the intended API family changed and unrelated rows did not.
9. Run focused tests, `pnpm node-compat:check`, and the full validation gate required for the change. If docs output changed, also run the docs gate.
Compatibility commands:
```bash
pnpm node-compat
pnpm node-compat:check
pnpm --filter @internal/compatibility check:upstream # networked pinned-input check
```
## Documentation site
The docs site is a standalone pnpm workspace under `docs/`. It uses Next.js App Router + MDX, with one topic per `docs/src/app/<topic>/page.mdx`.
### Naming and content
- The project is **scriptc**, lowercase, everywhere: page titles, prose, and code samples.
- Name-bearing strings such as the site name, tagline, GitHub URL, and canonical origin `https://scriptc.dev` live in `docs/src/lib/site.ts`.
- Coverage numbers on a page may only be output from `scriptc coverage` for a specific program shown in the same block; never publish invented aggregate coverage statistics.
- Every shell command shown in a fence must have run successfully against the current build. Output blocks contain real output; trimming is acceptable, invention is not.
- Document limitations plainly on the limitations page rather than scattering them as fine print.
### MDX conventions
- Use literal HTML `<table>` markup in MDX, never Markdown pipe tables.
- Fence info strings contain the language and optionally a filename after a colon, for example `ts:src/main.ts`; no other fence metadata survives.
- Use `console` fences for shell sessions (`$ command` followed by output).
- Use `diff` fences for additions and removals.
- Use HTML `<dl>/<dt>/<dd>` for flag and subcommand references.
### Adding a docs page
1. Add `docs/src/app/<topic>/page.mdx`.
2. Add `docs/src/app/<topic>/layout.tsx` exporting `pageMetadata("<topic>")`.
3. Add the slug to `PAGE_TITLES` in `docs/src/lib/page-titles.ts`.
4. Add navigation in `docs/src/lib/docs-navigation.ts`; it also drives mobile navigation and the sitemap.
### Docs development and verification
Do not start duplicate docs dev servers. Reuse the existing server and its hot reload process when one is running. CI-style builds must use a separate dist directory so they do not corrupt the dev server's `.next` state:
```bash
cd docs
NEXT_DIST_DIR=.next-check pnpm check
```
`pnpm check` runs the compatibility drift check, TypeScript check, and production build. It is required before landing docs or compatibility output changes.
## Releases
-52
View File
@@ -1,52 +0,0 @@
# Docs Site Conventions
This is the scriptc docs site: Next.js App Router + MDX, one topic per `src/app/<topic>/page.mdx`, standalone from the repo's pnpm workspace (it has its own lockfile; `pnpm install && pnpm check` runs entirely inside `docs/`).
## Naming
The project is **scriptc**, lowercase, everywhere — page titles, prose, code samples. All name-bearing strings (site name, tagline, GitHub URL, canonical origin `https://scriptc.dev`) live in `src/lib/site.ts`; copy elsewhere is written name-neutral and pulls from there.
## Content rules
- Coverage numbers on a page are only ever the output of `scriptc coverage` on a specific program shown in that same block — never aggregate statistics.
- Every shell command shown in a fence must have been run successfully against the current build before it lands on a page. Output blocks are real output (trimmed is fine, invented is not).
- Limitations are documented plainly on their own page, not scattered as fine print.
## MDX Tables
Always use HTML `<table>` syntax in MDX pages, never markdown pipe tables. This ensures consistent styling and avoids MDX parsing edge cases.
```html
<table>
<thead>
<tr>
<th>Column</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>field</code></td>
<td>What it does</td>
</tr>
</tbody>
</table>
```
## Code fences
The fence info string carries the language, and optionally a filename after a colon (` ```ts:src/main.ts `) — that is the only fence metadata that survives the MDX pipeline. `console` is the register for shell sessions ( `$ command` then output); `diff` fences get +/- coloring.
## Definition lists
Flag and subcommand references use HTML `<dl>/<dt>/<dd>` (styled via `article dl` rules in `globals.css`).
## Adding a page
1. `src/app/<topic>/page.mdx` for the content and `src/app/<topic>/layout.tsx` exporting `pageMetadata("<topic>")`.
2. Add the slug to `PAGE_TITLES` in `src/lib/page-titles.ts` (drives metadata and OG images).
3. Add a nav entry in `src/lib/docs-navigation.ts` (drives the sidebar, mobile nav, and sitemap).
## Verification
`pnpm check` (typecheck + production build) must pass before committing. Run it with `NEXT_DIST_DIR=.next-check` if a dev server may be running.
-4
View File
@@ -24,10 +24,6 @@ const nextConfig = {
// CI-style builds set NEXT_DIST_DIR so `pnpm check` never shares .next
// with a running dev server (a shared dist dir corrupts the dev cache).
distDir: process.env.NEXT_DIST_DIR || ".next",
// Keep check builds from retriggering a running dev server's watcher.
watchOptions: {
ignored: ["**/.next-check/**"],
},
};
export default withMDX(nextConfig);
+2 -1
View File
@@ -9,7 +9,8 @@
"build": "next build",
"start": "next start",
"typecheck": "tsc --noEmit",
"check": "pnpm typecheck && pnpm build"
"compat:check": "pnpm --dir .. node-compat:check",
"check": "pnpm compat:check && pnpm typecheck && pnpm build"
},
"dependencies": {
"@mdx-js/loader": "^3",
+14
View File
@@ -0,0 +1,14 @@
import snapshot from "@/generated/node-v24-compatibility.json";
export const dynamic = "force-static";
export const revalidate = false;
export function GET() {
return Response.json(snapshot, {
headers: {
// The client adds a generated content hash to the URL. Keep dev
// uncached; production can cache each immutable artifact URL.
"Cache-Control": process.env.NODE_ENV === "production" ? "public, max-age=31536000, immutable" : "no-store",
},
});
}
+7
View File
@@ -0,0 +1,7 @@
import { pageMetadata } from "@/lib/page-metadata";
export const metadata = pageMetadata("compatibility");
export default function Layout({ children }: { children: React.ReactNode }) {
return children;
}
+5
View File
@@ -0,0 +1,5 @@
import { NodeCompatibilityMatrix } from "@/components/node-compatibility-matrix";
export default function CompatibilityPage() {
return <NodeCompatibilityMatrix />;
}
+23
View File
@@ -439,6 +439,29 @@ article td {
color: var(--ds-gray-900);
}
.compatibility-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
.compatibility-table th {
border-bottom: 1px solid var(--ds-gray-alpha-400);
padding: 0.5rem 0.75rem;
text-align: left;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--ds-gray-900);
}
.compatibility-table td {
border-bottom: 1px solid var(--ds-gray-alpha-200);
padding: 0.5rem 0.75rem;
color: var(--ds-gray-900);
}
/* Definition lists (flag/subcommand references on the CLI page and
friends). Literal HTML in MDX bypasses the mdx-components mapping, so
these live next to the article table rules above. */
@@ -0,0 +1,3 @@
export function GET(request: Request) {
return Response.redirect(new URL("/compatibility/data", request.url), 308);
}
+5
View File
@@ -0,0 +1,5 @@
import { permanentRedirect } from "next/navigation";
export default function LegacyNodeCompatibilityPage() {
permanentRedirect("/compatibility");
}
+3 -3
View File
@@ -5,8 +5,8 @@ import { statSync } from "node:fs";
import path from "node:path";
export default function sitemap(): MetadataRoute.Sitemap {
// The homepage lives in the site header, not the docs nav, so list it explicitly.
const hrefs = ["/", ...allDocsPages.map((page) => page.href)];
// Top-level site pages live in the header, not the docs nav.
const hrefs = ["/", "/compatibility", ...allDocsPages.map((page) => page.href)];
return hrefs.map((href) => ({
url: `${siteUrl}${href}`,
lastModified: lastModifiedFor(href),
@@ -14,7 +14,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
}
function lastModifiedFor(href: string): Date {
const relative = href === "/" ? "page.tsx" : path.join(href.slice(1), "page.mdx");
const relative = href === "/" || href === "/compatibility" ? path.join(href.slice(1), "page.tsx") : path.join(href.slice(1), "page.mdx");
try {
return statSync(path.join(process.cwd(), "src", "app", relative)).mtime;
} catch {
+2 -2
View File
@@ -14,8 +14,8 @@ export function DocsMobileNav() {
return allDocsPages.find((page) => page.href === pathname) ?? allDocsPages[0];
}, [pathname]);
// The homepage is a full-width landing page without the docs chrome.
if (pathname === "/") {
// Top-level site pages are outside the documentation chrome.
if (pathname === "/" || pathname === "/compatibility") {
return null;
}
+4 -1
View File
@@ -45,10 +45,13 @@ function Sidebar() {
export function DocsNav({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
// The homepage is a full-width landing page without the docs sidebar.
// Top-level site pages bypass the documentation sidebar and article shell.
if (pathname === "/") {
return <main>{children}</main>;
}
if (pathname === "/compatibility") {
return <main className="h-[calc(100dvh-4rem)] overflow-hidden">{children}</main>;
}
return (
<div className="mx-auto max-w-5xl px-6 py-8 lg:py-12 xl:max-w-7xl">
+3 -2
View File
@@ -8,12 +8,13 @@ import { Sheet, SheetTrigger, SheetContent, SheetTitle } from "@/components/ui/s
const links = [
{ name: "Home", href: "/" },
{ name: "Docs", href: "/introduction" },
{ name: "Compatibility", href: "/compatibility" },
];
function isCurrent(href: string, pathname: string): boolean {
if (href === "/") return pathname === "/";
// Docs owns every other page.
return pathname !== "/";
if (href === "/compatibility") return pathname === "/compatibility";
return pathname !== "/" && pathname !== "/compatibility";
}
export function HeaderNav() {
@@ -0,0 +1,388 @@
"use client";
// Filter state is disposable developer UI state. Remount on Fast Refresh so
// an old search/status selection cannot make a newly generated ledger appear
// empty after an edit.
// @refresh reset
import { useEffect, useMemo, useState } from "react";
import compatibilityMeta from "@/generated/node-v24-compatibility-meta.json";
type Status =
| "supported"
| "partial"
| "refused"
| "not-implemented"
| "by-design"
| "unreviewed"
| "not-applicable";
type VerificationBasis =
| "test-backed"
| "explicit-refusal"
| "verified-absence"
| "declared-gap"
| "registry-gap"
| "architectural-policy"
| "unreviewed"
| "not-applicable"
| "derived";
type Verification = { label: string; detail: string };
type Tier = { status: Status; detail: string; verification: VerificationBasis };
type NodeStability = { index: string; level: number; text: string; inherited: boolean } | null;
type Row = {
id: string;
chapter: string;
kind: string;
name: string;
signature: string;
label: string;
apiSymbol: string;
depth: number;
scope: "api" | "documentation" | "metadata" | "configuration";
anchor: string;
nodeStability: NodeStability;
static: Tier;
dynamic: Tier;
};
type Chapter = { slug: string; title: string; entries: number; apiEntries: number };
type Snapshot = { nodeVersion: string; verificationBases: Record<VerificationBasis, Verification>; chapters: Chapter[]; rows: Row[] };
type TreeNode = { row: Row; children: TreeNode[] };
type VisibleNode = { node: TreeNode; level: number };
const labels: Record<Status, string> = {
supported: "Supported",
partial: "Partial",
refused: "Refused",
"not-implemented": "Not implemented",
"by-design": "By design",
unreviewed: "Unreviewed",
"not-applicable": "N/A",
};
const styles: Record<Status, string> = {
supported: "border-green-500/40 bg-green-100 text-green-900 dark:bg-green-200",
partial: "border-amber-500/50 bg-amber-100 text-amber-900 dark:bg-amber-200",
refused: "border-red-500/40 bg-red-100 text-red-900 dark:bg-red-200",
"not-implemented": "border-blue-500/40 bg-blue-100 text-blue-900 dark:bg-blue-200",
"by-design": "border-gray-alpha-500 bg-gray-alpha-200 text-gray-1000",
unreviewed: "border-gray-alpha-500 bg-gray-alpha-100 text-gray-900",
"not-applicable": "border-gray-alpha-400 bg-background-200 text-gray-700",
};
const statuses = ["supported", "partial", "not-implemented", "refused", "by-design", "unreviewed", "not-applicable"] as const;
const stabilityStyles: Record<number, string> = {
0: "border-red-500/40 bg-red-100 text-red-900 dark:bg-red-200",
1: "border-amber-500/50 bg-amber-100 text-amber-900 dark:bg-amber-200",
2: "border-green-500/40 bg-green-100 text-green-900 dark:bg-green-200",
3: "border-gray-alpha-500 bg-gray-alpha-100 text-gray-900",
};
function StatusBadge({ status, detail, verification }: { status: Status; detail?: string; verification?: Verification }) {
const tooltip = verification ? `${detail ?? ""}${detail ? "\n\n" : ""}Verification: ${verification.label}. ${verification.detail}` : detail;
return (
<span className="group relative inline-flex" tabIndex={tooltip ? 0 : undefined}>
<span
className={`inline-flex whitespace-nowrap rounded-full border px-2 py-0.5 text-[11px] font-medium ${styles[status]}`}
title={tooltip}
>
{labels[status]}
</span>
{tooltip ? (
<span role="tooltip" className="pointer-events-none invisible absolute left-0 top-full z-30 mt-2 w-64 rounded-md border border-gray-alpha-500 bg-background-100 p-2 text-left text-xs font-normal leading-5 text-gray-1000 opacity-0 shadow-popover transition-opacity group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100">
{detail ? <span className="block">{detail}</span> : null}
{verification ? <span className={detail ? "mt-2 block border-t border-gray-alpha-400 pt-2" : "block"}><strong>Verification:</strong> {verification.label}. {verification.detail}</span> : null}
</span>
) : null}
</span>
);
}
function StabilityBadge({ stability }: { stability: NodeStability }) {
if (stability === null) return <span className="text-xs text-gray-700" title="Node.js does not publish a stability index for this entry.">—</span>;
const detail = `Node.js Stability ${stability.index}: ${stability.text}${stability.inherited ? " (inherited from the nearest parent section)" : ""}`;
return (
<span className="group relative inline-flex" tabIndex={0}>
<span className={`inline-flex min-w-7 justify-center rounded-full border px-2 py-0.5 text-[11px] font-medium ${stabilityStyles[stability.level] ?? stabilityStyles[3]}`} title={detail}>
{stability.index}
</span>
<span role="tooltip" className="pointer-events-none invisible absolute left-1/2 top-full z-30 mt-2 w-72 -translate-x-1/2 rounded-md border border-gray-alpha-500 bg-background-100 p-2 text-left text-xs font-normal leading-5 text-gray-1000 opacity-0 shadow-popover transition-opacity group-hover:visible group-hover:opacity-100 group-focus-visible:visible group-focus-visible:opacity-100">
{detail}
</span>
</span>
);
}
function statusCounts(rows: Row[], tier: "static" | "dynamic") {
return rows.reduce<Record<Status, number>>(
(counts, row) => {
if (row.scope !== "api" || row.depth === 0) return counts;
counts[row[tier].status] += 1;
return counts;
},
{
supported: 0,
partial: 0,
refused: 0,
"not-implemented": 0,
"by-design": 0,
unreviewed: 0,
"not-applicable": 0,
},
);
}
/** Node's all.json is preorder with an explicit heading depth. */
function buildTree(rows: Row[]): TreeNode[] {
const roots: TreeNode[] = [];
const stack: TreeNode[] = [];
for (const row of rows) {
const node: TreeNode = { row, children: [] };
while (stack.length > row.depth) stack.pop();
const parent = stack[row.depth - 1];
if (row.depth === 0 || parent === undefined) roots.push(node);
else parent.children.push(node);
stack[row.depth] = node;
stack.length = row.depth + 1;
}
return roots;
}
function filterTree(nodes: TreeNode[], matches: (row: Row) => boolean): TreeNode[] {
const filtered: TreeNode[] = [];
for (const node of nodes) {
const children = filterTree(node.children, matches);
if (matches(node.row) || children.length > 0) filtered.push({ row: node.row, children });
}
return filtered;
}
function flattenTree(nodes: TreeNode[], expanded: Set<string>, forceExpanded: boolean): VisibleNode[] {
const visible: VisibleNode[] = [];
const visit = (items: TreeNode[], level: number) => {
for (const node of items) {
visible.push({ node, level });
if (node.children.length > 0 && (forceExpanded || expanded.has(node.row.id))) {
visit(node.children, level + 1);
}
}
};
visit(nodes, 1);
return visible;
}
function nodeCount(nodes: TreeNode[]): number {
return nodes.reduce((count, node) => count + 1 + nodeCount(node.children), 0);
}
function expandableIds(nodes: TreeNode[]): Set<string> {
const ids = new Set<string>();
const visit = (items: TreeNode[]) => {
for (const node of items) {
if (node.children.length > 0) ids.add(node.row.id);
visit(node.children);
}
};
visit(nodes);
return ids;
}
function Chevron({ open }: { open: boolean }) {
return (
<svg aria-hidden="true" viewBox="0 0 16 16" className={`h-3.5 w-3.5 transition-transform ${open ? "rotate-90" : ""}`} fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="m6 3 5 5-5 5" />
</svg>
);
}
export function NodeCompatibilityMatrix() {
const [snapshot, setSnapshot] = useState<Snapshot | null>(null);
const [loadError, setLoadError] = useState(false);
const [query, setQuery] = useState("");
const [staticFilter, setStaticFilter] = useState<"all" | Status>("all");
const [dynamicFilter, setDynamicFilter] = useState<"all" | Status>("all");
const [expanded, setExpanded] = useState<Set<string>>(() => new Set());
const rows = snapshot?.rows ?? [];
const chapters = snapshot?.chapters ?? [];
const verificationBases = snapshot?.verificationBases;
const apiRowCount = useMemo(() => rows.filter((row) => row.scope === "api").length, [rows]);
const chapterTitles = useMemo(() => new Map(chapters.map((item) => [item.slug, item.title])), [chapters]);
const base = `https://nodejs.org/docs/v${snapshot?.nodeVersion ?? "24.15.0"}/api/`;
const staticCounts = useMemo(() => statusCounts(rows, "static"), [rows]);
const dynamicCounts = useMemo(() => statusCounts(rows, "dynamic"), [rows]);
const visibleStatuses = useMemo(() => statuses.filter((status) => staticCounts[status] > 0 || dynamicCounts[status] > 0), [dynamicCounts, staticCounts]);
const tree = useMemo(() => buildTree(rows), [rows]);
useEffect(() => {
const controller = new AbortController();
fetch(`/compatibility/data?v=${compatibilityMeta.artifactVersion}`, { cache: "no-store", signal: controller.signal })
.then((response) => {
if (!response.ok) throw new Error(`compatibility data: ${response.status}`);
return response.json() as Promise<Snapshot>;
})
.then((data) => {
if (!Array.isArray(data.rows) || data.rows.length !== compatibilityMeta.rowCount) {
throw new Error(`compatibility data is stale or malformed (expected ${compatibilityMeta.rowCount} rows)`);
}
setSnapshot(data);
})
.catch((error: unknown) => {
if ((error as { name?: string }).name !== "AbortError") setLoadError(true);
});
return () => controller.abort();
}, []);
const needle = query.trim().toLocaleLowerCase();
const hasResultFilter = needle !== "" || staticFilter !== "all" || dynamicFilter !== "all";
const rowMatches = (row: Row) => {
if (staticFilter !== "all" && row.static.status !== staticFilter) return false;
if (dynamicFilter !== "all" && row.dynamic.status !== dynamicFilter) return false;
if (!needle) return true;
return [chapterTitles.get(row.chapter), row.kind, row.name, row.signature, row.apiSymbol, row.static.detail, row.dynamic.detail]
.join(" ")
.toLocaleLowerCase()
.includes(needle);
};
const resultTree = useMemo(
() => hasResultFilter ? filterTree(tree, rowMatches) : tree,
// rowMatches closes over these filter values.
// eslint-disable-next-line react-hooks/exhaustive-deps
[dynamicFilter, hasResultFilter, needle, staticFilter, tree],
);
const visible = useMemo(
() => flattenTree(resultTree, expanded, hasResultFilter),
[expanded, hasResultFilter, resultTree],
);
const matchedCount = useMemo(
() => hasResultFilter ? tree.reduce((count, node) => {
const visit = (item: TreeNode): number => (rowMatches(item.row) ? 1 : 0) + item.children.reduce((sum, child) => sum + visit(child), 0);
return count + visit(node);
}, 0) : nodeCount(tree),
// rowMatches closes over these filter values.
// eslint-disable-next-line react-hooks/exhaustive-deps
[dynamicFilter, hasResultFilter, needle, staticFilter, tree],
);
const changeQuery = (value: string) => setQuery(value);
const resetFilters = () => {
setQuery("");
setStaticFilter("all");
setDynamicFilter("all");
setExpanded(new Set());
};
const toggle = (id: string) => setExpanded((current) => {
const next = new Set(current);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
useEffect(() => {
if (!snapshot) return;
if (staticFilter !== "all" && staticCounts[staticFilter] === 0) setStaticFilter("all");
if (dynamicFilter !== "all" && dynamicCounts[dynamicFilter] === 0) setDynamicFilter("all");
}, [dynamicCounts, dynamicFilter, snapshot, staticCounts, staticFilter]);
if (loadError) {
return <div className="flex h-full items-center justify-center p-6"><div className="rounded-xl border border-red-500/40 bg-red-100 p-4 text-sm text-red-900">The compatibility dataset could not be loaded.</div></div>;
}
if (!snapshot) {
return <div className="flex h-full items-center justify-center p-6 text-sm text-gray-900">Loading {compatibilityMeta.rowCount.toLocaleString()} Node.js API rows…</div>;
}
return (
<section aria-labelledby="compatibility-title" className="flex h-full min-h-0 flex-col bg-background-100">
<div className="shrink-0 border-b border-gray-alpha-400 bg-background-200 px-3 py-3 md:px-4">
<div className="flex flex-col gap-3 xl:flex-row xl:items-center">
<div className="min-w-fit">
<h1 id="compatibility-title" className="text-lg font-semibold leading-6 text-gray-1000">Node.js 24 Compatibility</h1>
<p className="text-xs text-gray-800">Static native and dynamic island · Node {snapshot.nodeVersion}</p>
</div>
<div className="grid min-w-0 flex-1 gap-2 md:grid-cols-[minmax(240px,1fr)_auto_auto]">
<label className="relative block">
<span className="sr-only">Search the Node.js API inventory</span>
<svg aria-hidden="true" viewBox="0 0 16 16" className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-800" fill="none" stroke="currentColor" strokeWidth="1.5">
<circle cx="7" cy="7" r="4.5" /><path d="m10.5 10.5 3 3" />
</svg>
<input type="search" value={query} onChange={(event) => changeQuery(event.target.value)} placeholder={`Search ${apiRowCount.toLocaleString()} APIs and signatures…`} className="h-9 w-full rounded-md border border-gray-alpha-500 bg-background-100 pl-9 pr-3 text-sm text-gray-1000 placeholder:text-gray-700" />
</label>
<label className="flex items-center gap-2 text-xs text-gray-900">
<span>Static</span>
<select value={staticFilter} onChange={(event) => setStaticFilter(event.target.value as "all" | Status)} className="h-9 rounded-md border border-gray-alpha-500 bg-background-100 px-2 text-xs text-gray-1000">
<option value="all">All</option>
{statuses.filter((status) => staticCounts[status] > 0).map((status) => <option key={status} value={status}>{labels[status]} ({staticCounts[status]})</option>)}
</select>
</label>
<label className="flex items-center gap-2 text-xs text-gray-900">
<span>Dynamic</span>
<select value={dynamicFilter} onChange={(event) => setDynamicFilter(event.target.value as "all" | Status)} className="h-9 rounded-md border border-gray-alpha-500 bg-background-100 px-2 text-xs text-gray-1000">
<option value="all">All</option>
{statuses.filter((status) => dynamicCounts[status] > 0).map((status) => <option key={status} value={status}>{labels[status]} ({dynamicCounts[status]})</option>)}
</select>
</label>
</div>
<div className="flex shrink-0 items-center gap-3 text-xs text-gray-900">
<span aria-live="polite">{visible.length.toLocaleString()} visible · {matchedCount.toLocaleString()} {hasResultFilter ? "matching" : "API"} rows</span>
{!hasResultFilter ? (
<span className="flex gap-1">
<button type="button" onClick={() => setExpanded(expandableIds(tree))} className="rounded px-1.5 py-1 text-gray-900 hover:bg-gray-alpha-100 hover:text-gray-1000">Expand all</button>
<button type="button" onClick={() => setExpanded(new Set())} className="rounded px-1.5 py-1 text-gray-900 hover:bg-gray-alpha-100 hover:text-gray-1000">Collapse all</button>
</span>
) : null}
</div>
</div>
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-gray-900">
{visibleStatuses.map((status) => <StatusBadge key={status} status={status} />)}
</div>
</div>
<div className="min-h-0 flex-1 overflow-auto overscroll-contain bg-background-100">
<table role="treegrid" aria-label="Node.js 24 compatibility hierarchy" className="compatibility-table min-w-[900px]">
<thead className="sticky top-0 z-10 bg-background-200/95 backdrop-blur-sm">
<tr><th className="w-[40%]">Node.js API hierarchy</th><th className="w-[10%] text-center">Node stability</th><th className="w-[25%]">Static native</th><th className="w-[25%]">Dynamic island</th></tr>
</thead>
<tbody>
{visible.map(({ node, level }) => {
const row = node.row;
const hasChildren = node.children.length > 0;
const open = hasResultFilter || expanded.has(row.id);
const href = `${base}${row.chapter}.html${row.anchor ? `#${row.anchor}` : ""}`;
const isRoot = level === 1;
const displayName = isRoot ? chapterTitles.get(row.chapter) ?? row.label : row.label;
return (
<tr key={row.id} aria-level={level} aria-expanded={hasChildren ? open : undefined} className={`align-top hover:bg-gray-alpha-100/60 ${isRoot ? "bg-background-200/60" : ""}`}>
<td>
<div className="flex items-start gap-1.5" style={{ paddingLeft: `${(level - 1) * 18}px` }}>
{hasChildren ? (
<button type="button" onClick={() => toggle(row.id)} disabled={hasResultFilter} aria-label={`${open ? "Collapse" : "Expand"} ${displayName}`} className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded text-gray-800 hover:bg-gray-alpha-200 hover:text-gray-1000 disabled:cursor-default disabled:hover:bg-transparent">
<Chevron open={open} />
</button>
) : <span className="h-5 w-5 shrink-0" />}
<div className="min-w-0">
<a href={href} target="_blank" rel="noopener noreferrer" className={`${isRoot ? "font-sans text-sm font-medium" : "font-mono text-xs"} text-gray-1000 underline decoration-gray-alpha-500 underline-offset-2 hover:decoration-gray-1000`}>
{displayName}
</a>
<div className="mt-1 text-[11px] text-gray-800">
{row.kind}{row.apiSymbol && row.apiSymbol !== row.signature && !isRoot ? ` · ${row.apiSymbol}` : ""}
</div>
</div>
</div>
</td>
<td className="text-center"><StabilityBadge stability={row.nodeStability} /></td>
<td><StatusBadge status={row.static.status} detail={row.static.detail} verification={verificationBases?.[row.static.verification]} /></td>
<td><StatusBadge status={row.dynamic.status} detail={row.dynamic.detail} verification={verificationBases?.[row.dynamic.verification]} /></td>
</tr>
);
})}
</tbody>
</table>
{visible.length === 0 ? (
<div className="p-8 text-center text-sm text-gray-900">
<div>No API rows match these filters.</div>
<button type="button" onClick={resetFilters} className="mt-3 rounded-md border border-gray-alpha-500 px-3 py-1.5 font-medium text-gray-1000">Reset filters</button>
</div>
) : null}
</div>
</section>
);
}
@@ -0,0 +1,7 @@
{
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
"artifactVersion": "d2b35b13f573e422aed5",
"rowCount": 3662
}
File diff suppressed because it is too large Load Diff
+1
View File
@@ -8,6 +8,7 @@ export const PAGE_TITLES: Record<string, string> = {
ffi: "Native FFI",
"native-objects": "Native Program Objects",
platforms: "Platform Support",
compatibility: "Node.js 24 Compatibility",
"how-it-works": "How It Works",
limitations: "Limitations",
};
+16
View File
@@ -0,0 +1,16 @@
# Node compatibility ledger
This workspace package owns scriptc's Node.js parity system.
- `node-v24.json` pins the Node release tag, commit, and canonical docs/source inputs.
- `static-support.json` overlays dedicated compiler paths and their internal test evidence on the compiler's generated surface manifest.
- `dynamic-support.json` is the implementation-owned dynamic-island module/global registry. Its `globals.supported` entries are installed by the always-on web prelude, while `globals.npmSupported` entries are installed only by the embedded npm module bootstrap. It also generates `packages/runtime/src/scr_island_manifest.h`, so runtime exports and compatibility claims share one source.
- `generated/node-v24-internal.json` is the engineering ledger, including implementation evidence and repository test paths.
- `generated/node-v24-backlog.json` is the internal tier-specific work queue. It separates verification, implementation, explicit-refusal replacement, and partial-surface audit work, and prioritizes stable Node APIs above experimental, deprecated, and legacy APIs.
- `docs/src/generated/node-v24-compatibility.json` is the stripped public artifact consumed by the docs website. It contains compiler-relevant runtime APIs, statuses, verification bases, and user-facing details; documentation, metadata, command-line/configuration entries, and rows that are N/A in both tiers remain internal.
Statuses are evidence-aware: `supported` and `partial` require test evidence; `refused` requires an explicit compiler diagnostic or dynamic throwing stub; `not-implemented` is the actionable backlog for an owned API family with no implementation match; `by-design` records an architectural exclusion; `unreviewed` still needs classification; and `not-applicable` is not a runtime API for that execution tier.
Run `pnpm node-compat:backlog` from the repository root for a summary. The filters `--tier`, `--action`, `--priority`, `--chapter`, and `--status` compose; use `--format=json` or `--format=tsv` for tooling.
Run `pnpm node-compat` from the repository root after changing the Node pin or either support manifest. `pnpm node-compat:check` is the offline drift/evidence gate; `pnpm --filter @internal/compatibility check:upstream` additionally verifies the pinned upstream inputs over the network.
@@ -0,0 +1,84 @@
{
"schemaVersion": 1,
"chapterPolicies": {
"addons": "by-design",
"n-api": "by-design",
"embedding": "not-applicable",
"cli": "not-applicable",
"debugger": "not-applicable",
"deprecations": "not-applicable",
"intl": "not-applicable",
"permissions": "not-implemented",
"report": "not-implemented",
"single-executable-applications": "not-applicable",
"repl": "not-implemented",
"sqlite": "not-implemented",
"test": "not-implemented",
"tracing": "not-implemented",
"vm": "by-design",
"wasi": "by-design"
},
"features": [
{ "chapter": "environment_variables", "symbols": ["process.env"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
{ "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" },
{ "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "module", "require", "cache", "filename", "id", "loaded", "parent", "path"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/create-require/main.ts", "tests/fixtures/npm/cases/dynamic-import/main.ts"] },
{ "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths", "extensions", "main", "module.require", "children", "isPreloading", "paths", "Module"], "status": "not-implemented" },
{ "chapter": "modules", "symbols": ["node:", "node_modules"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/builtin-shims/main.ts", "tests/fixtures/npm/cases/create-require/main.ts"] },
{ "chapter": "esm", "symbols": ["import", "file:", "node:", "await", "url"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/dynamic-import/main.ts", "tests/corpus/2646-top-level-await.ts"] },
{ "chapter": "esm", "signatures": ["`import()` expressions"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/dynamic-import/main.ts", "tests/corpus/2050-dynamic-import-own-module/main.ts"] },
{ "chapter": "esm", "symbols": ["meta", "import.meta.resolve", "dirname", "filename", "main", "data:", "require", "__filename", "require.main", "require.resolve", "NODE_PATH", "require.extensions", "require.cache"], "status": "not-implemented" },
{ "chapter": "packages", "symbols": ["package.json", "\"name\"", "\"main\"", "\"type\"", "\"exports\"", "\"imports\""], "status": "partial", "evidence": ["tests/fixtures/npm/cases/dual-entry/main.ts", "tests/fixtures/npm/cases/self-name/main.ts", "tests/fixtures/npm/cases/scoped-nested/main.ts"] },
{ "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" },
{ "chapter": "typescript", "symbols": ["type"], "status": "not-implemented" }
],
"modules": {
"events": { "exports": ["EventEmitter", "once", "on", "listenerCount", "getEventListeners", "setMaxListeners", "defaultMaxListeners", "errorMonitor", "captureRejectionSymbol"], "members": ["emitter.addListener", "emitter.emit", "emitter.eventNames", "emitter.getMaxListeners", "emitter.listenerCount", "emitter.listeners", "emitter.off", "emitter.on", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.removeAllListeners", "emitter.removeListener", "emitter.setMaxListeners", "emitter.rawListeners", "events.getEventListeners", "events.listenerCount", "events.once", "events.on", "events.setMaxListeners"], "evidence": ["tests/fixtures/npm/cases/stream-shims/main.ts"] },
"path": { "exports": ["sep", "delimiter", "basename", "dirname", "extname", "join", "resolve", "normalize", "relative", "isAbsolute", "toNamespacedPath", "parse", "format", "posix", "win32"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"path/posix": { "exports": ["sep", "delimiter", "basename", "dirname", "extname", "join", "resolve", "normalize", "relative", "isAbsolute", "toNamespacedPath", "parse", "format", "posix", "win32"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"path/win32": { "exports": ["sep", "delimiter", "basename", "dirname", "extname", "join", "resolve", "normalize", "relative", "isAbsolute", "toNamespacedPath", "parse", "format", "posix", "win32"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"process": { "exports": ["argv", "env", "platform", "execPath", "execArgv", "version", "versions", "stdout", "stderr", "stdin", "cwd", "exit", "nextTick", "hrtime", "pid", "ppid", "title", "argv0", "release", "config", "allowedNodeEnvironmentFlags", "emitWarning", "uptime", "memoryUsage", "umask", "exitCode", "on", "once", "off", "removeListener", "emit"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"fs": { "exports": ["readFileSync", "writeFileSync", "appendFileSync", "existsSync", "realpathSync", "mkdirSync", "rmSync", "rmdirSync", "unlinkSync", "readdirSync", "statSync", "lstatSync", "accessSync", "mkdtempSync", "chmodSync", "copyFileSync", "renameSync", "constants", "Stats", "Dirent", "promises", "readFile", "writeFile", "appendFile", "exists", "realpath", "mkdir", "rm", "rmdir", "unlink", "readdir", "stat", "lstat", "access", "mkdtemp", "chmod", "copyFile", "rename", "readlink", "readlinkSync", "createReadStream", "createWriteStream", "watch", "watchFile", "unwatchFile", "openSync", "closeSync", "readSync", "read", "open"], "refused": ["watch", "watchFile", "openSync", "readSync", "read", "open"], "members": ["dirent.isBlockDevice", "dirent.isCharacterDevice", "dirent.isDirectory", "dirent.isFIFO", "dirent.isFile", "dirent.isSocket", "dirent.isSymbolicLink", "stats.isBlockDevice", "stats.isCharacterDevice", "stats.isDirectory", "stats.isFIFO", "stats.isFile", "stats.isSocket", "stats.isSymbolicLink"], "evidence": ["tests/fixtures/npm/cases/fs-shims/main.ts"] },
"fs/promises": { "exports": ["readFile", "writeFile", "appendFile", "realpath", "mkdir", "rm", "rmdir", "unlink", "readdir", "stat", "lstat", "access", "mkdtemp", "chmod", "copyFile", "rename", "readlink", "constants", "open"], "refused": ["open"], "evidence": ["tests/fixtures/npm/cases/fs-shims/main.ts"] },
"child_process": { "exports": ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync", "fork"], "refused": ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync", "fork"], "evidence": ["tests/harness/npm.test.ts"] },
"os": { "exports": ["EOL", "platform", "arch", "hostname", "homedir", "tmpdir", "type", "endianness", "userInfo", "release", "version", "machine", "cpus", "availableParallelism", "totalmem", "freemem", "loadavg", "uptime", "networkInterfaces", "constants"], "evidence": ["tests/fixtures/npm/cases/builtin-shims/main.ts"] },
"tty": { "exports": ["isatty", "ReadStream", "WriteStream"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"http": { "exports": ["request", "get", "Agent", "globalAgent", "ClientRequest", "IncomingMessage", "STATUS_CODES", "METHODS", "createServer", "Server"], "refused": ["createServer", "Server"], "members": ["agent.destroy", "request.abort", "request.end", "request.destroy", "request.flushHeaders", "request.getHeader", "request.removeHeader", "request.setHeader", "request.setTimeout", "request.write", "message.destroy", "message.setEncoding", "message.pause", "message.resume"], "evidence": ["tests/fixtures/fetch/cases/island-http/main.ts"] },
"https": { "exports": ["request", "get", "Agent", "globalAgent", "ClientRequest", "IncomingMessage", "STATUS_CODES", "METHODS", "createServer", "Server"], "refused": ["createServer", "Server"], "members": ["agent.destroy", "request.abort", "request.end", "request.destroy", "request.flushHeaders", "request.getHeader", "request.removeHeader", "request.setHeader", "request.setTimeout", "request.write", "message.destroy", "message.setEncoding", "message.pause", "message.resume"], "evidence": ["tests/fixtures/fetch/cases/island-http/main.ts"] },
"net": { "exports": ["isIP", "isIPv4", "isIPv6", "connect", "createConnection", "createServer", "Socket", "Server"], "refused": ["connect", "createConnection", "createServer", "Socket", "Server"], "evidence": ["tests/fixtures/fetch/cases/island-http/main.ts"] },
"tls": { "exports": ["connect", "createServer", "createSecureContext", "TLSSocket", "rootCertificates"], "refused": ["connect", "createServer", "createSecureContext", "TLSSocket"], "evidence": ["tests/fixtures/fetch/cases/island-http/main.ts"] },
"diagnostics_channel": { "exports": ["channel", "subscribe", "unsubscribe", "hasSubscribers", "tracingChannel"], "members": ["channel.publish", "channel.subscribe", "channel.unsubscribe", "tracingChannel.traceSync", "tracingChannel.tracePromise", "tracingChannel.traceCallback"], "evidence": ["tests/fixtures/npm/cases/builtin-shims/main.ts"] },
"module": { "exports": ["createRequire", "builtinModules", "isBuiltin", "syncBuiltinESMExports", "register", "findSourceMap"], "refused": ["register"], "evidence": ["tests/fixtures/npm/cases/create-require/main.ts", "tests/fixtures/npm/cases/misc-shims/main.ts"] },
"url": { "exports": ["URL", "URLSearchParams", "fileURLToPath", "pathToFileURL", "parse", "format", "resolve", "domainToASCII", "domainToUnicode", "urlToHttpOptions"], "members": ["url.toString", "url.toJSON", "URL.canParse", "URL.parse", "urlSearchParams.append", "urlSearchParams.delete", "urlSearchParams.entries", "urlSearchParams.forEach", "urlSearchParams.get", "urlSearchParams.getAll", "urlSearchParams.has", "urlSearchParams.keys", "urlSearchParams.set", "urlSearchParams.sort", "urlSearchParams.toString", "urlSearchParams.values", "urlSearchParams.size"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts", "tests/fixtures/npm/cases/island-web-plumbing/main.ts"] },
"buffer": { "exports": ["Buffer", "SlowBuffer", "INSPECT_MAX_BYTES", "kMaxLength", "kStringMaxLength", "constants", "isAscii", "isUtf8", "atob", "btoa", "Blob", "File", "transcode", "resolveObjectURL"], "members": ["Buffer.alloc", "Buffer.allocUnsafe", "Buffer.allocUnsafeSlow", "Buffer.from", "Buffer.isBuffer", "Buffer.isEncoding", "Buffer.byteLength", "Buffer.concat", "Buffer.compare", "buf.compare", "buf.copy", "buf.equals", "buf.fill", "buf.includes", "buf.indexOf", "buf.lastIndexOf", "buf.subarray", "buf.slice", "buf.swap16", "buf.swap32", "buf.swap64", "buf.toJSON", "buf.toString", "buf.write", "buf.readBigInt64BE", "buf.readBigInt64LE", "buf.readBigUInt64BE", "buf.readBigUInt64LE", "buf.readDoubleBE", "buf.readDoubleLE", "buf.readFloatBE", "buf.readFloatLE", "buf.readInt8", "buf.readInt16BE", "buf.readInt16LE", "buf.readInt32BE", "buf.readInt32LE", "buf.readIntBE", "buf.readIntLE", "buf.readUInt8", "buf.readUInt16BE", "buf.readUInt16LE", "buf.readUInt32BE", "buf.readUInt32LE", "buf.readUIntBE", "buf.readUIntLE", "buf.writeBigInt64BE", "buf.writeBigInt64LE", "buf.writeBigUInt64BE", "buf.writeBigUInt64LE", "buf.writeDoubleBE", "buf.writeDoubleLE", "buf.writeFloatBE", "buf.writeFloatLE", "buf.writeInt8", "buf.writeInt16BE", "buf.writeInt16LE", "buf.writeInt32BE", "buf.writeInt32LE", "buf.writeIntBE", "buf.writeIntLE", "buf.writeUInt8", "buf.writeUInt16BE", "buf.writeUInt16LE", "buf.writeUInt32BE", "buf.writeUInt32LE", "buf.writeUIntBE", "buf.writeUIntLE"], "evidence": ["tests/fixtures/npm/cases/buffer-shims/main.ts"] },
"string_decoder": { "exports": ["StringDecoder"], "members": ["stringDecoder.write", "stringDecoder.end"], "evidence": ["tests/fixtures/npm/cases/buffer-shims/main.ts"] },
"assert": { "exports": ["AssertionError", "ok", "fail", "equal", "notEqual", "strictEqual", "notStrictEqual", "deepEqual", "notDeepEqual", "deepStrictEqual", "notDeepStrictEqual", "throws", "doesNotThrow", "rejects", "doesNotReject", "match", "doesNotMatch", "ifError", "strict"], "evidence": ["tests/fixtures/npm/cases/assert-shims/main.ts"] },
"assert/strict": { "exports": ["AssertionError", "ok", "fail", "equal", "notEqual", "strictEqual", "notStrictEqual", "deepEqual", "notDeepEqual", "deepStrictEqual", "notDeepStrictEqual", "throws", "doesNotThrow", "rejects", "doesNotReject", "match", "doesNotMatch", "ifError", "strict"], "evidence": ["tests/fixtures/npm/cases/assert-shims/main.ts"] },
"domain": { "exports": ["create", "createDomain", "Domain", "active"], "members": ["domain.add", "domain.bind", "domain.enter", "domain.exit", "domain.intercept", "domain.remove", "domain.run"], "evidence": ["tests/fixtures/npm/cases/island-web-plumbing/main.ts"] },
"worker_threads": { "exports": ["isMainThread", "parentPort", "threadId", "workerData", "resourceLimits", "MessageChannel", "MessagePort", "Worker", "receiveMessageOnPort", "SHARE_ENV", "markAsUntransferable", "getEnvironmentData", "setEnvironmentData"], "refused": ["Worker"], "members": ["port.close", "port.postMessage", "port.hasRef", "port.ref", "port.start", "port.unref"], "evidence": ["tests/fixtures/npm/cases/island-web-plumbing/main.ts"] },
"perf_hooks": { "exports": ["performance", "PerformanceObserver", "monitorEventLoopDelay", "constants"], "members": ["performanceObserver.disconnect", "performanceObserver.observe", "performanceObserver.takeRecords", "performance.clearMarks", "performance.clearMeasures", "performance.getEntries", "performance.getEntriesByName", "performance.getEntriesByType", "performance.mark", "performance.measure", "performance.now", "performance.toJSON"], "evidence": ["tests/fixtures/npm/cases/island-web-plumbing/main.ts"] },
"v8": { "exports": ["startupSnapshot", "cachedDataVersionTag", "getHeapStatistics", "getHeapSpaceStatistics", "getHeapCodeStatistics", "getCppHeapStatistics", "setFlagsFromString", "takeCoverage", "stopCoverage", "setHeapSnapshotNearHeapLimit", "serialize", "deserialize", "writeHeapSnapshot", "getHeapSnapshot", "queryObjects", "startCpuProfile", "isStringOneByteRepresentation", "promiseHooks", "Serializer", "Deserializer", "DefaultSerializer", "DefaultDeserializer", "GCProfiler"], "refused": ["serialize", "deserialize", "writeHeapSnapshot", "getHeapSnapshot", "queryObjects", "startCpuProfile", "isStringOneByteRepresentation", "promiseHooks", "Serializer", "Deserializer", "DefaultSerializer", "DefaultDeserializer", "GCProfiler"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"dns": { "exports": ["lookup", "lookupService", "resolve", "resolve4", "resolve6", "resolveCname", "resolveMx", "resolveNs", "resolveSrv", "resolveTxt", "reverse", "getServers", "setServers", "Resolver", "promises", "ADDRCONFIG", "V4MAPPED", "ALL"], "refused": ["lookup", "lookupService", "resolve", "resolve4", "resolve6", "resolveCname", "resolveMx", "resolveNs", "resolveSrv", "resolveTxt", "reverse"], "evidence": ["tests/fixtures/npm/cases/island-web-plumbing/main.ts"] },
"async_hooks": { "exports": ["AsyncLocalStorage", "AsyncResource", "executionAsyncId", "triggerAsyncId", "executionAsyncResource", "createHook"], "members": ["asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable", "asyncResource.runInAsyncScope", "asyncResource.bind", "asyncResource.emitDestroy", "asyncResource.asyncId", "asyncResource.triggerAsyncId"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"punycode": { "exports": ["version", "ucs2", "decode", "encode", "toASCII", "toUnicode"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"querystring": { "exports": ["parse", "stringify", "decode", "encode", "escape", "unescape", "unescapeBuffer"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"constants": { "exports": ["F_OK", "R_OK", "W_OK", "X_OK"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"console": { "exports": ["Console", "log", "info", "debug", "warn", "error", "trace", "dir", "assert", "count", "countReset", "time", "timeEnd", "group", "groupEnd", "table", "clear"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"timers": { "exports": ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "setImmediate", "clearImmediate"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"timers/promises": { "exports": ["setTimeout", "setImmediate", "setInterval", "scheduler"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"zlib": { "exports": ["deflateSync", "inflateSync", "deflateRawSync", "inflateRawSync", "gzipSync", "gunzipSync", "unzipSync", "deflate", "inflate", "deflateRaw", "inflateRaw", "gzip", "gunzip", "unzip", "Deflate", "Inflate", "DeflateRaw", "InflateRaw", "Gzip", "Gunzip", "Unzip", "BrotliCompress", "BrotliDecompress", "createDeflate", "createInflate", "createDeflateRaw", "createInflateRaw", "createGzip", "createGunzip", "createUnzip", "createBrotliCompress", "createBrotliDecompress", "brotliCompressSync", "brotliDecompressSync", "constants"], "members": ["zlib.close", "zlib.flush", "zlib.reset"], "evidence": ["tests/fixtures/npm/cases/zlib-shims/main.ts"] },
"readline": { "exports": ["Interface", "createInterface", "clearLine", "clearScreenDown", "cursorTo", "moveCursor", "emitKeypressEvents"], "members": ["rl.question", "rl.close", "rl.pause", "rl.prompt", "rl.resume", "rl.setPrompt", "rl.getPrompt", "rl.write"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"stream": { "exports": ["Stream", "Readable", "Writable", "Duplex", "Transform", "PassThrough", "pipeline", "finished", "addAbortSignal", "promises", "isErrored", "isDestroyed", "isReadable", "isWritable"], "members": ["stream.Readable.from", "readable.read", "readable.push", "readable.unshift", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding", "writable.destroy", "transform.destroy"], "refusedMembers": ["readable.wrap"], "evidence": ["tests/fixtures/npm/cases/stream-shims/main.ts"] },
"stream/promises": { "exports": ["pipeline", "finished"], "evidence": ["tests/fixtures/npm/cases/stream-shims/main.ts"] },
"stream/consumers": { "exports": ["text", "buffer", "arrayBuffer", "json", "blob"], "evidence": ["tests/fixtures/npm/cases/stream-shims/main.ts"] },
"stream/web": { "exports": ["ReadableStream", "TransformStream", "TextDecoderStream"], "evidence": ["tests/fixtures/npm/cases/web-streams/main.ts"] },
"crypto": { "exports": ["createHash", "createHmac", "hash", "Hash", "Hmac", "randomBytes", "randomFillSync", "randomFill", "randomInt", "randomUUID", "getRandomValues", "timingSafeEqual", "pbkdf2", "pbkdf2Sync", "getHashes", "getCiphers", "getCurves", "webcrypto", "subtle", "constants", "KeyObject", "createCipheriv", "createDecipheriv", "createSign", "createVerify", "createDiffieHellman", "createECDH", "createPublicKey", "createPrivateKey", "createSecretKey", "diffieHellman", "generateKeyPair", "generateKeyPairSync", "generateKey", "generateKeySync", "sign", "verify", "publicEncrypt", "publicDecrypt", "privateEncrypt", "privateDecrypt", "scrypt", "scryptSync", "hkdf", "hkdfSync", "X509Certificate", "Certificate", "checkPrime", "checkPrimeSync", "generatePrime", "generatePrimeSync", "secureHeapUsed", "setEngine", "setFips", "getFips"], "refused": ["KeyObject", "createCipheriv", "createDecipheriv", "createSign", "createVerify", "createDiffieHellman", "createECDH", "createPublicKey", "createPrivateKey", "createSecretKey", "diffieHellman", "generateKeyPair", "generateKeyPairSync", "generateKey", "generateKeySync", "sign", "verify", "publicEncrypt", "publicDecrypt", "privateEncrypt", "privateDecrypt", "scrypt", "scryptSync", "hkdf", "hkdfSync", "X509Certificate", "Certificate", "checkPrime", "checkPrimeSync", "generatePrime", "generatePrimeSync", "secureHeapUsed", "setEngine"], "members": ["hash.copy", "hash.digest", "hash.update", "hmac.digest", "hmac.update"], "evidence": ["tests/fixtures/npm/cases/crypto-shims/main.ts"] },
"util": { "exports": ["format", "formatWithOptions", "inspect", "inherits", "promisify", "callbackify", "deprecate", "debuglog", "debug", "types", "isDeepStrictEqual", "stripVTControlCharacters", "styleText", "parseArgs", "toUSVString", "_extend", "TextEncoder", "TextDecoder", "isArray"], "evidence": ["tests/fixtures/npm/cases/util-shims/main.ts"] },
"util/types": { "exports": ["isAnyArrayBuffer", "isArrayBufferView", "isArgumentsObject", "isArrayBuffer", "isAsyncFunction", "isBigInt64Array", "isBigUint64Array", "isBooleanObject", "isBoxedPrimitive", "isBigIntObject", "isCryptoKey", "isDataView", "isDate", "isExternal", "isFloat16Array", "isFloat32Array", "isFloat64Array", "isGeneratorFunction", "isGeneratorObject", "isInt8Array", "isInt16Array", "isInt32Array", "isKeyObject", "isMap", "isMapIterator", "isModuleNamespaceObject", "isNativeError", "isNumberObject", "isPromise", "isProxy", "isRegExp", "isSet", "isSetIterator", "isSharedArrayBuffer", "isStringObject", "isSymbolObject", "isTypedArray", "isUint8Array", "isUint8ClampedArray", "isUint16Array", "isUint32Array", "isWeakMap", "isWeakSet"], "evidence": ["tests/fixtures/npm/cases/util-shims/main.ts"] }
},
"globals": {
"supported": ["atob", "btoa", "clearInterval", "clearTimeout", "queueMicrotask", "setInterval", "setTimeout", "structuredClone", "AbortController", "AbortSignal", "Blob", "CustomEvent", "DOMException", "Event", "EventTarget", "File", "Headers", "MessageChannel", "MessageEvent", "MessagePort", "Request", "Response", "TextDecoder", "TextDecoderStream", "TextEncoder", "TransformStream", "URLSearchParams", "ReadableStream"],
"npmSupported": ["Buffer", "URL", "clearImmediate", "setImmediate"],
"absent": ["BroadcastChannel", "ByteLengthQueuingStrategy", "CloseEvent", "CompressionStream", "CountQueuingStrategy", "CryptoKey", "DecompressionStream", "EventSource", "FormData", "Navigator", "ReadableByteStreamController", "ReadableStreamBYOBReader", "ReadableStreamBYOBRequest", "Storage", "SubtleCrypto", "TextEncoderStream", "TransformStreamDefaultController", "URLPattern", "WebAssembly", "WebSocket", "WritableStream", "WritableStreamDefaultController", "WritableStreamDefaultWriter"],
"evidence": ["tests/harness/web-globals.test.ts"]
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
{
"schemaVersion": 1,
"version": "24.15.0",
"tag": "v24.15.0",
"commit": "848430679556aed0bd073f2bc263331ad84fa119",
"allJsonUrl": "https://nodejs.org/download/release/v24.15.0/docs/api/all.json",
"indexMarkdownUrl": "https://nodejs.org/docs/v24.15.0/api/index.md",
"markdownBaseUrl": "https://nodejs.org/docs/v24.15.0/api/",
"sourceBaseUrl": "https://github.com/nodejs/node/tree/848430679556aed0bd073f2bc263331ad84fa119",
"chapterModules": {
"assert": ["assert", "assert/strict"],
"async_context": ["async_hooks"],
"async_hooks": ["async_hooks"],
"buffer": ["buffer"],
"child_process": ["child_process"],
"cluster": ["cluster"],
"console": ["console"],
"crypto": ["crypto"],
"diagnostics_channel": ["diagnostics_channel"],
"dns": ["dns"],
"domain": ["domain"],
"events": ["events"],
"fs": ["fs", "fs/promises"],
"http": ["http"],
"http2": ["http2"],
"https": ["https"],
"inspector": ["inspector", "inspector/promises"],
"module": ["module"],
"net": ["net"],
"os": ["os"],
"path": ["path", "path/posix", "path/win32"],
"perf_hooks": ["perf_hooks"],
"process": ["process"],
"punycode": ["punycode"],
"querystring": ["querystring"],
"readline": ["readline", "readline/promises"],
"repl": ["repl"],
"sqlite": ["sqlite"],
"stream": ["stream", "stream/promises", "stream/consumers", "stream/web"],
"string_decoder": ["string_decoder"],
"test": ["test", "test/reporters"],
"timers": ["timers", "timers/promises"],
"tls": ["tls"],
"tracing": ["trace_events"],
"tty": ["tty"],
"dgram": ["dgram"],
"url": ["url"],
"util": ["util", "util/types"],
"v8": ["v8"],
"vm": ["vm"],
"wasi": ["wasi"],
"webcrypto": ["crypto"],
"webstreams": ["stream/web"],
"worker_threads": ["worker_threads"],
"zlib": ["zlib"]
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "@internal/compatibility",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "pnpm check",
"update": "node src/update.mjs",
"backlog": "node src/backlog.mjs",
"check": "node src/generate-island-header.mjs --check && node src/generate.mjs --check-local",
"check:upstream": "node src/generate.mjs --check"
}
}
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
const packageRoot = fileURLToPath(new URL("../", import.meta.url));
const backlog = JSON.parse(readFileSync(`${packageRoot}generated/node-v24-backlog.json`, "utf8"));
const args = process.argv.slice(2);
function option(name) {
const prefix = `--${name}=`;
return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length) ?? null;
}
function integerOption(name, fallback) {
const value = option(name);
if (value === null) return fallback;
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`--${name} must be a non-negative integer`);
return parsed;
}
const tierFilter = option("tier");
const actionFilter = option("action");
const priorityFilter = option("priority");
const chapterFilter = option("chapter");
const statusFilter = option("status");
const format = option("format") ?? "summary";
const limit = integerOption("limit", format === "summary" ? 20 : Number.MAX_SAFE_INTEGER);
for (const [name, value, allowed] of [
["tier", tierFilter, ["static", "dynamic"]],
["action", actionFilter, ["verify-gap", "implement", "replace-refusal", "audit-partial", "classify"]],
["priority", priorityFilter, ["high", "normal", "low"]],
["format", format, ["summary", "json", "tsv"]],
]) {
if (value !== null && !allowed.includes(value)) throw new Error(`--${name} must be one of: ${allowed.join(", ")}`);
}
const items = [];
for (const task of backlog.tasks) {
if (priorityFilter && task.priority !== priorityFilter) continue;
if (chapterFilter && task.chapter !== chapterFilter) continue;
for (const [tier, work] of Object.entries(task.tiers)) {
if (tierFilter && tier !== tierFilter) continue;
if (actionFilter && work.action !== actionFilter) continue;
if (statusFilter && work.status !== statusFilter) continue;
items.push({
id: task.id,
chapter: task.chapter,
label: task.label,
apiSymbol: task.apiSymbol,
docsUrl: task.docsUrl,
nodeStability: task.nodeStability,
priority: task.priority,
tier,
...work,
});
}
}
const priorityRank = { high: 0, normal: 1, low: 2 };
const actionRank = { implement: 0, "replace-refusal": 1, "audit-partial": 2, "verify-gap": 3, classify: 4 };
items.sort((a, b) =>
priorityRank[a.priority] - priorityRank[b.priority] ||
actionRank[a.action] - actionRank[b.action] ||
a.chapter.localeCompare(b.chapter) ||
a.label.localeCompare(b.label) ||
a.tier.localeCompare(b.tier)
);
const selected = items.slice(0, limit);
if (format === "json") {
console.log(JSON.stringify({ nodeVersion: backlog.nodeVersion, matched: items.length, items: selected }, null, 2));
} else if (format === "tsv") {
console.log("priority\taction\ttier\tstatus\tverification\tchapter\tlabel\tdocsUrl");
for (const item of selected) {
console.log([item.priority, item.action, item.tier, item.status, item.verification, item.chapter, item.label, item.docsUrl]
.map((value) => String(value).replaceAll("\t", " ").replaceAll("\n", " "))
.join("\t"));
}
} else {
const filters = [tierFilter && `tier=${tierFilter}`, actionFilter && `action=${actionFilter}`, priorityFilter && `priority=${priorityFilter}`, chapterFilter && `chapter=${chapterFilter}`, statusFilter && `status=${statusFilter}`].filter(Boolean);
console.log(`Node ${backlog.nodeVersion} compatibility backlog: ${items.length} tier items${filters.length ? ` (${filters.join(", ")})` : ""}`);
const counts = {};
for (const item of items) counts[`${item.priority}/${item.action}`] = (counts[`${item.priority}/${item.action}`] ?? 0) + 1;
for (const [key, count] of Object.entries(counts).sort()) console.log(`${String(count).padStart(5)} ${key}`);
if (selected.length > 0) console.log("");
for (const item of selected) console.log(`${item.priority.padEnd(6)} ${item.action.padEnd(15)} ${item.tier.padEnd(7)} ${item.chapter.padEnd(24)} ${item.label}`);
if (selected.length < items.length) console.log(`\nShowing ${selected.length} of ${items.length}; use --limit=<n> or --format=json|tsv.`);
}
@@ -0,0 +1,47 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
const packageRoot = fileURLToPath(new URL("../", import.meta.url));
const root = fileURLToPath(new URL("../../../", import.meta.url));
const manifestPath = `${packageRoot}dynamic-support.json`;
const headerPath = `${root}packages/runtime/src/scr_island_manifest.h`;
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
function wrap(text, width = 88) {
const out = [];
for (let i = 0; i < text.length; i += width) out.push(text.slice(i, i + width));
return out;
}
function render() {
const lines = [
"/* Generated by @internal/compatibility from dynamic-support.json.",
" * Do not edit: the manifest is the implementation-owned builtin registry. */",
"static const struct {",
" const char *name;",
" const char *exports;",
"} isl_builtins[] = {",
];
for (const [module, entry] of Object.entries(manifest.modules)) {
const chunks = wrap(entry.exports.join(","));
lines.push(` {\"node:${module}\",`);
for (let i = 0; i < chunks.length; i++) {
lines.push(` \"${chunks[i]}\"${i === chunks.length - 1 ? "}," : ""}`);
}
}
lines.push("};", "");
return lines.join("\n");
}
const rendered = render();
if (process.argv.includes("--check")) {
if (readFileSync(headerPath, "utf8") !== rendered) {
throw new Error("scr_island_manifest.h is stale; run 'pnpm node-compat'");
}
console.log(`island manifest is current (${Object.keys(manifest.modules).length} modules)`);
} else {
writeFileSync(headerPath, rendered);
console.log(`wrote packages/runtime/src/scr_island_manifest.h (${Object.keys(manifest.modules).length} modules)`);
}
+981
View File
@@ -0,0 +1,981 @@
#!/usr/bin/env node
/* Generate the website's complete Node.js v24 parity inventory.
*
* Node owns the denominator: the pinned index.md names every API chapter and all.json
* supplies the structured API tree (classes, methods, properties, events,
* globals, overloads, and conceptual sections). scriptc owns the projection:
* the compiler surface/dedicated manifests and the runtime island manifest.
* Anything those sources do not prove is either "not-implemented" (when a
* known implementation surface has no matching implementation) or
* "unreviewed" (when no compatibility subsystem owns the classification).
* Neither state is optimistically treated as support or an explicit refusal.
*
* pnpm node-compat fetch latest-v24.x and rewrite the snapshot
* pnpm node-compat --check fetch and fail if the snapshot would change
* pnpm node-compat:check offline implementation/profile drift check
*/
import { createHash } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
const packageRoot = fileURLToPath(new URL("../", import.meta.url));
const root = fileURLToPath(new URL("../../../", import.meta.url));
const internalOutputPath = `${packageRoot}generated/node-v24-internal.json`;
const backlogOutputPath = `${packageRoot}generated/node-v24-backlog.json`;
const publicOutputPath = `${root}docs/src/generated/node-v24-compatibility.json`;
const publicMetaOutputPath = `${root}docs/src/generated/node-v24-compatibility-meta.json`;
const compilerManifestPath = `${root}packages/compiler/surface-manifest.json`;
const compilerCompatPath = `${packageRoot}static-support.json`;
const islandManifestPath = `${packageRoot}dynamic-support.json`;
const pinPath = `${packageRoot}node-v24.json`;
const pin = JSON.parse(readFileSync(pinPath, "utf8"));
const NODE_BASE = pin.markdownBaseUrl;
const CHILD_FIELDS = ["modules", "classes", "methods", "properties", "events", "globals", "miscs"];
const NON_FUNCTIONAL_CHAPTERS = new Set(["documentation", "synopsis"]);
const STATUSES = [
"supported",
"partial",
"refused",
"not-implemented",
"by-design",
"unreviewed",
"not-applicable",
];
const STATUS_SET = new Set(STATUSES);
const VERIFICATION_BASES = new Set([
"test-backed",
"explicit-refusal",
"verified-absence",
"declared-gap",
"registry-gap",
"architectural-policy",
"unreviewed",
"not-applicable",
"derived",
]);
function sha(value) {
return createHash("sha256").update(value).digest("hex");
}
function normalizeSymbol(value) {
if (!value) return "";
let out = value.replaceAll("`", "").replace(/^new\s+/, "").trim();
out = out.replace(/\s+Type:\s*.*$/, "").replace(/\s*\{[^}]*\}\s*$/, "");
const call = out.indexOf("(");
if (call >= 0) out = out.slice(0, call);
out = out.replace(/\[.*$/, "").replace(/\s*=.*$/, "").replace(/^Class:\s*/, "");
out = out.replace(/^Event:\s*/, "").replace(/^Static method:\s*/, "");
return out.trim();
}
function apiSymbolOf(node) {
const code = node.textRaw?.match(/`([^`]+)`/)?.[1];
return normalizeSymbol(code ?? node.name ?? node.textRaw ?? "");
}
/** Exact tools/doc/html.mjs getId() algorithm at the pinned Node commit. */
function nodeAnchor(textRaw, counters) {
const base = String(textRaw ?? "")
.toLowerCase()
.replace(/[^\w\- ]/g, "")
.replace(/ /g, "-");
const count = counters.get(base) ?? 0;
counters.set(base, count + 1);
return count === 0 ? base : `${base}_${count}`;
}
function displayLabel(textRaw) {
const raw = String(textRaw ?? "");
// all.json expands property headings with type/description prose. The
// visible Node heading is the first code span; keep the table equally terse.
if (raw.startsWith("`")) {
const firstCode = raw.match(/^`([^`]+)`/)?.[1];
if (firstCode !== undefined) return firstCode;
}
return raw
.replaceAll("`", "")
.replace(/^(?:Class|Event|Static method):\s*/, "")
.trim();
}
const STABILITY_LABELS = {
0: "Deprecated",
1: "Experimental",
2: "Stable",
3: "Legacy",
};
function nodeStability(node, inherited) {
if (node.stability === undefined || node.stability === null) {
return inherited === null ? null : { ...inherited, inherited: true };
}
const level = Number(node.stability);
const text = String(node.stabilityText ?? STABILITY_LABELS[level] ?? "").trim();
const sublevel = level === 1 ? text.match(/^\.(\d+)\s*-\s*(.*)$/s) : null;
return {
index: sublevel ? `1.${sublevel[1]}` : String(level),
level,
text: sublevel?.[2]?.trim() || text || STABILITY_LABELS[level] || `Stability ${level}`,
inherited: false,
};
}
function manifestIndex() {
const manifest = JSON.parse(readFileSync(compilerManifestPath, "utf8"));
const byName = new Map();
for (const item of manifest.entries) {
if (item.kind !== "node-builtin" && item.kind !== "stdlib") continue;
const key = normalizeSymbol(item.name);
const current = byName.get(key);
if (!current || current.status !== "static") byName.set(key, item);
}
return byName;
}
function firstEvidenceForManifestId(id, compat) {
let match = null;
for (const [prefix, files] of Object.entries(compat.evidenceByManifestPrefix)) {
if (id.startsWith(prefix) && (match === null || prefix.length > match.prefix.length)) {
match = { prefix, files };
}
}
return match?.files ?? [];
}
function validateEvidence(paths, label) {
if (!Array.isArray(paths) || paths.length === 0) {
throw new Error(`${label} makes a support claim without test evidence`);
}
for (const path of paths) {
try {
readFileSync(`${root}${path}`, "utf8");
} catch {
throw new Error(`${label} names missing evidence '${path}'`);
}
}
}
function validateStatus(status, label) {
if (!STATUS_SET.has(status)) throw new Error(`${label} has unknown status '${status}'`);
}
function dedicatedIndex(compat) {
const out = new Map();
for (const entry of compat.dedicated) {
validateStatus(entry.status, `dedicated symbols ${entry.symbols.join(", ")}`);
if (entry.status === "supported" || entry.status === "partial") {
validateEvidence(entry.evidence, `dedicated symbols ${entry.symbols.join(", ")}`);
}
for (const symbol of entry.symbols) {
if (out.has(symbol)) throw new Error(`duplicate dedicated compatibility symbol '${symbol}'`);
out.set(symbol, entry);
}
}
return out;
}
function featureEntries(compat) {
for (const entry of compat.features ?? []) {
validateStatus(entry.status, `${entry.chapter} compatibility features`);
if (entry.status === "supported" || entry.status === "partial") {
validateEvidence(entry.evidence, `${entry.chapter} compatibility features`);
}
}
return compat.features ?? [];
}
function featureOf(row, entries) {
return entries.find((entry) =>
entry.chapter === row.chapter &&
((entry.symbols ?? []).includes(row.apiSymbol) || (entry.signatures ?? []).includes(row.signature))
);
}
function symbolCandidates(row) {
const result = new Set([row.apiSymbol]);
const aliases = [
["fsPromises.", "fs/promises."],
["timersPromises.", "timers/promises."],
["streamPromises.", "stream/promises."],
["streamConsumers.", "stream/consumers."],
];
for (const value of [...result]) {
for (const [from, to] of aliases) if (value.startsWith(from)) result.add(to + value.slice(from.length));
}
return [...result].filter(Boolean);
}
function directExportName(row, module) {
const symbol = row.apiSymbol;
const subpathAliases = {
"fs/promises": "fsPromises",
"stream/promises": "streamPromises",
"stream/consumers": "streamConsumers",
"timers/promises": "timersPromises",
};
const alias = subpathAliases[module];
if (alias) {
// A chapter can contain both a bare module and one or more subpaths. Do
// not let the chapter prefix make a bare-module row look like an export
// from a subpath (for example, fs.chmod must not match
// fs/promises.chmod). The alias is the only valid spelling for a
// subpath's rows; the fallback prefixes below are for bare modules.
if (!symbol.startsWith(`${alias}.`)) return null;
const direct = symbol.slice(alias.length + 1);
return direct.includes(".") ? null : direct;
}
const moduleDots = module.replaceAll("/", ".");
const prefixes = new Set([
moduleDots,
module.split("/").at(-1),
row.chapter,
row.chapter.replaceAll("_", ""),
]);
for (const prefix of prefixes) {
if (prefix && symbol.startsWith(`${prefix}.`)) {
const direct = symbol.slice(prefix.length + 1);
return direct.includes(".") ? null : direct;
}
}
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(symbol)) return symbol;
if (row.depth === 1 && ["class", "method", "property", "global"].includes(row.kind)) return row.name;
return null;
}
function manifestSymbolCandidates(row) {
const modules = pin.chapterModules[row.chapter] ?? [];
const result = new Set(
symbolCandidates(row).filter((candidate) => candidate.includes(".")),
);
for (const module of modules) {
const direct = directExportName(row, module);
if (!direct) continue;
result.add(`${module}.${direct}`);
if (module.includes("/") && direct === module.split("/").at(-1)) result.add(module);
}
return [...result];
}
function tier(status, _reason, evidence) {
return {
status,
...(evidence?.source ? { evidence: evidence.source } : {}),
...(evidence?.tests?.length ? { tests: evidence.tests } : {}),
};
}
function scopeOf(row) {
if (row.depth === 0) return "api";
if (
row.chapter === "cli" ||
row.apiSymbol.startsWith("-") ||
/\bcommand-line option\b/i.test(row.signature) ||
row.chapter === "environment_variables" && row.apiSymbol === "export"
) return "configuration";
if (row.chapter === "deprecations") return "metadata";
if (row.chapter === "intl" && row.kind === "module") return "metadata";
return row.depth > 0 && (row.kind === "module" || row.kind === "misc") && !row.signature.includes("`")
? "documentation"
: "api";
}
function classificationContext() {
const compilerCompat = JSON.parse(readFileSync(compilerCompatPath, "utf8"));
const island = JSON.parse(readFileSync(islandManifestPath, "utf8"));
for (const [chapter, status] of Object.entries(compilerCompat.chapterPolicies)) {
validateStatus(status, `static chapter policy '${chapter}'`);
}
for (const [chapter, status] of Object.entries(island.chapterPolicies)) {
validateStatus(status, `dynamic chapter policy '${chapter}'`);
}
for (const [module, entry] of Object.entries(island.modules)) {
const exports = new Set(entry.exports);
for (const name of entry.refused ?? []) {
if (!exports.has(name)) throw new Error(`island module '${module}' marks non-export '${name}' refused`);
}
const members = new Set(entry.members ?? []);
for (const name of entry.refusedMembers ?? []) {
if (members.has(name)) throw new Error(`island module '${module}' marks member '${name}' both supported and refused`);
}
validateEvidence(entry.evidence, `island module '${module}'`);
}
const supportedGlobals = new Set(island.globals.supported);
const npmSupportedGlobals = new Set(island.globals.npmSupported ?? []);
for (const name of npmSupportedGlobals) {
if (supportedGlobals.has(name)) throw new Error(`island global '${name}' is both always-supported and npm-supported`);
}
for (const name of island.globals.absent) {
if (supportedGlobals.has(name) || npmSupportedGlobals.has(name)) throw new Error(`island global '${name}' is both supported and absent`);
}
validateEvidence(island.globals.evidence, "island globals");
return {
manifest: manifestIndex(),
compilerCompat,
dedicated: dedicatedIndex(compilerCompat),
staticFeatures: featureEntries(compilerCompat),
dynamicFeatures: featureEntries(island),
island,
};
}
function classifyStatic(row, chapter, ctx) {
if (row.depth === 0) return tier("unreviewed", "", { source: "chapter-summary" });
if (row.scope === "documentation" || row.scope === "configuration") return tier("not-applicable", "", { source: row.scope });
const feature = featureOf(row, ctx.staticFeatures);
if (feature) return tier(feature.status, "", { source: `compiler-feature:${row.chapter}.${row.apiSymbol}`, tests: feature.evidence });
for (const candidate of symbolCandidates(row)) {
// Dedicated lowering paths are authoritative when an older generic
// fence row shares the same API name (the surface manifest documents
// its omissions explicitly; this overlay closes them with evidence).
const dedicated = ctx.dedicated.get(candidate);
if (dedicated) return tier(dedicated.status, "", { source: `compiler-dedicated:${candidate}`, tests: dedicated.evidence });
}
for (const candidate of manifestSymbolCandidates(row)) {
const item = ctx.manifest.get(candidate);
if (item) {
if (item.status === "static") {
const tests = firstEvidenceForManifestId(item.id, ctx.compilerCompat);
if (tests.length === 0) return tier("unreviewed", "");
validateEvidence(tests, item.id);
return tier("partial", "", { source: `surface-manifest:${item.id}`, tests });
}
return tier("refused", "", { source: `surface-manifest:${item.status}:${item.id}` });
}
}
const policy = ctx.compilerCompat.chapterPolicies[row.chapter];
if (policy && policy !== "unreviewed") {
return tier(policy, "", { source: `compiler-chapter-policy:${row.chapter}` });
}
if ((pin.chapterModules[row.chapter] ?? []).length > 0 || row.chapter === "globals" || row.chapter === "errors") {
return tier("not-implemented", "", { source: `compiler-unmatched:${row.chapter}` });
}
return tier("unreviewed", "");
}
function classifyDynamic(row, chapter, ctx) {
if (row.depth === 0) return tier("unreviewed", "", { source: "chapter-summary" });
if (row.scope === "documentation" || row.scope === "configuration") return tier("not-applicable", "", { source: row.scope });
const feature = featureOf(row, ctx.dynamicFeatures);
if (feature) return tier(feature.status, "", { source: `island-feature:${row.chapter}.${row.apiSymbol}`, tests: feature.evidence });
if (row.chapter === "globals") {
if (ctx.island.globals.supported.includes(row.name)) {
return tier("partial", "", { source: `island-global:${row.name}`, tests: ctx.island.globals.evidence });
}
if (ctx.island.globals.npmSupported?.includes(row.name)) {
return tier("partial", "", { source: `island-npm-global:${row.name}`, tests: ctx.island.globals.evidence });
}
if (ctx.island.globals.absent.includes(row.name)) {
return tier("not-implemented", "", { source: `island-global-absent:${row.name}`, tests: ctx.island.globals.evidence });
}
}
const modules = pin.chapterModules[row.chapter] ?? [];
for (const module of modules) {
const moduleEntry = ctx.island.modules[module];
const direct = directExportName(row, module);
if (!moduleEntry) continue;
if (moduleEntry.refusedMembers?.includes(row.apiSymbol)) {
return tier("refused", "", { source: `island-member-stub:node:${module}.${row.apiSymbol}` });
}
if (moduleEntry.members?.includes(row.apiSymbol)) {
return tier("partial", "", { source: `island-member:node:${module}.${row.apiSymbol}`, tests: moduleEntry.evidence });
}
if (direct && moduleEntry.refused?.includes(direct)) {
return tier("refused", "", { source: `island-stub:node:${module}.${direct}` });
}
if (direct && moduleEntry.exports.includes(direct)) {
return tier("partial", "", { source: `island-export:node:${module}.${direct}`, tests: moduleEntry.evidence });
}
}
const policy = ctx.island.chapterPolicies[row.chapter];
if (policy && policy !== "unreviewed") {
return tier(policy, "", { source: `island-chapter-policy:${row.chapter}` });
}
if (modules.length > 0 && modules.every((module) => ctx.island.modules[module] === undefined)) {
return tier("not-implemented", "", { source: `unshimmed:${modules.map((module) => `node:${module}`).join(",")}` });
}
if (modules.length > 0 || row.chapter === "globals" || row.chapter === "errors") {
return tier("not-implemented", "", { source: `island-unmatched:${row.chapter}` });
}
return tier("unreviewed", "");
}
function treeSize(node) {
let size = 1;
for (const field of CHILD_FIELDS) for (const child of node[field] ?? []) size += treeSize(child);
return size;
}
function topLevelNodes(allJson) {
const result = [];
for (const field of CHILD_FIELDS) for (const node of allJson[field] ?? []) result.push(node);
return result;
}
function flattenChapter(rootNode, chapter, ctx) {
const rows = [];
const seenIds = new Map();
const anchorCounters = new Map();
const walk = (node, parents, depth, inheritedStability) => {
const signature = `${chapter.slug}\0${node.type}\0${parents.map((parent) => parent.name).join("/")}\0${node.name}\0${node.textRaw}`;
const baseId = `${chapter.slug}:${sha(signature).slice(0, 12)}`;
const duplicate = seenIds.get(baseId) ?? 0;
seenIds.set(baseId, duplicate + 1);
const id = duplicate === 0 ? baseId : `${baseId}:${duplicate + 1}`;
const apiSymbol = apiSymbolOf(node);
const stability = nodeStability(node, inheritedStability);
// Count every structured heading, including the chapter root, in the
// same preorder Node's Markdown renderer visits.
const generatedAnchor = nodeAnchor(node.textRaw, anchorCounters);
const row = {
id,
chapter: chapter.slug,
kind: node.type ?? "section",
name: node.name ?? node.textRaw ?? "(unnamed)",
signature: node.textRaw ?? node.name ?? "(unnamed)",
label: displayLabel(node.textRaw ?? node.name ?? "(unnamed)"),
apiSymbol,
depth,
scope: "api",
anchor: depth > 0 ? generatedAnchor : "",
nodeStability: stability,
};
row.scope = scopeOf(row);
row.static = classifyStatic(row, chapter, ctx);
row.dynamic = classifyDynamic(row, chapter, ctx);
rows.push(row);
for (const field of CHILD_FIELDS) {
for (const child of node[field] ?? []) walk(child, [...parents, node], depth + 1, stability);
}
};
walk(rootNode, [], 0, null);
return rows;
}
function apiChapterLinks(indexMarkdown) {
const links = [...indexMarkdown.matchAll(/^\* \[(.+?)\]\(([^)]+\.md)\)$/gm)]
.map((match) => ({ title: match[1].replaceAll("`", ""), file: match[2], slug: match[2].slice(0, -3) }));
return links.filter((link) => !NON_FUNCTIONAL_CHAPTERS.has(link.slug));
}
function summarizeTier(rows, key, chapterPolicy) {
const descendants = rows.slice(1).filter((row) => row.scope === "api").map((row) => row[key]);
const statuses = new Set(descendants.map((tier) => tier.status));
const tests = [...new Set(descendants.flatMap((tier) => tier.tests ?? []))].sort();
if (statuses.size === 0 && chapterPolicy) {
return tier(chapterPolicy, "", { source: `${key === "static" ? "compiler" : "island"}-chapter-policy:${rows[0].chapter}` });
}
if (statuses.size === 1) return tier([...statuses][0], "", { source: "derived:descendants", tests });
if (statuses.has("supported") || statuses.has("partial")) {
return tier("partial", "", { source: "derived:descendants", tests });
}
if (statuses.has("unreviewed")) return tier("unreviewed", "", { source: "derived:descendants" });
if (statuses.has("not-implemented")) return tier("not-implemented", "", { source: "derived:descendants" });
if (statuses.has("refused")) return tier("refused", "", { source: "derived:descendants" });
if (statuses.has("by-design")) return tier("by-design", "", { source: "derived:descendants" });
return tier("unreviewed", "", { source: "derived:descendants" });
}
function statusCounts(rows, key) {
const counts = Object.fromEntries(STATUSES.map((status) => [status, 0]));
for (const row of rows) if (row.scope === "api") counts[row[key].status] += 1;
return counts;
}
function verifiedAnchorIds(html) {
return new Set(
[...html.matchAll(/<a class="mark" href="#[^"]+" id="([^"]+)">#/g)]
.map((match) => match[1]),
);
}
function verifyAnchors(snapshot, htmlByChapter) {
let verified = 0;
let ancestorFallback = 0;
let chapterFallback = 0;
const ancestors = [];
for (const row of snapshot.rows) {
if (row.depth === 0) {
ancestors.length = 0;
ancestors[0] = row;
row.anchorSource = "chapter";
continue;
}
ancestors.length = row.depth;
const ids = htmlByChapter.get(row.chapter);
if (ids?.has(row.anchor)) {
verified += 1;
row.anchorSource = "exact";
} else {
const ancestor = [...ancestors].reverse().find((candidate) => candidate?.anchorSource === "exact" && candidate.anchor !== "");
if (ancestor) {
row.anchor = ancestor.anchor;
row.anchorSource = "ancestor";
ancestorFallback += 1;
} else {
// A chapter link is useful and correct; an invented fragment is not.
row.anchor = "";
row.anchorSource = "chapter";
chapterFallback += 1;
}
}
ancestors[row.depth] = row;
}
snapshot.anchors = { verified, ancestorFallback, chapterFallback };
}
function buildSnapshot({ allJson, indexMarkdown, allSha256, indexSha256 }) {
const chapterLinks = apiChapterLinks(indexMarkdown);
const top = topLevelNodes(allJson);
const ctx = classificationContext();
const chapters = [];
const rows = [];
if (chapterLinks.length === 0) throw new Error("the pinned Node index contains no functional API chapters");
for (const link of chapterLinks) {
const source = `doc/api/${link.file}`;
const candidates = top.filter((node) => node.source === source).sort((a, b) => treeSize(b) - treeSize(a));
if (candidates.length === 0) throw new Error(`all.json has no root for ${source}`);
const chapter = { slug: link.slug, title: link.title };
const chapterRows = flattenChapter(candidates[0], chapter, ctx);
chapterRows[0].static = summarizeTier(chapterRows, "static", ctx.compilerCompat.chapterPolicies[link.slug]);
chapterRows[0].dynamic = summarizeTier(chapterRows, "dynamic", ctx.island.chapterPolicies[link.slug]);
chapters.push({
slug: link.slug,
title: link.title,
entries: chapterRows.length,
apiEntries: chapterRows.filter((row) => row.scope === "api").length,
static: statusCounts(chapterRows, "static"),
dynamic: statusCounts(chapterRows, "dynamic"),
});
rows.push(...chapterRows);
}
const ids = new Set();
for (const row of rows) {
if (ids.has(row.id)) throw new Error(`duplicate generated row id ${row.id}`);
ids.add(row.id);
}
return {
schemaVersion: 4,
nodeVersion: pin.version,
nodeTag: pin.tag,
nodeCommit: pin.commit,
sources: {
allJson: { url: pin.allJsonUrl, sha256: allSha256 },
indexMarkdown: { url: pin.indexMarkdownUrl, sha256: indexSha256 },
sourceBaseUrl: pin.sourceBaseUrl,
},
supportManifests: {
compilerSurface: "packages/compiler/surface-manifest.json",
compilerDedicated: "internal/compatibility/static-support.json",
dynamicIsland: "internal/compatibility/dynamic-support.json",
},
chapters,
rows,
};
}
function render(snapshot) {
return `${JSON.stringify(snapshot, null, 2)}\n`;
}
function publicDetail(tier) {
const source = tier.evidence ?? "";
if (source === "derived:descendants") return "Derived from the exact API rows in this chapter.";
if (tier.status === "unreviewed") return "Not yet classified; tracked as open parity work.";
if (source.startsWith("surface-manifest:unsupported:")) return "Explicitly rejected by a named compiler diagnostic.";
if (source.startsWith("surface-manifest:dynamic-only:")) return "Explicitly refused in static code; this form requires the dynamic island.";
if (source.startsWith("surface-manifest:")) return "Implemented for the call shapes accepted by the compiler lowering.";
if (source.startsWith("compiler-dedicated:")) return "Implemented by a dedicated static compiler/runtime path.";
if (source.startsWith("compiler-feature:")) {
if (tier.status === "not-applicable") return "Node configuration or documentation that does not map to a compiled program API.";
if (tier.status === "not-implemented") return "Not implemented in scriptc's static module-loader subset yet.";
return "Implemented for the documented scriptc module-loader subset.";
}
if (source.startsWith("compiler-chapter-policy:")) {
if (tier.status === "by-design") return "Intentionally outside the static native execution model.";
if (tier.status === "not-applicable") return "This Node executable or embedding feature does not map to static native code.";
return "No static implementation exists for this Node API family yet.";
}
if (source.startsWith("compiler-unmatched:")) return "No static compiler lowering is registered for this API yet.";
if (source.startsWith("island-export:")) return "Available through the embedded Node compatibility shim; exact member and overload coverage may be narrower.";
if (source.startsWith("island-member:")) return "Implemented by the embedded Node compatibility shim.";
if (source.startsWith("island-member-stub:")) return "Exposed only as an explicit throwing or rejecting compatibility stub.";
if (source.startsWith("island-stub:")) return "Exposed only as an explicit throwing or rejecting compatibility stub.";
if (source.startsWith("island-global:")) return "Provided by the embedded engine's web/global compatibility layer.";
if (source.startsWith("island-npm-global:")) return "Provided when the embedded npm module bootstrap runs; absent from standalone dynamic islands.";
if (source.startsWith("island-global-absent:")) return "Verified absent from the embedded engine's web/global compatibility layer.";
if (source.startsWith("unshimmed:")) return "No dynamic-island shim exists for this Node module yet.";
if (source.startsWith("island-chapter-policy:")) {
if (tier.status === "by-design") return "Intentionally outside the dynamic-island execution model.";
if (tier.status === "not-applicable") return "This Node executable or embedding feature does not map to dynamic-island code.";
return "No dynamic-island implementation exists for this Node API family yet.";
}
if (source.startsWith("island-feature:")) {
if (tier.status === "not-applicable") return "Node configuration or documentation that does not map to an island runtime API.";
if (tier.status === "not-implemented") return "Not implemented in the embedded module-loader subset yet.";
return "Implemented for the documented embedded module-loader subset.";
}
if (source.startsWith("island-unmatched:")) return "This API is not implemented by the dynamic-island shim.";
if (source === "documentation") return "Documentation section; not a runtime API.";
if (source === "configuration") return "Node command-line or configuration entry; not a runtime API.";
if (tier.status === "not-applicable") return "This Node executable or embedding feature does not map to this scriptc tier.";
if (tier.status === "refused") return "Explicitly rejected by this scriptc tier.";
if (tier.status === "not-implemented") return "No implementation is registered for this scriptc tier yet.";
if (tier.status === "by-design") return "Intentionally outside this scriptc tier's execution model.";
return "Implemented for a documented subset of this API.";
}
const PUBLIC_VERIFICATION = {
"test-backed": {
label: "Test-backed mapping",
detail: "Mapped to implementation and relevant repository tests; evidence may cover an API family rather than every documented overload.",
},
"explicit-refusal": {
label: "Explicit refusal",
detail: "Backed by a named compiler refusal or an explicit dynamic throwing or rejecting stub.",
},
"verified-absence": {
label: "Verified absence",
detail: "A runtime presence test confirms that this API is absent.",
},
"declared-gap": {
label: "Declared gap",
detail: "The implementation-owned compatibility manifest explicitly records this API family or feature as not implemented.",
},
"registry-gap": {
label: "Registry gap",
detail: "No implementation registry entry matched this Node API; verify the gap before starting implementation.",
},
"architectural-policy": {
label: "Architectural policy",
detail: "An implementation-owned policy intentionally excludes this API from the execution tier.",
},
unreviewed: {
label: "Unreviewed",
detail: "No compatibility subsystem owns a reliable classification yet.",
},
"not-applicable": {
label: "Not applicable",
detail: "This entry is documentation, configuration, or an execution model that does not map to the tier.",
},
derived: {
label: "Derived summary",
detail: "Computed from the exact descendant API rows rather than asserted independently.",
},
};
function verificationBasis(tier) {
const source = tier.evidence ?? "";
let basis;
if (source === "derived:descendants") basis = "derived";
else if (tier.status === "supported" || tier.status === "partial") basis = "test-backed";
else if (tier.status === "refused") basis = "explicit-refusal";
else if (source.startsWith("island-global-absent:")) basis = "verified-absence";
else if (tier.status === "by-design") basis = "architectural-policy";
else if (tier.status === "not-applicable") basis = "not-applicable";
else if (tier.status === "unreviewed") basis = "unreviewed";
else if (
tier.status === "not-implemented" &&
(source.startsWith("compiler-chapter-policy:") ||
source.startsWith("compiler-feature:") ||
source.startsWith("island-chapter-policy:") ||
source.startsWith("island-feature:"))
) basis = "declared-gap";
else if (tier.status === "not-implemented") basis = "registry-gap";
else throw new Error(`cannot determine verification basis for ${tier.status}/${source || "no-source"}`);
if (!VERIFICATION_BASES.has(basis)) throw new Error(`unknown verification basis '${basis}'`);
return basis;
}
function classificationConfidence(basis) {
if (["test-backed", "explicit-refusal", "verified-absence", "architectural-policy"].includes(basis)) return "high";
if (["declared-gap", "not-applicable"].includes(basis)) return "medium";
if (["registry-gap", "unreviewed"].includes(basis)) return "low";
return "derived";
}
function nodePriority(stability) {
if (stability?.index === "2") return "high";
if (stability?.index === "0" || stability?.index === "3" || stability?.level === 1 && stability?.index !== "1.2") return "low";
return "normal";
}
function backlogAction(tier) {
if (tier.status === "partial") return "audit-partial";
if (tier.status === "refused") return "replace-refusal";
if (tier.status === "unreviewed") return "classify";
if (tier.status === "not-implemented") {
return verificationBasis(tier) === "registry-gap" ? "verify-gap" : "implement";
}
return null;
}
function buildBacklog(snapshot) {
const tasks = [];
const tierItems = { static: {}, dynamic: {} };
const priorities = { high: 0, normal: 0, low: 0 };
for (const row of snapshot.rows) {
if (row.depth === 0 || row.scope !== "api") continue;
const tiers = {};
for (const key of ["static", "dynamic"]) {
const action = backlogAction(row[key]);
if (action === null) continue;
tierItems[key][action] = (tierItems[key][action] ?? 0) + 1;
tiers[key] = {
status: row[key].status,
action,
verification: verificationBasis(row[key]),
confidence: classificationConfidence(verificationBasis(row[key])),
source: row[key].evidence ?? null,
tests: row[key].tests ?? [],
};
}
if (Object.keys(tiers).length === 0) continue;
const priority = nodePriority(row.nodeStability);
priorities[priority] += Object.keys(tiers).length;
tasks.push({
id: row.id,
chapter: row.chapter,
label: row.label,
signature: row.signature,
apiSymbol: row.apiSymbol,
docsUrl: `https://nodejs.org/docs/v${snapshot.nodeVersion}/api/${row.chapter}.html${row.anchor ? `#${row.anchor}` : ""}`,
nodeStability: row.nodeStability,
priority,
tiers,
});
}
return {
schemaVersion: 1,
nodeVersion: snapshot.nodeVersion,
nodeCommit: snapshot.nodeCommit,
summary: {
taskCount: tasks.length,
tierItemCount: Object.values(tierItems.static).reduce((sum, count) => sum + count, 0) +
Object.values(tierItems.dynamic).reduce((sum, count) => sum + count, 0),
tiers: tierItems,
priorities,
},
tasks,
};
}
function publicExclusion(row) {
if (row.scope === "documentation") return "documentation";
if (row.scope === "metadata") return "metadata";
if (row.scope === "configuration") return "configuration";
if (row.scope !== "api") return "non-api";
if (row.static.status === "not-applicable" && row.dynamic.status === "not-applicable") return "not-applicable";
return null;
}
function projectPublicMatrix(snapshot) {
const chapters = [];
const rows = [];
const excluded = { documentation: 0, metadata: 0, configuration: 0, "non-api": 0, "not-applicable": 0, chapters: 0 };
for (const chapter of snapshot.chapters) {
const sourceRows = snapshot.rows.filter((row) => row.chapter === chapter.slug);
const root = { ...sourceRows[0], depth: 0 };
const chapterRows = [root];
const ancestors = [root];
for (const row of sourceRows.slice(1)) {
ancestors.length = row.depth;
const exclusion = publicExclusion(row);
if (exclusion !== null) {
excluded[exclusion] += 1;
continue;
}
const parent = [...ancestors].reverse().find(Boolean) ?? root;
const projected = { ...row, depth: parent.depth + 1 };
chapterRows.push(projected);
ancestors[row.depth] = projected;
}
if (chapterRows.length === 1) {
excluded.chapters += 1;
continue;
}
root.static = summarizeTier(chapterRows, "static");
root.dynamic = summarizeTier(chapterRows, "dynamic");
chapters.push({
slug: chapter.slug,
title: chapter.title,
entries: chapterRows.length,
apiEntries: chapterRows.length,
static: statusCounts(chapterRows, "static"),
dynamic: statusCounts(chapterRows, "dynamic"),
});
rows.push(...chapterRows);
}
return {
chapters,
rows,
inventory: {
sourceChapters: snapshot.chapters.length,
sourceRows: snapshot.rows.length,
matrixChapters: chapters.length,
matrixRows: rows.length,
excluded,
},
};
}
function publicSnapshot(snapshot) {
const matrix = projectPublicMatrix(snapshot);
return {
schemaVersion: 4,
nodeVersion: snapshot.nodeVersion,
nodeTag: snapshot.nodeTag,
nodeCommit: snapshot.nodeCommit,
sources: snapshot.sources,
verificationBases: PUBLIC_VERIFICATION,
inventory: matrix.inventory,
chapters: matrix.chapters,
rows: matrix.rows.map((row) => ({
id: row.id,
chapter: row.chapter,
kind: row.kind,
name: row.name,
signature: row.signature,
label: row.label,
apiSymbol: row.apiSymbol,
depth: row.depth,
scope: row.scope,
anchor: row.anchor,
nodeStability: row.nodeStability,
static: { status: row.static.status, detail: publicDetail(row.static), verification: verificationBasis(row.static) },
dynamic: { status: row.dynamic.status, detail: publicDetail(row.dynamic), verification: verificationBasis(row.dynamic) },
})),
};
}
function publicMetadata(publicRendered, snapshot) {
const matrix = projectPublicMatrix(snapshot);
return {
schemaVersion: 3,
nodeVersion: snapshot.nodeVersion,
nodeCommit: snapshot.nodeCommit,
artifactVersion: sha(publicRendered).slice(0, 20),
rowCount: matrix.rows.length,
};
}
function checkLocal() {
const snapshot = JSON.parse(readFileSync(internalOutputPath, "utf8"));
if (snapshot.nodeVersion !== pin.version || snapshot.nodeCommit !== pin.commit) {
throw new Error(`snapshot targets Node ${snapshot.nodeVersion}/${snapshot.nodeCommit}, pin targets ${pin.version}/${pin.commit}`);
}
const ctx = classificationContext();
if (snapshot.chapters.length !== 62) throw new Error(`expected 62 pinned Node API chapters, found ${snapshot.chapters.length}`);
for (const row of snapshot.rows) {
if (row.depth === 0) continue;
const chapter = snapshot.chapters.find((item) => item.slug === row.chapter);
const expectedStatic = classifyStatic(row, chapter, ctx);
const expectedDynamic = classifyDynamic(row, chapter, ctx);
if (JSON.stringify(row.static) !== JSON.stringify(expectedStatic) || JSON.stringify(row.dynamic) !== JSON.stringify(expectedDynamic)) {
throw new Error(`generated classification is stale at ${row.id} (${row.signature}); run 'pnpm node-compat'`);
}
}
for (const row of snapshot.rows) {
if (row.depth > 0 && row.anchor !== "" && row.anchorSource !== "exact" && row.anchorSource !== "ancestor") {
throw new Error(`${row.id} publishes an unverified Node HTML anchor`);
}
}
if (readFileSync(publicOutputPath, "utf8") !== render(publicSnapshot(snapshot))) {
throw new Error("the docs compatibility artifact is stale; run 'pnpm node-compat'");
}
const publicRows = publicSnapshot(snapshot).rows;
for (const row of publicRows) {
if (row.depth > 0 && row.scope !== "api") throw new Error(`${row.id} exposes a non-API row in the public matrix`);
if (row.depth > 0 && row.static.status === "not-applicable" && row.dynamic.status === "not-applicable") {
throw new Error(`${row.id} exposes a row that is not applicable to either compiler tier`);
}
}
if (readFileSync(backlogOutputPath, "utf8") !== render(buildBacklog(snapshot))) {
throw new Error("the Node compatibility backlog is stale; run 'pnpm node-compat'");
}
const publicRendered = render(publicSnapshot(snapshot));
if (readFileSync(publicMetaOutputPath, "utf8") !== render(publicMetadata(publicRendered, snapshot))) {
throw new Error("the docs compatibility metadata is stale; run 'pnpm node-compat'");
}
for (const row of snapshot.rows) {
for (const key of ["static", "dynamic"]) {
if ((row[key].status === "supported" || row[key].status === "partial") && (row[key].tests?.length ?? 0) === 0) {
throw new Error(`${row.id} claims ${key} ${row[key].status} without test evidence`);
}
if (row[key].status === "refused") {
const source = row[key].evidence ?? "";
const explicit = key === "static"
? source.startsWith("surface-manifest:unsupported:") || source.startsWith("surface-manifest:dynamic-only:")
: source.startsWith("island-stub:") || source.startsWith("island-member-stub:");
if (!explicit && source !== "derived:descendants") {
throw new Error(`${row.id} claims ${key} refused without an explicit refusal source`);
}
}
if (row[key].status === "by-design") {
const source = row[key].evidence ?? "";
if (!source.includes("chapter-policy") && source !== "derived:descendants") {
throw new Error(`${row.id} claims ${key} by-design without an explicit chapter policy`);
}
}
verificationBasis(row[key]);
}
}
const matrix = projectPublicMatrix(snapshot);
console.log(`Node compatibility snapshot is locally current (${snapshot.rows.length} source rows, ${matrix.rows.length} public matrix rows, Node ${snapshot.nodeVersion})`);
}
async function fetchText(url) {
const response = await fetch(url, { headers: { accept: "application/json,text/markdown,text/html" } });
if (!response.ok) throw new Error(`fetch ${url}: ${response.status} ${response.statusText}`);
return response.text();
}
async function main() {
const args = new Set(process.argv.slice(2));
if (args.has("--check-local")) return checkLocal();
const [allText, indexMarkdown] = await Promise.all([
fetchText(pin.allJsonUrl),
fetchText(pin.indexMarkdownUrl),
]);
const snapshot = buildSnapshot({
allJson: JSON.parse(allText),
indexMarkdown,
allSha256: sha(allText),
indexSha256: sha(indexMarkdown),
});
const htmlByChapter = new Map(
await Promise.all(snapshot.chapters.map(async (chapter) => [
chapter.slug,
verifiedAnchorIds(await fetchText(`${NODE_BASE}${chapter.slug}.html`)),
])),
);
verifyAnchors(snapshot, htmlByChapter);
const internalRendered = render(snapshot);
const backlogRendered = render(buildBacklog(snapshot));
const publicRendered = render(publicSnapshot(snapshot));
const publicMetaRendered = render(publicMetadata(publicRendered, snapshot));
if (args.has("--check")) {
if (readFileSync(internalOutputPath, "utf8") !== internalRendered) throw new Error("internal Node compatibility snapshot is stale; run 'pnpm node-compat'");
if (readFileSync(backlogOutputPath, "utf8") !== backlogRendered) throw new Error("Node compatibility backlog is stale; run 'pnpm node-compat'");
if (readFileSync(publicOutputPath, "utf8") !== publicRendered) throw new Error("public Node compatibility snapshot is stale; run 'pnpm node-compat'");
if (readFileSync(publicMetaOutputPath, "utf8") !== publicMetaRendered) throw new Error("public Node compatibility metadata is stale; run 'pnpm node-compat'");
console.log(`Node compatibility snapshot matches ${pin.tag} (${snapshot.rows.length} API rows, commit ${pin.commit.slice(0, 12)})`);
return;
}
writeFileSync(internalOutputPath, internalRendered);
writeFileSync(backlogOutputPath, backlogRendered);
writeFileSync(publicOutputPath, publicRendered);
writeFileSync(publicMetaOutputPath, publicMetaRendered);
console.log(`wrote compatibility ledger, backlog, and public artifact (${snapshot.rows.length} source rows, ${projectPublicMatrix(snapshot).rows.length} public matrix rows, ${pin.tag})`);
}
await main();
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
const check = process.argv.slice(2).includes("--check");
const args = check ? ["--check"] : [];
for (const script of ["generate-island-header.mjs", "generate.mjs"]) {
const result = spawnSync(process.execPath, [`src/${script}`, ...args], { stdio: "inherit" });
if (result.error) throw result.error;
if (result.status !== 0) process.exit(result.status ?? 1);
}
@@ -0,0 +1,96 @@
{
"schemaVersion": 1,
"chapterPolicies": {
"addons": "by-design",
"n-api": "by-design",
"embedding": "not-applicable",
"cli": "not-applicable",
"debugger": "not-applicable",
"deprecations": "not-applicable",
"environment_variables": "unreviewed",
"errors": "unreviewed",
"globals": "unreviewed",
"intl": "not-applicable",
"modules": "unreviewed",
"esm": "unreviewed",
"packages": "unreviewed",
"typescript": "unreviewed",
"permissions": "not-implemented",
"report": "not-implemented",
"single-executable-applications": "not-applicable",
"repl": "not-implemented",
"sqlite": "not-implemented",
"tracing": "not-implemented",
"v8": "by-design",
"vm": "by-design",
"wasi": "by-design",
"webcrypto": "unreviewed",
"webstreams": "unreviewed"
},
"evidenceByManifestPrefix": {
"node-builtin.assert": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1609-assert-async.ts"],
"node-builtin.async_hooks": ["tests/corpus/2213-async-local-storage.cjs"],
"node-builtin.buffer": ["tests/corpus/1402-buffer-encodings.ts", "tests/corpus/1660-buffer-read-write-num.ts"],
"node-builtin.child_process": ["tests/corpus/1361-spawn-events.ts", "tests/corpus/1565-spawn-pipe-streams.ts"],
"node-builtin.cluster": ["tests/corpus/957-builtins-namespace.ts"],
"node-builtin.crypto": ["tests/corpus/1358-crypto-random.ts", "tests/corpus/1534-crypto-hash-chain.ts", "tests/corpus/2556-crypto-introspection.ts"],
"node-builtin.dgram": ["tests/corpus/2597-dgram-send-ladders.cjs"],
"node-builtin.diagnostics_channel": ["tests/corpus/2214-dc-bind-store.cjs"],
"node-builtin.dns": ["tests/corpus/2597-dgram-send-ladders.cjs"],
"node-builtin.events": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"],
"node-builtin.fs": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs"],
"node-builtin.http": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts"],
"node-builtin.http2": ["tests/corpus/2260-http2-constants.cjs"],
"node-builtin.https": ["tests/corpus/2645-https-client-url-argument.ts", "tests/corpus/2690-https-server-timeout-option.ts"],
"node-builtin.module": ["tests/corpus/1598-cjs-builtin-require/main.js"],
"node-builtin.net": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs"],
"node-builtin.os": ["tests/corpus/1480-os-network-interfaces.ts"],
"node-builtin.path": ["tests/corpus/957-builtins-namespace.ts"],
"node-builtin.perf_hooks": ["tests/corpus/957-builtins-namespace.ts"],
"node-builtin.process": ["tests/corpus/990-process-basics.ts", "tests/corpus/2314-process-introspection.ts"],
"node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts"],
"node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"],
"node-builtin.stream": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/2629-stream-consumers.ts"],
"node-builtin.string_decoder": ["tests/corpus/1662-string-decoder-encodings.ts"],
"node-builtin.test": ["tests/harness/node-test.test.ts"],
"node-builtin.timers": ["tests/corpus/1804-timers-module.ts", "tests/corpus/2093-timers-promises.ts"],
"node-builtin.tls": ["tests/corpus/2331-tls-server-options-typed.ts", "tests/corpus/2557-tls-ca-store.ts"],
"node-builtin.tty": ["tests/corpus/1448-stream-columns.ts"],
"node-builtin.url": ["tests/corpus/1794-searchparams-url-live.ts"],
"node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts"],
"node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts"],
"node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts"],
"stdlib.fetch": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.headers": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.request": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.response": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"],
"stdlib.abort": ["tests/harness/fetch-conformance.test.ts"]
},
"features": [
{ "chapter": "environment_variables", "symbols": ["export"], "status": "not-applicable" },
{ "chapter": "globals", "symbols": ["fetch"], "status": "partial", "evidence": ["tests/harness/fetch-conformance.test.ts"] },
{ "chapter": "modules", "symbols": ["__dirname", "__filename", "exports", "require", "module.require", "main", "filename"], "status": "partial", "evidence": ["tests/corpus/1612-cjs-module-globals.cjs", "tests/corpus/1596-cjs-modules/main.js", "tests/corpus/1629-require-main-filename.cjs"] },
{ "chapter": "modules", "symbols": ["require.resolve", "require.resolve.paths", "cache", "extensions", "children", "id", "isPreloading", "loaded", "parent", "paths", "Module"], "status": "not-implemented" },
{ "chapter": "modules", "symbols": ["node:", "node_modules"], "status": "partial", "evidence": ["tests/corpus/1598-cjs-builtin-require/main.js", "tests/corpus/1596-cjs-modules/main.js"] },
{ "chapter": "esm", "symbols": ["import", "file:", "node:", "await"], "status": "partial", "evidence": ["tests/corpus/950-modules-basic/main.ts", "tests/corpus/2646-top-level-await.ts"] },
{ "chapter": "esm", "symbols": ["meta", "import.meta.resolve", "dirname", "filename", "main", "data:", "require", "__filename", "require.main", "require.resolve", "NODE_PATH", "require.extensions", "require.cache"], "status": "not-implemented" },
{ "chapter": "packages", "symbols": ["package.json", "\"name\"", "\"main\"", "\"type\"", "\"exports\"", "\"imports\""], "status": "partial", "evidence": ["tests/corpus/2092-package-imports/main.ts", "tests/corpus/2120-package-self-import/main.ts", "tests/corpus/2124-imports-field-wildcard/main.ts", "tests/corpus/2390-dot-requires/main.cjs"] },
{ "chapter": "packages", "symbols": ["--input-type"], "status": "not-applicable" },
{ "chapter": "typescript", "symbols": ["type"], "status": "not-implemented" }
],
"dedicated": [
{ "symbols": ["console.log", "console.info", "console.debug", "console.error", "console.warn"], "status": "partial", "evidence": ["tests/corpus/1460-console-error-warn.ts", "tests/corpus/2440-console-inspect-args.ts"] },
{ "symbols": ["assert", "assert.ok", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.doesNotReject", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts"] },
{ "symbols": ["EventEmitter", "emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.off", "emitter.removeListener", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.listeners", "emitter.rawListeners", "emitter.eventNames", "emitter.setMaxListeners", "emitter.getMaxListeners", "events.getEventListeners", "events.listenerCount", "events.once", "events.on", "events.setMaxListeners"], "status": "partial", "evidence": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"] },
{ "symbols": ["AsyncLocalStorage", "asyncLocalStorage.run", "asyncLocalStorage.exit", "asyncLocalStorage.getStore", "asyncLocalStorage.enterWith", "asyncLocalStorage.disable"], "status": "partial", "evidence": ["tests/corpus/2213-async-local-storage.cjs"] },
{ "symbols": ["StringDecoder", "stringDecoder.write", "stringDecoder.end"], "status": "partial", "evidence": ["tests/corpus/1662-string-decoder-encodings.ts"] },
{ "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts"] },
{ "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs"] },
{ "symbols": ["net.createServer", "net.connect", "net.createConnection", "net.getDefaultAutoSelectFamilyAttemptTimeout", "net.setDefaultAutoSelectFamilyAttemptTimeout", "server.address", "server.close", "server.listen", "socket.destroy", "socket.end", "socket.pause", "socket.resume", "socket.setEncoding", "socket.setTimeout", "socket.write"], "status": "partial", "evidence": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs"] },
{ "symbols": ["dgram.createSocket", "socket.address", "socket.bind", "socket.close", "socket.connect", "socket.ref", "socket.remoteAddress", "socket.send", "socket.unref"], "status": "partial", "evidence": ["tests/corpus/2597-dgram-send-ladders.cjs"] },
{ "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts"] },
{ "symbols": ["test", "it", "test.skip", "test.todo", "test.only", "describe", "suite", "before", "after", "beforeEach", "afterEach", "context.test", "context.skip", "context.todo", "context.diagnostic", "context.name", "context.assert"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] },
{ "symbols": ["http.request", "http.get", "http.createServer", "https.request", "https.get", "https.createServer", "request.end", "request.destroy", "request.setTimeout", "request.write", "response.end", "response.getHeader", "response.hasHeader", "response.removeHeader", "response.setHeader", "response.write", "response.writeHead", "headersTimeout", "requestTimeout", "timeout", "keepAliveTimeout", "keepAliveTimeoutBuffer"], "status": "partial", "evidence": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2689-http-server-timeout-properties.ts"] }
]
}
+3
View File
@@ -15,6 +15,9 @@
"test:watch": "vitest",
"lint": "eslint packages/compiler/src packages/cli/src",
"manifest": "tsx scripts/surface-manifest.mjs",
"node-compat": "pnpm --filter @internal/compatibility run update",
"node-compat:backlog": "pnpm --filter @internal/compatibility run backlog --",
"node-compat:check": "pnpm --filter @internal/compatibility run check",
"scriptc": "tsx packages/cli/src/main.ts"
},
"devDependencies": {
+6 -147
View File
@@ -2380,145 +2380,10 @@ static char *isl_module_normalize(JSContext *ctx, const char *base,
/* Named export lists for the builtin ESM wrappers. The wrapper source is
* `const m = __scr_require("node:x"); export default m; export const
* {…} = m;` — the shims themselves live in the bootstrap below. */
static const struct {
const char *name;
const char *exports;
} isl_builtins[] = {
{"node:events",
"EventEmitter,once,on,listenerCount,getEventListeners,setMaxListeners,"
"defaultMaxListeners,errorMonitor,captureRejectionSymbol"},
{"node:path",
"sep,delimiter,basename,dirname,extname,join,resolve,normalize,relative,"
"isAbsolute,toNamespacedPath,parse,format,posix,win32"},
{"node:path/posix",
"sep,delimiter,basename,dirname,extname,join,resolve,normalize,relative,"
"isAbsolute,toNamespacedPath,parse,format,posix,win32"},
{"node:path/win32",
"sep,delimiter,basename,dirname,extname,join,resolve,normalize,relative,"
"isAbsolute,toNamespacedPath,parse,format,posix,win32"},
{"node:process",
"argv,env,platform,execPath,execArgv,version,versions,stdout,stderr,stdin,"
"cwd,exit,nextTick,hrtime,pid,ppid,title,argv0,release,config,"
"allowedNodeEnvironmentFlags,emitWarning,uptime,memoryUsage,umask,"
"exitCode,on,once,off,removeListener,emit"},
{"node:fs",
"readFileSync,writeFileSync,appendFileSync,existsSync,realpathSync,"
"mkdirSync,rmSync,rmdirSync,unlinkSync,readdirSync,statSync,lstatSync,"
"accessSync,mkdtempSync,chmodSync,copyFileSync,renameSync,constants,"
"Stats,Dirent,promises,readFile,writeFile,appendFile,exists,realpath,"
"mkdir,rm,rmdir,unlink,readdir,stat,lstat,access,mkdtemp,chmod,copyFile,"
"rename,readlink,readlinkSync,createReadStream,createWriteStream,watch,"
"watchFile,unwatchFile,openSync,closeSync,readSync,read,open"},
{"node:fs/promises",
"readFile,writeFile,appendFile,realpath,mkdir,rm,rmdir,unlink,readdir,"
"stat,lstat,access,mkdtemp,chmod,copyFile,rename,readlink,constants,open"},
{"node:child_process", "spawn,spawnSync,exec,execSync,execFile,execFileSync,fork"},
{"node:os",
"EOL,platform,arch,hostname,homedir,tmpdir,type,endianness,userInfo,"
"release,version,machine,cpus,availableParallelism,totalmem,freemem,"
"loadavg,uptime,networkInterfaces,constants"},
{"node:tty", "isatty,ReadStream,WriteStream"},
{"node:http",
"request,get,Agent,globalAgent,ClientRequest,IncomingMessage,STATUS_CODES,"
"METHODS,createServer,Server"},
{"node:https",
"request,get,Agent,globalAgent,ClientRequest,IncomingMessage,STATUS_CODES,"
"METHODS,createServer,Server"},
{"node:net", "isIP,isIPv4,isIPv6,connect,createConnection,createServer,Socket,Server"},
{"node:tls", "connect,createServer,createSecureContext,TLSSocket,rootCertificates"},
{"node:diagnostics_channel", "channel,subscribe,unsubscribe,hasSubscribers,tracingChannel"},
{"node:module",
"createRequire,builtinModules,isBuiltin,syncBuiltinESMExports,register,"
"findSourceMap"},
{"node:url",
"URL,URLSearchParams,fileURLToPath,pathToFileURL,parse,format,resolve,"
"domainToASCII,domainToUnicode,urlToHttpOptions"},
{"node:buffer",
"Buffer,SlowBuffer,INSPECT_MAX_BYTES,kMaxLength,kStringMaxLength,constants,"
"isAscii,isUtf8,atob,btoa,Blob,File,transcode,resolveObjectURL"},
{"node:string_decoder", "StringDecoder"},
{"node:assert",
"AssertionError,ok,fail,equal,notEqual,strictEqual,notStrictEqual,"
"deepEqual,notDeepEqual,deepStrictEqual,notDeepStrictEqual,throws,"
"doesNotThrow,rejects,doesNotReject,match,doesNotMatch,ifError,strict"},
{"node:assert/strict",
"AssertionError,ok,fail,equal,notEqual,strictEqual,notStrictEqual,"
"deepEqual,notDeepEqual,deepStrictEqual,notDeepStrictEqual,throws,"
"doesNotThrow,rejects,doesNotReject,match,doesNotMatch,ifError,strict"},
{"node:domain", "create,createDomain,Domain,active"},
{"node:worker_threads",
"isMainThread,parentPort,threadId,workerData,resourceLimits,"
"MessageChannel,MessagePort,Worker,receiveMessageOnPort,SHARE_ENV,"
"markAsUntransferable,getEnvironmentData,setEnvironmentData"},
{"node:perf_hooks", "performance,PerformanceObserver,monitorEventLoopDelay,constants"},
{"node:v8",
"startupSnapshot,cachedDataVersionTag,getHeapStatistics,"
"getHeapSpaceStatistics,getHeapCodeStatistics,getCppHeapStatistics,"
"setFlagsFromString,takeCoverage,stopCoverage,setHeapSnapshotNearHeapLimit,"
"serialize,deserialize,writeHeapSnapshot,getHeapSnapshot,queryObjects,"
"startCpuProfile,isStringOneByteRepresentation,promiseHooks,"
"Serializer,Deserializer,DefaultSerializer,DefaultDeserializer,GCProfiler"},
{"node:dns",
"lookup,lookupService,resolve,resolve4,resolve6,resolveCname,resolveMx,"
"resolveNs,resolveSrv,resolveTxt,reverse,getServers,setServers,Resolver,"
"promises,ADDRCONFIG,V4MAPPED,ALL"},
{"node:async_hooks",
"AsyncLocalStorage,AsyncResource,executionAsyncId,triggerAsyncId,"
"executionAsyncResource,createHook"},
{"node:punycode", "version,ucs2,decode,encode,toASCII,toUnicode"},
{"node:querystring", "parse,stringify,decode,encode,escape,unescape,unescapeBuffer"},
{"node:constants", "F_OK,R_OK,W_OK,X_OK"},
{"node:console", "Console,log,info,debug,warn,error,trace,dir,assert,count,countReset,time,timeEnd,group,groupEnd,table,clear"},
{"node:timers", "setTimeout,clearTimeout,setInterval,clearInterval,setImmediate,clearImmediate"},
{"node:timers/promises", "setTimeout,setImmediate,setInterval,scheduler"},
{"node:zlib",
"deflateSync,inflateSync,deflateRawSync,inflateRawSync,gzipSync,"
"gunzipSync,unzipSync,deflate,inflate,deflateRaw,inflateRaw,gzip,gunzip,"
"unzip,Deflate,Inflate,DeflateRaw,InflateRaw,Gzip,Gunzip,Unzip,"
"BrotliCompress,BrotliDecompress,createDeflate,createInflate,"
"createDeflateRaw,createInflateRaw,createGzip,createGunzip,createUnzip,"
"createBrotliCompress,createBrotliDecompress,brotliCompressSync,"
"brotliDecompressSync,constants"},
{"node:readline",
"Interface,createInterface,clearLine,clearScreenDown,cursorTo,moveCursor,"
"emitKeypressEvents"},
{"node:stream",
"Stream,Readable,Writable,Duplex,Transform,PassThrough,pipeline,finished,"
"addAbortSignal,promises,isErrored,isDestroyed,isReadable,isWritable"},
{"node:stream/promises", "pipeline,finished"},
{"node:stream/consumers", "text,buffer,arrayBuffer,json,blob"},
{"node:stream/web",
"ReadableStream,WritableStream,TransformStream,TextEncoderStream,"
"TextDecoderStream,CountQueuingStrategy,ByteLengthQueuingStrategy,"
"ReadableStreamDefaultReader,ReadableStreamDefaultController,"
"WritableStreamDefaultWriter"},
{"node:crypto",
"createHash,createHmac,hash,Hash,Hmac,randomBytes,randomFillSync,randomFill,"
"randomInt,randomUUID,getRandomValues,timingSafeEqual,pbkdf2,pbkdf2Sync,"
"getHashes,getCiphers,getCurves,webcrypto,subtle,constants,KeyObject,"
"createCipheriv,createDecipheriv,createSign,createVerify,"
"createDiffieHellman,createECDH,createPublicKey,createPrivateKey,"
"createSecretKey,diffieHellman,generateKeyPair,generateKeyPairSync,"
"generateKey,generateKeySync,sign,verify,publicEncrypt,publicDecrypt,"
"privateEncrypt,privateDecrypt,scrypt,scryptSync,hkdf,hkdfSync,"
"X509Certificate,Certificate,checkPrime,checkPrimeSync,generatePrime,"
"generatePrimeSync,secureHeapUsed,setEngine,setFips,getFips"},
{"node:util",
"format,formatWithOptions,inspect,inherits,promisify,callbackify,deprecate,"
"debuglog,debug,types,isDeepStrictEqual,stripVTControlCharacters,styleText,"
"parseArgs,toUSVString,_extend,TextEncoder,TextDecoder,isArray"},
{"node:util/types",
"isAnyArrayBuffer,isArrayBufferView,isArgumentsObject,isArrayBuffer,"
"isAsyncFunction,isBigInt64Array,isBigUint64Array,isBooleanObject,"
"isBoxedPrimitive,isBigIntObject,isCryptoKey,isDataView,isDate,isExternal,"
"isFloat16Array,isFloat32Array,isFloat64Array,isGeneratorFunction,"
"isGeneratorObject,isInt8Array,isInt16Array,isInt32Array,isKeyObject,isMap,"
"isMapIterator,isModuleNamespaceObject,isNativeError,isNumberObject,"
"isPromise,isProxy,isRegExp,isSet,isSetIterator,isSharedArrayBuffer,"
"isStringObject,isSymbolObject,isTypedArray,isUint8Array,"
"isUint8ClampedArray,isUint16Array,isUint32Array,isWeakMap,isWeakSet"},
};
* {…} = m;` — the shims themselves live in the bootstrap below. The
* implementation-owned manifest generates this exact registry, so the
* runtime and compatibility ledger cannot drift. */
#include "scr_island_manifest.h"
static JSModuleDef *isl_module_load(JSContext *ctx, const char *name, void *opaque) {
(void)opaque;
@@ -6536,18 +6401,12 @@ static const char isl_modules_bootstrap[] =
" c.default = c;\n"
" return c;\n"
" });\n"
/* stream/web re-exports the web prelude's classes; names the
* prelude does not carry stay undefined (honest absence). */
/* stream/web re-exports the web prelude's implemented classes. */
" builtins['stream/web'] = memo(() => {\n"
" const g = globalThis;\n"
" const w = {\n"
" ReadableStream: g.ReadableStream, WritableStream: g.WritableStream,\n"
" TransformStream: g.TransformStream, TextEncoderStream: g.TextEncoderStream,\n"
" ReadableStream: g.ReadableStream, TransformStream: g.TransformStream,\n"
" TextDecoderStream: g.TextDecoderStream,\n"
" CountQueuingStrategy: g.CountQueuingStrategy, ByteLengthQueuingStrategy: g.ByteLengthQueuingStrategy,\n"
" ReadableStreamDefaultReader: g.ReadableStreamDefaultReader,\n"
" ReadableStreamDefaultController: g.ReadableStreamDefaultController,\n"
" WritableStreamDefaultWriter: g.WritableStreamDefaultWriter,\n"
" };\n"
" w.default = w;\n"
" return w;\n"
+142
View File
@@ -0,0 +1,142 @@
/* Generated by @internal/compatibility from dynamic-support.json.
* Do not edit: the manifest is the implementation-owned builtin registry. */
static const struct {
const char *name;
const char *exports;
} isl_builtins[] = {
{"node:events",
"EventEmitter,once,on,listenerCount,getEventListeners,setMaxListeners,defaultMaxListeners"
",errorMonitor,captureRejectionSymbol"},
{"node:path",
"sep,delimiter,basename,dirname,extname,join,resolve,normalize,relative,isAbsolute,toName"
"spacedPath,parse,format,posix,win32"},
{"node:path/posix",
"sep,delimiter,basename,dirname,extname,join,resolve,normalize,relative,isAbsolute,toName"
"spacedPath,parse,format,posix,win32"},
{"node:path/win32",
"sep,delimiter,basename,dirname,extname,join,resolve,normalize,relative,isAbsolute,toName"
"spacedPath,parse,format,posix,win32"},
{"node:process",
"argv,env,platform,execPath,execArgv,version,versions,stdout,stderr,stdin,cwd,exit,nextTi"
"ck,hrtime,pid,ppid,title,argv0,release,config,allowedNodeEnvironmentFlags,emitWarning,up"
"time,memoryUsage,umask,exitCode,on,once,off,removeListener,emit"},
{"node:fs",
"readFileSync,writeFileSync,appendFileSync,existsSync,realpathSync,mkdirSync,rmSync,rmdir"
"Sync,unlinkSync,readdirSync,statSync,lstatSync,accessSync,mkdtempSync,chmodSync,copyFile"
"Sync,renameSync,constants,Stats,Dirent,promises,readFile,writeFile,appendFile,exists,rea"
"lpath,mkdir,rm,rmdir,unlink,readdir,stat,lstat,access,mkdtemp,chmod,copyFile,rename,read"
"link,readlinkSync,createReadStream,createWriteStream,watch,watchFile,unwatchFile,openSyn"
"c,closeSync,readSync,read,open"},
{"node:fs/promises",
"readFile,writeFile,appendFile,realpath,mkdir,rm,rmdir,unlink,readdir,stat,lstat,access,m"
"kdtemp,chmod,copyFile,rename,readlink,constants,open"},
{"node:child_process",
"spawn,spawnSync,exec,execSync,execFile,execFileSync,fork"},
{"node:os",
"EOL,platform,arch,hostname,homedir,tmpdir,type,endianness,userInfo,release,version,machi"
"ne,cpus,availableParallelism,totalmem,freemem,loadavg,uptime,networkInterfaces,constants"},
{"node:tty",
"isatty,ReadStream,WriteStream"},
{"node:http",
"request,get,Agent,globalAgent,ClientRequest,IncomingMessage,STATUS_CODES,METHODS,createS"
"erver,Server"},
{"node:https",
"request,get,Agent,globalAgent,ClientRequest,IncomingMessage,STATUS_CODES,METHODS,createS"
"erver,Server"},
{"node:net",
"isIP,isIPv4,isIPv6,connect,createConnection,createServer,Socket,Server"},
{"node:tls",
"connect,createServer,createSecureContext,TLSSocket,rootCertificates"},
{"node:diagnostics_channel",
"channel,subscribe,unsubscribe,hasSubscribers,tracingChannel"},
{"node:module",
"createRequire,builtinModules,isBuiltin,syncBuiltinESMExports,register,findSourceMap"},
{"node:url",
"URL,URLSearchParams,fileURLToPath,pathToFileURL,parse,format,resolve,domainToASCII,domai"
"nToUnicode,urlToHttpOptions"},
{"node:buffer",
"Buffer,SlowBuffer,INSPECT_MAX_BYTES,kMaxLength,kStringMaxLength,constants,isAscii,isUtf8"
",atob,btoa,Blob,File,transcode,resolveObjectURL"},
{"node:string_decoder",
"StringDecoder"},
{"node:assert",
"AssertionError,ok,fail,equal,notEqual,strictEqual,notStrictEqual,deepEqual,notDeepEqual,"
"deepStrictEqual,notDeepStrictEqual,throws,doesNotThrow,rejects,doesNotReject,match,doesN"
"otMatch,ifError,strict"},
{"node:assert/strict",
"AssertionError,ok,fail,equal,notEqual,strictEqual,notStrictEqual,deepEqual,notDeepEqual,"
"deepStrictEqual,notDeepStrictEqual,throws,doesNotThrow,rejects,doesNotReject,match,doesN"
"otMatch,ifError,strict"},
{"node:domain",
"create,createDomain,Domain,active"},
{"node:worker_threads",
"isMainThread,parentPort,threadId,workerData,resourceLimits,MessageChannel,MessagePort,Wo"
"rker,receiveMessageOnPort,SHARE_ENV,markAsUntransferable,getEnvironmentData,setEnvironme"
"ntData"},
{"node:perf_hooks",
"performance,PerformanceObserver,monitorEventLoopDelay,constants"},
{"node:v8",
"startupSnapshot,cachedDataVersionTag,getHeapStatistics,getHeapSpaceStatistics,getHeapCod"
"eStatistics,getCppHeapStatistics,setFlagsFromString,takeCoverage,stopCoverage,setHeapSna"
"pshotNearHeapLimit,serialize,deserialize,writeHeapSnapshot,getHeapSnapshot,queryObjects,"
"startCpuProfile,isStringOneByteRepresentation,promiseHooks,Serializer,Deserializer,Defau"
"ltSerializer,DefaultDeserializer,GCProfiler"},
{"node:dns",
"lookup,lookupService,resolve,resolve4,resolve6,resolveCname,resolveMx,resolveNs,resolveS"
"rv,resolveTxt,reverse,getServers,setServers,Resolver,promises,ADDRCONFIG,V4MAPPED,ALL"},
{"node:async_hooks",
"AsyncLocalStorage,AsyncResource,executionAsyncId,triggerAsyncId,executionAsyncResource,c"
"reateHook"},
{"node:punycode",
"version,ucs2,decode,encode,toASCII,toUnicode"},
{"node:querystring",
"parse,stringify,decode,encode,escape,unescape,unescapeBuffer"},
{"node:constants",
"F_OK,R_OK,W_OK,X_OK"},
{"node:console",
"Console,log,info,debug,warn,error,trace,dir,assert,count,countReset,time,timeEnd,group,g"
"roupEnd,table,clear"},
{"node:timers",
"setTimeout,clearTimeout,setInterval,clearInterval,setImmediate,clearImmediate"},
{"node:timers/promises",
"setTimeout,setImmediate,setInterval,scheduler"},
{"node:zlib",
"deflateSync,inflateSync,deflateRawSync,inflateRawSync,gzipSync,gunzipSync,unzipSync,defl"
"ate,inflate,deflateRaw,inflateRaw,gzip,gunzip,unzip,Deflate,Inflate,DeflateRaw,InflateRa"
"w,Gzip,Gunzip,Unzip,BrotliCompress,BrotliDecompress,createDeflate,createInflate,createDe"
"flateRaw,createInflateRaw,createGzip,createGunzip,createUnzip,createBrotliCompress,creat"
"eBrotliDecompress,brotliCompressSync,brotliDecompressSync,constants"},
{"node:readline",
"Interface,createInterface,clearLine,clearScreenDown,cursorTo,moveCursor,emitKeypressEven"
"ts"},
{"node:stream",
"Stream,Readable,Writable,Duplex,Transform,PassThrough,pipeline,finished,addAbortSignal,p"
"romises,isErrored,isDestroyed,isReadable,isWritable"},
{"node:stream/promises",
"pipeline,finished"},
{"node:stream/consumers",
"text,buffer,arrayBuffer,json,blob"},
{"node:stream/web",
"ReadableStream,TransformStream,TextDecoderStream"},
{"node:crypto",
"createHash,createHmac,hash,Hash,Hmac,randomBytes,randomFillSync,randomFill,randomInt,ran"
"domUUID,getRandomValues,timingSafeEqual,pbkdf2,pbkdf2Sync,getHashes,getCiphers,getCurves"
",webcrypto,subtle,constants,KeyObject,createCipheriv,createDecipheriv,createSign,createV"
"erify,createDiffieHellman,createECDH,createPublicKey,createPrivateKey,createSecretKey,di"
"ffieHellman,generateKeyPair,generateKeyPairSync,generateKey,generateKeySync,sign,verify,"
"publicEncrypt,publicDecrypt,privateEncrypt,privateDecrypt,scrypt,scryptSync,hkdf,hkdfSyn"
"c,X509Certificate,Certificate,checkPrime,checkPrimeSync,generatePrime,generatePrimeSync,"
"secureHeapUsed,setEngine,setFips,getFips"},
{"node:util",
"format,formatWithOptions,inspect,inherits,promisify,callbackify,deprecate,debuglog,debug"
",types,isDeepStrictEqual,stripVTControlCharacters,styleText,parseArgs,toUSVString,_exten"
"d,TextEncoder,TextDecoder,isArray"},
{"node:util/types",
"isAnyArrayBuffer,isArrayBufferView,isArgumentsObject,isArrayBuffer,isAsyncFunction,isBig"
"Int64Array,isBigUint64Array,isBooleanObject,isBoxedPrimitive,isBigIntObject,isCryptoKey,"
"isDataView,isDate,isExternal,isFloat16Array,isFloat32Array,isFloat64Array,isGeneratorFun"
"ction,isGeneratorObject,isInt8Array,isInt16Array,isInt32Array,isKeyObject,isMap,isMapIte"
"rator,isModuleNamespaceObject,isNativeError,isNumberObject,isPromise,isProxy,isRegExp,is"
"Set,isSetIterator,isSharedArrayBuffer,isStringObject,isSymbolObject,isTypedArray,isUint8"
"Array,isUint8ClampedArray,isUint16Array,isUint32Array,isWeakMap,isWeakSet"},
};
+2
View File
@@ -33,6 +33,8 @@ importers:
specifier: ^3.0.0
version: 3.2.7(@edge-runtime/vm@3.2.0)(@types/node@24.13.3)(tsx@4.23.0)
internal/compatibility: {}
packages/cli:
dependencies:
'@scriptc/compiler':
+1
View File
@@ -1,5 +1,6 @@
packages:
- "packages/*"
- "internal/*"
minimumReleaseAgeExclude:
- "vercel@58.1.0"
allowBuilds:
@@ -0,0 +1,7 @@
// @dynamic
// This is a scriptc-only probe: Node's node:stream/web has more exports than
// the deliberately bounded island shim, so it is not part of npm's
// byte-for-byte differential corpus.
import { modulePresence } from "webstream";
console.log(modulePresence());
+1
View File
@@ -1 +1,2 @@
export declare function run(): void;
export declare function modulePresence(): string;
+40
View File
@@ -8,8 +8,48 @@
// drains at loop quiescence, like Node's microtasks.
const out = (line) => process.stdout.write(line + "\n");
// This probe intentionally uses the private loader hook from inside an
// embedded npm module. Standalone islands do not install the hook because
// they have no module graph to load.
export function modulePresence() {
const m = globalThis.__scr_require("node:stream/web");
return [
typeof m.ReadableStream,
typeof m.TransformStream,
typeof m.TextDecoderStream,
typeof URL,
typeof Buffer,
typeof setImmediate,
typeof clearImmediate,
"WritableStream" in m,
"TextEncoderStream" in m,
"CountQueuingStrategy" in m,
"ByteLengthQueuingStrategy" in m,
"ReadableStreamDefaultReader" in m,
"ReadableStreamDefaultController" in m,
"WritableStreamDefaultWriter" in m,
].join(" ");
}
import {
ReadableStream as ModuleReadableStream,
TransformStream as ModuleTransformStream,
TextDecoderStream as ModuleTextDecoderStream,
} from "node:stream/web";
export function run() {
(async () => {
// node:stream/web's implemented named exports are the same constructors
// as the corresponding web globals in both runtimes.
out("module-stream " + [
typeof ModuleReadableStream,
typeof ModuleTransformStream,
typeof ModuleTextDecoderStream,
ModuleReadableStream === ReadableStream,
ModuleTransformStream === TransformStream,
ModuleTextDecoderStream === TextDecoderStream,
].join(" "));
// push source + async iteration
{
const rs = new ReadableStream({
+30
View File
@@ -0,0 +1,30 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
const repoRoot = join(import.meta.dirname, "../..");
const snapshot = JSON.parse(
readFileSync(join(repoRoot, "internal/compatibility/generated/node-v24-internal.json"), "utf8"),
) as {
rows: Array<{
chapter: string;
apiSymbol: string;
static?: { evidence?: string };
}>;
};
describe("Node compatibility generator", () => {
test("does not attribute bare fs APIs to fs/promises lowering", () => {
const bareFsRows = snapshot.rows.filter((row) => row.chapter === "fs" && row.apiSymbol.startsWith("fs."));
expect(bareFsRows.length).toBeGreaterThan(0);
expect(bareFsRows.every((row) => !row.static?.evidence?.includes("fs.promises"))).toBe(true);
});
test("keeps fsPromises APIs attributed to fs/promises lowering", () => {
const promiseRows = snapshot.rows.filter((row) => row.chapter === "fs" && row.apiSymbol.startsWith("fsPromises."));
expect(promiseRows.length).toBeGreaterThan(0);
expect(promiseRows.some((row) => row.static?.evidence?.includes("node-builtin.fs.promises"))).toBe(true);
});
});
+47 -6
View File
@@ -19,6 +19,7 @@ import { compile } from "@scriptc/compiler";
const execFileAsync = promisify(execFile);
const repoRoot = join(import.meta.dirname, "../..");
const fixturesRoot = join(repoRoot, "tests/fixtures");
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests");
const sanitize = process.env["SCRIPTC_SAN"] === "1";
@@ -66,6 +67,32 @@ async function compileAndRun(name: string, source: string): Promise<RunResult> {
}
}
/** Compiles and runs a fixture whose imports must be embedded from npm. */
async function compileFixtureAndRun(name: string, entry: string): Promise<RunResult> {
const outDir = join(cacheDir, `web-${name}`);
mkdirSync(outDir, { recursive: true });
const result = await compile(entry, {
outPath: join(outDir, name),
outDir,
sanitize,
dynamic: true,
});
if (!result.ok) {
throw new Error(
"web-globals fixture failed to compile:\n" +
result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"),
);
}
try {
const { stdout, stderr } = await execFileAsync(result.binaryPath, [], { encoding: "utf8" });
return { stdout, stderr, exitCode: 0 };
} catch (err) {
const e = err as { code?: unknown; stdout?: string; stderr?: string };
if (typeof e.code !== "number") throw err;
return { stdout: e.stdout ?? "", stderr: e.stderr ?? "", exitCode: e.code };
}
}
/** Evaluates island code that must produce a one-line string. */
async function islandEval(name: string, js: string): Promise<string> {
const r = await compileAndRun(name, `console.log(__island_eval(${JSON.stringify(js)}));\n`);
@@ -83,8 +110,10 @@ describe(`island web globals (scriptc-only${sanitize ? ", sanitized" : ""})`, ()
typeof TextDecoder, typeof TextDecoderStream, typeof URLSearchParams,
typeof btoa, typeof atob, typeof Headers, typeof crypto, typeof console,
typeof Blob, typeof File, typeof Event, typeof EventTarget, typeof CustomEvent,
typeof MessageEvent,
typeof WritableStream, typeof structuredClone, typeof FormData,
typeof WebSocket,
typeof WebSocket, typeof URL, typeof Buffer, typeof setImmediate,
typeof clearImmediate,
].join(' ')`,
);
// Blob/File and the Event triple joined the subset when the vercel
@@ -92,12 +121,24 @@ describe(`island web globals (scriptc-only${sanitize ? ", sanitized" : ""})`, ()
// Event and buffer.Blob at LOAD); structuredClone joined with the
// globals lane (the HTML StructuredSerialize subset, cycles
// included); WritableStream/FormData/WebSocket stay fenced by
// absence.
// absence. URL, Buffer, and the immediate-timer globals are installed
// only when the embedded npm module bootstrap runs.
expect(out).toBe(
"function function function function function function " +
"function function function object object " +
"function function function function function " +
"undefined function undefined undefined",
"function function function function function function function function function " +
"object object function function function function function function " +
"undefined function undefined undefined undefined undefined undefined undefined",
);
});
test("node:stream/web exposes only the implemented module constructors", async () => {
const result = await compileFixtureAndRun(
"web-stream-module-presence",
join(fixturesRoot, "npm/divergent/web-stream-module-presence.ts"),
);
expect(result.exitCode).toBe(0);
expect(result.stderr).toBe("");
expect(result.stdout.trimEnd()).toBe(
"function function function function function function function false false false false false false false",
);
});