@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/README.md +257 -416
- package/dist/client.d.ts +42 -51
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +39 -62
- package/dist/client.js.map +1 -1
- package/dist/fetch-middleware-DPLxOLL6.js +98 -0
- package/dist/fetch-middleware-DPLxOLL6.js.map +1 -0
- package/dist/server-DJRlVUcm.d.ts +260 -0
- package/dist/server-DJRlVUcm.d.ts.map +1 -0
- package/dist/server.d.ts +3 -236
- package/dist/server.js +46 -42
- package/dist/server.js.map +1 -1
- package/dist/{vite-plugin-D-W5WQWe.js → vite-plugin-9BSJgEL9.js} +3 -4
- package/dist/vite-plugin-9BSJgEL9.js.map +1 -0
- package/dist/{vite-plugin-CQou_tr5.d.ts → vite-plugin-Dl5DbheW.d.ts} +2 -17
- package/dist/vite-plugin-Dl5DbheW.d.ts.map +1 -0
- package/dist/vite-plugin.d.ts +1 -1
- package/dist/vite-plugin.js +1 -1
- package/dist/worker.d.ts +4 -3
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +69 -54
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +64 -73
- package/src/dev.ts +91 -61
- package/src/fetch-middleware.ts +166 -0
- package/src/server.ts +48 -23
- package/src/vite-plugin.ts +2 -24
- package/src/worker.ts +153 -105
- package/dist/server.d.ts.map +0 -1
- package/dist/vite-plugin-CQou_tr5.d.ts.map +0 -1
- package/dist/vite-plugin-D-W5WQWe.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,67 +1,66 @@
|
|
|
1
1
|
# vite-ember-ssr
|
|
2
2
|
|
|
3
|
+
Vite plugin and runtime for server side rendering (SSR) and static site generation (SSG) of Ember.js applications. Renders Ember in Node.js using [HappyDOM](https://github.com/capricorn86/happy-dom), with no FastBoot and no VM sandbox.
|
|
4
|
+
|
|
3
5
|
> [!WARNING]
|
|
4
|
-
> **EXPERIMENTAL
|
|
6
|
+
> **EXPERIMENTAL.** This project is in early development and targets **compatless** Ember apps only (no `@embroider/compat`, no `ember-cli-build.js`, no `classicEmberSupport()`). APIs will change. Do not use in production.
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
## Requirements
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
- An Ember app built with Embroider in compatless mode (no `@embroider/compat`, no `ember-cli-build.js`, no `classicEmberSupport()`).
|
|
11
|
+
- Vite 6+
|
|
12
|
+
- Node 22+
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
- **SSG** (`emberSsg`) — prerenders specified routes to static HTML files at build time. A single `vite build` produces deploy-ready files. No server required.
|
|
14
|
+
## Choose a mode
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
This library exposes two Vite plugins. They can be used independently or together.
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
- **`emberSsg`** prerenders a known list of routes to static HTML at build time. A single `vite build` produces deploy ready files. No server is required.
|
|
19
|
+
- **`emberSsr`** renders pages on every request from a Node.js server.
|
|
20
|
+
- **Combined** uses both plugins. Known routes are prerendered, everything else falls back to dynamic SSR.
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
| | SSG (`emberSsg`) | SSR (`emberSsr`) |
|
|
23
|
+
| -------------- | ---------------------------- | ------------------------------------------------- |
|
|
24
|
+
| Rendering | Build time | Request time |
|
|
25
|
+
| Server | Not required | Node.js server required |
|
|
26
|
+
| Build command | `vite build` | `vite build` + `vite build --ssr` |
|
|
27
|
+
| Deploy | Any static host | Node.js hosting |
|
|
28
|
+
| Dynamic routes | Must enumerate at build time | Any URL handled at runtime |
|
|
29
|
+
| Data freshness | Stale until next build | Fresh on every request |
|
|
30
|
+
| Best for | Marketing sites, docs, blogs | Apps with frequently changing or per request data |
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
If you are unsure, start with SSG. It has the fewest moving parts and the Quick Start below uses it.
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
- Your app's `config/environment` must be a direct ES module import (i.e. `import config from './config/environment.ts'`). Do not rely on `<meta>` config injection or `@embroider/config-meta-loader`.
|
|
26
|
-
- Vite 6+
|
|
27
|
-
- Node 22+
|
|
28
|
-
|
|
29
|
-
## Installation
|
|
34
|
+
## Install
|
|
30
35
|
|
|
31
36
|
```sh
|
|
32
37
|
pnpm add -D vite-ember-ssr
|
|
33
38
|
```
|
|
34
39
|
|
|
35
|
-
##
|
|
36
|
-
|
|
37
|
-
### SSR (Server-Side Rendering)
|
|
40
|
+
## Quick start (SSG)
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
This is the smallest end to end setup. It prerenders `index` and `about` to static HTML and ships them with the client bundle.
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
### `vite.config.mjs`
|
|
42
45
|
|
|
43
46
|
```js
|
|
44
|
-
// vite.config.mjs
|
|
45
47
|
import { defineConfig } from 'vite';
|
|
46
48
|
import { extensions, ember } from '@embroider/vite';
|
|
47
49
|
import { babel } from '@rollup/plugin-babel';
|
|
48
|
-
import {
|
|
50
|
+
import { emberSsg } from 'vite-ember-ssr/vite-plugin';
|
|
49
51
|
|
|
50
52
|
export default defineConfig({
|
|
51
53
|
plugins: [
|
|
52
54
|
ember(),
|
|
53
|
-
babel({
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
babel({ babelHelpers: 'runtime', extensions }),
|
|
56
|
+
emberSsg({
|
|
57
|
+
routes: ['index', 'about'],
|
|
56
58
|
}),
|
|
57
|
-
emberSsr(),
|
|
58
59
|
],
|
|
59
60
|
});
|
|
60
61
|
```
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
Add SSR markers to `index.html`:
|
|
63
|
+
### `index.html`
|
|
65
64
|
|
|
66
65
|
```html
|
|
67
66
|
<!DOCTYPE html>
|
|
@@ -77,15 +76,18 @@ Add SSR markers to `index.html`:
|
|
|
77
76
|
</html>
|
|
78
77
|
```
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Export a factory that creates the Ember app with `autoboot: false`:
|
|
79
|
+
### `app/app-ssr.ts`
|
|
83
80
|
|
|
84
81
|
```ts
|
|
85
82
|
import EmberApp from 'ember-strict-application-resolver';
|
|
86
83
|
import config from './config/environment.ts';
|
|
87
84
|
import Router from './router.ts';
|
|
88
85
|
|
|
86
|
+
// Re-exporting `settled` lets the renderer await Ember's run loop, pending
|
|
87
|
+
// timers, and any registered test-waiters before capturing the DOM. See
|
|
88
|
+
// the Concepts section below for details.
|
|
89
|
+
export { settled } from '@ember/test-helpers';
|
|
90
|
+
|
|
89
91
|
class App extends EmberApp {
|
|
90
92
|
modules = {
|
|
91
93
|
'./router': Router,
|
|
@@ -99,30 +101,22 @@ export function createSsrApp() {
|
|
|
99
101
|
}
|
|
100
102
|
```
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
#### 4. Client entry (`app/entry.ts`)
|
|
104
|
+
### `app/entry.ts`
|
|
105
105
|
|
|
106
106
|
```ts
|
|
107
107
|
import Application from './app.ts';
|
|
108
108
|
import config from './config/environment.ts';
|
|
109
|
+
import { bootRehydrated } from 'vite-ember-ssr/client';
|
|
109
110
|
|
|
110
|
-
Application
|
|
111
|
+
bootRehydrated(Application, config);
|
|
111
112
|
```
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
Call `cleanupSSRContent` from the application template so the SSR-rendered DOM is removed at the moment Ember renders, avoiding a flash of no content:
|
|
114
|
+
### `app/templates/application.gts`
|
|
116
115
|
|
|
117
116
|
```gts
|
|
118
|
-
import { pageTitle } from 'ember-page-title';
|
|
119
117
|
import { LinkTo } from '@ember/routing';
|
|
120
|
-
import { cleanupSSRContent } from 'vite-ember-ssr/client';
|
|
121
118
|
|
|
122
119
|
<template>
|
|
123
|
-
{{pageTitle "MyApp"}}
|
|
124
|
-
{{cleanupSSRContent}}
|
|
125
|
-
|
|
126
120
|
<nav>
|
|
127
121
|
<LinkTo @route="index">Home</LinkTo>
|
|
128
122
|
<LinkTo @route="about">About</LinkTo>
|
|
@@ -132,201 +126,185 @@ import { cleanupSSRContent } from 'vite-ember-ssr/client';
|
|
|
132
126
|
</template>
|
|
133
127
|
```
|
|
134
128
|
|
|
135
|
-
|
|
129
|
+
### Build
|
|
136
130
|
|
|
137
131
|
```sh
|
|
138
|
-
vite build
|
|
139
|
-
|
|
132
|
+
vite build
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Output:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
dist/
|
|
139
|
+
index.html prerendered index route
|
|
140
|
+
about/index.html prerendered about route
|
|
141
|
+
assets/
|
|
142
|
+
main-abc123.js
|
|
143
|
+
main-abc123.css
|
|
140
144
|
```
|
|
141
145
|
|
|
142
|
-
|
|
146
|
+
Serve `dist/` from any static host. That is the whole pipeline.
|
|
143
147
|
|
|
144
|
-
|
|
148
|
+
## Concepts
|
|
145
149
|
|
|
146
|
-
|
|
147
|
-
import { createEmberApp, assembleHTML } from 'vite-ember-ssr/server';
|
|
150
|
+
The Quick Start uses four building blocks. Every mode in this library uses the same four. SSR and Combined sections below only show what differs from the Quick Start.
|
|
148
151
|
|
|
149
|
-
|
|
150
|
-
const emberApp = await createEmberApp('./dist/server/app-ssr.mjs');
|
|
152
|
+
### How rendering works
|
|
151
153
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// });
|
|
154
|
+
- A per request HappyDOM `Window` provides a browser like environment in Node. Ember globals are swapped in for the duration of one render.
|
|
155
|
+
- `Application.visit(url, { _renderMode: 'serialize' })` drives the render cycle on the server, the same way FastBoot does. `_renderMode: 'serialize'` annotates the DOM with Glimmer rehydration markers.
|
|
156
|
+
- After Ember settles, the resulting `<head>` and `<body>` content are extracted and inserted into your HTML template at the SSR markers.
|
|
157
|
+
- On the client, `bootRehydrated` calls `app.visit(url, { _renderMode: 'rehydrate' })` and Glimmer attaches to the existing DOM in place. See [Client boot](#client-boot).
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
const rendered = await emberApp.renderRoute(request.url);
|
|
160
|
-
const html = assembleHTML(template, rendered);
|
|
161
|
-
// rendered.statusCode, rendered.error also available
|
|
159
|
+
### SSR markers in `index.html`
|
|
162
160
|
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
Two HTML comments tell the renderer where to inject content:
|
|
162
|
+
|
|
163
|
+
```html
|
|
164
|
+
<!-- VITE_EMBER_SSR_HEAD -->
|
|
165
|
+
<!-- VITE_EMBER_SSR_BODY -->
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The first is replaced with anything Ember rendered into `<head>` (page title, meta tags, etc.). The second is replaced with the rendered application body.
|
|
169
|
+
|
|
170
|
+
### SSR entry (`app/app-ssr.ts`)
|
|
171
|
+
|
|
172
|
+
This module exports a factory the renderer calls once per worker. The factory must:
|
|
173
|
+
|
|
174
|
+
- Create an `EmberApp` subclass that registers your routes, templates, and services via `import.meta.glob({ eager: true })`.
|
|
175
|
+
- Pass `autoboot: false` so the app does not try to boot itself when the module loads.
|
|
176
|
+
|
|
177
|
+
The exported function (named `createSsrApp` by convention) is wrapped in an async function inside the renderer that imports the SSR bundle on demand.
|
|
178
|
+
|
|
179
|
+
#### Settling
|
|
180
|
+
|
|
181
|
+
If your `app-ssr.ts` re-exports `settled` from `@ember/test-helpers`, the renderer awaits it after every `app.visit()`:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
export { settled } from '@ember/test-helpers';
|
|
165
185
|
```
|
|
166
186
|
|
|
167
|
-
|
|
187
|
+
`settled()` waits for Ember's run loop, pending Backburner timers, in-flight AJAX, route transitions, and any registered `@ember/test-waiters` (used by WarpDrive, ember-concurrency, etc.) to drain before the DOM is captured.
|
|
188
|
+
|
|
189
|
+
Without this export, the renderer falls back to a single microtask drain (`await new Promise(r => setTimeout(r, 0))`), which catches synchronous reactivity but misses async work scheduled outside the microtask queue.
|
|
190
|
+
|
|
191
|
+
The renderer races `settled()` against a configurable timeout (default 10s). If exceeded, a warning is logged and the DOM is captured anyway. See [`app.renderRoute`](#vite-ember-ssrserver) for `settledTimeout`.
|
|
192
|
+
|
|
193
|
+
### Client entry
|
|
194
|
+
|
|
195
|
+
The client entry calls `bootRehydrated(Application, config)`. This helper looks at `window.__vite_ember_ssr_rehydrate__` (set by the server) and either:
|
|
196
|
+
|
|
197
|
+
- Boots with `autoboot: false` and calls `app.visit(url, { _renderMode: 'rehydrate' })` so Glimmer attaches to the server rendered DOM, or
|
|
198
|
+
- Falls back to `Application.create(config.APP)` for pages that were not server rendered.
|
|
168
199
|
|
|
169
|
-
|
|
200
|
+
See [Client boot](#client-boot) for the full helper behaviour.
|
|
170
201
|
|
|
171
|
-
|
|
202
|
+
## SSR mode
|
|
172
203
|
|
|
173
|
-
|
|
204
|
+
Use SSR when you need fresh data on every request, or when routes cannot be enumerated at build time.
|
|
174
205
|
|
|
175
|
-
|
|
176
|
-
2. Runs a second SSR build internally to produce a temporary server bundle
|
|
177
|
-
3. Renders each specified route using HappyDOM
|
|
178
|
-
4. Writes the resulting HTML files into the output directory
|
|
179
|
-
5. Cleans up the temporary bundle
|
|
206
|
+
### Vite config
|
|
180
207
|
|
|
181
|
-
|
|
208
|
+
Replace `emberSsg` with `emberSsr`:
|
|
182
209
|
|
|
183
210
|
```js
|
|
184
|
-
// vite.config.mjs
|
|
185
211
|
import { defineConfig } from 'vite';
|
|
186
212
|
import { extensions, ember } from '@embroider/vite';
|
|
187
213
|
import { babel } from '@rollup/plugin-babel';
|
|
188
|
-
import {
|
|
214
|
+
import { emberSsr } from 'vite-ember-ssr/vite-plugin';
|
|
189
215
|
|
|
190
216
|
export default defineConfig({
|
|
191
217
|
plugins: [
|
|
192
218
|
ember(),
|
|
193
219
|
babel({ babelHelpers: 'runtime', extensions }),
|
|
194
|
-
|
|
195
|
-
routes: ['index', 'about', 'contact', 'pokemon/charmander'],
|
|
196
|
-
}),
|
|
220
|
+
emberSsr(),
|
|
197
221
|
],
|
|
198
222
|
});
|
|
199
223
|
```
|
|
200
224
|
|
|
201
|
-
|
|
225
|
+
### Build
|
|
202
226
|
|
|
203
|
-
|
|
227
|
+
SSR needs both a client build and a server build:
|
|
204
228
|
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
<head>
|
|
209
|
-
<meta charset="utf-8" />
|
|
210
|
-
<!-- VITE_EMBER_SSR_HEAD -->
|
|
211
|
-
</head>
|
|
212
|
-
<body>
|
|
213
|
-
<!-- VITE_EMBER_SSR_BODY -->
|
|
214
|
-
<script type="module" src="/app/entry.ts"></script>
|
|
215
|
-
</body>
|
|
216
|
-
</html>
|
|
229
|
+
```sh
|
|
230
|
+
vite build # client → dist/client
|
|
231
|
+
vite build --ssr app/app-ssr.ts # server → dist/server
|
|
217
232
|
```
|
|
218
233
|
|
|
219
|
-
|
|
234
|
+
### Server integration
|
|
220
235
|
|
|
221
|
-
|
|
236
|
+
Create the worker pool once at startup. Call `renderRoute` from your catch all handler.
|
|
222
237
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
Same as SSR — see [Client Boot Modes](#client-boot-modes) below for the two options:
|
|
238
|
+
```js
|
|
239
|
+
import { createEmberApp, assembleHTML } from 'vite-ember-ssr/server';
|
|
226
240
|
|
|
227
|
-
|
|
228
|
-
|
|
241
|
+
// Production: tinypool worker pool, SSR bundle loaded once per worker
|
|
242
|
+
const emberApp = await createEmberApp('./dist/server/app-ssr.mjs');
|
|
229
243
|
|
|
230
|
-
|
|
244
|
+
// Development: in process render via Vite's ssrLoadModule
|
|
245
|
+
// const vite = await createServer({ ... });
|
|
246
|
+
// const emberApp = await createEmberApp('app/app-ssr.ts', {
|
|
247
|
+
// dev: { ssrLoadModule: vite.ssrLoadModule.bind(vite) },
|
|
248
|
+
// });
|
|
231
249
|
|
|
232
|
-
|
|
250
|
+
const rendered = await emberApp.renderRoute(request.url);
|
|
251
|
+
const html = assembleHTML(template, rendered);
|
|
252
|
+
// rendered.statusCode and rendered.error are also available
|
|
233
253
|
|
|
234
|
-
|
|
235
|
-
|
|
254
|
+
// Shutdown:
|
|
255
|
+
await emberApp.destroy();
|
|
236
256
|
```
|
|
237
257
|
|
|
238
|
-
|
|
258
|
+
See [examples/fastify.md](https://github.com/evoactivity/vite-ember-ssr/blob/main/examples/fastify.md) for a complete Fastify server with both dev and prod modes.
|
|
239
259
|
|
|
240
|
-
|
|
241
|
-
dist/
|
|
242
|
-
index.html ← prerendered index route
|
|
243
|
-
about/index.html ← prerendered about route
|
|
244
|
-
contact/index.html ← prerendered contact route
|
|
245
|
-
pokemon/
|
|
246
|
-
charmander/index.html ← prerendered nested route
|
|
247
|
-
assets/
|
|
248
|
-
main-abc123.js ← client JS bundle
|
|
249
|
-
main-abc123.css ← client CSS bundle
|
|
250
|
-
```
|
|
260
|
+
## SSG mode
|
|
251
261
|
|
|
252
|
-
|
|
262
|
+
Quick Start covers the basic case. This section documents everything else.
|
|
253
263
|
|
|
254
|
-
|
|
264
|
+
### Route format
|
|
255
265
|
|
|
256
|
-
|
|
257
|
-
# Local preview
|
|
258
|
-
npx http-server dist
|
|
266
|
+
`routes` entries are URL paths without a leading slash. The renderer visits each URL and writes the result to disk.
|
|
259
267
|
|
|
260
|
-
|
|
261
|
-
|
|
268
|
+
- `'index'` is special cased and produces `dist/index.html`.
|
|
269
|
+
- Anything else produces `<path>/index.html`, so `'about'` becomes `dist/about/index.html` and `'pokemon/charmander'` becomes `dist/pokemon/charmander/index.html`.
|
|
262
270
|
|
|
263
|
-
|
|
271
|
+
### What `vite build` does
|
|
264
272
|
|
|
265
|
-
|
|
273
|
+
A single `vite build`:
|
|
266
274
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
275
|
+
1. Builds the client assets (JS, CSS, HTML shell).
|
|
276
|
+
2. Runs a second SSR build internally to produce a temporary server bundle.
|
|
277
|
+
3. Renders each route in `routes` using HappyDOM.
|
|
278
|
+
4. Writes the resulting HTML files into the output directory.
|
|
279
|
+
5. Cleans up the temporary bundle.
|
|
271
280
|
|
|
272
|
-
|
|
281
|
+
### Options
|
|
273
282
|
|
|
274
|
-
|
|
275
|
-
emberSsg({
|
|
276
|
-
// Required: routes to prerender
|
|
277
|
-
routes: ['index', 'about', 'contact'],
|
|
278
|
-
|
|
279
|
-
// SSR entry module (default: 'app/app-ssr.ts')
|
|
280
|
-
ssrEntry: 'app/app-ssr.ts',
|
|
281
|
-
|
|
282
|
-
// Enable shoebox fetch replay (default: false)
|
|
283
|
-
// When true, fetch responses from model hooks are serialized into the HTML
|
|
284
|
-
// so the client doesn't re-fetch on boot. See the Shoebox section below.
|
|
285
|
-
shoebox: false,
|
|
286
|
-
|
|
287
|
-
// Enable Glimmer rehydration (default: false)
|
|
288
|
-
// When true, prerendered HTML includes Glimmer serialization markers
|
|
289
|
-
// and a `window.__vite_ember_ssr_rehydrate__` flag. The client uses
|
|
290
|
-
// `shouldRehydrate()` to detect this and boot with
|
|
291
|
-
// `app.visit(url, { _renderMode: 'rehydrate' })` to reuse the
|
|
292
|
-
// static DOM instead of replacing it.
|
|
293
|
-
rehydrate: false,
|
|
294
|
-
|
|
295
|
-
// Output directory (default: 'dist')
|
|
296
|
-
outDir: 'dist',
|
|
297
|
-
});
|
|
298
|
-
```
|
|
283
|
+
See the [API reference](#emberssgoptions) for the full options table.
|
|
299
284
|
|
|
300
|
-
|
|
285
|
+
### Deploy
|
|
301
286
|
|
|
302
|
-
|
|
303
|
-
| ------------------ | ---------------------------- | ------------------------------------------------------------------------------- |
|
|
304
|
-
| **Rendering** | Build time | Request time |
|
|
305
|
-
| **Server** | Not required | Node.js server required |
|
|
306
|
-
| **Build command** | `vite build` | `vite build` + `vite build --ssr` |
|
|
307
|
-
| **Deploy** | Any static host | Node.js hosting |
|
|
308
|
-
| **Dynamic routes** | Must enumerate at build time | Any URL handled at runtime |
|
|
309
|
-
| **Data freshness** | Stale until next build | Fresh on every request |
|
|
310
|
-
| **Best for** | Marketing sites, docs, blogs | Apps with frequently changing content, dynamic per-request data, real-time data |
|
|
287
|
+
Serve `dist/` from any static host. No Node.js runtime required.
|
|
311
288
|
|
|
312
|
-
|
|
289
|
+
```sh
|
|
290
|
+
npx http-server dist
|
|
291
|
+
```
|
|
313
292
|
|
|
314
|
-
|
|
293
|
+
## Combined SSR + SSG
|
|
315
294
|
|
|
316
|
-
|
|
295
|
+
Use both plugins together to prerender known static routes while keeping dynamic SSR for everything else. Prerendered routes are served as static files. Other routes render on demand.
|
|
317
296
|
|
|
318
|
-
|
|
297
|
+
When `emberSsg` detects that `emberSsr` is also installed, it changes its output strategy:
|
|
319
298
|
|
|
320
|
-
1. Copies `dist/client/index.html` to `dist/client/_template.html` (preserving the SSR markers)
|
|
321
|
-
2. Prerenders each route
|
|
322
|
-
3. If `'index'` is in your routes list, `index.html` is overwritten with the prerendered index route
|
|
299
|
+
1. Copies `dist/client/index.html` to `dist/client/_template.html` (preserving the SSR markers).
|
|
300
|
+
2. Prerenders each route into `dist/client/`.
|
|
301
|
+
3. If `'index'` is in your routes list, `index.html` is overwritten with the prerendered index route.
|
|
323
302
|
|
|
324
|
-
|
|
303
|
+
The server reads `_template.html` as the SSR template for dynamic rendering.
|
|
325
304
|
|
|
326
|
-
|
|
305
|
+
### Vite config
|
|
327
306
|
|
|
328
307
|
```js
|
|
329
|
-
// vite.config.mjs
|
|
330
308
|
import { defineConfig } from 'vite';
|
|
331
309
|
import { extensions, ember } from '@embroider/vite';
|
|
332
310
|
import { babel } from '@rollup/plugin-babel';
|
|
@@ -344,7 +322,7 @@ export default defineConfig({
|
|
|
344
322
|
});
|
|
345
323
|
```
|
|
346
324
|
|
|
347
|
-
|
|
325
|
+
### Build
|
|
348
326
|
|
|
349
327
|
```sh
|
|
350
328
|
vite build # client + SSG prerender → dist/client
|
|
@@ -356,161 +334,94 @@ Output structure:
|
|
|
356
334
|
```
|
|
357
335
|
dist/
|
|
358
336
|
client/
|
|
359
|
-
_template.html
|
|
360
|
-
index.html
|
|
361
|
-
about/index.html
|
|
362
|
-
contact/index.html
|
|
337
|
+
_template.html original index.html with SSR markers (used for dynamic SSR)
|
|
338
|
+
index.html prerendered
|
|
339
|
+
about/index.html prerendered
|
|
340
|
+
contact/index.html prerendered
|
|
363
341
|
assets/
|
|
364
342
|
main-abc123.js
|
|
365
343
|
main-abc123.css
|
|
366
344
|
server/
|
|
367
|
-
app-ssr.mjs
|
|
345
|
+
app-ssr.mjs SSR server bundle
|
|
368
346
|
package.json
|
|
369
347
|
```
|
|
370
348
|
|
|
371
|
-
|
|
349
|
+
### Server integration
|
|
372
350
|
|
|
373
|
-
|
|
351
|
+
The server checks for a prerendered file first, then falls back to dynamic SSR using `_template.html`.
|
|
374
352
|
|
|
375
353
|
```js
|
|
376
354
|
import { readFile, access } from 'node:fs/promises';
|
|
377
355
|
import { createEmberApp, assembleHTML } from 'vite-ember-ssr/server';
|
|
378
356
|
|
|
379
|
-
// Load _template.html once at startup — it contains the SSR markers
|
|
380
357
|
const ssrTemplate = await readFile('dist/client/_template.html', 'utf-8');
|
|
381
|
-
|
|
382
|
-
// Create the worker pool once at startup
|
|
383
358
|
const emberApp = await createEmberApp('./dist/server/app-ssr.mjs');
|
|
384
359
|
|
|
385
|
-
// In your catch-all route handler:
|
|
386
360
|
app.get('*', async (request, reply) => {
|
|
387
361
|
const url = request.url;
|
|
388
362
|
|
|
389
|
-
// 1. Try
|
|
363
|
+
// 1. Try a prerendered file
|
|
390
364
|
const staticPath = resolveStaticFile(clientDir, url);
|
|
391
365
|
try {
|
|
392
366
|
await access(staticPath);
|
|
393
367
|
const html = await readFile(staticPath, 'utf-8');
|
|
394
368
|
return reply.code(200).type('text/html').send(html);
|
|
395
369
|
} catch {
|
|
396
|
-
// No prerendered file
|
|
370
|
+
// No prerendered file, fall through
|
|
397
371
|
}
|
|
398
372
|
|
|
399
373
|
// 2. Fall back to dynamic SSR
|
|
400
|
-
const rendered = await emberApp.renderRoute(url, {
|
|
401
|
-
shoebox: true, // opt-in: replay fetch responses on the client (see Shoebox section)
|
|
402
|
-
});
|
|
374
|
+
const rendered = await emberApp.renderRoute(url, { shoebox: true });
|
|
403
375
|
const html = assembleHTML(ssrTemplate, rendered);
|
|
404
376
|
|
|
405
377
|
return reply.code(rendered.statusCode).type('text/html').send(html);
|
|
406
378
|
});
|
|
407
379
|
```
|
|
408
380
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
### Client Boot Modes
|
|
412
|
-
|
|
413
|
-
There are two ways the client Ember app can take over from the server-rendered HTML. The server and client must agree on the mode.
|
|
381
|
+
See [examples/fastify-combined.md](https://github.com/evoactivity/vite-ember-ssr/blob/main/examples/fastify-combined.md) for a complete example.
|
|
414
382
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
The server wraps rendered content in boundary markers. On boot, `cleanupSSRContent()` removes those markers and the SSR content, then Ember renders fresh into the empty `<body>`. This is the simplest approach — SSR provides a visual shell while JS loads, then Ember replaces it.
|
|
418
|
-
|
|
419
|
-
**Server:** use default options (no `rehydrate` flag)
|
|
420
|
-
|
|
421
|
-
```js
|
|
422
|
-
const rendered = await emberApp.renderRoute(url);
|
|
423
|
-
const html = assembleHTML(template, rendered);
|
|
424
|
-
```
|
|
383
|
+
## Client boot
|
|
425
384
|
|
|
426
|
-
|
|
385
|
+
The library always renders pages with Glimmer rehydration markers. On the client, `bootRehydrated` calls `app.visit(url, { _renderMode: 'rehydrate' })` and Glimmer attaches to the server rendered DOM in place. There is no flash, no DOM tear down, and no `cleanupSSRContent` step.
|
|
427
386
|
|
|
428
387
|
```ts
|
|
429
388
|
import Application from './app.ts';
|
|
430
389
|
import config from './config/environment.ts';
|
|
390
|
+
import { bootRehydrated } from 'vite-ember-ssr/client';
|
|
431
391
|
|
|
432
|
-
Application
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
**Application template (`app/templates/application.gts`):**
|
|
436
|
-
|
|
437
|
-
```gts
|
|
438
|
-
import { cleanupSSRContent } from 'vite-ember-ssr/client';
|
|
439
|
-
|
|
440
|
-
<template>
|
|
441
|
-
{{cleanupSSRContent}}
|
|
442
|
-
{{outlet}}
|
|
443
|
-
</template>
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
Calling `cleanupSSRContent` from the template (rather than from `entry.ts` before boot) ensures the SSR content is removed at the moment Ember renders, avoiding a flash of no content.
|
|
447
|
-
|
|
448
|
-
#### Rehydrate mode
|
|
449
|
-
|
|
450
|
-
The server renders with `_renderMode: 'serialize'`, which annotates the DOM with Glimmer-specific markers. On boot, the client calls `app.visit()` with `_renderMode: 'rehydrate'`, and Glimmer walks the existing DOM and attaches its tracking/update machinery without tearing it down. This avoids the visual flash entirely — the server-rendered DOM becomes the live Ember app.
|
|
451
|
-
|
|
452
|
-
**Server:** pass `rehydrate: true`
|
|
453
|
-
|
|
454
|
-
```js
|
|
455
|
-
const rendered = await emberApp.renderRoute(url, { rehydrate: true });
|
|
456
|
-
const html = assembleHTML(template, rendered);
|
|
392
|
+
bootRehydrated(Application, config);
|
|
457
393
|
```
|
|
458
394
|
|
|
459
|
-
|
|
395
|
+
The server injects a `window.__vite_ember_ssr_rehydrate__` flag on every server rendered page. `bootRehydrated` checks for it and:
|
|
460
396
|
|
|
461
|
-
|
|
397
|
+
- If present, creates the application with `autoboot: false` and calls `app.visit(url, { _renderMode: 'rehydrate' })`. The visit URL is derived from `window.location.pathname + search` with `config.rootURL` stripped.
|
|
398
|
+
- If absent, calls `Application.create(config.APP)` for a normal boot. This matters for SSG apps where the user navigates to a route that was never prerendered, or for dev pages hit without an SSR middleware.
|
|
462
399
|
|
|
463
|
-
|
|
464
|
-
import Application from './app.ts';
|
|
465
|
-
import config from './config/environment.ts';
|
|
466
|
-
import { shouldRehydrate } from 'vite-ember-ssr/client';
|
|
467
|
-
|
|
468
|
-
if (shouldRehydrate()) {
|
|
469
|
-
const app = Application.create({ ...config.APP, autoboot: false });
|
|
470
|
-
|
|
471
|
-
const url = (window.location.pathname + window.location.search).replace(
|
|
472
|
-
config.rootURL,
|
|
473
|
-
'/',
|
|
474
|
-
);
|
|
475
|
-
|
|
476
|
-
void app.visit(url, {
|
|
477
|
-
_renderMode: 'rehydrate',
|
|
478
|
-
});
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
Application.create(config.APP);
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
No `cleanupSSRContent` is needed in rehydrate mode — Glimmer reuses the DOM as-is. No boundary markers are emitted by the server.
|
|
400
|
+
If you need to branch on rehydrate vs. plain boot yourself, `shouldRehydrate()` is exported and returns the same boolean.
|
|
486
401
|
|
|
487
402
|
> **Note:** `_renderMode` is a private Ember API (underscore prefix) that has existed since Ember 2.x for FastBoot rehydration. It is stable in practice but not part of the public API.
|
|
488
403
|
|
|
489
|
-
|
|
404
|
+
## Advanced topics
|
|
490
405
|
|
|
491
|
-
|
|
406
|
+
### Shoebox
|
|
492
407
|
|
|
493
|
-
|
|
408
|
+
The shoebox captures `fetch` responses made during SSR or SSG and serializes them into `<script>` tags in the rendered HTML. On the client, `installShoebox()` intercepts `fetch` and replays the cached responses, avoiding duplicate API requests on first load.
|
|
494
409
|
|
|
495
|
-
|
|
410
|
+
Shoebox is opt in. Enable it only when your routes make `fetch` calls during server rendering that the client would otherwise repeat.
|
|
496
411
|
|
|
497
|
-
|
|
412
|
+
Server (SSR):
|
|
498
413
|
|
|
499
414
|
```js
|
|
500
415
|
const rendered = await emberApp.renderRoute(url, { shoebox: true });
|
|
501
|
-
const html = assembleHTML(template, rendered);
|
|
502
416
|
```
|
|
503
417
|
|
|
504
|
-
|
|
418
|
+
Server (SSG):
|
|
505
419
|
|
|
506
420
|
```js
|
|
507
|
-
emberSsg({
|
|
508
|
-
routes: ['index', 'about', 'pokemon'],
|
|
509
|
-
shoebox: true,
|
|
510
|
-
});
|
|
421
|
+
emberSsg({ routes: ['index', 'about'], shoebox: true });
|
|
511
422
|
```
|
|
512
423
|
|
|
513
|
-
|
|
424
|
+
Client, before `Application.create`:
|
|
514
425
|
|
|
515
426
|
```ts
|
|
516
427
|
import Application from './app.ts';
|
|
@@ -521,84 +432,43 @@ installShoebox();
|
|
|
521
432
|
Application.create(config.APP);
|
|
522
433
|
```
|
|
523
434
|
|
|
524
|
-
For rehydrate mode:
|
|
525
|
-
|
|
526
|
-
```ts
|
|
527
|
-
import Application from './app.ts';
|
|
528
|
-
import config from './config/environment.ts';
|
|
529
|
-
import { installShoebox, shouldRehydrate } from 'vite-ember-ssr/client';
|
|
530
|
-
|
|
531
|
-
installShoebox();
|
|
532
|
-
|
|
533
|
-
if (shouldRehydrate()) {
|
|
534
|
-
const app = Application.create({ ...config.APP, autoboot: false });
|
|
535
|
-
|
|
536
|
-
const url = (window.location.pathname + window.location.search).replace(
|
|
537
|
-
config.rootURL,
|
|
538
|
-
'/',
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
void app.visit(url, {
|
|
542
|
-
_renderMode: 'rehydrate',
|
|
543
|
-
});
|
|
544
|
-
return;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
Application.create(config.APP);
|
|
548
|
-
```
|
|
549
|
-
|
|
550
435
|
#### How it works
|
|
551
436
|
|
|
552
|
-
1. During SSR
|
|
553
|
-
2.
|
|
554
|
-
3. On the client, `installShoebox()` reads those
|
|
555
|
-
4. Once all cached entries
|
|
437
|
+
1. During SSR or SSG, the server intercepts `fetch()` calls and records the responses.
|
|
438
|
+
2. Responses are serialized as `<script type="application/json" class="shoebox">` tags.
|
|
439
|
+
3. On the client, `installShoebox()` reads those tags, wraps `window.fetch`, and serves cached responses for matching URLs.
|
|
440
|
+
4. Once all cached entries are consumed, the original `fetch` is restored automatically.
|
|
556
441
|
|
|
557
442
|
#### When to use it
|
|
558
443
|
|
|
559
|
-
- Routes that fetch data in `model()` hooks
|
|
560
|
-
- Any
|
|
444
|
+
- Routes that fetch data in `model()` hooks.
|
|
445
|
+
- Any case where the client would re fetch the same data immediately on boot.
|
|
561
446
|
|
|
562
447
|
#### When to skip it
|
|
563
448
|
|
|
564
|
-
- Static pages with no server
|
|
565
|
-
- Apps
|
|
449
|
+
- Static pages with no server side data fetching.
|
|
450
|
+
- Apps that intentionally re fetch for freshness.
|
|
566
451
|
|
|
567
452
|
#### Caveats
|
|
568
453
|
|
|
569
454
|
- Embedding large API responses increases HTML payload size.
|
|
570
|
-
- Never serialize sensitive or user
|
|
455
|
+
- Never serialize sensitive or user specific data into the shoebox. The HTML is cached and served to all users.
|
|
571
456
|
|
|
572
|
-
### Lazy
|
|
457
|
+
### Lazy routes (`@embroider/router`)
|
|
573
458
|
|
|
574
|
-
Both SSR and SSG
|
|
459
|
+
Both SSR and SSG support `@embroider/router`'s lazy loaded route bundles (`window._embroiderRouteBundles_`). No additional configuration is required.
|
|
575
460
|
|
|
576
|
-
|
|
461
|
+
### SSR bundling (`ssr.noExternal`)
|
|
577
462
|
|
|
578
|
-
|
|
579
|
-
2. The `@embroider/core` babel plugin must have `active: true` in your babel config:
|
|
463
|
+
Both plugins set `ssr.noExternal: [/./]`, which tells Vite to bundle every dependency into the SSR build instead of leaving them as runtime `require`/`import` calls.
|
|
580
464
|
|
|
581
|
-
|
|
582
|
-
// babel.config.cjs
|
|
583
|
-
module.exports = {
|
|
584
|
-
plugins: [
|
|
585
|
-
['@embroider/core/babel-plugin', { active: true }],
|
|
586
|
-
// ...
|
|
587
|
-
],
|
|
588
|
-
};
|
|
589
|
-
```
|
|
465
|
+
This is necessary because Ember's virtual packages (`@glimmer/tracking`, `@ember/*`, etc.) are provided by `ember-source` and are not real packages on disk. If Vite externalises a dependency that imports one of them, Node's runtime resolution fails under pnpm's strict `node_modules` layout. Bundling everything also keeps CJS/UMD packages going through Vite's transform pipeline, where the plugin's CJS shim can wrap them.
|
|
590
466
|
|
|
591
|
-
|
|
467
|
+
There is no real downside to bundling on the server. SSR builds are not shipped to browsers, so bundle size is not a constraint, and a single self contained SSR bundle simplifies deployment.
|
|
592
468
|
|
|
593
|
-
|
|
594
|
-
const emberApp = await createEmberApp('./dist/server/app-ssr.mjs');
|
|
595
|
-
```
|
|
596
|
-
|
|
597
|
-
#### How it works
|
|
598
|
-
|
|
599
|
-
When `@embroider/router` is active, it registers route bundles on `window._embroiderRouteBundles_` at module load time. The library captures these bundles after the first render and re-applies them to subsequent HappyDOM windows, ensuring lazy routes resolve correctly across multiple SSR/SSG renders.
|
|
469
|
+
You should not need to touch this. If you do need to add to it, Vite deep merges arrays so your entries are concatenated with the built in pattern.
|
|
600
470
|
|
|
601
|
-
## API
|
|
471
|
+
## API reference
|
|
602
472
|
|
|
603
473
|
### `vite-ember-ssr/vite-plugin`
|
|
604
474
|
|
|
@@ -608,118 +478,89 @@ import { emberSsr, emberSsg } from 'vite-ember-ssr/vite-plugin';
|
|
|
608
478
|
|
|
609
479
|
#### `emberSsr(options?)`
|
|
610
480
|
|
|
611
|
-
Vite plugin for runtime SSR.
|
|
481
|
+
Vite plugin for runtime SSR. Configures `ssr.noExternal`, build output directories (`dist/client`, `dist/server`), SSR build defaults (`target: 'node22'`, `sourcemap: true`, `minify: false`), and writes `{"type": "module"}` to the SSR output directory.
|
|
612
482
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
Options:
|
|
619
|
-
|
|
620
|
-
```js
|
|
621
|
-
emberSsr({
|
|
622
|
-
clientOutDir: 'dist/client', // default
|
|
623
|
-
serverOutDir: 'dist/server', // default
|
|
624
|
-
});
|
|
625
|
-
```
|
|
483
|
+
| Option | Type | Default | Description |
|
|
484
|
+
| -------------- | -------- | --------------- | ----------------------- |
|
|
485
|
+
| `clientOutDir` | `string` | `'dist/client'` | Client build output dir |
|
|
486
|
+
| `serverOutDir` | `string` | `'dist/server'` | SSR bundle output dir |
|
|
626
487
|
|
|
627
488
|
#### `emberSsg(options)`
|
|
628
489
|
|
|
629
|
-
Vite plugin for static site generation.
|
|
490
|
+
Vite plugin for static site generation.
|
|
630
491
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
shoebox: false, // default: serialize fetch responses into HTML
|
|
638
|
-
rehydrate: false, // default: use Glimmer rehydration instead of cleanup mode
|
|
639
|
-
outDir: 'dist', // default: output directory (ignored when combined with emberSsr)
|
|
640
|
-
});
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
#### Third-party packages with CSS imports (`ssr.noExternal`)
|
|
644
|
-
|
|
645
|
-
Both plugins automatically configure `ssr.noExternal` for Ember ecosystem packages (`@ember/*`, `@glimmer/*`, `@embroider/*`, `@warp-drive/*`, `ember-*`, `decorator-transforms`). This tells Vite to bundle these packages into the SSR output, where their CSS imports are safely no-opped.
|
|
646
|
-
|
|
647
|
-
If your app uses third-party packages that contain bare CSS imports (e.g. `import './styles.css'`), you need to add them to `ssr.noExternal` in your Vite config. Without this, Node.js will try to load the `.css` files directly at runtime and fail with `ERR_UNKNOWN_FILE_EXTENSION`.
|
|
648
|
-
|
|
649
|
-
```js
|
|
650
|
-
// vite.config.mjs
|
|
651
|
-
export default defineConfig({
|
|
652
|
-
plugins: [
|
|
653
|
-
emberSsr(), // or emberSsg({ routes: [...] })
|
|
654
|
-
],
|
|
655
|
-
ssr: {
|
|
656
|
-
// Vite deep-merges this with the Ember ecosystem patterns
|
|
657
|
-
// that the plugin provides automatically.
|
|
658
|
-
noExternal: ['nvp.ui', 'some-other-package-with-css'],
|
|
659
|
-
},
|
|
660
|
-
});
|
|
661
|
-
```
|
|
662
|
-
|
|
663
|
-
This applies to both SSR and SSG modes. Vite's `config` hook deep-merges arrays, so your entries are concatenated with the built-in patterns — you don't need to repeat them.
|
|
492
|
+
| Option | Type | Default | Description |
|
|
493
|
+
| ---------- | ---------- | ------------------ | --------------------------------------------------------------------------------------- |
|
|
494
|
+
| `routes` | `string[]` | (required) | URL paths to prerender. `'index'` is special cased, see [Route format](#route-format) |
|
|
495
|
+
| `ssrEntry` | `string` | `'app/app-ssr.ts'` | Path to the SSR entry module |
|
|
496
|
+
| `shoebox` | `boolean` | `false` | Serialize captured fetch responses into the HTML, see [Shoebox](#shoebox) |
|
|
497
|
+
| `outDir` | `string` | `'dist'` | Output directory. Ignored when combined with `emberSsr` (output goes to `clientOutDir`) |
|
|
664
498
|
|
|
665
499
|
### `vite-ember-ssr/server`
|
|
666
500
|
|
|
667
501
|
```js
|
|
668
|
-
import {
|
|
502
|
+
import {
|
|
503
|
+
createEmberApp,
|
|
504
|
+
assembleHTML,
|
|
505
|
+
loadCssManifest,
|
|
506
|
+
hasSSRMarkers,
|
|
507
|
+
} from 'vite-ember-ssr/server';
|
|
669
508
|
```
|
|
670
509
|
|
|
671
|
-
- **`createEmberApp(ssrBundlePath, options?)`**
|
|
672
|
-
|
|
673
|
-
- **`app.
|
|
674
|
-
|
|
675
|
-
- **`
|
|
676
|
-
|
|
677
|
-
- **`assembleHTML(template, renderResult)`** — inserts rendered `head` and `body` fragments into the HTML template by replacing the `<!-- VITE_EMBER_SSR_HEAD -->` and `<!-- VITE_EMBER_SSR_BODY -->` markers.
|
|
678
|
-
|
|
679
|
-
- **`loadCssManifest(clientDir)`** — loads the CSS manifest from the client build output directory. Returns `undefined` if not present. Used with lazy routes.
|
|
680
|
-
|
|
681
|
-
- **`hasSSRMarkers(html)`** — checks whether an HTML string contains the SSR markers. Returns `{ head: boolean, body: boolean }`.
|
|
510
|
+
- **`createEmberApp(ssrBundlePath, options?)`** creates a long lived tinypool worker pool. Each worker imports the SSR bundle once at startup. Returns an `EmberApp`. Options: `{ workers?: number, recycleWorkerInterval?: number, isolateWorkers?: boolean, dev?: { ssrLoadModule } }`.
|
|
511
|
+
- **`app.renderRoute(url, options?)`** renders a URL path. Returns `{ head, body, statusCode, error }`. Options: `{ shoebox?, cssManifest?, settledTimeout? }`. `settledTimeout` (default `10000`) bounds how long the renderer waits for the SSR bundle's exported `settled()` to resolve, see [Settling](#settling).
|
|
512
|
+
- **`app.destroy()`** shuts down the worker pool.
|
|
513
|
+
- **`assembleHTML(template, renderResult)`** inserts rendered fragments into the template at the `<!-- VITE_EMBER_SSR_HEAD -->` and `<!-- VITE_EMBER_SSR_BODY -->` markers.
|
|
514
|
+
- **`loadCssManifest(clientDir)`** loads the CSS manifest from the client build output. Returns `undefined` if not present. Used with lazy routes.
|
|
515
|
+
- **`hasSSRMarkers(html)`** returns `{ head: boolean, body: boolean }` indicating which markers are present.
|
|
682
516
|
|
|
683
517
|
### `vite-ember-ssr/client`
|
|
684
518
|
|
|
685
519
|
```js
|
|
686
520
|
import {
|
|
521
|
+
bootRehydrated,
|
|
522
|
+
shouldRehydrate,
|
|
687
523
|
installShoebox,
|
|
688
|
-
cleanupSSRContent,
|
|
689
524
|
cleanupShoebox,
|
|
690
|
-
isSSRRendered,
|
|
691
|
-
shouldRehydrate,
|
|
692
525
|
} from 'vite-ember-ssr/client';
|
|
693
526
|
```
|
|
694
527
|
|
|
695
|
-
- **`
|
|
696
|
-
- **`
|
|
697
|
-
- **`
|
|
698
|
-
- **`
|
|
699
|
-
- **`shouldRehydrate()`** — returns `true` if the current page was rendered with rehydrate mode (the server injected `window.__vite_ember_ssr_rehydrate__`). Use this in `entry.ts` to decide whether to boot Ember in rehydrate mode or with a normal boot. Essential for SSG apps where only prerendered routes should rehydrate.
|
|
528
|
+
- **`bootRehydrated(Application, config)`** boots the client Ember app, rehydrating the server rendered DOM when present. Falls back to a normal `Application.create(config.APP)` when the page was not server rendered. See [Client boot](#client-boot).
|
|
529
|
+
- **`shouldRehydrate()`** returns `true` if the current page was rendered with rehydration markers (the server injected `window.__vite_ember_ssr_rehydrate__`). Useful when you need to branch on rehydrate vs. plain boot yourself.
|
|
530
|
+
- **`installShoebox()`** replays server captured fetch responses. Auto restores `fetch` once all entries are consumed. Call in `entry.ts` before booting.
|
|
531
|
+
- **`cleanupShoebox()`** manually restores the original `fetch`.
|
|
700
532
|
|
|
701
533
|
## Monorepo development
|
|
702
534
|
|
|
703
|
-
This repo contains
|
|
704
|
-
|
|
705
|
-
|
|
|
706
|
-
|
|
|
707
|
-
| `
|
|
708
|
-
| `
|
|
709
|
-
| `
|
|
710
|
-
| `
|
|
711
|
-
| `
|
|
712
|
-
| `
|
|
713
|
-
| `
|
|
535
|
+
This repo contains the library and a set of test apps that exercise it.
|
|
536
|
+
|
|
537
|
+
| Path | Description |
|
|
538
|
+
| ------------------------------------------ | ---------------------------------------- |
|
|
539
|
+
| `vite-ember-ssr/` | Core library and test suites |
|
|
540
|
+
| `test-apps/test-app/` | Ember test app (SSR) |
|
|
541
|
+
| `test-apps/test-app-ssg/` | Ember test app (SSG) |
|
|
542
|
+
| `test-apps/test-app-combined/` | Ember test app (SSR + SSG) |
|
|
543
|
+
| `test-apps/test-app-lazy-ssr/` | Ember test app (SSR + lazy routes) |
|
|
544
|
+
| `test-apps/test-app-lazy-ssg/` | Ember test app (SSG + lazy routes) |
|
|
545
|
+
| `test-apps/test-app-monorepo-ssr/` | Ember test app consuming a monorepo lib |
|
|
546
|
+
| `test-apps/test-app-monorepo-ssg/` | Same, for SSG |
|
|
547
|
+
| `test-apps/test-app-ssr-loading-substate/` | Loading substate behaviour (SSR) |
|
|
548
|
+
| `test-apps/test-app-ssg-loading-substate/` | Loading substate behaviour (SSG) |
|
|
549
|
+
| `test-apps/monorepo-lib/` | Shared library used by the monorepo apps |
|
|
550
|
+
| `test-apps/test-server/` | Fastify SSR server |
|
|
551
|
+
|
|
552
|
+
Top level scripts:
|
|
714
553
|
|
|
715
554
|
```sh
|
|
716
555
|
pnpm install
|
|
717
|
-
pnpm dev
|
|
718
|
-
pnpm build
|
|
719
|
-
pnpm demo
|
|
720
|
-
pnpm test
|
|
721
|
-
pnpm test:browser
|
|
722
|
-
pnpm test:all
|
|
556
|
+
pnpm dev # dev server (Fastify + Vite middleware)
|
|
557
|
+
pnpm build # build library + test app
|
|
558
|
+
pnpm demo # build everything, start production server
|
|
559
|
+
pnpm test # vitest SSR tests
|
|
560
|
+
pnpm test:browser # playwright browser tests
|
|
561
|
+
pnpm test:all # both
|
|
562
|
+
pnpm clean # remove dist directories
|
|
563
|
+
pnpm format # prettier --write .
|
|
723
564
|
```
|
|
724
565
|
|
|
725
566
|
## Performance
|