@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
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
|
+
import { access, copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { cpus } from "node:os";
|
|
5
|
+
//#region src/vite-plugin.ts
|
|
6
|
+
const SSR_HEAD_MARKER = "<!-- VITE_EMBER_SSR_HEAD -->";
|
|
7
|
+
const SSR_BODY_MARKER = "<!-- VITE_EMBER_SSR_BODY -->";
|
|
8
|
+
/**
|
|
9
|
+
* Name of the CSS manifest file generated during the client build.
|
|
10
|
+
* Maps dynamic entry source modules to their associated CSS asset paths.
|
|
11
|
+
*/
|
|
12
|
+
const CSS_MANIFEST_FILENAME = "css-manifest.json";
|
|
13
|
+
/**
|
|
14
|
+
* Derives an Ember route name from a source module path following
|
|
15
|
+
* Ember's conventional file layout.
|
|
16
|
+
*
|
|
17
|
+
* `app/templates/about.gts` → `about`
|
|
18
|
+
* `app/templates/blog/post.gts` → `blog.post`
|
|
19
|
+
* `app/templates/index.gts` → `index`
|
|
20
|
+
*
|
|
21
|
+
* Returns undefined if the path doesn't match the convention.
|
|
22
|
+
*/
|
|
23
|
+
function sourcePathToRouteName(facadeModuleId, root) {
|
|
24
|
+
let relativePath = facadeModuleId;
|
|
25
|
+
if (relativePath.startsWith(root)) relativePath = relativePath.slice(root.length);
|
|
26
|
+
if (relativePath.startsWith("/")) relativePath = relativePath.slice(1);
|
|
27
|
+
const match = relativePath.match(/^app\/templates\/(.+)\.(gts|gjs|hbs|ts|js)$/);
|
|
28
|
+
if (!match) return void 0;
|
|
29
|
+
return match[1].replace(/\//g, ".");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Walks the Rollup output bundle and collects CSS files associated
|
|
33
|
+
* with dynamic entry chunks. These are CSS imports that Vite extracted
|
|
34
|
+
* from code-split chunks (e.g., lazy-loaded route templates).
|
|
35
|
+
*
|
|
36
|
+
* The main entry's CSS is already linked in the HTML template by Vite,
|
|
37
|
+
* so we only collect CSS from `isDynamicEntry` chunks.
|
|
38
|
+
*
|
|
39
|
+
* When a component with CSS is shared across multiple lazy routes,
|
|
40
|
+
* Vite extracts the shared CSS into a separate chunk. We walk each
|
|
41
|
+
* dynamic entry's static `imports` graph to collect CSS from those
|
|
42
|
+
* shared chunks too, skipping the main entry chunk (whose CSS is
|
|
43
|
+
* already in the HTML template).
|
|
44
|
+
*
|
|
45
|
+
* Keys are Ember route names derived from the source file path using
|
|
46
|
+
* Ember's conventional `app/templates/` directory structure.
|
|
47
|
+
*/
|
|
48
|
+
function buildCssManifest(bundle, base, root) {
|
|
49
|
+
const manifest = {};
|
|
50
|
+
const chunksByFile = /* @__PURE__ */ new Map();
|
|
51
|
+
const mainEntryFiles = /* @__PURE__ */ new Set();
|
|
52
|
+
for (const [, output] of Object.entries(bundle)) {
|
|
53
|
+
if (output.type !== "chunk") continue;
|
|
54
|
+
const chunk = output;
|
|
55
|
+
chunksByFile.set(chunk.fileName, chunk);
|
|
56
|
+
if (chunk.isEntry && !chunk.isDynamicEntry) mainEntryFiles.add(chunk.fileName);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Recursively collect all CSS from a chunk and its static imports,
|
|
60
|
+
* excluding main entry chunks (whose CSS is already in the template).
|
|
61
|
+
*/
|
|
62
|
+
function collectCss(fileName, seen, css) {
|
|
63
|
+
if (seen.has(fileName)) return;
|
|
64
|
+
seen.add(fileName);
|
|
65
|
+
if (mainEntryFiles.has(fileName)) return;
|
|
66
|
+
const chunk = chunksByFile.get(fileName);
|
|
67
|
+
if (!chunk) return;
|
|
68
|
+
const importedCss = chunk.viteMetadata?.importedCss;
|
|
69
|
+
if (importedCss) for (const cssFile of importedCss) css.add(cssFile);
|
|
70
|
+
for (const imp of chunk.imports) collectCss(imp, seen, css);
|
|
71
|
+
}
|
|
72
|
+
for (const [, output] of Object.entries(bundle)) {
|
|
73
|
+
if (output.type !== "chunk") continue;
|
|
74
|
+
const chunk = output;
|
|
75
|
+
if (!chunk.isDynamicEntry) continue;
|
|
76
|
+
const css = /* @__PURE__ */ new Set();
|
|
77
|
+
collectCss(chunk.fileName, /* @__PURE__ */ new Set(), css);
|
|
78
|
+
if (css.size === 0) continue;
|
|
79
|
+
const routeName = chunk.facadeModuleId ? sourcePathToRouteName(chunk.facadeModuleId, root) ?? chunk.name : chunk.name;
|
|
80
|
+
if (!routeName) continue;
|
|
81
|
+
const cssFiles = Array.from(css).map((c) => `${base}${c}`);
|
|
82
|
+
if (cssFiles.length > 0) manifest[routeName] = cssFiles;
|
|
83
|
+
}
|
|
84
|
+
return manifest;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Returns SSR config appropriate for the current Vite command.
|
|
88
|
+
*
|
|
89
|
+
* Ember's virtual packages (`@glimmer/tracking`, `@ember/*`, etc.) are
|
|
90
|
+
* provided by `ember-source` and not published as real npm packages.
|
|
91
|
+
* When Vite externalizes a dependency that transitively imports one of
|
|
92
|
+
* these virtual packages, Node's runtime module resolution fails under
|
|
93
|
+
* pnpm's strict `node_modules` layout.
|
|
94
|
+
*
|
|
95
|
+
* For both production builds and dev mode:
|
|
96
|
+
* - Clears any user-specified `ssr.external` (explicit string entries
|
|
97
|
+
* take precedence over `noExternal` patterns in Vite, so we must
|
|
98
|
+
* remove them to ensure `noExternal: [/./]` applies).
|
|
99
|
+
* - Sets `ssr: { noExternal: [/./] }` so all deps go through Vite's
|
|
100
|
+
* transform pipeline. This lets `@embroider/vite`'s resolver handle
|
|
101
|
+
* virtual Ember/Glimmer packages that don't exist outside `ember-source`
|
|
102
|
+
* under pnpm's strict `node_modules` layout.
|
|
103
|
+
*
|
|
104
|
+
* In dev mode, `ssrLoadModule` uses `SSRCompatModuleRunner` +
|
|
105
|
+
* `ESModulesEvaluator`. Without bundling, this evaluates all module code
|
|
106
|
+
* inline. CJS/UMD packages (e.g. `@warp-drive/utilities/string`,
|
|
107
|
+
* `json-to-ast`) reference `module`, `exports`, or `global` which are not
|
|
108
|
+
* available in the evaluator's context.
|
|
109
|
+
*
|
|
110
|
+
* The `cjsSsrShimTransform` hook (applied by `emberSsr()` and `emberSsg()`)
|
|
111
|
+
* intercepts those files before they reach `ssrTransform` and wraps them
|
|
112
|
+
* with a lightweight CommonJS shim, providing the missing `module`,
|
|
113
|
+
* `exports`, and `global` bindings.
|
|
114
|
+
*
|
|
115
|
+
* See: https://github.com/evoactivity/vite-ember-ssr/issues/4
|
|
116
|
+
*/
|
|
117
|
+
function ssrDepsConfig(userConfig, _command) {
|
|
118
|
+
if (userConfig.ssr) delete userConfig.ssr.external;
|
|
119
|
+
return { ssr: { noExternal: [/./] } };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Returns a Vite `transform` hook that wraps CJS/UMD modules encountered
|
|
123
|
+
* during SSR transforms.
|
|
124
|
+
*
|
|
125
|
+
* When `noExternal: [/./]` is set, every dependency goes through Vite's
|
|
126
|
+
* `ssrTransform` → `ESModulesEvaluator` pipeline. CJS/UMD files that use
|
|
127
|
+
* `module`, `exports`, or `global` fail because those globals are not
|
|
128
|
+
* available inside `ESModulesEvaluator`'s `AsyncFunction` context.
|
|
129
|
+
*
|
|
130
|
+
* This transform detects CJS/UMD content (no top-level `import`/`export`
|
|
131
|
+
* statements, but contains `exports.xxx` or `module.exports`) and wraps
|
|
132
|
+
* the code so that:
|
|
133
|
+
* 1. `module`, `exports`, and `global` are available as local variables.
|
|
134
|
+
* 2. The module's exports are re-exported as the ES default export.
|
|
135
|
+
*
|
|
136
|
+
* The heuristic is intentionally simple and conservative — it only fires
|
|
137
|
+
* on files that have no ESM syntax at all, which covers the CJS/UMD
|
|
138
|
+
* packages that appear in the Ember + WarpDrive dependency tree without
|
|
139
|
+
* misidentifying genuine ESM files.
|
|
140
|
+
*/
|
|
141
|
+
function cjsSsrShimTransform(code, _id, options) {
|
|
142
|
+
if (!options?.ssr) return null;
|
|
143
|
+
if (/^(?:import\s|export\s|export\{|export default)/m.test(code)) return null;
|
|
144
|
+
if (!/\bexports\s*[.[=]|\bmodule\s*\.\s*exports\b/.test(code)) return null;
|
|
145
|
+
return {
|
|
146
|
+
code: `\
|
|
147
|
+
const __cjs_module__ = { exports: {} };
|
|
148
|
+
const __cjs_exports__ = __cjs_module__.exports;
|
|
149
|
+
const __cjs_global__ = typeof globalThis !== 'undefined' ? globalThis : typeof global !== 'undefined' ? global : {};
|
|
150
|
+
(function(module, exports, global) {
|
|
151
|
+
${code}
|
|
152
|
+
})(__cjs_module__, __cjs_exports__, __cjs_global__);
|
|
153
|
+
export default __cjs_module__.exports;
|
|
154
|
+
`,
|
|
155
|
+
map: null
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Flatten and filter a Vite plugins array, which may contain nested arrays,
|
|
160
|
+
* falsy values, and Promise-wrapped entries.
|
|
161
|
+
*/
|
|
162
|
+
function flatPlugins(plugins) {
|
|
163
|
+
if (!plugins) return [];
|
|
164
|
+
return plugins.flat(Infinity).filter((p) => p != null && typeof p === "object" && "name" in p);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Vite plugin that configures SSR support for Ember applications.
|
|
168
|
+
*
|
|
169
|
+
* Handles all SSR-related Vite configuration automatically:
|
|
170
|
+
*
|
|
171
|
+
* - Bundles all dependencies into SSR builds (`ssr.noExternal: [/./]`)
|
|
172
|
+
* to avoid runtime resolution failures under pnpm's strict
|
|
173
|
+
* node_modules layout (see issue #4)
|
|
174
|
+
* - Sets build defaults: `dist/client` for client builds,
|
|
175
|
+
* `dist/server` with `target: 'node22'` for SSR builds
|
|
176
|
+
* - Writes a `package.json` with `"type": "module"` to the SSR
|
|
177
|
+
* build output directory (needed for Node ESM compatibility)
|
|
178
|
+
*/
|
|
179
|
+
function emberSsr(options = {}) {
|
|
180
|
+
let resolvedConfig;
|
|
181
|
+
return {
|
|
182
|
+
name: "vite-ember-ssr",
|
|
183
|
+
config(userConfig, env) {
|
|
184
|
+
const ssrConfig = ssrDepsConfig(userConfig, env.command);
|
|
185
|
+
if (process.env.__VITE_EMBER_SSG_CHILD__) return ssrConfig;
|
|
186
|
+
if (env.isSsrBuild) return {
|
|
187
|
+
...ssrConfig,
|
|
188
|
+
build: {
|
|
189
|
+
outDir: options.serverOutDir ?? "dist/server",
|
|
190
|
+
target: "node22",
|
|
191
|
+
sourcemap: true,
|
|
192
|
+
minify: false
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
return {
|
|
196
|
+
...ssrConfig,
|
|
197
|
+
build: { outDir: options.clientOutDir ?? "dist/client" }
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
configResolved(config) {
|
|
201
|
+
resolvedConfig = config;
|
|
202
|
+
},
|
|
203
|
+
transform: cjsSsrShimTransform,
|
|
204
|
+
generateBundle(_outputOptions, bundle) {
|
|
205
|
+
if (resolvedConfig.build.ssr) return;
|
|
206
|
+
if (process.env.__VITE_EMBER_SSG_CHILD__) return;
|
|
207
|
+
const base = resolvedConfig.base ?? "/";
|
|
208
|
+
const root = resolvedConfig.root;
|
|
209
|
+
const manifest = buildCssManifest(bundle, base, root);
|
|
210
|
+
if (Object.keys(manifest).length === 0) return;
|
|
211
|
+
this.emitFile({
|
|
212
|
+
type: "asset",
|
|
213
|
+
fileName: CSS_MANIFEST_FILENAME,
|
|
214
|
+
source: JSON.stringify(manifest, null, 2)
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
async closeBundle() {
|
|
218
|
+
if (!resolvedConfig.build.ssr) return;
|
|
219
|
+
if (process.env.__VITE_EMBER_SSG_CHILD__) return;
|
|
220
|
+
const outDir = join(resolvedConfig.root, resolvedConfig.build.outDir);
|
|
221
|
+
const targetPath = join(outDir, "package.json");
|
|
222
|
+
await mkdir(outDir, { recursive: true });
|
|
223
|
+
await writeFile(targetPath, JSON.stringify({ type: "module" }, null, 2), "utf-8");
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Vite plugin for Static Site Generation (SSG) of Ember applications.
|
|
229
|
+
*
|
|
230
|
+
* Prerenders the specified routes to static HTML files at build time.
|
|
231
|
+
* Fully self-contained — only a single `vite build` is needed.
|
|
232
|
+
*
|
|
233
|
+
* After the client build completes, the plugin runs a second SSR build
|
|
234
|
+
* via `vite.build()` to produce a bundled SSR entry module, imports it,
|
|
235
|
+
* renders each route using HappyDOM, and writes the resulting HTML files
|
|
236
|
+
* into the client output directory. The temporary SSR bundle is cleaned
|
|
237
|
+
* up automatically.
|
|
238
|
+
*
|
|
239
|
+
* All dependencies are bundled into the SSR output (no externals) to
|
|
240
|
+
* avoid runtime resolution failures under pnpm's strict node_modules
|
|
241
|
+
* layout. See issue #4.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```js
|
|
245
|
+
* // vite.config.mjs
|
|
246
|
+
* import { emberSsg } from '@st-h/vite-ember-ssr/vite-plugin';
|
|
247
|
+
*
|
|
248
|
+
* export default defineConfig({
|
|
249
|
+
* plugins: [
|
|
250
|
+
* ember(),
|
|
251
|
+
* babel({ babelHelpers: 'runtime', extensions }),
|
|
252
|
+
* emberSsg({
|
|
253
|
+
* routes: ['index', 'about', 'pokemon', 'pokemon/charmander'],
|
|
254
|
+
* }),
|
|
255
|
+
* ],
|
|
256
|
+
* });
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
function emberSsg(options) {
|
|
260
|
+
const { routes, ssrEntry = "app/app-ssr.ts", shoebox = false, rehydrate = false } = options;
|
|
261
|
+
const explicitOutDir = options.outDir;
|
|
262
|
+
let resolvedConfig;
|
|
263
|
+
let isCombined = false;
|
|
264
|
+
return {
|
|
265
|
+
name: "vite-ember-ssg",
|
|
266
|
+
config(userConfig, env) {
|
|
267
|
+
const ssrConfig = ssrDepsConfig(userConfig, env.command);
|
|
268
|
+
if (process.env.__VITE_EMBER_SSG_CHILD__) return ssrConfig;
|
|
269
|
+
isCombined = flatPlugins(userConfig.plugins).some((p) => p.name === "vite-ember-ssr");
|
|
270
|
+
const outDir = explicitOutDir ?? (isCombined ? void 0 : "dist");
|
|
271
|
+
return {
|
|
272
|
+
...ssrConfig,
|
|
273
|
+
...outDir != null ? { build: { outDir } } : {}
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
configResolved(config) {
|
|
277
|
+
resolvedConfig = config;
|
|
278
|
+
},
|
|
279
|
+
transform: cjsSsrShimTransform,
|
|
280
|
+
generateBundle(_outputOptions, bundle) {
|
|
281
|
+
if (isCombined) return;
|
|
282
|
+
if (resolvedConfig.build.ssr) return;
|
|
283
|
+
if (process.env.__VITE_EMBER_SSG_CHILD__) return;
|
|
284
|
+
const base = resolvedConfig.base ?? "/";
|
|
285
|
+
const root = resolvedConfig.root;
|
|
286
|
+
const manifest = buildCssManifest(bundle, base, root);
|
|
287
|
+
if (Object.keys(manifest).length === 0) return;
|
|
288
|
+
this.emitFile({
|
|
289
|
+
type: "asset",
|
|
290
|
+
fileName: CSS_MANIFEST_FILENAME,
|
|
291
|
+
source: JSON.stringify(manifest, null, 2)
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
async closeBundle() {
|
|
295
|
+
if (resolvedConfig.build.ssr) return;
|
|
296
|
+
if (process.env.__VITE_EMBER_SSG_CHILD__) return;
|
|
297
|
+
const { build: viteBuild } = await import("vite");
|
|
298
|
+
const { assembleHTML, createEmberApp } = await import("./server.js");
|
|
299
|
+
const root = resolvedConfig.root;
|
|
300
|
+
const clientDir = join(root, resolvedConfig.build.outDir);
|
|
301
|
+
const ssrOutDir = join(root, ".ssg-tmp");
|
|
302
|
+
console.log("\n[vite-ember-ssg] Prerendering routes...");
|
|
303
|
+
const templatePath = join(clientDir, "index.html");
|
|
304
|
+
let template;
|
|
305
|
+
try {
|
|
306
|
+
template = await readFile(templatePath, "utf-8");
|
|
307
|
+
} catch (e) {
|
|
308
|
+
console.error(`[vite-ember-ssg] Failed to read template at ${templatePath}.`);
|
|
309
|
+
throw e;
|
|
310
|
+
}
|
|
311
|
+
let cssManifest;
|
|
312
|
+
const cssManifestPath = join(clientDir, CSS_MANIFEST_FILENAME);
|
|
313
|
+
try {
|
|
314
|
+
const raw = await readFile(cssManifestPath, "utf-8");
|
|
315
|
+
cssManifest = JSON.parse(raw);
|
|
316
|
+
} catch {}
|
|
317
|
+
if (isCombined) {
|
|
318
|
+
const savedTemplatePath = join(clientDir, "_template.html");
|
|
319
|
+
await copyFile(templatePath, savedTemplatePath);
|
|
320
|
+
console.log(` [vite-ember-ssg] Saved SSR template → ${savedTemplatePath.replace(root + "/", "")}`);
|
|
321
|
+
}
|
|
322
|
+
process.env.__VITE_EMBER_SSG_CHILD__ = "1";
|
|
323
|
+
try {
|
|
324
|
+
await viteBuild({
|
|
325
|
+
root,
|
|
326
|
+
configFile: resolvedConfig.configFile || void 0,
|
|
327
|
+
logLevel: "warn",
|
|
328
|
+
build: {
|
|
329
|
+
ssr: ssrEntry,
|
|
330
|
+
outDir: ssrOutDir,
|
|
331
|
+
target: "node22",
|
|
332
|
+
minify: false,
|
|
333
|
+
sourcemap: false
|
|
334
|
+
},
|
|
335
|
+
ssr: { noExternal: [/./] }
|
|
336
|
+
});
|
|
337
|
+
} catch (e) {
|
|
338
|
+
console.error("[vite-ember-ssg] SSR build failed:", e);
|
|
339
|
+
throw e;
|
|
340
|
+
} finally {
|
|
341
|
+
delete process.env.__VITE_EMBER_SSG_CHILD__;
|
|
342
|
+
}
|
|
343
|
+
await writeFile(join(ssrOutDir, "package.json"), JSON.stringify({ type: "module" }, null, 2), "utf-8");
|
|
344
|
+
let successCount = 0;
|
|
345
|
+
let errorCount = 0;
|
|
346
|
+
try {
|
|
347
|
+
const entryBasename = ssrEntry.split("/").pop().replace(/\.[^.]+$/, "");
|
|
348
|
+
let ssrBundlePath = join(ssrOutDir, `${entryBasename}.mjs`);
|
|
349
|
+
try {
|
|
350
|
+
await access(ssrBundlePath);
|
|
351
|
+
} catch {
|
|
352
|
+
ssrBundlePath = join(ssrOutDir, `${entryBasename}.js`);
|
|
353
|
+
}
|
|
354
|
+
const ssrBundleURL = pathToFileURL(ssrBundlePath).href;
|
|
355
|
+
const app = await createEmberApp(ssrBundleURL, { workers: cpus().length });
|
|
356
|
+
try {
|
|
357
|
+
await Promise.all(routes.map(async (route) => {
|
|
358
|
+
const url = route === "index" ? "/" : `/${route}`;
|
|
359
|
+
try {
|
|
360
|
+
const result = await app.renderRoute(url, {
|
|
361
|
+
shoebox,
|
|
362
|
+
rehydrate,
|
|
363
|
+
cssManifest
|
|
364
|
+
});
|
|
365
|
+
if (result.error) {
|
|
366
|
+
console.error(` [vite-ember-ssg] Error rendering ${url}:\n` + (result.error.stack ?? result.error.message));
|
|
367
|
+
errorCount++;
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const html = assembleHTML(template, result);
|
|
371
|
+
const outputPath = route === "index" ? join(clientDir, "index.html") : join(clientDir, route, "index.html");
|
|
372
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
373
|
+
await writeFile(outputPath, html, "utf-8");
|
|
374
|
+
console.log(` [vite-ember-ssg] ${url} → ${outputPath.replace(root + "/", "")}`);
|
|
375
|
+
successCount++;
|
|
376
|
+
} catch (e) {
|
|
377
|
+
console.error(` [vite-ember-ssg] Failed to prerender ${url}:\n` + (e instanceof Error ? e.stack ?? e.message : String(e)));
|
|
378
|
+
errorCount++;
|
|
379
|
+
}
|
|
380
|
+
}));
|
|
381
|
+
} finally {
|
|
382
|
+
await app.destroy();
|
|
383
|
+
}
|
|
384
|
+
} finally {
|
|
385
|
+
await rm(ssrOutDir, {
|
|
386
|
+
recursive: true,
|
|
387
|
+
force: true
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
console.log(`[vite-ember-ssg] Done. ${successCount} pages generated` + (errorCount > 0 ? `, ${errorCount} errors` : "") + ".");
|
|
391
|
+
if (errorCount > 0 && successCount === 0) throw new Error("[vite-ember-ssg] All routes failed to prerender.");
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
export { emberSsr as a, emberSsg as i, SSR_BODY_MARKER as n, SSR_HEAD_MARKER as r, CSS_MANIFEST_FILENAME as t };
|
|
397
|
+
|
|
398
|
+
//# sourceMappingURL=vite-plugin-D-W5WQWe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as SSR_BODY_MARKER, c as emberSsr, i as EmberSsrPluginOptions, n as CssManifest, o as SSR_HEAD_MARKER, r as EmberSsgPluginOptions, s as emberSsg, t as CSS_MANIFEST_FILENAME } from "./vite-plugin-CQou_tr5.js";
|
|
2
|
+
export { CSS_MANIFEST_FILENAME, CssManifest, EmberSsgPluginOptions, EmberSsrPluginOptions, SSR_BODY_MARKER, SSR_HEAD_MARKER, emberSsr as default, emberSsr, emberSsg };
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { n as CssManifest } from "./vite-plugin-CQou_tr5.js";
|
|
2
|
+
|
|
3
|
+
//#region src/worker.d.ts
|
|
4
|
+
interface WorkerRenderOptions {
|
|
5
|
+
ssrBundlePath: string;
|
|
6
|
+
url: string;
|
|
7
|
+
shoebox: boolean;
|
|
8
|
+
rehydrate: boolean;
|
|
9
|
+
cssManifest: CssManifest | null;
|
|
10
|
+
headers: Record<string, string> | null;
|
|
11
|
+
}
|
|
12
|
+
interface WorkerRenderResult {
|
|
13
|
+
head: string;
|
|
14
|
+
body: string;
|
|
15
|
+
bodyAttrs: Record<string, string>;
|
|
16
|
+
statusCode: number;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
declare function render(options: WorkerRenderOptions): Promise<WorkerRenderResult>;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { WorkerRenderOptions, WorkerRenderResult, render as default };
|
|
22
|
+
//# sourceMappingURL=worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.d.ts","names":[],"sources":["../src/worker.ts"],"mappings":";;;UA6BiB,mBAAA;EACf,aAAA;EACA,GAAA;EACA,OAAA;EACA,SAAA;EACA,WAAA,EAAa,WAAA;EACb,OAAA,EAAS,MAAA;AAAA;AAAA,UAGM,kBAAA;EACf,IAAA;EACA,IAAA;EACA,SAAA,EAAW,MAAA;EACX,UAAA;EACA,KAAA;AAAA;AAAA,iBAoM4B,MAAA,CAC5B,OAAA,EAAS,mBAAA,GACR,OAAA,CAAQ,kBAAA"}
|