Problem
The blog's home page (app/page.ts) ships its whole module to the browser (visible as page.ts in the network tab) instead of being dropped as import-only (#605). Root cause: examples/blog/lib/utils/cn.ts mixes two unrelated things in one file, a pure class-name merger (cn) and a set of custom-element helpers (Base, defineElement, ServerHTMLElementStub) that reference the customElements client global. The elision analyser's CLIENT_GLOBAL_RE (packages/server/src/component-elision.js:164) therefore flags the WHOLE module client-effecting. components/ui/input.ts / button.ts import only { cn } from it, chat-box imports those, so the page closure (app/page.ts -> chat-box -> ui/{input,button}.ts -> lib/utils/cn.ts) contains a client-effecting NON-component, which disqualifies the page from import-only (component-elision.js:1005).
Verified: Base / defineElement are DEAD code in the blog. Nothing imports them (grep -rnE '\b(Base|defineElement)\b' examples/blog --include=*.ts returns only the definition in cn.ts); the blog migrated every component to WebComponent. So the customElements reference that blocks import-only is entirely removable dead weight.
Separately, the docs claim webjs ships the lit classMap / styleMap / ifDefined directives "as runtime exports for parity" (docs/app/docs/directives/page.ts:91), but they are NOT exported from @webjsdev/core (confirmed at src, dist, .d.ts, and by importing the built bundle: classMap is missing; only until from that list exists). That is doc drift to correct in the same change.
Design / approach
Minimal, no core change (decided 2026-06-19):
- Delete the dead custom-element block from
examples/blog/lib/utils/cn.ts (ServerHTMLElementStub, Base, defineElement, and the HasHTMLElement guard). Leave cn + the layout/typography string helpers. With no customElements reference, cn.ts is no longer client-effecting, so the home-page closure has no client-effecting non-component and app/page.ts becomes import-only and drops from the boot.
- Fix the directives doc page so it stops claiming
classMap / styleMap / ifDefined are shipped exports. Describe them as native-JS patterns (which is the framework's actual stance per AGENTS.md), not core exports.
Explicitly NOT in scope: adding a cn or classMap export to @webjsdev/core. classMap does not do Tailwind conflict-merge so it cannot replace cn in the kit, and shipping a core cn is a larger DX decision deferred unless real demand appears.
Implementation notes (for the implementing agent)
- Where to edit:
examples/blog/lib/utils/cn.ts: remove the ServerHTMLElementStub class, the Base export, the defineElement export, and the HasHTMLElement const. Keep cn, ClassValue, walk, dedupeUtilities, variantPrefix, and the fieldClass / stackClass / typography helpers.
docs/app/docs/directives/page.ts around L91: correct the "webjs ships these as runtime exports for parity" sentence.
- Landmines:
- Confirm again before deleting that NOTHING (blog or any other in-repo app) imports
Base / defineElement from this file. If a future file does, move those helpers to their own module rather than deleting, so the class helper still sheds the customElements reference.
- The win is an ELISION verdict change, so it is invisible to
npm test. Prove it with the network-probe layer: after the change, app/page.ts must be ABSENT from the home page's boot script / modulepreload set, and the interactive leaves (counter, chat-box, etc.) still present. Use the e2e network-probe / the differential-elision harness, not a unit assertion alone.
- Do not "fix" the docs by ADDING the exports; the decision is to make the docs match the code (native JS), not the code match the docs.
- Invariants: elision must stay output-identical (server AGENTS invariant 7,
test/elision/differential-elision.test.js); light-DOM tag-prefix and styling conventions unaffected; no banned prose glyphs in the doc edit (root AGENTS invariant 11).
- Tests + docs:
- e2e/network-probe asserting
page.ts is no longer fetched on the blog home page, with a counterfactual (restore the dead block -> page.ts ships again).
- Run the four-app dogfood gate (the blog e2e covers this app).
- Docs: the directives page fix above; run the webjs-doc-sync skill to catch any sibling surface repeating the false classMap claim (website, agent-docs).
Acceptance criteria
Problem
The blog's home page (
app/page.ts) ships its whole module to the browser (visible aspage.tsin the network tab) instead of being dropped as import-only (#605). Root cause:examples/blog/lib/utils/cn.tsmixes two unrelated things in one file, a pure class-name merger (cn) and a set of custom-element helpers (Base,defineElement,ServerHTMLElementStub) that reference thecustomElementsclient global. The elision analyser'sCLIENT_GLOBAL_RE(packages/server/src/component-elision.js:164) therefore flags the WHOLE module client-effecting.components/ui/input.ts/button.tsimport only{ cn }from it,chat-boximports those, so the page closure (app/page.ts->chat-box->ui/{input,button}.ts->lib/utils/cn.ts) contains a client-effecting NON-component, which disqualifies the page from import-only (component-elision.js:1005).Verified:
Base/defineElementare DEAD code in the blog. Nothing imports them (grep -rnE '\b(Base|defineElement)\b' examples/blog --include=*.tsreturns only the definition incn.ts); the blog migrated every component toWebComponent. So thecustomElementsreference that blocks import-only is entirely removable dead weight.Separately, the docs claim webjs ships the lit
classMap/styleMap/ifDefineddirectives "as runtime exports for parity" (docs/app/docs/directives/page.ts:91), but they are NOT exported from@webjsdev/core(confirmed at src, dist,.d.ts, and by importing the built bundle:classMapis missing; onlyuntilfrom that list exists). That is doc drift to correct in the same change.Design / approach
Minimal, no core change (decided 2026-06-19):
examples/blog/lib/utils/cn.ts(ServerHTMLElementStub,Base,defineElement, and theHasHTMLElementguard). Leavecn+ the layout/typography string helpers. With nocustomElementsreference,cn.tsis no longer client-effecting, so the home-page closure has no client-effecting non-component andapp/page.tsbecomes import-only and drops from the boot.classMap/styleMap/ifDefinedare shipped exports. Describe them as native-JS patterns (which is the framework's actual stance per AGENTS.md), not core exports.Explicitly NOT in scope: adding a
cnorclassMapexport to@webjsdev/core.classMapdoes not do Tailwind conflict-merge so it cannot replacecnin the kit, and shipping a corecnis a larger DX decision deferred unless real demand appears.Implementation notes (for the implementing agent)
examples/blog/lib/utils/cn.ts: remove theServerHTMLElementStubclass, theBaseexport, thedefineElementexport, and theHasHTMLElementconst. Keepcn,ClassValue,walk,dedupeUtilities,variantPrefix, and thefieldClass/stackClass/ typography helpers.docs/app/docs/directives/page.tsaround L91: correct the "webjs ships these as runtime exports for parity" sentence.Base/defineElementfrom this file. If a future file does, move those helpers to their own module rather than deleting, so the class helper still sheds thecustomElementsreference.npm test. Prove it with the network-probe layer: after the change,app/page.tsmust be ABSENT from the home page's boot script / modulepreload set, and the interactive leaves (counter,chat-box, etc.) still present. Use the e2e network-probe / the differential-elision harness, not a unit assertion alone.test/elision/differential-elision.test.js); light-DOM tag-prefix and styling conventions unaffected; no banned prose glyphs in the doc edit (root AGENTS invariant 11).page.tsis no longer fetched on the blog home page, with a counterfactual (restore the dead block -> page.ts ships again).Acceptance criteria
Base/defineElement/ServerHTMLElementStubblock is removed fromexamples/blog/lib/utils/cn.ts;cnand the layout helpers remainapp/page.tsis no longer fetched on the blog home page (import-only), proven by a network-probe test with a counterfactualclassMap/styleMap/ifDefinedare shipped core exports