@st-h/vite-ember-ssr 0.2.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +7 -0
- package/README.md +733 -0
- package/dist/client.d.ts +96 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +158 -0
- package/dist/client.js.map +1 -0
- package/dist/server.d.ts +236 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +349 -0
- package/dist/server.js.map +1 -0
- package/dist/vite-plugin-CQou_tr5.d.ts +145 -0
- package/dist/vite-plugin-CQou_tr5.d.ts.map +1 -0
- package/dist/vite-plugin-D-W5WQWe.js +398 -0
- package/dist/vite-plugin-D-W5WQWe.js.map +1 -0
- package/dist/vite-plugin.d.ts +2 -0
- package/dist/vite-plugin.js +2 -0
- package/dist/worker.d.ts +22 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +186 -0
- package/dist/worker.js.map +1 -0
- package/package.json +73 -0
- package/src/client.ts +242 -0
- package/src/dev.ts +318 -0
- package/src/server.ts +399 -0
- package/src/vite-plugin.ts +775 -0
- package/src/worker.ts +334 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { t as CSS_MANIFEST_FILENAME } from "./vite-plugin-D-W5WQWe.js";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
import { cpus } from "node:os";
|
|
4
|
+
import { Window } from "happy-dom";
|
|
5
|
+
//#region src/dev.ts
|
|
6
|
+
/**
|
|
7
|
+
* Dev-mode SSR renderer for vite-ember-ssr.
|
|
8
|
+
*
|
|
9
|
+
* Renders in-process using Vite's `ssrLoadModule` pipeline instead of a
|
|
10
|
+
* tinypool worker pool. The SSR entry module is re-loaded on every render
|
|
11
|
+
* so HMR changes are picked up immediately.
|
|
12
|
+
*
|
|
13
|
+
* A fresh HappyDOM Window is created and torn down for each render — there
|
|
14
|
+
* is no long-lived state between requests.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* ```js
|
|
18
|
+
* import { createServer } from 'vite';
|
|
19
|
+
* import { createEmberApp, assembleHTML } from '@st-h/vite-ember-ssr/server';
|
|
20
|
+
*
|
|
21
|
+
* const vite = await createServer({ server: { middlewareMode: true }, appType: 'custom' });
|
|
22
|
+
* const app = await createEmberApp('app/app-ssr.ts', {
|
|
23
|
+
* dev: { ssrLoadModule: vite.ssrLoadModule.bind(vite) },
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // In your catch-all handler:
|
|
27
|
+
* const rendered = await app.renderRoute(req.url);
|
|
28
|
+
* const html = assembleHTML(template, rendered);
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
const BROWSER_GLOBALS = [
|
|
32
|
+
"window",
|
|
33
|
+
"document",
|
|
34
|
+
"navigator",
|
|
35
|
+
"location",
|
|
36
|
+
"history",
|
|
37
|
+
"HTMLElement",
|
|
38
|
+
"Element",
|
|
39
|
+
"Node",
|
|
40
|
+
"Event",
|
|
41
|
+
"CustomEvent",
|
|
42
|
+
"MutationObserver",
|
|
43
|
+
"requestAnimationFrame",
|
|
44
|
+
"cancelAnimationFrame",
|
|
45
|
+
"self",
|
|
46
|
+
"localStorage",
|
|
47
|
+
"sessionStorage",
|
|
48
|
+
"InputEvent",
|
|
49
|
+
"KeyboardEvent",
|
|
50
|
+
"MouseEvent",
|
|
51
|
+
"FocusEvent",
|
|
52
|
+
"PointerEvent",
|
|
53
|
+
"IntersectionObserver",
|
|
54
|
+
"ResizeObserver",
|
|
55
|
+
"CSSStyleSheet"
|
|
56
|
+
];
|
|
57
|
+
const SHOEBOX_SCRIPT_ID = "vite-ember-ssr-shoebox";
|
|
58
|
+
const SSR_BODY_START = "<script type=\"x/boundary\" id=\"ssr-body-start\"><\/script>";
|
|
59
|
+
const SSR_BODY_END = "<script type=\"x/boundary\" id=\"ssr-body-end\"><\/script>";
|
|
60
|
+
function installGlobals(win) {
|
|
61
|
+
const saved = {};
|
|
62
|
+
for (const name of BROWSER_GLOBALS) {
|
|
63
|
+
saved[name] = globalThis[name];
|
|
64
|
+
try {
|
|
65
|
+
Object.defineProperty(globalThis, name, {
|
|
66
|
+
value: win[name],
|
|
67
|
+
writable: true,
|
|
68
|
+
configurable: true,
|
|
69
|
+
enumerable: true
|
|
70
|
+
});
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
return saved;
|
|
74
|
+
}
|
|
75
|
+
function restoreGlobals(saved) {
|
|
76
|
+
for (const name of BROWSER_GLOBALS) try {
|
|
77
|
+
Object.defineProperty(globalThis, name, {
|
|
78
|
+
value: saved[name],
|
|
79
|
+
writable: true,
|
|
80
|
+
configurable: true,
|
|
81
|
+
enumerable: true
|
|
82
|
+
});
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
function serializeShoebox(entries) {
|
|
86
|
+
if (entries.length === 0) return "";
|
|
87
|
+
return `<script type="application/json" id="${SHOEBOX_SCRIPT_ID}">${JSON.stringify(entries).replace(/<\/(script)/gi, "<\\/$1")}<\/script>`;
|
|
88
|
+
}
|
|
89
|
+
function buildRouteCssLinks(manifest, instance) {
|
|
90
|
+
if (!instance.lookup) return "";
|
|
91
|
+
let routeName;
|
|
92
|
+
try {
|
|
93
|
+
routeName = instance.lookup("service:router")?.currentRouteName ?? void 0;
|
|
94
|
+
} catch {
|
|
95
|
+
return "";
|
|
96
|
+
}
|
|
97
|
+
if (!routeName) return "";
|
|
98
|
+
const segments = routeName.split(".");
|
|
99
|
+
const seen = /* @__PURE__ */ new Set();
|
|
100
|
+
const links = [];
|
|
101
|
+
for (let i = 1; i <= segments.length; i++) {
|
|
102
|
+
const cssFiles = manifest[segments.slice(0, i).join(".")];
|
|
103
|
+
if (!cssFiles) continue;
|
|
104
|
+
for (const href of cssFiles) {
|
|
105
|
+
if (seen.has(href)) continue;
|
|
106
|
+
seen.add(href);
|
|
107
|
+
links.push(`<link rel="stylesheet" href="${href}">`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return links.join("");
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Creates a dev-mode EmberApp that renders in-process via Vite's
|
|
114
|
+
* `ssrLoadModule`. Implements the same `EmberApp` interface as the
|
|
115
|
+
* production `createEmberApp` so it can be used as a drop-in.
|
|
116
|
+
*/
|
|
117
|
+
function createDevEmberApp(entryPath, devOptions) {
|
|
118
|
+
const { ssrLoadModule } = devOptions;
|
|
119
|
+
return {
|
|
120
|
+
async renderRoute(url, renderOptions = {}) {
|
|
121
|
+
const { shoebox = false, rehydrate = false, cssManifest, headers: forwardHeaders } = renderOptions;
|
|
122
|
+
const win = new Window({
|
|
123
|
+
url: "http://localhost/",
|
|
124
|
+
width: 1024,
|
|
125
|
+
height: 768,
|
|
126
|
+
settings: {
|
|
127
|
+
disableJavaScriptFileLoading: true,
|
|
128
|
+
disableJavaScriptEvaluation: true,
|
|
129
|
+
disableCSSFileLoading: true,
|
|
130
|
+
navigator: { userAgent: "vite-ember-ssr" }
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
const savedGlobals = installGlobals(win);
|
|
134
|
+
const realFetch = globalThis.fetch;
|
|
135
|
+
const shoeboxEntries = shoebox ? /* @__PURE__ */ new Map() : null;
|
|
136
|
+
if (shoebox || forwardHeaders) globalThis.fetch = async (input, init) => {
|
|
137
|
+
let effectiveInit = init;
|
|
138
|
+
if (forwardHeaders) {
|
|
139
|
+
effectiveInit = { ...init };
|
|
140
|
+
const existingHeaders = new Headers(effectiveInit.headers);
|
|
141
|
+
for (const [key, value] of Object.entries(forwardHeaders)) if (!existingHeaders.has(key)) existingHeaders.set(key, value);
|
|
142
|
+
effectiveInit.headers = existingHeaders;
|
|
143
|
+
}
|
|
144
|
+
const request = new Request(input, effectiveInit);
|
|
145
|
+
if (request.method.toUpperCase() !== "GET") return realFetch(request);
|
|
146
|
+
const response = await realFetch(request);
|
|
147
|
+
if (shoeboxEntries) try {
|
|
148
|
+
const clone = response.clone();
|
|
149
|
+
const body = await clone.text();
|
|
150
|
+
const headers = {};
|
|
151
|
+
clone.headers.forEach((v, k) => {
|
|
152
|
+
headers[k] = v;
|
|
153
|
+
});
|
|
154
|
+
shoeboxEntries.set(request.url, {
|
|
155
|
+
url: request.url,
|
|
156
|
+
status: clone.status,
|
|
157
|
+
statusText: clone.statusText,
|
|
158
|
+
headers,
|
|
159
|
+
body
|
|
160
|
+
});
|
|
161
|
+
} catch {}
|
|
162
|
+
return response;
|
|
163
|
+
};
|
|
164
|
+
let head = "";
|
|
165
|
+
let body = "";
|
|
166
|
+
let bodyAttrs = {};
|
|
167
|
+
let cssLinks = "";
|
|
168
|
+
let error;
|
|
169
|
+
try {
|
|
170
|
+
const document = win.document;
|
|
171
|
+
const mod = await ssrLoadModule(entryPath);
|
|
172
|
+
if (typeof mod.createSsrApp !== "function") throw new Error(`SSR entry '${entryPath}' does not export a 'createSsrApp' function. Found exports: ${Object.keys(mod).join(", ")}`);
|
|
173
|
+
const app = mod.createSsrApp();
|
|
174
|
+
const bootOptions = {
|
|
175
|
+
isBrowser: false,
|
|
176
|
+
document,
|
|
177
|
+
rootElement: document.body,
|
|
178
|
+
shouldRender: true,
|
|
179
|
+
...rehydrate ? { _renderMode: "serialize" } : {}
|
|
180
|
+
};
|
|
181
|
+
const instance = await app.visit(url, bootOptions);
|
|
182
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
183
|
+
if (cssManifest) cssLinks = buildRouteCssLinks(cssManifest, instance);
|
|
184
|
+
head = document.head?.innerHTML ?? "";
|
|
185
|
+
body = document.body?.innerHTML ?? "";
|
|
186
|
+
if (document.body) for (const attr of Array.from(document.body.attributes)) bodyAttrs[attr.name] = attr.value;
|
|
187
|
+
instance.destroy();
|
|
188
|
+
} catch (e) {
|
|
189
|
+
error = e instanceof Error ? e : new Error(String(e));
|
|
190
|
+
} finally {
|
|
191
|
+
if (shoebox || forwardHeaders) globalThis.fetch = realFetch;
|
|
192
|
+
restoreGlobals(savedGlobals);
|
|
193
|
+
await win.happyDOM?.close?.();
|
|
194
|
+
}
|
|
195
|
+
const shoeboxHTML = shoeboxEntries && shoeboxEntries.size > 0 ? serializeShoebox(Array.from(shoeboxEntries.values())) : "";
|
|
196
|
+
return {
|
|
197
|
+
head: cssLinks + (rehydrate ? "<script>window.__vite_ember_ssr_rehydrate__=true<\/script>" : "") + shoeboxHTML + head,
|
|
198
|
+
body: rehydrate ? body : `${SSR_BODY_START}${body}${SSR_BODY_END}`,
|
|
199
|
+
bodyAttrs,
|
|
200
|
+
statusCode: error ? 500 : 200,
|
|
201
|
+
...error ? { error } : {}
|
|
202
|
+
};
|
|
203
|
+
},
|
|
204
|
+
async destroy() {}
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/server.ts
|
|
209
|
+
const WORKER_PATH = fileURLToPath(new URL("./worker.js", import.meta.url));
|
|
210
|
+
/**
|
|
211
|
+
* Creates a long-lived worker thread pool for SSR/SSG rendering.
|
|
212
|
+
*
|
|
213
|
+
* Each worker imports the SSR bundle once at startup and reuses it for all
|
|
214
|
+
* subsequent renders — no bundle re-import, no Worker respawn.
|
|
215
|
+
*
|
|
216
|
+
* Pass `dev: { ssrLoadModule }` to run in dev mode instead: renders happen
|
|
217
|
+
* in-process via Vite's module resolution pipeline with no tinypool workers.
|
|
218
|
+
* The SSR entry is re-loaded on every render so HMR changes are reflected
|
|
219
|
+
* immediately.
|
|
220
|
+
*
|
|
221
|
+
* @example Production
|
|
222
|
+
* ```js
|
|
223
|
+
* import { createEmberApp, assembleHTML } from '@st-h/vite-ember-ssr/server';
|
|
224
|
+
* import { resolve } from 'node:path';
|
|
225
|
+
*
|
|
226
|
+
* const app = await createEmberApp(resolve('dist/server/app-ssr.mjs'));
|
|
227
|
+
*
|
|
228
|
+
* // In a request handler:
|
|
229
|
+
* const result = await app.renderRoute(req.url);
|
|
230
|
+
* const html = assembleHTML(template, result);
|
|
231
|
+
*
|
|
232
|
+
* // On server shutdown:
|
|
233
|
+
* await app.destroy();
|
|
234
|
+
* ```
|
|
235
|
+
*
|
|
236
|
+
* @example Development
|
|
237
|
+
* ```js
|
|
238
|
+
* import { createServer } from 'vite';
|
|
239
|
+
* import { createEmberApp, assembleHTML } from '@st-h/vite-ember-ssr/server';
|
|
240
|
+
*
|
|
241
|
+
* const vite = await createServer({ server: { middlewareMode: true }, appType: 'custom' });
|
|
242
|
+
* const app = await createEmberApp('app/app-ssr.ts', {
|
|
243
|
+
* dev: { ssrLoadModule: vite.ssrLoadModule.bind(vite) },
|
|
244
|
+
* });
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
async function createEmberApp(ssrBundlePath, options = {}) {
|
|
248
|
+
if (options.dev) return createDevEmberApp(ssrBundlePath, options.dev);
|
|
249
|
+
const bundleURL = ssrBundlePath.startsWith("file://") ? ssrBundlePath : pathToFileURL(ssrBundlePath).href;
|
|
250
|
+
const workerCount = options.workers ?? cpus().length;
|
|
251
|
+
const { default: Tinypool } = await import("tinypool");
|
|
252
|
+
const pool = new Tinypool({
|
|
253
|
+
filename: WORKER_PATH,
|
|
254
|
+
minThreads: workerCount,
|
|
255
|
+
maxThreads: workerCount,
|
|
256
|
+
isolateWorkers: options.isolateWorkers ?? false,
|
|
257
|
+
workerData: { ssrBundlePath: bundleURL }
|
|
258
|
+
});
|
|
259
|
+
let recycleTimer;
|
|
260
|
+
const recycleInterval = options.recycleWorkerInterval ?? 0;
|
|
261
|
+
if (recycleInterval > 0) {
|
|
262
|
+
recycleTimer = setInterval(() => {
|
|
263
|
+
pool.recycleWorkers().catch(() => {});
|
|
264
|
+
}, recycleInterval);
|
|
265
|
+
recycleTimer.unref();
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
async renderRoute(url, renderOptions = {}) {
|
|
269
|
+
const result = await pool.run({
|
|
270
|
+
ssrBundlePath: bundleURL,
|
|
271
|
+
url,
|
|
272
|
+
shoebox: renderOptions.shoebox ?? false,
|
|
273
|
+
rehydrate: renderOptions.rehydrate ?? false,
|
|
274
|
+
cssManifest: renderOptions.cssManifest ?? null,
|
|
275
|
+
headers: renderOptions.headers ?? null
|
|
276
|
+
});
|
|
277
|
+
return {
|
|
278
|
+
head: result.head,
|
|
279
|
+
body: result.body,
|
|
280
|
+
bodyAttrs: result.bodyAttrs ?? {},
|
|
281
|
+
statusCode: result.statusCode,
|
|
282
|
+
error: result.error ? new Error(result.error) : void 0
|
|
283
|
+
};
|
|
284
|
+
},
|
|
285
|
+
async destroy() {
|
|
286
|
+
clearInterval(recycleTimer);
|
|
287
|
+
await pool.destroy();
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const SSR_HEAD_MARKER = "<!-- VITE_EMBER_SSR_HEAD -->";
|
|
292
|
+
const SSR_BODY_MARKER = "<!-- VITE_EMBER_SSR_BODY -->";
|
|
293
|
+
const SSR_MARKER_REGEX = /<!-- VITE_EMBER_SSR_(HEAD|BODY) -->/g;
|
|
294
|
+
/**
|
|
295
|
+
* Assembles the final HTML response by inserting rendered content
|
|
296
|
+
* into the index.html template.
|
|
297
|
+
*
|
|
298
|
+
* When `rendered.bodyAttrs` is provided, attributes set on the `<body>`
|
|
299
|
+
* element during SSR (e.g., `data-theme`, `class`) are applied to the
|
|
300
|
+
* `<body>` tag in the template HTML.
|
|
301
|
+
*/
|
|
302
|
+
function assembleHTML(template, rendered) {
|
|
303
|
+
let headReplaced = false;
|
|
304
|
+
let bodyReplaced = false;
|
|
305
|
+
let html = template.replace(SSR_MARKER_REGEX, (_match, tag) => {
|
|
306
|
+
if (tag === "HEAD" && !headReplaced) {
|
|
307
|
+
headReplaced = true;
|
|
308
|
+
return rendered.head;
|
|
309
|
+
}
|
|
310
|
+
if (tag === "BODY" && !bodyReplaced) {
|
|
311
|
+
bodyReplaced = true;
|
|
312
|
+
return rendered.body;
|
|
313
|
+
}
|
|
314
|
+
return "";
|
|
315
|
+
});
|
|
316
|
+
const attrs = rendered.bodyAttrs;
|
|
317
|
+
if (attrs && Object.keys(attrs).length > 0) {
|
|
318
|
+
const attrString = Object.entries(attrs).map(([key, value]) => `${key}="${value.replace(/"/g, """)}"`).join(" ");
|
|
319
|
+
html = html.replace(/<body([^>]*)>/, `<body$1 ${attrString}>`);
|
|
320
|
+
}
|
|
321
|
+
return html;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Checks whether an HTML template contains the required SSR markers.
|
|
325
|
+
*/
|
|
326
|
+
function hasSSRMarkers(html) {
|
|
327
|
+
return {
|
|
328
|
+
head: html.includes(SSR_HEAD_MARKER),
|
|
329
|
+
body: html.includes(SSR_BODY_MARKER)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Loads the CSS manifest from the client build output directory.
|
|
334
|
+
*/
|
|
335
|
+
async function loadCssManifest(clientDir) {
|
|
336
|
+
const { readFile } = await import("node:fs/promises");
|
|
337
|
+
const { join } = await import("node:path");
|
|
338
|
+
const { CSS_MANIFEST_FILENAME: filename } = await import("./vite-plugin.js");
|
|
339
|
+
try {
|
|
340
|
+
const raw = await readFile(join(clientDir, filename), "utf-8");
|
|
341
|
+
return JSON.parse(raw);
|
|
342
|
+
} catch {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
export { CSS_MANIFEST_FILENAME, assembleHTML, createEmberApp, hasSSRMarkers, loadCssManifest };
|
|
348
|
+
|
|
349
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","names":[],"sources":["../src/dev.ts","../src/server.ts"],"sourcesContent":["/**\n * Dev-mode SSR renderer for vite-ember-ssr.\n *\n * Renders in-process using Vite's `ssrLoadModule` pipeline instead of a\n * tinypool worker pool. The SSR entry module is re-loaded on every render\n * so HMR changes are picked up immediately.\n *\n * A fresh HappyDOM Window is created and torn down for each render — there\n * is no long-lived state between requests.\n *\n * Usage:\n * ```js\n * import { createServer } from 'vite';\n * import { createEmberApp, assembleHTML } from '@st-h/vite-ember-ssr/server';\n *\n * const vite = await createServer({ server: { middlewareMode: true }, appType: 'custom' });\n * const app = await createEmberApp('app/app-ssr.ts', {\n * dev: { ssrLoadModule: vite.ssrLoadModule.bind(vite) },\n * });\n *\n * // In your catch-all handler:\n * const rendered = await app.renderRoute(req.url);\n * const html = assembleHTML(template, rendered);\n * ```\n */\n\nimport { Window } from 'happy-dom';\nimport type {\n EmberApplication,\n EmberApplicationInstance,\n BootOptions,\n RenderRouteOptions,\n RenderResult,\n ShoeboxEntry,\n EmberApp,\n EmberAppDevOptions,\n} from './server.js';\n\n// ─── Constants ────────────────────────────────────────────────────────\n\nconst BROWSER_GLOBALS = [\n 'window',\n 'document',\n 'navigator',\n 'location',\n 'history',\n 'HTMLElement',\n 'Element',\n 'Node',\n 'Event',\n 'CustomEvent',\n 'MutationObserver',\n 'requestAnimationFrame',\n 'cancelAnimationFrame',\n 'self',\n 'localStorage',\n 'sessionStorage',\n 'InputEvent',\n 'KeyboardEvent',\n 'MouseEvent',\n 'FocusEvent',\n 'PointerEvent',\n 'IntersectionObserver',\n 'ResizeObserver',\n 'CSSStyleSheet',\n] as const;\n\nconst SHOEBOX_SCRIPT_ID = 'vite-ember-ssr-shoebox';\nconst SSR_BODY_START =\n '<script type=\"x/boundary\" id=\"ssr-body-start\"></script>';\nconst SSR_BODY_END = '<script type=\"x/boundary\" id=\"ssr-body-end\"></script>';\n\n// ─── Helpers ──────────────────────────────────────────────────────────\n\nfunction installGlobals(win: Window): Record<string, unknown> {\n const saved: Record<string, unknown> = {};\n for (const name of BROWSER_GLOBALS) {\n saved[name] = (globalThis as Record<string, unknown>)[name];\n try {\n Object.defineProperty(globalThis, name, {\n value: (win as unknown as Record<string, unknown>)[name],\n writable: true,\n configurable: true,\n enumerable: true,\n });\n } catch {\n /* skip non-overridable globals */\n }\n }\n return saved;\n}\n\nfunction restoreGlobals(saved: Record<string, unknown>): void {\n for (const name of BROWSER_GLOBALS) {\n try {\n Object.defineProperty(globalThis, name, {\n value: saved[name],\n writable: true,\n configurable: true,\n enumerable: true,\n });\n } catch {\n /* skip */\n }\n }\n}\n\nfunction serializeShoebox(entries: ShoeboxEntry[]): string {\n if (entries.length === 0) return '';\n const safeJson = JSON.stringify(entries).replace(/<\\/(script)/gi, '<\\\\/$1');\n return `<script type=\"application/json\" id=\"${SHOEBOX_SCRIPT_ID}\">${safeJson}</script>`;\n}\n\nfunction buildRouteCssLinks(\n manifest: NonNullable<RenderRouteOptions['cssManifest']>,\n instance: EmberApplicationInstance,\n): string {\n if (!instance.lookup) return '';\n let routeName: string | undefined;\n try {\n const router = instance.lookup('service:router') as\n | { currentRouteName?: string }\n | undefined;\n routeName = router?.currentRouteName ?? undefined;\n } catch {\n return '';\n }\n if (!routeName) return '';\n\n const segments = routeName.split('.');\n const seen = new Set<string>();\n const links: string[] = [];\n for (let i = 1; i <= segments.length; i++) {\n const cssFiles = manifest[segments.slice(0, i).join('.')];\n if (!cssFiles) continue;\n for (const href of cssFiles) {\n if (seen.has(href)) continue;\n seen.add(href);\n links.push(`<link rel=\"stylesheet\" href=\"${href}\">`);\n }\n }\n return links.join('');\n}\n\n// ─── Dev EmberApp factory ─────────────────────────────────────────────\n\n/**\n * Creates a dev-mode EmberApp that renders in-process via Vite's\n * `ssrLoadModule`. Implements the same `EmberApp` interface as the\n * production `createEmberApp` so it can be used as a drop-in.\n */\nexport function createDevEmberApp(\n entryPath: string,\n devOptions: EmberAppDevOptions,\n): EmberApp {\n const { ssrLoadModule } = devOptions;\n\n return {\n async renderRoute(\n url: string,\n renderOptions: RenderRouteOptions = {},\n ): Promise<RenderResult> {\n const {\n shoebox = false,\n rehydrate = false,\n cssManifest,\n headers: forwardHeaders,\n } = renderOptions;\n\n // Fresh Window per request — no state bleeds between renders in dev.\n const win = new Window({\n url: 'http://localhost/',\n width: 1024,\n height: 768,\n settings: {\n disableJavaScriptFileLoading: true,\n disableJavaScriptEvaluation: true,\n disableCSSFileLoading: true,\n navigator: { userAgent: 'vite-ember-ssr' },\n },\n });\n\n const savedGlobals = installGlobals(win);\n\n // Shoebox: intercept fetch for this render only.\n const realFetch = globalThis.fetch;\n const shoeboxEntries: Map<string, ShoeboxEntry> | null = shoebox\n ? new Map()\n : null;\n\n if (shoebox || forwardHeaders) {\n globalThis.fetch = async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ) => {\n // Inject forwarded request headers into outgoing fetches\n let effectiveInit = init;\n if (forwardHeaders) {\n effectiveInit = { ...init };\n const existingHeaders = new Headers(effectiveInit.headers);\n for (const [key, value] of Object.entries(forwardHeaders)) {\n if (!existingHeaders.has(key)) {\n existingHeaders.set(key, value);\n }\n }\n effectiveInit.headers = existingHeaders;\n }\n\n const request = new Request(input, effectiveInit);\n if (request.method.toUpperCase() !== 'GET')\n return realFetch(request);\n const response = await realFetch(request);\n if (shoeboxEntries) {\n try {\n const clone = response.clone();\n const body = await clone.text();\n const headers: Record<string, string> = {};\n clone.headers.forEach((v: string, k: string) => {\n headers[k] = v;\n });\n shoeboxEntries.set(request.url, {\n url: request.url,\n status: clone.status,\n statusText: clone.statusText,\n headers,\n body,\n });\n } catch {\n /* skip */\n }\n }\n return response;\n };\n }\n\n let head = '';\n let body = '';\n let bodyAttrs: Record<string, string> = {};\n let cssLinks = '';\n let error: Error | undefined;\n\n try {\n const document = win.document;\n\n // Re-load the module on every request so HMR changes are reflected.\n const mod = (await ssrLoadModule(entryPath)) as {\n createSsrApp?: () => EmberApplication;\n };\n if (typeof mod.createSsrApp !== 'function') {\n throw new Error(\n `SSR entry '${entryPath}' does not export a 'createSsrApp' function. ` +\n `Found exports: ${Object.keys(mod).join(', ')}`,\n );\n }\n const app = mod.createSsrApp();\n\n const bootOptions: BootOptions = {\n isBrowser: false,\n document: document as unknown as Document,\n rootElement: document.body as unknown as Element,\n shouldRender: true,\n ...(rehydrate ? { _renderMode: 'serialize' as const } : {}),\n };\n\n const instance = await app.visit(url, bootOptions);\n\n // Drain Backburner's autorun microtask before reading the DOM.\n await new Promise<void>((resolve) => setTimeout(resolve, 0));\n\n if (cssManifest) {\n cssLinks = buildRouteCssLinks(cssManifest, instance);\n }\n\n head = document.head?.innerHTML ?? '';\n body = document.body?.innerHTML ?? '';\n\n // Extract attributes set on <body> during rendering\n if (document.body) {\n for (const attr of Array.from(document.body.attributes)) {\n bodyAttrs[attr.name] = attr.value;\n }\n }\n\n instance.destroy();\n } catch (e) {\n error = e instanceof Error ? e : new Error(String(e));\n } finally {\n if (shoebox || forwardHeaders) globalThis.fetch = realFetch;\n restoreGlobals(savedGlobals);\n await win.happyDOM?.close?.();\n }\n\n const shoeboxHTML =\n shoeboxEntries && shoeboxEntries.size > 0\n ? serializeShoebox(Array.from(shoeboxEntries.values()))\n : '';\n const rehydrateHTML = rehydrate\n ? '<script>window.__vite_ember_ssr_rehydrate__=true</script>'\n : '';\n const fullHead = cssLinks + rehydrateHTML + shoeboxHTML + head;\n const wrappedBody = rehydrate\n ? body\n : `${SSR_BODY_START}${body}${SSR_BODY_END}`;\n\n return {\n head: fullHead,\n body: wrappedBody,\n bodyAttrs,\n statusCode: error ? 500 : 200,\n ...(error ? { error } : {}),\n };\n },\n\n async destroy(): Promise<void> {\n // Nothing to tear down — no worker pool in dev mode.\n },\n };\n}\n","import { pathToFileURL, fileURLToPath } from 'node:url';\nimport { cpus } from 'node:os';\nimport type { CssManifest } from './vite-plugin.js';\nimport { createDevEmberApp } from './dev.js';\n\n// ─── Worker script path ───────────────────────────────────────────────\n\n// Resolve the worker script relative to this compiled file.\n// In the dist/ output both server.js and worker.js sit side-by-side.\nconst WORKER_PATH = fileURLToPath(new URL('./worker.js', import.meta.url));\n\n// ─── Types ───────────────────────────────────────────────────────────\n\n/**\n * Minimal interface for an Ember Application that supports SSR.\n *\n * The app must be created with `autoboot: false` so the server can\n * control boot timing via `app.visit(url, options)`.\n */\nexport interface EmberApplication {\n visit(url: string, options?: BootOptions): Promise<EmberApplicationInstance>;\n destroy(): void;\n}\n\nexport interface EmberApplicationInstance {\n destroy(): void;\n getURL?(): string;\n _booted?: boolean;\n lookup?(fullName: string): unknown;\n}\n\nexport interface BootOptions {\n isBrowser: boolean;\n isInteractive?: boolean;\n document: Document;\n rootElement: Element;\n shouldRender: boolean;\n location?: string;\n _renderMode?: 'serialize' | 'rehydrate' | undefined;\n}\n\nexport interface RenderRouteOptions {\n /**\n * When true, intercepts all fetch() calls during SSR rendering and\n * serializes the responses into a <script> tag in the HTML output.\n */\n shoebox?: boolean;\n\n /**\n * Enable Glimmer VM rehydration mode.\n *\n * When true, the server renders with `_renderMode: 'serialize'`,\n * annotating the DOM with markers Glimmer can reuse on the client.\n *\n * @default false\n */\n rehydrate?: boolean;\n\n /**\n * CSS manifest mapping route names to their associated CSS asset paths.\n *\n * Generated automatically by the `emberSsr()` Vite plugin during the\n * client build (written as `css-manifest.json`).\n */\n cssManifest?: CssManifest;\n\n /**\n * HTTP headers from the incoming request to forward to fetch() calls\n * made during SSR rendering.\n *\n * Use this to forward authentication cookies, authorization tokens,\n * or other request-scoped headers so the SSR render can make\n * authenticated API calls on behalf of the user.\n *\n * Only the specified headers are forwarded. Common usage:\n * ```js\n * const rendered = await app.renderRoute(req.url, {\n * headers: { cookie: req.headers.cookie },\n * });\n * ```\n */\n headers?: Record<string, string>;\n}\n\nexport interface RenderResult {\n /** Rendered HTML from the document's <head> */\n head: string;\n /** Rendered HTML from the document's <body> */\n body: string;\n /** Attributes set on the <body> element during rendering (e.g., data-theme, class) */\n bodyAttrs: Record<string, string>;\n /** HTTP status code (200 by default) */\n statusCode: number;\n /** Any error that occurred during rendering */\n error?: Error;\n}\n\n// ─── Shoebox Types ───────────────────────────────────────────────────\n\n/**\n * A captured fetch response for transfer from server to client.\n */\nexport interface ShoeboxEntry {\n url: string;\n status: number;\n statusText: string;\n headers: Record<string, string>;\n body: string;\n}\n\n// ─── EmberApp ────────────────────────────────────────────────────────\n\nexport interface EmberAppDevOptions {\n /**\n * Vite's `ssrLoadModule` function from the dev server.\n *\n * When provided, `createEmberApp` skips tinypool entirely and renders\n * in-process using Vite's module resolution pipeline. The SSR entry is\n * re-loaded on every render so HMR changes are reflected immediately.\n *\n * Obtain this from your Vite dev server instance:\n * ```js\n * const vite = await createServer({ ... });\n * await createEmberApp('app/app-ssr.ts', {\n * dev: { ssrLoadModule: vite.ssrLoadModule.bind(vite) },\n * });\n * ```\n */\n ssrLoadModule: (path: string) => Promise<Record<string, unknown>>;\n}\n\nexport interface EmberAppOptions {\n /**\n * Number of long-lived worker threads in the pool.\n *\n * Each worker imports the SSR bundle once and handles all subsequent\n * render requests without re-importing — making per-render cost ~4ms\n * instead of ~200ms for a fresh-worker approach.\n *\n * Ignored when `dev` is provided.\n *\n * @default os.cpus().length\n */\n workers?: number;\n\n /**\n * How often (in milliseconds) to recycle all workers in the pool.\n *\n * When set, `pool.recycleWorkers()` is called on this interval —\n * tinypool waits for all in-flight tasks to complete, then replaces\n * every worker with a fresh one. This bounds memory growth in\n * long-running processes where workers accumulate state over time.\n *\n * Set to `0` or omit to disable periodic recycling.\n *\n * Ignored when `dev` is provided.\n *\n * @example\n * // Recycle workers every hour\n * await createEmberApp(bundlePath, { recycleWorkerInterval: 60 * 60 * 1000 });\n */\n recycleWorkerInterval?: number;\n\n /**\n * When `true`, each render task is handled by a freshly-started worker.\n *\n * This maps directly to tinypool's `isolateWorkers` option. The worker is\n * replaced after every task, so module-level state (caches, singletons,\n * open handles) never bleeds between requests. The trade-off is that every\n * render pays the full worker-startup and bundle-import cost instead of\n * reusing a warm worker.\n *\n * For most apps the default (long-lived, warm workers) is preferred.\n * Enable isolation when you need strict request-level process boundaries,\n * e.g. when the SSR bundle keeps global state that cannot be reset between\n * renders.\n *\n * Ignored when `dev` is provided.\n *\n * @default false\n */\n isolateWorkers?: boolean;\n\n /**\n * Dev mode options. When provided, skips tinypool and renders in-process\n * via Vite's `ssrLoadModule` so HMR changes are picked up on every render.\n */\n dev?: EmberAppDevOptions;\n}\n\nexport interface EmberApp {\n /**\n * Renders a route and returns the raw head/body HTML fragments.\n *\n * @param url The URL path to render, e.g. `'/'` or `'/about'`\n */\n renderRoute(url: string, options?: RenderRouteOptions): Promise<RenderResult>;\n\n /**\n * Shuts down the worker pool. Call this when the app server is\n * stopping or after SSG prerendering is complete.\n */\n destroy(): Promise<void>;\n}\n\n// ─── EmberApp factory ────────────────────────────────────────────────\n\n/**\n * Creates a long-lived worker thread pool for SSR/SSG rendering.\n *\n * Each worker imports the SSR bundle once at startup and reuses it for all\n * subsequent renders — no bundle re-import, no Worker respawn.\n *\n * Pass `dev: { ssrLoadModule }` to run in dev mode instead: renders happen\n * in-process via Vite's module resolution pipeline with no tinypool workers.\n * The SSR entry is re-loaded on every render so HMR changes are reflected\n * immediately.\n *\n * @example Production\n * ```js\n * import { createEmberApp, assembleHTML } from '@st-h/vite-ember-ssr/server';\n * import { resolve } from 'node:path';\n *\n * const app = await createEmberApp(resolve('dist/server/app-ssr.mjs'));\n *\n * // In a request handler:\n * const result = await app.renderRoute(req.url);\n * const html = assembleHTML(template, result);\n *\n * // On server shutdown:\n * await app.destroy();\n * ```\n *\n * @example Development\n * ```js\n * import { createServer } from 'vite';\n * import { createEmberApp, assembleHTML } from '@st-h/vite-ember-ssr/server';\n *\n * const vite = await createServer({ server: { middlewareMode: true }, appType: 'custom' });\n * const app = await createEmberApp('app/app-ssr.ts', {\n * dev: { ssrLoadModule: vite.ssrLoadModule.bind(vite) },\n * });\n * ```\n */\nexport async function createEmberApp(\n ssrBundlePath: string,\n options: EmberAppOptions = {},\n): Promise<EmberApp> {\n if (options.dev) {\n return createDevEmberApp(ssrBundlePath, options.dev);\n }\n\n const bundleURL = ssrBundlePath.startsWith('file://')\n ? ssrBundlePath\n : pathToFileURL(ssrBundlePath).href;\n\n const workerCount = options.workers ?? cpus().length;\n\n const { default: Tinypool } = await import('tinypool');\n const pool = new Tinypool({\n filename: WORKER_PATH,\n minThreads: workerCount,\n maxThreads: workerCount,\n isolateWorkers: options.isolateWorkers ?? false,\n // Pass the bundle URL so the worker can import it eagerly at startup,\n // paying the cold-start cost once (at server init) rather than on the\n // first render request.\n workerData: { ssrBundlePath: bundleURL },\n });\n\n // Schedule periodic worker recycling when requested. pool.recycleWorkers()\n // waits for all in-flight renders to finish before replacing every worker\n // with a fresh one, bounding memory growth in long-running processes.\n let recycleTimer: ReturnType<typeof setInterval> | undefined;\n const recycleInterval = options.recycleWorkerInterval ?? 0;\n if (recycleInterval > 0) {\n recycleTimer = setInterval(() => {\n pool.recycleWorkers().catch(() => {\n // recycleWorkers rejects only if the pool is already being destroyed;\n // swallow the error to avoid an unhandled rejection on shutdown.\n });\n }, recycleInterval);\n // Allow the process to exit naturally without waiting for the next tick.\n recycleTimer.unref();\n }\n\n return {\n async renderRoute(\n url: string,\n renderOptions: RenderRouteOptions = {},\n ): Promise<RenderResult> {\n const result = (await pool.run({\n ssrBundlePath: bundleURL,\n url,\n shoebox: renderOptions.shoebox ?? false,\n rehydrate: renderOptions.rehydrate ?? false,\n cssManifest: renderOptions.cssManifest ?? null,\n headers: renderOptions.headers ?? null,\n })) as {\n head: string;\n body: string;\n bodyAttrs: Record<string, string>;\n statusCode: number;\n error?: string;\n };\n\n return {\n head: result.head,\n body: result.body,\n bodyAttrs: result.bodyAttrs ?? {},\n statusCode: result.statusCode,\n error: result.error ? new Error(result.error) : undefined,\n };\n },\n\n async destroy(): Promise<void> {\n clearInterval(recycleTimer);\n await pool.destroy();\n },\n };\n}\n\n// ─── HTML Assembly ───────────────────────────────────────────────────\n\nconst SSR_HEAD_MARKER = '<!-- VITE_EMBER_SSR_HEAD -->';\nconst SSR_BODY_MARKER = '<!-- VITE_EMBER_SSR_BODY -->';\nconst SSR_MARKER_REGEX = /<!-- VITE_EMBER_SSR_(HEAD|BODY) -->/g;\n\n/**\n * Assembles the final HTML response by inserting rendered content\n * into the index.html template.\n *\n * When `rendered.bodyAttrs` is provided, attributes set on the `<body>`\n * element during SSR (e.g., `data-theme`, `class`) are applied to the\n * `<body>` tag in the template HTML.\n */\nexport function assembleHTML(\n template: string,\n rendered: Pick<RenderResult, 'head' | 'body' | 'bodyAttrs'>,\n): string {\n let headReplaced = false;\n let bodyReplaced = false;\n\n let html = template.replace(SSR_MARKER_REGEX, (_match, tag: string) => {\n if (tag === 'HEAD' && !headReplaced) {\n headReplaced = true;\n return rendered.head;\n }\n if (tag === 'BODY' && !bodyReplaced) {\n bodyReplaced = true;\n return rendered.body;\n }\n return '';\n });\n\n // Apply body attributes from SSR rendering\n const attrs = rendered.bodyAttrs;\n if (attrs && Object.keys(attrs).length > 0) {\n const attrString = Object.entries(attrs)\n .map(([key, value]) => `${key}=\"${value.replace(/\"/g, '"')}\"`)\n .join(' ');\n html = html.replace(/<body([^>]*)>/, `<body$1 ${attrString}>`);\n }\n\n return html;\n}\n\n/**\n * Checks whether an HTML template contains the required SSR markers.\n */\nexport function hasSSRMarkers(html: string): { head: boolean; body: boolean } {\n return {\n head: html.includes(SSR_HEAD_MARKER),\n body: html.includes(SSR_BODY_MARKER),\n };\n}\n\n// ─── CSS Manifest Loading ────────────────────────────────────────────\n\nexport type { CssManifest } from './vite-plugin.js';\nexport { CSS_MANIFEST_FILENAME } from './vite-plugin.js';\n\n/**\n * Loads the CSS manifest from the client build output directory.\n */\nexport async function loadCssManifest(\n clientDir: string,\n): Promise<CssManifest | undefined> {\n const { readFile } = await import('node:fs/promises');\n const { join } = await import('node:path');\n const { CSS_MANIFEST_FILENAME: filename } = await import('./vite-plugin.js');\n\n try {\n const raw = await readFile(join(clientDir, filename), 'utf-8');\n return JSON.parse(raw) as CssManifest;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Plugin } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/vite-plugin.d.ts
|
|
4
|
+
declare const SSR_HEAD_MARKER = "<!-- VITE_EMBER_SSR_HEAD -->";
|
|
5
|
+
declare const SSR_BODY_MARKER = "<!-- VITE_EMBER_SSR_BODY -->";
|
|
6
|
+
/**
|
|
7
|
+
* Name of the CSS manifest file generated during the client build.
|
|
8
|
+
* Maps dynamic entry source modules to their associated CSS asset paths.
|
|
9
|
+
*/
|
|
10
|
+
declare const CSS_MANIFEST_FILENAME = "css-manifest.json";
|
|
11
|
+
/**
|
|
12
|
+
* The CSS manifest maps Ember route names to the CSS files that Vite
|
|
13
|
+
* extracted from their lazy-loaded template chunks during the client build.
|
|
14
|
+
*
|
|
15
|
+
* Route names use Ember's dot-separated convention for nested routes:
|
|
16
|
+
* - `about` for `app/templates/about.gts`
|
|
17
|
+
* - `blog.post` for `app/templates/blog/post.gts`
|
|
18
|
+
*
|
|
19
|
+
* Example:
|
|
20
|
+
* ```json
|
|
21
|
+
* {
|
|
22
|
+
* "about": ["/assets/about-VWk4xp3e.css"]
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* During SSR, the renderer queries the active route name from Ember's
|
|
27
|
+
* router service and looks up CSS files to inject as `<link>` tags.
|
|
28
|
+
*/
|
|
29
|
+
type CssManifest = Record<string, string[]>;
|
|
30
|
+
interface EmberSsrPluginOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Output directory for the client build.
|
|
33
|
+
* @default 'dist/client'
|
|
34
|
+
*/
|
|
35
|
+
clientOutDir?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Output directory for the SSR build.
|
|
38
|
+
* @default 'dist/server'
|
|
39
|
+
*/
|
|
40
|
+
serverOutDir?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Vite plugin that configures SSR support for Ember applications.
|
|
44
|
+
*
|
|
45
|
+
* Handles all SSR-related Vite configuration automatically:
|
|
46
|
+
*
|
|
47
|
+
* - Bundles all dependencies into SSR builds (`ssr.noExternal: [/./]`)
|
|
48
|
+
* to avoid runtime resolution failures under pnpm's strict
|
|
49
|
+
* node_modules layout (see issue #4)
|
|
50
|
+
* - Sets build defaults: `dist/client` for client builds,
|
|
51
|
+
* `dist/server` with `target: 'node22'` for SSR builds
|
|
52
|
+
* - Writes a `package.json` with `"type": "module"` to the SSR
|
|
53
|
+
* build output directory (needed for Node ESM compatibility)
|
|
54
|
+
*/
|
|
55
|
+
declare function emberSsr(options?: EmberSsrPluginOptions): Plugin;
|
|
56
|
+
interface EmberSsgPluginOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Routes to prerender as static HTML files.
|
|
59
|
+
*
|
|
60
|
+
* Each entry is a route path (without leading slash).
|
|
61
|
+
* 'index' produces `index.html` at the root, other routes produce
|
|
62
|
+
* `<route>/index.html` (e.g., 'about' → `about/index.html`).
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```js
|
|
66
|
+
* emberSsg({
|
|
67
|
+
* routes: ['index', 'about', 'pokemon', 'pokemon/charmander'],
|
|
68
|
+
* })
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
routes: string[];
|
|
72
|
+
/**
|
|
73
|
+
* The SSR entry module path, relative to the project root.
|
|
74
|
+
* This file must export a `createSsrApp` function.
|
|
75
|
+
* @default 'app/app-ssr.ts'
|
|
76
|
+
*/
|
|
77
|
+
ssrEntry?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Enable shoebox (fetch replay) for prerendered pages.
|
|
80
|
+
*
|
|
81
|
+
* When true, fetch responses from route model hooks are captured during
|
|
82
|
+
* prerendering and serialized into the HTML. The client calls
|
|
83
|
+
* `installShoebox()` before boot to replay those responses and avoid
|
|
84
|
+
* duplicate API requests.
|
|
85
|
+
*
|
|
86
|
+
* @default false
|
|
87
|
+
*/
|
|
88
|
+
shoebox?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Output directory for the client build.
|
|
91
|
+
* @default 'dist'
|
|
92
|
+
*/
|
|
93
|
+
outDir?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Enable Glimmer rehydration for prerendered pages.
|
|
96
|
+
*
|
|
97
|
+
* When `true`, the server renders with `_renderMode: 'serialize'`,
|
|
98
|
+
* annotating the DOM with Glimmer markers. The client boots with
|
|
99
|
+
* `app.visit(url, { _renderMode: 'rehydrate' })` to reuse the
|
|
100
|
+
* static DOM instead of replacing it.
|
|
101
|
+
*
|
|
102
|
+
* When `false` (default), boundary markers are emitted and the
|
|
103
|
+
* client uses `cleanupSSRContent()` in the application template
|
|
104
|
+
* to remove the SSR content before Ember renders fresh.
|
|
105
|
+
*
|
|
106
|
+
* @default false
|
|
107
|
+
*/
|
|
108
|
+
rehydrate?: boolean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Vite plugin for Static Site Generation (SSG) of Ember applications.
|
|
112
|
+
*
|
|
113
|
+
* Prerenders the specified routes to static HTML files at build time.
|
|
114
|
+
* Fully self-contained — only a single `vite build` is needed.
|
|
115
|
+
*
|
|
116
|
+
* After the client build completes, the plugin runs a second SSR build
|
|
117
|
+
* via `vite.build()` to produce a bundled SSR entry module, imports it,
|
|
118
|
+
* renders each route using HappyDOM, and writes the resulting HTML files
|
|
119
|
+
* into the client output directory. The temporary SSR bundle is cleaned
|
|
120
|
+
* up automatically.
|
|
121
|
+
*
|
|
122
|
+
* All dependencies are bundled into the SSR output (no externals) to
|
|
123
|
+
* avoid runtime resolution failures under pnpm's strict node_modules
|
|
124
|
+
* layout. See issue #4.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```js
|
|
128
|
+
* // vite.config.mjs
|
|
129
|
+
* import { emberSsg } from '@st-h/vite-ember-ssr/vite-plugin';
|
|
130
|
+
*
|
|
131
|
+
* export default defineConfig({
|
|
132
|
+
* plugins: [
|
|
133
|
+
* ember(),
|
|
134
|
+
* babel({ babelHelpers: 'runtime', extensions }),
|
|
135
|
+
* emberSsg({
|
|
136
|
+
* routes: ['index', 'about', 'pokemon', 'pokemon/charmander'],
|
|
137
|
+
* }),
|
|
138
|
+
* ],
|
|
139
|
+
* });
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
declare function emberSsg(options: EmberSsgPluginOptions): Plugin;
|
|
143
|
+
//#endregion
|
|
144
|
+
export { SSR_BODY_MARKER as a, emberSsr as c, EmberSsrPluginOptions as i, CssManifest as n, SSR_HEAD_MARKER as o, EmberSsgPluginOptions as r, emberSsg as s, CSS_MANIFEST_FILENAME as t };
|
|
145
|
+
//# sourceMappingURL=vite-plugin-CQou_tr5.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite-plugin-CQou_tr5.d.ts","names":[],"sources":["../src/vite-plugin.ts"],"mappings":";;;cAaa,eAAA;AAAA,cACA,eAAA;AADb;;;;AAAA,cAOa,qBAAA;AANb;;;;;AAMA;;;;;AAoBA;;;;;AAkQA;;;AA5RA,KA0BY,WAAA,GAAc,MAAA;AAAA,UAkQT,qBAAA;EA2BD;;;;EAtBd,YAAA;EAsBuB;;;;EAhBvB,YAAA;AAAA;;;;;;;;;;AAwMF;;;;iBAxLgB,QAAA,CAAS,OAAA,GAAS,qBAAA,GAA6B,MAAA;AAAA,UA6F9C,qBAAA;EA2FyC;;;;;;;;;;;;;;EA5ExD,MAAA;;;;;;EAOA,QAAA;;;;;;;;;;;EAYA,OAAA;;;;;EAMA,MAAA;;;;;;;;;;;;;;;EAgBA,SAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAmCc,QAAA,CAAS,OAAA,EAAS,qBAAA,GAAwB,MAAA"}
|