mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { NodeCompatibilityMatrix } from "@/components/node-compatibility-matrix";
|
||||
|
||||
export default function CompatibilityPage() {
|
||||
return <NodeCompatibilityMatrix />;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { permanentRedirect } from "next/navigation";
|
||||
|
||||
export default function LegacyNodeCompatibilityPage() {
|
||||
permanentRedirect("/compatibility");
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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",
|
||||
};
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)`);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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"] }
|
||||
]
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"},
|
||||
};
|
||||
Generated
+2
@@ -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,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
@@ -1 +1,2 @@
|
||||
export declare function run(): void;
|
||||
export declare function modulePresence(): string;
|
||||
|
||||
+40
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user