@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 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(`🔌 Overtaking 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 ejects DI instances affected by
437
- * the changed modules.
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
- moostRestartCleanup(adapters, options.onEject, cleanupInstances);
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 render(url);
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(`🔌 Overtaking 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 ejects DI instances affected by
406
- * the changed modules.
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
- moostRestartCleanup(adapters, options.onEject, cleanupInstances);
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 render(url);
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,
@@ -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 render(url);
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 render(url);
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,
@@ -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 };
@@ -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 render(url);
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 render(url);
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.29",
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.29",
62
- "moost": "^0.6.29"
61
+ "@moostjs/event-http": "^0.6.31",
62
+ "moost": "^0.6.31"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "sirv": {