Skip to content

Commit ffc498d

Browse files
committed
feat(runtimes): add partial styles and Yoga WebAssembly
Add partial native style updates and the Yoga-compatible package backed by WebAssembly. Refs: #26, #27, #29
1 parent 96b010b commit ffc498d

71 files changed

Lines changed: 2091 additions & 2569 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.agents/docs/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@
1515
- [@taffyjs/wasm package design](taffyjs-wasm-package.md) — public contract, artifact layout, initialization model, ownership boundary, and verification requirements for the explicit Wasm package.
1616
- [@taffyjs/yoga decisions](taffyjs-yoga-decisions.md) — compatibility decisions for replacing yoga-layout through the TaffyJS Yoga layer.
1717
- [@taffyjs/yoga implementation reference](taffyjs-yoga-reference.md) — the audited Yoga 3.2.1 public API inventory, implemented compatibility boundary, evidence, and ongoing maintenance constraints.
18-
- [Tooling decisions](tooling-decisions.md) — rulings for npm scope and test workspace package naming, the repository-wide TypeScript default, JavaScript package builds, repository command orchestration, tool ownership, and demand-driven API code generation.
18+
- [@taffyjs/yoga-wasm package design](taffyjs-yoga-wasm-package.md) — the shared-source Wasm transport, package boundary, loading behavior, and verification contract for the Yoga facade.
19+
- [Tooling decisions](tooling-decisions.md) — rulings for third-party license filenames, npm scope and test workspace package naming, the repository-wide TypeScript default, JavaScript package builds, repository command orchestration, tool ownership, and demand-driven API code generation.

‎.agents/docs/architecture.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Architecture
22

3-
This repository is a small Rust and JavaScript monorepo with one shared napi-rs binding and three public packages.
3+
This repository is a small Rust and JavaScript monorepo with one shared napi-rs binding and four public packages.
44

55
## Ownership
66

@@ -9,20 +9,26 @@ This repository is a small Rust and JavaScript monorepo with one shared napi-rs
99
- `packages/taffyjs-node` owns the public ESM wrapper and declarations, the private napi-rs loader and declarations, and npm metadata. The wrapper owns JavaScript-only NodeId validity data and context; Taffy owns topology, Style, Layout, cache, and computation state.
1010
- `packages/taffyjs-wasm` compiles the same authored source from `packages/taffyjs-node/src` against generated private Node and browser adapters that share one inline payload. It owns package metadata, generated artifacts, and build-time binding redirection; its package-specific typed generator lives under `tools/taffy-wasm`. It has no authored public wrapper source or separate initialization API.
1111
- `packages/taffyjs-yoga` owns the Node-only ESM Yoga 3.2.1 compatibility facade, Yoga declarations and visible state, input translation, and Yoga-shaped output projection. It depends only on the public `@taffyjs/node` package boundary and does not modify Taffy or the native binding.
12+
- `packages/taffyjs-yoga-wasm` builds the exact facade source owned by `packages/taffyjs-yoga/src` against the public `@taffyjs/wasm` backend. It owns only its package metadata and build-time backend redirection; it has no copied facade source or second compatibility policy.
1213
- `tests/taffyjs-node` is a private consumer package that tests `@taffyjs/node` through its package boundary.
1314
- `tests/taffyjs-wasm` is a private consumer package that checks the Wasm package through Node and browser package resolution, reuses the existing public type tests, mechanically inspects the published file set, and installs the packed result in fresh npm and pnpm consumers.
1415
- `tests/taffyjs-yoga` is the corresponding private consumer package for compatibility, differential, and end-to-end tests through the `@taffyjs/yoga` package boundary; it does not contain a redundant nested `tests/` directory.
16+
- `tests/taffyjs-yoga-wasm` reruns the same Yoga behavior and declaration suites against the Wasm transport, inspects the package boundary, and installs the packed packages in fresh npm and pnpm consumers for Node and Chromium checks.
1517

1618
The `@taffyjs/node` public entry bundles the private napi-rs root loader, which selects the matching optional platform package. Its generated loader and declaration remain repository build inputs rather than published package files. The `@taffyjs/wasm` default entry reaches a no-TLA ESM bridge that imports the shared payload and immediately invokes a synchronous CommonJS factory mechanically derived from napi-rs's eager Node loader; its browser entry reaches a TLA adapter and the unmodified deferred loader. Neither direct runtime package exposes an intermediate binding package, JavaScript shadow tree, separate core crate, public binding, or public initialization API.
1719

1820
`@taffyjs/yoga` is a Node-only TypeScript compatibility layer over `@taffyjs/node`; browser and WASM backends never live in that package. It performs bounded input normalization and output processing, but it does not modify or fork Taffy core, add a Yoga-specific native algorithm, or reimplement Flex layout in JavaScript; behavior that cannot cross that boundary is documented as Different or Unsupported as recorded in [@taffyjs/yoga decisions](taffyjs-yoga-decisions.md).
1921

22+
`@taffyjs/yoga-wasm` is a separate transport over `@taffyjs/wasm`, selected at build time by replacing only the exact bare `@taffyjs/node` dependency. The eager root entry and lazy `/load` entry keep Yoga's public shapes; `/load` dynamically imports the facade so a browser compiles no WebAssembly until `loadYoga()` is called. Compatibility semantics stay owned by the shared Yoga source and documentation.
23+
2024
## Testing
2125

2226
JavaScript integration tests are the primary proof of observable behavior. Package-local JavaScript and Rust unit tests are limited to critical behavior that is clearer in isolation. Maintained test names follow product behavior rather than temporary milestone or acceptance labels.
2327

2428
The Wasm package runs the complete Node public API behavior suite without copied test files: a Wasm-only Vite+ configuration replaces the exact `@taffyjs/node` import with `@taffyjs/wasm`. Subprocess fixtures select the same test entry through an inherited test-only file URL. The Wasm package additionally has a real Chromium runtime test through Vite+ and Playwright. A separate Vite consumer build proves that the `browser` export is selected, the payload occurs once in JavaScript, and no external `.wasm` asset is emitted; the browser runtime check exercises synchronous measurement and reuse after a controlled error without cross-origin isolation or `SharedArrayBuffer`.
2529

30+
The Yoga Wasm package likewise reuses the complete maintained Yoga suite and public declaration test. Its packed-consumer fixture proves unchanged `yoga-layout` root and `/load` imports in Node and bundled Chromium, including the lazy compilation boundary, rather than maintaining a second copy of compatibility tests.
31+
2632
A test earns its place only when it is the clearest durable check for a distinct observable behavior or safety property. Before adding one, identify the exact failure it would catch and explain why tests beside the code that owns the behavior would not already catch it. Distinct coverage is not enough: the protection must justify the code to maintain, time to run, ways it can fail for unrelated reasons, and platform-specific behavior it introduces. Prefer the smallest direct check. When consolidating duplicate files, move any genuinely unique assertion into the owning test before deleting the duplicate.
2733

2834
Review test changes in this order: whether the protection is worth its cost, whether the check sits beside the right code, then whether its implementation is correct. A passing test or a working harness proves only that it runs. Do not use runtime tests that read README, source, or test files as a substitute for testing product behavior or enforcing architecture. If an explicit repository rule needs automatic enforcement, such as which module may import a private loader, use a focused lint or CI check against the source files. Generated-artifact freshness follows [API code generation](api-codegen.md#verification), not generator unit tests.

‎.agents/docs/binding-cases.md‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ The `calc` feature is enabled by Taffy's default features, which this repository
112112

113113
### Selected JavaScript container semantics
114114

115-
The direct analogue of Rust's update syntax is a plain `StyleInput` object whose fields are optional. A missing field or a field whose value is `undefined` takes the corresponding `Style::DEFAULT` value. Node creation stores the resulting complete Style. `setStyle(node, input)` constructs another complete Style from defaults and the supplied fields and replaces the old Style; it does not merge omitted fields with the node's current Style. A separate merge or patch API would be additive convenience with different semantics.
115+
The direct analogue of default-based Rust Style construction is a plain `StyleInput` object whose fields are optional. A missing field or a field whose value is `undefined` takes the corresponding `Style::DEFAULT` value. Node creation stores the resulting complete Style. `setStyle(node, input)` constructs another complete Style from defaults and the supplied fields and replaces the old Style; it does not merge omitted fields with the node's current Style. The additive `updateStyle(node, update)` operation instead preserves omitted values.
116116

117117
Fixed-shape geometry records used directly as Style fields are partial on input for the same default-based construction. Each missing or explicit-`undefined` `Point`, `Size`, `Rect`, or `Line` component comes from that component of the enclosing field's `Style::DEFAULT` value. Thus `size: { width: value }` gets the default `auto` height, `padding: { left: value }` gets zero for the other three sides, and `inset: { left: value }` gets `auto` for the other three sides. This never reads the stored Style. Unknown enumerable string components are rejected because every component is optional and a misspelling would otherwise silently select the default. This rule does not make tagged semantic values, grid payloads, or arbitrary nested records recursively partial.
118118

@@ -155,12 +155,34 @@ export interface Style {
155155

156156
The reusable geometry declarations referenced here are selected below.
157157

158-
The explicit `| undefined` members keep the declarations truthful for consumers that enable TypeScript's `exactOptionalPropertyTypes`: the runtime accepts both a missing property and an explicitly undefined property. Those forms use the field's default, while `null` explicitly requests `None` for a publicly nullable field. Conversion stores only the resulting Rust value: when a default is `Some(value)`, omission or `undefined` and `null` remain observably different; when the default is already `None`, they converge. Every property where `null` and `undefined` differ must explain both meanings in property-level JSDoc rather than relying only on an interface-level note. A complete `Style` keeps every field present and readonly, emits the concrete value for `Some(value)`, emits `null` for `None`, and never uses a missing or `undefined` field for nullable output.
158+
The explicit `| undefined` members keep the declarations truthful for consumers that enable TypeScript's `exactOptionalPropertyTypes`: the runtime accepts both a missing property and an explicitly undefined property. Those forms use the field's default for construction and replacement and preserve the field for update, while `null` explicitly requests `None` for a publicly nullable field. Conversion stores only the resulting Rust value: when a default is `Some(value)`, omission or `undefined` and `null` remain observably different during replacement; when the default is already `None`, they converge. Every property where `null` and `undefined` differ must explain the operation-specific omission and explicit-null meanings in property-level JSDoc rather than relying only on an interface-level note. A complete `Style` keeps every field present and readonly, emits the concrete value for `Some(value)`, emits `null` for `None`, and never uses a missing or `undefined` field for nullable output.
159159

160160
`StyleInput` properties remain mutable in TypeScript. Conversion uses ordinary property access and does not inspect, reject, copy, freeze, or repeat validation solely because an input may contain accessors or be a Proxy; the caller owns those behaviors and side effects. Every binding-produced `Style` record uses readonly TypeScript properties because it is a detached snapshot whose mutation cannot change native state. The direct runtime representation is a complete eagerly materialized ordinary plain object without runtime freezing, sealing, or a Proxy. No lazy or selective output facility belongs to the initial API.
161161

162162
The first checkpoint is demonstrated by ordinary integration cases: `{}` and explicit `undefined` produce Taffy defaults; `setStyle(node, {})` resets a previously nondefault Style instead of merging; a partial fixed-shape nested record fills each omitted component from the corresponding enclosing Style default rather than stored state; an unknown top-level or partial-record component and `null` for a required field throw without changing the stored Style; `null` succeeds only for a publicly nullable field; a complete output uses `null` rather than an omitted or undefined field for Rust `None`; and declarations accept explicit `undefined` under `exactOptionalPropertyTypes` and retain the required property-level JSDoc. Getter- or Proxy-driven tree mutation is deliberately not a baseline fixture.
163163

164+
### Additive partial updates
165+
166+
`StyleUpdate` reuses the structural field types of `StyleInput`, including readonly collection inputs, without applying a recursive `Partial`. `updateStyle` interprets a missing or explicit-`undefined` outer field as “preserve,” and interprets missing components of the four public partial geometry families the same way. A supplied shorthand geometry value still supplies every component. Arrays, tagged-union branches, and other complete input records remain complete whole replacements; `[]` clears a collection and accepted `null` maps to `None`.
167+
168+
Rust converts the JavaScript object and all supplied collections into an owned patch before tree access. It compares only supplied fields and components with the stored Style, including bitwise comparison for direct floating-point fields so repeated `NaN` and distinct signed zero behave predictably. Empty and unchanged patches do not call Taffy's dirtying setter. For a real change, the complete candidate is validated before one `set_style` call, so a failure preserves both the old Style and dirty state.
169+
170+
Taffy 0.13 exposes the stored value through `style(&self) -> &Style` and replaces it through `set_style(Style)`, with no public mutable or take operation. `std::mem::take` would move a `Vec` allocation rather than clone it if the binding had `&mut Style`, but that reference cannot be obtained safely from the high-level tree. The implemented fallback returns an empty patch before cloning; every nonempty patch clones the current Style once, then compares and applies supplied fields in one traversal. An unchanged candidate is discarded without dirtying, while a changed candidate is validated and written once. Replacing a collection can therefore clone the old collection before overwriting it, but there is no workload benchmark justifying the larger selective-reconstruction implementation in the initial API. This preserves Taffy's sole ownership and avoids a JavaScript shadow Style; a future upstream closure-style update operation could safely expose in-place mutation and automatic dirty propagation.
171+
172+
A focused end-to-end native-binding microbenchmark on implementation commit `6ed48b4` retained the JavaScript-to-Rust boundary, input conversion, mutation, and alternating real value changes. It ran on Node.js 24.19.0 on Linux with an Intel Core i5-13500H, pinned to one core, after warmup, with 13 samples in each of two runs. Representative per-operation timings were:
173+
174+
| Preserved state and changed value | `setStyle` reconstruction | `updateStyle` | Observed relation |
175+
| ------------------------------------------------------------ | ------------------------: | ------------: | ------------------------: |
176+
| One retained scalar; replace one scalar | 3.83 µs | 3.89 µs | Effectively equal |
177+
| 13 retained sparse fields; replace one scalar | 11.8 µs | 3.89 µs | `updateStyle` 3.0× faster |
178+
| Complete 41-field `getStyle` snapshot; replace one scalar | 23.1 µs | 3.97 µs | `updateStyle` 5.8× faster |
179+
| 1,000 retained `gridAutoRows`; replace one scalar | 428 µs | 4.60 µs | `updateStyle` 93× faster |
180+
| Replace 1,000 `gridAutoRows` | 446 µs | 448 µs | Effectively equal |
181+
| 1,000 retained nested string grid values; replace one scalar | 139 µs | 64.3 µs | `updateStyle` 2.2× faster |
182+
| Replace 1,000 nested string grid values | 139 µs | 196 µs | `setStyle` 1.4× faster |
183+
184+
These measurements support recommending `updateStyle` for incremental changes: it avoids reconverting retained JavaScript fields, and the advantage grows when retained input is larger. They do not establish per-call dominance. When the changed field itself is the large collection, both operations must parse its replacement and can converge; with string-heavy replacements, the current full-Style clone fallback can make `updateStyle` slower. This focused boundary microbenchmark also does not justify adding selective reconstruction without representative workload evidence. Public documentation should therefore say that `updateStyle` is generally faster for incremental changes, not that every call must be faster.
185+
164186
The pointer-backed calc variant is a stop rather than a routine field mapping. The public high-level Style vocabulary excludes calc because `TaffyTree` resolves every calc pointer to zero and exposes no resolver. Whether the Cargo feature is also disabled is an implementation choice to verify separately; keeping an internal feature enabled does not make calc a supported JavaScript value.
165187

166188
### Selected closed-enum representation

0 commit comments

Comments
 (0)