Files
ujsx/docs/architecture/build-distribution.md
glm-5.1 fbf13ed444 fix: update platform support docs for cross-runtime compatibility
- README: Node.js 18+ → Node.js 18+, Deno, and Bun
- package.json: add deno:true field
- build-distribution.md: consolidate engine/platform sections
2026-05-19 07:15:45 +00:00

173 lines
8.4 KiB
Markdown

---
status: stable
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.
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).
## Platform Support
UJSX runs in Node.js 18+, Deno, and Bun. Core has no Node-specific API calls — no `fs`, `path`, `child_process`, or other builtins. `src/core/` and `src/transform/` are fully platform-agnostic.
The `engines` field in `package.json` specifies Node as a lower bound for npm consumers. Deno support is provided via `deno.json` with direct source imports.
## 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`