@moostjs/vite 0.6.29 → 0.6.31
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/dist/index.cjs +79 -6
- package/dist/index.d.ts +55 -0
- package/dist/index.mjs +79 -6
- package/dist/prod-server.cjs +43 -2
- package/dist/prod-server.d.ts +24 -1
- package/dist/prod-server.mjs +43 -2
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -40,6 +40,32 @@ const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
|
|
|
40
40
|
const DEFAULT_SSR_STATE = "<!--ssr-state-->";
|
|
41
41
|
const DEFAULT_SSR_HEAD = "<!--ssr-head-->";
|
|
42
42
|
/**
|
|
43
|
+
* Invokes the SSR `render()` for a page request. Shared by all three render call
|
|
44
|
+
* sites so the render contract stays identical across them:
|
|
45
|
+
* - always passes the page request context ({@link TSSRRenderContext}) as the
|
|
46
|
+
* second `render()` argument;
|
|
47
|
+
* - when a Moost HTTP adapter is provided (and supports it), runs the render
|
|
48
|
+
* inside an HTTP event context seeded from the real `(req, res)` pair, so
|
|
49
|
+
* SSR-time `fetch('/api/...')` self-calls inherit the viewer's identity
|
|
50
|
+
* headers (`forwardHeaders`) instead of arriving anonymous, and `Set-Cookie`
|
|
51
|
+
* headers from those self-calls are drained onto the page response;
|
|
52
|
+
* - degrades to a plain `render(url, ctx)` call when the adapter is absent or
|
|
53
|
+
* predates `withHttpContext`.
|
|
54
|
+
*/ async function renderSSRPage(opts) {
|
|
55
|
+
const { render, url, req, res, http } = opts;
|
|
56
|
+
const ctx = {
|
|
57
|
+
headers: req.headers,
|
|
58
|
+
method: req.method || "GET",
|
|
59
|
+
req
|
|
60
|
+
};
|
|
61
|
+
if (http && typeof http.withHttpContext === "function") {
|
|
62
|
+
const { result, response } = await http.withHttpContext(req, res, () => render(url, ctx));
|
|
63
|
+
for (const cookie of response.getSetCookieStrings()) res.appendHeader("set-cookie", cookie);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
return render(url, ctx);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
43
69
|
* Applies an SSR render result to the HTTP response: sets the status code and
|
|
44
70
|
* headers, then substitutes the outlet, state and head markers into the HTML
|
|
45
71
|
* template. Shared by all three render call sites (the dev middleware, the dev
|
|
@@ -389,11 +415,33 @@ function moostVite(options) {
|
|
|
389
415
|
const prefixes = normalizePrefixes(options.prefix);
|
|
390
416
|
let moostMiddleware = null;
|
|
391
417
|
let localFetchTeardown = null;
|
|
418
|
+
/**
|
|
419
|
+
* Live ref to the booted MoostHttp instance (from Vite's SSR module graph).
|
|
420
|
+
* Used to run SSR renders inside an HTTP event context (`withHttpContext`)
|
|
421
|
+
* so SSR self-fetches inherit the page request's identity. Exposed on the
|
|
422
|
+
* resolved config for the `createSSRServer()` dev branch; re-pointed on every
|
|
423
|
+
* boot, cleared on eject.
|
|
424
|
+
*/ const moostHttpRef = { current: null };
|
|
425
|
+
const ssrForwarding = options.ssrFetchForwarding !== false;
|
|
426
|
+
/**
|
|
427
|
+
* Boot-identity stamps (dev-only "mongrel state" diagnostic — a stale pipeline
|
|
428
|
+
* presents as security middleware silently switched off). `bootGeneration` is
|
|
429
|
+
* bumped on every eject; `bootingGeneration` records which generation the
|
|
430
|
+
* in-flight/most recent boot belongs to (set by runReload). When listen()
|
|
431
|
+
* captures a middleware for a boot that is no longer current — e.g. a delayed
|
|
432
|
+
* listen() from a torn boot racing a newer eject — it logs loudly.
|
|
433
|
+
*/ let bootGeneration = 0;
|
|
434
|
+
let bootingGeneration = 0;
|
|
435
|
+
/** Whether the HTTP listen() patch has ever captured a middleware (i.e. this is an HTTP app). */ let httpCaptured = false;
|
|
436
|
+
/** Module IDs awaiting DI cleanup — consumed in runReload; see ejectApp. */ let pendingCleanup = null;
|
|
392
437
|
/** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
|
|
393
438
|
const adapters = isTest ? [] : [
|
|
394
439
|
createAdapterDetector("http", (MoostHttp, moduleExports) => {
|
|
395
440
|
MoostHttp.prototype.listen = function(...args) {
|
|
396
441
|
logger.log(`🔌 [2mOvertaking HTTP.listen`);
|
|
442
|
+
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.`);
|
|
443
|
+
httpCaptured = true;
|
|
444
|
+
moostHttpRef.current = this;
|
|
397
445
|
if (options.middleware) moostMiddleware = this.getServerCb((req) => {
|
|
398
446
|
pendingNextMap.get(req)?.();
|
|
399
447
|
});
|
|
@@ -433,15 +481,24 @@ function moostVite(options) {
|
|
|
433
481
|
*/ const getEntryModule = (moduleGraph) => moduleGraph.getModuleByUrl(options.entry);
|
|
434
482
|
/**
|
|
435
483
|
* Drops the captured Moost app so the next request triggers a full reload:
|
|
436
|
-
* releases the middleware + local fetch and
|
|
437
|
-
*
|
|
484
|
+
* releases the middleware + local fetch and queues the changed module IDs for
|
|
485
|
+
* DI cleanup. The cleanup itself (infact ejects, wooks reset) is deferred to
|
|
486
|
+
* runReload, under the reload lock: running it here, per hot-update wave,
|
|
487
|
+
* raced in-flight requests and mid-boot imports mutating the same global
|
|
488
|
+
* containers — ejected-but-still-referenced instances were lazily re-created
|
|
489
|
+
* into the OLD pipeline, producing a mongrel half-old/half-new app. Waves
|
|
490
|
+
* arriving before the lazy reload (editor bulk-save storms) coalesce into one
|
|
491
|
+
* pending set and one cleanup.
|
|
438
492
|
*/ const ejectApp = (cleanupInstances) => {
|
|
493
|
+
bootGeneration++;
|
|
439
494
|
moostMiddleware = null;
|
|
495
|
+
moostHttpRef.current = null;
|
|
440
496
|
if (localFetchTeardown) {
|
|
441
497
|
localFetchTeardown();
|
|
442
498
|
localFetchTeardown = null;
|
|
443
499
|
}
|
|
444
|
-
|
|
500
|
+
if (pendingCleanup) for (const id of cleanupInstances) pendingCleanup.add(id);
|
|
501
|
+
else pendingCleanup = cleanupInstances;
|
|
445
502
|
reloadRequired = true;
|
|
446
503
|
};
|
|
447
504
|
patchMoostHandlerLogging();
|
|
@@ -455,7 +512,8 @@ function moostVite(options) {
|
|
|
455
512
|
const serverDefines = {
|
|
456
513
|
"process.env.MOOST_DEFERRED_ENV": "\"production\"",
|
|
457
514
|
__MOOST_ENTRY__: JSON.stringify(`./${entryKey}.js`),
|
|
458
|
-
__MOOST_PREFIX__: JSON.stringify(prefixes ?? null)
|
|
515
|
+
__MOOST_PREFIX__: JSON.stringify(prefixes ?? null),
|
|
516
|
+
__MOOST_SSR_FORWARDING__: JSON.stringify(ssrForwarding)
|
|
459
517
|
};
|
|
460
518
|
const ssrInput = {
|
|
461
519
|
server: options.serverEntry || generatedServerEntry(cfg.root),
|
|
@@ -557,8 +615,10 @@ function moostVite(options) {
|
|
|
557
615
|
port: options.port,
|
|
558
616
|
ssrOutlet: options.ssrOutlet,
|
|
559
617
|
ssrState: options.ssrState,
|
|
560
|
-
ssrHead: options.ssrHead
|
|
618
|
+
ssrHead: options.ssrHead,
|
|
619
|
+
ssrFetchForwarding: options.ssrFetchForwarding
|
|
561
620
|
};
|
|
621
|
+
config.__moostViteHttpRef = moostHttpRef;
|
|
562
622
|
},
|
|
563
623
|
async transform(code, id) {
|
|
564
624
|
if (!id.endsWith(".ts")) return null;
|
|
@@ -595,6 +655,8 @@ function moostVite(options) {
|
|
|
595
655
|
bootError = null;
|
|
596
656
|
reloadRequired = false;
|
|
597
657
|
reloadPromise = null;
|
|
658
|
+
pendingCleanup = null;
|
|
659
|
+
bootingGeneration = bootGeneration;
|
|
598
660
|
const runReload = () => {
|
|
599
661
|
reloadRequired = false;
|
|
600
662
|
console.log();
|
|
@@ -602,11 +664,16 @@ function moostVite(options) {
|
|
|
602
664
|
console.log();
|
|
603
665
|
return (async () => {
|
|
604
666
|
try {
|
|
667
|
+
bootingGeneration = bootGeneration;
|
|
668
|
+
const cleanupInstances = pendingCleanup ?? void 0;
|
|
669
|
+
pendingCleanup = null;
|
|
670
|
+
moostRestartCleanup(adapters, options.onEject, cleanupInstances);
|
|
605
671
|
for (const adapter of adapters) if (adapter.detected) await adapter.init();
|
|
606
672
|
const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
|
|
607
673
|
if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
|
|
608
674
|
await ssrImport(options.entry);
|
|
609
675
|
bootError = null;
|
|
676
|
+
if (httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
|
|
610
677
|
} catch (error) {
|
|
611
678
|
bootError = error;
|
|
612
679
|
logger.error(`✖️ Failed to reload Moost App: ${error.message}`);
|
|
@@ -684,7 +751,13 @@ function moostVite(options) {
|
|
|
684
751
|
let template = await fs.readFile((0, node_path.resolve)(server.config.root, "index.html"), "utf8");
|
|
685
752
|
template = await server.transformIndexHtml(url, template);
|
|
686
753
|
const { render } = await server.ssrLoadModule(options.ssrEntry);
|
|
687
|
-
const result = await
|
|
754
|
+
const result = await renderSSRPage({
|
|
755
|
+
render,
|
|
756
|
+
url,
|
|
757
|
+
req,
|
|
758
|
+
res,
|
|
759
|
+
http: ssrForwarding ? moostHttpRef.current : null
|
|
760
|
+
});
|
|
688
761
|
sendSSRResponse(res, template, {
|
|
689
762
|
ssrOutlet,
|
|
690
763
|
ssrState,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PluginOption } from 'vite';
|
|
2
|
+
import { IncomingMessage } from 'node:http';
|
|
2
3
|
|
|
3
4
|
interface TMoostViteDevOptions {
|
|
4
5
|
/**
|
|
@@ -76,6 +77,20 @@ interface TMoostViteDevOptions {
|
|
|
76
77
|
* Default: `true`.
|
|
77
78
|
*/
|
|
78
79
|
ssrFetch?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* Run each SSR render inside an HTTP event context seeded from the incoming
|
|
82
|
+
* page request. SSR-time `fetch('/api/...')` self-calls then inherit the
|
|
83
|
+
* viewer's identity headers (authorization, cookie, accept-language,
|
|
84
|
+
* x-forwarded-for, x-request-id — see the HTTP adapter's `forwardHeaders`
|
|
85
|
+
* option) instead of arriving anonymous, and `Set-Cookie` headers they
|
|
86
|
+
* produce are drained onto the page response. Applies in dev and is baked
|
|
87
|
+
* into the generated production server.
|
|
88
|
+
*
|
|
89
|
+
* Set to `false` to keep SSR self-calls anonymous.
|
|
90
|
+
*
|
|
91
|
+
* Default: `true`.
|
|
92
|
+
*/
|
|
93
|
+
ssrFetchForwarding?: boolean;
|
|
79
94
|
/**
|
|
80
95
|
* Run Moost as Connect middleware instead of taking over the server.
|
|
81
96
|
* When enabled, Moost runs first and unmatched requests fall through to
|
|
@@ -161,4 +176,44 @@ interface TMoostViteDevOptions {
|
|
|
161
176
|
}
|
|
162
177
|
declare function moostVite(options: TMoostViteDevOptions): PluginOption;
|
|
163
178
|
|
|
179
|
+
/**
|
|
180
|
+
* The value an SSR `render(url)` function may return. Only `html` is required;
|
|
181
|
+
* every other field is optional, so a render returning `{ html }` (or
|
|
182
|
+
* `{ html, state }`) keeps working unchanged.
|
|
183
|
+
*/
|
|
184
|
+
interface TSSRRenderResult {
|
|
185
|
+
/** Server-rendered app markup, substituted into the `ssrOutlet` marker. */
|
|
186
|
+
html: string;
|
|
187
|
+
/**
|
|
188
|
+
* Serialized app state. Wrapped in `<script>window.__SSR_STATE__=…</script>`
|
|
189
|
+
* and substituted into the `ssrState` marker (nothing is emitted when absent).
|
|
190
|
+
*/
|
|
191
|
+
state?: string;
|
|
192
|
+
/**
|
|
193
|
+
* Ready-to-insert `<head>` tags (`<title>`, `<meta>`, canonical, Open Graph,
|
|
194
|
+
* JSON-LD, …), substituted verbatim into the `ssrHead` marker. This is exactly
|
|
195
|
+
* the shape head managers emit (e.g. unhead's `renderSSRHead(head).headTags`).
|
|
196
|
+
*/
|
|
197
|
+
head?: string;
|
|
198
|
+
/** HTTP status code for the response (default `200`) — e.g. `404` for a soft-404, `301` for a redirect. */
|
|
199
|
+
status?: number;
|
|
200
|
+
/** Extra response headers — e.g. `cache-control`, or `location` alongside `status: 301`. */
|
|
201
|
+
headers?: Record<string, string>;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Context of the incoming page request, passed as the second argument to the
|
|
205
|
+
* SSR `render()` entry. Additive: entries typed `render(url)` keep working.
|
|
206
|
+
*/
|
|
207
|
+
interface TSSRRenderContext {
|
|
208
|
+
/** Raw request headers of the page request (node `IncomingMessage.headers`). */
|
|
209
|
+
headers: IncomingMessage['headers'];
|
|
210
|
+
/** HTTP method of the page request (the SSR fallbacks only render `GET` today). */
|
|
211
|
+
method: string;
|
|
212
|
+
/** Escape hatch for everything else (socket address, etc.). */
|
|
213
|
+
req: IncomingMessage;
|
|
214
|
+
}
|
|
215
|
+
/** The SSR entry contract: `render(url, ctx?)`. `ctx` is new and optional. */
|
|
216
|
+
type TSSRRender = (url: string, ctx?: TSSRRenderContext) => Promise<TSSRRenderResult> | TSSRRenderResult;
|
|
217
|
+
|
|
164
218
|
export { moostVite };
|
|
219
|
+
export type { TMoostViteDevOptions, TSSRRender, TSSRRenderContext, TSSRRenderResult };
|
package/dist/index.mjs
CHANGED
|
@@ -11,6 +11,32 @@ const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
|
|
|
11
11
|
const DEFAULT_SSR_STATE = "<!--ssr-state-->";
|
|
12
12
|
const DEFAULT_SSR_HEAD = "<!--ssr-head-->";
|
|
13
13
|
/**
|
|
14
|
+
* Invokes the SSR `render()` for a page request. Shared by all three render call
|
|
15
|
+
* sites so the render contract stays identical across them:
|
|
16
|
+
* - always passes the page request context ({@link TSSRRenderContext}) as the
|
|
17
|
+
* second `render()` argument;
|
|
18
|
+
* - when a Moost HTTP adapter is provided (and supports it), runs the render
|
|
19
|
+
* inside an HTTP event context seeded from the real `(req, res)` pair, so
|
|
20
|
+
* SSR-time `fetch('/api/...')` self-calls inherit the viewer's identity
|
|
21
|
+
* headers (`forwardHeaders`) instead of arriving anonymous, and `Set-Cookie`
|
|
22
|
+
* headers from those self-calls are drained onto the page response;
|
|
23
|
+
* - degrades to a plain `render(url, ctx)` call when the adapter is absent or
|
|
24
|
+
* predates `withHttpContext`.
|
|
25
|
+
*/ async function renderSSRPage(opts) {
|
|
26
|
+
const { render, url, req, res, http } = opts;
|
|
27
|
+
const ctx = {
|
|
28
|
+
headers: req.headers,
|
|
29
|
+
method: req.method || "GET",
|
|
30
|
+
req
|
|
31
|
+
};
|
|
32
|
+
if (http && typeof http.withHttpContext === "function") {
|
|
33
|
+
const { result, response } = await http.withHttpContext(req, res, () => render(url, ctx));
|
|
34
|
+
for (const cookie of response.getSetCookieStrings()) res.appendHeader("set-cookie", cookie);
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
return render(url, ctx);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
14
40
|
* Applies an SSR render result to the HTTP response: sets the status code and
|
|
15
41
|
* headers, then substitutes the outlet, state and head markers into the HTML
|
|
16
42
|
* template. Shared by all three render call sites (the dev middleware, the dev
|
|
@@ -358,11 +384,33 @@ function moostVite(options) {
|
|
|
358
384
|
const prefixes = normalizePrefixes(options.prefix);
|
|
359
385
|
let moostMiddleware = null;
|
|
360
386
|
let localFetchTeardown = null;
|
|
387
|
+
/**
|
|
388
|
+
* Live ref to the booted MoostHttp instance (from Vite's SSR module graph).
|
|
389
|
+
* Used to run SSR renders inside an HTTP event context (`withHttpContext`)
|
|
390
|
+
* so SSR self-fetches inherit the page request's identity. Exposed on the
|
|
391
|
+
* resolved config for the `createSSRServer()` dev branch; re-pointed on every
|
|
392
|
+
* boot, cleared on eject.
|
|
393
|
+
*/ const moostHttpRef = { current: null };
|
|
394
|
+
const ssrForwarding = options.ssrFetchForwarding !== false;
|
|
395
|
+
/**
|
|
396
|
+
* Boot-identity stamps (dev-only "mongrel state" diagnostic — a stale pipeline
|
|
397
|
+
* presents as security middleware silently switched off). `bootGeneration` is
|
|
398
|
+
* bumped on every eject; `bootingGeneration` records which generation the
|
|
399
|
+
* in-flight/most recent boot belongs to (set by runReload). When listen()
|
|
400
|
+
* captures a middleware for a boot that is no longer current — e.g. a delayed
|
|
401
|
+
* listen() from a torn boot racing a newer eject — it logs loudly.
|
|
402
|
+
*/ let bootGeneration = 0;
|
|
403
|
+
let bootingGeneration = 0;
|
|
404
|
+
/** Whether the HTTP listen() patch has ever captured a middleware (i.e. this is an HTTP app). */ let httpCaptured = false;
|
|
405
|
+
/** Module IDs awaiting DI cleanup — consumed in runReload; see ejectApp. */ let pendingCleanup = null;
|
|
361
406
|
/** In middleware mode: maps req → next() for the onNoMatch callback */ const pendingNextMap = /* @__PURE__ */ new WeakMap();
|
|
362
407
|
const adapters = [
|
|
363
408
|
createAdapterDetector("http", (MoostHttp, moduleExports) => {
|
|
364
409
|
MoostHttp.prototype.listen = function(...args) {
|
|
365
410
|
logger.log(`🔌 [2mOvertaking HTTP.listen`);
|
|
411
|
+
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.`);
|
|
412
|
+
httpCaptured = true;
|
|
413
|
+
moostHttpRef.current = this;
|
|
366
414
|
if (options.middleware) moostMiddleware = this.getServerCb((req) => {
|
|
367
415
|
pendingNextMap.get(req)?.();
|
|
368
416
|
});
|
|
@@ -402,15 +450,24 @@ function moostVite(options) {
|
|
|
402
450
|
*/ const getEntryModule = (moduleGraph) => moduleGraph.getModuleByUrl(options.entry);
|
|
403
451
|
/**
|
|
404
452
|
* Drops the captured Moost app so the next request triggers a full reload:
|
|
405
|
-
* releases the middleware + local fetch and
|
|
406
|
-
*
|
|
453
|
+
* releases the middleware + local fetch and queues the changed module IDs for
|
|
454
|
+
* DI cleanup. The cleanup itself (infact ejects, wooks reset) is deferred to
|
|
455
|
+
* runReload, under the reload lock: running it here, per hot-update wave,
|
|
456
|
+
* raced in-flight requests and mid-boot imports mutating the same global
|
|
457
|
+
* containers — ejected-but-still-referenced instances were lazily re-created
|
|
458
|
+
* into the OLD pipeline, producing a mongrel half-old/half-new app. Waves
|
|
459
|
+
* arriving before the lazy reload (editor bulk-save storms) coalesce into one
|
|
460
|
+
* pending set and one cleanup.
|
|
407
461
|
*/ const ejectApp = (cleanupInstances) => {
|
|
462
|
+
bootGeneration++;
|
|
408
463
|
moostMiddleware = null;
|
|
464
|
+
moostHttpRef.current = null;
|
|
409
465
|
if (localFetchTeardown) {
|
|
410
466
|
localFetchTeardown();
|
|
411
467
|
localFetchTeardown = null;
|
|
412
468
|
}
|
|
413
|
-
|
|
469
|
+
if (pendingCleanup) for (const id of cleanupInstances) pendingCleanup.add(id);
|
|
470
|
+
else pendingCleanup = cleanupInstances;
|
|
414
471
|
reloadRequired = true;
|
|
415
472
|
};
|
|
416
473
|
patchMoostHandlerLogging();
|
|
@@ -424,7 +481,8 @@ function moostVite(options) {
|
|
|
424
481
|
const serverDefines = {
|
|
425
482
|
"process.env.MOOST_DEFERRED_ENV": "\"production\"",
|
|
426
483
|
__MOOST_ENTRY__: JSON.stringify(`./${entryKey}.js`),
|
|
427
|
-
__MOOST_PREFIX__: JSON.stringify(prefixes ?? null)
|
|
484
|
+
__MOOST_PREFIX__: JSON.stringify(prefixes ?? null),
|
|
485
|
+
__MOOST_SSR_FORWARDING__: JSON.stringify(ssrForwarding)
|
|
428
486
|
};
|
|
429
487
|
const ssrInput = {
|
|
430
488
|
server: options.serverEntry || generatedServerEntry(cfg.root),
|
|
@@ -526,8 +584,10 @@ function moostVite(options) {
|
|
|
526
584
|
port: options.port,
|
|
527
585
|
ssrOutlet: options.ssrOutlet,
|
|
528
586
|
ssrState: options.ssrState,
|
|
529
|
-
ssrHead: options.ssrHead
|
|
587
|
+
ssrHead: options.ssrHead,
|
|
588
|
+
ssrFetchForwarding: options.ssrFetchForwarding
|
|
530
589
|
};
|
|
590
|
+
config.__moostViteHttpRef = moostHttpRef;
|
|
531
591
|
},
|
|
532
592
|
async transform(code, id) {
|
|
533
593
|
if (!id.endsWith(".ts")) return null;
|
|
@@ -564,6 +624,8 @@ function moostVite(options) {
|
|
|
564
624
|
bootError = null;
|
|
565
625
|
reloadRequired = false;
|
|
566
626
|
reloadPromise = null;
|
|
627
|
+
pendingCleanup = null;
|
|
628
|
+
bootingGeneration = bootGeneration;
|
|
567
629
|
const runReload = () => {
|
|
568
630
|
reloadRequired = false;
|
|
569
631
|
console.log();
|
|
@@ -571,11 +633,16 @@ function moostVite(options) {
|
|
|
571
633
|
console.log();
|
|
572
634
|
return (async () => {
|
|
573
635
|
try {
|
|
636
|
+
bootingGeneration = bootGeneration;
|
|
637
|
+
const cleanupInstances = pendingCleanup ?? void 0;
|
|
638
|
+
pendingCleanup = null;
|
|
639
|
+
moostRestartCleanup(adapters, options.onEject, cleanupInstances);
|
|
574
640
|
for (const adapter of adapters) if (adapter.detected) await adapter.init();
|
|
575
641
|
const entryModule = await getEntryModule(server.environments.ssr.moduleGraph);
|
|
576
642
|
if (entryModule?.transformResult) server.environments.ssr.moduleGraph.invalidateModule(entryModule);
|
|
577
643
|
await ssrImport(options.entry);
|
|
578
644
|
bootError = null;
|
|
645
|
+
if (httpCaptured && !moostMiddleware) logger.error(`⚠️ Moost app reloaded but no HTTP middleware was captured — the entry did not re-run listen().`);
|
|
579
646
|
} catch (error) {
|
|
580
647
|
bootError = error;
|
|
581
648
|
logger.error(`✖️ Failed to reload Moost App: ${error.message}`);
|
|
@@ -645,7 +712,13 @@ function moostVite(options) {
|
|
|
645
712
|
let template = await fs.readFile(resolve(server.config.root, "index.html"), "utf8");
|
|
646
713
|
template = await server.transformIndexHtml(url, template);
|
|
647
714
|
const { render } = await server.ssrLoadModule(options.ssrEntry);
|
|
648
|
-
const result = await
|
|
715
|
+
const result = await renderSSRPage({
|
|
716
|
+
render,
|
|
717
|
+
url,
|
|
718
|
+
req,
|
|
719
|
+
res,
|
|
720
|
+
http: ssrForwarding ? moostHttpRef.current : null
|
|
721
|
+
});
|
|
649
722
|
sendSSRResponse(res, template, {
|
|
650
723
|
ssrOutlet,
|
|
651
724
|
ssrState,
|
package/dist/prod-server.cjs
CHANGED
|
@@ -7,6 +7,32 @@ const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
|
|
|
7
7
|
const DEFAULT_SSR_STATE = "<!--ssr-state-->";
|
|
8
8
|
const DEFAULT_SSR_HEAD = "<!--ssr-head-->";
|
|
9
9
|
/**
|
|
10
|
+
* Invokes the SSR `render()` for a page request. Shared by all three render call
|
|
11
|
+
* sites so the render contract stays identical across them:
|
|
12
|
+
* - always passes the page request context ({@link TSSRRenderContext}) as the
|
|
13
|
+
* second `render()` argument;
|
|
14
|
+
* - when a Moost HTTP adapter is provided (and supports it), runs the render
|
|
15
|
+
* inside an HTTP event context seeded from the real `(req, res)` pair, so
|
|
16
|
+
* SSR-time `fetch('/api/...')` self-calls inherit the viewer's identity
|
|
17
|
+
* headers (`forwardHeaders`) instead of arriving anonymous, and `Set-Cookie`
|
|
18
|
+
* headers from those self-calls are drained onto the page response;
|
|
19
|
+
* - degrades to a plain `render(url, ctx)` call when the adapter is absent or
|
|
20
|
+
* predates `withHttpContext`.
|
|
21
|
+
*/ async function renderSSRPage(opts) {
|
|
22
|
+
const { render, url, req, res, http } = opts;
|
|
23
|
+
const ctx = {
|
|
24
|
+
headers: req.headers,
|
|
25
|
+
method: req.method || "GET",
|
|
26
|
+
req
|
|
27
|
+
};
|
|
28
|
+
if (http && typeof http.withHttpContext === "function") {
|
|
29
|
+
const { result, response } = await http.withHttpContext(req, res, () => render(url, ctx));
|
|
30
|
+
for (const cookie of response.getSetCookieStrings()) res.appendHeader("set-cookie", cookie);
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
return render(url, ctx);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
10
36
|
* Applies an SSR render result to the HTTP response: sets the status code and
|
|
11
37
|
* headers, then substitutes the outlet, state and head markers into the HTML
|
|
12
38
|
* template. Shared by all three render call sites (the dev middleware, the dev
|
|
@@ -72,6 +98,8 @@ async function createSSRServer(options) {
|
|
|
72
98
|
const ssrOutlet = config.ssrOutlet || DEFAULT_SSR_OUTLET;
|
|
73
99
|
const ssrState = config.ssrState || DEFAULT_SSR_STATE;
|
|
74
100
|
const ssrHead = config.ssrHead || DEFAULT_SSR_HEAD;
|
|
101
|
+
const ssrForwarding = config.ssrFetchForwarding !== false;
|
|
102
|
+
const httpRef = vite.config.__moostViteHttpRef;
|
|
75
103
|
let ssrFallback = null;
|
|
76
104
|
if (ssrEntry) {
|
|
77
105
|
const fs = await import("node:fs/promises");
|
|
@@ -83,7 +111,13 @@ async function createSSRServer(options) {
|
|
|
83
111
|
let template = await fs.readFile(path.resolve(vite.config.root, "index.html"), "utf8");
|
|
84
112
|
template = await vite.transformIndexHtml(url, template);
|
|
85
113
|
const { render } = await vite.ssrLoadModule(ssrEntry);
|
|
86
|
-
const result = await
|
|
114
|
+
const result = await renderSSRPage({
|
|
115
|
+
render,
|
|
116
|
+
url,
|
|
117
|
+
req,
|
|
118
|
+
res,
|
|
119
|
+
http: ssrForwarding ? httpRef?.current : null
|
|
120
|
+
});
|
|
87
121
|
sendSSRResponse(res, template, {
|
|
88
122
|
ssrOutlet,
|
|
89
123
|
ssrState,
|
|
@@ -117,6 +151,7 @@ async function createSSRServer(options) {
|
|
|
117
151
|
const ssrState = opts.ssrState || (__MOOST_SSR_STATE__ !== void 0 ? __MOOST_SSR_STATE__ : DEFAULT_SSR_STATE);
|
|
118
152
|
const ssrHead = opts.ssrHead || (__MOOST_SSR_HEAD__ !== void 0 ? __MOOST_SSR_HEAD__ : DEFAULT_SSR_HEAD);
|
|
119
153
|
const prefixes = normalizePrefixes(opts.prefix ?? __MOOST_PREFIX__);
|
|
154
|
+
const ssrForwarding = opts.ssrFetchForwarding ?? __MOOST_SSR_FORWARDING__ ?? true;
|
|
120
155
|
const defaultPort = opts.port || Number(process.env.PORT) || 3e3;
|
|
121
156
|
const template = await fs.readFile(path.resolve(clientDir, "index.html"), "utf8");
|
|
122
157
|
let render = null;
|
|
@@ -165,7 +200,13 @@ async function createSSRServer(options) {
|
|
|
165
200
|
}
|
|
166
201
|
try {
|
|
167
202
|
if (render) {
|
|
168
|
-
const result = await
|
|
203
|
+
const result = await renderSSRPage({
|
|
204
|
+
render,
|
|
205
|
+
url,
|
|
206
|
+
req,
|
|
207
|
+
res,
|
|
208
|
+
http: ssrForwarding ? moostHttpRef.instance : null
|
|
209
|
+
});
|
|
169
210
|
sendSSRResponse(res, template, {
|
|
170
211
|
ssrOutlet,
|
|
171
212
|
ssrState,
|
package/dist/prod-server.d.ts
CHANGED
|
@@ -24,6 +24,20 @@ interface TSSRRenderResult {
|
|
|
24
24
|
/** Extra response headers — e.g. `cache-control`, or `location` alongside `status: 301`. */
|
|
25
25
|
headers?: Record<string, string>;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Context of the incoming page request, passed as the second argument to the
|
|
29
|
+
* SSR `render()` entry. Additive: entries typed `render(url)` keep working.
|
|
30
|
+
*/
|
|
31
|
+
interface TSSRRenderContext {
|
|
32
|
+
/** Raw request headers of the page request (node `IncomingMessage.headers`). */
|
|
33
|
+
headers: IncomingMessage['headers'];
|
|
34
|
+
/** HTTP method of the page request (the SSR fallbacks only render `GET` today). */
|
|
35
|
+
method: string;
|
|
36
|
+
/** Escape hatch for everything else (socket address, etc.). */
|
|
37
|
+
req: IncomingMessage;
|
|
38
|
+
}
|
|
39
|
+
/** The SSR entry contract: `render(url, ctx?)`. `ctx` is new and optional. */
|
|
40
|
+
type TSSRRender = (url: string, ctx?: TSSRRenderContext) => Promise<TSSRRenderResult> | TSSRRenderResult;
|
|
27
41
|
|
|
28
42
|
type TMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
|
|
29
43
|
interface TSSRServerOptions {
|
|
@@ -48,6 +62,15 @@ interface TSSRServerOptions {
|
|
|
48
62
|
ssrState?: string;
|
|
49
63
|
/** Override: HTML placeholder for SSR-rendered `<head>` tags */
|
|
50
64
|
ssrHead?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Override: run each SSR render inside an HTTP event context seeded from the
|
|
67
|
+
* incoming page request, so SSR-time `fetch('/api/...')` self-calls inherit
|
|
68
|
+
* the viewer's identity headers (authorization, cookie, accept-language,
|
|
69
|
+
* x-forwarded-for, x-request-id — see the adapter's `forwardHeaders` option)
|
|
70
|
+
* and their `Set-Cookie` headers land on the page response.
|
|
71
|
+
* Set to `false` to keep SSR self-calls anonymous. Default: `true`.
|
|
72
|
+
*/
|
|
73
|
+
ssrFetchForwarding?: boolean;
|
|
51
74
|
}
|
|
52
75
|
interface TSSRServer {
|
|
53
76
|
/** Add Connect-compatible middleware (runs in both dev and prod) */
|
|
@@ -58,4 +81,4 @@ interface TSSRServer {
|
|
|
58
81
|
declare function createSSRServer(options?: TSSRServerOptions): Promise<TSSRServer>;
|
|
59
82
|
|
|
60
83
|
export { createSSRServer };
|
|
61
|
-
export type { TSSRRenderResult, TSSRServer, TSSRServerOptions };
|
|
84
|
+
export type { TSSRRender, TSSRRenderContext, TSSRRenderResult, TSSRServer, TSSRServerOptions };
|
package/dist/prod-server.mjs
CHANGED
|
@@ -8,6 +8,32 @@ const DEFAULT_SSR_OUTLET = "<!--ssr-outlet-->";
|
|
|
8
8
|
const DEFAULT_SSR_STATE = "<!--ssr-state-->";
|
|
9
9
|
const DEFAULT_SSR_HEAD = "<!--ssr-head-->";
|
|
10
10
|
/**
|
|
11
|
+
* Invokes the SSR `render()` for a page request. Shared by all three render call
|
|
12
|
+
* sites so the render contract stays identical across them:
|
|
13
|
+
* - always passes the page request context ({@link TSSRRenderContext}) as the
|
|
14
|
+
* second `render()` argument;
|
|
15
|
+
* - when a Moost HTTP adapter is provided (and supports it), runs the render
|
|
16
|
+
* inside an HTTP event context seeded from the real `(req, res)` pair, so
|
|
17
|
+
* SSR-time `fetch('/api/...')` self-calls inherit the viewer's identity
|
|
18
|
+
* headers (`forwardHeaders`) instead of arriving anonymous, and `Set-Cookie`
|
|
19
|
+
* headers from those self-calls are drained onto the page response;
|
|
20
|
+
* - degrades to a plain `render(url, ctx)` call when the adapter is absent or
|
|
21
|
+
* predates `withHttpContext`.
|
|
22
|
+
*/ async function renderSSRPage(opts) {
|
|
23
|
+
const { render, url, req, res, http } = opts;
|
|
24
|
+
const ctx = {
|
|
25
|
+
headers: req.headers,
|
|
26
|
+
method: req.method || "GET",
|
|
27
|
+
req
|
|
28
|
+
};
|
|
29
|
+
if (http && typeof http.withHttpContext === "function") {
|
|
30
|
+
const { result, response } = await http.withHttpContext(req, res, () => render(url, ctx));
|
|
31
|
+
for (const cookie of response.getSetCookieStrings()) res.appendHeader("set-cookie", cookie);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
return render(url, ctx);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
11
37
|
* Applies an SSR render result to the HTTP response: sets the status code and
|
|
12
38
|
* headers, then substitutes the outlet, state and head markers into the HTML
|
|
13
39
|
* template. Shared by all three render call sites (the dev middleware, the dev
|
|
@@ -73,6 +99,8 @@ async function createSSRServer(options) {
|
|
|
73
99
|
const ssrOutlet = config.ssrOutlet || DEFAULT_SSR_OUTLET;
|
|
74
100
|
const ssrState = config.ssrState || DEFAULT_SSR_STATE;
|
|
75
101
|
const ssrHead = config.ssrHead || DEFAULT_SSR_HEAD;
|
|
102
|
+
const ssrForwarding = config.ssrFetchForwarding !== false;
|
|
103
|
+
const httpRef = vite.config.__moostViteHttpRef;
|
|
76
104
|
let ssrFallback = null;
|
|
77
105
|
if (ssrEntry) {
|
|
78
106
|
const fs = await import("node:fs/promises");
|
|
@@ -84,7 +112,13 @@ async function createSSRServer(options) {
|
|
|
84
112
|
let template = await fs.readFile(path.resolve(vite.config.root, "index.html"), "utf8");
|
|
85
113
|
template = await vite.transformIndexHtml(url, template);
|
|
86
114
|
const { render } = await vite.ssrLoadModule(ssrEntry);
|
|
87
|
-
const result = await
|
|
115
|
+
const result = await renderSSRPage({
|
|
116
|
+
render,
|
|
117
|
+
url,
|
|
118
|
+
req,
|
|
119
|
+
res,
|
|
120
|
+
http: ssrForwarding ? httpRef?.current : null
|
|
121
|
+
});
|
|
88
122
|
sendSSRResponse(res, template, {
|
|
89
123
|
ssrOutlet,
|
|
90
124
|
ssrState,
|
|
@@ -118,6 +152,7 @@ async function createSSRServer(options) {
|
|
|
118
152
|
const ssrState = opts.ssrState || (__MOOST_SSR_STATE__ !== void 0 ? __MOOST_SSR_STATE__ : DEFAULT_SSR_STATE);
|
|
119
153
|
const ssrHead = opts.ssrHead || (__MOOST_SSR_HEAD__ !== void 0 ? __MOOST_SSR_HEAD__ : DEFAULT_SSR_HEAD);
|
|
120
154
|
const prefixes = normalizePrefixes(opts.prefix ?? __MOOST_PREFIX__);
|
|
155
|
+
const ssrForwarding = opts.ssrFetchForwarding ?? __MOOST_SSR_FORWARDING__ ?? true;
|
|
121
156
|
const defaultPort = opts.port || Number(process.env.PORT) || 3e3;
|
|
122
157
|
const template = await fs.readFile(path.resolve(clientDir, "index.html"), "utf8");
|
|
123
158
|
let render = null;
|
|
@@ -166,7 +201,13 @@ async function createSSRServer(options) {
|
|
|
166
201
|
}
|
|
167
202
|
try {
|
|
168
203
|
if (render) {
|
|
169
|
-
const result = await
|
|
204
|
+
const result = await renderSSRPage({
|
|
205
|
+
render,
|
|
206
|
+
url,
|
|
207
|
+
req,
|
|
208
|
+
res,
|
|
209
|
+
http: ssrForwarding ? moostHttpRef.instance : null
|
|
210
|
+
});
|
|
170
211
|
sendSSRResponse(res, template, {
|
|
171
212
|
ssrOutlet,
|
|
172
213
|
ssrState,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moostjs/vite",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.31",
|
|
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.31",
|
|
62
|
+
"moost": "^0.6.31"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"sirv": {
|