Critical fixes: - Restructure pointers.md: move setNode prop-key writes section under its own heading (was incorrectly nested under selectNode) - Add Context/Density/Direction/RenderContext documentation section to host-config.md (was only a brief constraint bullet) - Advance all 5 ADRs from Status: Proposed → Accepted and frontmatter from status: draft → status: stable (decisions are driving implementation) - Add error handling philosophy section to README Warning/suggestion fixes: - Add isUElement null check (node !== null) to schema.md discriminator table - Add UjsxEnvelope convenience type documentation to events.md - Add Direction Unicode arrow naming note to transforms.md - Standardize all cross-references from absolute docs/research/ paths to relative ../research/ paths across all architecture docs - Fix schema.md ADR references to use relative paths - Reduce redundancy between transforms.md and host-config.md Direction notes - Update all architecture doc frontmatter from draft → stable Deferred: - Performance model section (reconciler not yet built) - Concepts/glossary document (low ROI at current scale) - Line counts in source references (would date quickly)
8.8 KiB
status, last_updated
| status | last_updated |
|---|---|
| stable | 2026-05-18 |
Build & Distribution
Package structure, exports map, dependencies, and runtime targets.
Package
- Name:
@alkdev/ujsx - Version: 0.1.0 (pre-release)
- License: MIT OR Apache-2.0 (dual-licensed, consumer chooses)
- Module type: ESM (
"type": "module")
Build
UJSX uses tsup for building. tsup compiles TypeScript to dual-format output (ESM + CJS) with type declarations in a single build step, replacing the need for separate tsc and bundler passes.
Build commands
| Command | Purpose |
|---|---|
npm run build |
tsup production build (writes to dist/) |
npm run build:tsc |
tsc --noEmit (type checking only) |
npm run lint |
tsc --noEmit (same as build:tsc) |
npm run test |
vitest run |
npm run test:watch |
vitest in watch mode |
npm run test:coverage |
vitest run --coverage |
Output format
Each entry point produces four files:
| File | Purpose |
|---|---|
*.js |
ESM module |
*.cjs |
CJS module |
*.d.ts |
ESM type declarations |
*.d.cts |
CJS type declarations |
tsup generates both ESM and CJS from a single TypeScript source. The exports map in package.json routes consumers to the correct output based on their module resolution strategy.
Sub-path Exports
{
".": "src/mod.ts",
"./schema": "src/core/schema.ts",
"./h": "src/core/h.ts",
"./reactive": "src/core/reactive.ts",
"./context": "src/core/context.ts",
"./events": "src/core/events.ts",
"./pointer": "src/core/pointer.ts",
"./host": "src/host/config.ts",
"./transform": "src/transform/registry.ts",
"./jsx-runtime": "src/core/jsx-runtime.ts"
}
Each sub-path export maps directly to a single source file. There is no barrel re-export within sub-paths — ./schema resolves to schema.ts only, not to a directory index that re-exports multiple modules.
Design rationale
Sub-path exports exist for tree-shaking. Without them, importing ValuePointer would pull in the entire mod.ts barrel, including the HostConfig, TransformRegistry, and event system. With sub-paths, a consumer can import only what they use:
import { ValuePointer } from "@alkdev/ujsx/pointer";
This results in a smaller bundle because bundlers can eliminate unused exports at the sub-path boundary. The barrel export (.) remains available for convenience — consumers who want everything can import from the root.
jsx-runtime
The ./jsx-runtime export enables jsxImportSource configuration. When a consumer sets:
{ "compilerOptions": { "jsxImportSource": "@alkdev/ujsx" } }
TypeScript and other JSX transforms resolve jsx, jsxs, and jsxDEV from @alkdev/ujsx/jsx-runtime. All three are aliases for h() — UJSX does not distinguish between static children, dynamic children, or dev mode at the factory level.
The jsx-runtime export also re-exports Fragment from h.ts. This means JSX consumers can import { Fragment } from @alkdev/ujsx/jsx-runtime as well as from @alkdev/ujsx/h or the barrel export.
Dependencies
| Package | Version | Role | Hard/Peer? |
|---|---|---|---|
@alkdev/typebox |
^0.34.49 |
Schema definition, runtime validation (Value.Check, Module) |
Hard |
@preact/signals-core |
^1.14.1 |
Reactive primitives (signal, effect, computed, batch) |
Hard |
@alkdev/pubsub |
^0.1.0 |
PubSubLike interface for event system |
Hard (see note) |
@alkdev/pubsub consideration
@alkdev/pubsub provides the PubSubLike interface used by the event system. It is currently a hard dependency, but UJSX only uses it for type definitions — the PubSubLike<TEventMap> interface and EventEnvelope type. The actual pubsub implementation is injected by consumers.
This is a candidate for moving to a peer dependency or even an optional dependency. Making it a peer would:
- Remove the runtime dependency for consumers that don't use the event system
- Make it explicit that UJSX does not provide a pubsub implementation
- Allow consumers to choose their pubsub version independently
The current hard dependency works because @alkdev/pubsub is lightweight and unlikely to cause version conflicts within the @alkdev scope, but it should be reconsidered before 1.0.
@alkdev/typebox
TypeBox is a hard dependency, not optional. The UJSX module is a Type.Module — it is imported and used at runtime for Value.Check() in schema validation and matchesSchema() in transforms. Removing TypeBox would remove the ability to validate UNode structures at runtime, which is a core feature.
@preact/signals-core
Preact signals-core is the reactive layer. ValuePointer wraps signal<T>, Context uses signal<ContextValue>, and the reactive module (reactive.ts) composes effect, computed, and batch. This is a hard dependency because reactivity is fundamental to UJSX's update model, not an optional feature.
Dev Dependencies
| Package | Purpose |
|---|---|
typescript |
Type checking and declaration generation |
tsup |
Build tool (dual ESM/CJS output) |
vitest |
Test runner |
@vitest/coverage-v8 |
Code coverage |
@types/node |
Node.js type definitions |
No runtime dev dependencies. No test framework besides Vitest. No linter configuration (type checking via tsc --noEmit serves as the static analysis step).
Engine Requirements
"engines": { "node": ">=18.0.0" }
Node 18 is the minimum for ESM support, structuredClone, and stable fetch. UJSX does not use Node-specific APIs in its core module paths — no fs, no path, no child_process. Platform-specific features (file I/O, Node APIs) should go in separate packages.
Platform Agnosticism
UJSX core is platform-agnostic. It runs in:
- Node.js 18+
- Deno (with npm specifiers)
- Bun
- Any browser that supports ESM and the
@preact/signals-corepackage
There are no Node-specific API calls in src/core/ or src/transform/. The src/host/ directory contains host configurations, but these are also platform-agnostic — they define what "create instance" and "update instance" mean for a given target, not how to interact with Node.js APIs.
Published Files
"files": ["dist"]
Only the dist/ directory is published to npm. Source maps are generated by tsup but source files (src/) are not included in the package. This keeps the package small and signals that dist/ is the stable API surface.
"publishConfig": { "access": "public" }
The package is publicly accessible on npm under the @alkdev scope.
Known Gaps
No source maps in production
tsup generates source maps, but the files field only includes dist/. Consumers can debug the compiled output but cannot step through the original TypeScript source without cloning the repo. Adding src/ to the published files or enabling source map hosting would improve the debugging experience.
No tree-shaking validation
The sub-path export strategy is designed for tree-shaking, but there is no CI step that validates bundle sizes or checks that unused sub-paths are eliminated. A size-limit or bundlewatch check would confirm that the exports map achieves its goal.
CJS compatibility is untested
The exports map provides CJS entry points, but the test suite runs via Vitest with ESM imports. CJS consumers may encounter edge cases (named exports, default export handling) that are not covered by automated tests.
Constraints
- ESM primary — the module is
"type": "module". CJS is a distribution compatibility layer, not the primary module format. Consumers should import as ESM. - No platform-specific APIs in core —
src/core/andsrc/transform/must not importfs,path,child_process, or other Node.js built-in modules. Platform-specific features belong in separate packages. - Dual format via tsup — the same TypeScript source produces ESM and CJS. No separate build pipelines. The
exportsmap must always have matchingimportandrequireentries for each sub-path. - @alkdev/typebox is non-negotiable — TypeBox provides the schema system. Without it,
Value.Check,Module.Import, andmatchesSchemacannot function. Do not make it optional or replace it with a lighter alternative without re-evaluating the entire validation strategy. - @preact/signals-core is the reactive primitive — do not introduce an alternative reactive system (RxJS, Solid signals, Vue reactivity) alongside Preact signals. All reactive state flows through
signal,effect,computed, andbatchfrom@preact/signals-core.
References
- Source:
package.json - Build tool: tsup configuration (inline in
package.jsonortsup.config.ts) - TypeBox:
@alkdev/typebox - Preact signals:
@preact/signals-core - PubSub:
@alkdev/pubsub