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

183 lines
8.6 KiB
Markdown

---
status: draft
last_updated: 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](https://tsup.egg.garden/) 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
```json
{
".": "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:
```typescript
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:
```json
{ "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
```json
"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
```json
"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.
```json
"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/` 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`