Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .agents/docs/technology-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.

Expand All @@ -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.

Expand Down
12 changes: 12 additions & 0 deletions .agents/docs/tooling-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
6 changes: 3 additions & 3 deletions packages/taffyjs-node/binding.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 3 additions & 3 deletions packages/taffyjs-node/index.js
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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;
}
Expand Down
29 changes: 29 additions & 0 deletions tests/taffyjs-node/tests/package/node-builtin-specifiers.test.mts
Original file line number Diff line number Diff line change
@@ -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",
);
Comment on lines +14 to +17

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(", ")}`,
);
});
35 changes: 35 additions & 0 deletions tools/taffy-node/normalize-node-builtin-specifiers.ts
Original file line number Diff line number Diff line change
@@ -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);
11 changes: 9 additions & 2 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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",
Expand Down