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

8.4 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).

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

"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