Add SSR support to docs-app using vite-ember-ssr#292
Add SSR support to docs-app using vite-ember-ssr#292NullVoxPopuli-ai-agent wants to merge 6 commits into
Conversation
|
|
|
@NullVoxPopuli is attempting to deploy a commit to the universal-ember Team on Vercel. A member of the Team first needs to authorize it. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR universal-ember#292 wired up vite-ember-ssr from a pre-hydration snapshot that boot-replaced the SSR markup on the client — no actual rehydration, just a flash of server HTML. Upstream vite-ember-ssr@0.1.0 has since shipped `_renderMode: serialize` on the server and a `bootRehydrated` client helper, so the client now attaches Glimmer to the existing DOM in place. Server changes: - Switch to the new `createEmberApp(ssrBundle, { dev: { ssrLoadModule } })` + `app.renderRoute(url, { shoebox })` API. Drops the hand-written per-request happy-dom plumbing. - Re-export `settled` from `@ember/test-helpers` in `app-ssr.ts` so the renderer awaits reactiveweb's `getPromiseState` test-waiters and the `.gjs.md` page loader resolves before we read the DOM. Without this every route SSRs as the `<Page>` loading-state. - Enable `isolateWorkers` for the prod tinypool. Components in this app schedule autoruns that can fire after `instance.destroy()` and corrupt the worker's shared HappyDOM Window, taking out subsequent renders. Fresh-window-per-request avoids the bleed. uncaughtException is also logged-not-fatal as a belt-and-braces guard. - Drop `vite.config.ssr.mjs` and fold SSR into the single `vite.config.js` via `emberSsr()` (handles ssr.noExternal, outDirs, CJS shim, CSS manifest). Client changes: - New `src/entry.ts` calls `bootRehydrated(Application, config)`. Removes the inline boot script in `index.html` that was manually tearing down SSR content before booting. Kolay-internal fixes that block multi-request SSR: - `kolay/setup` virtual module: read `window[SECRET]` fresh on every `setupKolay()` call instead of caching at module load. Module-level capture pinned to the first request's HappyDOM Window; subsequent requests added owners to a destroyed window and `getKey()` (which reads window fresh) crashed with "cannot read 'owners' of undefined". - `registry.ts`: drop `.md` from the eager `import.meta.glob`. The `.gjs.md` pages are loaded by Selected via the `kolay/compiled-docs` pages map; eager-importing them forces every compiled-page chunk to evaluate at app boot, and in the prod SSR bundle that creates a circular import where each chunk's top-level `templateOnly()` runs before app-ssr.js initializes the binding. - `kolaySsrStubs` Vite plugin: no-op `repl-sdk`, `ember-repl`, and the babel-plugin-ember-template-compilation chain during SSR transforms. Those modules touch Web Workers / CJS named exports the SSR pipeline can't bundle, and runtime markdown compilation isn't needed on the server — `.gjs.md` is pre-compiled by `kolay/vite`. Verified: dev:ssr and start:ssr both render every route 200 with full nav, layout, and markdown content. The HTML carries Glimmer rehydration markers (`<!--%+b:N%-->`) and `window.__vite_ember_ssr_rehydrate__=true`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6a8477f to
30ed831
Compare
30ed831 to
6f79d6e
Compare
6f79d6e to
54cb04d
Compare
54cb04d to
273ce3e
Compare
273ce3e to
fdf7fee
Compare
fdf7fee to
d665bff
Compare
d665bff to
0e26fb4
Compare
0e26fb4 to
65d76e2
Compare
Wires up server-side rendering for the docs-app using vite-ember-ssr's
real Glimmer rehydration path (`_renderMode: 'serialize'` on the server +
`bootRehydrated` on the client). The client attaches Glimmer to the
existing DOM in place — no flash, no boot-replace.
Plumbing:
- `server.mjs` — Fastify server with dev (Vite middleware + HMR via
`createEmberApp(entry, { dev: { ssrLoadModule } })`) and prod
(`createEmberApp(bundle, { isolateWorkers: true })` + tinypool)
modes. Adds `dev:ssr`, `start:ssr`, `build:ssr`, `build:all` scripts.
- `src/app-ssr.ts` — SSR entry exporting `createSsrApp()` and re-exporting
`settled` from `@ember/test-helpers` so the renderer awaits the run
loop, pending timers, and `@ember/test-waiters` (reactiveweb's
`getPromiseState`, etc.) before capturing the DOM.
- `src/entry.ts` — client entry calling `bootRehydrated(Application,
config)`. Falls back to `Application.create(config.APP)` for non-SSR
pages so `pnpm start:vite` still works.
- `vite.config.js` — folds in the previous separate `vite.config.ssr.mjs`
and adds the `emberSsr()` plugin (handles `ssr.noExternal`, dist/client
vs dist/server outDirs, CJS shim, CSS manifest).
SSR compatibility fixes:
- `kolay/setup` virtual module: read `window[SECRET]` fresh on every
`setupKolay()` call instead of capturing at module load. Module-level
capture pinned to the first request's HappyDOM Window; subsequent
requests added owners to a destroyed window and `getKey()` (which
reads window fresh) crashed with "cannot read 'owners' of undefined".
- `docs-app/src/registry.ts`: drop `.md` from the eager `import.meta.glob`.
Pre-compiled `.gjs.md` pages are loaded by Selected via the
`kolay/compiled-docs:virtual` pages map; eager-importing them statically
forces every compiled-page chunk to evaluate at app boot, and in the
prod SSR bundle that creates a circular import where each chunk's
top-level `templateOnly()` runs before app-ssr.js initializes the
binding ("templateOnly is not a function").
- `kolaySsrStubs` Vite plugin (in `vite.config.js`): no-op `repl-sdk`,
`ember-repl`, and the babel-plugin-ember-template-compilation chain
during SSR transforms. Those modules touch Web Workers / CJS named
exports the SSR pipeline can't bundle, and runtime markdown compilation
isn't needed on the server — `.gjs.md` is pre-compiled by `kolay/vite`.
Verified: `pnpm dev:ssr` and `pnpm start:ssr` both render every route
200 with full nav, layout, and markdown content. The HTML carries
Glimmer rehydration markers (`<!--%+b:N%-->`) and
`window.__vite_ember_ssr_rehydrate__=true` so `bootRehydrated` attaches.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
65d76e2 to
9232ccb
Compare
9232ccb to
f848dbe
Compare
f848dbe to
f248eb5
Compare
f248eb5 to
f988ce0
Compare
57529d4 to
b3d6110
Compare
ac12fc3 to
c7b24b1
Compare
Previously the gjs.md plugin emitted a `kolay/virtual:live:N.gjs`
virtual module per demo and an `import DemoN from <virtualId>` line
in the parent `.gjs.md`. Vite then resolved and content-tag-processed
each virtual module separately, producing one chunk per page with N+1
modules in its graph.
Replaced that with the limber `repl-sdk/render-to-string` helpers:
`splitModule` lifts each demo's top-level imports out of its body and
rewrites `export default <expr>` to `return <expr>`; `wrapAsConst`
puts the body in a uniquely-named IIFE; `replacePlaceholder` rewrites
the `<div id="…" class="…">` placeholders to invocations of that const
wrapped in a div that keeps the original class. The output is a single
`.gjs` module that content-tag processes in one pass.
Two patches let kolay test this before limber publishes:
- `patches/repl-sdk.patch`: brings the `renderToString` API +
`buildGmdModule` / `splitModule` / etc. (and exports them via
`repl-sdk/render-to-string`).
- `patches/ember-repl.patch`: adds `CompilerService.compileToSource`
next to the existing content-tag/standalone fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three independent bugs together caused the SSG'd content to be
unmounted and re-rendered on rehydration:
1. `prefetchPage`'s dynamic import was annotated with `@vite-ignore`,
so Vite left the `kolay/compiled-docs:virtual` specifier intact
and the browser threw "Failed to resolve module specifier" at
runtime. moduleCache stayed empty, every `Selected.loader` access
went through `getPromiseState`'s pending state, and `<Page>`
rendered its `<:pending>` block on top of the SSG'd success
content. Drop the `@vite-ignore` — the specifier IS resolvable at
build time because kolay's plugin registers it.
2. `Selected.#path` and `prefetchPage` weren't normalizing trailing
slashes. An SSG'd `/usage/setup/index.html` served as
`/usage/setup/` produced `findByPath('/usage/setup/')` which
missed the manifest's `/usage/setup` entry, so Selected returned
a "page not found" error and stomped the prerendered DOM.
Factor out `normalizePagePath` and use it from both sites.
3. `<APIDocs>` / `<Load>` always started in `request.isLoading = true`
on rehydration because the typedoc JSON fetch had to round-trip
even when the same content was already in the DOM. Add a project-
level cache (deserialized `ProjectReflection` keyed by package
name) and a synchronous fast path in `Load#request` that reads
from it. Expose `prewarmTypedocCache(pkg, loader)` and
`prewarmTypedocCaches()` so the client entry can pre-fetch +
deserialize every configured `apiDocs({ packages })` entry before
`Application.create`, mirroring how `prefetchPage` warms the page
module cache.
Verified via a CPU-throttled puppeteer trace sampling every 25ms
across every documented route: each page now reports exactly one
unique `proseLen` value over the full hydration window (was
4424 → 3381 → 4424 on `/plugins/kolay/` before this commit).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed What changed:
On the limber side, the same string-emission path is shared between the runtime Verified locally:
|
Pulls in limber#2165's latest commit: gmd's runtime path now
registers its live scope via `api.provide('repl-sdk:gmd-scope:N', scope)`
instead of writing to `globalThis` directly. The emitted module
threads scope through a regular `import * as __scope__ from '<specifier>'`
that resolves via repl-sdk's existing `manual:` resolver.
Verified the docs-app build still SSGs all 23 pages and rehydration
holds prose mounted with no FOUC across every documented route.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review feedback (universal-ember#292): - Bring back the three inline-snapshot assertions that I'd replaced with `toContain` checks during the refactor away from `kolay/virtual:live:N.gjs` virtual modules. - Restore the "handles a big dog with top-level component invocation" case I'd dropped. The snapshots now reflect the new output shape — content-tag's deduped `template as template_<hash>` import, the inlined `const Demo<N>_<id> = (() => { return template_<hash>(\`...\`, …); })()` IIFE, and the rewritten placeholder div wrapping a `<Demo<N>_<id> />` invocation — and serve as a guard against accidental changes to that shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls limber#2165's latest commit: `api.provide(specifier, value) → unregister`
is gone, replaced by a per-compile `CompileAPI` whose `provideScope(value)
→ { specifier }` is auto-released by the Compiler when the compile's
destroy fires. gmd no longer threads an unregister through extras.
Verified docs-app rehydration still has no FOUC across every documented
route (0 `<:pending>` flashes; SSG content stays mounted).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| } | ||
| ); | ||
|
|
||
| expect(virtualModulesByMarkdownFile).toMatchInlineSnapshot(` |
There was a problem hiding this comment.
bring back the inline snapshot
| } | ||
| ); | ||
|
|
||
| expect(virtualModulesByMarkdownFile).toMatchInlineSnapshot(` |
There was a problem hiding this comment.
bring back the inline snapshot
| "test.gjs.md" => Map {}, | ||
| } | ||
| `); | ||
| expect(result.code).toMatchInlineSnapshot(` |
There was a problem hiding this comment.
bring back inline snapshot
| expect(result.code).toContain('<APIDocs @module="declarations/browser"'); | ||
| }); | ||
|
|
||
| test('handles a big dog with top-level component invocation', async () => { |
There was a problem hiding this comment.
why'd this test get removed?
|
Pushed b28af17 — inline snapshots restored on the existing two tests and the big-dog |
Summary
server.mjs) with both development (Vite middleware + HMR) and production modesember-replruntime compilationNew files
docs-app/server.mjs— Fastify SSR server with dev/prod modesdocs-app/src/app-ssr.ts— SSR entry point exportingcreateSsrApp()factorydocs-app/vite.config.ssr.mjs— Separate Vite config for SSR build (targets Node 22)New scripts
dev:ssr— Start dev server with SSR + HMRstart:ssr— Start production SSR serverbuild:ssr— Build SSR bundle todist/server/build:all— Build both client and SSR bundlesSSR compatibility workarounds
window/documentat load time (e.g.,ember-primitives/color-scheme)window[SECRET]ownership tracking across per-request Happy DOM window isolationDependencies
vite-ember-ssr(not yet published to npm — currently referencing git repo)fastify,@fastify/middie,@fastify/static,@fastify/compresshappy-domNotes
vite-ember-ssris not yet published to npm. Thepackage.jsondependency reference will need updating once it's published.ember-repl(runtime markdown compiler) is stubbed. The full page content hydrates on the client. Pre-compiled.gjs.mdpages may render fully with further work.Test plan
pnpm dev:ssrindocs-app/and verify the page loads with SSR content visible before JS hydrationpnpm build:allandpnpm start:ssrto test production modepnpm start:vite(non-SSR) still works unchanged🤖 Generated with Claude Code