diff --git a/.agents/docs/taffyjs-wasm-package.md b/.agents/docs/taffyjs-wasm-package.md index fc87bac..49bcdb9 100644 --- a/.agents/docs/taffyjs-wasm-package.md +++ b/.agents/docs/taffyjs-wasm-package.md @@ -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: @@ -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 diff --git a/.agents/docs/tooling-decisions.md b/.agents/docs/tooling-decisions.md index 7f90268..272c8d7 100644 --- a/.agents/docs/tooling-decisions.md +++ b/.agents/docs/tooling-decisions.md @@ -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 diff --git a/packages/taffyjs-wasm/package.json b/packages/taffyjs-wasm/package.json index 598aed4..9ce79f4 100644 --- a/packages/taffyjs-wasm/package.json +++ b/packages/taffyjs-wasm/package.json @@ -48,7 +48,7 @@ }, "napi": { "binaryName": "taffyjs", - "packageName": "taffyjs-binding", + "packageName": "@taffyjs/binding", "targets": [ "wasm32-wasip1" ], diff --git a/tests/taffyjs-wasm/tests/package-contents.ts b/tests/taffyjs-wasm/tests/package-contents.ts index bbe6dd1..68cbb1f 100644 --- a/tests/taffyjs-wasm/tests/package-contents.ts +++ b/tests/taffyjs-wasm/tests/package-contents.ts @@ -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, ); diff --git a/tools/taffy-wasm/generate-inline-wasm-runtime-files.ts b/tools/taffy-wasm/generate-inline-wasm-runtime-files.ts index 9225889..d735178 100644 --- a/tools/taffy-wasm/generate-inline-wasm-runtime-files.ts +++ b/tools/taffy-wasm/generate-inline-wasm-runtime-files.ts @@ -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') @@ -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 @@ -124,7 +126,6 @@ for (const forbidden of [ "preopens", "__wasmFilePath", "@taffyjs/binding-wasm", - "taffyjs-binding-wasm", "require('./taffyjs.wasm-base64", ]) { if (nodeLoaderSource.includes(forbidden)) {