@st-h/vite-ember-ssr 0.2.0-alpha.1 → 0.3.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/src/server.ts CHANGED
@@ -39,6 +39,27 @@ export interface BootOptions {
39
39
  _renderMode?: 'serialize' | 'rehydrate' | undefined;
40
40
  }
41
41
 
42
+ /**
43
+ * Configures forwarding of the incoming request's `Cookie` header to
44
+ * fetch() calls made during SSR rendering.
45
+ *
46
+ * `allowedHosts` is required: forwarding the session cookie to every
47
+ * outbound fetch would leak credentials to third-party APIs the route
48
+ * happens to call. Each entry is matched against the request URL's
49
+ * `host` (hostname plus port) using exact equality — suffix wildcards
50
+ * are not supported.
51
+ */
52
+ export interface ForwardedCookie {
53
+ /** Cookie header value from the incoming request. */
54
+ value: string;
55
+ /**
56
+ * Hosts (`URL.host`) the cookie may be sent to. Exact match, no wildcards.
57
+ *
58
+ * @example ['api.example.com', 'auth.example.com:8080']
59
+ */
60
+ allowedHosts: string[];
61
+ }
62
+
42
63
  export interface RenderRouteOptions {
43
64
  /**
44
65
  * When true, intercepts all fetch() calls during SSR rendering and
@@ -46,16 +67,6 @@ export interface RenderRouteOptions {
46
67
  */
47
68
  shoebox?: boolean;
48
69
 
49
- /**
50
- * Enable Glimmer VM rehydration mode.
51
- *
52
- * When true, the server renders with `_renderMode: 'serialize'`,
53
- * annotating the DOM with markers Glimmer can reuse on the client.
54
- *
55
- * @default false
56
- */
57
- rehydrate?: boolean;
58
-
59
70
  /**
60
71
  * CSS manifest mapping route names to their associated CSS asset paths.
61
72
  *
@@ -65,21 +76,35 @@ export interface RenderRouteOptions {
65
76
  cssManifest?: CssManifest;
66
77
 
67
78
  /**
68
- * HTTP headers from the incoming request to forward to fetch() calls
69
- * made during SSR rendering.
79
+ * Maximum time (in milliseconds) to wait for `settled()` to resolve after
80
+ * `app.visit()`. Only applies when the SSR bundle exports a `settled`
81
+ * function (typically re-exported from `@ember/test-helpers`).
70
82
  *
71
- * Use this to forward authentication cookies, authorization tokens,
72
- * or other request-scoped headers so the SSR render can make
73
- * authenticated API calls on behalf of the user.
83
+ * If the timeout is exceeded, a warning is logged and the DOM is captured
84
+ * regardless. Use this to bound render time when a route registers a
85
+ * waiter that never resolves.
74
86
  *
75
- * Only the specified headers are forwarded. Common usage:
87
+ * @default 10000
88
+ */
89
+ settledTimeout?: number;
90
+
91
+ /**
92
+ * Forward the incoming request's `Cookie` header to fetch() calls made
93
+ * during SSR rendering. The cookie is only sent to hosts listed in
94
+ * `allowedHosts`, so credentials never leak to third-party APIs the
95
+ * route may also call.
96
+ *
97
+ * @example
76
98
  * ```js
77
- * const rendered = await app.renderRoute(req.url, {
78
- * headers: { cookie: req.headers.cookie },
99
+ * await app.renderRoute(req.url, {
100
+ * forwardCookie: {
101
+ * value: req.headers.cookie ?? '',
102
+ * allowedHosts: ['api.example.com'],
103
+ * },
79
104
  * });
80
105
  * ```
81
106
  */
82
- headers?: Record<string, string>;
107
+ forwardCookie?: ForwardedCookie;
83
108
  }
84
109
 
85
110
  export interface RenderResult {
@@ -218,7 +243,7 @@ export interface EmberApp {
218
243
  *
219
244
  * @example Production
220
245
  * ```js
221
- * import { createEmberApp, assembleHTML } from '@st-h/vite-ember-ssr/server';
246
+ * import { createEmberApp, assembleHTML } from 'vite-ember-ssr/server';
222
247
  * import { resolve } from 'node:path';
223
248
  *
224
249
  * const app = await createEmberApp(resolve('dist/server/app-ssr.mjs'));
@@ -234,7 +259,7 @@ export interface EmberApp {
234
259
  * @example Development
235
260
  * ```js
236
261
  * import { createServer } from 'vite';
237
- * import { createEmberApp, assembleHTML } from '@st-h/vite-ember-ssr/server';
262
+ * import { createEmberApp, assembleHTML } from 'vite-ember-ssr/server';
238
263
  *
239
264
  * const vite = await createServer({ server: { middlewareMode: true }, appType: 'custom' });
240
265
  * const app = await createEmberApp('app/app-ssr.ts', {
@@ -293,9 +318,9 @@ export async function createEmberApp(
293
318
  ssrBundlePath: bundleURL,
294
319
  url,
295
320
  shoebox: renderOptions.shoebox ?? false,
296
- rehydrate: renderOptions.rehydrate ?? false,
297
321
  cssManifest: renderOptions.cssManifest ?? null,
298
- headers: renderOptions.headers ?? null,
322
+ settledTimeout: renderOptions.settledTimeout ?? 10_000,
323
+ forwardCookie: renderOptions.forwardCookie ?? null,
299
324
  })) as {
300
325
  head: string;
301
326
  body: string;
@@ -457,22 +457,6 @@ export interface EmberSsgPluginOptions {
457
457
  * @default 'dist'
458
458
  */
459
459
  outDir?: string;
460
-
461
- /**
462
- * Enable Glimmer rehydration for prerendered pages.
463
- *
464
- * When `true`, the server renders with `_renderMode: 'serialize'`,
465
- * annotating the DOM with Glimmer markers. The client boots with
466
- * `app.visit(url, { _renderMode: 'rehydrate' })` to reuse the
467
- * static DOM instead of replacing it.
468
- *
469
- * When `false` (default), boundary markers are emitted and the
470
- * client uses `cleanupSSRContent()` in the application template
471
- * to remove the SSR content before Ember renders fresh.
472
- *
473
- * @default false
474
- */
475
- rehydrate?: boolean;
476
460
  }
477
461
 
478
462
  /**
@@ -494,7 +478,7 @@ export interface EmberSsgPluginOptions {
494
478
  * @example
495
479
  * ```js
496
480
  * // vite.config.mjs
497
- * import { emberSsg } from '@st-h/vite-ember-ssr/vite-plugin';
481
+ * import { emberSsg } from 'vite-ember-ssr/vite-plugin';
498
482
  *
499
483
  * export default defineConfig({
500
484
  * plugins: [
@@ -508,12 +492,7 @@ export interface EmberSsgPluginOptions {
508
492
  * ```
509
493
  */
510
494
  export function emberSsg(options: EmberSsgPluginOptions): Plugin {
511
- const {
512
- routes,
513
- ssrEntry = 'app/app-ssr.ts',
514
- shoebox = false,
515
- rehydrate = false,
516
- } = options;
495
+ const { routes, ssrEntry = 'app/app-ssr.ts', shoebox = false } = options;
517
496
 
518
497
  // Track whether the user explicitly provided outDir
519
498
  const explicitOutDir = options.outDir;
@@ -712,7 +691,6 @@ export function emberSsg(options: EmberSsgPluginOptions): Plugin {
712
691
  try {
713
692
  const result = await app.renderRoute(url, {
714
693
  shoebox,
715
- rehydrate,
716
694
  cssManifest,
717
695
  });
718
696
 
package/src/worker.ts CHANGED
@@ -6,9 +6,12 @@
6
6
  * every render. Renders are serialised by tinypool (concurrentTasksPerWorker:1),
7
7
  * so there is no concurrency concern within a single worker.
8
8
  *
9
- * app.visit() fully owns document.head/body between calls, so DOM state does
10
- * not bleed across renders. A fresh ApplicationInstance is created per visit
11
- * and destroyed after the DOM is read, keeping container singletons clean.
9
+ * A fresh ApplicationInstance is created per visit and destroyed after the
10
+ * DOM is read, keeping container singletons clean. Because the Window is
11
+ * long-lived, every render resets the mutable per-request window state in a
12
+ * finally block: document.body (content + attributes), document.head/title,
13
+ * and local/session storage. Without this, one request's DOM or storage
14
+ * writes would bleed into the next request served by this worker.
12
15
  *
13
16
  * The shoebox fetch interceptor is installed once at startup. Each render
14
17
  * assigns a fresh entries Map before visiting, so entries never bleed between
@@ -23,7 +26,14 @@ import type {
23
26
  EmberApplicationInstance,
24
27
  BootOptions,
25
28
  ShoeboxEntry,
29
+ ForwardedCookie,
26
30
  } from './server.js';
31
+ import {
32
+ abortSignalMiddleware,
33
+ compose,
34
+ forwardCookieMiddleware,
35
+ shoeboxMiddleware,
36
+ } from './fetch-middleware.js';
27
37
 
28
38
  // ─── Types ────────────────────────────────────────────────────────────
29
39
 
@@ -31,9 +41,9 @@ export interface WorkerRenderOptions {
31
41
  ssrBundlePath: string;
32
42
  url: string;
33
43
  shoebox: boolean;
34
- rehydrate: boolean;
35
44
  cssManifest: CssManifest | null;
36
- headers: Record<string, string> | null;
45
+ settledTimeout: number;
46
+ forwardCookie: ForwardedCookie | null;
37
47
  }
38
48
 
39
49
  export interface WorkerRenderResult {
@@ -113,6 +123,7 @@ const { ssrBundlePath: startupBundlePath } = (
113
123
 
114
124
  const startupMod = (await import(startupBundlePath)) as {
115
125
  createSsrApp?: () => EmberApplication;
126
+ settled?: () => Promise<void>;
116
127
  };
117
128
  if (typeof startupMod.createSsrApp !== 'function') {
118
129
  throw new Error(
@@ -123,75 +134,37 @@ if (typeof startupMod.createSsrApp !== 'function') {
123
134
 
124
135
  const app: EmberApplication = startupMod.createSsrApp();
125
136
 
126
- // ─── Shoebox ──────────────────────────────────────────────────────────
137
+ // Optional: the SSR bundle may re-export `settled` from `@ember/test-helpers`.
138
+ // When present, the renderer awaits it after `app.visit()` so any registered
139
+ // `@ember/test-waiters` (used by WarpDrive, ember-concurrency, etc.) drain
140
+ // before the DOM is captured. When absent, we fall back to a single
141
+ // Backburner autorun drain via `setTimeout(0)`.
142
+ const appSettled: (() => Promise<void>) | null =
143
+ typeof startupMod.settled === 'function' ? startupMod.settled : null;
144
+
145
+ // ─── Fetch middleware pipeline ────────────────────────────────────────
127
146
 
128
147
  const SHOEBOX_SCRIPT_ID = 'vite-ember-ssr-shoebox';
129
148
 
130
- // The fetch interceptor is installed once at startup. globalThis.fetch
131
- // never changes. Each render passes fresh per-render state so there is
132
- // no bleed between requests.
149
+ // The fetch pipeline is installed once at startup. globalThis.fetch never
150
+ // changes. Per-render state (shoebox entries, forwarded cookie) lives in
151
+ // module-level variables that the middlewares read via getters.
133
152
  const realFetch = globalThis.fetch;
134
- let shoeboxEntries: Map<string, ShoeboxEntry> | null = null;
135
- let requestHeaders: Record<string, string> | null = null;
136
-
137
- const interceptedFetch: typeof fetch = async (input, init) => {
138
- const request = new Request(input, init);
139
-
140
- // Inject forwarded request headers (e.g., cookies) into outgoing fetches.
141
- // Only applies to requests without an existing cookie/authorization header,
142
- // so explicit headers in app code are not overwritten.
143
- if (requestHeaders) {
144
- const mergedInit = { ...init };
145
- const existingHeaders = new Headers(mergedInit.headers);
146
- for (const [key, value] of Object.entries(requestHeaders)) {
147
- if (!existingHeaders.has(key)) {
148
- existingHeaders.set(key, value);
149
- }
150
- }
151
- mergedInit.headers = existingHeaders;
152
- const mergedRequest = new Request(input, mergedInit);
153
-
154
- if (mergedRequest.method.toUpperCase() !== 'GET') return realFetch(mergedRequest);
155
- const response = await realFetch(mergedRequest);
156
- if (shoeboxEntries) {
157
- captureShoeboxEntry(mergedRequest, response);
158
- }
159
- return response;
160
- }
161
-
162
- if (request.method.toUpperCase() !== 'GET') return realFetch(input, init);
163
- const response = await realFetch(input, init);
164
- if (shoeboxEntries) {
165
- captureShoeboxEntry(request, response);
166
- }
167
- return response;
168
- };
169
-
170
- async function captureShoeboxEntry(
171
- request: Request,
172
- response: Response,
173
- ): Promise<void> {
174
- try {
175
- const clone = response.clone();
176
- const body = await clone.text();
177
- const headers: Record<string, string> = {};
178
- clone.headers.forEach((v, k) => {
179
- headers[k] = v;
180
- });
181
- shoeboxEntries?.set(request.url, {
182
- url: request.url,
183
- status: clone.status,
184
- statusText: clone.statusText,
185
- headers,
186
- body,
187
- });
188
- } catch {
189
- /* skip */
190
- }
191
- }
153
+ let activeShoebox: Map<string, ShoeboxEntry> | null = null;
154
+ let activeCookie: ForwardedCookie | null = null;
155
+ let activeAbort: AbortController | null = null;
156
+
157
+ const fetchWithMiddleware = compose(
158
+ [
159
+ forwardCookieMiddleware(() => activeCookie),
160
+ shoeboxMiddleware(() => activeShoebox),
161
+ abortSignalMiddleware(() => activeAbort?.signal ?? null),
162
+ ],
163
+ (request) => realFetch(request),
164
+ );
192
165
 
193
166
  // Install once — never needs to be restored.
194
- globalThis.fetch = interceptedFetch;
167
+ globalThis.fetch = fetchWithMiddleware;
195
168
 
196
169
  function serializeShoebox(entries: ShoeboxEntry[]): string {
197
170
  if (entries.length === 0) return '';
@@ -237,27 +210,73 @@ function buildRouteCssLinks(
237
210
  return links.join('');
238
211
  }
239
212
 
213
+ let warnedMissingSettled = false;
214
+
215
+ async function awaitSettled(timeoutMs: number): Promise<void> {
216
+ if (!appSettled) {
217
+ if (timeoutMs > 0 && !warnedMissingSettled) {
218
+ warnedMissingSettled = true;
219
+ console.warn(
220
+ '[vite-ember-ssr] settledTimeout is set but the SSR bundle does not ' +
221
+ 'export `settled` — renders will NOT wait for the app to settle ' +
222
+ 'and may capture incomplete HTML. Add ' +
223
+ "`export { settled } from '@ember/test-helpers';` to your SSR entry.",
224
+ );
225
+ }
226
+ // Fallback: drain Backburner's autorun microtask before reading the DOM.
227
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
228
+ return;
229
+ }
230
+
231
+ let timer: ReturnType<typeof setTimeout> | undefined;
232
+ try {
233
+ await Promise.race([
234
+ appSettled(),
235
+ new Promise<never>((_, reject) => {
236
+ timer = setTimeout(
237
+ () => reject(new Error(`settled() timed out after ${timeoutMs}ms`)),
238
+ timeoutMs,
239
+ );
240
+ }),
241
+ ]);
242
+ } catch (e) {
243
+ console.warn(
244
+ `[vite-ember-ssr] settled() did not resolve within ${timeoutMs}ms, ` +
245
+ `capturing DOM anyway:`,
246
+ e instanceof Error ? e.message : e,
247
+ );
248
+ } finally {
249
+ if (timer) clearTimeout(timer);
250
+ }
251
+ }
252
+
240
253
  export default async function render(
241
254
  options: WorkerRenderOptions,
242
255
  ): Promise<WorkerRenderResult> {
243
- const { url, shoebox, rehydrate, cssManifest, headers } = options;
256
+ const { url, shoebox, cssManifest, settledTimeout, forwardCookie } = options;
244
257
 
245
258
  // Use the long-lived document directly — no new Window, no globalThis swap.
246
259
  const document = win.document;
247
260
 
248
- // Give the interceptor a fresh Map for this render, or null if shoebox
249
- // is disabled, so entries never bleed between requests.
250
- shoeboxEntries = shoebox ? new Map() : null;
261
+ // Set per-render state. The middlewares read these via getters, so a
262
+ // single shared pipeline can serve every render without re-installation.
263
+ activeShoebox = shoebox ? new Map() : null;
264
+ activeCookie = forwardCookie;
265
+ activeAbort = new AbortController();
251
266
 
252
- // Forward request headers (e.g., cookies) to outgoing fetch calls
253
- // for this render only. Cleared after the render completes.
254
- requestHeaders = headers;
267
+ // Snapshot the pre-render <head> state so the finally below can restore
268
+ // it. ember-page-title and similar addons write into <head> during the
269
+ // render; without a reset, one request's document title (which may contain
270
+ // private data) bleeds into every later render served by this worker.
271
+ const preRenderTitle = document.title;
272
+ const preRenderHead = document.head?.innerHTML ?? '';
255
273
 
256
274
  let head = '';
257
275
  let body = '';
258
276
  let bodyAttrs: Record<string, string> = {};
259
277
  let cssLinks = '';
260
278
  let error: Error | undefined;
279
+ let instance: EmberApplicationInstance | undefined;
261
280
 
262
281
  try {
263
282
  const bootOptions: BootOptions = {
@@ -266,13 +285,15 @@ export default async function render(
266
285
  document: document as unknown as Document,
267
286
  rootElement: document.body as unknown as Element,
268
287
  shouldRender: true,
269
- ...(rehydrate ? { _renderMode: 'serialize' as const } : {}),
288
+ _renderMode: 'serialize',
270
289
  };
271
290
 
272
- const instance = await app.visit(url, bootOptions);
291
+ instance = await app.visit(url, bootOptions);
273
292
 
274
- // Drain Backburner's autorun microtask before reading the DOM.
275
- await new Promise<void>((resolve) => setTimeout(resolve, 0));
293
+ // Wait for the app to settle (test waiters, run loop, pending timers, etc.)
294
+ // before reading the DOM. Falls back to a microtask drain when the SSR
295
+ // bundle doesn't export `settled`.
296
+ await awaitSettled(settledTimeout);
276
297
 
277
298
  if (cssManifest) cssLinks = buildRouteCssLinks(cssManifest, instance);
278
299
  head = document.head?.innerHTML ?? '';
@@ -284,47 +305,74 @@ export default async function render(
284
305
  bodyAttrs[attr.name] = attr.value;
285
306
  }
286
307
  }
287
-
288
- // Destroy the instance so its container is torn down cleanly.
289
- // app.visit() creates a fresh ApplicationInstance per call; without
290
- // destroying it the container's singletons (including location:none)
291
- // remain live and can corrupt the next visit.
292
- instance.destroy();
293
-
294
- // rehydrate mode causes left over rehydration markers to remain in the DOM, so
295
- // we clear the body to ensure a clean slate for the next render.
296
- if (rehydrate) {
297
- document.body.innerHTML = '';
308
+ } catch (e) {
309
+ error = e instanceof Error ? e : new Error(String(e));
310
+ } finally {
311
+ // Destroy the instance so its container is torn down cleanly. app.visit()
312
+ // creates a fresh ApplicationInstance per call; without destroying it the
313
+ // container's singletons (including location:none) remain live and can
314
+ // corrupt the next visit. This MUST run even when the render above throws
315
+ // (settle timeout, CSS build, DOM read) otherwise the leaked instance
316
+ // accumulates in the long-lived worker. `instance` is undefined when
317
+ // app.visit() itself threw before assigning it. Guard the call: a destroy
318
+ // that throws inside this finally would skip the DOM reset below and mask
319
+ // the render's own error.
320
+ try {
321
+ instance?.destroy();
322
+ } catch {
323
+ /* instance teardown failed — the DOM reset below must still run */
298
324
  }
299
325
 
300
- // Clear body attributes so they don't bleed into the next render.
326
+ // Serialize mode leaves rehydration markers in the DOM; reset the body so
327
+ // the next render starts from a clean slate regardless of success/failure.
301
328
  if (document.body) {
329
+ document.body.innerHTML = '';
330
+
331
+ // Clear body attributes so they don't bleed into the next render.
302
332
  for (const attr of Array.from(document.body.attributes)) {
303
333
  document.body.removeAttribute(attr.name);
304
334
  }
305
335
  }
306
- } catch (e) {
307
- error = e instanceof Error ? e : new Error(String(e));
308
- }
309
336
 
310
- // Clear per-render state to prevent bleed between requests.
311
- requestHeaders = null;
337
+ // Restore <head> (and the title) to its pre-render state. The rendered
338
+ // head HTML was already captured above, so anything the render added —
339
+ // <title> via ember-page-title, meta tags, etc. — must not survive into
340
+ // the next request's document.
341
+ if (document.head) document.head.innerHTML = preRenderHead;
342
+ if (document.title !== preRenderTitle) document.title = preRenderTitle;
343
+
344
+ // Clear web storage so values written during the render (user
345
+ // preferences, cached tokens) don't bleed into the next request.
346
+ try {
347
+ win.localStorage.clear();
348
+ win.sessionStorage.clear();
349
+ } catch {
350
+ /* storage unavailable in this happy-dom configuration */
351
+ }
352
+
353
+ // Abort any fetches this render left in flight (e.g. after a settled()
354
+ // timeout) so they stop consuming the connection instead of lingering
355
+ // into later renders. Their shoebox entries — if a response still
356
+ // arrives — go to this render's already-dead map (see shoeboxMiddleware).
357
+ activeAbort?.abort();
358
+ activeAbort = null;
359
+ }
312
360
 
313
361
  const shoeboxHTML =
314
- shoeboxEntries && shoeboxEntries.size > 0
315
- ? serializeShoebox(Array.from(shoeboxEntries.values()))
362
+ activeShoebox && activeShoebox.size > 0
363
+ ? serializeShoebox(Array.from(activeShoebox.values()))
316
364
  : '';
317
- const rehydrateHTML = rehydrate
318
- ? '<script>window.__vite_ember_ssr_rehydrate__=true</script>'
319
- : '';
365
+
366
+ // Clear per-render state so a stray late fetch can't see stale config.
367
+ activeShoebox = null;
368
+ activeCookie = null;
369
+ const rehydrateHTML =
370
+ '<script>window.__vite_ember_ssr_rehydrate__=true</script>';
320
371
  const fullHead = cssLinks + rehydrateHTML + shoeboxHTML + head;
321
- const wrappedBody = rehydrate
322
- ? body
323
- : `<script type="x/boundary" id="ssr-body-start"></script>${body}<script type="x/boundary" id="ssr-body-end"></script>`;
324
372
 
325
373
  return {
326
374
  head: fullHead,
327
- body: wrappedBody,
375
+ body,
328
376
  bodyAttrs,
329
377
  statusCode: error ? 500 : 200,
330
378
  ...(error
@@ -1 +0,0 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;AAmBA;;;;UAAiB,gBAAA;EACf,KAAA,CAAM,GAAA,UAAa,OAAA,GAAU,WAAA,GAAc,OAAA,CAAQ,wBAAA;EACnD,OAAA;AAAA;AAAA,UAGe,wBAAA;EACf,OAAA;EACA,MAAA;EACA,OAAA;EACA,MAAA,EAAQ,QAAA;AAAA;AAAA,UAGO,WAAA;EACf,SAAA;EACA,aAAA;EACA,QAAA,EAAU,QAAA;EACV,WAAA,EAAa,OAAA;EACb,YAAA;EACA,QAAA;EACA,WAAA;AAAA;AAAA,UAGe,kBAAA;EAbf;;;;EAkBA,OAAA;EAf0B;;;;;;;;EAyB1B,SAAA;EApBA;;;;;AAKF;EAuBE,WAAA,GAAc,WAAA;;;;;;;;;;;AAoBhB;;;;;EAHE,OAAA,GAAU,MAAA;AAAA;AAAA,UAGK,YAAA;EAQf;EANA,IAAA;EAQQ;EANR,IAAA;EAMa;EAJb,SAAA,EAAW,MAAA;EAYgB;EAV3B,UAAA;EAce;EAZf,KAAA,GAAQ,KAAA;AAAA;;;;UAQO,YAAA;EACf,GAAA;EACA,MAAA;EACA,UAAA;EACA,OAAA,EAAS,MAAA;EACT,IAAA;AAAA;AAAA,UAKe,kBAAA;EAgBC;;;;;AAGlB;;;;;;;;;;EAHE,aAAA,GAAgB,IAAA,aAAiB,OAAA,CAAQ,MAAA;AAAA;AAAA,UAG1B,eAAA;;;;;;;;;;;;EAYf,OAAA;EAqDgE;;;;;AAgDlE;;;;;;;;;;;EAnFE,qBAAA;EAsFS;;;AAyFX;;;;;;;;;;AAkCA;;;;;EA7LE,cAAA;EA6L4D;;;AAe9D;EAtME,GAAA,GAAM,kBAAA;AAAA;AAAA,UAGS,QAAA;EAoMf;;;;;EA9LA,WAAA,CAAY,GAAA,UAAa,OAAA,GAAU,kBAAA,GAAqB,OAAA,CAAQ,YAAA;;;;;EAMhE,OAAA,IAAW,OAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0CS,cAAA,CACpB,aAAA,UACA,OAAA,GAAS,eAAA,GACR,OAAA,CAAQ,QAAA;;;;;;;;;iBAyFK,YAAA,CACd,QAAA,UACA,QAAA,EAAU,IAAA,CAAK,YAAA;;;;iBAgCD,aAAA,CAAc,IAAA;EAAiB,IAAA;EAAe,IAAA;AAAA;;;;iBAexC,eAAA,CACpB,SAAA,WACC,OAAA,CAAQ,WAAA"}
@@ -1 +0,0 @@
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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"vite-plugin-D-W5WQWe.js","names":[],"sources":["../src/vite-plugin.ts"],"sourcesContent":["import type { Plugin, PluginOption, ResolvedConfig, UserConfig } from 'vite';\nimport { join, dirname } from 'node:path';\nimport {\n mkdir,\n writeFile,\n readFile,\n rm,\n copyFile,\n access,\n} from 'node:fs/promises';\nimport { pathToFileURL } from 'node:url';\nimport { cpus } from 'node:os';\n\nexport const SSR_HEAD_MARKER = '<!-- VITE_EMBER_SSR_HEAD -->';\nexport const SSR_BODY_MARKER = '<!-- VITE_EMBER_SSR_BODY -->';\n\n/**\n * Name of the CSS manifest file generated during the client build.\n * Maps dynamic entry source modules to their associated CSS asset paths.\n */\nexport const CSS_MANIFEST_FILENAME = 'css-manifest.json';\n\n/**\n * The CSS manifest maps Ember route names to the CSS files that Vite\n * extracted from their lazy-loaded template chunks during the client build.\n *\n * Route names use Ember's dot-separated convention for nested routes:\n * - `about` for `app/templates/about.gts`\n * - `blog.post` for `app/templates/blog/post.gts`\n *\n * Example:\n * ```json\n * {\n * \"about\": [\"/assets/about-VWk4xp3e.css\"]\n * }\n * ```\n *\n * During SSR, the renderer queries the active route name from Ember's\n * router service and looks up CSS files to inject as `<link>` tags.\n */\nexport type CssManifest = Record<string, string[]>;\n\n/**\n * Derives an Ember route name from a source module path following\n * Ember's conventional file layout.\n *\n * `app/templates/about.gts` → `about`\n * `app/templates/blog/post.gts` → `blog.post`\n * `app/templates/index.gts` → `index`\n *\n * Returns undefined if the path doesn't match the convention.\n */\nfunction sourcePathToRouteName(\n facadeModuleId: string,\n root: string,\n): string | undefined {\n // Make the path relative to the project root\n let relativePath = facadeModuleId;\n if (relativePath.startsWith(root)) {\n relativePath = relativePath.slice(root.length);\n }\n // Strip leading slash\n if (relativePath.startsWith('/')) {\n relativePath = relativePath.slice(1);\n }\n\n // Match app/templates/<route-path>.<ext>\n const match = relativePath.match(\n /^app\\/templates\\/(.+)\\.(gts|gjs|hbs|ts|js)$/,\n );\n if (!match) return undefined;\n\n // Convert path separators to dots for nested routes\n return match[1].replace(/\\//g, '.');\n}\n\n/**\n * Minimal type for a Rollup output chunk with Vite metadata.\n * We define this locally to avoid a direct dependency on the 'rollup' package.\n */\ninterface OutputChunkWithMeta {\n type: 'chunk';\n isDynamicEntry: boolean;\n isEntry: boolean;\n facadeModuleId: string | null;\n name: string;\n fileName: string;\n imports: string[];\n viteMetadata?: {\n importedCss?: Set<string>;\n };\n}\n\n/**\n * Walks the Rollup output bundle and collects CSS files associated\n * with dynamic entry chunks. These are CSS imports that Vite extracted\n * from code-split chunks (e.g., lazy-loaded route templates).\n *\n * The main entry's CSS is already linked in the HTML template by Vite,\n * so we only collect CSS from `isDynamicEntry` chunks.\n *\n * When a component with CSS is shared across multiple lazy routes,\n * Vite extracts the shared CSS into a separate chunk. We walk each\n * dynamic entry's static `imports` graph to collect CSS from those\n * shared chunks too, skipping the main entry chunk (whose CSS is\n * already in the HTML template).\n *\n * Keys are Ember route names derived from the source file path using\n * Ember's conventional `app/templates/` directory structure.\n */\nfunction buildCssManifest(\n bundle: Record<string, { type: string }>,\n base: string,\n root: string,\n): CssManifest {\n const manifest: CssManifest = {};\n\n // Build a lookup of fileName → chunk for walking the import graph.\n const chunksByFile = new Map<string, OutputChunkWithMeta>();\n const mainEntryFiles = new Set<string>();\n\n for (const [, output] of Object.entries(bundle)) {\n if (output.type !== 'chunk') continue;\n const chunk = output as unknown as OutputChunkWithMeta;\n chunksByFile.set(chunk.fileName, chunk);\n\n // Track main entry chunks so we can exclude their CSS.\n // Main entry CSS is already linked in the HTML template by Vite.\n if (chunk.isEntry && !chunk.isDynamicEntry) {\n mainEntryFiles.add(chunk.fileName);\n }\n }\n\n /**\n * Recursively collect all CSS from a chunk and its static imports,\n * excluding main entry chunks (whose CSS is already in the template).\n */\n function collectCss(\n fileName: string,\n seen: Set<string>,\n css: Set<string>,\n ): void {\n if (seen.has(fileName)) return;\n seen.add(fileName);\n\n // Don't collect CSS from the main entry — it's already in the HTML.\n if (mainEntryFiles.has(fileName)) return;\n\n const chunk = chunksByFile.get(fileName);\n if (!chunk) return;\n\n const importedCss = chunk.viteMetadata?.importedCss;\n if (importedCss) {\n for (const cssFile of importedCss) {\n css.add(cssFile);\n }\n }\n\n // Walk static imports (shared chunks extracted by Vite).\n for (const imp of chunk.imports) {\n collectCss(imp, seen, css);\n }\n }\n\n for (const [, output] of Object.entries(bundle)) {\n if (output.type !== 'chunk') continue;\n\n const chunk = output as unknown as OutputChunkWithMeta;\n\n // Only collect CSS from dynamic entries (code-split chunks).\n if (!chunk.isDynamicEntry) continue;\n\n // Collect CSS from this chunk and all its static imports.\n const css = new Set<string>();\n collectCss(chunk.fileName, new Set(), css);\n\n if (css.size === 0) continue;\n\n // Derive the Ember route name from the source module path.\n // If the path doesn't match Ember conventions, fall back to\n // the chunk name (e.g., 'about' from 'about-B5EiMzMx.js').\n const routeName = chunk.facadeModuleId\n ? (sourcePathToRouteName(chunk.facadeModuleId, root) ?? chunk.name)\n : chunk.name;\n\n if (!routeName) continue;\n\n // Prefix CSS paths with the base URL so they work as href values.\n const cssFiles = Array.from(css).map((c) => `${base}${c}`);\n\n if (cssFiles.length > 0) {\n manifest[routeName] = cssFiles;\n }\n }\n\n return manifest;\n}\n\n/**\n * Returns SSR config appropriate for the current Vite command.\n *\n * Ember's virtual packages (`@glimmer/tracking`, `@ember/*`, etc.) are\n * provided by `ember-source` and not published as real npm packages.\n * When Vite externalizes a dependency that transitively imports one of\n * these virtual packages, Node's runtime module resolution fails under\n * pnpm's strict `node_modules` layout.\n *\n * For both production builds and dev mode:\n * - Clears any user-specified `ssr.external` (explicit string entries\n * take precedence over `noExternal` patterns in Vite, so we must\n * remove them to ensure `noExternal: [/./]` applies).\n * - Sets `ssr: { noExternal: [/./] }` so all deps go through Vite's\n * transform pipeline. This lets `@embroider/vite`'s resolver handle\n * virtual Ember/Glimmer packages that don't exist outside `ember-source`\n * under pnpm's strict `node_modules` layout.\n *\n * In dev mode, `ssrLoadModule` uses `SSRCompatModuleRunner` +\n * `ESModulesEvaluator`. Without bundling, this evaluates all module code\n * inline. CJS/UMD packages (e.g. `@warp-drive/utilities/string`,\n * `json-to-ast`) reference `module`, `exports`, or `global` which are not\n * available in the evaluator's context.\n *\n * The `cjsSsrShimTransform` hook (applied by `emberSsr()` and `emberSsg()`)\n * intercepts those files before they reach `ssrTransform` and wraps them\n * with a lightweight CommonJS shim, providing the missing `module`,\n * `exports`, and `global` bindings.\n *\n * See: https://github.com/evoactivity/vite-ember-ssr/issues/4\n */\nfunction ssrDepsConfig(\n userConfig: UserConfig,\n _command: 'build' | 'serve',\n): { ssr?: UserConfig['ssr'] } {\n if (userConfig.ssr) {\n delete userConfig.ssr.external;\n }\n return { ssr: { noExternal: [/./] } };\n}\n\n/**\n * Returns a Vite `transform` hook that wraps CJS/UMD modules encountered\n * during SSR transforms.\n *\n * When `noExternal: [/./]` is set, every dependency goes through Vite's\n * `ssrTransform` → `ESModulesEvaluator` pipeline. CJS/UMD files that use\n * `module`, `exports`, or `global` fail because those globals are not\n * available inside `ESModulesEvaluator`'s `AsyncFunction` context.\n *\n * This transform detects CJS/UMD content (no top-level `import`/`export`\n * statements, but contains `exports.xxx` or `module.exports`) and wraps\n * the code so that:\n * 1. `module`, `exports`, and `global` are available as local variables.\n * 2. The module's exports are re-exported as the ES default export.\n *\n * The heuristic is intentionally simple and conservative — it only fires\n * on files that have no ESM syntax at all, which covers the CJS/UMD\n * packages that appear in the Ember + WarpDrive dependency tree without\n * misidentifying genuine ESM files.\n */\nfunction cjsSsrShimTransform(\n code: string,\n _id: string,\n options?: { ssr?: boolean },\n): { code: string; map: null } | null {\n // Only apply during SSR transforms\n if (!options?.ssr) return null;\n\n // Skip if the file contains any top-level import/export → it's ESM\n if (/^(?:import\\s|export\\s|export\\{|export default)/m.test(code)) return null;\n\n // Only wrap files that use CommonJS exports or module.exports\n if (!/\\bexports\\s*[.[=]|\\bmodule\\s*\\.\\s*exports\\b/.test(code)) return null;\n\n const wrapped = `\\\nconst __cjs_module__ = { exports: {} };\nconst __cjs_exports__ = __cjs_module__.exports;\nconst __cjs_global__ = typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : {};\n(function(module, exports, global) {\n${code}\n})(__cjs_module__, __cjs_exports__, __cjs_global__);\nexport default __cjs_module__.exports;\n`;\n return { code: wrapped, map: null };\n}\n\n/**\n * Flatten and filter a Vite plugins array, which may contain nested arrays,\n * falsy values, and Promise-wrapped entries.\n */\nfunction flatPlugins(plugins: PluginOption[] | undefined): Plugin[] {\n if (!plugins) return [];\n return (plugins as unknown[])\n .flat(Infinity)\n .filter(\n (p): p is Plugin => p != null && typeof p === 'object' && 'name' in p,\n );\n}\n\nexport interface EmberSsrPluginOptions {\n /**\n * Output directory for the client build.\n * @default 'dist/client'\n */\n clientOutDir?: string;\n\n /**\n * Output directory for the SSR build.\n * @default 'dist/server'\n */\n serverOutDir?: string;\n}\n\n/**\n * Vite plugin that configures SSR support for Ember applications.\n *\n * Handles all SSR-related Vite configuration automatically:\n *\n * - Bundles all dependencies into SSR builds (`ssr.noExternal: [/./]`)\n * to avoid runtime resolution failures under pnpm's strict\n * node_modules layout (see issue #4)\n * - Sets build defaults: `dist/client` for client builds,\n * `dist/server` with `target: 'node22'` for SSR builds\n * - Writes a `package.json` with `\"type\": \"module\"` to the SSR\n * build output directory (needed for Node ESM compatibility)\n */\nexport function emberSsr(options: EmberSsrPluginOptions = {}): Plugin {\n let resolvedConfig: ResolvedConfig;\n\n return {\n name: 'vite-ember-ssr',\n\n config(userConfig, env): UserConfig {\n // Bundle all dependencies for SSR builds and dev mode to avoid runtime\n // failures under pnpm's strict node_modules layout when external packages\n // transitively import virtual Ember/Glimmer packages (e.g.\n // @glimmer/tracking) that only exist inside ember-source.\n // In dev mode, the `transform: cjsSsrShimTransform` hook wraps\n // CJS/UMD packages so they work with ESModulesEvaluator.\n // See: https://github.com/evoactivity/vite-ember-ssr/issues/4\n const ssrConfig = ssrDepsConfig(userConfig, env.command);\n\n // During the SSG child build, only set ssr config — don't\n // override build.outDir (the SSG plugin sets it explicitly\n // via inline config to a temp directory).\n if (process.env.__VITE_EMBER_SSG_CHILD__) {\n return ssrConfig;\n }\n\n if (env.isSsrBuild) {\n return {\n ...ssrConfig,\n build: {\n outDir: options.serverOutDir ?? 'dist/server',\n target: 'node22',\n sourcemap: true,\n minify: false,\n },\n };\n }\n\n return {\n ...ssrConfig,\n build: {\n outDir: options.clientOutDir ?? 'dist/client',\n },\n };\n },\n\n configResolved(config) {\n resolvedConfig = config;\n },\n\n transform: cjsSsrShimTransform,\n\n generateBundle(_outputOptions, bundle) {\n // Only generate the CSS manifest for client builds.\n // SSR builds strip CSS imports, so they have nothing to map.\n if (resolvedConfig.build.ssr) return;\n\n // Don't generate during the SSG child build (it's an SSR build)\n if (process.env.__VITE_EMBER_SSG_CHILD__) return;\n\n const base = resolvedConfig.base ?? '/';\n const root = resolvedConfig.root;\n const manifest = buildCssManifest(bundle, base, root);\n\n // Only emit the manifest if there are dynamic entries with CSS.\n // Apps without lazy-loaded CSS don't need this file.\n if (Object.keys(manifest).length === 0) return;\n\n this.emitFile({\n type: 'asset',\n fileName: CSS_MANIFEST_FILENAME,\n source: JSON.stringify(manifest, null, 2),\n });\n },\n\n async closeBundle() {\n // Only write package.json for SSR builds\n if (!resolvedConfig.build.ssr) return;\n\n // Don't interfere with the SSG child build's temp directory\n if (process.env.__VITE_EMBER_SSG_CHILD__) return;\n\n const outDir = join(resolvedConfig.root, resolvedConfig.build.outDir);\n const targetPath = join(outDir, 'package.json');\n await mkdir(outDir, { recursive: true });\n await writeFile(\n targetPath,\n JSON.stringify({ type: 'module' }, null, 2),\n 'utf-8',\n );\n },\n };\n}\n\n// ─── SSG Plugin ──────────────────────────────────────────────────────\n\nexport interface EmberSsgPluginOptions {\n /**\n * Routes to prerender as static HTML files.\n *\n * Each entry is a route path (without leading slash).\n * 'index' produces `index.html` at the root, other routes produce\n * `<route>/index.html` (e.g., 'about' → `about/index.html`).\n *\n * @example\n * ```js\n * emberSsg({\n * routes: ['index', 'about', 'pokemon', 'pokemon/charmander'],\n * })\n * ```\n */\n routes: string[];\n\n /**\n * The SSR entry module path, relative to the project root.\n * This file must export a `createSsrApp` function.\n * @default 'app/app-ssr.ts'\n */\n ssrEntry?: string;\n\n /**\n * Enable shoebox (fetch replay) for prerendered pages.\n *\n * When true, fetch responses from route model hooks are captured during\n * prerendering and serialized into the HTML. The client calls\n * `installShoebox()` before boot to replay those responses and avoid\n * duplicate API requests.\n *\n * @default false\n */\n shoebox?: boolean;\n\n /**\n * Output directory for the client build.\n * @default 'dist'\n */\n outDir?: string;\n\n /**\n * Enable Glimmer rehydration for prerendered pages.\n *\n * When `true`, the server renders with `_renderMode: 'serialize'`,\n * annotating the DOM with Glimmer markers. The client boots with\n * `app.visit(url, { _renderMode: 'rehydrate' })` to reuse the\n * static DOM instead of replacing it.\n *\n * When `false` (default), boundary markers are emitted and the\n * client uses `cleanupSSRContent()` in the application template\n * to remove the SSR content before Ember renders fresh.\n *\n * @default false\n */\n rehydrate?: boolean;\n}\n\n/**\n * Vite plugin for Static Site Generation (SSG) of Ember applications.\n *\n * Prerenders the specified routes to static HTML files at build time.\n * Fully self-contained — only a single `vite build` is needed.\n *\n * After the client build completes, the plugin runs a second SSR build\n * via `vite.build()` to produce a bundled SSR entry module, imports it,\n * renders each route using HappyDOM, and writes the resulting HTML files\n * into the client output directory. The temporary SSR bundle is cleaned\n * up automatically.\n *\n * All dependencies are bundled into the SSR output (no externals) to\n * avoid runtime resolution failures under pnpm's strict node_modules\n * layout. See issue #4.\n *\n * @example\n * ```js\n * // vite.config.mjs\n * import { emberSsg } from '@st-h/vite-ember-ssr/vite-plugin';\n *\n * export default defineConfig({\n * plugins: [\n * ember(),\n * babel({ babelHelpers: 'runtime', extensions }),\n * emberSsg({\n * routes: ['index', 'about', 'pokemon', 'pokemon/charmander'],\n * }),\n * ],\n * });\n * ```\n */\nexport function emberSsg(options: EmberSsgPluginOptions): Plugin {\n const {\n routes,\n ssrEntry = 'app/app-ssr.ts',\n shoebox = false,\n rehydrate = false,\n } = options;\n\n // Track whether the user explicitly provided outDir\n const explicitOutDir = options.outDir;\n\n let resolvedConfig: ResolvedConfig;\n\n // Whether emberSsr is also registered — detected in config() hook\n let isCombined = false;\n\n return {\n name: 'vite-ember-ssg',\n\n config(userConfig, env): UserConfig {\n // Bundle all dependencies for SSR builds — see ssrDepsConfig().\n const ssrConfig = ssrDepsConfig(userConfig, env.command);\n\n // During the SSG child build, only set ssr config — don't touch\n // build.outDir or detect isCombined (irrelevant for child build).\n if (process.env.__VITE_EMBER_SSG_CHILD__) {\n return ssrConfig;\n }\n\n // Detect if emberSsr is also registered in this config.\n // When combined, defer build.outDir to emberSsr so that\n // prerendered files land in the SSR client directory.\n isCombined = flatPlugins(userConfig.plugins).some(\n (p) => p.name === 'vite-ember-ssr',\n );\n\n // Only set outDir when:\n // - the user explicitly passed outDir to emberSsg, OR\n // - emberSsr is NOT present (standalone SSG mode, default 'dist')\n const outDir = explicitOutDir ?? (isCombined ? undefined : 'dist');\n\n return {\n ...ssrConfig,\n ...(outDir != null ? { build: { outDir } } : {}),\n };\n },\n\n configResolved(config) {\n resolvedConfig = config;\n },\n\n transform: cjsSsrShimTransform,\n\n generateBundle(_outputOptions, bundle) {\n // When combined with emberSsr, the SSR plugin already emits\n // the CSS manifest — skip to avoid duplicate emission.\n if (isCombined) return;\n\n // Only generate the CSS manifest for client builds.\n if (resolvedConfig.build.ssr) return;\n\n // Don't generate during the SSG child build (it's an SSR build)\n if (process.env.__VITE_EMBER_SSG_CHILD__) return;\n\n const base = resolvedConfig.base ?? '/';\n const root = resolvedConfig.root;\n const manifest = buildCssManifest(bundle, base, root);\n\n if (Object.keys(manifest).length === 0) return;\n\n this.emitFile({\n type: 'asset',\n fileName: CSS_MANIFEST_FILENAME,\n source: JSON.stringify(manifest, null, 2),\n });\n },\n\n async closeBundle() {\n // Don't prerender during SSR builds (if the user also has emberSsr)\n if (resolvedConfig.build.ssr) return;\n\n // Prevent recursive prerendering when the child build\n // loads the same config file and re-registers this plugin.\n if (process.env.__VITE_EMBER_SSG_CHILD__) return;\n\n const { build: viteBuild } = await import('vite');\n const { assembleHTML, createEmberApp } = await import('./server.js');\n\n const root = resolvedConfig.root;\n const clientDir = join(root, resolvedConfig.build.outDir);\n const ssrOutDir = join(root, '.ssg-tmp');\n\n console.log('\\n[vite-ember-ssg] Prerendering routes...');\n\n // Read the built client index.html as template\n const templatePath = join(clientDir, 'index.html');\n let template: string;\n try {\n template = await readFile(templatePath, 'utf-8');\n } catch (e) {\n console.error(\n `[vite-ember-ssg] Failed to read template at ${templatePath}.`,\n );\n throw e;\n }\n\n // Read the CSS manifest (if it exists) so we can inject\n // lazy-loaded CSS into prerendered pages.\n let cssManifest: CssManifest | undefined;\n const cssManifestPath = join(clientDir, CSS_MANIFEST_FILENAME);\n try {\n const raw = await readFile(cssManifestPath, 'utf-8');\n cssManifest = JSON.parse(raw) as CssManifest;\n } catch {\n // No CSS manifest — app has no lazy-loaded CSS\n }\n\n // When combined with emberSsr, preserve the original index.html\n // as _template.html before prerendering overwrites it. The\n // production server reads _template.html for dynamic SSR rendering.\n if (isCombined) {\n const savedTemplatePath = join(clientDir, '_template.html');\n await copyFile(templatePath, savedTemplatePath);\n console.log(\n ` [vite-ember-ssg] Saved SSR template → ${savedTemplatePath.replace(root + '/', '')}`,\n );\n }\n\n // ── Step 1: Build the SSR bundle ────────────────────────────\n // Run vite.build() with ssr entry to produce a fully bundled\n // ESM module. This handles all CJS→ESM transforms, Babel,\n // Glimmer template compilation, etc. at build time.\n process.env.__VITE_EMBER_SSG_CHILD__ = '1';\n\n try {\n await viteBuild({\n root,\n configFile: resolvedConfig.configFile || undefined,\n logLevel: 'warn',\n build: {\n ssr: ssrEntry,\n outDir: ssrOutDir,\n target: 'node22',\n minify: false,\n sourcemap: false,\n },\n ssr: {\n // Belt-and-suspenders: the config hooks already call\n // ssrDepsConfig() for the child build, but setting it here\n // in inline config guarantees it even if the user's config\n // file doesn't register the SSR/SSG plugins for some reason.\n noExternal: [/./],\n },\n });\n } catch (e) {\n console.error('[vite-ember-ssg] SSR build failed:', e);\n throw e;\n } finally {\n delete process.env.__VITE_EMBER_SSG_CHILD__;\n }\n\n // Write package.json so Node loads the bundle as ESM\n await writeFile(\n join(ssrOutDir, 'package.json'),\n JSON.stringify({ type: 'module' }, null, 2),\n 'utf-8',\n );\n\n // ── Step 2: Import the SSR bundle and prerender ─────────────\n let successCount = 0;\n let errorCount = 0;\n\n try {\n // Determine the output filename — Vite names SSR output\n // after the entry: 'app/app-ssr.ts' → 'app-ssr.mjs'.\n // Some Vite versions using Rolldown output '.js' instead of '.mjs',\n // so we try both extensions.\n const entryBasename = ssrEntry\n .split('/')\n .pop()!\n .replace(/\\.[^.]+$/, '');\n\n let ssrBundlePath = join(ssrOutDir, `${entryBasename}.mjs`);\n try {\n await access(ssrBundlePath);\n } catch {\n ssrBundlePath = join(ssrOutDir, `${entryBasename}.js`);\n }\n const ssrBundleURL = pathToFileURL(ssrBundlePath).href;\n\n // Prerender all routes in parallel using a long-lived worker pool.\n // Workers import the SSR bundle once and reuse it across renders,\n // making per-render cost ~4ms vs ~200ms for a fresh-worker approach.\n const app = await createEmberApp(ssrBundleURL, {\n workers: cpus().length,\n });\n\n try {\n await Promise.all(\n routes.map(async (route) => {\n const url = route === 'index' ? '/' : `/${route}`;\n\n try {\n const result = await app.renderRoute(url, {\n shoebox,\n rehydrate,\n cssManifest,\n });\n\n if (result.error) {\n console.error(\n ` [vite-ember-ssg] Error rendering ${url}:\\n` +\n (result.error.stack ?? result.error.message),\n );\n errorCount++;\n return;\n }\n\n const html = assembleHTML(template, result);\n\n // 'index' → index.html (overwrite the shell)\n // 'about' → about/index.html\n // 'pokemon/charmander' → pokemon/charmander/index.html\n const outputPath =\n route === 'index'\n ? join(clientDir, 'index.html')\n : join(clientDir, route, 'index.html');\n\n await mkdir(dirname(outputPath), { recursive: true });\n await writeFile(outputPath, html, 'utf-8');\n\n console.log(\n ` [vite-ember-ssg] ${url} → ${outputPath.replace(root + '/', '')}`,\n );\n successCount++;\n } catch (e) {\n console.error(\n ` [vite-ember-ssg] Failed to prerender ${url}:\\n` +\n (e instanceof Error ? (e.stack ?? e.message) : String(e)),\n );\n errorCount++;\n }\n }),\n );\n } finally {\n await app.destroy();\n }\n } finally {\n // ── Step 3: Clean up the temporary SSR bundle ─────────────\n await rm(ssrOutDir, { recursive: true, force: true });\n }\n\n console.log(\n `[vite-ember-ssg] Done. ${successCount} pages generated` +\n (errorCount > 0 ? `, ${errorCount} errors` : '') +\n '.',\n );\n\n if (errorCount > 0 && successCount === 0) {\n throw new Error('[vite-ember-ssg] All routes failed to prerender.');\n }\n },\n };\n}\n\nexport default emberSsr;\n"],"mappings":"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"}