ADR-023: Generated Production Runtime Is Parser-Free
ADR-022 moved generated messages from runtime-parsed ICU strings to executable functions. Generated applications still imported the public package roots, however, so the lazy parser for hand-written catalogs remained reachable from the production module graph even when no generated message used it.
Status: Accepted Date: 2026-08-02
Context
The parser is valuable compatibility code, but it should not be part of the standard browser cost for applications that compile their catalogs and macros.
Decision
Palamedes exposes explicit compiled entrypoints for the generated production
path:
@palamedes/core/compiled@palamedes/react/compiled@palamedes/solid/compiled
Generated catalog modules import their ABI from @palamedes/core/compiled.
Macro transforms and generated MDX modules import Trans from the matching
framework compiled entrypoint. These entrypoints depend only on the compiled
message engine and Intl formatters; they do not import the ICU parser.
The package roots remain the compatibility entrypoints. They continue to
support hand-written ICU string catalogs, parseMessagePattern(), and direct
getMessageNodes() access. Custom transform or MDX module overrides remain
honored and therefore own their chosen runtime boundary.
Within each framework, the package root and compiled entrypoint share one
framework-specific message renderer. That shared renderer imports only
@palamedes/core/compiled; the package root injects its parser as a
compatibility fallback, while the compiled entrypoint does not. Full Core
instances additionally expose an optional parse-only parsePattern()
capability, which treats its argument as raw ICU rather than looking it up as a
catalog key. Parser-free Core instances omit the capability.
React and Solid retain separate renderers because their result types, element cloning, wrapper-component behavior, and reactivity contracts differ. The boundary is shared across entrypoints within a framework, not across frameworks.
The parser-free Core factory rejects an unbranded string catalog when it is
loaded. A generated lazy fallback caused by invalid or unsupported ICU reports
the error through onError and returns the raw fallback. Normal generated
messages never use this path.
Alternatives Considered
1. Rely only on tree-shaking at the package root
Rejected because the compatibility factory closes over the parser and keeps it reachable. A separate leaf entrypoint makes the dependency boundary explicit and verifiable across bundlers.
2. Remove the parser from the package root
Rejected because it would break hand-written and runtime-loaded ICU catalogs. The compatibility behavior remains useful when applications deliberately opt out of build-time compilation.
3. Publish a separate runtime package
Rejected because the ABI belongs to Core and the framework adapters already have stable package identities. Subpath exports express the distinction without adding another package or version boundary.
Consequences
- Generated production builds do not ship the ICU parser by default.
- Hand-written ICU catalogs must import
createI18nfrom@palamedes/core. - Applications using generated catalogs can import
createI18nand catalog types from@palamedes/core/compiledto keep the parser unreachable. - Directly authored
Transimports may continue using package roots; generated transforms select the compiled entry automatically. - Root and compiled entrypoints no longer maintain parallel copies of their framework's message walker and runtime adapter.
- Raw fallback patterns cannot collide with an equal catalog key on current full Core instances because parsing no longer re-enters catalog lookup.
pnpm benchmark:runtime-browserbuilds the real Vite MDX example, verifies a stable parser sentinel against the compatibility entry, rejects that sentinel in browser assets, and reports raw, gzip, and Brotli JavaScript sizes.