@moostjs/vite 0.6.28 → 0.6.30
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 +25 -0
- package/dist/index.cjs +62 -8
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +62 -8
- package/dist/prod-server.cjs +37 -10
- package/dist/prod-server.d.ts +28 -1
- package/dist/prod-server.mjs +37 -10
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -122,6 +122,30 @@ In dev, the plugin adds an SSR fallback middleware that renders pages on the ser
|
|
|
122
122
|
- **ssr** — server-side render function (`dist/server/ssr/`)
|
|
123
123
|
- **server** — production Node.js server (`dist/server/server.js`)
|
|
124
124
|
|
|
125
|
+
### Render Contract
|
|
126
|
+
|
|
127
|
+
Your SSR entry exports a `render(url)` that returns HTML plus optional metadata. Only `html` is required — every other field is optional, so `{ html }` (or `{ html, state }`) keeps working unchanged:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// entry-server.ts
|
|
131
|
+
import type { TSSRRenderResult } from '@moostjs/vite/server'
|
|
132
|
+
|
|
133
|
+
export async function render(url: string): Promise<TSSRRenderResult> {
|
|
134
|
+
// ...render your app for `url`...
|
|
135
|
+
return {
|
|
136
|
+
html, // → substituted into `<!--ssr-outlet-->`
|
|
137
|
+
state, // → wrapped as <script>window.__SSR_STATE__=…</script> at `<!--ssr-state-->`
|
|
138
|
+
head, // → per-page <head> tags injected at `<!--ssr-head-->`
|
|
139
|
+
status, // → HTTP status code (default 200) — e.g. 404 for an unknown slug
|
|
140
|
+
headers, // → extra response headers — e.g. cache-control, or location with status: 301
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The three markers (`ssrOutlet`, `ssrState`, `ssrHead`) are plain string replacements — a marker missing from `index.html` is simply skipped. Put `<!--ssr-head-->` inside `<head>` to get crawler-visible per-page `<title>` / `<meta>` / canonical / Open Graph / JSON-LD tags. `head` is exactly the string a head manager emits (e.g. unhead's `renderSSRHead(head).headTags`).
|
|
146
|
+
|
|
147
|
+
`status` and `headers` give the render control over the response: return `status: 404` for a real not-found (crawlers treat soft-404s worse), set `cache-control`, or redirect with `status: 301` + `headers: { location: '/new-url' }`. Headers are applied after the default `Content-Type: text/html`, so a render may override it.
|
|
148
|
+
|
|
125
149
|
### SPA Mode
|
|
126
150
|
|
|
127
151
|
Omit `ssrEntry` and Vite serves the app as a standard SPA. The production build still generates a server that serves static files and API routes — it just skips server-side rendering.
|
|
@@ -236,6 +260,7 @@ The plugin injects a `__VITE_ID` decorator on `@Injectable` and `@Controller` cl
|
|
|
236
260
|
| `ssrEntry` | `string` | — | Vue/React SSR entry module (e.g. `'/src/entry-server.ts'`) |
|
|
237
261
|
| `ssrOutlet` | `string` | `'<!--ssr-outlet-->'` | HTML placeholder for SSR-rendered content |
|
|
238
262
|
| `ssrState` | `string` | `'<!--ssr-state-->'` | HTML placeholder for SSR state transfer script |
|
|
263
|
+
| `ssrHead` | `string` | `'<!--ssr-head-->'` | HTML placeholder for SSR-rendered `<head>` tags (place inside `<head>`) |
|
|
239
264
|
| `serverEntry` | `string` | — | Custom production server entry file (e.g. `'./server.ts'`) |
|
|
240
265
|
| `ssrExternal` | `string[]` | — | Packages to keep external in the SSR build (middleware mode, `vite build` only). Concatenated with `cfg.ssr.external`. See [SSR Bundle Size](#ssr-bundle-size). |
|
|
241
266
|
|
package/dist/index.cjs
CHANGED
|
@@ -38,6 +38,27 @@ let moost = require("moost");
|
|
|
38
38
|
const PLUGIN_NAME = "moost-vite";
|
|
39
39
|
const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
|
|
40
40
|
const DEFAULT_SSR_STATE = "<!--ssr-state-->";
|
|
41
|
+
const DEFAULT_SSR_HEAD = "<!--ssr-head-->";
|
|
42
|
+
/**
|
|
43
|
+
* Applies an SSR render result to the HTTP response: sets the status code and
|
|
44
|
+
* headers, then substitutes the outlet, state and head markers into the HTML
|
|
45
|
+
* template. Shared by all three render call sites (the dev middleware, the dev
|
|
46
|
+
* `createSSRServer` branch and the prod `createSSRServer` branch) so the render
|
|
47
|
+
* contract stays identical across them.
|
|
48
|
+
*
|
|
49
|
+
* Fully backwards compatible: every field beyond `html` is optional, a missing
|
|
50
|
+
* marker in the template is a no-op `String.replace`, and headers from the render
|
|
51
|
+
* result are applied after the default `Content-Type: text/html` so a render can
|
|
52
|
+
* override it. Replacements use a function argument so `$`-sequences in the
|
|
53
|
+
* rendered payloads (JSON state, JSON-LD `<head>` tags) are inserted literally
|
|
54
|
+
* rather than interpreted as `String.replace` special patterns.
|
|
55
|
+
*/ function sendSSRResponse(res, template, markers, result) {
|
|
56
|
+
res.statusCode = result.status ?? 200;
|
|
57
|
+
res.setHeader("Content-Type", "text/html");
|
|
58
|
+
if (result.headers) for (const [key, value] of Object.entries(result.headers)) res.setHeader(key, value);
|
|
59
|
+
const stateScript = result.state ? `<script>window.__SSR_STATE__=${result.state}<\/script>` : "";
|
|
60
|
+
res.end(template.replace(markers.ssrOutlet, () => result.html).replace(markers.ssrState, () => stateScript).replace(markers.ssrHead, () => result.head ?? ""));
|
|
61
|
+
}
|
|
41
62
|
function entryBasename(entry) {
|
|
42
63
|
return entry.split("/").pop().replace(/\.ts$/, ".js");
|
|
43
64
|
}
|
|
@@ -368,11 +389,24 @@ function moostVite(options) {
|
|
|
368
389
|
const prefixes = normalizePrefixes(options.prefix);
|
|
369
390
|
let moostMiddleware = null;
|
|
370
391
|
let localFetchTeardown = null;
|
|
392
|
+
/**
|
|
393
|
+
* Boot-identity stamps (dev-only "mongrel state" diagnostic — a stale pipeline
|
|
394
|
+
* presents as security middleware silently switched off). `bootGeneration` is
|
|
395
|
+
* bumped on every eject; `bootingGeneration` records which generation the
|
|
396
|
+
* in-flight/most recent boot belongs to (set by runReload). When listen()
|
|
397
|
+
* captures a middleware for a boot that is no longer current — e.g. a delayed
|
|
398
|
+
* listen() from a torn boot racing a newer eject — it logs loudly.
|
|
399
|
+
*/ let bootGeneration = 0;
|
|
400
|
+
let bootingGeneration = 0;
|
|
401
|
+
/** Whether the HTTP listen() patch has ever captured a middleware (i.e. this is an HTTP app). */ let httpCaptured = false;
|
|
402
|
+
/** Module IDs awaiting DI cleanup — consumed in runReload; see ejectApp. */ let pendingCleanup = null;
|
|
371
403
|
/** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
|
|
372
404
|
const adapters = isTest ? [] : [
|
|
373
405
|
createAdapterDetector("http", (MoostHttp, moduleExports) => {
|
|
374
406
|
MoostHttp.prototype.listen = function(...args) {
|
|
375
407
|
logger.log(`🔌 [2mOvertaking HTTP.listen`);
|
|
408
|
+
if (bootingGeneration !== bootGeneration) logger.error(`⚠️ A stale Moost boot captured the HTTP middleware (boot generation ${bootingGeneration}, latest ${bootGeneration}) — an HMR reload race; a follow-up reload will replace it.`);
|
|
409
|
+
httpCaptured = true;
|
|
376
410
|
if (options.middleware) moostMiddleware = this.getServerCb((req) => {
|
|
377
411
|
pendingNextMap.get(req)?.();
|
|
378
412
|
});
|
|
@@ -412,15 +446,23 @@ function moostVite(options) {
|
|
|
412
446
|
*/ const getEntryModule = (moduleGraph) => moduleGraph.getModuleByUrl(options.entry);
|
|
413
447
|
/**
|
|
414
448
|
* Drops the captured Moost app so the next request triggers a full reload:
|
|
415
|
-
* releases the middleware + local fetch and
|
|
416
|
-
*
|
|
449
|
+
* releases the middleware + local fetch and queues the changed module IDs for
|
|
450
|
+
* DI cleanup. The cleanup itself (infact ejects, wooks reset) is deferred to
|
|
451
|
+
* runReload, under the reload lock: running it here, per hot-update wave,
|
|
452
|
+
* raced in-flight requests and mid-boot imports mutating the same global
|
|
453
|
+
* containers — ejected-but-still-referenced instances were lazily re-created
|
|
454
|
+
* into the OLD pipeline, producing a mongrel half-old/half-new app. Waves
|
|
455
|
+
* arriving before the lazy reload (editor bulk-save storms) coalesce into one
|
|
456
|
+
* pending set and one cleanup.
|
|
417
457
|
*/ const ejectApp = (cleanupInstances) => {
|
|
458
|
+
bootGeneration++;
|
|
418
459
|
moostMiddleware = null;
|
|
419
460
|
if (localFetchTeardown) {
|
|
420
461
|
localFetchTeardown();
|
|
421
462
|
localFetchTeardown = null;
|
|
422
463
|
}
|
|
423
|
-
|
|
464
|
+
if (pendingCleanup) for (const id of cleanupInstances) pendingCleanup.add(id);
|
|
465
|
+
else pendingCleanup = cleanupInstances;
|
|
424
466
|
reloadRequired = true;
|
|
425
467
|
};
|
|
426
468
|
patchMoostHandlerLogging();
|
|
@@ -446,6 +488,7 @@ function moostVite(options) {
|
|
|
446
488
|
serverDefines.__MOOST_SSR_ENTRY__ = JSON.stringify(`./ssr/${ssrBasename}`);
|
|
447
489
|
serverDefines.__MOOST_SSR_OUTLET__ = JSON.stringify(options.ssrOutlet || DEFAULT_SSR_OUTLET);
|
|
448
490
|
serverDefines.__MOOST_SSR_STATE__ = JSON.stringify(options.ssrState || DEFAULT_SSR_STATE);
|
|
491
|
+
serverDefines.__MOOST_SSR_HEAD__ = JSON.stringify(options.ssrHead || DEFAULT_SSR_HEAD);
|
|
449
492
|
}
|
|
450
493
|
const outDir = cfg.build?.outDir || "dist";
|
|
451
494
|
const isBuild = env.command === "build";
|
|
@@ -534,7 +577,8 @@ function moostVite(options) {
|
|
|
534
577
|
prefix: prefixes,
|
|
535
578
|
port: options.port,
|
|
536
579
|
ssrOutlet: options.ssrOutlet,
|
|
537
|
-
ssrState: options.ssrState
|
|
580
|
+
ssrState: options.ssrState,
|
|
581
|
+
ssrHead: options.ssrHead
|
|
538
582
|
};
|
|
539
583
|
},
|
|
540
584
|
async transform(code, id) {
|
|
@@ -572,6 +616,8 @@ function moostVite(options) {
|
|
|
572
616
|
bootError = null;
|
|
573
617
|
reloadRequired = false;
|
|
574
618
|
reloadPromise = null;
|
|
619
|
+
pendingCleanup = null;
|
|
620
|
+
bootingGeneration = bootGeneration;
|
|
575
621
|
const runReload = () => {
|
|
576
622
|
reloadRequired = false;
|
|
577
623
|
console.log();
|
|
@@ -579,11 +625,16 @@ function moostVite(options) {
|
|
|
579
625
|
console.log();
|
|
580
626
|
return (async () => {
|
|
581
627
|
try {
|
|
628
|
+
bootingGeneration = bootGeneration;
|
|
629
|
+
const cleanupInstances = pendingCleanup ?? void 0;
|
|
630
|
+
pendingCleanup = null;
|
|
631
|
+
moostRestartCleanup(adapters, options.onEject, cleanupInstances);
|
|
582
632
|
for (const adapter of adapters) if (adapter.detected) await adapter.init();
|
|
583
633
|
const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
|
|
584
634
|
if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
|
|
585
635
|
await ssrImport(options.entry);
|
|
586
636
|
bootError = null;
|
|
637
|
+
if (httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
|
|
587
638
|
} catch (error) {
|
|
588
639
|
bootError = error;
|
|
589
640
|
logger.error(`✖️ Failed to reload Moost App: ${error.message}`);
|
|
@@ -650,6 +701,7 @@ function moostVite(options) {
|
|
|
650
701
|
async configureServer(server) {
|
|
651
702
|
const ssrOutlet = options.ssrOutlet || DEFAULT_SSR_OUTLET;
|
|
652
703
|
const ssrState = options.ssrState || DEFAULT_SSR_STATE;
|
|
704
|
+
const ssrHead = options.ssrHead || DEFAULT_SSR_HEAD;
|
|
653
705
|
const fs = await import("node:fs/promises");
|
|
654
706
|
return () => {
|
|
655
707
|
server.middlewares.use(async (req, res, next) => {
|
|
@@ -660,10 +712,12 @@ function moostVite(options) {
|
|
|
660
712
|
let template = await fs.readFile((0, node_path.resolve)(server.config.root, "index.html"), "utf8");
|
|
661
713
|
template = await server.transformIndexHtml(url, template);
|
|
662
714
|
const { render } = await server.ssrLoadModule(options.ssrEntry);
|
|
663
|
-
const
|
|
664
|
-
res
|
|
665
|
-
|
|
666
|
-
|
|
715
|
+
const result = await render(url);
|
|
716
|
+
sendSSRResponse(res, template, {
|
|
717
|
+
ssrOutlet,
|
|
718
|
+
ssrState,
|
|
719
|
+
ssrHead
|
|
720
|
+
}, result);
|
|
667
721
|
} catch (error) {
|
|
668
722
|
server.ssrFixStacktrace(error);
|
|
669
723
|
console.error(error);
|
package/dist/index.d.ts
CHANGED
|
@@ -117,6 +117,12 @@ interface TMoostViteDevOptions {
|
|
|
117
117
|
* Default: `'<!--ssr-state-->'`
|
|
118
118
|
*/
|
|
119
119
|
ssrState?: string;
|
|
120
|
+
/**
|
|
121
|
+
* HTML placeholder for SSR-rendered `<head>` tags (`<title>`, `<meta>`,
|
|
122
|
+
* canonical, Open Graph, JSON-LD). Place the marker inside `<head>` and return
|
|
123
|
+
* `head` from `render()` to inject per-page tags. Default: `'<!--ssr-head-->'`
|
|
124
|
+
*/
|
|
125
|
+
ssrHead?: string;
|
|
120
126
|
/**
|
|
121
127
|
* Path to a custom server entry file (e.g., `'./server.ts'`).
|
|
122
128
|
* When provided, this file is used as the production server build entry.
|
package/dist/index.mjs
CHANGED
|
@@ -9,6 +9,27 @@ import { Moost, clearGlobalWooks, createLogger, getMoostInfact, getMoostMate } f
|
|
|
9
9
|
const PLUGIN_NAME = "moost-vite";
|
|
10
10
|
const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
|
|
11
11
|
const DEFAULT_SSR_STATE = "<!--ssr-state-->";
|
|
12
|
+
const DEFAULT_SSR_HEAD = "<!--ssr-head-->";
|
|
13
|
+
/**
|
|
14
|
+
* Applies an SSR render result to the HTTP response: sets the status code and
|
|
15
|
+
* headers, then substitutes the outlet, state and head markers into the HTML
|
|
16
|
+
* template. Shared by all three render call sites (the dev middleware, the dev
|
|
17
|
+
* `createSSRServer` branch and the prod `createSSRServer` branch) so the render
|
|
18
|
+
* contract stays identical across them.
|
|
19
|
+
*
|
|
20
|
+
* Fully backwards compatible: every field beyond `html` is optional, a missing
|
|
21
|
+
* marker in the template is a no-op `String.replace`, and headers from the render
|
|
22
|
+
* result are applied after the default `Content-Type: text/html` so a render can
|
|
23
|
+
* override it. Replacements use a function argument so `$`-sequences in the
|
|
24
|
+
* rendered payloads (JSON state, JSON-LD `<head>` tags) are inserted literally
|
|
25
|
+
* rather than interpreted as `String.replace` special patterns.
|
|
26
|
+
*/ function sendSSRResponse(res, template, markers, result) {
|
|
27
|
+
res.statusCode = result.status ?? 200;
|
|
28
|
+
res.setHeader("Content-Type", "text/html");
|
|
29
|
+
if (result.headers) for (const [key, value] of Object.entries(result.headers)) res.setHeader(key, value);
|
|
30
|
+
const stateScript = result.state ? `<script>window.__SSR_STATE__=${result.state}<\/script>` : "";
|
|
31
|
+
res.end(template.replace(markers.ssrOutlet, () => result.html).replace(markers.ssrState, () => stateScript).replace(markers.ssrHead, () => result.head ?? ""));
|
|
32
|
+
}
|
|
12
33
|
function entryBasename(entry) {
|
|
13
34
|
return entry.split("/").pop().replace(/\.ts$/, ".js");
|
|
14
35
|
}
|
|
@@ -337,11 +358,24 @@ function moostVite(options) {
|
|
|
337
358
|
const prefixes = normalizePrefixes(options.prefix);
|
|
338
359
|
let moostMiddleware = null;
|
|
339
360
|
let localFetchTeardown = null;
|
|
361
|
+
/**
|
|
362
|
+
* Boot-identity stamps (dev-only "mongrel state" diagnostic — a stale pipeline
|
|
363
|
+
* presents as security middleware silently switched off). `bootGeneration` is
|
|
364
|
+
* bumped on every eject; `bootingGeneration` records which generation the
|
|
365
|
+
* in-flight/most recent boot belongs to (set by runReload). When listen()
|
|
366
|
+
* captures a middleware for a boot that is no longer current — e.g. a delayed
|
|
367
|
+
* listen() from a torn boot racing a newer eject — it logs loudly.
|
|
368
|
+
*/ let bootGeneration = 0;
|
|
369
|
+
let bootingGeneration = 0;
|
|
370
|
+
/** Whether the HTTP listen() patch has ever captured a middleware (i.e. this is an HTTP app). */ let httpCaptured = false;
|
|
371
|
+
/** Module IDs awaiting DI cleanup — consumed in runReload; see ejectApp. */ let pendingCleanup = null;
|
|
340
372
|
/** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
|
|
341
373
|
const adapters = [
|
|
342
374
|
createAdapterDetector("http", (MoostHttp, moduleExports) => {
|
|
343
375
|
MoostHttp.prototype.listen = function(...args) {
|
|
344
376
|
logger.log(`🔌 [2mOvertaking HTTP.listen`);
|
|
377
|
+
if (bootingGeneration !== bootGeneration) logger.error(`⚠️ A stale Moost boot captured the HTTP middleware (boot generation ${bootingGeneration}, latest ${bootGeneration}) — an HMR reload race; a follow-up reload will replace it.`);
|
|
378
|
+
httpCaptured = true;
|
|
345
379
|
if (options.middleware) moostMiddleware = this.getServerCb((req) => {
|
|
346
380
|
pendingNextMap.get(req)?.();
|
|
347
381
|
});
|
|
@@ -381,15 +415,23 @@ function moostVite(options) {
|
|
|
381
415
|
*/ const getEntryModule = (moduleGraph) => moduleGraph.getModuleByUrl(options.entry);
|
|
382
416
|
/**
|
|
383
417
|
* Drops the captured Moost app so the next request triggers a full reload:
|
|
384
|
-
* releases the middleware + local fetch and
|
|
385
|
-
*
|
|
418
|
+
* releases the middleware + local fetch and queues the changed module IDs for
|
|
419
|
+
* DI cleanup. The cleanup itself (infact ejects, wooks reset) is deferred to
|
|
420
|
+
* runReload, under the reload lock: running it here, per hot-update wave,
|
|
421
|
+
* raced in-flight requests and mid-boot imports mutating the same global
|
|
422
|
+
* containers — ejected-but-still-referenced instances were lazily re-created
|
|
423
|
+
* into the OLD pipeline, producing a mongrel half-old/half-new app. Waves
|
|
424
|
+
* arriving before the lazy reload (editor bulk-save storms) coalesce into one
|
|
425
|
+
* pending set and one cleanup.
|
|
386
426
|
*/ const ejectApp = (cleanupInstances) => {
|
|
427
|
+
bootGeneration++;
|
|
387
428
|
moostMiddleware = null;
|
|
388
429
|
if (localFetchTeardown) {
|
|
389
430
|
localFetchTeardown();
|
|
390
431
|
localFetchTeardown = null;
|
|
391
432
|
}
|
|
392
|
-
|
|
433
|
+
if (pendingCleanup) for (const id of cleanupInstances) pendingCleanup.add(id);
|
|
434
|
+
else pendingCleanup = cleanupInstances;
|
|
393
435
|
reloadRequired = true;
|
|
394
436
|
};
|
|
395
437
|
patchMoostHandlerLogging();
|
|
@@ -415,6 +457,7 @@ function moostVite(options) {
|
|
|
415
457
|
serverDefines.__MOOST_SSR_ENTRY__ = JSON.stringify(`./ssr/${ssrBasename}`);
|
|
416
458
|
serverDefines.__MOOST_SSR_OUTLET__ = JSON.stringify(options.ssrOutlet || DEFAULT_SSR_OUTLET);
|
|
417
459
|
serverDefines.__MOOST_SSR_STATE__ = JSON.stringify(options.ssrState || DEFAULT_SSR_STATE);
|
|
460
|
+
serverDefines.__MOOST_SSR_HEAD__ = JSON.stringify(options.ssrHead || DEFAULT_SSR_HEAD);
|
|
418
461
|
}
|
|
419
462
|
const outDir = cfg.build?.outDir || "dist";
|
|
420
463
|
const isBuild = env.command === "build";
|
|
@@ -503,7 +546,8 @@ function moostVite(options) {
|
|
|
503
546
|
prefix: prefixes,
|
|
504
547
|
port: options.port,
|
|
505
548
|
ssrOutlet: options.ssrOutlet,
|
|
506
|
-
ssrState: options.ssrState
|
|
549
|
+
ssrState: options.ssrState,
|
|
550
|
+
ssrHead: options.ssrHead
|
|
507
551
|
};
|
|
508
552
|
},
|
|
509
553
|
async transform(code, id) {
|
|
@@ -541,6 +585,8 @@ function moostVite(options) {
|
|
|
541
585
|
bootError = null;
|
|
542
586
|
reloadRequired = false;
|
|
543
587
|
reloadPromise = null;
|
|
588
|
+
pendingCleanup = null;
|
|
589
|
+
bootingGeneration = bootGeneration;
|
|
544
590
|
const runReload = () => {
|
|
545
591
|
reloadRequired = false;
|
|
546
592
|
console.log();
|
|
@@ -548,11 +594,16 @@ function moostVite(options) {
|
|
|
548
594
|
console.log();
|
|
549
595
|
return (async () => {
|
|
550
596
|
try {
|
|
597
|
+
bootingGeneration = bootGeneration;
|
|
598
|
+
const cleanupInstances = pendingCleanup ?? void 0;
|
|
599
|
+
pendingCleanup = null;
|
|
600
|
+
moostRestartCleanup(adapters, options.onEject, cleanupInstances);
|
|
551
601
|
for (const adapter of adapters) if (adapter.detected) await adapter.init();
|
|
552
602
|
const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
|
|
553
603
|
if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
|
|
554
604
|
await ssrImport(options.entry);
|
|
555
605
|
bootError = null;
|
|
606
|
+
if (httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
|
|
556
607
|
} catch (error) {
|
|
557
608
|
bootError = error;
|
|
558
609
|
logger.error(`✖️ Failed to reload Moost App: ${error.message}`);
|
|
@@ -611,6 +662,7 @@ function moostVite(options) {
|
|
|
611
662
|
async configureServer(server) {
|
|
612
663
|
const ssrOutlet = options.ssrOutlet || DEFAULT_SSR_OUTLET;
|
|
613
664
|
const ssrState = options.ssrState || DEFAULT_SSR_STATE;
|
|
665
|
+
const ssrHead = options.ssrHead || DEFAULT_SSR_HEAD;
|
|
614
666
|
const fs = await import("node:fs/promises");
|
|
615
667
|
return () => {
|
|
616
668
|
server.middlewares.use(async (req, res, next) => {
|
|
@@ -621,10 +673,12 @@ function moostVite(options) {
|
|
|
621
673
|
let template = await fs.readFile(resolve(server.config.root, "index.html"), "utf8");
|
|
622
674
|
template = await server.transformIndexHtml(url, template);
|
|
623
675
|
const { render } = await server.ssrLoadModule(options.ssrEntry);
|
|
624
|
-
const
|
|
625
|
-
res
|
|
626
|
-
|
|
627
|
-
|
|
676
|
+
const result = await render(url);
|
|
677
|
+
sendSSRResponse(res, template, {
|
|
678
|
+
ssrOutlet,
|
|
679
|
+
ssrState,
|
|
680
|
+
ssrHead
|
|
681
|
+
}, result);
|
|
628
682
|
} catch (error) {
|
|
629
683
|
server.ssrFixStacktrace(error);
|
|
630
684
|
console.error(error);
|
package/dist/prod-server.cjs
CHANGED
|
@@ -5,6 +5,27 @@ let moost = require("moost");
|
|
|
5
5
|
const PLUGIN_NAME = "moost-vite";
|
|
6
6
|
const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
|
|
7
7
|
const DEFAULT_SSR_STATE = "<!--ssr-state-->";
|
|
8
|
+
const DEFAULT_SSR_HEAD = "<!--ssr-head-->";
|
|
9
|
+
/**
|
|
10
|
+
* Applies an SSR render result to the HTTP response: sets the status code and
|
|
11
|
+
* headers, then substitutes the outlet, state and head markers into the HTML
|
|
12
|
+
* template. Shared by all three render call sites (the dev middleware, the dev
|
|
13
|
+
* `createSSRServer` branch and the prod `createSSRServer` branch) so the render
|
|
14
|
+
* contract stays identical across them.
|
|
15
|
+
*
|
|
16
|
+
* Fully backwards compatible: every field beyond `html` is optional, a missing
|
|
17
|
+
* marker in the template is a no-op `String.replace`, and headers from the render
|
|
18
|
+
* result are applied after the default `Content-Type: text/html` so a render can
|
|
19
|
+
* override it. Replacements use a function argument so `$`-sequences in the
|
|
20
|
+
* rendered payloads (JSON state, JSON-LD `<head>` tags) are inserted literally
|
|
21
|
+
* rather than interpreted as `String.replace` special patterns.
|
|
22
|
+
*/ function sendSSRResponse(res, template, markers, result) {
|
|
23
|
+
res.statusCode = result.status ?? 200;
|
|
24
|
+
res.setHeader("Content-Type", "text/html");
|
|
25
|
+
if (result.headers) for (const [key, value] of Object.entries(result.headers)) res.setHeader(key, value);
|
|
26
|
+
const stateScript = result.state ? `<script>window.__SSR_STATE__=${result.state}<\/script>` : "";
|
|
27
|
+
res.end(template.replace(markers.ssrOutlet, () => result.html).replace(markers.ssrState, () => stateScript).replace(markers.ssrHead, () => result.head ?? ""));
|
|
28
|
+
}
|
|
8
29
|
/**
|
|
9
30
|
* Normalizes the `prefix` option (single mount or list of mounts) into a list of
|
|
10
31
|
* normalized entries: leading slash ensured, trailing slash stripped.
|
|
@@ -50,6 +71,7 @@ async function createSSRServer(options) {
|
|
|
50
71
|
const ssrEntry = config.ssrEntry;
|
|
51
72
|
const ssrOutlet = config.ssrOutlet || DEFAULT_SSR_OUTLET;
|
|
52
73
|
const ssrState = config.ssrState || DEFAULT_SSR_STATE;
|
|
74
|
+
const ssrHead = config.ssrHead || DEFAULT_SSR_HEAD;
|
|
53
75
|
let ssrFallback = null;
|
|
54
76
|
if (ssrEntry) {
|
|
55
77
|
const fs = await import("node:fs/promises");
|
|
@@ -61,10 +83,12 @@ async function createSSRServer(options) {
|
|
|
61
83
|
let template = await fs.readFile(path.resolve(vite.config.root, "index.html"), "utf8");
|
|
62
84
|
template = await vite.transformIndexHtml(url, template);
|
|
63
85
|
const { render } = await vite.ssrLoadModule(ssrEntry);
|
|
64
|
-
const
|
|
65
|
-
res
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
const result = await render(url);
|
|
87
|
+
sendSSRResponse(res, template, {
|
|
88
|
+
ssrOutlet,
|
|
89
|
+
ssrState,
|
|
90
|
+
ssrHead
|
|
91
|
+
}, result);
|
|
68
92
|
} catch (error) {
|
|
69
93
|
vite.ssrFixStacktrace(error);
|
|
70
94
|
console.error(error);
|
|
@@ -89,8 +113,9 @@ async function createSSRServer(options) {
|
|
|
89
113
|
const path = await import("node:path");
|
|
90
114
|
const { createServer: createHttpServer } = await import("node:http");
|
|
91
115
|
const clientDir = opts.clientDir || path.resolve(__dirname, "../client");
|
|
92
|
-
const ssrOutlet = opts.ssrOutlet || (__MOOST_SSR_OUTLET__ !== void 0 ? __MOOST_SSR_OUTLET__ :
|
|
93
|
-
const ssrState = opts.ssrState || (__MOOST_SSR_STATE__ !== void 0 ? __MOOST_SSR_STATE__ :
|
|
116
|
+
const ssrOutlet = opts.ssrOutlet || (__MOOST_SSR_OUTLET__ !== void 0 ? __MOOST_SSR_OUTLET__ : DEFAULT_SSR_OUTLET);
|
|
117
|
+
const ssrState = opts.ssrState || (__MOOST_SSR_STATE__ !== void 0 ? __MOOST_SSR_STATE__ : DEFAULT_SSR_STATE);
|
|
118
|
+
const ssrHead = opts.ssrHead || (__MOOST_SSR_HEAD__ !== void 0 ? __MOOST_SSR_HEAD__ : DEFAULT_SSR_HEAD);
|
|
94
119
|
const prefixes = normalizePrefixes(opts.prefix ?? __MOOST_PREFIX__);
|
|
95
120
|
const defaultPort = opts.port || Number(process.env.PORT) || 3e3;
|
|
96
121
|
const template = await fs.readFile(path.resolve(clientDir, "index.html"), "utf8");
|
|
@@ -140,10 +165,12 @@ async function createSSRServer(options) {
|
|
|
140
165
|
}
|
|
141
166
|
try {
|
|
142
167
|
if (render) {
|
|
143
|
-
const
|
|
144
|
-
res
|
|
145
|
-
|
|
146
|
-
|
|
168
|
+
const result = await render(url);
|
|
169
|
+
sendSSRResponse(res, template, {
|
|
170
|
+
ssrOutlet,
|
|
171
|
+
ssrState,
|
|
172
|
+
ssrHead
|
|
173
|
+
}, result);
|
|
147
174
|
} else {
|
|
148
175
|
res.statusCode = 200;
|
|
149
176
|
res.setHeader("Content-Type", "text/html");
|
package/dist/prod-server.d.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The value an SSR `render(url)` function may return. Only `html` is required;
|
|
5
|
+
* every other field is optional, so a render returning `{ html }` (or
|
|
6
|
+
* `{ html, state }`) keeps working unchanged.
|
|
7
|
+
*/
|
|
8
|
+
interface TSSRRenderResult {
|
|
9
|
+
/** Server-rendered app markup, substituted into the `ssrOutlet` marker. */
|
|
10
|
+
html: string;
|
|
11
|
+
/**
|
|
12
|
+
* Serialized app state. Wrapped in `<script>window.__SSR_STATE__=…</script>`
|
|
13
|
+
* and substituted into the `ssrState` marker (nothing is emitted when absent).
|
|
14
|
+
*/
|
|
15
|
+
state?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Ready-to-insert `<head>` tags (`<title>`, `<meta>`, canonical, Open Graph,
|
|
18
|
+
* JSON-LD, …), substituted verbatim into the `ssrHead` marker. This is exactly
|
|
19
|
+
* the shape head managers emit (e.g. unhead's `renderSSRHead(head).headTags`).
|
|
20
|
+
*/
|
|
21
|
+
head?: string;
|
|
22
|
+
/** HTTP status code for the response (default `200`) — e.g. `404` for a soft-404, `301` for a redirect. */
|
|
23
|
+
status?: number;
|
|
24
|
+
/** Extra response headers — e.g. `cache-control`, or `location` alongside `status: 301`. */
|
|
25
|
+
headers?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
|
|
3
28
|
type TMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
|
|
4
29
|
interface TSSRServerOptions {
|
|
5
30
|
/** Override: lazy import for the Moost app entry. */
|
|
@@ -21,6 +46,8 @@ interface TSSRServerOptions {
|
|
|
21
46
|
ssrOutlet?: string;
|
|
22
47
|
/** Override: HTML placeholder for SSR state */
|
|
23
48
|
ssrState?: string;
|
|
49
|
+
/** Override: HTML placeholder for SSR-rendered `<head>` tags */
|
|
50
|
+
ssrHead?: string;
|
|
24
51
|
}
|
|
25
52
|
interface TSSRServer {
|
|
26
53
|
/** Add Connect-compatible middleware (runs in both dev and prod) */
|
|
@@ -31,4 +58,4 @@ interface TSSRServer {
|
|
|
31
58
|
declare function createSSRServer(options?: TSSRServerOptions): Promise<TSSRServer>;
|
|
32
59
|
|
|
33
60
|
export { createSSRServer };
|
|
34
|
-
export type { TSSRServer, TSSRServerOptions };
|
|
61
|
+
export type { TSSRRenderResult, TSSRServer, TSSRServerOptions };
|
package/dist/prod-server.mjs
CHANGED
|
@@ -6,6 +6,27 @@ import { createLogger } from "moost";
|
|
|
6
6
|
const PLUGIN_NAME = "moost-vite";
|
|
7
7
|
const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
|
|
8
8
|
const DEFAULT_SSR_STATE = "<!--ssr-state-->";
|
|
9
|
+
const DEFAULT_SSR_HEAD = "<!--ssr-head-->";
|
|
10
|
+
/**
|
|
11
|
+
* Applies an SSR render result to the HTTP response: sets the status code and
|
|
12
|
+
* headers, then substitutes the outlet, state and head markers into the HTML
|
|
13
|
+
* template. Shared by all three render call sites (the dev middleware, the dev
|
|
14
|
+
* `createSSRServer` branch and the prod `createSSRServer` branch) so the render
|
|
15
|
+
* contract stays identical across them.
|
|
16
|
+
*
|
|
17
|
+
* Fully backwards compatible: every field beyond `html` is optional, a missing
|
|
18
|
+
* marker in the template is a no-op `String.replace`, and headers from the render
|
|
19
|
+
* result are applied after the default `Content-Type: text/html` so a render can
|
|
20
|
+
* override it. Replacements use a function argument so `$`-sequences in the
|
|
21
|
+
* rendered payloads (JSON state, JSON-LD `<head>` tags) are inserted literally
|
|
22
|
+
* rather than interpreted as `String.replace` special patterns.
|
|
23
|
+
*/ function sendSSRResponse(res, template, markers, result) {
|
|
24
|
+
res.statusCode = result.status ?? 200;
|
|
25
|
+
res.setHeader("Content-Type", "text/html");
|
|
26
|
+
if (result.headers) for (const [key, value] of Object.entries(result.headers)) res.setHeader(key, value);
|
|
27
|
+
const stateScript = result.state ? `<script>window.__SSR_STATE__=${result.state}<\/script>` : "";
|
|
28
|
+
res.end(template.replace(markers.ssrOutlet, () => result.html).replace(markers.ssrState, () => stateScript).replace(markers.ssrHead, () => result.head ?? ""));
|
|
29
|
+
}
|
|
9
30
|
/**
|
|
10
31
|
* Normalizes the `prefix` option (single mount or list of mounts) into a list of
|
|
11
32
|
* normalized entries: leading slash ensured, trailing slash stripped.
|
|
@@ -51,6 +72,7 @@ async function createSSRServer(options) {
|
|
|
51
72
|
const ssrEntry = config.ssrEntry;
|
|
52
73
|
const ssrOutlet = config.ssrOutlet || DEFAULT_SSR_OUTLET;
|
|
53
74
|
const ssrState = config.ssrState || DEFAULT_SSR_STATE;
|
|
75
|
+
const ssrHead = config.ssrHead || DEFAULT_SSR_HEAD;
|
|
54
76
|
let ssrFallback = null;
|
|
55
77
|
if (ssrEntry) {
|
|
56
78
|
const fs = await import("node:fs/promises");
|
|
@@ -62,10 +84,12 @@ async function createSSRServer(options) {
|
|
|
62
84
|
let template = await fs.readFile(path.resolve(vite.config.root, "index.html"), "utf8");
|
|
63
85
|
template = await vite.transformIndexHtml(url, template);
|
|
64
86
|
const { render } = await vite.ssrLoadModule(ssrEntry);
|
|
65
|
-
const
|
|
66
|
-
res
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
const result = await render(url);
|
|
88
|
+
sendSSRResponse(res, template, {
|
|
89
|
+
ssrOutlet,
|
|
90
|
+
ssrState,
|
|
91
|
+
ssrHead
|
|
92
|
+
}, result);
|
|
69
93
|
} catch (error) {
|
|
70
94
|
vite.ssrFixStacktrace(error);
|
|
71
95
|
console.error(error);
|
|
@@ -90,8 +114,9 @@ async function createSSRServer(options) {
|
|
|
90
114
|
const path = await import("node:path");
|
|
91
115
|
const { createServer: createHttpServer } = await import("node:http");
|
|
92
116
|
const clientDir = opts.clientDir || path.resolve(import.meta.dirname, "../client");
|
|
93
|
-
const ssrOutlet = opts.ssrOutlet || (__MOOST_SSR_OUTLET__ !== void 0 ? __MOOST_SSR_OUTLET__ :
|
|
94
|
-
const ssrState = opts.ssrState || (__MOOST_SSR_STATE__ !== void 0 ? __MOOST_SSR_STATE__ :
|
|
117
|
+
const ssrOutlet = opts.ssrOutlet || (__MOOST_SSR_OUTLET__ !== void 0 ? __MOOST_SSR_OUTLET__ : DEFAULT_SSR_OUTLET);
|
|
118
|
+
const ssrState = opts.ssrState || (__MOOST_SSR_STATE__ !== void 0 ? __MOOST_SSR_STATE__ : DEFAULT_SSR_STATE);
|
|
119
|
+
const ssrHead = opts.ssrHead || (__MOOST_SSR_HEAD__ !== void 0 ? __MOOST_SSR_HEAD__ : DEFAULT_SSR_HEAD);
|
|
95
120
|
const prefixes = normalizePrefixes(opts.prefix ?? __MOOST_PREFIX__);
|
|
96
121
|
const defaultPort = opts.port || Number(process.env.PORT) || 3e3;
|
|
97
122
|
const template = await fs.readFile(path.resolve(clientDir, "index.html"), "utf8");
|
|
@@ -141,10 +166,12 @@ async function createSSRServer(options) {
|
|
|
141
166
|
}
|
|
142
167
|
try {
|
|
143
168
|
if (render) {
|
|
144
|
-
const
|
|
145
|
-
res
|
|
146
|
-
|
|
147
|
-
|
|
169
|
+
const result = await render(url);
|
|
170
|
+
sendSSRResponse(res, template, {
|
|
171
|
+
ssrOutlet,
|
|
172
|
+
ssrState,
|
|
173
|
+
ssrHead
|
|
174
|
+
}, result);
|
|
148
175
|
} else {
|
|
149
176
|
res.statusCode = 200;
|
|
150
177
|
res.setHeader("Content-Type", "text/html");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moostjs/vite",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.30",
|
|
4
4
|
"description": "Vite Dev plugin for moostjs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"composables",
|
|
@@ -58,8 +58,8 @@
|
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"sirv": "^3.0.0",
|
|
60
60
|
"vite": "^8.0.0",
|
|
61
|
-
"@moostjs/event-http": "^0.6.
|
|
62
|
-
"moost": "^0.6.
|
|
61
|
+
"@moostjs/event-http": "^0.6.30",
|
|
62
|
+
"moost": "^0.6.30"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"sirv": {
|