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
4 changes: 2 additions & 2 deletions .agents/docs/taffyjs-wasm-package.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The package is ESM-only, matching `@taffyjs/node`. A dedicated public CommonJS b

The package contains the threadless `wasm32-wasip1` artifact as one raw base64 string in a generated private JavaScript module. The npm tarball does not contain an independent `.wasm` file. Base64 increases the uncompressed payload size, but avoiding a separate binary asset is more important for this package and ordinary package compression still applies during transfer.

The base64 payload occurs exactly once in the package, even though the package retains browser and default public entries. The private Node and browser ESM adapters both import that generated no-TLA ESM payload module normally. The generated unscoped target package such as `taffyjs-binding-wasm32-wasip1` is a build-only staging artifact, not a public dependency or separately published TaffyJS package.
The base64 payload occurs exactly once in the package, even though the package retains browser and default public entries. The private Node and browser ESM adapters both import that generated no-TLA ESM payload module normally. The generated scoped target package such as `@taffyjs/binding-wasm32-wasip1` is a build-only staging artifact and an explicit exception to the normal scope boundary. It is not a public dependency or separately published TaffyJS package: the generator matches the napi-rs file-loading fallback as source text and removes that complete `require.resolve` branch when it inlines the artifact.

The package root uses conditional exports with one declaration surface, a browser entry, and a default Node.js entry. The intended shape is:

Expand Down Expand Up @@ -75,7 +75,7 @@ Both runtime chains use napi-rs's JavaScript WASI implementation without forward
- A Node.js ESM consumer must import `@taffyjs/wasm` and use the ordinary public Taffy API without initialization calls, top-level await in the selected package graph, or native platform packages.
- A bundled browser consumer must use the same import and API, include the inline payload without emitting a `.wasm` asset, and work without `SharedArrayBuffer`, cross-origin isolation, COOP, or COEP.
- The Node and browser builds must derive their JavaScript and TypeScript surface from the same authored source. Except for a documented and evidenced host-specific exception, every public Node binding behavior test must run unchanged against both `@taffyjs/node` and `@taffyjs/wasm`; one test configuration redirects the exact package import instead of copying test files.
- Package inspection must show exactly one base64 Wasm payload, no independent `.wasm` file, exactly one browser-side `WebAssembly.compile`, synchronous `instantiateNapiModuleSync` in the Node graph, no Node environment or filesystem-root WASI capabilities, no dependency on `taffyjs-binding-wasm32-wasip1`, no public initialization subpath, and no accidentally published raw binding entry.
- Package inspection must show exactly one base64 Wasm payload, no independent `.wasm` file, exactly one browser-side `WebAssembly.compile`, synchronous `instantiateNapiModuleSync` in the Node graph, no Node environment or filesystem-root WASI capabilities, no retained `require.resolve` or dependency on `@taffyjs/binding-wasm32-wasip1`, no public initialization subpath, and no accidentally published raw binding entry.
- Generated napi-rs artifacts and all derived package modules must be reproducible from the pinned toolchain and repository generator. The napi-rs deferred loader must not contain maintained TaffyJS edits, and the Node adapter transformation must reject unexpected upstream template changes.

## Evidence
Expand Down
10 changes: 5 additions & 5 deletions .agents/docs/tooling-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ This ledger records only tooling judgments that Yunfei explicitly expressed for

## Decided

### The @taffyjs scope is reserved for published packages
### @taffyjs scope boundary

**Ruling:** A package name may use the `@taffyjs` npm scope only when that package is intended to be published separately under the organization. Repository-only workspaces and generated or staged build-only packages must use unscoped names.
**Ruling:** A package name may use the `@taffyjs` npm scope only when that package is intended to be published separately under the organization, with one explicit build-only exception: the napi-rs generated Wasm target used to assemble inline `@taffyjs/wasm` output also retains the scope. Other repository-only workspaces and generated or staged packages must use unscoped names.

**Limits:** Separately published implementation artifacts qualify even when consumers should not import them directly. The native platform packages used as `@taffyjs/node` optional dependencies therefore remain scoped because they must be published with the public package, while the generated Wasm target package is unscoped because it is only a staging input. A temporary `private` field or missing publication automation during repository bootstrap does not by itself override the intended distribution boundary.
**Limits:** Separately published implementation artifacts qualify even when consumers should not import them directly. The native platform packages used as `@taffyjs/node` optional dependencies therefore remain scoped because they must be published with the public package. The Wasm exception applies only to its generated staging identity: the scoped package fallback appears in napi-rs template source but is removed when the binary is inlined, so the final package must retain neither `require.resolve` nor a binding package dependency. A temporary `private` field or missing publication automation during repository bootstrap does not by itself override the intended distribution boundary.

**Why:** Yunfei reserved the organization scope for packages that will actually be published; no additional rationale was given.
**Why:** Yunfei reserved the organization scope for packages that will actually be published, then explicitly kept the inline Wasm staging target scoped as a special case; no additional rationale was given.

**Source:** Yunfei (`@hyfdev`), 2026-08-16; asked to generalize the test-package correction so every package that is not truly published avoids the `@taffyjs` scope.
**Source:** Yunfei (`@hyfdev`), 2026-08-16; asked to generalize the test-package correction so every package that is not truly published avoids the `@taffyjs` scope, then corrected the Wasm staging target as an explicit scoped exception because the public Wasm package is inline.

### Direct names for top-level test packages

Expand Down
2 changes: 1 addition & 1 deletion packages/taffyjs-wasm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
},
"napi": {
"binaryName": "taffyjs",
"packageName": "taffyjs-binding",
"packageName": "@taffyjs/binding",
"targets": [
"wasm32-wasip1"
],
Expand Down
13 changes: 11 additions & 2 deletions tests/taffyjs-wasm/tests/package-contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,24 @@ assert.equal(nodeGraph.includes("WebAssembly.compile("), false);
assert.equal(/\bawait\b/.test(nodeGraph), false);
assert.equal(nodeGraph.includes("instantiateNapiModuleSync"), true);
assert.equal(nodeGraph.includes("initial: 4000"), true);
assert.equal(nodeGraph.includes("require.resolve("), false);
assert.equal(nodeGraph.includes("@taffyjs/binding-wasm"), false);
assert.equal(browserAdapter.includes("await WebAssembly.compile("), true);
assert.equal(browserAdapter.includes("await instantiate("), true);
assert.equal(deferredLoader.includes("initial: 1024"), true);

const manifest = await import("@taffyjs/wasm/package.json", { with: { type: "json" } });
assert.deepEqual(Object.keys(manifest.default.exports).sort(), [".", "./package.json"]);
assert.equal(manifest.default.napi.packageName, "taffyjs-binding");
assert.equal(manifest.default.napi.packageName, "@taffyjs/binding");
const dependencyNames = Object.entries(manifest.default).flatMap(([key, dependencies]) => {
const isDependencyMap = key === "dependencies" || key.endsWith("Dependencies");
if (!isDependencyMap || typeof dependencies !== "object" || dependencies === null) {
return [];
}
return Object.keys(dependencies);
});
assert.equal(
Object.keys(manifest.default.dependencies).some((name) => name.includes("binding-wasm")),
dependencyNames.some((name) => name.includes("binding-wasm")),
false,
);

Expand Down
7 changes: 4 additions & 3 deletions tools/taffy-wasm/generate-inline-wasm-runtime-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ const __wasi = new __nodeWASI({
})`,
"Node WASI capability block and factory boundary",
);
// This is the exact file-loading fallback emitted by napi-rs. The replacement removes the
// whole block, so neither require.resolve nor a binding package dependency reaches dist.
nodeLoaderSource = replaceExactly(
nodeLoaderSource,
`let __wasmFilePath = __nodePath.join(__dirname, 'taffyjs.wasm32-wasip1.wasm')
Expand All @@ -91,14 +93,14 @@ const __wasmDebugFilePath = __nodePath.join(__dirname, 'taffyjs.wasm32-wasip1.de
if (__nodeFs.existsSync(__wasmDebugFilePath)) {
__wasmFilePath = __wasmDebugFilePath
} else if (!__nodeFs.existsSync(__wasmFilePath)) {
const __wasiPackageEntry = require.resolve('taffyjs-binding-wasm32-wasip1')
const __wasiPackageEntry = require.resolve('@taffyjs/binding-wasm32-wasip1')
const __packagedWasmFilePath = __nodePath.join(
__nodePath.dirname(__wasiPackageEntry),
'taffyjs.wasm32-wasip1.wasm',
)
if (!__nodeFs.existsSync(__packagedWasmFilePath)) {
throw new Error(
'taffyjs-binding-wasm32-wasip1 is installed but is missing taffyjs.wasm32-wasip1.wasm.',
'@taffyjs/binding-wasm32-wasip1 is installed but is missing taffyjs.wasm32-wasip1.wasm.',
)
}
__wasmFilePath = __packagedWasmFilePath
Expand All @@ -124,7 +126,6 @@ for (const forbidden of [
"preopens",
"__wasmFilePath",
"@taffyjs/binding-wasm",
"taffyjs-binding-wasm",
"require('./taffyjs.wasm-base64",
]) {
if (nodeLoaderSource.includes(forbidden)) {
Expand Down