Files
ujsx/docs/architecture/build-distribution.md
glm-5.1 09f32f0c64 add architecture docs synced to current source and sdd process
Phase 1 of SDD process: syncing docs/architecture/ to reflect the
existing source code. Eight component documents describe WHAT and WHY
(not HOW) for each module: schema, element factory, reactive layer,
host config, transforms, events, pointers, and build distribution.
Three ADRs capture key decisions (HTML-agnostic core, TypeBox Module
as type registry, Preact signals-core for reactivity). Each doc
documents known reconciler gaps and references the research in
docs/research/reconciler/.

Also adds docs/sdd_process.md (process reference shared across
alkdev projects) matching the taskgraph_ts pattern.
2026-05-18 15:00:33 +00:00

8.6 KiB

status, last_updated
status last_updated
draft 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.

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-core package

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 coresrc/core/ and src/transform/ must not import fs, 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 exports map must always have matching import and require entries for each sub-path.
  • @alkdev/typebox is non-negotiable — TypeBox provides the schema system. Without it, Value.Check, Module.Import, and matchesSchema cannot 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, and batch from @preact/signals-core.

References

  • Source: package.json
  • Build tool: tsup configuration (inline in package.json or tsup.config.ts)
  • TypeBox: @alkdev/typebox
  • Preact signals: @preact/signals-core
  • PubSub: @alkdev/pubsub