Skip to content

docs(docs-app): wire up build-time .gjs.md docs, start .md migration - #743

Open
patricklx wants to merge 5 commits into
mainfrom
docs/kolay-wrap-demos-status
Open

docs(docs-app): wire up build-time .gjs.md docs, start .md migration#743
patricklx wants to merge 5 commits into
mainfrom
docs/kolay-wrap-demos-status

Conversation

@patricklx

@patricklx patricklx commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to the "check whether kolay's new shadow-DOM support can replace our downstream implementation" investigation, now extended into starting the migration of docs-app's component docs from runtime-compiled .md (kolay's in-browser REPL) to build-time-compiled .gjs.md.

Why move to .gjs.md

.md docs are compiled on-demand in the browser (via repl-sdk); .gjs.md docs are compiled once at build time into real Ember components. Moving reduces runtime REPL/compile cost per page and gets these docs full type-checking via glint.

What this PR adds

kolay's vite plugin (kolay()) already forwards scope/rehypePlugins/remarkPlugins to its build-time .gjs.md compiler (undocumented in kolay's public Options type, but wired in combined.js), so no upstream change was needed. Two real gaps had to close first, both scoped to the build-time path only:

  • rehypeShadowDemo silently produced empty demos. It renames each demo's placeholder <div id="repl_N"> to <carbon-shadow-demo id="repl_N">. Runtime .md grafts the compiled demo into that element by id directly. Build-time .gjs.md (kolay's gjs-md.js) instead finds the node by id and injects the invocation by string-replacing the node's literal </div> — which no longer exists once the div is renamed, so the replace silently no-ops and the demo renders styled but empty. Fixed by giving rehypeShadowDemo an opt-in { forBuildTimeInjection: true } mode that keeps an inner <div> for the injector to target (CarbonShadowDemo re-parents whatever ends up inside it regardless of tag, so the wrapper is otherwise inert). The runtime call site in routes/application.ts is untouched and emits byte-identical output.
  • Two demo-fence import specifiers don't actually resolve. carbon-components-ember/components and carbon-components-ember/helpers only work at runtime because setupKolay's dynamic modules map intercepts them before real module resolution — the package's real exports only has the /index forms. Aliased both (anchored RegExps, so already-correct /index//icon specifiers elsewhere aren't affected) in vite.config.mjs. Also added a docs-support alias (new docs-app/app/docs-support/index.ts barrel) backing the ThemeSupport/didInsert imports demos use, and a scope string so <ThemeSwitcher />/<APIDocs>/<ComponentSignature>/<ModifierSignature>/<Callout> resolve at the top level of a .gjs.md file, mirroring the runtime topLevelScope.

Converted so far

tags.md and list/item.md (one flat page, one nested-folder page, to exercise both URL shapes) — verified in a real browser (not just glint/a green build) that the demo's shadow root contains real rendered component markup, compared against an unconverted .md control page in the same session. An empty demo looks identical to a known docs-app live-preview flake otherwise, so build success alone isn't proof.

notifications.md is intentionally left as .md: its demo uses setOwner() on a plain (non-Component) class to get @service injection working, which has no build-time-import equivalent without rewriting it as a real Component subclass first.

~58 .md docs remain. Recipe and gotchas recorded for follow-up work.

The original comment fix

shadow-demo-element.ts still claimed it imports the app's stylesheets into each shadow root "mirroring <Shadowed includeStyles>". It stopped doing that in 1d7bfa3 (do not add host styles), which left STYLESHEET_SELECTOR dead and the comment wrong. Shadow demos are styled anyway because component docs render <ThemeSupport /> inside the demo, emitting the Carbon stylesheets as an inline <style> within the shadow root; :root theme tokens inherit across the boundary as custom properties.

Test plan

  • glint in docs-app: 20 errors, identical to the main baseline (all pre-existing, none in touched files)
  • DOCS_URL=versions/main pnpm build in docs-app: succeeds
  • Served the production build and browser-checked (Playwright) that 2-components/tags and 2-components/list/item render real component markup inside their demos' shadow roots, matching a .md control page
  • root pnpm lint: no new errors/warnings

🤖 Generated with Claude Code

@patricklx
patricklx force-pushed the docs/kolay-wrap-demos-status branch from 2d8fa61 to 17681b4 Compare August 7, 2026 14:57
@patricklx patricklx changed the title docs(docs-app): correct shadow-demo comments; note kolay wrapDemos status docs(docs-app): wire up build-time .gjs.md docs, start .md migration Aug 14, 2026
patricklx and others added 2 commits August 14, 2026 12:51
…atus

`shadow-demo-element.ts` still claimed it imports the app's stylesheets into
each shadow root "mirroring `<Shadowed includeStyles>`". It stopped doing that
in 1d7bfa3 ("do not add host styles"), which left the `STYLESHEET_SELECTOR`
constant dead and the comment wrong. Drop the constant and describe what
actually supplies the CSS: demos render `<ThemeSupport />` themselves, so the
Carbon stylesheets land inside the shadow root as an inline `<style>`, and
`:root` theme tokens inherit across the boundary as custom properties.

Also record why `rehype-shadow-demo.ts` stays hand-rolled. kolay merged a
generic `wrapDemos` rehype plugin (universal-ember/kolay#361, unreleased as of
kolay 5.4.0 - it ships in the pending 6.0.0), but it wraps the demo placeholder
in a component invocation rather than renaming it. repl-sdk grafts each
compiled demo in afterwards via `element.querySelector('#<placeholderId>')`,
which does not cross a shadow boundary, so a shadow-DOM wrapper would hide the
placeholder and trip repl-sdk's "Could not find placeholder / target element"
assertion. Renaming the placeholder keeps it in the light DOM, which is why the
current implementation works.

Comments only - no behavior change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Patrick Pircher <patrick.pircher@ibm.com>
Start of moving docs-app component docs off runtime-compiled .md (kolay's
in-browser REPL) to build-time-compiled .gjs.md, following up on this PR's
own investigation into kolay's wrapDemos status.

Two infra gaps had to close first, both scoped to build-time docs only:

- rehypeShadowDemo renames each demo placeholder div to a custom element,
  which kolay's build-time compiler can't find (it string-replaces the
  placeholder's literal `</div>`, which no longer exists), silently
  producing empty demos. Give it an opt-in `forBuildTimeInjection` mode
  that keeps an inner `<div>` for the injector to target; the runtime call
  site is untouched and emits byte-identical output.
- Demo fences import `carbon-components-ember/components` and
  `carbon-components-ember/helpers`, which only resolve at runtime via
  setupKolay's dynamic module map (the package's real exports are the
  `/index` forms). Alias them for the build-time path, plus a `docs-support`
  alias backing `ThemeSupport`/`didInsert` and a `scope` string so
  `<ThemeSwitcher />`/`<APIDocs>`/etc. resolve at the top level of a
  `.gjs.md` file, mirroring the runtime `topLevelScope`.

Converts tags.md and list/item.md (one flat, one nested-folder page) as a
verified spike - browser-checked (not just glint/build) against an
unconverted control page, since an empty demo looks identical to a known
docs-app live-preview flake otherwise. notifications.md is intentionally
left as .md: its demo uses setOwner() on a plain class for @service
injection, which has no build-time equivalent without rewriting it as a
real Component.

Remaining ~58 .md docs still need converting; recipe and gotchas recorded
in .pjp-runner/todo.md for follow-up work.

Signed-off-by: Patrick Pircher <patrick.pircher@ibm.com>
@patricklx
patricklx force-pushed the docs/kolay-wrap-demos-status branch from 54523d9 to c9f3c2c Compare August 14, 2026 10:51
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

📖 Docs Preview

🔗 Preview URL: https://ibm.github.io/carbon-components-ember/pr-previews/pr-743/

Built from commit 17d51ac

github-actions Bot pushed a commit that referenced this pull request Aug 14, 2026
Applies the recipe from the Tag/List Item spike (previous commit) to the
remaining 32 flat top-level component docs, moving them off the
runtime-compiled REPL onto kolay's build-time .gjs.md compiler. Each file
browser-verified (Playwright, headless build of dist) to confirm its live
demos actually render component markup inside their shadow root, not just
that glint/build stay green - an empty demo looks identical to a known
docs-app live-preview flake otherwise.

pagination.md's "sizes" demo used `@onPageChanged={{() => {}}}`, an inline
arrow expression inside a mustache that kolay's runtime REPL tolerated but
the real ember-template-compiler used for build-time .gjs.md does not
(`Expecting 'OPEN_SEXPR', ... got 'CLOSE_SEXPR'`). Fixed to use the same
`const noop = () => null` + `{{noop}}` pattern already used by the other two
demos in that file.

Remaining nested-folder docs (form/, list/, select/, etc.) and
1-get-started/index.md are left for a follow-up batch; notifications.md
stays .md pending its Context class becoming a real Component (see
.pjp-runner/todo.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Patrick Pircher <patrick.pircher@ibm.com>
github-actions Bot pushed a commit that referenced this pull request Aug 14, 2026
…js.md

Continues the flat-file batch: converts the form/, indicator/, layout/,
list/, select/, skeleton/, and text-input/ subfolders (22 files) to
build-time .gjs.md, following the same recipe. Each browser-verified
(Playwright against a built dist) to confirm live demos render real content
in their shadow root.

progress/bar.md is left as .md: invoking bare <ProgressBar /> in a built
.gjs.md page throws "Cannot destructure property 'compilable' of 'component'
as it is null" during template compilation, regardless of args. Bisected via
scratch pages to confirm it's not markdown content, not IconMap/registerIcon
state (nothing in the library ever calls registerIcon, so that registry is
always empty and unrelated), and not the generic <Icon> component in
isolation (a standalone <Icon @ICON='...' /> demo builds and renders fine).
ProgressBar is the only component in the library that invokes the generic
<Icon @ICON='...'> string-keyed component from within a conditional block,
so the bug is scoped to how kolay's build-time per-page compiler resolves
that specific nested-conditional-component pattern from a precompiled
library template. Needs deeper investigation into kolay/embroider's
component resolution for this case; noted in .pjp-runner/todo.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Patrick Pircher <patrick.pircher@ibm.com>
github-actions Bot pushed a commit that referenced this pull request Aug 14, 2026
@patricklx

Copy link
Copy Markdown
Collaborator Author

the code blocks in docs do not have syntax highlighting. it works on main

patricklx reported code blocks lost syntax highlighting on pages converted
to build-time .gjs.md (works on main's runtime-compiled .md). vite.config.mjs's
kolay() plugin only wired up rehypeShadowDemo for the build-time compiler;
routes/application.ts's runtime setupKolay() call separately registers
rehypeShikiFromHighlighter, which the build-time path never got.

Added the equivalent rehypeShiki plugin to kolay()'s rehypePlugins, using
@shikijs/rehype's default export (self-contained highlighter creation) since
vite.config.mjs runs in Node at build time and doesn't need the runtime
path's manual getHighlighterCore/loadWasm dance. Verified via Playwright
that a converted page (tags) and an unconverted control (notifications,
still runtime .md) render visually identical multi-color token highlighting,
and that every built .gjs chunk contains shiki output.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Patrick Pircher <patrick.pircher@ibm.com>
@patricklx

Copy link
Copy Markdown
Collaborator Author

Fixed the syntax-highlighting regression (17d51acd).

Root cause: vite.config.mjs's kolay() plugin (build-time .gjs.md compiler) only registered rehypeShadowDemo in rehypePlugins. routes/application.ts's runtime setupKolay() call separately registers rehypeShikiFromHighlighter for syntax highlighting — the build-time path never got the equivalent plugin, so converted pages' code fences rendered as plain unhighlighted text.

Fix: added @shikijs/rehype's default export to kolay()'s rehypePlugins, with the same langs/theme config as the runtime setup. Since vite.config.mjs runs in Node at build time, it can use the package's self-contained highlighter creation instead of the runtime path's manual getHighlighterCore/loadWasm (which exists only to work around browser WASM loading).

Verified:

  • DOCS_URL=versions/main pnpm build succeeds; every built .gjs chunk now contains shiki output (grep shiki-light on all dist/assets/*.gjs-*.js)
  • Playwright, served prod build: 2-components/tags (converted, build-time path) and 2-components/notifications (still .md, runtime path — the one intentionally-unconverted control page) render visually identical multi-color token highlighting, same shiki shiki-themes classes
  • glint/root pnpm lint stay at the existing baseline (no new errors/warnings)

🤖 Generated with Claude Code

@patricklx patricklx removed the review label Aug 14, 2026
github-actions Bot pushed a commit that referenced this pull request Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant