Skip to content

Development and Production Builds

lit-ui-router and lit-ui-router-mobx each publish two builds of the same code. Your bundler picks one automatically through the development export condition: a dev server or a development build resolves dist/development/index.js, a production build resolves dist/index.js. There is nothing to install, configure, or opt into.

The two builds behave identically except for one thing: the development build carries console warnings that the production build does not. Those warnings are not merely disabled in production — import.meta.env.DEV is substituted with a literal at emit time, so the guarded bodies fold out of dist/*.js entirely, message strings included. A production bundle cannot be made to print them.

Which packages ship it

PackageTwo buildsWhy
lit-ui-routeryesthree development warnings
lit-ui-router-mobxyesone development warning
navigation-location-pluginnono runtime warnings to fold out
ui-router-servernono runtime warnings to fold out
eslint-plugin-lit-ui-routernoa lint plugin — it never runs in your app bundle

The split exists where there is something to strip. A package with a single build is not missing a feature.

Every entry point of lit-ui-router is covered, not just the root: pure, register, ui-router.register, and ui-view.register each carry their own development condition.

The second gate: lit's own build

Every warning here is gated twice. Past import.meta.env.DEV, it also checks that lit itself resolved to its development build, by probing for ReactiveElement.enableWarning — a method lit declares optional precisely because it exists only in development.

So a development build of these packages paired with a production lit stays silent. That pairing is unusual, but it is what a consumer gets when their bundler resolves the two packages under different conditions, and a warning that fires there would be noise rather than signal.

The warnings

lit-ui-router

WarningFires whenFix
uiSref wrote an inert hrefassignHref: true (the default) writes href to an element with no href in HTMLPass assignHref: 'auto' to scope it to real links — see Design System Links
uiSrefActive took over an aria-currentthe directive finds an aria-current it did not set, and will remove it when the state goes inactivePass ariaCurrentValue: false to keep the attribute yours
no <ui-router> ancestora <ui-view>, uiSref, or uiSrefActive finds no router after its seek has actually runWrap the subtree in <ui-router>, or pass a router explicitly

The missing-router warning fires once per element across all three sites, not once per binding: a single missing provider trips uiSref's render and its click on the same element, and a wall of near-identical messages obscures the one fix.

lit-ui-router-mobx

WarningFires whenFix
no <ui-router> ancestora RouterReactionController's host has no <ui-router> above itWrap the subtree in <ui-router>, or pass options.router

This one has a consequence the message does not state: the controller then observes nothing. .value stays at options.initialValue for the life of the host, and the host is never asked to update — so the symptom is a component that renders once with its initial value and never again.

What still warns in production

Two warnings are not part of the split and ship in both builds: <ui-router> and <ui-view> each warn if their tag name is already defined, then skip registration. That is a first-definition-wins degradation rather than a define() throw, and it usually means two copies of the package are loaded — worth saying in production, because it is a packaging fault rather than an authoring mistake.

Catching the same problems at author time

Two of the warnings above have static counterparts in eslint-plugin-lit-ui-router — the inert href and the silent aria-current takeover — alongside a rule for the directives' element-part-only constructor throw, which is unconditional and fires in both builds. Lint reports all three across the whole codebase at author time, including for code paths a production build will never warn about and a development run may never reach.

How the split is kept honest

Each of the two packages declares its development-only message prefixes in a dev-warnings.json beside its manifest. The check:dev-split task (@tools/oxc-emit) reads that list after a build and asserts both directions: every declared prefix appears in dist/development/*.js, and none appears in dist/*.js. Adding a warning behind the import.meta.env.DEV guard without declaring it fails the check — so the list cannot silently drift from the code, in either direction.