diff --git a/.agents/docs/README.md b/.agents/docs/README.md index a26aa4e..812f67e 100644 --- a/.agents/docs/README.md +++ b/.agents/docs/README.md @@ -15,4 +15,5 @@ - [@taffyjs/wasm package design](taffyjs-wasm-package.md) — public contract, artifact layout, initialization model, ownership boundary, and verification requirements for the explicit Wasm package. - [@taffyjs/yoga decisions](taffyjs-yoga-decisions.md) — compatibility decisions for replacing yoga-layout through the TaffyJS Yoga layer. - [@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. -- [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. +- [@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. +- [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. diff --git a/.agents/docs/architecture.md b/.agents/docs/architecture.md index 62fabfe..848fd10 100644 --- a/.agents/docs/architecture.md +++ b/.agents/docs/architecture.md @@ -1,6 +1,6 @@ # Architecture -This repository is a small Rust and JavaScript monorepo with one shared napi-rs binding and three public packages. +This repository is a small Rust and JavaScript monorepo with one shared napi-rs binding and four public packages. ## Ownership @@ -9,20 +9,26 @@ This repository is a small Rust and JavaScript monorepo with one shared napi-rs - `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. - `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. - `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. +- `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. - `tests/taffyjs-node` is a private consumer package that tests `@taffyjs/node` through its package boundary. - `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. - `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. +- `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. 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. `@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). +`@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. + ## Testing 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. 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`. +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. + 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. 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. diff --git a/.agents/docs/intent.md b/.agents/docs/intent.md index ff902ea..18cc157 100644 --- a/.agents/docs/intent.md +++ b/.agents/docs/intent.md @@ -7,6 +7,7 @@ TaffyJS is intended to bring Taffy, a high-performance layout engine written in - `@taffyjs/node` is the first entry point and provides native Node-API bindings built with napi-rs. - `@taffyjs/wasm` is the explicit WebAssembly entry point for Node.js and bundled browsers. It reuses the `@taffyjs/node` public API over a threadless napi-rs WASIP binding rather than acting as an automatic fallback; its vouched contract is in [@taffyjs/wasm package design](taffyjs-wasm-package.md). - `@taffyjs/yoga` is the Node-only Yoga 3.2.1 compatibility package built on top of `@taffyjs/node`; browser and WASM backends remain outside that package. +- `@taffyjs/yoga-wasm` is the separate Node.js and bundled-browser transport for that same compatibility facade. It builds the same source over `@taffyjs/wasm` so the two packages cannot drift into separate Yoga implementations. - The package family should support real consumers such as terminal UI frameworks without forcing those consumers to own their own Taffy bindings. ## Bun and Deno runtime support @@ -31,5 +32,5 @@ The binding role and API priority are recorded in [@taffyjs/node decisions](taff ## Open questions -- Runtime and platform support beyond the current native targets, the approved `@taffyjs/wasm` Node.js and bundled-browser scope, Bun 1.2+ within major 1, and Deno 2.2+ within major 2 requires a concrete consumer need. +- Runtime and platform support beyond the current native targets, the approved `@taffyjs/wasm` and `@taffyjs/yoga-wasm` Node.js and bundled-browser scope, Bun 1.2+ within major 1, and Deno 2.2+ within major 2 requires a concrete consumer need. - The release timing of `@taffyjs/yoga` remains open. Yoga compatibility is not part of the `@taffyjs/node` contract. diff --git a/.agents/docs/taffyjs-yoga-reference.md b/.agents/docs/taffyjs-yoga-reference.md index af82484..ef28b28 100644 --- a/.agents/docs/taffyjs-yoga-reference.md +++ b/.agents/docs/taffyjs-yoga-reference.md @@ -6,7 +6,7 @@ This is the implementation reference for @taffyjs/yoga. It records the audited Y The ownership design rejects a normal create, insert, remove, detach, or free path that implicitly reconstructs or migrates an entire subtree or all unrelated roots. Operations whose meaning requires traversal, such as freeRecursive, are exempt. Style and topology changes must not implicitly compute layout; calculateLayout remains the explicit calculation boundary. -Internal file splitting, private class names, code generation versus handwritten code, exact error wording, benchmark thresholds, a named external consumer, and multi-version Yoga support are deliberately deferred unless evidence makes one of them necessary to preserve the compatibility boundary. Browser and WASM backends are excluded: @taffyjs/yoga permanently uses the native @taffyjs/node backend, including when that backend runs on supported Bun and Deno versions; any future WASM implementation would use a separate package. +Internal file splitting, private class names, code generation versus handwritten code, exact error wording, benchmark thresholds, a named external consumer, and multi-version Yoga support are deliberately deferred unless evidence makes one of them necessary to preserve the compatibility boundary. Browser and WASM backends remain excluded from @taffyjs/yoga: it permanently uses the native @taffyjs/node backend, including when that backend runs on supported Bun and Deno versions. @taffyjs/yoga-wasm builds the same facade source as a separate package over @taffyjs/wasm. ## Inventory method @@ -219,7 +219,7 @@ This matrix answers whether the public capability has a credible implementation No first-release feasibility blocker remains in the inventoried Yoga 3.2.1 surface. The grouped result is: - Package entries, factories, ordinary Config state, tree ownership and mutation, declaration getters and setters, calculation on any live Node, logical dirty and new-layout flags, ordinary measure callbacks, Yoga-shaped computed output, and per-Node point-grid rounding are Compatible through bounded JavaScript or TypeScript mapping. -- Yoga cache artifacts, MeasureMode-sensitive measurement results, aspect-ratio layout, min/max combined with flex growth or shrinkage, main-axis auto margins combined with justification offsets, and the documented reversed-axis, overflowing, or zero-size distribution cases are Different. The same API remains callable and returns Taffy's stable geometry under the documented trigger. +- Yoga cache artifacts, MeasureMode-sensitive measurement results, aspect-ratio layout, min/max combined with flex growth or shrinkage, shrink factors totaling below one, percentage main-axis gaps in auto-sized containers, main-axis auto margins combined with justification offsets, and the documented reversed-axis, overflowing, or zero-size distribution cases are Different. The same API remains callable and returns Taffy's stable geometry under the documented trigger. - Display.Contents, PositionType.Static, non-None Errata values, reference-baseline selection, property-invalid Align values, positive or negative infinite calculation owner dimensions, and undeclared broken setter coercions are Unsupported. Omit a wholly unsupported member where practical and always reject it at runtime. ### Package entries, factories, and public value shapes @@ -316,45 +316,47 @@ The TypeScript surface should preserve Yoga 3.2.1's published declarations for e Keep these restrictions as simple named unions. Do not add a conditional type that admits the exact broad upstream enum while trying to reject particular enum literals: such a signature either leaves unsupported paths statically callable or makes generic forwarding unpredictable. `AlignContentValue`, `AlignItemsValue`, and `AlignSelfValue` are exported from both package entries as the direct migration types and are the single source for the corresponding setter declarations. Code that must compile against either Yoga provider can instead derive the active provider's parameter with `Parameters[0]`. Runtime validation remains mandatory for JavaScript, `any`, assertions, and dynamic data, and every rejected call must fail before declaration state changes. Inputs outside Yoga's published declarations are not a compatibility surface merely because the generic wrapper happens to coerce them. -| APIs or capability | Classification | Mapping and boundary | -| --------------------------------------------------------------------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| The 25 style getters other than getGap() | Compatible | Read the raw Yoga declaration or Yoga-resolved getter default from JavaScript state. Construct fresh Value records with Yoga Unit values and NaN payloads where the Yoga runtime does so. | -| getGap() TypeScript declaration and runtime result | Compatible | Preserve Yoga's declared Value return for source compatibility while returning the raw number or NaN at runtime exactly as Yoga 3.2.1 does. The declaration/runtime contradiction is pinned reference behavior, not a reason to invent a third shape. | -| setDirection(), setFlexDirection(), setFlexWrap(), setJustifyContent(), setOverflow(), and setBoxSizing() | Compatible | Direct enum translation, with Direction.Inherit resolved before calculation. | -| setDisplay(Flex or None) and setPositionType(Relative or Absolute) | Compatible | Direct translation. Display.Contents and PositionType.Static use their unsupported rows above. | -| Valid-context setAlignContent(), setAlignItems(), and setAlignSelf() values | Compatible | Direct mapping. Broad-enum values invalid for a particular property use the unsupported-input row in the enum matrix rather than being silently normalized. | -| setFlexGrow(), setFlexShrink(), setFlexBasis(), setFlexBasisPercent(), and setFlexBasisAuto() | Compatible | Preserve separate Yoga declarations and derive the effective Taffy fields with Yoga precedence and Config defaults. | -| setFlex() | Compatible | Retain numeric flex and derive grow, shrink, and basis exactly as Yoga does: positive values supply grow and a legacy zero basis or web-default auto basis; negative values supply legacy shrink; explicit longhands win. The declaration mapping is finite; cache-derived stale-layout behavior after a live useWebDefaults change follows the decided difference above. | -| setWidth/Height and Auto/Percent variants; setMin/Max Width/Height and Percent variants | Compatible | Normalize every declared point, percent, auto, undefined, and NaN form into Yoga declaration state, then convert to Taffy dimensions. A negative point or percent width or height remains visible through its Yoga getter but becomes Auto only in the Taffy Style used for calculation. An undefined Yoga minimum becomes an explicit zero-length Taffy minimum rather than Taffy's CSS automatic minimum, while the Yoga getter retains Unit.Undefined. Valid percent strings are parsed once; malformed strings and Value-like objects are outside the declared input contract and use the unsupported-input row below. | -| Margin, padding, border, position, and gap setter families | Compatible | Retain all shorthand and logical declarations, apply Yoga edge precedence and direction when producing Taffy's physical Rect and Size values, and preserve original declarations for getters. Auto margin affects layout but Yoga's computed-margin getter reports zero for an auto declaration, so computed output must mask Taffy's resolved auto-margin value. | -| setAspectRatio() storage and getAspectRatio() | Compatible | Optional numeric declaration storage and getter shape are finite. Calculated layout is classified separately. | -| setGap() and setGapPercent() TypeScript declaration and runtime result | Compatible | Preserve Yoga's declared Value return while returning undefined at runtime, matching Yoga 3.2.1. Normal consumers ignore these setter returns; exact source declarations avoid an unnecessary substitution-time type change. | -| Undeclared generic-setter coercions and Value-like getter-to-setter inputs | Unsupported | Reject malformed strings, arbitrary objects, and other values outside the published setter declarations. Yoga's wrapper attempts broad parseFloat and Value-like coercion, but its own getWidth-to-setWidth round trip resets the value rather than preserving it. Reproducing accidental and broken coercions would add maintenance without preserving a working consumer capability. | +| APIs or capability | Classification | Mapping and boundary | +| --------------------------------------------------------------------------------------------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| The 25 style getters other than getGap() | Compatible | Read the raw Yoga declaration or Yoga-resolved getter default from JavaScript state. Construct fresh Value records with Yoga Unit values and NaN payloads where the Yoga runtime does so. | +| getGap() TypeScript declaration and runtime result | Compatible | Preserve Yoga's declared Value return for source compatibility while returning the raw number or NaN at runtime exactly as Yoga 3.2.1 does. The declaration/runtime contradiction is pinned reference behavior, not a reason to invent a third shape. | +| setDirection(), setFlexDirection(), setFlexWrap(), setJustifyContent(), setOverflow(), and setBoxSizing() | Compatible | Direct enum translation, with Direction.Inherit resolved before calculation. | +| setDisplay(Flex or None) and setPositionType(Relative or Absolute) | Compatible | Direct translation. Display.Contents and PositionType.Static use their unsupported rows above. | +| Valid-context setAlignContent(), setAlignItems(), and setAlignSelf() values | Compatible | Direct mapping. Broad-enum values invalid for a particular property use the unsupported-input row in the enum matrix rather than being silently normalized. | +| setFlexGrow(), setFlexShrink(), setFlexBasis(), setFlexBasisPercent(), and setFlexBasisAuto() | Compatible | Preserve separate Yoga declarations and derive the effective Taffy fields with Yoga precedence and Config defaults. | +| setFlex() | Compatible | Retain numeric flex and derive grow, shrink, and basis exactly as Yoga does: positive values supply grow and a legacy zero basis or web-default auto basis; negative values supply legacy shrink; explicit longhands win. The declaration mapping is finite; cache-derived stale-layout behavior after a live useWebDefaults change follows the decided difference above. | +| setWidth/Height and Auto/Percent variants; setMin/Max Width/Height and Percent variants | Compatible | Normalize every declared point, percent, auto, undefined, and NaN form into Yoga declaration state, then convert to Taffy dimensions. A negative point or percent width or height remains visible through its Yoga getter but becomes Auto only in the Taffy Style used for calculation. An undefined Yoga minimum becomes an explicit zero-length Taffy minimum rather than Taffy's CSS automatic minimum, while the Yoga getter retains Unit.Undefined. Valid `${number}%` strings, including surrounding whitespace and JavaScript hexadecimal, binary, or octal numeric forms, follow Yoga's `parseFloat` result after validation; malformed strings and Value-like objects are outside the declared input contract and use the unsupported-input row below. | +| Margin, padding, border, position, and gap setter families | Compatible | Retain all shorthand and logical declarations, apply Yoga edge precedence and direction when producing Taffy's physical Rect and Size values, and preserve original declarations for getters. Auto margin affects layout but Yoga's computed-margin getter reports zero for an auto declaration, so computed output must mask Taffy's resolved auto-margin value. | +| setAspectRatio() storage and getAspectRatio() | Compatible | Optional numeric declaration storage and getter shape are finite. Calculated layout is classified separately. | +| setGap() and setGapPercent() TypeScript declaration and runtime result | Compatible | Preserve Yoga's declared Value return while returning undefined at runtime, matching Yoga 3.2.1. Normal consumers ignore these setter returns; exact source declarations avoid an unnecessary substitution-time type change. | +| Undeclared generic-setter coercions and Value-like getter-to-setter inputs | Unsupported | Reject malformed strings, arbitrary objects, and other values outside the published setter declarations. Yoga's wrapper attempts broad parseFloat and Value-like coercion, but its own getWidth-to-setWidth round trip resets the value rather than preserving it. Reproducing accidental and broken coercions would add maintenance without preserving a working consumer capability. | ### Computed layout getters and cross-cutting result classes Compatible numeric comparisons allow an absolute error below `0.0001` for finite unrounded float fields, matching Yoga's own internal float-comparison tolerance. Rounded point-grid output and fields covered by strict fixtures continue to compare exactly; the tolerance covers only last-bit differences where Yoga and Taffy evaluate equivalent percentage arithmetic in different float32 steps. -| APIs or result class | Classification | Mapping or observed boundary | -| --------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| getComputedLeft/Top/Width/Height(), getComputedRight/Bottom(), and getComputedLayout() object shape | Compatible | Width, height, and normal left/top come from Taffy's unrounded layout followed by Yoga point-grid processing. Yoga right/bottom are stored trailing margin plus relative inset values rather than geometric remaining space, and can be reconstructed from finite declaration state. Root left/top also need Yoga's leading margin and relative inset because Taffy roots remain at zero. | -| getComputedMargin(), getComputedPadding(), and getComputedBorder() for Left/Top/Right/Bottom/Start/End | Compatible | Taffy's unrounded Layout contains the resolved physical records; Start and End map through the resolved layout direction, and auto declarations mask Taffy's distributed margin back to Yoga's reported zero. Yoga resolves a vertical percentage margin against containing width for the computed-margin value but against containing height when that same margin participates in absolute Top or Bottom placement, so projection keeps those two bases separate. Bounded comparisons cover positive and negative point margins, percentage padding and margins, physical and logical edges under LTR and RTL, auto margins, borders, and overflow. | -| Computed margin, padding, or border queried with Horizontal, Vertical, or All | Compatible | Reject these multi-edge shorthands; Yoga aborts rather than returning an aggregate result. Exact error class and wording are deferred. | -| Point-scale rounding | Compatible | It is an output conversion over unrounded layouts, including Yoga's ceil-sensitive text-node sizes, and does not require a Taffy core change. | -| Ordinary non-degenerate fixed sizes, simple grow or shrink without min/max, percentages, and simple nested flex trees | Compatible | Bounded differential probes found matching unrounded geometry outside the explicit difference triggers below. A committed real-Yoga oracle suite remains a release gate and must not be presented as a compatibility percentage. | -| min/max combined with flex growth or shrinkage | Different | Yoga and Taffy can produce different final sizes; the API remains available with Taffy's stable result. | -| Conflicting min/max on an intrinsically sized selected root | Different | Yoga routes roots with no definite size through root-kind-specific sizing modes before applying conflicting constraints, while Taffy retains its ordinary minimum-first result. With point rounding disabled, both a root containing a width-25 child and a measured root returning width 75 resolve to width 50 in Yoga but width 100 in Taffy when min-width is 100 and max-width is 50. Definite root sizes remain Compatible through bounded calculation-only translation. Reproducing Yoga's empty, Flex-content, and Measure sizing dispatch would duplicate layout-algorithm control flow. | -| Overlapping physical and logical main-axis margins under a child direction that differs from its parent | Different | Yoga can use one direction while totaling margins for Flex sizing and the child's resolved direction when it stores computed physical margins. In a width-100 LTR Row, a width-20 RTL child with margin-left 4 and margin-start 10% reports computed margins left 4 and right 10 in both engines, but Yoga resolves width 16 while Taffy remains width 20. Reproducing the hidden intermediate margin total would require Flex-specific size correction, so Taffy's stable result is retained. | -| aspectRatio in calculated layout | Different | Yoga derives the cross size from the final flex main size, while Taffy follows its own CSS-oriented aspect-ratio and stretch rules. With rounding disabled, a 100x100 Row parent and width-40 ratio-2 child produce 40x20 in Yoga but 40x100 in Taffy; a Column child declaring width 40, height 30, and ratio 2 produces 60x30 in Yoga but 40x30 in Taffy. Min/max and flex growth diverge further. Aligning all cases would require parent- and algorithm-dependent size rewriting, which crosses the accepted JavaScript boundary. | -| MeasureMode-sensitive callbacks | Different | Taffy does not preserve whether a finite constraint originated as Yoga Exactly or AtMost and may request intrinsic measurements in different phases. The bridge therefore gives each Taffy request a meaningful Yoga-shaped mode but does not promise a Yoga callback trace. Fixed-size and constraint-insensitive callbacks remain useful; callbacks branching on MeasureMode can have stable result differences. | -| WebFlexBasis disabled across a changed containing size | Different | Yoga 3.2.1 can reuse a stale computed percentage flex basis when the experiment is false, while Taffy recomputes it. Emulating Yoga's stale cache would require Yoga-internal flex-basis state and is outside the boundary; the enabled Yoga result matched the tested Taffy relayout. | -| Attached-subtree calculation followed by an ancestor calculation that Yoga serves from cache | Different | Yoga can retain geometry written by the separate subtree calculation because the clean ancestor does not revisit it. Taffy may recompute that ancestor after internal Style translation propagated native dirty state and can therefore produce current ancestor-context geometry instead. The package keeps calculateLayout available on both nodes and does not reproduce Yoga's complete cache algorithm. | -| Oversized WrapReverse lines with distribution-style alignContent | Different | With point rounding disabled, a 100x30 Row/WrapReverse container holding three 60-point items on separate 10, 20, and 15-point lines produces Yoga top positions 20, 0, and -15 but Taffy positions 35, 15, and 0 under Stretch, SpaceBetween, SpaceAround, or SpaceEvenly. The content's 45-point cross size exceeds the 30-point container; FlexStart, FlexEnd, and Center are not covered by this difference. Reconstructing Yoga's negative-free-space line placement would duplicate Flex logic. | -| A non-overflowing WrapReverse line with a cross-axis margin | Different | Cross-axis margin placement can also differ without overflow or distribution-style alignContent. In a 100x100 Row/WrapReverse container with alignItems FlexStart, a 40x20 child with an 11-point bottom margin has top 80 in Yoga and top 69 in Taffy. Correcting this requires reproducing WrapReverse line and item alignment, so Taffy's placement is retained. | -| Oversized single line on a reversed main axis with distribution-style justifyContent | Different | When a RowReverse or ColumnReverse no-wrap line overflows and justifyContent is SpaceBetween, SpaceAround, or SpaceEvenly, Yoga and Taffy choose different negative-free-space fallback placement. In a 240-point RowReverse line with 140, 130, and 120-point children, Yoga left positions are 100, -30, and -150 while Taffy reports 250, 120, and 0. This affects sibling distribution and is not an output-only correction. | -| Zero-cross-size wrapped line under Stretch item alignment | Different | A wrapped line whose items all resolve to zero cross size stays on Yoga's cross-start or cross-end edge, while Taffy distributes the zero-size line under FlexEnd, Center, SpaceAround, or SpaceEvenly alignContent. For a 180-point cross axis, ordinary Wrap plus Center places Yoga at 0 and Taffy at 90; WrapReverse plus Center places Yoga at 180 and Taffy at 90. Reconstructing this degenerate line placement would duplicate Flex logic. | -| Main-axis auto margins on a reversed axis or combined with a nonzero justifyContent offset | Different | Yoga lets positive-free-space main-axis auto margins consume the remaining space and can suppress justifyContent, while Taffy can distribute the margin and retain an additional offset. In a width-240 RowReverse, a width-60 child with margin-left auto has left 180 in Yoga and 360 in Taffy even under FlexStart. In a 240x180 Column with justifyContent Center, Yoga child tops are 65 and 160 while Taffy reports 130 and 225. Correcting either sibling distribution would duplicate Flex logic, so Taffy's geometry is retained and computed auto margins remain masked to zero. | -| Relative insets, including reversed axes | Compatible | Relative positioning does not participate in normal-flow sizing. When Taffy and Yoga apply opposite signs on a reversed axis, the projection can remove Taffy's declaration-derived offset and apply Yoga's declaration-derived offset to the Node's projected position and descendant absolute accumulation without redistributing siblings or calculating Flex geometry. A combination that also triggers the main-axis auto-margin difference remains covered by the Different row above. | +| APIs or result class | Classification | Mapping or observed boundary | +| ---------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| getComputedLeft/Top/Width/Height(), getComputedRight/Bottom(), and getComputedLayout() object shape | Compatible | Width, height, and normal left/top come from Taffy's unrounded layout followed by Yoga point-grid processing. Yoga right/bottom are stored trailing margin plus relative inset values rather than geometric remaining space, and can be reconstructed from finite declaration state. Root left/top also need Yoga's leading margin and relative inset because Taffy roots remain at zero. | +| getComputedMargin(), getComputedPadding(), and getComputedBorder() for Left/Top/Right/Bottom/Start/End | Compatible | Taffy's unrounded Layout contains the resolved physical records; Start and End map through the resolved layout direction, and auto declarations mask Taffy's distributed margin back to Yoga's reported zero. Yoga resolves a vertical percentage margin against containing width for the computed-margin value but against containing height when that same margin participates in absolute Top or Bottom placement, so projection keeps those two bases separate. Bounded comparisons cover positive and negative point margins, percentage padding and margins, physical and logical edges under LTR and RTL, auto margins, borders, and overflow. | +| Computed margin, padding, or border queried with Horizontal, Vertical, or All | Compatible | Reject these multi-edge shorthands; Yoga aborts rather than returning an aggregate result. Exact error class and wording are deferred. | +| Point-scale rounding | Compatible | It is an output conversion over unrounded layouts, including Yoga's ceil-sensitive text-node sizes, and does not require a Taffy core change. | +| Ordinary non-degenerate fixed sizes, grow or shrink without a listed Difference trigger, percentages, and simple nested flex trees | Compatible | Bounded differential probes found matching unrounded geometry outside the explicit difference triggers below. A committed real-Yoga oracle suite remains a release gate and must not be presented as a compatibility percentage. | +| min/max combined with flex growth or shrinkage | Different | Yoga and Taffy can produce different final sizes; the API remains available with Taffy's stable result. | +| Flex shrink factors whose positive total is below one | Different | Yoga scales the shrink contribution so negative free space is fully consumed, while Taffy applies the factors directly and can retain overflow. With point rounding disabled, width-80 children with shrink factors 0.5 and 0 in a width-100 Row resolve to widths 20 and 80 in Yoga but 50 and 80 in Taffy. Correcting this would replace Taffy's flex-sizing result rather than project output. | +| Percentage main-axis gap in an auto-sized container | Different | Yoga resolves the gap against the resulting main size and includes it in that size, while Taffy does not let the percentage gap contribute to intrinsic main sizing. With point rounding disabled, two height-20 children and a 10% Row gap produce root heights 44 in Yoga and 40 in Taffy; both place the second child at top 24. Reproducing Yoga's circular gap sizing would require algorithm-specific container sizing. | +| Conflicting min/max on an intrinsically sized selected root | Different | Yoga routes roots with no definite size through root-kind-specific sizing modes before applying conflicting constraints, while Taffy retains its ordinary minimum-first result. With point rounding disabled, both a root containing a width-25 child and a measured root returning width 75 resolve to width 50 in Yoga but width 100 in Taffy when min-width is 100 and max-width is 50. Definite root sizes remain Compatible through bounded calculation-only translation. Reproducing Yoga's empty, Flex-content, and Measure sizing dispatch would duplicate layout-algorithm control flow. | +| Overlapping physical and logical main-axis margins under a child direction that differs from its parent | Different | Yoga can use one direction while totaling margins for Flex sizing and the child's resolved direction when it stores computed physical margins. In a width-100 LTR Row, a width-20 RTL child with margin-left 4 and margin-start 10% reports computed margins left 4 and right 10 in both engines, but Yoga resolves width 16 while Taffy remains width 20. Reproducing the hidden intermediate margin total would require Flex-specific size correction, so Taffy's stable result is retained. | +| aspectRatio in calculated layout | Different | Yoga derives the cross size from the final flex main size, while Taffy follows its own CSS-oriented aspect-ratio and stretch rules. With rounding disabled, a 100x100 Row parent and width-40 ratio-2 child produce 40x20 in Yoga but 40x100 in Taffy; a Column child declaring width 40, height 30, and ratio 2 produces 60x30 in Yoga but 40x30 in Taffy. Min/max and flex growth diverge further. Aligning all cases would require parent- and algorithm-dependent size rewriting, which crosses the accepted JavaScript boundary. | +| MeasureMode-sensitive callbacks | Different | Taffy does not preserve whether a finite constraint originated as Yoga Exactly or AtMost and may request intrinsic measurements in different phases. The bridge therefore gives each Taffy request a meaningful Yoga-shaped mode but does not promise a Yoga callback trace. Fixed-size and constraint-insensitive callbacks remain useful; callbacks branching on MeasureMode can have stable result differences. | +| WebFlexBasis disabled across a changed containing size | Different | Yoga 3.2.1 can reuse a stale computed percentage flex basis when the experiment is false, while Taffy recomputes it. Emulating Yoga's stale cache would require Yoga-internal flex-basis state and is outside the boundary; the enabled Yoga result matched the tested Taffy relayout. | +| Attached-subtree calculation followed by an ancestor calculation that Yoga serves from cache | Different | Yoga can retain geometry written by the separate subtree calculation because the clean ancestor does not revisit it. Taffy may recompute that ancestor after internal Style translation propagated native dirty state and can therefore produce current ancestor-context geometry instead. The package keeps calculateLayout available on both nodes and does not reproduce Yoga's complete cache algorithm. | +| Oversized WrapReverse lines with distribution-style alignContent | Different | With point rounding disabled, a 100x30 Row/WrapReverse container holding three 60-point items on separate 10, 20, and 15-point lines produces Yoga top positions 20, 0, and -15 but Taffy positions 35, 15, and 0 under Stretch, SpaceBetween, SpaceAround, or SpaceEvenly. The content's 45-point cross size exceeds the 30-point container; FlexStart, FlexEnd, and Center are not covered by this difference. Reconstructing Yoga's negative-free-space line placement would duplicate Flex logic. | +| A non-overflowing WrapReverse line with a cross-axis margin | Different | Cross-axis margin placement can also differ without overflow or distribution-style alignContent. In a 100x100 Row/WrapReverse container with alignItems FlexStart, a 40x20 child with an 11-point bottom margin has top 80 in Yoga and top 69 in Taffy. Correcting this requires reproducing WrapReverse line and item alignment, so Taffy's placement is retained. | +| Oversized single line on a reversed main axis with distribution-style justifyContent | Different | When a RowReverse or ColumnReverse no-wrap line overflows and justifyContent is SpaceBetween, SpaceAround, or SpaceEvenly, Yoga and Taffy choose different negative-free-space fallback placement. In a 240-point RowReverse line with 140, 130, and 120-point children, Yoga left positions are 100, -30, and -150 while Taffy reports 250, 120, and 0. This affects sibling distribution and is not an output-only correction. | +| Zero-cross-size wrapped line under Stretch item alignment | Different | A wrapped line whose items all resolve to zero cross size stays on Yoga's cross-start or cross-end edge, while Taffy distributes the zero-size line under FlexEnd, Center, SpaceAround, or SpaceEvenly alignContent. For a 180-point cross axis, ordinary Wrap plus Center places Yoga at 0 and Taffy at 90; WrapReverse plus Center places Yoga at 180 and Taffy at 90. Reconstructing this degenerate line placement would duplicate Flex logic. | +| Main-axis auto margins on a reversed axis or combined with a nonzero justifyContent offset | Different | Yoga lets positive-free-space main-axis auto margins consume the remaining space and can suppress justifyContent, while Taffy can distribute the margin and retain an additional offset. In a width-240 RowReverse, a width-60 child with margin-left auto has left 180 in Yoga and 360 in Taffy even under FlexStart. In a 240x180 Column with justifyContent Center, Yoga child tops are 65 and 160 while Taffy reports 130 and 225. Correcting either sibling distribution would duplicate Flex logic, so Taffy's geometry is retained and computed auto margins remain masked to zero. | +| Relative insets, including reversed axes | Compatible | Relative positioning does not participate in normal-flow sizing. When Taffy and Yoga apply opposite signs on a reversed axis, the projection can remove Taffy's declaration-derived offset and apply Yoga's declaration-derived offset to the Node's projected position and descendant absolute accumulation without redistributing siblings or calculating Flex geometry. A combination that also triggers the main-axis auto-margin difference remains covered by the Different row above. | ### Yoga-shaped output projection diff --git a/.agents/docs/taffyjs-yoga-wasm-package.md b/.agents/docs/taffyjs-yoga-wasm-package.md new file mode 100644 index 0000000..7080e4a --- /dev/null +++ b/.agents/docs/taffyjs-yoga-wasm-package.md @@ -0,0 +1,40 @@ +# @taffyjs/yoga-wasm Package Design + +`@taffyjs/yoga-wasm` is the separate Wasm transport for the Yoga 3.2.1 compatibility facade. It builds the authored source owned by `packages/taffyjs-yoga/src` against the public `@taffyjs/wasm` API and does not copy, fork, or independently evolve the facade. + +## Public contract + +- The package is ESM-only and exposes the same root and `/load` shapes, declarations, constants, and Yoga compatibility classifications as `@taffyjs/yoga`. +- The package supports Node.js `>=22.18` and modern bundled browsers within the existing `@taffyjs/wasm` contract. It does not claim direct CDN, legacy-browser, Bun, Deno, workerd, or other edge-runtime support without separate evidence. +- Consumers may install it under Yoga's original name with the registry alias `"yoga-layout": "npm:@taffyjs/yoga-wasm@"`; supported application imports remain unchanged. +- `PositionType.Static` and every other Unsupported capability stay unsupported. Replacing the runtime transport does not create missing Taffy layout semantics. + +## Source and package boundary + +`packages/taffyjs-yoga-wasm` has no facade source. Its Vite+ entries point to the sibling Yoga package's `src/index.ts` and `src/load.ts`; a filtered resolver replaces only the exact bare `@taffyjs/node` import with an external bare `@taffyjs/wasm` import. The emitted Yoga artifact is otherwise host-neutral. + +The package has one runtime dependency, `@taffyjs/wasm`. It must not depend on `@taffyjs/yoga` or `@taffyjs/node`, which would pull the native package into the Wasm transport. It does not embed another Wasm payload: the consuming tool resolves `@taffyjs/wasm` and owns selection and bundling of that package's default or browser graph. + +`packages/taffyjs-yoga-wasm/dist` is ignored build output, matching the direct Wasm and native Yoga packages. Every Yoga Wasm verification branch builds it before inspecting declarations, package contents, tarballs, or runtime behavior; publication must likewise build first. + +The declaration bundle uses the tsconfig located beside the shared Yoga source. With the pinned TypeScript 7 and declaration generator, placing that tsconfig under the Yoga Wasm package makes the generator treat the wrong directory as `rootDir`, fail with TS6059, and potentially leave declarations beside the shared source. Clean builds and package checks therefore require the sibling Yoga tsconfig and reject any `.d.ts` written under `packages/taffyjs-yoga/src`. + +## Loading and facade ownership + +The root entry imports the facade normally and creates one eager Yoga object. The `/load` entry exports enums and types without statically importing the facade; `loadYoga()` dynamically imports it and then calls `createYoga()`. In a browser, WebAssembly compilation therefore begins only when `loadYoga()` is called. + +JavaScript module caching means repeated `loadYoga()` calls share the loaded `@taffyjs/wasm` module and its memory. Each call still creates a separate logical facade, hidden `TaffyTree`, Node and Config registry, and object brand. Cross-facade handles are rejected before reaching Taffy. + +## Failure boundary + +Expected validation, callback, and Taffy errors retain the compatibility facade's retry and state-commit rules. Unexpected Rust panics inherit `@taffyjs/wasm` behavior: the Wasm module or worker can abort instead of unwinding to JavaScript as the native binding does. The facade must not claim that its JavaScript poisoning path can recover an already aborted Wasm runtime. + +## Verification contract + +- Build the same Yoga source against `@taffyjs/wasm`, then reject any emitted runtime reference to `@taffyjs/node`, any backend reference in public declarations, a second inline Wasm payload, or declarations written beside the shared source. +- Run the complete maintained Yoga behavior suite unchanged against the Wasm backend, including the subprocess path-selection fixture, and reuse the public declaration test. +- Pack both `@taffyjs/wasm` and `@taffyjs/yoga-wasm`; install the latter under the bare `yoga-layout` dependency name in fresh npm and pnpm consumers; run both Node entries. +- From the installed tarballs, build bare `yoga-layout` and `yoga-layout/load` imports with Vite. Each browser bundle must select the `@taffyjs/wasm` browser condition, contain one inline payload and one `WebAssembly.compile`, emit no `.wasm` asset, and contain no `node:wasi` path. +- Run both installed browser entries in real Chromium without cross-origin isolation or `SharedArrayBuffer`. Importing `/load` must compile zero Wasm modules; the first `loadYoga()` call must compile exactly one. + +The Yoga Wasm checks live in the existing Linux-only `check:wasm` graph because they require the WASIP Rust target and Chromium. Native cross-platform checks continue to own `@taffyjs/yoga`. diff --git a/.agents/docs/technology-stack.md b/.agents/docs/technology-stack.md index 14b664f..2940c36 100644 --- a/.agents/docs/technology-stack.md +++ b/.agents/docs/technology-stack.md @@ -24,11 +24,13 @@ The `@taffyjs/wasm` Vite+ pack configuration compiles that same entry twice. A f The root Vite+ task graph builds the napi-rs artifacts into `packages/taffyjs-wasm/.napi-build`, runs `vp pack` with `dist` cleanup enabled for the two public entries, then runs `tools/taffy-wasm/generate-inline-wasm-runtime-files.ts`. That typed, package-specific generator validates and base64-encodes the staged release binary, copies the deferred loader unchanged, generates the small host adapters, and transforms the freshly generated eager Node loader only at asserted input and WASI-construction sites. Because only final runtime files are written after `vp pack` cleans `dist`, the build has no separate package-finalization or deletion phase. `dist` is ignored build output: CI, package inspection, and packed-consumer tests build it before use rather than committing compiler output to Git. -For `packages/taffyjs-yoga`, `vp pack` builds the root and `/load` entries plus shared chunks and declarations over the public `@taffyjs/node` dependency. Its build cleans the complete output directory first, and CI freshness checks include untracked chunks before comparing the committed artifacts. +For `packages/taffyjs-yoga`, `vp pack` builds the root and `/load` entries plus shared chunks and declarations over the public `@taffyjs/node` dependency. Its `dist` is ignored build output: the native task graph builds it before runtime and declaration tests, and any future publication workflow must build it before packing or publishing. Test-only ambient declarations resolve authored Yoga test types to source so a clean checkout does not require `dist` without remapping the public runtime specifiers that Bun, Deno, and Node resolve; exact Vite aliases, subprocess entry injection, runtime smoke checks, and the dedicated public-declaration check still exercise the built package entries. + +For `packages/taffyjs-yoga-wasm`, Vite+ builds those same sibling source entries and redirects only the exact bare `@taffyjs/node` import to the external public `@taffyjs/wasm` dependency. The package owns no facade source and emits no second Wasm payload. Its `dist` is ignored build output; the Wasm verification graph builds it before type, package, tarball, Node, bundle, or Chromium checks. `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. -CI has five jobs. Ubuntu x64 and Windows x64 each build the native addon and run all Rust, JavaScript, and type tests with Node.js 22.18.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 separate Ubuntu WASIP job installs the Rust target and Playwright Chromium, builds `@taffyjs/wasm`, reruns the complete Node public API suite against it, runs type, package-content, packed-consumer, bundled-consumer, and browser-runtime checks against the generated package, and applies the same Bun 1.2.0 and Deno 2.2.0 smoke checks to the public entry. Alternate-runtime smoke checks perform one fixed layout rather than copying the complete behavior suite. A Node-only job checks formatting, JavaScript and repository TypeScript including maintained tools through Vite+'s type-aware lint path, and generated-source drift. A Rust-only job checks formatting and Clippy. macOS and publication workflows are not configured. +CI has five jobs. Ubuntu x64 and Windows x64 each build the native addon and run all Rust, JavaScript, and type tests with Node.js 22.18.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 separate Ubuntu WASIP job installs the Rust target and Playwright Chromium, builds `@taffyjs/wasm`, reruns the complete Node public API suite against it, runs type, package-content, packed-consumer, bundled-consumer, and browser-runtime checks against the generated package, and applies the same Bun 1.2.0 and Deno 2.2.0 smoke checks to the public entry. That job also builds `@taffyjs/yoga-wasm`, reruns the complete maintained Yoga behavior and declaration suites, inspects and installs the packed package with npm and pnpm, and exercises both public entries in bundled Chromium. Alternate-runtime smoke checks perform one fixed layout rather than copying the complete behavior suite. A Node-only job checks formatting, JavaScript and repository TypeScript including maintained tools through Vite+'s type-aware lint path, and generated-source drift. A Rust-only job checks formatting and Clippy. macOS and publication workflows are not configured. ## Public website @@ -44,10 +46,10 @@ 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 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. +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—and the Yoga Wasm build depends on that completed public backend. Native build completion precedes package-local and consumer tests. Repository formatting completes before build-backed native tests so generated artifacts cannot race the formatter; lint and Rust checks remain independent. Rust tests are part of the full test task, while Rust formatting and Clippy remain separate checks. 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. -`check:wasm` is a separate Linux-capable graph rather than a dependency of the cross-platform default `check:test`: it requires the `wasm32-wasip1` Rust target and a Playwright browser. Independent verification branches run in parallel after the shared Wasm build, while browser-bundle inspection waits only for its consumer build. Existing native jobs therefore keep their target assumptions while the dedicated WASIP job exercises the complete package boundary. +`check:wasm` is a separate Linux-capable graph rather than a dependency of the cross-platform default `check:test`: it requires the `wasm32-wasip1` Rust target and a Playwright browser. Independent direct and Yoga verification branches run in parallel after their shared Wasm build, while browser-bundle inspection waits only for its consumer build. Existing native jobs therefore keep their target assumptions while the dedicated WASIP job exercises both complete package boundaries. The corresponding rulings are recorded in [tooling decisions](tooling-decisions.md). diff --git a/.agents/docs/tooling-decisions.md b/.agents/docs/tooling-decisions.md index 037bd65..45a290e 100644 --- a/.agents/docs/tooling-decisions.md +++ b/.agents/docs/tooling-decisions.md @@ -4,6 +4,18 @@ This ledger records only tooling judgments that Yunfei explicitly expressed for ## Decided +### Third-party license filename + +[VOUCHED @hyfdev 2026-08-16] + +**Ruling:** A package or distributable artifact that ships third-party license text must collect it in a root-level file named exactly `THIRD-PARTY-LICENSES`, without a filename extension. Do not use `THIRD_PARTY_NOTICES.md` or another naming variant. + +**Limits:** This establishes the repository filename and consolidation convention. It does not decide whether a particular dependency or derived implementation requires attribution, alter the required license text, or replace package-specific verification that the file is included in the published artifact. + +**Why:** Yunfei selected one explicit filename for this repository's third-party license bundles; no additional rationale was given. + +**Source:** Yunfei (`@hyfdev`), 2026-08-16; explicitly required `THIRD-PARTY-LICENSES` instead of `THIRD_PARTY_NOTICES.md`, extended the convention to future analogous cases, and asked to vouch it. + ### @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, 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. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8dd0853..8667b82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,9 +33,7 @@ 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 - name: Verify committed package outputs are current - run: | - git add --intent-to-add --all -- packages/taffyjs-yoga/dist - git diff --exit-code -- packages/taffyjs-node/index.js packages/taffyjs-node/index.d.ts packages/taffyjs-node/binding.js packages/taffyjs-node/binding.d.ts packages/taffyjs-yoga/dist + run: git diff --exit-code -- packages/taffyjs-node/index.js packages/taffyjs-node/index.d.ts packages/taffyjs-node/binding.js packages/taffyjs-node/binding.d.ts test-windows-x64-msvc: name: Build and test Windows x64 diff --git a/.gitignore b/.gitignore index b0c3164..493c5f4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ coverage/ packages/taffyjs-wasm/dist/ packages/taffyjs-wasm/.napi-build/ +packages/taffyjs-yoga/dist/ +packages/taffyjs-yoga-wasm/dist/ tests/taffyjs-wasm/browser/dist/ apps/website/.vitepress/cache/ diff --git a/README.md b/README.md index 5225a37..21dc4da 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ TaffyJS brings the Taffy layout engine to JavaScript through native, WebAssembly - **Native performance:** Runs Taffy as native Rust code in Node.js through Node-API. - **WebAssembly support:** Runs Taffy in bundled browsers and deployments where a native addon is not the right fit. - **One API, two runtimes:** The same JavaScript API, backed by native code in `@taffyjs/node` for performance or WebAssembly in `@taffyjs/wasm` for flexibility. -- **Yoga compatibility:** Use `@taffyjs/yoga` or `@taffyjs/yoga-wasm` (work in progress) while preserving Yoga's API shape for straightforward migration. +- **Yoga compatibility:** Use `@taffyjs/yoga` or `@taffyjs/yoga-wasm` while preserving Yoga's API shape for straightforward migration. ## Installation @@ -24,7 +24,7 @@ npm install @taffyjs/node | [`@taffyjs/node`](https://www.npmjs.com/package/@taffyjs/node) | Native Node.js runtime for maximum performance. | — | | [`@taffyjs/wasm`](https://www.npmjs.com/package/@taffyjs/wasm) | WebAssembly runtime for flexible deployment. | — | | [`@taffyjs/yoga`](https://www.npmjs.com/package/@taffyjs/yoga) | Yoga-compatible API backed by `@taffyjs/node`. | — | -| [`@taffyjs/yoga-wasm`](https://www.npmjs.com/package/@taffyjs/yoga-wasm) | Yoga-compatible API backed by `@taffyjs/wasm`. | WIP | +| [`@taffyjs/yoga-wasm`](https://www.npmjs.com/package/@taffyjs/yoga-wasm) | Yoga-compatible API backed by `@taffyjs/wasm`. | — | ## Runtime compatibility @@ -42,6 +42,7 @@ TaffyJS supports Node.js 22.18+, Bun 1.2+ within the Bun 1 major, and Deno 2.2+ - [Guide](apps/website/guide/index.md) explains the shared layout model, layout modes, and text and image measurement. - [`@taffyjs/node`](apps/website/node/index.md) documents runtime support and the current public API. - [`@taffyjs/wasm`](apps/website/wasm/index.md) documents WebAssembly setup, supported environments, and differences from the native package. +- [`@taffyjs/yoga`](packages/taffyjs-yoga/README.md) and [`@taffyjs/yoga-wasm`](packages/taffyjs-yoga-wasm/README.md) document dependency substitution and the shared Yoga 3.2.1 compatibility boundary. ## Credits diff --git a/packages/taffyjs-yoga-wasm/README.md b/packages/taffyjs-yoga-wasm/README.md new file mode 100644 index 0000000..4c327a1 --- /dev/null +++ b/packages/taffyjs-yoga-wasm/README.md @@ -0,0 +1,52 @@ +# @taffyjs/yoga-wasm + +`@taffyjs/yoga-wasm` is an ESM Yoga 3.2.1 compatibility facade backed by `@taffyjs/wasm`. It uses the same TypeScript facade, public entries, declarations, and compatibility classifications as `@taffyjs/yoga`; only the TaffyJS runtime backend changes. + +The package supports Node.js `>=22.18` and modern bundled browsers within the [documented `@taffyjs/wasm` runtime boundary](https://github.com/hyfdev/taffyjs/blob/main/packages/taffyjs-wasm/README.md). Browser consumers need a bundler that honors package export conditions and top-level await. Direct CDN scripts, legacy browsers, and non-browser edge runtimes are not currently supported. + +This package is still private and unpublished. The installation form below describes the intended registry substitution after publication. + +## Substitute it for yoga-layout + +Use a package-manager alias so application source can keep importing `yoga-layout` unchanged: + +```json +{ + "dependencies": { + "yoga-layout": "npm:@taffyjs/yoga-wasm@" + } +} +``` + +The eager root entry keeps Yoga's normal import shape: + +```ts +import Yoga from "yoga-layout"; + +const node = Yoga.Node.create(); +node.setWidth(100); +node.calculateLayout(undefined, undefined); +console.log(node.getComputedWidth()); +node.free(); +``` + +The `/load` entry defers loading and compiling the WebAssembly backend until `loadYoga()` is called: + +```ts +import { loadYoga } from "yoga-layout/load"; + +const Yoga = await loadYoga(); +const node = Yoga.Node.create(); +node.calculateLayout(undefined, undefined); +node.free(); +``` + +Each `loadYoga()` call creates an isolated Yoga facade and hidden Taffy tree. Facades share the package's loaded WebAssembly module and memory, but Nodes and Configs cannot cross facade boundaries. + +## Compatibility boundary + +The supported Yoga surface and known layout differences are exactly those documented for [`@taffyjs/yoga`](https://github.com/hyfdev/taffyjs/blob/main/packages/taffyjs-yoga/COMPATIBILITY.md). In particular, `PositionType.Static` remains unsupported because changing the transport to WebAssembly does not add a Taffy representation for Yoga's static-positioning semantics. + +Expected validation and callback failures remain recoverable. An unexpected Rust panic follows the `@taffyjs/wasm` abort boundary and can terminate the current WebAssembly module or worker rather than unwinding like the native Node backend. + +[THIRD-PARTY-LICENSES](THIRD-PARTY-LICENSES) contains the required third-party license for the Yoga PixelGrid logic shared by both compatibility packages. diff --git a/packages/taffyjs-yoga-wasm/THIRD-PARTY-LICENSES b/packages/taffyjs-yoga-wasm/THIRD-PARTY-LICENSES new file mode 100644 index 0000000..c06696f --- /dev/null +++ b/packages/taffyjs-yoga-wasm/THIRD-PARTY-LICENSES @@ -0,0 +1,15 @@ +# Third-Party Licenses + +## Yoga 3.2.1 + +The point-grid rounding implementation compiled into this package from `../taffyjs-yoga/src/projection.ts` is derived from Yoga 3.2.1's `yoga/algorithm/PixelGrid.cpp`. + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/taffyjs-yoga-wasm/package.json b/packages/taffyjs-yoga-wasm/package.json new file mode 100644 index 0000000..afacc6e --- /dev/null +++ b/packages/taffyjs-yoga-wasm/package.json @@ -0,0 +1,47 @@ +{ + "name": "@taffyjs/yoga-wasm", + "version": "0.0.0", + "private": true, + "description": "Yoga-compatible facade backed by TaffyJS WebAssembly.", + "license": "UNLICENSED", + "repository": { + "type": "git", + "url": "git+https://github.com/hyfdev/taffyjs.git", + "directory": "packages/taffyjs-yoga-wasm" + }, + "files": [ + "README.md", + "THIRD-PARTY-LICENSES", + "dist" + ], + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./load": { + "types": "./dist/load.d.ts", + "default": "./dist/load.js" + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "vp pack", + "check": "vp check" + }, + "dependencies": { + "@taffyjs/wasm": "workspace:*" + }, + "devDependencies": { + "@types/node": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + "vite-plus": "catalog:" + }, + "engines": { + "node": ">=22.18.0" + } +} diff --git a/packages/taffyjs-yoga-wasm/tsconfig.json b/packages/taffyjs-yoga-wasm/tsconfig.json new file mode 100644 index 0000000..f2f1b41 --- /dev/null +++ b/packages/taffyjs-yoga-wasm/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["es2023"], + "moduleDetection": "force", + "module": "nodenext", + "moduleResolution": "nodenext", + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": ["vite.config.ts"] +} diff --git a/packages/taffyjs-yoga-wasm/vite.config.ts b/packages/taffyjs-yoga-wasm/vite.config.ts new file mode 100644 index 0000000..39dd366 --- /dev/null +++ b/packages/taffyjs-yoga-wasm/vite.config.ts @@ -0,0 +1,52 @@ +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { defineConfig, type Plugin } from "vite-plus"; + +const packageDirectory = dirname(fileURLToPath(import.meta.url)); +const yogaSourceDirectory = resolve(packageDirectory, "../taffyjs-yoga/src"); + +function resolveWasmBackend(): Plugin { + return { + name: "resolve-yoga-wasm-backend", + resolveId: { + filter: { id: /^@taffyjs\/node$/ }, + handler() { + return { + id: "@taffyjs/wasm", + external: true, + }; + }, + }, + }; +} + +export default defineConfig({ + pack: { + entry: { + index: resolve(yogaSourceDirectory, "index.ts"), + load: resolve(yogaSourceDirectory, "load.ts"), + }, + format: "esm", + outDir: "dist", + root: yogaSourceDirectory, + clean: true, + dts: { + tsconfig: resolve(packageDirectory, "../taffyjs-yoga/tsconfig.json"), + }, + platform: "neutral", + target: "es2022", + fixedExtension: false, + plugins: [resolveWasmBackend()], + }, + fmt: { + ignorePatterns: ["dist"], + }, + lint: { + ignorePatterns: ["dist"], + options: { + typeAware: true, + typeCheck: true, + }, + }, +}); diff --git a/packages/taffyjs-yoga/COMPATIBILITY.md b/packages/taffyjs-yoga/COMPATIBILITY.md index dd424cb..535d249 100644 --- a/packages/taffyjs-yoga/COMPATIBILITY.md +++ b/packages/taffyjs-yoga/COMPATIBILITY.md @@ -14,7 +14,7 @@ Compatible numeric comparisons allow an absolute error below `0.0001` for finite - A successful calculation commits Yoga-shaped output for the complete selected subtree. `getComputedLayout()` and the six scalar layout getters preserve Yoga's left, right, top, bottom, width, and height semantics across LTR, RTL, reversed axes, relative positioning, and absolute positioning. - `getComputedMargin()`, `getComputedPadding()`, and `getComputedBorder()` support Left, Top, Right, Bottom, Start, and End. Auto margins report zero, and Horizontal, Vertical, and All are rejected. - Point-grid conversion follows each Node's own Config, including factor zero, mixed factors in one tree, and positive infinity. Computed edges other than left and top remain unrounded, as in Yoga. -- Ordinary fixed and percentage sizes, simple grow and shrink without min/max, and simple nested Flex trees match the pinned Yoga oracle. This is a bounded compatibility corpus, not a universal compatibility percentage. +- Bounded fixtures for ordinary fixed and percentage sizes, grow or shrink without a listed Difference trigger, and simple nested Flex trees match the pinned Yoga oracle. This is a bounded compatibility corpus, not a universal compatibility percentage. ## Measurement @@ -74,21 +74,23 @@ An upstream pull request is not a support promise or an ETA. Before reclassifica These rows are deliberate, version-specific differences with regression fixtures: -| Capability and trigger | `@taffyjs/yoga` behavior | Yoga 3.2.1 behavior | -| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| Change `useWebDefaults` on a live Config, then calculate a tree whose unresolved shrink values are already cached | Applies the current Config-sensitive Style when the selected calculation reaches Taffy. In the fixture, two 80-point children in a 100-point row become 50/50. | The Config setter does not advance Yoga's cache version, so the same calculation retains 80/80 even though `getFlexShrink()` reports 1. | -| `setDirtiedFunc()` callback argument and `setDirtiedFunc(null)` | Calls a real callback with its canonical Node after state commit; null unsets the callback. | Calls a real callback with zero arguments; null is retained and a later clean-to-dirty transition throws a wrapper TypeError. | -| A min or max constraint combined with Flex growth or shrinkage | Keeps Taffy's Flex result. The pinned width-120, max-width-90, shrink-1 child in a width-60 Row resolves to 60. | The pinned child resolves to 67.5; other combinations in this result group can likewise follow Yoga's different min/max ordering. | -| A selected root without a definite size has a conflicting minimum and maximum on that axis | Keeps Taffy's minimum-first constraint result. The child-content and measured fixtures both resolve a min-width-100/max-width-50 root to 100. | Root sizing mode affects Yoga's constraint order. The pinned 25-point child-content and measured-width-75 fixtures both resolve the root to 50. | -| Overlapping physical and logical main-axis margins under a child direction that differs from its parent | Resolves the declarations to one physical Taffy margin rect. A width-20 RTL child with `margin-left: 4` and `margin-start: 10%` in a width-100 LTR Row remains width 20. | Computed margins are also left 4 and right 10, but Yoga's separate Flex margin resolution reduces the child width to 16. | -| Calculated `aspectRatio` | Keeps Taffy's CSS-oriented aspect-ratio and stretch result. A width-40, ratio-2 child stretched by a 100-by-100 Row is 40-by-100. | The same child is 40-by-20. | -| `WebFlexBasis` disabled while a percentage basis is recalculated under a changed containing size | Recomputes the percentage basis. A 50% basis changes from 50 to 100 when its Row changes from width 100 to 200. | Reuses the stale 50-point basis while the experiment is disabled; enabling the experiment produces 100 in both implementations. | -| Calculate an attached subtree with different owner dimensions, then calculate a clean ancestor | Restoring the selected Node's ordinary native Style invalidates Taffy's ancestor cache. The fixture returns the attached Row from width 100 to 200 and its children from 50 to 80. | The clean ancestor retains the attached-subtree result: Row width 100 and child width 50. | -| Oversized `WrapReverse` lines with distribution-style `alignContent` | Keeps Taffy's line placement. Three 60-point lines with heights 10, 20, and 15 in a 100-by-30 Row have tops 35, 15, and 0 under Stretch. | The same tops are 20, 0, and -15. | -| A non-overflowing `WrapReverse` line with a cross-axis margin | Keeps Taffy's cross-axis margin placement. A 40-by-20 child with an 11-point bottom margin in a 100-by-100 Row has top 69 under `alignItems: FlexStart`. | The same child has top 80. | -| An oversized no-wrap reversed main axis with `SpaceBetween`, `SpaceAround`, or `SpaceEvenly` | Keeps Taffy's negative-free-space fallback. Widths 140, 130, and 120 in a width-240 `RowReverse` with `SpaceBetween` have left positions 250, 120, and 0. | The same left positions are 100, -30, and -150. | -| A wrapped line whose stretched items resolve to zero cross size under distribution-style `alignContent` | Keeps Taffy's line distribution. Two 60-point, flex-basis-zero children in a 100-by-180 Row with `alignContent: Center` both have top 90 for Wrap and WrapReverse. | Both children have top 0 for Wrap and 180 for WrapReverse. | -| Main-axis auto margins on a reversed axis or combined with a nonzero `justifyContent` offset | Keeps Taffy's distribution and masks computed auto margins to zero. A width-60 child with `margin-left: auto` in a width-240 `RowReverse` has left 360; the Center fixture has child tops 130 and 225. | Yoga places the reversed child at left 180; auto margins suppress the Center offset and produce tops 65 and 160. | -| Measure callback count, order, and exact mode trace | Maps every Taffy request independently. A constant 30-by-10 callback for a child in a 100-by-40 Row runs four times in the pinned fixture and still produces the same 30-by-40 layout. | Runs once with width `AtMost(100)` and height `Exactly(40)` in the same fixture. | -| A Measure callback whose result changes with `MeasureMode` | Keeps Taffy's multi-stage result. In the pinned 100-by-40 Row, a callback returning width 80 for `Undefined` and 20 for `AtMost` produces width 80. | Calls the callback with `AtMost(100)` and produces width 20. | -| Replace or remove a Measure callback on an otherwise clean Node | Privately invalidates Taffy's measurement cache. Replacing a cached width-10 callback with width 20 produces 20 on the next calculation; removing it then produces zero. | Leaves the Node publicly clean and reuses the cached width 10 after both operations. | +| Capability and trigger | `@taffyjs/yoga` behavior | Yoga 3.2.1 behavior | +| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| Change `useWebDefaults` on a live Config, then calculate a tree whose unresolved shrink values are already cached | Applies the current Config-sensitive Style when the selected calculation reaches Taffy. In the fixture, two 80-point children in a 100-point row become 50/50. | The Config setter does not advance Yoga's cache version, so the same calculation retains 80/80 even though `getFlexShrink()` reports 1. | +| `setDirtiedFunc()` callback argument and `setDirtiedFunc(null)` | Calls a real callback with its canonical Node after state commit; null unsets the callback. | Calls a real callback with zero arguments; null is retained and a later clean-to-dirty transition throws a wrapper TypeError. | +| A min or max constraint combined with Flex growth or shrinkage | Keeps Taffy's Flex result. The pinned width-120, max-width-90, shrink-1 child in a width-60 Row resolves to 60. | The pinned child resolves to 67.5; other combinations in this result group can likewise follow Yoga's different min/max ordering. | +| Flex shrink factors whose positive total is below one | Keeps Taffy's partial shrink. In the fixture, width-80 children with shrink factors 0.5 and 0 in a width-100 Row resolve to 50 and 80. | Scales the shrink contribution to consume all negative free space, so the same children resolve to 20 and 80. | +| A percentage main-axis gap in an auto-sized container | Does not add the percentage gap to the container's intrinsic main size. Two height-20 children with a 10% Row gap resolve the root height to 40 while placing the second child at 24. | Resolves the same gap against the resulting size and includes it in the root size, producing height 44 while also placing the second child at 24. | +| A selected root without a definite size has a conflicting minimum and maximum on that axis | Keeps Taffy's minimum-first constraint result. The child-content and measured fixtures both resolve a min-width-100/max-width-50 root to 100. | Root sizing mode affects Yoga's constraint order. The pinned 25-point child-content and measured-width-75 fixtures both resolve the root to 50. | +| Overlapping physical and logical main-axis margins under a child direction that differs from its parent | Resolves the declarations to one physical Taffy margin rect. A width-20 RTL child with `margin-left: 4` and `margin-start: 10%` in a width-100 LTR Row remains width 20. | Computed margins are also left 4 and right 10, but Yoga's separate Flex margin resolution reduces the child width to 16. | +| Calculated `aspectRatio` | Keeps Taffy's CSS-oriented aspect-ratio and stretch result. A width-40, ratio-2 child stretched by a 100-by-100 Row is 40-by-100. | The same child is 40-by-20. | +| `WebFlexBasis` disabled while a percentage basis is recalculated under a changed containing size | Recomputes the percentage basis. A 50% basis changes from 50 to 100 when its Row changes from width 100 to 200. | Reuses the stale 50-point basis while the experiment is disabled; enabling the experiment produces 100 in both implementations. | +| Calculate an attached subtree with different owner dimensions, then calculate a clean ancestor | Restoring the selected Node's ordinary native Style invalidates Taffy's ancestor cache. The fixture returns the attached Row from width 100 to 200 and its children from 50 to 80. | The clean ancestor retains the attached-subtree result: Row width 100 and child width 50. | +| Oversized `WrapReverse` lines with distribution-style `alignContent` | Keeps Taffy's line placement. Three 60-point lines with heights 10, 20, and 15 in a 100-by-30 Row have tops 35, 15, and 0 under Stretch. | The same tops are 20, 0, and -15. | +| A non-overflowing `WrapReverse` line with a cross-axis margin | Keeps Taffy's cross-axis margin placement. A 40-by-20 child with an 11-point bottom margin in a 100-by-100 Row has top 69 under `alignItems: FlexStart`. | The same child has top 80. | +| An oversized no-wrap reversed main axis with `SpaceBetween`, `SpaceAround`, or `SpaceEvenly` | Keeps Taffy's negative-free-space fallback. Widths 140, 130, and 120 in a width-240 `RowReverse` with `SpaceBetween` have left positions 250, 120, and 0. | The same left positions are 100, -30, and -150. | +| A wrapped line whose stretched items resolve to zero cross size under distribution-style `alignContent` | Keeps Taffy's line distribution. Two 60-point, flex-basis-zero children in a 100-by-180 Row with `alignContent: Center` both have top 90 for Wrap and WrapReverse. | Both children have top 0 for Wrap and 180 for WrapReverse. | +| Main-axis auto margins on a reversed axis or combined with a nonzero `justifyContent` offset | Keeps Taffy's distribution and masks computed auto margins to zero. A width-60 child with `margin-left: auto` in a width-240 `RowReverse` has left 360; the Center fixture has child tops 130 and 225. | Yoga places the reversed child at left 180; auto margins suppress the Center offset and produce tops 65 and 160. | +| Measure callback count, order, and exact mode trace | Maps every Taffy request independently. A constant 30-by-10 callback for a child in a 100-by-40 Row runs four times in the pinned fixture and still produces the same 30-by-40 layout. | Runs once with width `AtMost(100)` and height `Exactly(40)` in the same fixture. | +| A Measure callback whose result changes with `MeasureMode` | Keeps Taffy's multi-stage result. In the pinned 100-by-40 Row, a callback returning width 80 for `Undefined` and 20 for `AtMost` produces width 80. | Calls the callback with `AtMost(100)` and produces width 20. | +| Replace or remove a Measure callback on an otherwise clean Node | Privately invalidates Taffy's measurement cache. Replacing a cached width-10 callback with width 20 produces 20 on the next calculation; removing it then produces zero. | Leaves the Node publicly clean and reuses the cached width 10 after both operations. | diff --git a/packages/taffyjs-yoga/README.md b/packages/taffyjs-yoga/README.md index 898d1b2..98dbc7e 100644 --- a/packages/taffyjs-yoga/README.md +++ b/packages/taffyjs-yoga/README.md @@ -76,7 +76,7 @@ A broad `Align` variable must be narrowed before being passed to one of these se The compatibility baseline is exactly Yoga 3.2.1. Config, Style, tree ownership and mutation, calculation on any live Node, computed output, dirty and new-layout state, and synchronous Measure callbacks are implemented. Taffy owns topology and Flex calculation; the facade retains only the Yoga declarations and public state needed to translate inputs and project Yoga-shaped outputs. -This is core behavioral compatibility, not a claim that Taffy and Yoga produce identical geometry for every degenerate Flex combination or that their internal cache and callback traces are identical. [COMPATIBILITY.md](COMPATIBILITY.md) classifies the complete public surface and lists every known Difference and Unsupported trigger. [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) contains the required third-party notice for the Yoga PixelGrid logic used by the facade. +This is core behavioral compatibility, not a claim that Taffy and Yoga produce identical geometry for every degenerate Flex combination or that their internal cache and callback traces are identical. [COMPATIBILITY.md](COMPATIBILITY.md) classifies the complete public surface and lists every known Difference and Unsupported trigger. [THIRD-PARTY-LICENSES](THIRD-PARTY-LICENSES) contains the required third-party license for the Yoga PixelGrid logic used by the facade. ## Versioning before 1.0 diff --git a/packages/taffyjs-yoga/THIRD_PARTY_NOTICES.md b/packages/taffyjs-yoga/THIRD-PARTY-LICENSES similarity index 98% rename from packages/taffyjs-yoga/THIRD_PARTY_NOTICES.md rename to packages/taffyjs-yoga/THIRD-PARTY-LICENSES index 3ed6e14..66d19f3 100644 --- a/packages/taffyjs-yoga/THIRD_PARTY_NOTICES.md +++ b/packages/taffyjs-yoga/THIRD-PARTY-LICENSES @@ -1,4 +1,4 @@ -# Third-Party Notices +# Third-Party Licenses ## Yoga 3.2.1 diff --git a/packages/taffyjs-yoga/dist/facade-CjSanW_Y.js b/packages/taffyjs-yoga/dist/facade-CjSanW_Y.js deleted file mode 100644 index 2a2130b..0000000 --- a/packages/taffyjs-yoga/dist/facade-CjSanW_Y.js +++ /dev/null @@ -1,1958 +0,0 @@ -import { AlignContent, AlignItems, AvailableSpace, AvailableSpaceKind, BoxSizing, Dimension, Direction, Display, FlexDirection, FlexWrap, Overflow, Position, TaffyTree } from "@taffyjs/node"; -//#region src/enums.ts -let Align = /* @__PURE__ */ function(Align) { - Align[Align["Auto"] = 0] = "Auto"; - Align[Align["FlexStart"] = 1] = "FlexStart"; - Align[Align["Center"] = 2] = "Center"; - Align[Align["FlexEnd"] = 3] = "FlexEnd"; - Align[Align["Stretch"] = 4] = "Stretch"; - Align[Align["Baseline"] = 5] = "Baseline"; - Align[Align["SpaceBetween"] = 6] = "SpaceBetween"; - Align[Align["SpaceAround"] = 7] = "SpaceAround"; - Align[Align["SpaceEvenly"] = 8] = "SpaceEvenly"; - return Align; -}({}); -let BoxSizing$1 = /* @__PURE__ */ function(BoxSizing) { - BoxSizing[BoxSizing["BorderBox"] = 0] = "BorderBox"; - BoxSizing[BoxSizing["ContentBox"] = 1] = "ContentBox"; - return BoxSizing; -}({}); -let Dimension$1 = /* @__PURE__ */ function(Dimension) { - Dimension[Dimension["Width"] = 0] = "Width"; - Dimension[Dimension["Height"] = 1] = "Height"; - return Dimension; -}({}); -let Direction$1 = /* @__PURE__ */ function(Direction) { - Direction[Direction["Inherit"] = 0] = "Inherit"; - Direction[Direction["LTR"] = 1] = "LTR"; - Direction[Direction["RTL"] = 2] = "RTL"; - return Direction; -}({}); -let Display$1 = /* @__PURE__ */ function(Display) { - Display[Display["Flex"] = 0] = "Flex"; - Display[Display["None"] = 1] = "None"; - return Display; -}({}); -let Edge = /* @__PURE__ */ function(Edge) { - Edge[Edge["Left"] = 0] = "Left"; - Edge[Edge["Top"] = 1] = "Top"; - Edge[Edge["Right"] = 2] = "Right"; - Edge[Edge["Bottom"] = 3] = "Bottom"; - Edge[Edge["Start"] = 4] = "Start"; - Edge[Edge["End"] = 5] = "End"; - Edge[Edge["Horizontal"] = 6] = "Horizontal"; - Edge[Edge["Vertical"] = 7] = "Vertical"; - Edge[Edge["All"] = 8] = "All"; - return Edge; -}({}); -let Errata = /* @__PURE__ */ function(Errata) { - Errata[Errata["None"] = 0] = "None"; - return Errata; -}({}); -let ExperimentalFeature = /* @__PURE__ */ function(ExperimentalFeature) { - ExperimentalFeature[ExperimentalFeature["WebFlexBasis"] = 0] = "WebFlexBasis"; - return ExperimentalFeature; -}({}); -let FlexDirection$1 = /* @__PURE__ */ function(FlexDirection) { - FlexDirection[FlexDirection["Column"] = 0] = "Column"; - FlexDirection[FlexDirection["ColumnReverse"] = 1] = "ColumnReverse"; - FlexDirection[FlexDirection["Row"] = 2] = "Row"; - FlexDirection[FlexDirection["RowReverse"] = 3] = "RowReverse"; - return FlexDirection; -}({}); -let Gutter = /* @__PURE__ */ function(Gutter) { - Gutter[Gutter["Column"] = 0] = "Column"; - Gutter[Gutter["Row"] = 1] = "Row"; - Gutter[Gutter["All"] = 2] = "All"; - return Gutter; -}({}); -let Justify = /* @__PURE__ */ function(Justify) { - Justify[Justify["FlexStart"] = 0] = "FlexStart"; - Justify[Justify["Center"] = 1] = "Center"; - Justify[Justify["FlexEnd"] = 2] = "FlexEnd"; - Justify[Justify["SpaceBetween"] = 3] = "SpaceBetween"; - Justify[Justify["SpaceAround"] = 4] = "SpaceAround"; - Justify[Justify["SpaceEvenly"] = 5] = "SpaceEvenly"; - return Justify; -}({}); -let LogLevel = /* @__PURE__ */ function(LogLevel) { - LogLevel[LogLevel["Error"] = 0] = "Error"; - LogLevel[LogLevel["Warn"] = 1] = "Warn"; - LogLevel[LogLevel["Info"] = 2] = "Info"; - LogLevel[LogLevel["Debug"] = 3] = "Debug"; - LogLevel[LogLevel["Verbose"] = 4] = "Verbose"; - LogLevel[LogLevel["Fatal"] = 5] = "Fatal"; - return LogLevel; -}({}); -let MeasureMode = /* @__PURE__ */ function(MeasureMode) { - MeasureMode[MeasureMode["Undefined"] = 0] = "Undefined"; - MeasureMode[MeasureMode["Exactly"] = 1] = "Exactly"; - MeasureMode[MeasureMode["AtMost"] = 2] = "AtMost"; - return MeasureMode; -}({}); -let NodeType = /* @__PURE__ */ function(NodeType) { - NodeType[NodeType["Default"] = 0] = "Default"; - NodeType[NodeType["Text"] = 1] = "Text"; - return NodeType; -}({}); -let Overflow$1 = /* @__PURE__ */ function(Overflow) { - Overflow[Overflow["Visible"] = 0] = "Visible"; - Overflow[Overflow["Hidden"] = 1] = "Hidden"; - Overflow[Overflow["Scroll"] = 2] = "Scroll"; - return Overflow; -}({}); -let PositionType = /* @__PURE__ */ function(PositionType) { - PositionType[PositionType["Relative"] = 1] = "Relative"; - PositionType[PositionType["Absolute"] = 2] = "Absolute"; - return PositionType; -}({}); -let Unit = /* @__PURE__ */ function(Unit) { - Unit[Unit["Undefined"] = 0] = "Undefined"; - Unit[Unit["Point"] = 1] = "Point"; - Unit[Unit["Percent"] = 2] = "Percent"; - Unit[Unit["Auto"] = 3] = "Auto"; - return Unit; -}({}); -let Wrap = /* @__PURE__ */ function(Wrap) { - Wrap[Wrap["NoWrap"] = 0] = "NoWrap"; - Wrap[Wrap["Wrap"] = 1] = "Wrap"; - Wrap[Wrap["WrapReverse"] = 2] = "WrapReverse"; - return Wrap; -}({}); -const legacyConstants = { - ALIGN_AUTO: 0, - ALIGN_FLEX_START: 1, - ALIGN_CENTER: 2, - ALIGN_FLEX_END: 3, - ALIGN_STRETCH: 4, - ALIGN_BASELINE: 5, - ALIGN_SPACE_BETWEEN: 6, - ALIGN_SPACE_AROUND: 7, - ALIGN_SPACE_EVENLY: 8, - BOX_SIZING_BORDER_BOX: 0, - BOX_SIZING_CONTENT_BOX: 1, - DIMENSION_WIDTH: 0, - DIMENSION_HEIGHT: 1, - DIRECTION_INHERIT: 0, - DIRECTION_LTR: 1, - DIRECTION_RTL: 2, - DISPLAY_FLEX: 0, - DISPLAY_NONE: 1, - EDGE_LEFT: 0, - EDGE_TOP: 1, - EDGE_RIGHT: 2, - EDGE_BOTTOM: 3, - EDGE_START: 4, - EDGE_END: 5, - EDGE_HORIZONTAL: 6, - EDGE_VERTICAL: 7, - EDGE_ALL: 8, - ERRATA_NONE: 0, - EXPERIMENTAL_FEATURE_WEB_FLEX_BASIS: 0, - FLEX_DIRECTION_COLUMN: 0, - FLEX_DIRECTION_COLUMN_REVERSE: 1, - FLEX_DIRECTION_ROW: 2, - FLEX_DIRECTION_ROW_REVERSE: 3, - GUTTER_COLUMN: 0, - GUTTER_ROW: 1, - GUTTER_ALL: 2, - JUSTIFY_FLEX_START: 0, - JUSTIFY_CENTER: 1, - JUSTIFY_FLEX_END: 2, - JUSTIFY_SPACE_BETWEEN: 3, - JUSTIFY_SPACE_AROUND: 4, - JUSTIFY_SPACE_EVENLY: 5, - LOG_LEVEL_ERROR: 0, - LOG_LEVEL_WARN: 1, - LOG_LEVEL_INFO: 2, - LOG_LEVEL_DEBUG: 3, - LOG_LEVEL_VERBOSE: 4, - LOG_LEVEL_FATAL: 5, - MEASURE_MODE_UNDEFINED: 0, - MEASURE_MODE_EXACTLY: 1, - MEASURE_MODE_AT_MOST: 2, - NODE_TYPE_DEFAULT: 0, - NODE_TYPE_TEXT: 1, - OVERFLOW_VISIBLE: 0, - OVERFLOW_HIDDEN: 1, - OVERFLOW_SCROLL: 2, - POSITION_TYPE_RELATIVE: 1, - POSITION_TYPE_ABSOLUTE: 2, - UNIT_UNDEFINED: 0, - UNIT_POINT: 1, - UNIT_PERCENT: 2, - UNIT_AUTO: 3, - WRAP_NO_WRAP: 0, - WRAP_WRAP: 1, - WRAP_WRAP_REVERSE: 2 -}; -//#endregion -//#region src/values.ts -const percentPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?%$/i; -function float32(value) { - if (!Number.isFinite(value)) return value; - if (value === 0) return 0; - return Math.fround(value); -} -function finiteFloat32(value) { - const normalized = float32(value); - return Number.isFinite(normalized) ? normalized : void 0; -} -function undefinedValue() { - return { - unit: 0, - value: NaN - }; -} -function autoValue() { - return { - unit: 3, - value: NaN - }; -} -function pointValue(value) { - return { - unit: 1, - value - }; -} -function percentValue(value) { - return { - unit: 2, - value - }; -} -function cloneValue(value) { - return { - unit: value.unit, - value: value.value - }; -} -function sameValue(left, right) { - return left.unit === right.unit && Object.is(left.value, right.value); -} -function publicValue(value) { - return { - value: value.value, - unit: value.unit - }; -} -function resolvePercentage(basis, percentage) { - return Math.fround(Math.fround(basis * percentage) * Math.fround(.01)); -} -function normalizeLength(value, name, allowAuto) { - if (value === void 0) return undefinedValue(); - if (typeof value === "number") { - if (!Number.isFinite(value)) return undefinedValue(); - const normalized = finiteFloat32(value); - return normalized === void 0 ? undefinedValue() : pointValue(normalized); - } - if (typeof value !== "string") throw new TypeError(`${name} must be a number, percentage,${allowAuto ? " auto," : ""} or undefined`); - if (allowAuto && value === "auto") return autoValue(); - if (!percentPattern.test(value)) throw new TypeError(`${name} must be a number, percentage,${allowAuto ? " auto," : ""} or undefined`); - const numeric = Number(value.slice(0, -1)); - if (!Number.isFinite(numeric)) throw new TypeError(`${name} percentage must be finite`); - const normalized = finiteFloat32(numeric); - return normalized === void 0 ? undefinedValue() : percentValue(normalized); -} -function normalizePercent(value, name) { - if (value === void 0) return undefinedValue(); - if (typeof value !== "number") throw new TypeError(`${name} must be a number or undefined`); - if (!Number.isFinite(value)) return undefinedValue(); - const normalized = finiteFloat32(value); - return normalized === void 0 ? undefinedValue() : percentValue(normalized); -} -function normalizePoint(value, name) { - if (value === void 0) return void 0; - if (typeof value !== "number") throw new TypeError(`${name} must be a number or undefined`); - if (!Number.isFinite(value)) return void 0; - return finiteFloat32(value); -} -function normalizeFlexNumber(value, name) { - if (value === void 0) return void 0; - if (typeof value !== "number") throw new TypeError(`${name} must be a number or undefined`); - if (Number.isNaN(value)) return void 0; - return float32(value); -} -function normalizeAspectRatio(value) { - if (value === void 0) return void 0; - if (typeof value !== "number") throw new TypeError("aspectRatio must be a number or undefined"); - if (!Number.isFinite(value) || value === 0) return void 0; - const normalized = finiteFloat32(value); - return normalized === void 0 || normalized === 0 ? void 0 : normalized; -} -function toDimension(value) { - switch (value.unit) { - case 1: return Dimension.Length(value.value); - case 2: return Dimension.Percent(value.value); - case 3: - case 0: return Dimension.Auto; - } -} -function toSizeDimension(value) { - if ((value.unit === 1 || value.unit === 2) && value.value < 0) return Dimension.Auto; - return toDimension(value); -} -function toMinDimension(value) { - return value.unit === 0 ? Dimension.Length(0) : toDimension(value); -} -function toLengthPercentage(value) { - switch (value.unit) { - case 1: return Dimension.Length(value.value); - case 2: return Dimension.Percent(value.value); - case 3: - case 0: return 0; - } -} -function toLengthPercentageAuto(value) { - return value.unit === 0 ? 0 : toDimension(value); -} -//#endregion -//#region src/declarations.ts -function edgeValues(create) { - return [ - create(), - create(), - create(), - create(), - create(), - create(), - create(), - create(), - create() - ]; -} -function gutterValues(create) { - return [ - create(), - create(), - create() - ]; -} -function createDeclarations(useWebDefaults) { - return { - alignContent: useWebDefaults ? 4 : 1, - alignItems: 4, - alignSelf: 0, - aspectRatio: void 0, - border: edgeValues(() => void 0), - direction: 0, - display: 0, - flex: void 0, - flexBasis: autoValue(), - flexDirection: useWebDefaults ? 2 : 0, - flexGrow: void 0, - flexShrink: void 0, - flexWrap: 0, - height: autoValue(), - justifyContent: 0, - gap: gutterValues(undefinedValue), - margin: edgeValues(undefinedValue), - maxHeight: undefinedValue(), - maxWidth: undefinedValue(), - minHeight: undefinedValue(), - minWidth: undefinedValue(), - overflow: 0, - padding: edgeValues(undefinedValue), - position: edgeValues(undefinedValue), - positionType: 1, - boxSizing: 0, - width: autoValue() - }; -} -function cloneDeclarations(value) { - return { - ...value, - border: [...value.border], - flexBasis: cloneValue(value.flexBasis), - height: cloneValue(value.height), - gap: value.gap.map(cloneValue), - margin: value.margin.map(cloneValue), - maxHeight: cloneValue(value.maxHeight), - maxWidth: cloneValue(value.maxWidth), - minHeight: cloneValue(value.minHeight), - minWidth: cloneValue(value.minWidth), - padding: value.padding.map(cloneValue), - position: value.position.map(cloneValue), - width: cloneValue(value.width) - }; -} -function sameNumber(left, right) { - return Object.is(left, right); -} -function sameArray(left, right, same) { - return left.every((value, index) => same(value, right[index])); -} -function sameDeclarations(left, right) { - return left.alignContent === right.alignContent && left.alignItems === right.alignItems && left.alignSelf === right.alignSelf && sameNumber(left.aspectRatio, right.aspectRatio) && sameArray(left.border, right.border, sameNumber) && left.direction === right.direction && left.display === right.display && sameNumber(left.flex, right.flex) && sameValue(left.flexBasis, right.flexBasis) && left.flexDirection === right.flexDirection && sameNumber(left.flexGrow, right.flexGrow) && sameNumber(left.flexShrink, right.flexShrink) && left.flexWrap === right.flexWrap && sameValue(left.height, right.height) && left.justifyContent === right.justifyContent && sameArray(left.gap, right.gap, sameValue) && sameArray(left.margin, right.margin, sameValue) && sameValue(left.maxHeight, right.maxHeight) && sameValue(left.maxWidth, right.maxWidth) && sameValue(left.minHeight, right.minHeight) && sameValue(left.minWidth, right.minWidth) && left.overflow === right.overflow && sameArray(left.padding, right.padding, sameValue) && sameArray(left.position, right.position, sameValue) && left.positionType === right.positionType && left.boxSizing === right.boxSizing && sameValue(left.width, right.width); -} -function edgeCandidates(edge, direction) { - switch (edge) { - case 0: return [ - direction === 1 ? 4 : 5, - 0, - 6, - 8 - ]; - case 2: return [ - direction === 1 ? 5 : 4, - 2, - 6, - 8 - ]; - case 1: return [ - 1, - 7, - 8 - ]; - case 3: return [ - 3, - 7, - 8 - ]; - default: throw new TypeError("A physical edge is required"); - } -} -function resolveEdge(values, edge, direction, isDefined, fallback) { - for (const candidate of edgeCandidates(edge, direction)) { - const value = values[candidate]; - if (isDefined(value)) return value; - } - return fallback(); -} -function resolveGutter(values, gutter, isDefined, fallback) { - const direct = values[gutter]; - if (isDefined(direct)) return direct; - const all = values[2]; - return isDefined(all) ? all : fallback(); -} -//#endregion -//#region src/projection.ts -function initialComputedEdges() { - return { - left: 0, - top: 0, - right: 0, - bottom: 0 - }; -} -function floatAdd(left, right) { - return Math.fround(left + right); -} -function floatSubtract(left, right) { - return Math.fround(left - right); -} -function inexactEquals(left, right) { - if (!Number.isNaN(left) && !Number.isNaN(right)) return Math.abs(left - right) < 1e-4; - return Number.isNaN(left) && Number.isNaN(right); -} -function roundValueToPixelGrid(value, pointScaleFactor, forceCeil, forceFloor) { - let scaledValue = value * pointScaleFactor; - let fractional = scaledValue % 1; - if (fractional < 0) fractional += 1; - if (inexactEquals(fractional, 0)) scaledValue -= fractional; - else if (inexactEquals(fractional, 1)) scaledValue = scaledValue - fractional + 1; - else if (forceCeil) scaledValue = scaledValue - fractional + 1; - else if (forceFloor) scaledValue -= fractional; - else scaledValue = scaledValue - fractional + (!Number.isNaN(fractional) && (fractional > .5 || inexactEquals(fractional, .5)) ? 1 : 0); - return Math.fround(Number.isNaN(scaledValue) || Number.isNaN(pointScaleFactor) ? NaN : scaledValue / pointScaleFactor); -} -function roundLayout(layout, absoluteLeft, absoluteTop, pointScaleFactor, textRounding) { - if (pointScaleFactor === 0) return layout; - const absoluteRight = absoluteLeft + layout.width; - const absoluteBottom = absoluteTop + layout.height; - const fractionalWidth = layout.width * pointScaleFactor % 1; - const fractionalHeight = layout.height * pointScaleFactor % 1; - const hasFractionalWidth = !inexactEquals(fractionalWidth, 0) && !inexactEquals(fractionalWidth, 1); - const hasFractionalHeight = !inexactEquals(fractionalHeight, 0) && !inexactEquals(fractionalHeight, 1); - return { - left: roundValueToPixelGrid(layout.left, pointScaleFactor, false, textRounding), - right: layout.right, - top: roundValueToPixelGrid(layout.top, pointScaleFactor, false, textRounding), - bottom: layout.bottom, - width: Math.fround(roundValueToPixelGrid(absoluteRight, pointScaleFactor, textRounding && hasFractionalWidth, textRounding && !hasFractionalWidth) - roundValueToPixelGrid(absoluteLeft, pointScaleFactor, false, textRounding)), - height: Math.fround(roundValueToPixelGrid(absoluteBottom, pointScaleFactor, textRounding && hasFractionalHeight, textRounding && !hasFractionalHeight) - roundValueToPixelGrid(absoluteTop, pointScaleFactor, false, textRounding)) - }; -} -function physicalValue$1(values, edge, direction) { - return resolveEdge(values, edge, direction, (value) => value.unit !== 0, () => ({ - unit: 0, - value: NaN - })); -} -function resolvePosition(value, basis) { - switch (value.unit) { - case 1: return value.value; - case 2: return basis === void 0 ? void 0 : resolvePercentage(basis, value.value); - case 3: - case 0: return; - } -} -function resolveTaffyPosition(value, basis) { - return value.unit === 2 && basis !== void 0 ? Math.fround(basis * Math.fround(value.value / 100)) : resolvePosition(value, basis); -} -function axisRelativePosition(declarations, start, end, direction, basis, resolve = resolvePosition) { - const resolvedStart = resolve(physicalValue$1(declarations.position, start, direction), basis); - if (resolvedStart !== void 0) return resolvedStart; - const resolvedEnd = resolve(physicalValue$1(declarations.position, end, direction), basis); - return resolvedEnd === void 0 ? 0 : Math.fround(-resolvedEnd); -} -function nativeMarginEdges(entry, suppressed) { - if (suppressed) return initialComputedEdges(); - const { declarations, direction, nativeLayout } = entry; - const read = (edge) => physicalValue$1(declarations.margin, edge, direction).unit === 3 ? 0 : nativeLayout.margin[edge === 0 ? "left" : edge === 1 ? "top" : edge === 2 ? "right" : "bottom"]; - return { - left: read(0), - top: read(1), - right: read(2), - bottom: read(3) - }; -} -function declaredMarginEdges(entry, basis, suppressed) { - if (suppressed) return initialComputedEdges(); - const read = (edge) => { - const value = physicalValue$1(entry.declarations.margin, edge, entry.direction); - return value.unit === 3 ? 0 : resolvePosition(value, basis) ?? 0; - }; - return { - left: read(0), - top: read(1), - right: read(2), - bottom: read(3) - }; -} -function nativeEdges(edges, suppressed) { - return suppressed ? initialComputedEdges() : { - left: edges.left, - top: edges.top, - right: edges.right, - bottom: edges.bottom - }; -} -function resolveMainAxis(flexDirection, direction) { - if (direction === 2) { - if (flexDirection === 2) return 3; - if (flexDirection === 3) return 2; - } - return flexDirection; -} -function resolveCrossAxis(mainAxis, direction) { - return mainAxis === 0 || mainAxis === 1 ? resolveMainAxis(2, direction) : 0; -} -function isHorizontal(axis) { - return axis === 2 || axis === 3; -} -function startsAtEnd(axis) { - return axis === 3 || axis === 1; -} -function innerWidth(layout) { - return Math.max(0, Math.fround(layout.size.width - layout.padding.left - layout.padding.right - layout.border.left - layout.border.right)); -} -function innerHeight(layout) { - return Math.max(0, Math.fround(layout.size.height - layout.padding.top - layout.padding.bottom - layout.border.top - layout.border.bottom)); -} -function absoluteAxisLocation(entry, parentEntry, horizontal) { - const parentLayout = parentEntry.nativeLayout; - const containingWidth = Math.max(0, Math.fround(parentLayout.size.width - parentLayout.border.left - parentLayout.border.right)); - const containingHeight = Math.max(0, Math.fround(parentLayout.size.height - parentLayout.border.top - parentLayout.border.bottom)); - const start = horizontal ? parentEntry.direction === 2 ? 2 : 0 : 1; - const end = horizontal ? parentEntry.direction === 2 ? 0 : 2 : 3; - const positionBasis = horizontal ? containingWidth : containingHeight; - const startPosition = resolvePosition(physicalValue$1(entry.declarations.position, start, parentEntry.direction), positionBasis); - const endPosition = resolvePosition(physicalValue$1(entry.declarations.position, end, parentEntry.direction), positionBasis); - const selectedEdge = startPosition === void 0 ? end : start; - const selectedPosition = startPosition ?? endPosition; - if (selectedPosition === void 0) return void 0; - const selectedMargin = resolvePosition(physicalValue$1(entry.declarations.margin, selectedEdge, parentEntry.direction), positionBasis) ?? 0; - const selectedBorder = selectedEdge === 0 ? parentLayout.border.left : selectedEdge === 1 ? parentLayout.border.top : selectedEdge === 2 ? parentLayout.border.right : parentLayout.border.bottom; - const offset = floatAdd(floatAdd(selectedPosition, selectedMargin), selectedBorder); - if (!(selectedEdge === 2 || selectedEdge === 3)) return { - location: offset, - trailing: void 0 - }; - return { - location: floatSubtract(floatSubtract(horizontal ? parentLayout.size.width : parentLayout.size.height, horizontal ? entry.nativeLayout.size.width : entry.nativeLayout.size.height), offset), - trailing: startPosition === void 0 ? void 0 : offset - }; -} -function projectOutputs(entries, options) { - const states = []; - for (const [index, entry] of entries.entries()) { - const parent = entry.parentIndex === null ? void 0 : states[entry.parentIndex]; - if (entry.parentIndex !== null && parent === void 0) throw new Error("Yoga projection requires parents before children"); - const suppressed = parent?.suppressed === true || index !== 0 && entry.declarations.display === 1; - const parentEntry = entry.parentIndex === null ? void 0 : entries[entry.parentIndex]; - const containingWidth = parentEntry === void 0 ? options.ownerWidth : Math.max(0, Math.fround(parentEntry.nativeLayout.size.width - parentEntry.nativeLayout.border.left - parentEntry.nativeLayout.border.right)); - const margin = entry.declarations.positionType === 2 ? declaredMarginEdges(entry, containingWidth, suppressed) : nativeMarginEdges(entry, suppressed); - const positionMargin = entry.declarations.positionType === 2 && parentEntry !== void 0 ? declaredMarginEdges(entry, innerWidth(parentEntry.nativeLayout), suppressed) : margin; - const padding = nativeEdges(entry.nativeLayout.padding, suppressed); - const border = nativeEdges(entry.nativeLayout.border, suppressed); - let rawLayout; - let absoluteLeft; - let absoluteTop; - if (suppressed) { - rawLayout = { - left: 0, - right: 0, - top: 0, - bottom: 0, - width: 0, - height: 0 - }; - absoluteLeft = parent?.absoluteLeft ?? 0; - absoluteTop = parent?.absoluteTop ?? 0; - } else if (parent === void 0) { - const positionDirection = options.rootHasOwner ? entry.direction : 1; - const relativeX = axisRelativePosition(entry.declarations, positionDirection === 2 ? 2 : 0, positionDirection === 2 ? 0 : 2, positionDirection, options.ownerWidth); - const relativeY = axisRelativePosition(entry.declarations, 1, 3, positionDirection, options.ownerHeight); - rawLayout = { - left: floatAdd(margin.left, relativeX), - right: floatAdd(margin.right, relativeX), - top: floatAdd(margin.top, relativeY), - bottom: floatAdd(margin.bottom, relativeY), - width: entry.nativeLayout.size.width, - height: entry.nativeLayout.size.height - }; - absoluteLeft = rawLayout.left; - absoluteTop = rawLayout.top; - } else { - const parentEntry = entries[entry.parentIndex]; - const parentLayout = parentEntry.nativeLayout; - const ownerWidth = innerWidth(parentLayout); - const ownerHeight = innerHeight(parentLayout); - const relativeX = axisRelativePosition(entry.declarations, entry.direction === 2 ? 2 : 0, entry.direction === 2 ? 0 : 2, entry.direction, ownerWidth); - const relativeY = axisRelativePosition(entry.declarations, 1, 3, entry.direction, ownerHeight); - const mainAxis = resolveMainAxis(parentEntry.declarations.flexDirection, parentEntry.direction); - const crossAxis = resolveCrossAxis(mainAxis, parentEntry.direction); - const horizontalAxis = isHorizontal(mainAxis) ? mainAxis : crossAxis; - const verticalAxis = isHorizontal(mainAxis) ? crossAxis : mainAxis; - const horizontalStartsAtEnd = startsAtEnd(horizontalAxis); - const verticalStartsAtEnd = startsAtEnd(verticalAxis); - let left = entry.nativeLayout.location.x; - let top = entry.nativeLayout.location.y; - let reverseRight; - let reverseBottom; - if (entry.declarations.positionType === 1) { - const physicalLeftFirst = axisRelativePosition(entry.declarations, 0, 2, entry.direction, ownerWidth, resolveTaffyPosition); - const physicalRightFirst = axisRelativePosition(entry.declarations, 2, 0, entry.direction, ownerWidth, resolveTaffyPosition); - const taffyX = parentEntry.direction === 2 ? Math.fround(-physicalRightFirst) : physicalLeftFirst; - const taffyY = axisRelativePosition(entry.declarations, 1, 3, entry.direction, ownerHeight, resolveTaffyPosition); - const desiredX = horizontalStartsAtEnd ? Math.fround(-relativeX) : relativeX; - const desiredY = verticalStartsAtEnd ? Math.fround(-relativeY) : relativeY; - const flowLeft = floatSubtract(left, taffyX); - const flowTop = floatSubtract(top, taffyY); - left = floatAdd(flowLeft, desiredX); - top = floatAdd(flowTop, desiredY); - if (horizontalStartsAtEnd) reverseRight = floatAdd(floatSubtract(floatSubtract(parentLayout.size.width, entry.nativeLayout.size.width), flowLeft), relativeX); - if (verticalStartsAtEnd) reverseBottom = floatAdd(floatSubtract(floatSubtract(parentLayout.size.height, entry.nativeLayout.size.height), flowTop), relativeY); - } else { - const horizontalLocation = absoluteAxisLocation(entry, parentEntry, true); - const verticalLocation = absoluteAxisLocation(entry, parentEntry, false); - if (horizontalLocation !== void 0) { - left = horizontalLocation.location; - if (horizontalStartsAtEnd) reverseRight = horizontalLocation.trailing; - } - if (verticalLocation !== void 0) { - top = verticalLocation.location; - if (verticalStartsAtEnd) reverseBottom = verticalLocation.trailing; - } - } - rawLayout = { - left, - right: horizontalStartsAtEnd ? reverseRight ?? floatSubtract(floatSubtract(parentLayout.size.width, entry.nativeLayout.size.width), left) : floatAdd(positionMargin.right, relativeX), - top, - bottom: verticalStartsAtEnd ? reverseBottom ?? floatSubtract(floatSubtract(parentLayout.size.height, entry.nativeLayout.size.height), top) : floatAdd(positionMargin.bottom, relativeY), - width: entry.nativeLayout.size.width, - height: entry.nativeLayout.size.height - }; - absoluteLeft = parent.absoluteLeft + left; - absoluteTop = parent.absoluteTop + top; - } - states.push({ - layout: roundLayout(rawLayout, absoluteLeft, absoluteTop, entry.pointScaleFactor, entry.measured), - margin, - padding, - border, - direction: entry.direction, - absoluteLeft, - absoluteTop, - suppressed - }); - } - return states; -} -//#endregion -//#region src/measurement.ts -function mapConstraint(knownDimension, availableSpace, forceExactly) { - if (knownDimension !== void 0 || forceExactly) return { - mode: 1, - value: availableSpace.kind === AvailableSpaceKind.Definite ? Math.max(0, availableSpace.value) : Math.max(0, knownDimension ?? 0) - }; - switch (availableSpace.kind) { - case AvailableSpaceKind.Definite: return { - mode: 2, - value: Math.max(0, availableSpace.value) - }; - case AvailableSpaceKind.MinContent: return { - mode: 2, - value: 0 - }; - case AvailableSpaceKind.MaxContent: return { - mode: 0, - value: NaN - }; - } -} -function normalizeDimension(value) { - const number = Number(value); - return Number.isNaN(number) || number < 0 ? 0 : number; -} -function invokeYogaMeasure(args, measure, forceExactly) { - const width = mapConstraint(args.knownDimensions.width, args.availableSpace.width, forceExactly.width); - const height = mapConstraint(args.knownDimensions.height, args.availableSpace.height, forceExactly.height); - const { width: measuredWidth = NaN, height: measuredHeight = NaN } = measure(width.value, width.mode, height.value, height.mode); - return { - width: normalizeDimension(measuredWidth), - height: normalizeDimension(measuredHeight) - }; -} -//#endregion -//#region src/translate.ts -function alignItems(value) { - switch (value) { - case 1: return AlignItems.FlexStart; - case 2: return AlignItems.Center; - case 3: return AlignItems.FlexEnd; - case 4: return AlignItems.Stretch; - case 5: return AlignItems.Baseline; - default: throw new TypeError("Unsupported item alignment"); - } -} -function alignContent(value) { - switch (value) { - case 1: return AlignContent.FlexStart; - case 2: return AlignContent.Center; - case 3: return AlignContent.FlexEnd; - case 4: return AlignContent.Stretch; - case 6: return AlignContent.SpaceBetween; - case 7: return AlignContent.SpaceAround; - case 8: return AlignContent.SpaceEvenly; - default: throw new TypeError("Unsupported content alignment"); - } -} -function justifyContent(value) { - switch (value) { - case 0: return AlignContent.FlexStart; - case 1: return AlignContent.Center; - case 2: return AlignContent.FlexEnd; - case 3: return AlignContent.SpaceBetween; - case 4: return AlignContent.SpaceAround; - case 5: return AlignContent.SpaceEvenly; - } -} -function direction(value) { - return value === 2 ? Direction.Rtl : Direction.Ltr; -} -function flexDirection(value) { - switch (value) { - case 0: return FlexDirection.Column; - case 1: return FlexDirection.ColumnReverse; - case 2: return FlexDirection.Row; - case 3: return FlexDirection.RowReverse; - } -} -function flexWrap(value) { - switch (value) { - case 0: return FlexWrap.NoWrap; - case 1: return FlexWrap.Wrap; - case 2: return FlexWrap.WrapReverse; - } -} -function physicalValue(values, edge, resolvedDirection) { - return resolveEdge(values, edge, resolvedDirection, (value) => value.unit !== 0, undefinedValue); -} -function physicalNumber(values, edge, resolvedDirection) { - return resolveEdge(values, edge, resolvedDirection, (value) => value !== void 0, () => 0) ?? 0; -} -function rect(read) { - return { - left: read(0), - top: read(1), - right: read(2), - bottom: read(3) - }; -} -function effectiveFlexBasis(declarations, config) { - if (declarations.flexBasis.unit === 3 && declarations.flex !== void 0 && declarations.flex > 0 && !config.useWebDefaults) return Dimension.Length(0); - return toDimension(declarations.flexBasis); -} -function effectiveFlexGrow(declarations) { - if (declarations.flexGrow !== void 0) return declarations.flexGrow; - return declarations.flex !== void 0 && declarations.flex > 0 ? declarations.flex : 0; -} -function effectiveFlexShrink(declarations, config) { - if (declarations.flexShrink !== void 0) return declarations.flexShrink; - if (!config.useWebDefaults && declarations.flex !== void 0 && declarations.flex < 0) return -declarations.flex; - return config.useWebDefaults ? 1 : 0; -} -function translateStyle(declarations, config, resolvedDirection, ownerDirection) { - const absoluteDirection = declarations.positionType === 2 ? ownerDirection : resolvedDirection; - const margin = rect((edge) => toLengthPercentageAuto(physicalValue(declarations.margin, edge, absoluteDirection))); - const padding = rect((edge) => toLengthPercentage(physicalValue(declarations.padding, edge, resolvedDirection))); - const position = rect((edge) => toDimension(physicalValue(declarations.position, edge, absoluteDirection))); - const border = rect((edge) => physicalNumber(declarations.border, edge, resolvedDirection)); - const columnGap = resolveGutter(declarations.gap, 0, (value) => value.unit !== 0, undefinedValue); - const rowGap = resolveGutter(declarations.gap, 1, (value) => value.unit !== 0, undefinedValue); - return { - display: declarations.display === 1 ? Display.None : Display.Flex, - boxSizing: declarations.boxSizing === 1 ? BoxSizing.ContentBox : BoxSizing.BorderBox, - direction: direction(resolvedDirection), - overflow: { - x: declarations.overflow === 1 ? Overflow.Hidden : declarations.overflow === 2 ? Overflow.Scroll : Overflow.Visible, - y: declarations.overflow === 1 ? Overflow.Hidden : declarations.overflow === 2 ? Overflow.Scroll : Overflow.Visible - }, - position: declarations.positionType === 2 ? Position.Absolute : Position.Relative, - inset: position, - size: { - width: toSizeDimension(declarations.width), - height: toSizeDimension(declarations.height) - }, - minSize: { - width: toMinDimension(declarations.minWidth), - height: toMinDimension(declarations.minHeight) - }, - maxSize: { - width: toDimension(declarations.maxWidth), - height: toDimension(declarations.maxHeight) - }, - aspectRatio: declarations.aspectRatio ?? null, - margin, - padding, - border, - alignItems: alignItems(declarations.alignItems), - alignSelf: declarations.alignSelf === 0 ? null : alignItems(declarations.alignSelf), - alignContent: alignContent(declarations.alignContent), - justifyContent: justifyContent(declarations.justifyContent), - gap: { - width: toLengthPercentage(columnGap), - height: toLengthPercentage(rowGap) - }, - flexDirection: flexDirection(declarations.flexDirection), - flexWrap: flexWrap(declarations.flexWrap), - flexBasis: effectiveFlexBasis(declarations, config), - flexGrow: effectiveFlexGrow(declarations), - flexShrink: effectiveFlexShrink(declarations, config) - }; -} -function resolveLength(value, basis) { - switch (value.unit) { - case 1: return value.value; - case 2: return basis === void 0 ? void 0 : resolvePercentage(basis, value.value); - case 3: - case 0: return; - } -} -function exactRootDimension(declarations, axis, ownerSize, ownerWidth, resolvedDirection) { - if (ownerSize === void 0 || Number.isNaN(ownerSize) || ownerSize === Number.NEGATIVE_INFINITY) return; - const declared = axis === "width" ? declarations.width : declarations.height; - const maximum = axis === "width" ? declarations.maxWidth : declarations.maxHeight; - const declaredLength = resolveLength(declared, ownerSize); - if (declaredLength !== void 0 && declaredLength >= 0) return void 0; - if (resolveLength(maximum, ownerSize) !== void 0) return void 0; - const startEdge = axis === "width" ? 0 : 1; - const endEdge = axis === "width" ? 2 : 3; - const marginStart = resolveLength(physicalValue(declarations.margin, startEdge, resolvedDirection), ownerWidth) ?? 0; - const marginEnd = resolveLength(physicalValue(declarations.margin, endEdge, resolvedDirection), ownerWidth) ?? 0; - let outerSize = Math.fround(Math.fround(ownerSize - marginStart) - marginEnd); - if (declarations.boxSizing === 1) { - const paddingStart = Math.max(0, resolveLength(physicalValue(declarations.padding, startEdge, resolvedDirection), ownerWidth) ?? 0); - const paddingEnd = Math.max(0, resolveLength(physicalValue(declarations.padding, endEdge, resolvedDirection), ownerWidth) ?? 0); - const borderStart = Math.max(0, physicalNumber(declarations.border, startEdge, resolvedDirection)); - const borderEnd = Math.max(0, physicalNumber(declarations.border, endEdge, resolvedDirection)); - outerSize = Math.fround(Math.fround(Math.fround(Math.fround(outerSize - paddingStart) - paddingEnd) - borderStart) - borderEnd); - } - return Math.max(0, outerSize); -} -function rootMinimumOverride(declared, minimum, maximum, ownerSize) { - const declaredLength = resolveLength(declared, ownerSize); - const minimumLength = resolveLength(minimum, ownerSize); - const maximumLength = resolveLength(maximum, ownerSize); - return declaredLength !== void 0 && declaredLength >= 0 && minimumLength !== void 0 && minimumLength >= 0 && maximumLength !== void 0 && maximumLength >= 0 && minimumLength > maximumLength && declaredLength > maximumLength ? toDimension(maximum) : void 0; -} -function translateCalculationStyle(declarations, config, resolvedDirection, ownerDirection, ownerWidth, ownerHeight) { - const width = exactRootDimension(declarations, "width", ownerWidth, ownerWidth, resolvedDirection); - const height = exactRootDimension(declarations, "height", ownerHeight, ownerWidth, resolvedDirection); - const forceFlexDisplay = declarations.display === 1; - const minimumWidth = rootMinimumOverride(declarations.width, declarations.minWidth, declarations.maxWidth, ownerWidth); - const minimumHeight = rootMinimumOverride(declarations.height, declarations.minHeight, declarations.maxHeight, ownerHeight); - if (width === void 0 && height === void 0 && minimumWidth === void 0 && minimumHeight === void 0 && !forceFlexDisplay) return { - style: null, - exactWidth: false, - exactHeight: false - }; - const ordinary = translateStyle(declarations, config, resolvedDirection, ownerDirection); - return { - style: { - ...ordinary, - display: forceFlexDisplay ? Display.Flex : ordinary.display, - size: { - width: width === void 0 ? toSizeDimension(declarations.width) : Dimension.Length(width), - height: height === void 0 ? toSizeDimension(declarations.height) : Dimension.Length(height) - }, - minSize: { - width: minimumWidth ?? toMinDimension(declarations.minWidth), - height: minimumHeight ?? toMinDimension(declarations.minHeight) - } - }, - exactWidth: width !== void 0, - exactHeight: height !== void 0 - }; -} -function declarationDirection(declarations, ownerDirection) { - return declarations.direction === 0 ? ownerDirection : declarations.direction; -} -//#endregion -//#region src/facade.ts -const initialLayout = () => ({ - left: 0, - right: 0, - top: 0, - bottom: 0, - width: NaN, - height: NaN -}); -var ConfigState = class { - useWebDefaults = false; - pointScaleFactor = 1; - errata = 0; - webFlexBasis = false; - revision = 0; -}; -var FacadeRuntime = class { - tree = new TaffyTree(); - nodes = /* @__PURE__ */ new Map(); - defaultConfig = new ConfigState(); - measure = (args) => { - const callback = recordForId(this, args.node).measureFunction; - if (callback === null) return { - width: 0, - height: 0 - }; - const context = this.activeMeasureContext; - const isSelectedRoot = context?.root === args.node; - return invokeYogaMeasure(args, callback, { - width: isSelectedRoot === true && context.exactWidth, - height: isSelectedRoot === true && context.exactHeight - }); - }; - activeMeasureContext = null; - measurementRevision = 0; - poisoned = null; - constructor() { - this.tree.disableRounding(); - } -}; -function assertRuntimeUsable(runtime) { - if (runtime.poisoned !== null) throw runtime.poisoned; -} -function poisonRuntime(runtime, cause) { - const error = new Error("Yoga facade is unusable after an unexpected partial native failure", { cause }); - runtime.poisoned = error; - return error; -} -const configRecords = /* @__PURE__ */ new WeakMap(); -function requireBoolean(value, name) { - if (typeof value !== "boolean") throw new TypeError(`${name} must be a boolean`); - return value; -} -function requireEnum(value, name, allowed) { - if (typeof value !== "number" || !allowed.includes(value)) throw new TypeError(`${name} is unsupported`); - return value; -} -var YogaConfig = class { - constructor(runtime, state) { - assertRuntimeUsable(runtime); - configRecords.set(this, { - runtime, - state, - alive: true - }); - } - free() { - freeConfig(requireConfig(void 0, this)); - } - isExperimentalFeatureEnabled(feature) { - const record = requireConfig(void 0, this); - requireEnum(feature, "experimental feature", [0]); - return record.state.webFlexBasis; - } - setExperimentalFeatureEnabled(feature, enabled) { - const record = requireConfig(void 0, this); - requireEnum(feature, "experimental feature", [0]); - const next = requireBoolean(enabled, "enabled"); - if (record.state.webFlexBasis === next) return; - record.state.webFlexBasis = next; - record.state.revision += 1; - } - setPointScaleFactor(factor) { - const record = requireConfig(void 0, this); - if (typeof factor !== "number" || Number.isNaN(factor) || factor < 0) throw new TypeError("pointScaleFactor must be a non-negative number"); - const next = Number.isFinite(factor) ? Math.fround(factor) : factor; - if (Object.is(record.state.pointScaleFactor, next)) return; - record.state.pointScaleFactor = next; - record.state.revision += 1; - } - getErrata() { - return requireConfig(void 0, this).state.errata; - } - setErrata(errata) { - const record = requireConfig(void 0, this); - requireEnum(errata, "errata", [0]); - record.state.errata = 0; - } - useWebDefaults() { - return requireConfig(void 0, this).state.useWebDefaults; - } - setUseWebDefaults(useWebDefaults) { - const record = requireConfig(void 0, this); - const next = requireBoolean(useWebDefaults, "useWebDefaults"); - if (record.state.useWebDefaults === next) return; - record.state.useWebDefaults = next; - record.state.revision += 1; - } -}; -function requireConfig(runtime, value) { - if (!(value instanceof YogaConfig)) throw new TypeError("Expected a Yoga Config"); - const record = configRecords.get(value); - if (record === void 0) throw new Error("Yoga Config state is unavailable"); - if (runtime !== void 0 && record.runtime !== runtime) throw new TypeError("Config belongs to another Yoga facade"); - assertRuntimeUsable(record.runtime); - if (!record.alive) throw new Error("Config has been freed"); - return record; -} -function freeConfig(record) { - record.alive = false; -} -function requireCalculationDimension(value, name) { - if (value === void 0 || value === "auto" || Number.isNaN(value)) return AvailableSpace.MaxContent; - if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${name} must be a finite number, auto, or undefined`); - return value; -} -const nodeRecords = /* @__PURE__ */ new WeakMap(); -function requireNode(runtime, value) { - if (!(value instanceof YogaNode)) throw new TypeError("Expected a Yoga Node"); - const record = nodeRecords.get(value); - if (record === void 0) throw new Error("Yoga Node state is unavailable"); - if (runtime !== void 0 && record.runtime !== runtime) throw new TypeError("Node belongs to another Yoga facade"); - assertRuntimeUsable(record.runtime); - if (!record.alive) throw new Error("Node has been freed"); - return record; -} -function nodeForRecord(record) { - const node = record.runtime.nodes.get(record.nodeId); - if (node === void 0) throw poisonRuntime(record.runtime, /* @__PURE__ */ new Error("Node registry mismatch")); - return node; -} -function recordForId(runtime, nodeId) { - const node = runtime.nodes.get(nodeId); - if (node === void 0) throw poisonRuntime(runtime, /* @__PURE__ */ new Error("Node registry mismatch")); - const record = nodeRecords.get(node); - if (record === void 0 || !record.alive) throw poisonRuntime(runtime, /* @__PURE__ */ new Error("Node record mismatch")); - return record; -} -function collectAncestors(record, includeSelf) { - const records = []; - let nodeId = includeSelf ? record.nodeId : record.runtime.tree.getParent(record.nodeId); - while (nodeId !== null) { - const current = recordForId(record.runtime, nodeId); - records.push(current); - nodeId = record.runtime.tree.getParent(nodeId); - } - return records; -} -function collectSubtree(record) { - const records = []; - const pending = [record.nodeId]; - while (pending.length > 0) { - const nodeId = pending.pop(); - if (nodeId === void 0) break; - records.push(recordForId(record.runtime, nodeId)); - const children = record.runtime.tree.getChildren(nodeId); - for (let index = children.length - 1; index >= 0; index -= 1) pending.push(children[index]); - } - return records; -} -function markDirtyRecords(records) { - const notifications = []; - for (const record of records) { - if (record.dirty) continue; - record.dirty = true; - if (record.dirtiedFunction !== null) notifications.push([record.dirtiedFunction, nodeForRecord(record)]); - } - for (const [callback, node] of notifications) callback(node); -} -function effectiveDirection(record, declarations = record.declarations) { - return declarationDirection(declarations, record.ownerDirection); -} -function applyDeclarations(record, declarations) { - if (sameDeclarations(record.declarations, declarations)) return; - const dirtyRecords = collectAncestors(record, true); - const direction = effectiveDirection(record, declarations); - const style = translateStyle(declarations, record.config, direction, record.ownerDirection); - record.runtime.tree.setStyle(record.nodeId, style); - record.declarations = declarations; - record.appliedDirection = direction; - record.appliedConfigRevision = record.config.revision; - markDirtyRecords(dirtyRecords); -} -function mutateDeclarations(node, mutate) { - const record = requireNode(void 0, node); - const next = cloneDeclarations(record.declarations); - mutate(next); - applyDeclarations(record, next); -} -function syncSubtreeStyles(root, rootOwnerDirection, subtree) { - const directions = /* @__PURE__ */ new Map(); - const plan = []; - for (const record of subtree) { - let ownerDirection; - if (record === root) ownerDirection = rootOwnerDirection; - else { - const parentId = root.runtime.tree.getParent(record.nodeId); - const parentDirection = parentId === null ? void 0 : directions.get(parentId); - if (parentDirection === void 0) throw poisonRuntime(root.runtime, /* @__PURE__ */ new Error("Yoga subtree traversal order mismatch")); - ownerDirection = parentDirection; - } - const appliedDirection = declarationDirection(record.declarations, ownerDirection); - const stale = record.appliedConfigRevision !== record.config.revision || record.appliedDirection !== appliedDirection || record.ownerDirection !== ownerDirection; - plan.push({ - record, - ownerDirection, - appliedDirection, - style: stale ? translateStyle(record.declarations, record.config, appliedDirection, ownerDirection) : void 0 - }); - directions.set(record.nodeId, appliedDirection); - } - let nativeWrites = 0; - for (const entry of plan) { - if (entry.style === void 0) continue; - try { - root.runtime.tree.setStyle(entry.record.nodeId, entry.style); - } catch (error) { - if (nativeWrites > 0) throw poisonRuntime(root.runtime, error); - throw error; - } - nativeWrites += 1; - } - for (const entry of plan) { - entry.record.ownerDirection = entry.ownerDirection; - entry.record.appliedDirection = entry.appliedDirection; - entry.record.appliedConfigRevision = entry.record.config.revision; - } - return nativeWrites > 0; -} -const physicalEdges = [ - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8 -]; -function requireEdge(edge) { - return requireEnum(edge, "edge", physicalEdges); -} -function requireGutter(gutter) { - return requireEnum(gutter, "gutter", [ - 0, - 1, - 2 - ]); -} -function requireInsertIndex(index, childCount) { - if (typeof index !== "number" || !Number.isInteger(index) || index < 0 || index > childCount) throw new RangeError("Child index is out of range"); - return index; -} -function lookupChildIndex(index) { - if (typeof index !== "number") throw new TypeError("Child index must be a number"); - return Number.isInteger(index) && index >= 0 ? index : null; -} -function assertAcyclicInsertion(parent, child) { - let current = parent.nodeId; - while (current !== null) { - if (current === child.nodeId) throw new Error("Cannot create a cycle in a Yoga tree"); - current = parent.runtime.tree.getParent(current); - } -} -function sameCalculation(left, right) { - return left !== void 0 && Object.is(left.width, right.width) && Object.is(left.height, right.height) && left.direction === right.direction && left.measurementRevision === right.measurementRevision; -} -var YogaNode = class { - constructor(runtime, config) { - assertRuntimeUsable(runtime); - const declarations = createDeclarations(config.useWebDefaults); - const ownerDirection = 1; - const appliedDirection = declarationDirection(declarations, ownerDirection); - const nodeId = runtime.tree.newLeaf(translateStyle(declarations, config, appliedDirection, ownerDirection)); - nodeRecords.set(this, { - runtime, - config, - nodeId, - alive: true, - declarations, - ownerDirection, - appliedDirection, - appliedConfigRevision: config.revision, - layout: initialLayout(), - computedMargin: initialComputedEdges(), - computedPadding: initialComputedEdges(), - computedBorder: initialComputedEdges(), - layoutDirection: appliedDirection, - dirty: true, - hasNewLayout: true, - dirtiedFunction: null, - measureFunction: null, - measurementRevision: 0, - outputStale: false, - referenceBaseline: false, - alwaysFormsContainingBlock: false, - lastCalculation: void 0 - }); - runtime.nodes.set(nodeId, this); - } - free() { - freeNode(requireNode(void 0, this)); - } - freeRecursive() { - freeRecursiveNode(requireNode(void 0, this)); - } - getChild(index) { - const record = requireNode(void 0, this); - const resolvedIndex = lookupChildIndex(index); - if (resolvedIndex === null || resolvedIndex >= record.runtime.tree.getChildCount(record.nodeId)) return null; - const childId = record.runtime.tree.getChildAtIndex(record.nodeId, resolvedIndex); - return nodeForRecord(recordForId(record.runtime, childId)); - } - getChildCount() { - const record = requireNode(void 0, this); - return record.runtime.tree.getChildCount(record.nodeId); - } - getParent() { - const record = requireNode(void 0, this); - const parentId = record.runtime.tree.getParent(record.nodeId); - return parentId === null ? null : nodeForRecord(recordForId(record.runtime, parentId)); - } - insertChild(child, index) { - const parentRecord = requireNode(void 0, this); - const childRecord = requireNode(parentRecord.runtime, child); - const resolvedIndex = requireInsertIndex(index, parentRecord.runtime.tree.getChildCount(parentRecord.nodeId)); - if (parentRecord.measureFunction !== null) throw new Error("Measured Yoga nodes cannot have children"); - if (parentRecord.runtime.tree.getParent(childRecord.nodeId) !== null) throw new Error("Yoga child already has a parent"); - assertAcyclicInsertion(parentRecord, childRecord); - const dirtyRecords = collectAncestors(parentRecord, true); - parentRecord.runtime.tree.insertChildAtIndex(parentRecord.nodeId, resolvedIndex, childRecord.nodeId); - childRecord.lastCalculation = void 0; - markDirtyRecords(dirtyRecords); - } - removeChild(child) { - const parentRecord = requireNode(void 0, this); - const childRecord = requireNode(parentRecord.runtime, child); - if (parentRecord.runtime.tree.getParent(childRecord.nodeId) !== parentRecord.nodeId) return; - const dirtyRecords = collectAncestors(parentRecord, true); - parentRecord.runtime.tree.removeChild(parentRecord.nodeId, childRecord.nodeId); - try { - parentRecord.runtime.tree.markDirty(childRecord.nodeId); - } catch (error) { - throw poisonRuntime(parentRecord.runtime, error); - } - childRecord.layout = initialLayout(); - childRecord.computedMargin = initialComputedEdges(); - childRecord.computedPadding = initialComputedEdges(); - childRecord.computedBorder = initialComputedEdges(); - childRecord.layoutDirection = childRecord.appliedDirection; - childRecord.lastCalculation = void 0; - markDirtyRecords(dirtyRecords); - } - isDirty() { - return requireNode(void 0, this).dirty; - } - markDirty() { - const record = requireNode(void 0, this); - if (record.measureFunction === null || record.runtime.tree.getChildCount(record.nodeId) !== 0) throw new Error("Only measured Yoga leaves can be manually marked dirty"); - const dirtyRecords = collectAncestors(record, true); - record.runtime.tree.markDirty(record.nodeId); - markDirtyRecords(dirtyRecords); - } - hasNewLayout() { - return requireNode(void 0, this).hasNewLayout; - } - markLayoutSeen() { - requireNode(void 0, this).hasNewLayout = false; - } - reset() { - const record = requireNode(void 0, this); - if (record.runtime.tree.getParent(record.nodeId) !== null || record.runtime.tree.getChildCount(record.nodeId) !== 0) throw new Error("Only a detached Yoga leaf can be reset"); - const declarations = createDeclarations(record.config.useWebDefaults); - const ownerDirection = 1; - const appliedDirection = declarationDirection(declarations, ownerDirection); - record.runtime.tree.setStyle(record.nodeId, translateStyle(declarations, record.config, appliedDirection, ownerDirection)); - record.declarations = declarations; - record.ownerDirection = ownerDirection; - record.appliedDirection = appliedDirection; - record.appliedConfigRevision = record.config.revision; - record.layout = initialLayout(); - record.computedMargin = initialComputedEdges(); - record.computedPadding = initialComputedEdges(); - record.computedBorder = initialComputedEdges(); - record.layoutDirection = appliedDirection; - record.dirty = true; - record.hasNewLayout = true; - record.dirtiedFunction = null; - if (record.measureFunction !== null) { - record.runtime.measurementRevision += 1; - record.measurementRevision = record.runtime.measurementRevision; - } - record.measureFunction = null; - record.outputStale = false; - record.referenceBaseline = false; - record.alwaysFormsContainingBlock = false; - record.lastCalculation = void 0; - } - isReferenceBaseline() { - return requireNode(void 0, this).referenceBaseline; - } - setIsReferenceBaseline(isReferenceBaseline) { - const record = requireNode(void 0, this); - if (requireBoolean(isReferenceBaseline, "isReferenceBaseline")) throw new TypeError("Reference baselines are unsupported"); - record.referenceBaseline = false; - } - setAlwaysFormsContainingBlock(alwaysFormsContainingBlock) { - const record = requireNode(void 0, this); - record.alwaysFormsContainingBlock = requireBoolean(alwaysFormsContainingBlock, "alwaysFormsContainingBlock"); - } - setMeasureFunc(measureFunc) { - const record = requireNode(void 0, this); - if (measureFunc !== null && typeof measureFunc !== "function") throw new TypeError("measureFunc must be a function or null"); - if (measureFunc !== null && record.runtime.tree.getChildCount(record.nodeId) !== 0) throw new Error("Measured Yoga nodes cannot have children"); - if (record.measureFunction === measureFunc) return; - record.runtime.tree.markDirty(record.nodeId); - record.measureFunction = measureFunc; - record.runtime.measurementRevision += 1; - record.measurementRevision = record.runtime.measurementRevision; - } - unsetMeasureFunc() { - this.setMeasureFunc(null); - } - setDirtiedFunc(dirtiedFunc) { - const record = requireNode(void 0, this); - if (dirtiedFunc !== null && typeof dirtiedFunc !== "function") throw new TypeError("dirtiedFunc must be a function or null"); - record.dirtiedFunction = dirtiedFunc; - } - unsetDirtiedFunc() { - requireNode(void 0, this).dirtiedFunction = null; - } - copyStyle(node) { - const record = requireNode(void 0, this); - applyDeclarations(record, cloneDeclarations(requireNode(record.runtime, node).declarations)); - } - getAlignContent() { - return requireNode(void 0, this).declarations.alignContent; - } - getAlignItems() { - return requireNode(void 0, this).declarations.alignItems; - } - getAlignSelf() { - return requireNode(void 0, this).declarations.alignSelf; - } - getAspectRatio() { - return requireNode(void 0, this).declarations.aspectRatio ?? NaN; - } - getBorder(edge) { - return requireNode(void 0, this).declarations.border[requireEdge(edge)] ?? NaN; - } - getDirection() { - return requireNode(void 0, this).declarations.direction; - } - getDisplay() { - return requireNode(void 0, this).declarations.display; - } - getFlexBasis() { - return publicValue(requireNode(void 0, this).declarations.flexBasis); - } - getFlexDirection() { - return requireNode(void 0, this).declarations.flexDirection; - } - getFlexGrow() { - return requireNode(void 0, this).declarations.flexGrow ?? 0; - } - getFlexShrink() { - const record = requireNode(void 0, this); - return record.declarations.flexShrink ?? (record.config.useWebDefaults ? 1 : 0); - } - getFlexWrap() { - return requireNode(void 0, this).declarations.flexWrap; - } - getHeight() { - return publicValue(requireNode(void 0, this).declarations.height); - } - getJustifyContent() { - return requireNode(void 0, this).declarations.justifyContent; - } - getGap(gutter) { - return requireNode(void 0, this).declarations.gap[requireGutter(gutter)].value; - } - getMargin(edge) { - return publicValue(requireNode(void 0, this).declarations.margin[requireEdge(edge)]); - } - getMaxHeight() { - return publicValue(requireNode(void 0, this).declarations.maxHeight); - } - getMaxWidth() { - return publicValue(requireNode(void 0, this).declarations.maxWidth); - } - getMinHeight() { - return publicValue(requireNode(void 0, this).declarations.minHeight); - } - getMinWidth() { - return publicValue(requireNode(void 0, this).declarations.minWidth); - } - getOverflow() { - return requireNode(void 0, this).declarations.overflow; - } - getPadding(edge) { - return publicValue(requireNode(void 0, this).declarations.padding[requireEdge(edge)]); - } - getPosition(edge) { - return publicValue(requireNode(void 0, this).declarations.position[requireEdge(edge)]); - } - getPositionType() { - return requireNode(void 0, this).declarations.positionType; - } - getBoxSizing() { - return requireNode(void 0, this).declarations.boxSizing; - } - getWidth() { - return publicValue(requireNode(void 0, this).declarations.width); - } - setAlignContent(alignContent) { - const value = requireEnum(alignContent, "alignContent", [ - 1, - 2, - 3, - 4, - 6, - 7, - 8 - ]); - mutateDeclarations(this, (declarations) => { - declarations.alignContent = value; - }); - } - setAlignItems(alignItems) { - const value = requireEnum(alignItems, "alignItems", [ - 1, - 2, - 3, - 4, - 5 - ]); - mutateDeclarations(this, (declarations) => { - declarations.alignItems = value; - }); - } - setAlignSelf(alignSelf) { - const value = requireEnum(alignSelf, "alignSelf", [ - 0, - 1, - 2, - 3, - 4, - 5 - ]); - mutateDeclarations(this, (declarations) => { - declarations.alignSelf = value; - }); - } - setAspectRatio(aspectRatio) { - const value = normalizeAspectRatio(aspectRatio); - mutateDeclarations(this, (declarations) => { - declarations.aspectRatio = value; - }); - } - setBorder(edge, borderWidth) { - const index = requireEdge(edge); - const value = normalizePoint(borderWidth, "borderWidth"); - mutateDeclarations(this, (declarations) => { - declarations.border[index] = value; - }); - } - setDirection(direction) { - const value = requireEnum(direction, "direction", [ - 0, - 1, - 2 - ]); - mutateDeclarations(this, (declarations) => { - declarations.direction = value; - }); - } - setDisplay(display) { - const value = requireEnum(display, "display", [0, 1]); - mutateDeclarations(this, (declarations) => { - declarations.display = value; - }); - } - setFlex(flex) { - const value = normalizeFlexNumber(flex, "flex"); - mutateDeclarations(this, (declarations) => { - declarations.flex = value; - }); - } - setFlexBasis(flexBasis) { - const value = normalizeLength(flexBasis, "flexBasis", true); - mutateDeclarations(this, (declarations) => { - declarations.flexBasis = value; - }); - } - setFlexBasisPercent(flexBasis) { - const value = normalizePercent(flexBasis, "flexBasis"); - mutateDeclarations(this, (declarations) => { - declarations.flexBasis = value; - }); - } - setFlexBasisAuto() { - mutateDeclarations(this, (declarations) => { - declarations.flexBasis = autoValue(); - }); - } - setFlexDirection(flexDirection) { - const value = requireEnum(flexDirection, "flexDirection", [ - 0, - 1, - 2, - 3 - ]); - mutateDeclarations(this, (declarations) => { - declarations.flexDirection = value; - }); - } - setFlexGrow(flexGrow) { - const value = normalizeFlexNumber(flexGrow, "flexGrow"); - mutateDeclarations(this, (declarations) => { - declarations.flexGrow = value; - }); - } - setFlexShrink(flexShrink) { - const value = normalizeFlexNumber(flexShrink, "flexShrink"); - mutateDeclarations(this, (declarations) => { - declarations.flexShrink = value; - }); - } - setFlexWrap(flexWrap) { - const value = requireEnum(flexWrap, "flexWrap", [ - 0, - 1, - 2 - ]); - mutateDeclarations(this, (declarations) => { - declarations.flexWrap = value; - }); - } - setHeight(height) { - const value = normalizeLength(height, "height", true); - mutateDeclarations(this, (declarations) => { - declarations.height = value; - }); - } - setHeightAuto() { - mutateDeclarations(this, (declarations) => { - declarations.height = autoValue(); - }); - } - setHeightPercent(height) { - const value = normalizePercent(height, "height"); - mutateDeclarations(this, (declarations) => { - declarations.height = value; - }); - } - setJustifyContent(justifyContent) { - const value = requireEnum(justifyContent, "justifyContent", [ - 0, - 1, - 2, - 3, - 4, - 5 - ]); - mutateDeclarations(this, (declarations) => { - declarations.justifyContent = value; - }); - } - setGap(gutter, gapLength) { - const index = requireGutter(gutter); - const value = normalizeLength(gapLength, "gapLength", false); - mutateDeclarations(this, (declarations) => { - declarations.gap[index] = value; - }); - } - setGapPercent(gutter, gapLength) { - const index = requireGutter(gutter); - const value = normalizePercent(gapLength, "gapLength"); - mutateDeclarations(this, (declarations) => { - declarations.gap[index] = value; - }); - } - setMargin(edge, margin) { - const index = requireEdge(edge); - const value = normalizeLength(margin, "margin", true); - mutateDeclarations(this, (declarations) => { - declarations.margin[index] = value; - }); - } - setMarginAuto(edge) { - const index = requireEdge(edge); - mutateDeclarations(this, (declarations) => { - declarations.margin[index] = autoValue(); - }); - } - setMarginPercent(edge, margin) { - const index = requireEdge(edge); - const value = normalizePercent(margin, "margin"); - mutateDeclarations(this, (declarations) => { - declarations.margin[index] = value; - }); - } - setMaxHeight(maxHeight) { - const value = normalizeLength(maxHeight, "maxHeight", false); - mutateDeclarations(this, (declarations) => { - declarations.maxHeight = value; - }); - } - setMaxHeightPercent(maxHeight) { - const value = normalizePercent(maxHeight, "maxHeight"); - mutateDeclarations(this, (declarations) => { - declarations.maxHeight = value; - }); - } - setMaxWidth(maxWidth) { - const value = normalizeLength(maxWidth, "maxWidth", false); - mutateDeclarations(this, (declarations) => { - declarations.maxWidth = value; - }); - } - setMaxWidthPercent(maxWidth) { - const value = normalizePercent(maxWidth, "maxWidth"); - mutateDeclarations(this, (declarations) => { - declarations.maxWidth = value; - }); - } - setMinHeight(minHeight) { - const value = normalizeLength(minHeight, "minHeight", false); - mutateDeclarations(this, (declarations) => { - declarations.minHeight = value; - }); - } - setMinHeightPercent(minHeight) { - const value = normalizePercent(minHeight, "minHeight"); - mutateDeclarations(this, (declarations) => { - declarations.minHeight = value; - }); - } - setMinWidth(minWidth) { - const value = normalizeLength(minWidth, "minWidth", false); - mutateDeclarations(this, (declarations) => { - declarations.minWidth = value; - }); - } - setMinWidthPercent(minWidth) { - const value = normalizePercent(minWidth, "minWidth"); - mutateDeclarations(this, (declarations) => { - declarations.minWidth = value; - }); - } - setOverflow(overflow) { - const value = requireEnum(overflow, "overflow", [ - 0, - 1, - 2 - ]); - mutateDeclarations(this, (declarations) => { - declarations.overflow = value; - }); - } - setPadding(edge, padding) { - const index = requireEdge(edge); - const value = normalizeLength(padding, "padding", false); - mutateDeclarations(this, (declarations) => { - declarations.padding[index] = value; - }); - } - setPaddingPercent(edge, padding) { - const index = requireEdge(edge); - const value = normalizePercent(padding, "padding"); - mutateDeclarations(this, (declarations) => { - declarations.padding[index] = value; - }); - } - setPosition(edge, position) { - const index = requireEdge(edge); - const value = normalizeLength(position, "position", false); - mutateDeclarations(this, (declarations) => { - declarations.position[index] = value; - }); - } - setPositionPercent(edge, position) { - const index = requireEdge(edge); - const value = normalizePercent(position, "position"); - mutateDeclarations(this, (declarations) => { - declarations.position[index] = value; - }); - } - setPositionType(positionType) { - const value = requireEnum(positionType, "positionType", [1, 2]); - mutateDeclarations(this, (declarations) => { - declarations.positionType = value; - }); - } - setPositionAuto(edge) { - const index = requireEdge(edge); - mutateDeclarations(this, (declarations) => { - declarations.position[index] = autoValue(); - }); - } - setBoxSizing(boxSizing) { - const value = requireEnum(boxSizing, "boxSizing", [0, 1]); - mutateDeclarations(this, (declarations) => { - declarations.boxSizing = value; - }); - } - setWidth(width) { - const value = normalizeLength(width, "width", true); - mutateDeclarations(this, (declarations) => { - declarations.width = value; - }); - } - setWidthAuto() { - mutateDeclarations(this, (declarations) => { - declarations.width = autoValue(); - }); - } - setWidthPercent(width) { - const value = normalizePercent(width, "width"); - mutateDeclarations(this, (declarations) => { - declarations.width = value; - }); - } - calculateLayout(width, height, direction = 1) { - const record = requireNode(void 0, this); - const requestedDirection = requireEnum(direction, "calculateLayout direction", [ - 0, - 1, - 2 - ]); - const ownerDirection = requestedDirection === 0 ? 1 : requestedDirection; - const availableWidth = requireCalculationDimension(width, "width"); - const availableHeight = requireCalculationDimension(height, "height"); - const subtree = collectSubtree(record); - const signature = { - width: typeof availableWidth === "number" ? Math.fround(availableWidth) : void 0, - height: typeof availableHeight === "number" ? Math.fround(availableHeight) : void 0, - direction: ownerDirection, - measurementRevision: subtree.reduce((revision, current) => Math.max(revision, current.measurementRevision), 0) - }; - const translationIsStale = syncSubtreeStyles(record, ownerDirection, subtree); - const recomputeSubtree = record.dirty || translationIsStale || subtree.some((current) => current.outputStale) || !sameCalculation(record.lastCalculation, signature); - const rootHasOwner = record.runtime.tree.getParent(record.nodeId) !== null; - const calculationStyle = translateCalculationStyle(record.declarations, record.config, record.appliedDirection, record.ownerDirection, signature.width, signature.height); - const temporaryStyle = calculationStyle.style; - const ordinaryStyle = temporaryStyle === null ? null : translateStyle(record.declarations, record.config, record.appliedDirection, record.ownerDirection); - for (const current of subtree) current.outputStale = true; - if (temporaryStyle !== null) record.runtime.tree.setStyle(record.nodeId, temporaryStyle); - let calculationFailed = false; - let calculationError; - try { - const options = { - root: record.nodeId, - availableSpace: { - width: availableWidth, - height: availableHeight - } - }; - if (subtree.some((current) => current.measureFunction !== null)) { - record.runtime.activeMeasureContext = { - root: record.nodeId, - exactWidth: calculationStyle.exactWidth, - exactHeight: calculationStyle.exactHeight - }; - try { - record.runtime.tree.computeLayoutWithMeasure({ - ...options, - measure: record.runtime.measure - }); - } finally { - record.runtime.activeMeasureContext = null; - } - } else record.runtime.tree.computeLayout(options); - } catch (error) { - calculationFailed = true; - calculationError = error; - } - if (ordinaryStyle !== null) try { - record.runtime.tree.setStyle(record.nodeId, ordinaryStyle); - } catch (error) { - throw poisonRuntime(record.runtime, error); - } - if (calculationFailed) throw calculationError; - const subtreeIndices = /* @__PURE__ */ new Map(); - for (const [index, current] of subtree.entries()) subtreeIndices.set(current.nodeId, index); - const outputs = projectOutputs(subtree.map((current, index) => { - let parentIndex = null; - if (index !== 0) { - const parentId = record.runtime.tree.getParent(current.nodeId); - const resolvedParentIndex = parentId === null ? void 0 : subtreeIndices.get(parentId); - if (resolvedParentIndex === void 0) throw poisonRuntime(record.runtime, /* @__PURE__ */ new Error("Yoga subtree projection mismatch")); - parentIndex = resolvedParentIndex; - } - return { - declarations: current.declarations, - direction: current.appliedDirection, - pointScaleFactor: current.config.pointScaleFactor, - measured: current.measureFunction !== null, - nativeLayout: record.runtime.tree.getUnroundedLayout(current.nodeId), - parentIndex - }; - }), { - ownerWidth: signature.width, - ownerHeight: signature.height, - rootHasOwner - }); - for (const [index, current] of subtree.entries()) { - const output = outputs[index]; - current.layout = output.layout; - current.computedMargin = output.margin; - current.computedPadding = output.padding; - current.computedBorder = output.border; - current.layoutDirection = output.direction; - } - for (const current of subtree) { - current.dirty = false; - if (recomputeSubtree) current.hasNewLayout = true; - current.outputStale = false; - } - record.hasNewLayout = true; - record.lastCalculation = signature; - } - getComputedLeft() { - return requireNode(void 0, this).layout.left; - } - getComputedRight() { - return requireNode(void 0, this).layout.right; - } - getComputedTop() { - return requireNode(void 0, this).layout.top; - } - getComputedBottom() { - return requireNode(void 0, this).layout.bottom; - } - getComputedWidth() { - return requireNode(void 0, this).layout.width; - } - getComputedHeight() { - return requireNode(void 0, this).layout.height; - } - getComputedMargin(edge) { - const record = requireNode(void 0, this); - return computedEdge(record, record.computedMargin, edge); - } - getComputedPadding(edge) { - const record = requireNode(void 0, this); - return computedEdge(record, record.computedPadding, edge); - } - getComputedBorder(edge) { - const record = requireNode(void 0, this); - return computedEdge(record, record.computedBorder, edge); - } - getComputedLayout() { - return { ...requireNode(void 0, this).layout }; - } -}; -function computedEdge(record, edges, edge) { - const resolved = requireEnum(edge, "computed edge", [ - 0, - 1, - 2, - 3, - 4, - 5 - ]); - if (resolved === 4) return record.layoutDirection === 2 ? edges.right : edges.left; - if (resolved === 5) return record.layoutDirection === 2 ? edges.left : edges.right; - if (resolved === 0) return edges.left; - if (resolved === 1) return edges.top; - if (resolved === 2) return edges.right; - return edges.bottom; -} -function freeNode(record) { - const survivingAncestors = collectAncestors(record, false); - const directChildren = record.runtime.tree.getChildren(record.nodeId).map((nodeId) => recordForId(record.runtime, nodeId)); - record.runtime.tree.remove(record.nodeId); - record.runtime.nodes.delete(record.nodeId); - record.alive = false; - for (const child of directChildren) child.lastCalculation = void 0; - try { - const survivingParent = survivingAncestors[0]; - if (survivingParent !== void 0) record.runtime.tree.markDirty(survivingParent.nodeId); - for (const child of directChildren) record.runtime.tree.markDirty(child.nodeId); - } catch (error) { - throw poisonRuntime(record.runtime, error); - } - markDirtyRecords(survivingAncestors); -} -function freeRecursiveNode(record) { - const survivingAncestors = collectAncestors(record, false); - const postorder = collectSubtree(record).reverse(); - let removed = 0; - for (const current of postorder) { - try { - record.runtime.tree.remove(current.nodeId); - } catch (error) { - if (removed > 0) throw poisonRuntime(record.runtime, error); - throw error; - } - record.runtime.nodes.delete(current.nodeId); - current.alive = false; - removed += 1; - } - const survivingParent = survivingAncestors[0]; - if (survivingParent !== void 0) try { - record.runtime.tree.markDirty(survivingParent.nodeId); - } catch (error) { - throw poisonRuntime(record.runtime, error); - } - markDirtyRecords(survivingAncestors); -} -function createFactories(runtime) { - return { - Config: { - create: () => new YogaConfig(runtime, new ConfigState()), - destroy: (config) => freeConfig(requireConfig(runtime, config)) - }, - Node: { - create: (config) => new YogaNode(runtime, config === void 0 ? runtime.defaultConfig : requireConfig(runtime, config).state), - createDefault: () => new YogaNode(runtime, runtime.defaultConfig), - createWithConfig: (config) => new YogaNode(runtime, requireConfig(runtime, config).state), - destroy: (node) => freeNode(requireNode(runtime, node)) - } - }; -} -function createYoga() { - return { - ...createFactories(new FacadeRuntime()), - ...legacyConstants - }; -} -//#endregion -export { PositionType as _, Direction$1 as a, Errata as c, Gutter as d, Justify as f, Overflow$1 as g, NodeType as h, Dimension$1 as i, ExperimentalFeature as l, MeasureMode as m, Align as n, Display$1 as o, LogLevel as p, BoxSizing$1 as r, Edge as s, createYoga as t, FlexDirection$1 as u, Unit as v, Wrap as y }; diff --git a/packages/taffyjs-yoga/dist/index.d.ts b/packages/taffyjs-yoga/dist/index.d.ts deleted file mode 100644 index 943ab83..0000000 --- a/packages/taffyjs-yoga/dist/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { C as Overflow, E as Wrap, S as NodeType, T as Unit, _ as FlexDirection, a as DirtiedFunction, b as LogLevel, c as Yoga$1, d as Dimension, f as Direction, g as ExperimentalFeature, h as Errata, i as Config, l as Align, m as Edge, n as AlignItemsValue, o as MeasureFunction, p as Display, r as AlignSelfValue, s as Node, t as AlignContentValue, u as BoxSizing, v as Gutter, w as PositionType, x as MeasureMode, y as Justify } from "./types-CQnSOAjF.js"; -//#region src/index.d.ts -declare const Yoga: Yoga$1; -//#endregion -export { Align, type AlignContentValue, type AlignItemsValue, type AlignSelfValue, BoxSizing, type Config, Dimension, Direction, type DirtiedFunction, Display, Edge, Errata, ExperimentalFeature, FlexDirection, Gutter, Justify, LogLevel, type MeasureFunction, MeasureMode, type Node, NodeType, Overflow, PositionType, Unit, Wrap, Yoga as default }; \ No newline at end of file diff --git a/packages/taffyjs-yoga/dist/index.js b/packages/taffyjs-yoga/dist/index.js deleted file mode 100644 index 003e434..0000000 --- a/packages/taffyjs-yoga/dist/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { _ as PositionType, a as Direction, c as Errata, d as Gutter, f as Justify, g as Overflow, h as NodeType, i as Dimension, l as ExperimentalFeature, m as MeasureMode, n as Align, o as Display, p as LogLevel, r as BoxSizing, s as Edge, t as createYoga, u as FlexDirection, v as Unit, y as Wrap } from "./facade-CjSanW_Y.js"; -//#region src/index.ts -const Yoga = createYoga(); -//#endregion -export { Align, BoxSizing, Dimension, Direction, Display, Edge, Errata, ExperimentalFeature, FlexDirection, Gutter, Justify, LogLevel, MeasureMode, NodeType, Overflow, PositionType, Unit, Wrap, Yoga as default }; diff --git a/packages/taffyjs-yoga/dist/load.d.ts b/packages/taffyjs-yoga/dist/load.d.ts deleted file mode 100644 index 01f5654..0000000 --- a/packages/taffyjs-yoga/dist/load.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { C as Overflow, E as Wrap, S as NodeType, T as Unit, _ as FlexDirection, a as DirtiedFunction, b as LogLevel, c as Yoga, d as Dimension, f as Direction, g as ExperimentalFeature, h as Errata, i as Config, l as Align, m as Edge, n as AlignItemsValue, o as MeasureFunction, p as Display, r as AlignSelfValue, s as Node, t as AlignContentValue, u as BoxSizing, v as Gutter, w as PositionType, x as MeasureMode, y as Justify } from "./types-CQnSOAjF.js"; -//#region src/load.d.ts -declare function loadYoga(): Promise; -//#endregion -export { Align, type AlignContentValue, type AlignItemsValue, type AlignSelfValue, BoxSizing, type Config, Dimension, Direction, type DirtiedFunction, Display, Edge, Errata, ExperimentalFeature, FlexDirection, Gutter, Justify, LogLevel, type MeasureFunction, MeasureMode, type Node, NodeType, Overflow, PositionType, Unit, Wrap, type Yoga, loadYoga }; \ No newline at end of file diff --git a/packages/taffyjs-yoga/dist/load.js b/packages/taffyjs-yoga/dist/load.js deleted file mode 100644 index 92c9aaa..0000000 --- a/packages/taffyjs-yoga/dist/load.js +++ /dev/null @@ -1,7 +0,0 @@ -import { _ as PositionType, a as Direction, c as Errata, d as Gutter, f as Justify, g as Overflow, h as NodeType, i as Dimension, l as ExperimentalFeature, m as MeasureMode, n as Align, o as Display, p as LogLevel, r as BoxSizing, s as Edge, t as createYoga, u as FlexDirection, v as Unit, y as Wrap } from "./facade-CjSanW_Y.js"; -//#region src/load.ts -async function loadYoga() { - return createYoga(); -} -//#endregion -export { Align, BoxSizing, Dimension, Direction, Display, Edge, Errata, ExperimentalFeature, FlexDirection, Gutter, Justify, LogLevel, MeasureMode, NodeType, Overflow, PositionType, Unit, Wrap, loadYoga }; diff --git a/packages/taffyjs-yoga/dist/types-CQnSOAjF.d.ts b/packages/taffyjs-yoga/dist/types-CQnSOAjF.d.ts deleted file mode 100644 index 1c368f7..0000000 --- a/packages/taffyjs-yoga/dist/types-CQnSOAjF.d.ts +++ /dev/null @@ -1,324 +0,0 @@ -//#region src/enums.d.ts -declare enum Align { - Auto = 0, - FlexStart = 1, - Center = 2, - FlexEnd = 3, - Stretch = 4, - Baseline = 5, - SpaceBetween = 6, - SpaceAround = 7, - SpaceEvenly = 8 -} -declare enum BoxSizing { - BorderBox = 0, - ContentBox = 1 -} -declare enum Dimension { - Width = 0, - Height = 1 -} -declare enum Direction { - Inherit = 0, - LTR = 1, - RTL = 2 -} -declare enum Display { - Flex = 0, - None = 1 -} -declare enum Edge { - Left = 0, - Top = 1, - Right = 2, - Bottom = 3, - Start = 4, - End = 5, - Horizontal = 6, - Vertical = 7, - All = 8 -} -declare enum Errata { - None = 0 -} -declare enum ExperimentalFeature { - WebFlexBasis = 0 -} -declare enum FlexDirection { - Column = 0, - ColumnReverse = 1, - Row = 2, - RowReverse = 3 -} -declare enum Gutter { - Column = 0, - Row = 1, - All = 2 -} -declare enum Justify { - FlexStart = 0, - Center = 1, - FlexEnd = 2, - SpaceBetween = 3, - SpaceAround = 4, - SpaceEvenly = 5 -} -declare enum LogLevel { - Error = 0, - Warn = 1, - Info = 2, - Debug = 3, - Verbose = 4, - Fatal = 5 -} -declare enum MeasureMode { - Undefined = 0, - Exactly = 1, - AtMost = 2 -} -declare enum NodeType { - Default = 0, - Text = 1 -} -declare enum Overflow { - Visible = 0, - Hidden = 1, - Scroll = 2 -} -declare enum PositionType { - Relative = 1, - Absolute = 2 -} -declare enum Unit { - Undefined = 0, - Point = 1, - Percent = 2, - Auto = 3 -} -declare enum Wrap { - NoWrap = 0, - Wrap = 1, - WrapReverse = 2 -} -declare const legacyConstants: { - ALIGN_AUTO: Align; - ALIGN_FLEX_START: Align; - ALIGN_CENTER: Align; - ALIGN_FLEX_END: Align; - ALIGN_STRETCH: Align; - ALIGN_BASELINE: Align; - ALIGN_SPACE_BETWEEN: Align; - ALIGN_SPACE_AROUND: Align; - ALIGN_SPACE_EVENLY: Align; - BOX_SIZING_BORDER_BOX: BoxSizing; - BOX_SIZING_CONTENT_BOX: BoxSizing; - DIMENSION_WIDTH: Dimension; - DIMENSION_HEIGHT: Dimension; - DIRECTION_INHERIT: Direction; - DIRECTION_LTR: Direction; - DIRECTION_RTL: Direction; - DISPLAY_FLEX: Display; - DISPLAY_NONE: Display; - EDGE_LEFT: Edge; - EDGE_TOP: Edge; - EDGE_RIGHT: Edge; - EDGE_BOTTOM: Edge; - EDGE_START: Edge; - EDGE_END: Edge; - EDGE_HORIZONTAL: Edge; - EDGE_VERTICAL: Edge; - EDGE_ALL: Edge; - ERRATA_NONE: Errata; - EXPERIMENTAL_FEATURE_WEB_FLEX_BASIS: ExperimentalFeature; - FLEX_DIRECTION_COLUMN: FlexDirection; - FLEX_DIRECTION_COLUMN_REVERSE: FlexDirection; - FLEX_DIRECTION_ROW: FlexDirection; - FLEX_DIRECTION_ROW_REVERSE: FlexDirection; - GUTTER_COLUMN: Gutter; - GUTTER_ROW: Gutter; - GUTTER_ALL: Gutter; - JUSTIFY_FLEX_START: Justify; - JUSTIFY_CENTER: Justify; - JUSTIFY_FLEX_END: Justify; - JUSTIFY_SPACE_BETWEEN: Justify; - JUSTIFY_SPACE_AROUND: Justify; - JUSTIFY_SPACE_EVENLY: Justify; - LOG_LEVEL_ERROR: LogLevel; - LOG_LEVEL_WARN: LogLevel; - LOG_LEVEL_INFO: LogLevel; - LOG_LEVEL_DEBUG: LogLevel; - LOG_LEVEL_VERBOSE: LogLevel; - LOG_LEVEL_FATAL: LogLevel; - MEASURE_MODE_UNDEFINED: MeasureMode; - MEASURE_MODE_EXACTLY: MeasureMode; - MEASURE_MODE_AT_MOST: MeasureMode; - NODE_TYPE_DEFAULT: NodeType; - NODE_TYPE_TEXT: NodeType; - OVERFLOW_VISIBLE: Overflow; - OVERFLOW_HIDDEN: Overflow; - OVERFLOW_SCROLL: Overflow; - POSITION_TYPE_RELATIVE: PositionType; - POSITION_TYPE_ABSOLUTE: PositionType; - UNIT_UNDEFINED: Unit; - UNIT_POINT: Unit; - UNIT_PERCENT: Unit; - UNIT_AUTO: Unit; - WRAP_NO_WRAP: Wrap; - WRAP_WRAP: Wrap; - WRAP_WRAP_REVERSE: Wrap; -}; -//#endregion -//#region src/types.d.ts -interface Layout { - left: number; - right: number; - top: number; - bottom: number; - width: number; - height: number; -} -interface Size { - width: number; - height: number; -} -interface Value { - unit: Unit; - value: number; -} -type DirtiedFunction = (node: Node) => void; -type MeasureFunction = (width: number, widthMode: MeasureMode, height: number, heightMode: MeasureMode) => Size; -type LayoutDimension = number | "auto" | undefined; -/** Values supported by {@link Node.setAlignContent}. */ -type AlignContentValue = Align.FlexStart | Align.Center | Align.FlexEnd | Align.Stretch | Align.SpaceBetween | Align.SpaceAround | Align.SpaceEvenly; -/** Values supported by {@link Node.setAlignItems}. */ -type AlignItemsValue = Align.FlexStart | Align.Center | Align.FlexEnd | Align.Stretch | Align.Baseline; -/** Values supported by {@link Node.setAlignSelf}. */ -type AlignSelfValue = Align.Auto | AlignItemsValue; -interface Config { - free(): void; - isExperimentalFeatureEnabled(feature: ExperimentalFeature): boolean; - setExperimentalFeatureEnabled(feature: ExperimentalFeature, enabled: boolean): void; - setPointScaleFactor(factor: number): void; - getErrata(): Errata; - setErrata(errata: Errata): void; - useWebDefaults(): boolean; - setUseWebDefaults(useWebDefaults: boolean): void; -} -interface Node { - free(): void; - freeRecursive(): void; - copyStyle(node: Node): void; - getChild(index: number): Node; - getChildCount(): number; - getParent(): Node | null; - insertChild(child: Node, index: number): void; - removeChild(child: Node): void; - isDirty(): boolean; - markDirty(): void; - hasNewLayout(): boolean; - markLayoutSeen(): void; - reset(): void; - isReferenceBaseline(): boolean; - setIsReferenceBaseline(isReferenceBaseline: false): void; - setAlwaysFormsContainingBlock(alwaysFormsContainingBlock: boolean): void; - setMeasureFunc(measureFunc: MeasureFunction | null): void; - unsetMeasureFunc(): void; - setDirtiedFunc(dirtiedFunc: DirtiedFunction | null): void; - unsetDirtiedFunc(): void; - getAlignContent(): Align; - getAlignItems(): Align; - getAlignSelf(): Align; - getAspectRatio(): number; - getBorder(edge: Edge): number; - getDirection(): Direction; - getDisplay(): Display; - getFlexBasis(): Value; - getFlexDirection(): FlexDirection; - getFlexGrow(): number; - getFlexShrink(): number; - getFlexWrap(): Wrap; - getHeight(): Value; - getJustifyContent(): Justify; - getGap(gutter: Gutter): Value; - getMargin(edge: Edge): Value; - getMaxHeight(): Value; - getMaxWidth(): Value; - getMinHeight(): Value; - getMinWidth(): Value; - getOverflow(): Overflow; - getPadding(edge: Edge): Value; - getPosition(edge: Edge): Value; - getPositionType(): PositionType; - getBoxSizing(): BoxSizing; - getWidth(): Value; - setAlignContent(alignContent: AlignContentValue): void; - setAlignItems(alignItems: AlignItemsValue): void; - setAlignSelf(alignSelf: AlignSelfValue): void; - setAspectRatio(aspectRatio: number | undefined): void; - setBorder(edge: Edge, borderWidth: number | undefined): void; - setDirection(direction: Direction): void; - setDisplay(display: Display): void; - setFlex(flex: number | undefined): void; - setFlexBasis(flexBasis: number | "auto" | `${number}%` | undefined): void; - setFlexBasisPercent(flexBasis: number | undefined): void; - setFlexBasisAuto(): void; - setFlexDirection(flexDirection: FlexDirection): void; - setFlexGrow(flexGrow: number | undefined): void; - setFlexShrink(flexShrink: number | undefined): void; - setFlexWrap(flexWrap: Wrap): void; - setHeight(height: number | "auto" | `${number}%` | undefined): void; - setHeightAuto(): void; - setHeightPercent(height: number | undefined): void; - setJustifyContent(justifyContent: Justify): void; - setGap(gutter: Gutter, gapLength: number | `${number}%` | undefined): Value; - setGapPercent(gutter: Gutter, gapLength: number | undefined): Value; - setMargin(edge: Edge, margin: number | "auto" | `${number}%` | undefined): void; - setMarginAuto(edge: Edge): void; - setMarginPercent(edge: Edge, margin: number | undefined): void; - setMaxHeight(maxHeight: number | `${number}%` | undefined): void; - setMaxHeightPercent(maxHeight: number | undefined): void; - setMaxWidth(maxWidth: number | `${number}%` | undefined): void; - setMaxWidthPercent(maxWidth: number | undefined): void; - setMinHeight(minHeight: number | `${number}%` | undefined): void; - setMinHeightPercent(minHeight: number | undefined): void; - setMinWidth(minWidth: number | `${number}%` | undefined): void; - setMinWidthPercent(minWidth: number | undefined): void; - setOverflow(overflow: Overflow): void; - setPadding(edge: Edge, padding: number | `${number}%` | undefined): void; - setPaddingPercent(edge: Edge, padding: number | undefined): void; - setPosition(edge: Edge, position: number | `${number}%` | undefined): void; - setPositionPercent(edge: Edge, position: number | undefined): void; - setPositionType(positionType: PositionType): void; - setPositionAuto(edge: Edge): void; - setBoxSizing(boxSizing: BoxSizing): void; - setWidth(width: number | "auto" | `${number}%` | undefined): void; - setWidthAuto(): void; - setWidthPercent(width: number | undefined): void; - calculateLayout(width: LayoutDimension, height: LayoutDimension, direction?: Direction): void; - getComputedLeft(): number; - getComputedRight(): number; - getComputedTop(): number; - getComputedBottom(): number; - getComputedWidth(): number; - getComputedHeight(): number; - getComputedMargin(edge: Edge): number; - getComputedPadding(edge: Edge): number; - getComputedBorder(edge: Edge): number; - getComputedLayout(): Layout; -} -interface ConfigFactory { - create(): Config; - destroy(config: Config): void; -} -interface NodeFactory { - create(config?: Config): Node; - createDefault(): Node; - createWithConfig(config: Config): Node; - destroy(node: Node): void; -} -type Yoga = { - Config: ConfigFactory; - Node: NodeFactory; -} & typeof legacyConstants; -//#endregion -export { Overflow as C, Wrap as E, NodeType as S, Unit as T, FlexDirection as _, DirtiedFunction as a, LogLevel as b, Yoga as c, Dimension as d, Direction as f, ExperimentalFeature as g, Errata as h, Config as i, Align as l, Edge as m, AlignItemsValue as n, MeasureFunction as o, Display as p, AlignSelfValue as r, Node as s, AlignContentValue as t, BoxSizing as u, Gutter as v, PositionType as w, MeasureMode as x, Justify as y }; \ No newline at end of file diff --git a/packages/taffyjs-yoga/package.json b/packages/taffyjs-yoga/package.json index f6c03dc..6a01e8b 100644 --- a/packages/taffyjs-yoga/package.json +++ b/packages/taffyjs-yoga/package.json @@ -12,7 +12,7 @@ "files": [ "README.md", "COMPATIBILITY.md", - "THIRD_PARTY_NOTICES.md", + "THIRD-PARTY-LICENSES", "dist" ], "type": "module", diff --git a/packages/taffyjs-yoga/src/load.ts b/packages/taffyjs-yoga/src/load.ts index 8554748..7c9a93d 100644 --- a/packages/taffyjs-yoga/src/load.ts +++ b/packages/taffyjs-yoga/src/load.ts @@ -29,9 +29,9 @@ export type { Yoga, } from "./types.js"; -import { createYoga } from "./facade.js"; import type { Yoga } from "./types.js"; export async function loadYoga(): Promise { + const { createYoga } = await import("./facade.js"); return createYoga(); } diff --git a/packages/taffyjs-yoga/src/projection.ts b/packages/taffyjs-yoga/src/projection.ts index 32f780e..55c6b42 100644 --- a/packages/taffyjs-yoga/src/projection.ts +++ b/packages/taffyjs-yoga/src/projection.ts @@ -57,7 +57,7 @@ function inexactEquals(left: number, right: number): boolean { return Number.isNaN(left) && Number.isNaN(right); } -// Ported from Yoga 3.2.1's `yoga/algorithm/PixelGrid.cpp`, copyright Facebook, Inc. and its affiliates, under the MIT license. See `../THIRD_PARTY_NOTICES.md`. +// Ported from Yoga 3.2.1's `yoga/algorithm/PixelGrid.cpp`, copyright Facebook, Inc. and its affiliates, under the MIT license. See `../THIRD-PARTY-LICENSES`. function roundValueToPixelGrid( value: number, pointScaleFactor: number, diff --git a/packages/taffyjs-yoga/src/values.ts b/packages/taffyjs-yoga/src/values.ts index 74f7884..7fce95a 100644 --- a/packages/taffyjs-yoga/src/values.ts +++ b/packages/taffyjs-yoga/src/values.ts @@ -12,7 +12,8 @@ export interface YogaValue { readonly value: number; } -const percentPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?%$/i; +const percentPattern = + /^\s*(?:[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|0x[\da-f]+|0b[01]+|0o[0-7]+)\s*%$/i; function float32(value: number): number { if (!Number.isFinite(value)) return value; @@ -79,10 +80,14 @@ export function normalizeLength( `${name} must be a number, percentage,${allowAuto ? " auto," : ""} or undefined`, ); } - const numeric = Number(value.slice(0, -1)); - if (!Number.isFinite(numeric)) { + const numericSource = value.slice(0, -1); + if (!Number.isFinite(Number(numericSource))) { throw new TypeError(`${name} percentage must be finite`); } + const numeric = Number.parseFloat(numericSource); + if (Number.isNaN(numeric)) { + throw new TypeError(`${name} percentage must be numeric`); + } const normalized = finiteFloat32(numeric); return normalized === undefined ? undefinedValue() : percentValue(normalized); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3bfe9a..c94ad63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -327,6 +327,25 @@ importers: specifier: 'catalog:' version: 0.2.8(@types/node@22.18.0)(@vitest/browser-playwright@4.1.10)(@voidzero-dev/vite-plus-core@0.2.8(@types/node@22.18.0)(typescript@7.0.2))(typescript@7.0.2) + packages/taffyjs-yoga-wasm: + dependencies: + '@taffyjs/wasm': + specifier: workspace:* + version: link:../taffyjs-wasm + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 22.18.0 + typescript: + specifier: 'catalog:' + version: 7.0.2 + vite: + specifier: 'catalog:' + version: '@voidzero-dev/vite-plus-core@0.2.8(@types/node@22.18.0)(typescript@7.0.2)' + vite-plus: + specifier: 'catalog:' + version: 0.2.8(@types/node@22.18.0)(@vitest/browser-playwright@4.1.10)(@voidzero-dev/vite-plus-core@0.2.8(@types/node@22.18.0)(typescript@7.0.2))(typescript@7.0.2) + tests/taffyjs-node: dependencies: '@taffyjs/node': @@ -399,6 +418,31 @@ importers: specifier: npm:yoga-layout@3.2.1 version: yoga-layout@3.2.1 + tests/taffyjs-yoga-wasm: + dependencies: + '@taffyjs/wasm': + specifier: workspace:* + version: link:../../packages/taffyjs-wasm + '@taffyjs/yoga-wasm': + specifier: workspace:* + version: link:../../packages/taffyjs-yoga-wasm + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 22.18.0 + playwright: + specifier: 1.62.1 + version: 1.62.1 + typescript: + specifier: 'catalog:' + version: 7.0.2 + vite: + specifier: 'catalog:' + version: '@voidzero-dev/vite-plus-core@0.2.8(@types/node@22.18.0)(typescript@7.0.2)' + vite-plus: + specifier: 'catalog:' + version: 0.2.8(@types/node@22.18.0)(@vitest/browser-playwright@4.1.10)(@voidzero-dev/vite-plus-core@0.2.8(@types/node@22.18.0)(typescript@7.0.2))(typescript@7.0.2) + packages: '@babel/code-frame@7.29.7': diff --git a/tests/taffyjs-wasm/tests/packed-consumer.ts b/tests/taffyjs-wasm/tests/packed-consumer.ts index 69b9c8b..d24e154 100644 --- a/tests/taffyjs-wasm/tests/packed-consumer.ts +++ b/tests/taffyjs-wasm/tests/packed-consumer.ts @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; -import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises"; +import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; @@ -35,6 +35,10 @@ try { for (const packageManager of ["npm", "pnpm"] as const) { const consumerDirectory = resolve(temporaryDirectory, packageManager); await mkdir(consumerDirectory); + await writeFile( + resolve(consumerDirectory, "package.json"), + `${JSON.stringify({ name: `taffyjs-wasm-${packageManager}-consumer`, private: true, type: "module" }, null, 2)}\n`, + ); if (packageManager === "npm") { execFileSync(npm, ["install", tarball, "--ignore-scripts", "--no-audit", "--no-fund"], { cwd: consumerDirectory, diff --git a/tests/taffyjs-yoga-wasm/package.json b/tests/taffyjs-yoga-wasm/package.json new file mode 100644 index 0000000..1b3cd80 --- /dev/null +++ b/tests/taffyjs-yoga-wasm/package.json @@ -0,0 +1,23 @@ +{ + "name": "tests-taffy-yoga-wasm", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "check:package": "node tests/package-contents.ts", + "check:types": "tsc --project tsconfig.public-api.json", + "test:api": "vp test --run --config vite.config.ts", + "test:packed": "node tests/packed-consumer.ts" + }, + "dependencies": { + "@taffyjs/wasm": "workspace:*", + "@taffyjs/yoga-wasm": "workspace:*" + }, + "devDependencies": { + "@types/node": "catalog:", + "playwright": "1.62.1", + "typescript": "catalog:", + "vite": "catalog:", + "vite-plus": "catalog:" + } +} diff --git a/tests/taffyjs-yoga-wasm/tests/package-contents.ts b/tests/taffyjs-yoga-wasm/tests/package-contents.ts new file mode 100644 index 0000000..cb3edc3 --- /dev/null +++ b/tests/taffyjs-yoga-wasm/tests/package-contents.ts @@ -0,0 +1,75 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { readdir, readFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const testDirectory = dirname(fileURLToPath(import.meta.url)); +const packageDirectory = resolve(testDirectory, "../../../packages/taffyjs-yoga-wasm"); +const sharedSourceDirectory = resolve(testDirectory, "../../../packages/taffyjs-yoga/src"); +const npm = process.platform === "win32" ? "npm.cmd" : "npm"; +const output = execFileSync(npm, ["pack", "--dry-run", "--json"], { + cwd: packageDirectory, + encoding: "utf8", +}); +const [pack] = JSON.parse(output) as [{ files: Array<{ path: string }> }]; +const files = pack.files.map(({ path }) => path).sort(); +const distFiles = (await readdir(resolve(packageDirectory, "dist"))) + .map((name) => `dist/${name}`) + .sort(); + +assert.deepEqual(files, ["README.md", "THIRD-PARTY-LICENSES", ...distFiles, "package.json"]); +assert.equal( + files.some((path) => path.endsWith(".wasm")), + false, +); + +const javascriptFiles = distFiles.filter((path) => path.endsWith(".js")); +const declarationFiles = distFiles.filter((path) => path.endsWith(".d.ts")); +const javascriptSources = await Promise.all( + javascriptFiles.map((path) => readFile(resolve(packageDirectory, path), "utf8")), +); +const declarationSources = await Promise.all( + declarationFiles.map((path) => readFile(resolve(packageDirectory, path), "utf8")), +); +const packageJavaScript = javascriptSources.join("\n"); +const packageDeclarations = declarationSources.join("\n"); + +assert.equal(packageJavaScript.includes("@taffyjs/node"), false); +assert.equal((packageJavaScript.match(/@taffyjs\/wasm/g) ?? []).length, 1); +assert.equal(packageJavaScript.includes("AGFzbQE"), false); +assert.equal(packageDeclarations.includes("@taffyjs/node"), false); +assert.equal(packageDeclarations.includes("@taffyjs/wasm"), false); + +const sourceByPath = new Map( + javascriptFiles.map((path, index) => [path, javascriptSources[index]]), +); +const loadEntry = packageSource("dist/load.js"); +assert.equal(loadEntry.includes('import("./facade-'), true); +assert.equal(loadEntry.includes('from "./facade-'), false); + +const sharedSourceFiles = await readdir(sharedSourceDirectory); +assert.deepEqual( + sharedSourceFiles.filter((name) => name.endsWith(".d.ts")), + [], + "the Yoga Wasm declaration build must not write into the shared source directory", +); + +const manifest = await import("@taffyjs/yoga-wasm/package.json", { with: { type: "json" } }); +assert.deepEqual(manifest.default.dependencies, { "@taffyjs/wasm": "workspace:*" }); +assert.deepEqual(Object.keys(manifest.default.exports).sort(), [".", "./load", "./package.json"]); + +console.log( + JSON.stringify({ + packageFiles: files, + backendImports: 1, + embeddedWasmPayloads: 0, + sharedSourceDeclarations: 0, + }), +); + +function packageSource(path: string): string { + const source = sourceByPath.get(path); + if (source === undefined) throw new Error(`Missing package source ${path}`); + return source; +} diff --git a/tests/taffyjs-yoga-wasm/tests/packed-consumer.ts b/tests/taffyjs-yoga-wasm/tests/packed-consumer.ts new file mode 100644 index 0000000..852fa59 --- /dev/null +++ b/tests/taffyjs-yoga-wasm/tests/packed-consumer.ts @@ -0,0 +1,291 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { createServer, type Server } from "node:http"; +import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, extname, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { chromium, type Browser } from "playwright"; + +const testDirectory = dirname(fileURLToPath(import.meta.url)); +const repositoryDirectory = resolve(testDirectory, "../../.."); +const wasmPackageDirectory = resolve(repositoryDirectory, "packages/taffyjs-wasm"); +const yogaWasmPackageDirectory = resolve(repositoryDirectory, "packages/taffyjs-yoga-wasm"); +const temporaryDirectory = await mkdtemp(resolve(tmpdir(), "taffyjs-yoga-wasm-consumer-")); +const packDirectory = resolve(temporaryDirectory, "packed"); +const npm = process.platform === "win32" ? "npm.cmd" : "npm"; +const pnpm = process.platform === "win32" ? "pnpm.cmd" : "pnpm"; +const vp = process.platform === "win32" ? "vp.cmd" : "vp"; + +const nodeConsumerProgram = ` + import Yoga from "yoga-layout"; + import { loadYoga } from "yoga-layout/load"; + + const node = Yoga.Node.create(); + node.setMeasureFunc(() => ({ width: 31, height: 17 })); + node.calculateLayout(undefined, undefined); + + const otherYoga = await loadYoga(); + let isolated = false; + try { + otherYoga.Node.destroy(node); + } catch { + isolated = true; + } + + console.log(JSON.stringify({ + width: node.getComputedWidth(), + height: node.getComputedHeight(), + isolated, + })); + node.free(); +`; + +try { + await mkdir(packDirectory); + pack(wasmPackageDirectory); + pack(yogaWasmPackageDirectory); + + const tarballs = await readdir(packDirectory); + const wasmTarball = resolveSingleTarball(tarballs, "taffyjs-wasm-"); + const yogaWasmTarball = resolveSingleTarball(tarballs, "taffyjs-yoga-wasm-"); + + for (const packageManager of ["npm", "pnpm"] as const) { + const consumerDirectory = resolve(temporaryDirectory, packageManager); + await mkdir(consumerDirectory); + await writeConsumerManifest(consumerDirectory, wasmTarball, yogaWasmTarball); + + if (packageManager === "pnpm") { + await writeFile( + resolve(consumerDirectory, "pnpm-workspace.yaml"), + `packages:\n - .\noverrides:\n "@taffyjs/wasm@0.0.0": "file:${wasmTarball}"\n`, + ); + } + + execFileSync(packageManager === "npm" ? npm : pnpm, ["install", "--ignore-scripts"], { + cwd: consumerDirectory, + stdio: "pipe", + }); + + const output = execFileSync( + process.execPath, + ["--input-type=module", "--eval", nodeConsumerProgram], + { + cwd: consumerDirectory, + encoding: "utf8", + }, + ); + assert.deepEqual(JSON.parse(output), { width: 31, height: 17, isolated: true }, packageManager); + + if (packageManager === "npm") await verifyPackedBrowserConsumer(consumerDirectory); + } + + console.log( + JSON.stringify({ + packedConsumers: ["npm", "pnpm"], + browserEntries: ["root", "load"], + browserWasmPayloadsPerBundle: 1, + }), + ); +} finally { + await rm(temporaryDirectory, { recursive: true, force: true }); +} + +function pack(packageDirectory: string): void { + execFileSync(pnpm, ["pack", "--pack-destination", packDirectory], { + cwd: packageDirectory, + stdio: "pipe", + }); +} + +function resolveSingleTarball(tarballs: string[], prefix: string): string { + const matches = tarballs.filter((name) => name.startsWith(prefix) && name.endsWith(".tgz")); + assert.equal(matches.length, 1, prefix); + const [match] = matches; + if (match === undefined) throw new Error(`Missing tarball ${prefix}`); + return resolve(packDirectory, match); +} + +async function writeConsumerManifest( + consumerDirectory: string, + wasmTarball: string, + yogaWasmTarball: string, +): Promise { + await writeFile( + resolve(consumerDirectory, "package.json"), + `${JSON.stringify( + { + name: `taffyjs-yoga-wasm-${consumerDirectory.split(sep).at(-1)}-consumer`, + private: true, + type: "module", + dependencies: { + "@taffyjs/wasm": `file:${wasmTarball}`, + "yoga-layout": `file:${yogaWasmTarball}`, + }, + }, + null, + 2, + )}\n`, + ); +} + +async function verifyPackedBrowserConsumer(consumerDirectory: string): Promise { + const rootApplication = resolve(consumerDirectory, "root"); + const loadApplication = resolve(consumerDirectory, "load"); + await writeBrowserApplication( + rootApplication, + ` + import Yoga from "yoga-layout"; + + const node = Yoga.Node.create(); + node.setMeasureFunc(() => ({ width: 31, height: 17 })); + node.calculateLayout(undefined, undefined); + document.querySelector("#result").textContent = JSON.stringify({ + width: node.getComputedWidth(), + height: node.getComputedHeight(), + crossOriginIsolated: globalThis.crossOriginIsolated, + sharedArrayBufferType: typeof globalThis.SharedArrayBuffer, + }); + node.free(); + `, + ); + await writeBrowserApplication( + loadApplication, + ` + const originalCompile = WebAssembly.compile; + let compileCalls = 0; + WebAssembly.compile = (...args) => { + compileCalls += 1; + return originalCompile(...args); + }; + + const { loadYoga } = await import("yoga-layout/load"); + const compileCallsAfterImport = compileCalls; + const Yoga = await loadYoga(); + const compileCallsAfterLoad = compileCalls; + const node = Yoga.Node.create(); + node.setWidth(13); + node.calculateLayout(undefined, undefined); + document.querySelector("#result").textContent = JSON.stringify({ + width: node.getComputedWidth(), + compileCallsAfterImport, + compileCallsAfterLoad, + }); + node.free(); + WebAssembly.compile = originalCompile; + `, + ); + + for (const application of [rootApplication, loadApplication]) { + execFileSync(vp, ["build", application, "--base", "./", "--target", "esnext"], { + cwd: consumerDirectory, + stdio: "pipe", + }); + await inspectBrowserBundle(resolve(application, "dist")); + } + + let browser: Browser | undefined; + let server: Server | undefined; + try { + server = createStaticServer(consumerDirectory); + const origin = await listen(server); + browser = await chromium.launch({ headless: true }); + const page = await browser.newPage(); + const pageErrors: Error[] = []; + page.on("pageerror", (error) => pageErrors.push(error)); + + await page.goto(`${origin}/root/dist/`); + await page.locator("#result").filter({ hasText: '"width":31' }).waitFor(); + assert.deepEqual(JSON.parse((await page.locator("#result").textContent()) ?? "null"), { + width: 31, + height: 17, + crossOriginIsolated: false, + sharedArrayBufferType: "undefined", + }); + + await page.goto(`${origin}/load/dist/`); + await page.locator("#result").filter({ hasText: '"compileCallsAfterLoad":1' }).waitFor(); + assert.deepEqual(JSON.parse((await page.locator("#result").textContent()) ?? "null"), { + width: 13, + compileCallsAfterImport: 0, + compileCallsAfterLoad: 1, + }); + assert.deepEqual(pageErrors, []); + } finally { + await browser?.close(); + if (server !== undefined) await close(server); + } +} + +async function writeBrowserApplication( + applicationDirectory: string, + program: string, +): Promise { + await mkdir(resolve(applicationDirectory, "src"), { recursive: true }); + await writeFile( + resolve(applicationDirectory, "index.html"), + 'pending\n', + ); + await writeFile(resolve(applicationDirectory, "src/main.js"), program); +} + +async function inspectBrowserBundle(outputDirectory: string): Promise { + const assetDirectory = resolve(outputDirectory, "assets"); + const assets = await readdir(assetDirectory); + assert.deepEqual( + assets.filter((name) => name.endsWith(".wasm")), + [], + ); + const scripts = await Promise.all( + assets + .filter((name) => name.endsWith(".js")) + .map((name) => readFile(resolve(assetDirectory, name), "utf8")), + ); + const bundledJavaScript = scripts.join("\n"); + assert.equal((bundledJavaScript.match(/AGFzbQE/g) ?? []).length, 1); + assert.equal((bundledJavaScript.match(/WebAssembly\.compile\(/g) ?? []).length, 1); + assert.equal(bundledJavaScript.includes("node:wasi"), false); +} + +function createStaticServer(rootDirectory: string): Server { + return createServer(async (request, response) => { + try { + const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1"); + const pathname = decodeURIComponent(requestUrl.pathname); + const relativePath = pathname.endsWith("/") ? `${pathname}index.html` : pathname; + const file = resolve(rootDirectory, `.${relativePath}`); + const rootPrefix = `${resolve(rootDirectory)}${sep}`; + if (!file.startsWith(rootPrefix)) throw new Error("Invalid path"); + const body = await readFile(file); + response.statusCode = 200; + response.setHeader("content-type", contentType(file)); + response.end(body); + } catch { + response.statusCode = 404; + response.end("Not found"); + } + }); +} + +function contentType(file: string): string { + if (extname(file) === ".html") return "text/html; charset=utf-8"; + if (extname(file) === ".js") return "text/javascript; charset=utf-8"; + return "application/octet-stream"; +} + +async function listen(server: Server): Promise { + await new Promise((resolvePromise, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolvePromise); + }); + const address = server.address(); + if (address === null || typeof address === "string") throw new Error("Missing server address"); + return `http://127.0.0.1:${address.port}`; +} + +async function close(server: Server): Promise { + await new Promise((resolvePromise, reject) => { + server.close((error) => (error === undefined ? resolvePromise() : reject(error))); + }); +} diff --git a/tests/taffyjs-yoga-wasm/tsconfig.json b/tests/taffyjs-yoga-wasm/tsconfig.json new file mode 100644 index 0000000..f482c6d --- /dev/null +++ b/tests/taffyjs-yoga-wasm/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "esnext", + "lib": ["es2023", "dom"], + "moduleDetection": "force", + "module": "nodenext", + "moduleResolution": "nodenext", + "types": ["node"], + "strict": true, + "noUnusedLocals": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true + }, + "include": ["**/*.ts"] +} diff --git a/tests/taffyjs-yoga-wasm/tsconfig.public-api.json b/tests/taffyjs-yoga-wasm/tsconfig.public-api.json new file mode 100644 index 0000000..8d8434d --- /dev/null +++ b/tests/taffyjs-yoga-wasm/tsconfig.public-api.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "exactOptionalPropertyTypes": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "paths": { + "yoga-layout": ["../../packages/taffyjs-yoga-wasm/dist/index.d.ts"], + "yoga-layout/load": ["../../packages/taffyjs-yoga-wasm/dist/load.d.ts"] + }, + "strict": true, + "target": "ES2022" + }, + "files": ["../taffyjs-yoga/types/public-api.test-d.ts"] +} diff --git a/tests/taffyjs-yoga-wasm/vite.config.ts b/tests/taffyjs-yoga-wasm/vite.config.ts new file mode 100644 index 0000000..93f5868 --- /dev/null +++ b/tests/taffyjs-yoga-wasm/vite.config.ts @@ -0,0 +1,33 @@ +import { resolve } from "node:path"; +import { pathToFileURL } from "node:url"; + +import { defineConfig } from "vite-plus"; + +const wasmEntry = resolve(import.meta.dirname, "../../packages/taffyjs-wasm/dist/index.js"); +const yogaWasmEntry = resolve( + import.meta.dirname, + "../../packages/taffyjs-yoga-wasm/dist/index.js", +); +const yogaWasmLoadEntry = resolve( + import.meta.dirname, + "../../packages/taffyjs-yoga-wasm/dist/load.js", +); + +export default defineConfig({ + resolve: { + alias: [ + { find: /^yoga-layout$/, replacement: yogaWasmEntry }, + { find: /^yoga-layout\/load$/, replacement: yogaWasmLoadEntry }, + { find: /^@taffyjs\/node$/, replacement: wasmEntry }, + ], + }, + test: { + include: ["../taffyjs-yoga/*.test.mts"], + retry: 0, + sequence: { concurrent: true }, + env: { + TAFFYJS_TEST_ENTRY: pathToFileURL(wasmEntry).href, + TAFFYJS_YOGA_TEST_ENTRY: pathToFileURL(yogaWasmEntry).href, + }, + }, +}); diff --git a/tests/taffyjs-yoga/differences.test.mts b/tests/taffyjs-yoga/differences.test.mts index b265b97..535d634 100644 --- a/tests/taffyjs-yoga/differences.test.mts +++ b/tests/taffyjs-yoga/differences.test.mts @@ -5,6 +5,7 @@ import Yoga, { Edge, ExperimentalFeature, FlexDirection, + Gutter, Justify, Wrap, } from "yoga-layout"; @@ -14,6 +15,7 @@ import OracleYoga, { Edge as OracleEdge, ExperimentalFeature as OracleExperimentalFeature, FlexDirection as OracleFlexDirection, + Gutter as OracleGutter, Justify as OracleJustify, Wrap as OracleWrap, } from "yoga-layout-oracle"; @@ -146,6 +148,94 @@ test("Different: max size combined with flex shrink keeps Taffy geometry", () => } }); +test("Different: shrink factors totaling below one keep Taffy's partial shrink", () => { + const config = Yoga.Config.create(); + const oracleConfig = OracleYoga.Config.create(); + config.setPointScaleFactor(0); + oracleConfig.setPointScaleFactor(0); + const root = Yoga.Node.createWithConfig(config); + const first = Yoga.Node.createWithConfig(config); + const second = Yoga.Node.createWithConfig(config); + const oracleRoot = OracleYoga.Node.createWithConfig(oracleConfig); + const oracleFirst = OracleYoga.Node.createWithConfig(oracleConfig); + const oracleSecond = OracleYoga.Node.createWithConfig(oracleConfig); + try { + root.setFlexDirection(FlexDirection.Row); + root.setWidth(100); + first.setWidth(80); + first.setFlexShrink(0.5); + second.setWidth(80); + second.setFlexShrink(0); + root.insertChild(first, 0); + root.insertChild(second, 1); + oracleRoot.setFlexDirection(OracleFlexDirection.Row); + oracleRoot.setWidth(100); + oracleFirst.setWidth(80); + oracleFirst.setFlexShrink(0.5); + oracleSecond.setWidth(80); + oracleSecond.setFlexShrink(0); + oracleRoot.insertChild(oracleFirst, 0); + oracleRoot.insertChild(oracleSecond, 1); + root.calculateLayout(undefined, undefined); + oracleRoot.calculateLayout(undefined, undefined); + assert.deepEqual([first.getComputedWidth(), second.getComputedWidth()], [50, 80]); + assert.deepEqual([oracleFirst.getComputedWidth(), oracleSecond.getComputedWidth()], [20, 80]); + } finally { + root.freeRecursive(); + oracleRoot.freeRecursive(); + config.free(); + oracleConfig.free(); + } +}); + +test("Different: a percentage gap does not contribute to an auto main size", () => { + const config = Yoga.Config.create(); + const oracleConfig = OracleYoga.Config.create(); + config.setPointScaleFactor(0); + oracleConfig.setPointScaleFactor(0); + const root = Yoga.Node.createWithConfig(config); + const first = Yoga.Node.createWithConfig(config); + const second = Yoga.Node.createWithConfig(config); + const oracleRoot = OracleYoga.Node.createWithConfig(oracleConfig); + const oracleFirst = OracleYoga.Node.createWithConfig(oracleConfig); + const oracleSecond = OracleYoga.Node.createWithConfig(oracleConfig); + try { + root.setGapPercent(Gutter.Row, 10); + first.setWidth(30); + first.setHeight(20); + second.setWidth(30); + second.setHeight(20); + root.insertChild(first, 0); + root.insertChild(second, 1); + oracleRoot.setGapPercent(OracleGutter.Row, 10); + oracleFirst.setWidth(30); + oracleFirst.setHeight(20); + oracleSecond.setWidth(30); + oracleSecond.setHeight(20); + oracleRoot.insertChild(oracleFirst, 0); + oracleRoot.insertChild(oracleSecond, 1); + root.calculateLayout(undefined, undefined); + oracleRoot.calculateLayout(undefined, undefined); + assert.deepEqual( + [root.getComputedHeight(), second.getComputedTop(), second.getComputedHeight()], + [40, 24, 20], + ); + assert.deepEqual( + [ + oracleRoot.getComputedHeight(), + oracleSecond.getComputedTop(), + oracleSecond.getComputedHeight(), + ], + [44, 24, 20], + ); + } finally { + root.freeRecursive(); + oracleRoot.freeRecursive(); + config.free(); + oracleConfig.free(); + } +}); + test("Different: intrinsic selected roots keep Taffy's conflicting constraint order", () => { for (const kind of ["child content", "measurement"] as const) { const config = Yoga.Config.create(); diff --git a/tests/taffyjs-yoga/fixtures/measure-path-profile.mjs b/tests/taffyjs-yoga/fixtures/measure-path-profile.mjs index 2780b52..9a2e350 100644 --- a/tests/taffyjs-yoga/fixtures/measure-path-profile.mjs +++ b/tests/taffyjs-yoga/fixtures/measure-path-profile.mjs @@ -1,5 +1,7 @@ -import { TaffyTree } from "@taffyjs/node"; -import Yoga from "yoga-layout"; +const testEntry = process.env.TAFFYJS_TEST_ENTRY ?? "@taffyjs/node"; +const yogaTestEntry = process.env.TAFFYJS_YOGA_TEST_ENTRY ?? "yoga-layout"; +const { TaffyTree } = await import(testEntry); +const { default: Yoga } = await import(yogaTestEntry); const ordinaryCompute = Object.getOwnPropertyDescriptor(TaffyTree.prototype, "computeLayout").value; const measuredCompute = Object.getOwnPropertyDescriptor( diff --git a/tests/taffyjs-yoga/measurement.test.mts b/tests/taffyjs-yoga/measurement.test.mts index 2aab379..f674231 100644 --- a/tests/taffyjs-yoga/measurement.test.mts +++ b/tests/taffyjs-yoga/measurement.test.mts @@ -422,7 +422,7 @@ test("same-facade native reentrancy fails before mutation while JavaScript gette node.free(); }); -test("unmeasured calculations stay on the ordinary native path", () => { +test("unmeasured calculations stay on the ordinary backend path", () => { const fixture = fileURLToPath(new URL("./fixtures/measure-path-profile.mjs", import.meta.url)); const result = spawnSync(process.execPath, [fixture], { encoding: "utf8", timeout: 20_000 }); assert.equal(result.signal, null); diff --git a/tests/taffyjs-yoga/package.json b/tests/taffyjs-yoga/package.json index b82fc64..0562eb0 100644 --- a/tests/taffyjs-yoga/package.json +++ b/tests/taffyjs-yoga/package.json @@ -1,5 +1,5 @@ { - "name": "@taffyjs/yoga-integration-tests", + "name": "tests-taffy-yoga", "version": "0.0.0", "private": true, "type": "module", diff --git a/tests/taffyjs-yoga/style.test.mts b/tests/taffyjs-yoga/style.test.mts index debf622..6b9d1bb 100644 --- a/tests/taffyjs-yoga/style.test.mts +++ b/tests/taffyjs-yoga/style.test.mts @@ -220,7 +220,18 @@ test("declared numeric boundary inputs normalize like Yoga", () => { -1e39, 1e-50, ] as const; - const lengths = [...numbers, "25%", "-0%", "1e2%", "1e39%", "-1e39%"] as const; + const lengths = [ + ...numbers, + "25%", + "-0%", + "1e2%", + " 1%", + "0x10%", + "0b10%", + "0o10%", + "1e39%", + "-1e39%", + ] as const; const autoLengths = [...lengths, "auto"] as const; for (const [setter, getter, values] of [ diff --git a/tests/taffyjs-yoga/tsconfig.json b/tests/taffyjs-yoga/tsconfig.json index 1536c15..b8e9546 100644 --- a/tests/taffyjs-yoga/tsconfig.json +++ b/tests/taffyjs-yoga/tsconfig.json @@ -6,6 +6,10 @@ "module": "nodenext", "moduleResolution": "nodenext", "types": ["node"], + "paths": { + "#taffyjs-yoga-test-source": ["../../packages/taffyjs-yoga/src/index.ts"], + "#taffyjs-yoga-test-load-source": ["../../packages/taffyjs-yoga/src/load.ts"] + }, "strict": true, "noUnusedLocals": true, "noEmit": true, @@ -15,5 +19,5 @@ "verbatimModuleSyntax": true, "skipLibCheck": true }, - "include": ["*.test.mts", "vite.config.ts"] + "include": ["*.test.mts", "vite.config.ts", "yoga-layout-source.d.ts"] } diff --git a/tests/taffyjs-yoga/types/public-api.test-d.ts b/tests/taffyjs-yoga/types/public-api.test-d.ts index 7cdd747..3e4434a 100644 --- a/tests/taffyjs-yoga/types/public-api.test-d.ts +++ b/tests/taffyjs-yoga/types/public-api.test-d.ts @@ -127,6 +127,10 @@ node.setFlexGrow(1); node.setFlexShrink(1); node.setFlexWrap(Wrap.Wrap); node.setWidth(100); +node.setWidth(" 1%"); +node.setWidth("0x10%"); +node.setWidth("0b10%"); +node.setWidth("0o10%"); node.setWidthPercent(50); node.setWidthAuto(); node.setHeight(40); diff --git a/tests/taffyjs-yoga/vite.config.ts b/tests/taffyjs-yoga/vite.config.ts index ffabb08..84434e8 100644 --- a/tests/taffyjs-yoga/vite.config.ts +++ b/tests/taffyjs-yoga/vite.config.ts @@ -1,13 +1,31 @@ +import { resolve } from "node:path"; + import { defineConfig } from "vite-plus"; +const yogaEntry = resolve(import.meta.dirname, "../../packages/taffyjs-yoga/dist/index.js"); +const yogaLoadEntry = resolve(import.meta.dirname, "../../packages/taffyjs-yoga/dist/load.js"); + export default defineConfig({ fmt: {}, lint: { + ignorePatterns: ["types/public-api.test-d.ts"], options: { typeAware: true, typeCheck: true, }, }, + resolve: { + alias: [ + { + find: /^yoga-layout$/, + replacement: yogaEntry, + }, + { + find: /^yoga-layout\/load$/, + replacement: yogaLoadEntry, + }, + ], + }, test: { include: ["*.test.mts"], retry: 0, diff --git a/tests/taffyjs-yoga/yoga-layout-source.d.ts b/tests/taffyjs-yoga/yoga-layout-source.d.ts new file mode 100644 index 0000000..86219a3 --- /dev/null +++ b/tests/taffyjs-yoga/yoga-layout-source.d.ts @@ -0,0 +1,9 @@ +// Keep the public specifiers out of tsconfig paths because Bun applies those mappings at runtime. +declare module "yoga-layout" { + export { default } from "#taffyjs-yoga-test-source"; + export * from "#taffyjs-yoga-test-source"; +} + +declare module "yoga-layout/load" { + export * from "#taffyjs-yoga-test-load-source"; +} diff --git a/vite.config.ts b/vite.config.ts index 9fd5613..9059418 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -18,7 +18,7 @@ const testTasks = { dependsOn: ["build"], }, "check:test:yoga": { - command: "vp run @taffyjs/yoga-integration-tests#test", + command: "vp run tests-taffy-yoga#test", dependsOn: ["build"], }, "check:test:yoga:types": { @@ -56,6 +56,22 @@ const wasmTasks = { command: "vp run tests-taffy-wasm#check:browser-bundle", dependsOn: ["build:wasm:browser-consumer"], }, + "check:wasm:yoga:api": { + command: "vp run tests-taffy-yoga-wasm#test:api", + dependsOn: ["build:yoga-wasm"], + }, + "check:wasm:yoga:types": { + command: "vp run tests-taffy-yoga-wasm#check:types", + dependsOn: ["build:yoga-wasm"], + }, + "check:wasm:yoga:package": { + command: "vp run tests-taffy-yoga-wasm#check:package", + dependsOn: ["build:yoga-wasm"], + }, + "check:wasm:yoga:packed": { + command: "vp run tests-taffy-yoga-wasm#test:packed", + dependsOn: ["build:yoga-wasm"], + }, }; export default defineConfig({ @@ -68,6 +84,7 @@ export default defineConfig({ "packages/**/.napi-rs-filesystem-transaction*", "tests/taffyjs-wasm/browser/dist", "packages/taffyjs-yoga/dist", + "packages/taffyjs-yoga-wasm/dist", ], overrides: [ { @@ -86,7 +103,9 @@ export default defineConfig({ "packages/.taffyjs-*.napi-stage-*", "packages/**/.napi-rs-filesystem-transaction*", "tests/taffyjs-wasm/browser/dist", + "tests/taffyjs-yoga/types/public-api.test-d.ts", "packages/taffyjs-yoga/dist", + "packages/taffyjs-yoga-wasm/dist", ], jsPlugins: [{ name: "vite-plus", specifier: "vite-plus/oxlint-plugin" }], rules: { @@ -149,6 +168,10 @@ export default defineConfig({ command: "vp run @taffyjs/yoga#build", dependsOn: ["build:node:entries"], }, + "build:yoga-wasm": { + command: "vp run @taffyjs/yoga-wasm#build", + dependsOn: ["build:wasm"], + }, build: { command: "echo build ok", dependsOn: ["build:node:platform-artifact", "build:node:entries", "build:yoga"], @@ -156,10 +179,6 @@ export default defineConfig({ "check:format": { command: "vp fmt --check", }, - "check:format:after-build": { - command: "vp fmt --check", - dependsOn: ["build"], - }, "check:lint": { command: "vp lint --deny-warnings", }, @@ -173,9 +192,13 @@ export default defineConfig({ command: "echo tests ok", dependsOn: Object.keys(testTasks), }, + "check:test:after-format": { + command: "vp run check:test", + dependsOn: ["check:format"], + }, check: { command: "echo check ok", - dependsOn: ["check:format:after-build", "check:lint", "check:rust", "check:test"], + dependsOn: ["check:lint", "check:rust", "check:test:after-format"], }, "check:wasm": { command: "echo wasm checks passed", @@ -186,6 +209,10 @@ export default defineConfig({ "check:wasm:package", "check:wasm:browser-runtime", "check:wasm:browser-bundle", + "check:wasm:yoga:api", + "check:wasm:yoga:types", + "check:wasm:yoga:package", + "check:wasm:yoga:packed", "build:website", ], },