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
39 changes: 34 additions & 5 deletions .agents/docs/api-codegen.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

`tools/api-codegen` is the repository's long-term home for generators that keep one API description aligned across Rust and TypeScript. New API generators should extend this tool instead of adding isolated scripts with their own parsing, writing, and checking rules.

This document defines the shared organization and safety rules. It does not require every repetitive source file to become generated, and it does not approve a public API merely because that API could be generated. The implemented families are the numeric constants shared by the Node wrapper and Rust binding, the accepted absolute-length and definite-available-space input shorthands, and the compact Style codec. The future query design is recorded separately in [API query code generation](api-codegen-query.md).
This document defines the shared organization and safety rules. It does not require every repetitive source file to become generated, and it does not approve a public API merely because that API could be generated. The implemented families are the numeric constants shared by the Node wrapper and Rust binding, the accepted absolute-length and definite-available-space input shorthands, the compact Style codec, and the fixed Layout codec. The future query design is recorded separately in [API query code generation](api-codegen-query.md).

## One maintained input

Expand Down Expand Up @@ -42,27 +42,56 @@ The TypeScript emitter writes `packages/taffyjs-node/src/style-input.ts`, which

The wire version is distinct from the maintained input format version. A change that only extends generator metadata without changing bytes need not change the wire version; a change that reinterprets existing private bytes must. The current format, buffer lifetime, format choice, and mutation rules are recorded in [Compact Style codec](style-codec.md).

## Generated Layout codec

The complete public `Layout` field tree and its fixed 21-number private transport have one versioned description in `api/layout-codec.json`. Input order is the public property and slot order. The compiler validates the supported scalar and geometry shapes, resolves JavaScript and Rust field paths, assigns every slot once, and derives the 168-byte buffer size.

The TypeScript emitter owns the public `Layout` declaration, slot constants, and straight-line reconstruction of fresh ordinary objects. The Rust emitter owns the same slot constants and the straight-line writer from Taffy's stored `Layout`. The authored tree wrapper owns one module-local `Float64Array` scratch buffer built over an explicit `ArrayBuffer`; the private binding writers synchronously fill it and never retain a pointer to it. The explicit `ArrayBuffer` is required because JavaScriptCore materializes the backing buffer of a length-constructed typed array lazily and Bun loses the first pointer write into any such buffer. Native targets fill the buffer through the borrowed slice; the Wasm target fills the same slots through `napi_set_element`, because a Wasm module cannot receive a pointer into the JavaScript heap. Both targets share one public method name, so the wrapper and the generated decoder never branch on the runtime.

The transport stays specific to Layout. It does not establish a general serialization format, runtime schema interpreter, public typed-array API, or automatic precedent for variable-sized outputs. Public behavior tests cover complete values, object shape and ownership, errors, Native/WASI parity, and browser execution; `check:codegen` covers agreement between the maintained model and committed generated sources.

## Tool organization

The first implementation should establish the permanent boundaries instead of starting as a single numeric-specific script:

```text
api/
├── layout-codec.json
├── numeric-families.json
├── style-codec.json
├── tagged-values.json
└── schemas/
└── numeric-families.schema.json
├── layout-codec.schema.json
├── numeric-families.schema.json
├── style-codec.schema.json
└── tagged-values.schema.json

tools/api-codegen/src/
├── generate.ts
├── index.ts
├── diagnostics.ts
├── input/
│ ├── load.ts
│ └── numeric-families.ts
│ ├── layout-codec.ts
│ ├── numeric-families.ts
│ ├── style-codec.ts
│ └── tagged-values.ts
├── compiler/
│ └── numeric-families.ts
│ ├── layout-codec.ts
│ ├── numeric-families.ts
│ ├── style-codec.ts
│ └── tagged-values.ts
├── emit/
│ └── numeric-families/
│ ├── layout-codec/
│ │ ├── rust.ts
│ │ └── typescript.ts
│ ├── numeric-families/
│ │ ├── rust.ts
│ │ └── typescript.ts
│ ├── style-codec/
│ │ ├── rust.ts
│ │ └── typescript.ts
│ └── tagged-values/
│ ├── rust.ts
│ └── typescript.ts
└── output/
Expand Down
8 changes: 8 additions & 0 deletions .agents/docs/complete-output-transport.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ General generation makes the mechanism maintainable; it does not prove that ever
Layout is the first candidate because its complete output is a fixed numeric record. Style and DetailedLayoutInfo should use the same direction only after representative default, nested collection, tagged-variant, string-heavy, single-value, and batch workloads demonstrate a material end-to-end benefit.

The public complete getter remains the semantic baseline regardless of its private transport. This direction does not add a new public API by itself and does not weaken the need for selective query when a consumer wants only part of a value.

## Implemented Layout transport

`getLayout()` and `getUnroundedLayout()` use one private caller-owned 21-slot `Float64Array` whose order is generated from `api/layout-codec.json`. Rust validates the exact length, synchronously writes all slots through a borrowed view, and retains neither the view nor its pointer. TypeScript immediately reconstructs a fresh complete ordinary `Layout` object, preserving the public API and detached-snapshot semantics.

Native targets write into the JavaScript buffer through the borrowed Node-API view. The scratch buffer is allocated over an explicit `ArrayBuffer` rather than by length: JavaScriptCore materializes the backing buffer of a length-constructed typed array lazily, and Bun loses the first pointer write into any such buffer, so an eagerly allocated buffer keeps one shared path correct on Node, Deno, and Bun alike.

The Wasm target fills the same buffer through `napi_set_element` instead. Node-API promises that `napi_get_typedarray_info` yields a pointer into the array's own storage, and a Wasm module cannot be given such a pointer, because its linear memory cannot address the JavaScript heap; Emnapi can only hand back a copy. Writing the elements keeps the Wasm binding inside portable Node-API and leaves the generated loaders untouched, at the cost of 21 calls per read instead of one.
2 changes: 1 addition & 1 deletion .agents/docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ TaffyJS has two independently dispatched release groups. Core publishes `@taffyj

Dispatch `Publish Core` or `Publish Yoga` from `main`. The default `auto` bump reads Conventional Commits that changed the selected group's owned paths since its previous tag. A feature or breaking change selects a minor bump; a fix, performance change, or revert selects a patch bump. An explicit patch or minor override exists for an intentional release whose commit type does not carry a bump. A group's first stable plan is always `0.0.1`.

The dispatch is the sole human authorization. Build jobs have read-only repository access and no npm identity. Ordinary pull requests retain only the Linux x64 GNU and Windows x64 MSVC native runtime jobs; they do not build macOS, Wasm, or any other publication artifact. The publication workflows run the complete Wasm package verification graph, and Core publication is the only place that builds all 13 supported native release targets. The assembly job downloads every required build artifact, requires the exact target or package set, writes final metadata, packs the public packages, records each tarball integrity, and installs those same tarballs in a fresh consumer. Only the final job receives `id-token: write`; it publishes the verified tarballs in dependency order, verifies each published version against its assembled integrity, then pushes the group tag and writes the release notes into the workflow summary. The workflow creates no GitHub Release.
The dispatch is the sole human authorization. Build jobs have read-only repository access and no npm identity. Ordinary pull requests retain the Linux x64 GNU and Windows x64 MSVC native runtime jobs and the complete Wasm verification graph; they do not build macOS or any other publication artifact. The publication workflows run the complete Wasm package verification graph, and Core publication is the only place that builds all 13 supported native release targets. The assembly job downloads every required build artifact, requires the exact target or package set, writes final metadata, packs the public packages, records each tarball integrity, and installs those same tarballs in a fresh consumer. Only the final job receives `id-token: write`; it publishes the verified tarballs in dependency order, verifies each published version against its assembled integrity, then pushes the group tag and writes the release notes into the workflow summary. The workflow creates no GitHub Release.

Publishing is not transactional. A retry uses the retained bundle: a package version already present with the same integrity is skipped, a different integrity stops the release, and missing packages continue in dependency order. Before the first npm write, the workflow requires the group tag to be absent or already point to the bundle commit; it verifies that identity again after pushing the tag, and the tag is pushed only after the complete npm group reaches the registry. Nothing is installed from the registry afterwards: npm serves a new version from its exact-version endpoint immediately but refreshes the package index that installers read a few minutes later, and a check placed after an irreversible publication cannot prevent a bad release. The assembly job's consumer install is the test that gates publication. Workflow concurrency never cancels an in-progress publication.

Expand Down
2 changes: 1 addition & 1 deletion .agents/docs/technology-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ For `packages/taffyjs-yoga-wasm`, Vite+ builds those same sibling source entries

`tools/api-codegen` owns source generation that must keep Rust and TypeScript API facts aligned. Its first maintained input is `api/numeric-families.json`; `vp run codegen` updates both language outputs. CI runs `vp run check:codegen`, which regenerates and rejects any resulting Git diff.

Ordinary CI has four logical jobs. Ubuntu x64 GNU and Windows x64 MSVC build the native addon and run all Rust, JavaScript, and type tests with Node.js 22.20.0, then smoke-test the public `@taffyjs/node` and `@taffyjs/yoga` entries with Bun 1.2.0 and Deno 2.2.0. Ubuntu also rejects stale committed package JavaScript and declarations after the build. A Node-only Ubuntu job checks formatting, JavaScript and repository TypeScript including maintained tools through Vite+'s type-aware lint path, the alignment among target declarations and platform package metadata, release configuration, and generated-source drift. A Rust-only Ubuntu job checks formatting and Clippy. Ordinary CI deliberately has no macOS, WASIP, or distribution-target build job.
Ordinary CI has five logical jobs. Ubuntu x64 GNU and Windows x64 MSVC build the native addon and run all Rust, JavaScript, and type tests with Node.js 22.20.0, then smoke-test the public `@taffyjs/node` and `@taffyjs/yoga` entries with Bun 1.2.0 and Deno 2.2.0. Ubuntu also rejects stale committed package JavaScript and declarations after the build. A Node-only Ubuntu job checks formatting, JavaScript and repository TypeScript including maintained tools through Vite+'s type-aware lint path, the alignment among target declarations and platform package metadata, release configuration, and generated-source drift. A Rust-only Ubuntu job checks formatting and Clippy. A Wasm Ubuntu job runs the whole `check:wasm` graph and smoke-tests `@taffyjs/wasm` on Bun 1.2.0 and Deno 2.2.0, because the binding compiles target-specific code for `wasm32` that no other ordinary job builds or runs. Running the whole graph rather than a subset costs a small amount of extra job time and removes the need to maintain a list of which Wasm tasks a pull request may run. Ordinary CI deliberately has no macOS or distribution-target build job.

Both publication workflows install the WASIP Rust target and Playwright Chromium, then run `check:wasm` before any final tarball can reach the publish job. That graph builds `@taffyjs/wasm`, reruns the complete Node public API suite against it, verifies types and package contents, installs packed consumers with npm and pnpm, exercises the browser entry, and builds the production website. It also builds `@taffyjs/yoga-wasm`, reruns the maintained Yoga behavior and declaration suites, inspects and installs the packed package, and exercises both public entries in bundled Chromium. The owning workflow additionally runs its Wasm package through Bun 1.2.0 and Deno 2.2.0. Core publication compiles all twelve non-FreeBSD template targets with napi-rs's maintained recipes, builds the thirteenth target in a FreeBSD 15 VM, uploads every binary, and assembles the exact artifact set with the Node and Wasm packages before publishing. Linux x64 GNU and Windows x64 MSVC are runtime-tested in ordinary CI; the other eleven native targets are build-covered only during publication.

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

### Ordinary CI platform boundary

**Ruling:** Ordinary pull-request and `main` CI runs the full native test graph only on Linux x64 GNU and Windows x64 MSVC. macOS, Wasm-specific verification, and every build whose purpose is to produce a publication artifact run only inside the manually dispatched publication workflows.
**Ruling:** Ordinary pull-request and `main` CI runs the full native test graph on Linux x64 GNU and Windows x64 MSVC, and runs the complete Wasm verification graph on Linux. macOS and every build whose purpose is to produce a publication artifact run only inside the manually dispatched publication workflows.

**Limits:** The Ubuntu Node and Rust static-check jobs remain ordinary CI, and developers may still run every graph locally. This boundary does not reduce the 13-target published native set: targets other than Linux x64 GNU and Windows x64 MSVC remain build-covered during Core publication rather than runtime-tested on every change. Both publication workflows must run the complete Wasm verification graph before publishing their package group.
**Limits:** The Ubuntu Node and Rust static-check jobs remain ordinary CI, and developers may still run every graph locally. This boundary does not reduce the 13-target published native set: targets other than Linux x64 GNU and Windows x64 MSVC remain build-covered during Core publication rather than runtime-tested on every change. Both publication workflows must still run the complete Wasm verification graph before publishing their package group. Ordinary CI runs the whole `check:wasm` graph rather than a hand-picked subset, so no one has to maintain a list of which Wasm tasks a pull request may run.

**Why:** Yunfei does not want ordinary changes to pay for distribution builds that are needed only when a release is actually authorized.
**Why:** The binding compiles target-specific code for `wasm32`, so a Wasm defect can reach `main` without any native job noticing. Yunfei still does not want ordinary changes to pay for distribution builds, and macOS and the release targets stay at publication time; the Wasm job is verification of code the repository already ships, not production of a release artifact.

**Source:** Yunfei (`@hyfdev`), 2026-08-18; explicitly required ordinary CI to test only Windows and Linux and moved publication builds to publication time.
**Source:** Yunfei (`@hyfdev`), 2026-08-18; explicitly required ordinary CI to test only Windows and Linux and moved publication builds to publication time. Revised by Yunfei (`@hyfdev`), 2026-08-22; after the Layout codec introduced a `wasm32`-only binding path, he explicitly required Wasm to run in ordinary CI and asked to record the change.

### Initial public versions

Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ jobs:
- name: Smoke test @taffyjs/yoga with Deno 2.2
run: pnpm dlx deno@2.2.0 run --node-modules-dir=manual --allow-env --allow-read --allow-ffi tests/taffyjs-yoga/runtime-smoke.mjs

test-wasm:
name: Build and test Wasm
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: pnpm/setup@v2
with:
version: 11.20.0
runtime: node@22.20.0
cache: true
install: false
- uses: dtolnay/rust-toolchain@stable
with:
targets: wasm32-wasip1
- run: pnpm install --frozen-lockfile
- name: Install the browser used by the Wasm runtime tests
run: pnpm --filter tests-taffy-wasm exec playwright install --with-deps chromium
- name: Build and test the Wasm packages
run: pnpm exec vp run check:wasm
- name: Smoke test @taffyjs/wasm with Bun 1.2
run: pnpm dlx bun@1.2.0 packages/taffyjs-wasm/tests/runtime-smoke.mjs
- name: Smoke test @taffyjs/wasm with Deno 2.2
run: pnpm dlx deno@2.2.0 run --node-modules-dir=manual packages/taffyjs-wasm/tests/runtime-smoke.mjs

check-node:
name: Node checks
runs-on: ubuntu-24.04
Expand Down
53 changes: 53 additions & 0 deletions api/layout-codec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"$schema": "./schemas/layout-codec.schema.json",
"formatVersion": 1,
"fields": [
{
"name": "order",
"shape": "number",
"documentation": "Reports this node's stable traversal order in the stored layout."
},
{
"name": "location",
"shape": "point",
"components": ["x", "y"],
"documentation": "Reports this node's position relative to its parent."
},
{
"name": "size",
"shape": "size",
"components": ["width", "height"],
"documentation": "Reports this node's outer width and height."
},
{
"name": "contentSize",
"shape": "size",
"components": ["width", "height"],
"documentation": "Reports the width and height of this node's content."
},
{
"name": "scrollbarSize",
"shape": "size",
"components": ["width", "height"],
"documentation": "Reports the width and height reserved for scrollbars."
},
{
"name": "border",
"shape": "rect",
"components": ["left", "right", "top", "bottom"],
"documentation": "Reports this node's resolved border widths."
},
{
"name": "padding",
"shape": "rect",
"components": ["left", "right", "top", "bottom"],
"documentation": "Reports this node's resolved padding widths."
},
{
"name": "margin",
"shape": "rect",
"components": ["left", "right", "top", "bottom"],
"documentation": "Reports this node's resolved margins."
}
]
}
Loading