docs: add README, dual license files, AGENTS.md; fix tsup DTS build
- Add README.md with full API reference and usage examples for npm publishing - Add LICENSE-MIT and LICENSE-APACHE for dual licensing (MIT OR Apache-2.0) - Update AGENTS.md with project instructions for AI agents - Fix tsconfig.json: remove declaration, declarationMap, outDir, and sourceMap which conflicted with tsup's own DTS generation (TS5055 overwrite errors)
This commit is contained in:
129
AGENTS.md
Normal file
129
AGENTS.md
Normal file
@@ -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<T>, selectNode(), setNode()
|
||||||
|
jsx-runtime.ts # Re-exports jsx/jsxs/jsxDEV/Fragment for jsxImportSource
|
||||||
|
host/
|
||||||
|
config.ts # HostConfig<TTag,Instance,RootCtx>, Root, createRoot(), mount/reconcile/unmount pipeline
|
||||||
|
fiber.ts # Fiber<I> type, Effect<I> 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<UNode>` 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<T>` wraps `signal<T>` 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
|
||||||
199
LICENSE-APACHE
Normal file
199
LICENSE-APACHE
Normal file
@@ -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.
|
||||||
21
LICENSE-MIT
Normal file
21
LICENSE-MIT
Normal file
@@ -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.
|
||||||
358
README.md
Normal file
358
README.md
Normal file
@@ -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<string, MyInstance, MyRootCtx> = {
|
||||||
|
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 = <div class="container"><span>hello</span></div>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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<UNode, unknown, unknown>();
|
||||||
|
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<string, PropValue | undefined>
|
||||||
|
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<string, unknown> | ((...args: unknown[]) => unknown);
|
||||||
|
```
|
||||||
|
|
||||||
|
### ComponentFn & UComponent
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type ComponentFn = (props: UniversalProps & { children?: UNode[] }) => UNode;
|
||||||
|
|
||||||
|
interface UComponent<P extends UniversalProps = UniversalProps> {
|
||||||
|
(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<TTag, Instance, RootCtx>` interface defines how UJSX interacts with a target platform:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface HostConfig<TTag extends string, Instance, RootCtx> {
|
||||||
|
name: string;
|
||||||
|
createRootContext(container: unknown, options?: Record<string, unknown>, context?: Context): RootCtx;
|
||||||
|
finalizeRoot?(ctx: RootCtx): void;
|
||||||
|
createInstance(tag: TTag, props: Record<string, unknown>, 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<string, unknown>, nextProps: Record<string, unknown>, ctx: RootCtx): unknown | null;
|
||||||
|
commitUpdate?(instance: Instance, payload: unknown, tag: TTag, prevProps: Record<string, unknown>, nextProps: Record<string, unknown>, 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<I> {
|
||||||
|
instance: I;
|
||||||
|
tag: string;
|
||||||
|
props: Record<string, unknown>;
|
||||||
|
key: string | undefined;
|
||||||
|
children: Fiber<I>[];
|
||||||
|
parent: Fiber<I> | null;
|
||||||
|
effect: Effect<I> | null;
|
||||||
|
signalDisposers: (() => void)[];
|
||||||
|
prevProps: Record<string, unknown> | null;
|
||||||
|
disposed: boolean;
|
||||||
|
cachedNode: UNode | null;
|
||||||
|
hash: bigint | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Effect<I> =
|
||||||
|
| { type: "update"; payload: unknown }
|
||||||
|
| { type: "insert"; before: Fiber<I> | null }
|
||||||
|
| { type: "move"; before: Fiber<I> | 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<UNode>;
|
||||||
|
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<ContextValue>);
|
||||||
|
get(): ContextValue;
|
||||||
|
get signal(): ReadonlySignal<ContextValue>;
|
||||||
|
set(partial: Partial<ContextValue>): void;
|
||||||
|
subscribe(fn: (value: ContextValue) => void): () => void;
|
||||||
|
fork(overrides: Partial<ContextValue>): 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<string, unknown>; }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface EventEnvelope<TType extends string = string, TPayload = unknown> {
|
||||||
|
readonly type: TType;
|
||||||
|
readonly id: string;
|
||||||
|
readonly payload: TPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PubSubLike<TEventMap> {
|
||||||
|
publish<TType>(type: TType, id: string, payload: TEventMap[TType]): void;
|
||||||
|
subscribe<TType>(type: TType, id: string): AsyncIterable<EventEnvelope<TType, TEventMap[TType]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type UjsxEventMap = {
|
||||||
|
"root.render": { childCount: number };
|
||||||
|
"root.unmount": Record<string, unknown>;
|
||||||
|
"instance.create": { kind: "text" | "element"; tag?: string; value?: string; props?: Record<string, unknown> };
|
||||||
|
"component.invoke": { type: string };
|
||||||
|
"type.call": { objectName: string; methodName: string; args: unknown[] };
|
||||||
|
"transform.apply": { ruleName: string; direction: string };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tree Pointers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class ValuePointer<T> {
|
||||||
|
constructor(initial: T, path?: string[]);
|
||||||
|
get value(): T;
|
||||||
|
set value(v: T);
|
||||||
|
get reactive(): ReadonlySignal<T>;
|
||||||
|
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.
|
||||||
@@ -4,11 +4,7 @@
|
|||||||
"lib": ["ES2022"],
|
"lib": ["ES2022"],
|
||||||
"module": "Node16",
|
"module": "Node16",
|
||||||
"moduleResolution": "Node16",
|
"moduleResolution": "Node16",
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user