diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3a7f901 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,129 @@ +# AGENTS.md + +Instructions for AI agents working with the @alkdev/ujsx codebase. + +## Project Overview + +`@alkdev/ujsx` is a runtime-agnostic reactive tree library. JSX serves as an intermediate representation — the same declarative tree can target different hosts (markdown, graph structures, DOM, workflow engines) through a `HostConfig` adapter. The core has zero platform-specific assumptions: no `onClick`, no `className`, no `style`. + +## Build & Test Commands + +```bash +npm run build # tsup production build (ESM + CJS) → dist/ +npm run build:tsc # Type checking only (tsc --noEmit) +npm run lint # Same as build:tsc — tsc --noEmit +npm run test # vitest run +npm run test:watch # vitest in watch mode +npm run test:coverage # vitest run --coverage +``` + +Always run `npm run lint` and `npm run test` after making changes. + +## Architecture + +### Source Layout + +``` +src/ + mod.ts # Barrel export (all public API) + core/ + schema.ts # TypeBox Module (UJSX), UNode/UElement/URoot/UPrimitive types, type guards + h.ts # Element factory: h(), createRoot(), createComponent(), Fragment, jsx/jsxs/jsxDEV + reactive.ts # ReactiveRoot, reactiveComponent, reactiveElement; re-exports signal/computed/effect/batch + context.ts # Context class (signal-backed), Density, Direction, RenderContext + events.ts # EventEnvelope, PubSubLike, UjsxEventMap, createPubSubEmitter, proxyEventEmitter + pointer.ts # ValuePointer, selectNode(), setNode() + jsx-runtime.ts # Re-exports jsx/jsxs/jsxDEV/Fragment for jsxImportSource + host/ + config.ts # HostConfig, Root, createRoot(), mount/reconcile/unmount pipeline + fiber.ts # Fiber type, Effect type, disposeFiber(), HostLike + reconcile.ts # reconcileProps, reconcileChildren, commitMutations, scheduleUpdate, flushUpdates, LIS + transform/ + registry.ts # TransformRegistry, TransformRule, TransformContext, childCtx, matchesSchema, ctx +``` + +### Core Data Model + +The tree is the truth. All nodes are `UNode`: + +```typescript +type UNode = UPrimitive | UElement | URoot; +// UPrimitive = string | number | boolean | null +// UElement = { type: string; props: UniversalProps; children: UNode[]; key?: string } +// URoot = { type: "root"; props: UniversalProps; children: UNode[] } +``` + +- `key` on `UElement` is extracted from props by `h()` and promoted to the element level — it is never in `props` +- `URoot` is a transparent container: never has a `key`, its children mount directly into the parent +- Function components are transparent: called with `{ ...props, children }`, their output mounts in place + +### Rendering Pipeline + +1. `h()` / JSX creates `UElement` trees (pure data) +2. `createHostRoot(host, container)` creates a `Root` with a `HostConfig` +3. `root.render(node)` mounts the tree via `HostConfig` methods +4. Re-renders use `reconcileChildren` (key-based, LIS-optimized) and `reconcileProps` (TypeBox `Value.Diff`/`Value.Equal`/`Value.Hash`) +5. `wireSignalToFiber` binds Preact signals to fibers for automatic `scheduleUpdate` → `flushUpdates` + +### Key Subsystems + +- **Reactivity**: Built on `@preact/signals-core`. `Signal` flows through `computed` → `effect` → reconciler. Do not introduce alternative reactive systems. +- **HostConfig**: The sole integration point for platform-specific logic. Three type params: `TTag` (allowed tags), `Instance` (host instance type), `RootCtx` (root context type). Required methods: `createRootContext`, `createInstance`, `createTextInstance`, `appendChild`. Optional: `insertBefore`, `removeChild`, `prepareUpdate`, `commitUpdate`, `finalizeRoot`, `finalizeInstance`, `emit`. +- **Transforms**: `TransformRegistry` with priority-sorted `TransformRule`s. Rules match by `direction` and `match()` predicate. The `next()` callback enables recursive transformation. Directions use Unicode arrows (e.g. `"ujsx→mdast"`). +- **Pointers**: `ValuePointer` wraps `signal` with a path. `selectNode`/`setNode` provide immutable tree navigation and updates. + +### TypeBox Usage + +`UJSX` is a `Type.Module` from `@alkdev/typebox`. Schema keys: `UPrimitive`, `PropValue`, `UniversalProps`, `UElement`, `URoot`, `UNode`. Runtime validation via `Value.Check(UJSX.Import("UElement"), node)`. The reconciler uses `Value.Diff`, `Value.Equal`, and `Value.Hash` for prop diffing with function-prop stripping. + +## Code Conventions + +- **No comments in code** unless explicitly requested +- **ESM primary**: `"type": "module"`, CJS is a distribution compatibility layer +- **No Node-specific APIs in core**: `src/core/` and `src/transform/` must not import `fs`, `path`, `child_process`, etc. +- **Dual format via tsup**: Same source produces ESM and CJS; exports map always has matching `import` and `require` entries +- **TypeBox is non-negotiable**: Not optional, not replaceable +- **`@preact/signals-core` is the only reactive primitive**: No RxJS, Solid signals, or Vue reactivity +- **Platform agnostic**: No DOM APIs, Three.js APIs, or any platform-specific API calls in core + +## Sub-path Exports + +Each sub-path maps to a single source file — no barrel re-exports within sub-paths: + +| Import | Source | +|--------|--------| +| `@alkdev/ujsx` | `src/mod.ts` | +| `@alkdev/ujsx/schema` | `src/core/schema.ts` | +| `@alkdev/ujsx/h` | `src/core/h.ts` | +| `@alkdev/ujsx/reactive` | `src/core/reactive.ts` | +| `@alkdev/ujsx/context` | `src/core/context.ts` | +| `@alkdev/ujsx/events` | `src/core/events.ts` | +| `@alkdev/ujsx/pointer` | `src/core/pointer.ts` | +| `@alkdev/ujsx/host` | `src/host/config.ts` | +| `@alkdev/ujsx/transform` | `src/transform/registry.ts` | +| `@alkdev/ujsx/jsx-runtime` | `src/core/jsx-runtime.ts` | + +## Testing + +Tests are in `test/` using Vitest. Each test file maps to a specific feature area. Tests import directly from `src/` (not from `dist/`). When adding features, add or update corresponding test files. + +## Architecture Documentation + +Detailed architecture docs live in `docs/architecture/`: +- `schema.md` — TypeBox Module, type definitions, type guards +- `element-factory.md` — h(), createRoot(), createComponent(), Fragment +- `reactive-layer.md` — ReactiveRoot, reactiveComponent, reactiveElement, signal integration +- `host-config.md` — HostConfig interface, createRoot(), mount pipeline, reconciler gap +- `reconciler.md` — Fiber tree, reconciliation algorithm, update scheduling, TypeBox optimizations +- `lifecycle.md` — Mount, update, unmount/dispose lifecycle, signal cleanup +- `transforms.md` — TransformRegistry, TransformRule, TransformContext, bi-directional transforms +- `events.md` — EventEnvelope, PubSubLike, UjsxEventMap +- `pointers.md` — ValuePointer, selectNode, setNode, tree navigation +- `build-distribution.md` — Package structure, exports map, dependencies, runtime targets + +Design decisions (ADRs) are in `docs/architecture/decisions/`: +- 001: HTML-agnostic core +- 002: TypeBox Module as type registry +- 003: Preact signals-core for reactivity +- 004: `key` as first-class field on UElement +- 005: Signal-driven updates for props, reconciliation for structure \ No newline at end of file diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..0beb6a4 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,199 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributing patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party acknowledgments normally appear. The + contents of the NOTICE file are for informational purposes only + and do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work; provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, NOTHING herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + approprilicability of any law or regulation to your use of the Work + and for obtaining any necessary consents, authorizations, or other + permissions that may be necessary for Your use of the Work. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. + + Copyright 2025 alkdev + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..fbe1922 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 alkdev + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..62e3e7a --- /dev/null +++ b/README.md @@ -0,0 +1,358 @@ +# @alkdev/ujsx + +Universal JSX — runtime-agnostic reactive tree primitives with TypeBox schemas. + +UJSX treats JSX as an intermediate representation for multi-target rendering. The same declarative tree can target different hosts (markdown, graph structures, DOM, workflow engines) through a `HostConfig` adapter. No `onClick`, no `className`, no `style` — the tree is a pure data structure, and hosts are interpreters. + +## Install + +```bash +npm install @alkdev/ujsx +``` + +Requires Node.js 18+. Dual ESM/CJS output with TypeScript declarations. + +## Quick Start + +### Element Construction + +```typescript +import { h, createRoot, createComponent } from "@alkdev/ujsx"; +import type { UNode, UElement, URoot } from "@alkdev/ujsx"; + +const el: UElement = h("div", { class: "container" }, "hello", h("span", null, "world")); +const root: URoot = createRoot("app", h("h1", null, "Title")); +const MyComp = createComponent("MyComp", (props) => h("div", null, props.text as string)); +``` + +### HostConfig Rendering + +```typescript +import { createRoot as createHostRoot } from "@alkdev/ujsx/host"; +import type { HostConfig } from "@alkdev/ujsx/host"; +import { h } from "@alkdev/ujsx/h"; + +const host: HostConfig = { + name: "my-host", + createRootContext: (container) => ({ container }), + createInstance: (tag, props, ctx) => /* create your instance */, + createTextInstance: (text, ctx) => /* create text instance */, + appendChild: (parent, child, ctx) => /* attach child to parent */, +}; + +const root = createHostRoot(host, container); +root.render(h("div", { color: "red" }, "hello")); +root.unmount(); +``` + +### Reactive Trees + +```typescript +import { ReactiveRoot, signal, reactiveComponent } from "@alkdev/ujsx/reactive"; +import { h, createComponent } from "@alkdev/ujsx/h"; + +const r = new ReactiveRoot(h("div", null, "initial")); +r.update((prev) => h("div", null, "updated")); +const unsub = r.subscribe((node) => console.log(node)); +r.render((event) => console.log(event)); +unsub(); +r.dispose(); +``` + +### JSX Configuration + +Set `jsxImportSource` in `tsconfig.json` to use JSX syntax directly: + +```json +{ + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "@alkdev/ujsx" + } +} +``` + +Then write JSX that produces `UElement` trees: + +```tsx +const tree =
hello
; +``` + +### Bi-directional Transforms + +```typescript +import { TransformRegistry, childCtx, ctx as transformCtx } from "@alkdev/ujsx/transform"; +import type { TransformRule, Direction } from "@alkdev/ujsx/transform"; + +const registry = new TransformRegistry(); +registry.register({ + name: "div-to-paragraph", + direction: "ujsx→mdast" as Direction, + match: (n) => isUElement(n) && n.type === "div", + transform: (n, ctx, next) => ({ + type: "paragraph", + children: (n as UElement).children.map((c, i) => next(c, childCtx(n, ctx, i))), + }), + priority: 1, +}); + +const result = registry.transform(myNode, transformCtx("ujsx→mdast" as Direction)); +``` + +## Sub-path Exports + +Tree-shakeable imports — only pull in what you use: + +| Sub-path | Source | Key Exports | +|----------|--------|-------------| +| `@alkdev/ujsx` | `src/mod.ts` | All exports (barrel) | +| `@alkdev/ujsx/schema` | `src/core/schema.ts` | `UJSX`, `UElement`, `URoot`, `UNode`, `UPrimitive`, `isUElement`, `isURoot`, `isUPrimitive` | +| `@alkdev/ujsx/h` | `src/core/h.ts` | `h`, `createRoot`, `createComponent`, `Fragment`, `jsx`, `jsxs`, `jsxDEV` | +| `@alkdev/ujsx/reactive` | `src/core/reactive.ts` | `ReactiveRoot`, `reactiveComponent`, `reactiveElement`, `signal`, `computed`, `effect`, `batch` | +| `@alkdev/ujsx/context` | `src/core/context.ts` | `Context`, `Density`, `Direction`, `RenderContext` | +| `@alkdev/ujsx/events` | `src/core/events.ts` | `EventEnvelope`, `PubSubLike`, `UjsxEventMap`, `createPubSubEmitter`, `proxyEventEmitter` | +| `@alkdev/ujsx/pointer` | `src/core/pointer.ts` | `ValuePointer`, `selectNode`, `setNode` | +| `@alkdev/ujsx/host` | `src/host/config.ts` | `HostConfig`, `Root`, `createRoot` | +| `@alkdev/ujsx/transform` | `src/transform/registry.ts` | `TransformRegistry`, `TransformRule`, `TransformContext`, `TransformFn`, `childCtx`, `matchesSchema`, `ctx` | +| `@alkdev/ujsx/jsx-runtime` | `src/core/jsx-runtime.ts` | `jsx`, `jsxs`, `jsxDEV`, `Fragment` | + +## Core Types + +### UNode (union type) + +The fundamental tree node type. Every value in a UJSX tree is a `UNode`: + +```typescript +type UPrimitive = string | number | boolean | null; +type UElement = { + type: string; // tag name or component function + props: UniversalProps; // Record + children: UNode[]; + key?: string; // extracted from props, not in props +}; +type URoot = { + type: "root"; + props: UniversalProps; + children: UNode[]; +}; +type UNode = UPrimitive | UElement | URoot; +``` + +### PropValue + +```typescript +type PropValue = string | number | boolean | null | unknown[] | UNode | Record | ((...args: unknown[]) => unknown); +``` + +### ComponentFn & UComponent + +```typescript +type ComponentFn = (props: UniversalProps & { children?: UNode[] }) => UNode; + +interface UComponent

{ + (props: P & { children?: UNode[] }): UNode; + displayName?: string; + targets?: string[]; +} +``` + +### Type Guards + +```typescript +function isUElement(node: UNode): node is UElement; +function isURoot(node: UNode): node is URoot; +function isUPrimitive(node: UNode): node is UPrimitive; +``` + +## HostConfig Interface + +The `HostConfig` interface defines how UJSX interacts with a target platform: + +```typescript +interface HostConfig { + name: string; + createRootContext(container: unknown, options?: Record, context?: Context): RootCtx; + finalizeRoot?(ctx: RootCtx): void; + createInstance(tag: TTag, props: Record, ctx: RootCtx, parent?: Instance): Instance; + createTextInstance(text: string, ctx: RootCtx, parent?: Instance): Instance; + appendChild(parent: Instance, child: Instance, ctx: RootCtx): void; + insertBefore?(parent: Instance, child: Instance, before: Instance, ctx: RootCtx): void; + removeChild?(parent: Instance, child: Instance, ctx: RootCtx): void; + prepareUpdate?(instance: Instance, tag: TTag, prevProps: Record, nextProps: Record, ctx: RootCtx): unknown | null; + commitUpdate?(instance: Instance, payload: unknown, tag: TTag, prevProps: Record, nextProps: Record, ctx: RootCtx): void; + emit?(type: string, id: string, payload: unknown): void; + finalizeInstance?(instance: Instance, ctx: RootCtx): void; +} +``` + +**Type parameters:** +- `TTag` — string literal union constraining allowed element types +- `Instance` — host-specific instance type (e.g. `HTMLElement`, `Object3D`) +- `RootCtx` — host-specific root context (carries refs, handles, etc.) + +**Required methods:** `name`, `createRootContext`, `createInstance`, `createTextInstance`, `appendChild` +**Optional methods:** `finalizeRoot`, `insertBefore`, `removeChild`, `prepareUpdate`, `commitUpdate`, `emit`, `finalizeInstance` + +## Reconciler + +The reconciler manages fiber tree diffing, key-based children reconciliation, and signal-driven updates: + +| Export | Purpose | +|--------|---------| +| `scheduleUpdate(fiber, nextNode, host, ctx)` | Queue a fiber update (via `queueMicrotask`) | +| `flushUpdates(host, ctx)` | Process all pending updates | +| `reconcileProps(fiber, nextNode, host, ctx)` | Diff props using `Value.Diff` / `Value.Equal` / `Value.Hash` | +| `reconcileChildren(oldFibers, newChildren)` | Key-based classification into matched/added/removed/moves | +| `commitMutations(parentFiber, classification, commitCtx)` | Apply insertions, moves, removals to host instances | +| `commitEffects(fiber, host, ctx)` | Walk fiber tree and call `commitUpdate` for pending effects | +| `wireSignalToFiber(fiber, signalGetter, host, ctx)` | Bind a Preact signal to a fiber for automatic updates | +| `longestIncreasingSubsequence(arr)` | LIS algorithm for minimum-move reorder detection | + +### Fiber Type + +```typescript +interface Fiber { + instance: I; + tag: string; + props: Record; + key: string | undefined; + children: Fiber[]; + parent: Fiber | null; + effect: Effect | null; + signalDisposers: (() => void)[]; + prevProps: Record | null; + disposed: boolean; + cachedNode: UNode | null; + hash: bigint | null; +} + +type Effect = + | { type: "update"; payload: unknown } + | { type: "insert"; before: Fiber | null } + | { type: "move"; before: Fiber | null } + | { type: "remove" }; +``` + +### Children Reconciliation + +`reconcileChildren` uses key-based matching with LIS (Longest Increasing Subsequence) to minimize DOM moves: + +- **Keyed children** are matched by `key` across old and new lists +- **Unkeyed children** are matched positionally (left-to-right, first-available) +- The LIS of matched indices identifies children that don't need moving +- Non-LIS matched children are marked as moves +- Unmatched old children are removed; unmatched new children are added + +## Reactive Root + +```typescript +class ReactiveRoot { + constructor(initial: UNode); + get value(): ReadonlySignal; + update(fn: (current: UNode) => UNode): void; + subscribe(listener: (node: UNode) => void): () => void; + render(emit: (event: { type: string; id: string; payload: unknown }) => void): () => void; + dispose(): void; +} +``` + +## Context + +```typescript +class Context { + constructor(initial?: Partial); + get(): ContextValue; + get signal(): ReadonlySignal; + set(partial: Partial): void; + subscribe(fn: (value: ContextValue) => void): () => void; + fork(overrides: Partial): Context; +} + +type Density = "full" | "compact" | "minimal"; +type Direction = "ujsx→mdast" | "mdast→ujsx" | "ujsx→jpath" | "jpath→ujsx" | "ujsx→hast" | "hast→ujsx"; +interface ContextValue { density: Density; target: string; metadata: Record; } +``` + +## Events + +```typescript +interface EventEnvelope { + readonly type: TType; + readonly id: string; + readonly payload: TPayload; +} + +interface PubSubLike { + publish(type: TType, id: string, payload: TEventMap[TType]): void; + subscribe(type: TType, id: string): AsyncIterable>; +} + +type UjsxEventMap = { + "root.render": { childCount: number }; + "root.unmount": Record; + "instance.create": { kind: "text" | "element"; tag?: string; value?: string; props?: Record }; + "component.invoke": { type: string }; + "type.call": { objectName: string; methodName: string; args: unknown[] }; + "transform.apply": { ruleName: string; direction: string }; +}; +``` + +## Tree Pointers + +```typescript +class ValuePointer { + constructor(initial: T, path?: string[]); + get value(): T; + set value(v: T); + get reactive(): ReadonlySignal; + get path(): string[]; +} + +function selectNode(root: UNode, path: string[]): UNode | undefined; +function setNode(root: UNode, path: string[], value: UNode): UNode; +``` + +## TypeBox Runtime Validation + +The `UJSX` export is a `Type.Module` from `@alkdev/typebox`. Use it with `Value.Check` for runtime validation: + +```typescript +import { UJSX } from "@alkdev/ujsx/schema"; +import { Value } from "@alkdev/typebox/value"; + +const UElementSchema = UJSX.Import("UElement"); +Value.Check(UElementSchema, myElement); // true | false +``` + +Available schema keys: `UPrimitive`, `PropValue`, `UniversalProps`, `UElement`, `URoot`, `UNode`. + +## Design Principles + +1. **The tree is the truth. Hosts are interpreters.** UJSX defines what a tree looks like, not what it means. +2. **HTML-agnostic core.** No DOM-specific props. `onClick`, `className`, `style` are not special. +3. **TypeBox Module IS the type registry.** Runtime validation via `Value.Check`, compile-time types via TypeScript. +4. **Preact signals for reactivity.** Signal-driven updates for props, reconciliation for structure. +5. **`key` as first-class field.** Extracted from props, promoted to `UElement.key` — not stored in `props`. + +## Dependencies + +| Package | Version | Role | +|---------|---------|------| +| `@alkdev/typebox` | `^0.34.49` | Schema definition and runtime validation | +| `@preact/signals-core` | `^1.14.1` | Reactive primitives (`signal`, `effect`, `computed`, `batch`) | +| `@alkdev/pubsub` | `^0.1.0` | `PubSubLike` interface for event system | + +## Scripts + +| Command | Description | +|---------|-------------| +| `npm run build` | tsup production build (ESM + CJS) | +| `npm run build:tsc` | Type checking only (`tsc --noEmit`) | +| `npm run lint` | Type checking (`tsc --noEmit`) | +| `npm run test` | Run tests with Vitest | +| `npm run test:watch` | Vitest in watch mode | +| `npm run test:coverage` | Vitest with V8 coverage | + +## License + +Dual-licensed under [MIT](LICENSE-MIT) or [Apache 2.0](LICENSE-APACHE) at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project shall be dual-licensed as above, without any additional terms or conditions. \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index b591733..b0c7ff1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,11 +4,7 @@ "lib": ["ES2022"], "module": "Node16", "moduleResolution": "Node16", - "outDir": "./dist", "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "sourceMap": true, "strict": true, "noUncheckedIndexedAccess": true, "noUnusedLocals": true,