diff --git a/.agents/docs/technology-stack.md b/.agents/docs/technology-stack.md index d888c92..7d58285 100644 --- a/.agents/docs/technology-stack.md +++ b/.agents/docs/technology-stack.md @@ -2,7 +2,7 @@ ## Native bindings and distribution -napi-rs owns the Rust-to-Node boundary, private native declarations, native loader, and target-specific package metadata. It generates an ESM loader, which Vite+ bundles into the ESM public entry without maintaining a custom loader. +napi-rs owns the Rust-to-Node boundary, private native declarations, native loader, and target-specific package metadata. It generates an ESM loader, which Vite+ bundles into the ESM public entry without maintaining a custom loader. The pinned napi-rs template deliberately emits bare Node builtin specifiers to retain Node 12 compatibility; TaffyJS targets Node 22.18 or newer, so the repository mechanically normalizes those generated specifiers to the explicit `node:` protocol before formatting and bundling the loader. The generated ESM loader is a private build input. The authored wrapper imports it privately, and the bundled public entry does not re-export raw native operations. @@ -18,7 +18,7 @@ At the current pins, napi-rs 3.8.2 requires emnapi 2.0.0-alpha.3, but its genera ## JavaScript package builds -Vite+ is the JavaScript toolchain. The root task graph builds the `@taffyjs/node` napi-rs binding, formats its generated loader and declaration, then lets platform-artifact synchronization and `vp pack` run as independent final branches. `vp pack` compiles the public source in `packages/taffyjs-node/src`, bundles the private napi-rs loader, and emits `index.js` and `index.d.ts`, including public types and JSDoc. The private native declaration remains a generated build input rather than a published file; package metadata does not duplicate the repository build pipeline as a compound script. +Vite+ is the JavaScript toolchain. The root task graph builds the `@taffyjs/node` napi-rs binding, normalizes Node builtin module specifiers in its generated loader, formats the loader and declaration, then lets platform-artifact synchronization and `vp pack` run as independent final branches. `vp pack` compiles the public source in `packages/taffyjs-node/src`, bundles the private napi-rs loader, and emits `index.js` and `index.d.ts`, including public types and JSDoc. The private native declaration remains a generated build input rather than a published file; package metadata does not duplicate the repository build pipeline as a compound script. The `@taffyjs/wasm` Vite+ pack configuration compiles that same entry twice. A filtered build-time resolver redirects only the resolved private binding import to `./taffyjs.node.js` for Node and `./taffyjs.browser.js` for browsers, leaving those generated adapters external. Both adapters reference `./taffyjs.wasm-base64.js`, so the base64 literal occurs once in the package. The Node compilation emits the one public declaration bundle; the browser compilation does not maintain a second declaration source. @@ -42,7 +42,7 @@ The interactive Getting Started example imports the real browser entry from `@ta ## Task orchestration -The root `vite.config.ts` defines build and verification dependencies. The native build is an explicit graph from napi-rs binding generation through generated-file formatting to the independent platform-artifact and Vite+ entry branches. The Wasm build is a three-stage chain—staged napi-rs binding, Vite+ public entries, then final inline runtime files. Native build completion precedes package-local and consumer tests. Rust tests are part of the full test task, while Rust formatting and Clippy remain separate checks. Node formatting and linting do not depend on a native build. Generated-source drift is checked separately in CI and is not part of the default local `check` or `ready` graph. +The root `vite.config.ts` defines build and verification dependencies. The native build is an explicit graph from napi-rs binding generation through Node builtin specifier normalization and generated-file formatting to the independent platform-artifact and Vite+ entry branches. The Wasm build is a three-stage chain—staged napi-rs binding, Vite+ public entries, then final inline runtime files. Native build completion precedes package-local and consumer tests. Rust tests are part of the full test task, while Rust formatting and Clippy remain separate checks. Node formatting and linting do not depend on a native build. Generated-source drift is checked separately in CI and is not part of the default local `check` or `ready` graph. Task caching is disabled at the root. This keeps native artifacts and runtime tests from being skipped or restored from stale task outputs until the project makes a new explicit caching decision. diff --git a/.agents/docs/tooling-decisions.md b/.agents/docs/tooling-decisions.md index 272c8d7..037bd65 100644 --- a/.agents/docs/tooling-decisions.md +++ b/.agents/docs/tooling-decisions.md @@ -38,6 +38,18 @@ This ledger records only tooling judgments that Yunfei explicitly expressed for **Source:** Yunfei (`@hyfdev`), 2026-08-16; explicitly stated that everything should use TypeScript unless there is a special reason not to, then explicitly promoted and vouched this as a repository-wide PCR rule. +### Node builtin module protocol + +[VOUCHED @hyfdev 2026-08-16] + +**Ruling:** Every reference to a Node.js builtin module in repository code must use the explicit `node:` protocol by default, including imports, exports, dynamic imports, and `require` calls in authored source and generated or distributed JavaScript controlled by the repository build. + +**Limits:** This does not modify third-party packages or their source templates in place. When an upstream generator emits bare builtin specifiers for an older Node.js compatibility floor that TaffyJS does not support, the repository must normalize its generated artifact at the owned build boundary. Only a concrete supported-runtime requirement that cannot resolve `node:` may reopen this default. + +**Why:** A Deno compatibility check exposed the generated public entry's bare `from "module"` specifier, after which Yunfei required every Node builtin module reference to default to the `node:` protocol; no additional rationale was given. + +**Source:** Yunfei (`@hyfdev`), 2026-08-16; explicitly required repository code to default every Node builtin module reference to the `node:` protocol, asked to vouch the rule, and requested the implementation and pull request. + ### Vite+ owns JavaScript package builds [VOUCHED @hyfdev 2026-08-09] diff --git a/packages/taffyjs-node/binding.js b/packages/taffyjs-node/binding.js index 9539e8d..b24edf7 100644 --- a/packages/taffyjs-node/binding.js +++ b/packages/taffyjs-node/binding.js @@ -3,11 +3,11 @@ // @ts-nocheck /* auto-generated by NAPI-RS */ -import { createRequire } from 'module' +import { createRequire } from 'node:module' const require = createRequire(import.meta.url); const __dirname = new URL(".", import.meta.url).pathname; -const { readFileSync } = require("fs"); +const { readFileSync } = require("node:fs"); let nativeBinding = null; const loadErrors = []; @@ -57,7 +57,7 @@ const isMuslFromReport = () => { const isMuslFromChildProcess = () => { try { - return require("child_process") + return require("node:child_process") .execSync("ldd --version", { encoding: "utf8" }) .includes("musl"); } catch (e) { diff --git a/packages/taffyjs-node/index.js b/packages/taffyjs-node/index.js index 975defa..fbab752 100644 --- a/packages/taffyjs-node/index.js +++ b/packages/taffyjs-node/index.js @@ -1,4 +1,4 @@ -import { createRequire } from "module"; +import { createRequire } from "node:module"; //#region src/numeric-families.ts /** Lists the supported display choices as stable numeric constants. */ const Display = Object.freeze({ @@ -409,7 +409,7 @@ const AvailableSpace = Object.freeze({ //#region binding.js const require = createRequire(import.meta.url); new URL(".", import.meta.url).pathname; -const { readFileSync } = require("fs"); +const { readFileSync } = require("node:fs"); let nativeBinding = null; const loadErrors = []; const isMusl = () => { @@ -444,7 +444,7 @@ const isMuslFromReport = () => { }; const isMuslFromChildProcess = () => { try { - return require("child_process").execSync("ldd --version", { encoding: "utf8" }).includes("musl"); + return require("node:child_process").execSync("ldd --version", { encoding: "utf8" }).includes("musl"); } catch (e) { return false; } diff --git a/tests/taffyjs-node/tests/package/node-builtin-specifiers.test.mts b/tests/taffyjs-node/tests/package/node-builtin-specifiers.test.mts new file mode 100644 index 0000000..ac55835 --- /dev/null +++ b/tests/taffyjs-node/tests/package/node-builtin-specifiers.test.mts @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import { builtinModules } from "node:module"; +import { fileURLToPath } from "node:url"; + +import { test } from "vite-plus/test"; + +const bareBuiltinAlternatives = [ + ...new Set(builtinModules.map((specifier) => specifier.replace(/^node:/, ""))), +] + .sort((left, right) => right.length - left.length) + .map((specifier) => specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")) + .join("|"); +const bareBuiltinReference = new RegExp( + String.raw`(?:\bfrom\s+|\bimport\s+|\brequire(?:\.resolve)?\s*\(\s*|\bimport\s*\(\s*)(["'])(${bareBuiltinAlternatives})\1`, + "g", +); + +test("public entry uses the node protocol for built-in modules", async () => { + const entryPath = fileURLToPath(import.meta.resolve("@taffyjs/node")); + const source = await readFile(entryPath, "utf8"); + const bareReferences = [...source.matchAll(bareBuiltinReference)].map((match) => match[2]); + + assert.deepEqual( + bareReferences, + [], + `Expected node: prefixes for built-in modules, found: ${bareReferences.join(", ")}`, + ); +}); diff --git a/tools/taffy-node/normalize-node-builtin-specifiers.ts b/tools/taffy-node/normalize-node-builtin-specifiers.ts new file mode 100644 index 0000000..3f409aa --- /dev/null +++ b/tools/taffy-node/normalize-node-builtin-specifiers.ts @@ -0,0 +1,35 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { builtinModules } from "node:module"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const repositoryDirectory = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const bindingPath = resolve(repositoryDirectory, "packages/taffyjs-node/binding.js"); +const bareBuiltins = new Set(builtinModules.map((specifier) => specifier.replace(/^node:/, ""))); +const moduleReferencePatterns = [ + /(\bfrom\s*)(["'])([^"']+)\2/g, + /(\bimport\s*)(["'])([^"']+)\2/g, + /(\brequire(?:\.resolve)?\s*\(\s*)(["'])([^"']+)\2/g, + /(\bimport\s*\(\s*)(["'])([^"']+)\2/g, +]; + +let source = await readFile(bindingPath, "utf8"); +const originalSource = source; + +for (const pattern of moduleReferencePatterns) { + source = source.replace(pattern, (reference, prefix, quote, specifier: string) => { + if (!bareBuiltins.has(specifier)) return reference; + return `${prefix}${quote}node:${specifier}${quote}`; + }); +} + +for (const pattern of moduleReferencePatterns) { + for (const match of source.matchAll(pattern)) { + const specifier = match[3]; + if (specifier && bareBuiltins.has(specifier)) { + throw new Error(`Generated Node loader still uses bare builtin ${JSON.stringify(specifier)}`); + } + } +} + +if (source !== originalSource) await writeFile(bindingPath, source); diff --git a/vite.config.ts b/vite.config.ts index 459424f..a81b155 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -79,7 +79,10 @@ export default defineConfig({ "tests/taffyjs-wasm/browser/dist", ], jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], - rules: { "vite-plus/prefer-vite-plus-imports": "error" }, + rules: { + "unicorn/prefer-node-protocol": "error", + "vite-plus/prefer-vite-plus-imports": "error", + }, options: { typeAware: true, typeCheck: true }, }, run: { @@ -96,9 +99,13 @@ export default defineConfig({ command: "vp exec --filter @taffyjs/node -- napi build --manifest-path ../../crates/taffyjs_binding/Cargo.toml --package-json-path package.json --output-dir . --platform --js binding.js --dts binding.d.ts --esm --release -- --locked", }, + "build:node:normalize-builtins": { + command: "node tools/taffy-node/normalize-node-builtin-specifiers.ts", + dependsOn: ["build:node:binding"], + }, "build:node:format": { command: "vp exec --filter @taffyjs/node -- vp fmt binding.js binding.d.ts package.json", - dependsOn: ["build:node:binding"], + dependsOn: ["build:node:normalize-builtins"], }, "build:node:platform-artifact": { command: "node tools/sync-platform-artifact.ts",