Skip to content

dogfood: ship a class-name utility (cn) in @webjsdev/core #619

@vivek7405

Description

@vivek7405

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):

  1. 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.
  2. 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

  • The dead Base / defineElement / ServerHTMLElementStub block is removed from examples/blog/lib/utils/cn.ts; cn and the layout helpers remain
  • app/page.ts is no longer fetched on the blog home page (import-only), proven by a network-probe test with a counterfactual
  • The directives doc no longer claims classMap / styleMap / ifDefined are shipped core exports
  • Differential elision stays output-identical; blog e2e green
  • webjs-doc-sync run to catch any sibling surface repeating the false claim

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions