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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/compiler/src/coverage/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,9 @@ export function renderCoverage(input: CoverageInput, opts: { color?: boolean; so
);
for (const b of builtins) {
const status = b.shimmed
? c(GREEN, "shimmed".padEnd(widestS))
? b.partial
? c(YELLOW, "partial".padEnd(widestS))
: c(GREEN, "shimmed".padEnd(widestS))
: b.lazy
? c(YELLOW, "not shimmed — lazy trap".padEnd(widestS))
: c(RED, "not shimmed".padEnd(widestS));
Expand All @@ -226,6 +228,11 @@ export function renderCoverage(input: CoverageInput, opts: { color?: boolean; so
c(DIM, `(${t.via.join("/")} in ${t.packages.join(", ")})`),
);
}
if (builtins.some((b) => b.partial)) {
out.push(
` ${c(DIM, "(partial: the shim exists but covers only part of Node's surface; unsupported members throw at the call)")}`,
);
}
if (builtins.some((b) => b.lazy) || traps.length > 0) {
out.push(
` ${c(DIM, "(lazy trap: only reachable through require()/import() boundaries — the build embeds Node's call-time error; the call throws at runtime)")}`,
Expand Down
42 changes: 42 additions & 0 deletions packages/compiler/src/frontend/npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ export interface NpmBuiltinUse {
builtin: string;
/** Whether the island ships a shim for it. */
shimmed: boolean;
/** Shimmed, but the island covers only PART of Node's surface: the
* implemented members work, the rest are throwing stubs. Coverage reports
* this so a partial shim is not indistinguishable from a complete one.
* Always false when not shimmed. */
partial: boolean;
/** Unshimmed AND only require()/import() edges reach it: the build
* embeds the island's lazy throw at the call instead of failing —
* Node's laziness for those edge kinds (Node itself would LOAD the
Expand Down Expand Up @@ -270,6 +275,42 @@ const SHIMMED_BUILTINS = new Set([
"v8",
]);

/** Shimmed builtins whose island shim covers only PART of Node's surface:
* the members the shim carries work, the rest are honest throwing stubs
* (scr_island.c). `SHIMMED_BUILTINS` minus this set is the full-shim set,
* so coverage can tell a complete shim from a partial one without a
* per-function capability database. A partial shim still LOADS and runs
* programs that stay within its implemented slice, so it is not a blocker. */
const PARTIAL_SHIM_BUILTINS = new Set([
// Broad process plumbing with explicit fences: process.umask only supports
// its read form; module.register is unavailable; child_process is a
// load-only surface whose process-launching members all throw.
"process", "module", "child_process",
// Buffer is broadly implemented, but transcode remains unavailable.
"buffer",
// Whole-file reads/writes (readFile/writeFile/mkdir/stat/readdir/...);
// watch, open, and incremental read/write throw.
"fs", "fs/promises",
// The hashing/random/pbkdf2 slice; keys, ciphers, signing, and the rest
// throw at the call (the embedded runtime carries the hashing/random
// slice only).
"crypto",
// The stream classes and pipeline helpers work; the consumers submodule's
// Blob conversion remains an explicit fence.
"stream/consumers",
// deflate/gzip and the buffering stream classes; brotli and zstd throw.
"zlib",
// DNS has the loadable Node shape but every resolver call fences. The
// main-thread worker plumbing is real, while Worker construction throws.
"dns", "worker_threads",
// HTTP(S) implements the request/get client slice only. net/tls provide
// address/load plumbing for it, but their direct socket surfaces throw.
"http", "https", "net", "tls",
// Startup-snapshot/heap metadata is loadable; V8 serialization, heap
// snapshots, profiling, and promise hooks remain call-time fences.
"v8",
]);

/** Node builtins importable WITHOUT the "node:" prefix — used to tell
* "missing builtin shim" apart from "missing package" for bare specifiers.
* ("node:"-prefixed specifiers are always builtins: the prefix cannot name
Expand Down Expand Up @@ -971,6 +1012,7 @@ export class NpmGraphBuilder {
return {
builtin,
shimmed,
partial: shimmed && PARTIAL_SHIM_BUILTINS.has(builtin.slice(5)),
lazy: !shimmed && !this.builtinsEager.has(builtin),
packages: [...packages].sort(),
};
Expand Down
3 changes: 2 additions & 1 deletion tests/harness/__snapshots__/coverage-npm-lazy-builtin.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ scriptc coverage tests/fixtures/npm/cases/esbuild-require/main.ts

embedded npm code imports Node builtins:
node:http2 not shimmed — lazy trap (esbundled)
node:module shimmed (esbundled)
node:module partial (esbundled)
node:os shimmed (esbundled)
node:tty shimmed (esbundled)
(partial: the shim exists but covers only part of Node's surface; unsupported members throw at the call)
(lazy trap: only reachable through require()/import() boundaries — the build embeds Node's call-time error; the call throws at runtime)

builds with --dynamic — no remaining blockers (the island sites above run in the embedded engine).
11 changes: 11 additions & 0 deletions tests/harness/__snapshots__/coverage-npm-partial-builtin.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
scriptc coverage tests/fixtures/npm/cases/crypto-shims/main.ts

statements analyzed 3
compile statically 2 (66%)
compile dynamically 1 (33%) (island sites — the embedded engine runs them)

embedded npm code imports Node builtins:
node:crypto partial (cryptozoo)
(partial: the shim exists but covers only part of Node's surface; unsupported members throw at the call)

builds with --dynamic — no remaining blockers (the island sites above run in the embedded engine).
51 changes: 48 additions & 3 deletions tests/harness/coverage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,59 @@ test("lazy edges inventory: unresolvable require()/import() targets mark as lazy
test("lazy builtin edges mark in the builtins table, __require sites included", async () => {
// The esbuild-require fixture routes external requires through the
// bundle's __require helper — its literal call sites collect as require
// edges, so the builtins table lists node:os/node:tty (shimmed) and
// node:stream as a lazy trap (unshimmed, reached only by the
// never-called require) without failing the build.
// edges, so the builtins table lists node:module (partial), node:os/
// node:tty (shimmed), and node:http2 as a lazy trap (unshimmed, reached
// only by the never-called require) without failing the build.
await expect(
report(join(repoRoot, "tests/fixtures/npm/cases/esbuild-require/main.ts"), { dynamic: true }),
).toMatchFileSnapshot("__snapshots__/coverage-npm-lazy-builtin.txt");
});

test("a partial dynamic shim is reported as partial, not fully shimmed", async () => {
// The crypto-shims fixture imports node:crypto through cryptozoo. The
// island ships a crypto shim, but only the hashing/random/pbkdf2 slice;
// keys, ciphers, signing, and the rest throw at the call. The builtins
// table marks it "partial" with an explanatory note, distinct from a
// fully implemented shim (the esbuild-require snapshot pins that side).
await expect(
report(join(repoRoot, "tests/fixtures/npm/cases/crypto-shims/main.ts"), { dynamic: true }),
).toMatchFileSnapshot("__snapshots__/coverage-npm-partial-builtin.txt");
});

test("known call-time-fenced builtin shims are never reported as complete", () => {
const cases = [
[
join(repoRoot, "tests/fixtures/commander-calc/calc.ts"),
["node:child_process", "node:fs", "node:process"],
],
[
join(repoRoot, "tests/fixtures/npm/cases/island-web-plumbing/main.ts"),
["node:buffer", "node:dns", "node:module", "node:worker_threads"],
],
[
join(repoRoot, "tests/fixtures/npm/cases/misc-shims/main.ts"),
["node:v8"],
],
[
join(repoRoot, "tests/fixtures/npm/cases/stream-shims/main.ts"),
["node:stream/consumers"],
],
[
join(repoRoot, "tests/fixtures/fetch/cases/island-http/main.ts"),
["node:http"],
],
] as const;

for (const [entry, builtins] of cases) {
const lines = report(entry, { dynamic: true }).split("\n");
for (const builtin of builtins) {
const row = lines.find((line) => line.trimStart().startsWith(`${builtin} `));
expect(row, `${builtin} coverage row for ${entry}`).toBeDefined();
expect(row!.trim().split(/\s+/).slice(0, 2)).toEqual([builtin, "partial"]);
}
}
});

test("import fences no longer stop analysis: percentage plus module blockers", async () => {
// The fenced module reports ONE grouped blocker (the import line plus
// every use of its bindings carry the same message); the rest of the
Expand Down