Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs-app/app/app-ssr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import PageTitleService from 'ember-page-title/services/page-title';
import Application from 'ember-strict-application-resolver';

import Router from './router.ts';

export default class SsrApp extends Application {
modules = {
...import.meta.glob('./router.ts', { eager: true }),
...import.meta.glob('./templates/**/*.{gjs,gts,md}', { eager: true }),
...import.meta.glob('./routes/**/*.{gjs,gts,js,ts,md}', { eager: true }),
'./router': Router,
'./services/page-title': PageTitleService,
};
}

/**
* Exported so vite-ember-ssr's worker can await it with a timeout per
* render (`settledTimeout`, default 10s). Demos with `ReactiveImage`
* register `waitForPromise` waiters that never resolve under Node
* (happy-dom doesn't fire `<img>` onload), so an unbounded `settled()`
* inside `app.visit` would hang the whole render forever.
*/
export { settled } from '@ember/test-helpers';

export function createSsrApp() {
return SsrApp.create({ autoboot: false });
}
14 changes: 14 additions & 0 deletions docs-app/app/boot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { shouldRehydrate } from 'vite-ember-ssr/client';

import Application from './app.ts';
import environment from './config/environment.ts';

if (shouldRehydrate()) {
const app = Application.create({ ...environment.APP, autoboot: false });

void app.visit(window.location.pathname + window.location.search, {
_renderMode: 'rehydrate',
});
} else {
Application.create(environment.APP);
}
2 changes: 1 addition & 1 deletion docs-app/app/routes/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default class Application extends Route {
});

await Promise.all([
setupTabster(this),
import.meta.env?.SSR ? Promise.resolve() : setupTabster(this),
setupKolay(this, {
// This won't work, because the compiler can't find the element to rendedr in to.
// remarkPlugins: [
Expand Down
9 changes: 3 additions & 6 deletions docs-app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,10 @@
as="font"
rel="font/woff2"
/>
<!-- VITE_EMBER_SSR_HEAD -->
</head>
<body>
<script type="module">
import Application from "./app/app";
import environment from "./app/config/environment";

Application.create(environment.APP);
</script>
<!-- VITE_EMBER_SSR_BODY -->
<script type="module" src="./app/boot.ts"></script>
</body>
</html>
3 changes: 2 additions & 1 deletion docs-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@
"testem": "^3.17.0",
"typedoc": "^0.28.15",
"typescript": "^6.0.2",
"vite": "^8.0.3"
"vite": "^8.0.3",
"vite-ember-ssr": "^0.1.0"
},
"engines": {
"node": ">= 20"
Expand Down
127 changes: 126 additions & 1 deletion docs-app/vite.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,130 @@ import { ember, extensions } from "@embroider/vite";

import { babel } from "@rollup/plugin-babel";
import { kolay } from "kolay/vite";
import { globby } from "globby";
import { defineConfig } from "vite";
import { scopedCSS } from "ember-scoped-css/vite";
import { emberSsg } from "vite-ember-ssr/vite-plugin";
import rehypeShiki from "@shikijs/rehype";

export default defineConfig(async (/* { mode } */) => {
/**
* Replace runtime-only deps with empty stubs during SSR. Pre-rendering
* never exercises the REPL's runtime template compilation, but bundling
* content-tag/babel-standalone into the SSR build trips up rolldown
* (and isn't useful even if it built).
*/
function stubRuntimeOnlyDepsForSsr() {
const stubs = new Map([
["content-tag", "export class Preprocessor {}; export default { Preprocessor };"],
["@babel/standalone", "export default {};"],
/**
* Imported via named export by demo snippets in markdown samples.
* lorem-ipsum is CJS and rolldown can't extract named exports from
* CJS during SSR builds, so substitute an ESM stub. The real package
* loads client-side after rehydration.
*/
[
"lorem-ipsum",
'export const loremIpsum = () => ""; export class LoremIpsum { generateSentences() { return ""; } generateParagraphs() { return ""; } generateWords() { return ""; } } export default { loremIpsum, LoremIpsum };',
],
]);

return {
name: "docs-app:stub-runtime-only-deps-for-ssr",
enforce: "pre",
resolveId(id, _importer, options) {
if (!options?.ssr) return;
if (stubs.has(id)) return `\0virtual:ssr-stub:${id}`;
},
load(id) {
const prefix = "\0virtual:ssr-stub:";
if (!id.startsWith(prefix)) return;
return stubs.get(id.slice(prefix.length));
},
};
}

export default defineConfig(async ({ isSsrBuild } = {}) => {
const docsRoutes = (await globby("app/templates/**/*.gjs.md", { cwd: import.meta.dirname }))
.map((p) => p.replace(/^app\/templates\//, "").replace(/\.gjs\.md$/, ".md"))
.sort();

return {
/**
* Force a single SSR bundle. Without this, rolldown splits Ember
* into chunks that circular-import `@ember/object` internals — the
* `ComputedProperty` class isn't initialized by the time
* `proxy.js`'s top-level `computed()` calls run.
*
* The banner installs no-op shims for globals that downstream
* modules read while loading (repl-sdk constructs a `new Worker(...)`
* at module-eval; @embroider/macros expects `process.env`). Rolldown
* inlines our app's module bodies after vendor inits, so a polyfill
* import isn't early enough — the banner runs before the first
* statement of the bundle.
*/
build: isSsrBuild
? {
rollupOptions: {
output: {
codeSplitting: false,
banner: [
"globalThis.process ??= { env: {} };",
"globalThis.Buffer ??= {};",
"globalThis.Worker ??= class { postMessage(){} terminate(){} addEventListener(){} removeEventListener(){} };",
/**
* repl-sdk captures browser-only globals into a
* `standardScope` map at module init. Provide minimal
* shims so the capture step doesn't throw under Node.
*/
"globalThis.postMessage ??= () => {};",
"globalThis.localStorage ??= { getItem(){return null}, setItem(){}, removeItem(){}, clear(){}, key(){return null}, length: 0 };",
"globalThis.sessionStorage ??= globalThis.localStorage;",
"globalThis.isSecureContext ??= false;",
/**
* happy-dom installs `window` but not its DOM
* interfaces or layout APIs on globalThis. A few demos
* touch them at module init.
*/
"if (globalThis.window) {",
" globalThis.Text ??= globalThis.window.Text;",
" globalThis.getComputedStyle ??= globalThis.window.getComputedStyle?.bind(globalThis.window);",
/**
* happy-dom lacks the Popover API. Stub it so demos
* that call showPopover() on render don't throw.
*/
" if (globalThis.window.HTMLElement) {",
" globalThis.window.HTMLElement.prototype.showPopover ??= function() {};",
" globalThis.window.HTMLElement.prototype.hidePopover ??= function() {};",
" globalThis.window.HTMLElement.prototype.togglePopover ??= function() {};",
" }",
/**
* happy-dom never fires onload/onerror on `<img>`
* elements (no resource fetching). Make src setter
* synthesise an onload microtask so `ReactiveImage`'s
* waitForPromise waiter resolves and settled() can
* complete without hitting the per-route timeout.
*/
" const HTMLImageElement = globalThis.window.HTMLImageElement;",
" if (HTMLImageElement) {",
" const desc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src');",
" Object.defineProperty(HTMLImageElement.prototype, 'src', {",
" configurable: true,",
" get() { return desc?.get ? desc.get.call(this) : this.getAttribute('src'); },",
" set(v) {",
" if (desc?.set) desc.set.call(this, v); else this.setAttribute('src', v);",
" queueMicrotask(() => { try { this.onload?.(new Event('load')); } catch {} });",
" },",
" });",
" }",
"}",
].join("\n"),
},
},
}
: {},
plugins: [
stubRuntimeOnlyDepsForSsr(),
scopedCSS(),
ember(),
kolay({
Expand Down Expand Up @@ -42,7 +159,15 @@ export default defineConfig(async (/* { mode } */) => {
babelHelpers: "runtime",
extensions,
}),
emberSsg({
routes: ["index", ...docsRoutes],
ssrEntry: "app/app-ssr.ts",
rehydrate: true,
}),
],
ssr: {
noExternal: [/./],
},
optimizeDeps: {
exclude: [
// a wasm-providing dependency
Expand Down
57 changes: 57 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading