@analogjs/router 3.0.0-alpha.64 → 3.0.0-alpha.66

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.
Files changed (31) hide show
  1. package/fesm2022/analogjs-router-server.mjs +684 -112
  2. package/fesm2022/analogjs-router-server.mjs.map +1 -1
  3. package/fesm2022/analogjs-router.mjs +133 -131
  4. package/fesm2022/analogjs-router.mjs.map +1 -1
  5. package/fesm2022/route-files.mjs +1 -1
  6. package/fesm2022/route-files.mjs.map +1 -1
  7. package/package.json +4 -4
  8. package/types/server/src/defer-reconcile-runtime.d.ts +23 -0
  9. package/types/server/src/index.d.ts +9 -2
  10. package/types/server/src/render-stream.d.ts +40 -0
  11. package/types/server/src/render.d.ts +2 -2
  12. package/types/server/src/server-fn/app-injector.d.ts +30 -0
  13. package/types/server/src/server-fn/dispatch.d.ts +56 -0
  14. package/types/server/src/server-fn/event-handler.d.ts +22 -0
  15. package/types/server/src/server-fn/interceptors.d.ts +30 -0
  16. package/types/server/src/server-fn/node-context.d.ts +14 -0
  17. package/types/server/src/server-fn/registry.d.ts +7 -0
  18. package/types/server/src/server-fn/same-origin.d.ts +46 -0
  19. package/types/server/src/server-fn/server-fn.d.ts +21 -0
  20. package/types/server/src/server-fn/ssr-dispatcher.d.ts +17 -0
  21. package/types/server/src/utils/reset-component-def-tviews.d.ts +14 -0
  22. package/types/server/src/utils/stream-html.d.ts +13 -0
  23. package/types/server/src/utils/stream-request.d.ts +24 -0
  24. package/types/src/index.d.ts +4 -1
  25. package/types/src/lib/server-fn/dispatcher.d.ts +23 -0
  26. package/types/src/lib/server-fn/inject-server-fn.d.ts +44 -0
  27. package/types/src/lib/server-fn/server-fn-ref.d.ts +24 -0
  28. package/types/src/lib/server-fn/types.d.ts +55 -0
  29. package/types/server/src/server-component-render.d.ts +0 -4
  30. package/types/server/src/tokens.d.ts +0 -7
  31. package/types/src/lib/server.component.d.ts +0 -33
@@ -1,8 +1,11 @@
1
- import { BASE_URL, INTERNAL_FETCH, LOCALE, REQUEST, RESPONSE } from "./analogjs-router-tokens.mjs";
2
- import { APP_ID, InjectionToken, TransferState, assertInInjectionContext, enableProdMode, inject, makeStateKey, reflectComponentType, ɵConsole } from "@angular/core";
3
- import { bootstrapApplication } from "@angular/platform-browser";
4
- import { provideServerRendering, renderApplication, ɵSERVER_CONTEXT } from "@angular/platform-server";
5
- import { json } from "node:stream/consumers";
1
+ import { BASE_URL as BASE_URL$1, INTERNAL_FETCH, LOCALE as LOCALE$1, REQUEST as REQUEST$1, RESPONSE as RESPONSE$1 } from "./analogjs-router-tokens.mjs";
2
+ import { InjectionToken, Injector, enableProdMode, runInInjectionContext } from "@angular/core";
3
+ import { bootstrapApplication, createApplication } from "@angular/platform-browser";
4
+ import { BASE_URL, LOCALE, REQUEST, RESPONSE } from "@analogjs/router/tokens";
5
+ import { eventHandler, getRouterParam, readBody } from "nitro/h3";
6
+ import { INITIAL_CONFIG, platformServer, provideServerRendering, renderApplication, ɵSERVER_CONTEXT, ɵrenderInternal } from "@angular/platform-server";
7
+ import { AsyncLocalStorage } from "node:async_hooks";
8
+ import { createServerFnRef } from "@analogjs/router";
6
9
  //#region packages/router/server/src/provide-server-context.ts
7
10
  function getHeaderValue(value) {
8
11
  return Array.isArray(value) ? value[0] : value;
@@ -22,15 +25,15 @@ function provideServerContext({ req, res, fetch }) {
22
25
  useValue: "ssr-analog"
23
26
  },
24
27
  {
25
- provide: REQUEST,
28
+ provide: REQUEST$1,
26
29
  useValue: req
27
30
  },
28
31
  {
29
- provide: RESPONSE,
32
+ provide: RESPONSE$1,
30
33
  useValue: res
31
34
  },
32
35
  {
33
- provide: BASE_URL,
36
+ provide: BASE_URL$1,
34
37
  useValue: baseUrl
35
38
  },
36
39
  {
@@ -38,7 +41,7 @@ function provideServerContext({ req, res, fetch }) {
38
41
  useValue: fetch
39
42
  },
40
43
  ...locale ? [{
41
- provide: LOCALE,
44
+ provide: LOCALE$1,
42
45
  useValue: locale
43
46
  }] : []
44
47
  ];
@@ -88,106 +91,7 @@ function parseAcceptLanguage(header) {
88
91
  }).sort((a, b) => b.q - a.q)[0]?.locale || void 0;
89
92
  }
90
93
  //#endregion
91
- //#region packages/router/server/src/tokens.ts
92
- var STATIC_PROPS = new InjectionToken("Static Props");
93
- function provideStaticProps(props) {
94
- return {
95
- provide: STATIC_PROPS,
96
- useFactory() {
97
- return props;
98
- }
99
- };
100
- }
101
- function injectStaticProps() {
102
- assertInInjectionContext(injectStaticProps);
103
- return inject(STATIC_PROPS);
104
- }
105
- function injectStaticOutputs() {
106
- const transferState = inject(TransferState);
107
- const outputsKey = makeStateKey("_analog_output");
108
- return { set(data) {
109
- transferState.set(outputsKey, data);
110
- } };
111
- }
112
- //#endregion
113
- //#region packages/router/server/src/server-component-render.ts
114
- function serverComponentRequest(serverContext) {
115
- const serverComponentId = serverContext.req?.headers?.["x-analog-component"];
116
- if (!serverComponentId && serverContext.req?.url && serverContext.req.url.startsWith("/_analog/components")) return serverContext.req.url.split("/")?.[3];
117
- return serverComponentId;
118
- }
119
- var components = /* @__PURE__ */ Object.assign({});
120
- async function renderServerComponent(url, serverContext, config) {
121
- const { componentLoader, componentId } = getComponentLoader(serverComponentRequest(serverContext));
122
- if (!componentLoader) return new Response(`Server Component Not Found ${componentId}`, { status: 404 });
123
- const component = (await componentLoader()).default;
124
- if (!component) return new Response(`No default export for ${componentId}`, { status: 422 });
125
- const selector = reflectComponentType(component)?.selector.split(",")?.[0] || "server-component";
126
- const body = await json(serverContext.req).catch(() => ({})) || {};
127
- const appId = `analog-server-${selector.toLowerCase()}-${(/* @__PURE__ */ new Date()).getTime()}`;
128
- const bootstrap = (context) => bootstrapApplication(component, { providers: [
129
- provideServerRendering(),
130
- provideStaticProps(body),
131
- {
132
- provide: ɵSERVER_CONTEXT,
133
- useValue: "analog-server-component"
134
- },
135
- {
136
- provide: APP_ID,
137
- useFactory() {
138
- return appId;
139
- }
140
- },
141
- ...config?.providers || []
142
- ] }, context);
143
- const html = await renderApplication(bootstrap, {
144
- url,
145
- document: `<${selector}></${selector}>`,
146
- platformProviders: [{
147
- provide: ɵConsole,
148
- useFactory() {
149
- return {
150
- warn: () => {},
151
- log: () => {}
152
- };
153
- }
154
- }]
155
- });
156
- const responseData = {
157
- html,
158
- outputs: retrieveTransferredState(html, appId)
159
- };
160
- return new Response(JSON.stringify(responseData), { headers: { "X-Analog-Component": "true" } });
161
- }
162
- function getComponentLoader(componentReqId) {
163
- const _componentId = `/src/server/components/${componentReqId.toLowerCase()}`;
164
- let componentLoader = void 0;
165
- let componentId = _componentId;
166
- if (components[`${_componentId}.ts`]) {
167
- componentId = `${_componentId}.ts`;
168
- componentLoader = components[componentId];
169
- }
170
- return {
171
- componentLoader,
172
- componentId
173
- };
174
- }
175
- function retrieveTransferredState(html, appId) {
176
- const regex = new RegExp(`<script id="${appId}-state" type="application/json">(.*?)<\/script>`);
177
- const match = html.match(regex);
178
- if (match) {
179
- const scriptContent = match[1];
180
- if (scriptContent) try {
181
- return JSON.parse(scriptContent)._analog_output || {};
182
- } catch (e) {
183
- console.warn("Exception while parsing static outputs for " + appId, e);
184
- }
185
- return {};
186
- } else return {};
187
- }
188
- //#endregion
189
- //#region packages/router/server/src/render.ts
190
- enableProdMode();
94
+ //#region packages/router/server/src/utils/reset-component-def-tviews.ts
191
95
  /**
192
96
  * Nulls `def.tView` on every component definition that Angular has
193
97
  * compiled in this process. Angular caches the result of `consts()` on
@@ -206,6 +110,9 @@ function resetComponentDefTViews() {
206
110
  if (!defs) return;
207
111
  for (const def of defs) def.tView = null;
208
112
  }
113
+ //#endregion
114
+ //#region packages/router/server/src/render.ts
115
+ enableProdMode();
209
116
  /**
210
117
  * Returns a function that accepts the navigation URL,
211
118
  * the root HTML, and server context.
@@ -213,14 +120,13 @@ function resetComponentDefTViews() {
213
120
  * @param rootComponent
214
121
  * @param config
215
122
  * @param platformProviders
216
- * @returns Promise<string | Reponse>
123
+ * @returns Promise<string>
217
124
  */
218
125
  function render(rootComponent, config, platformProviders = []) {
219
126
  function bootstrap(context) {
220
127
  return bootstrapApplication(rootComponent, config, context);
221
128
  }
222
129
  return async function render(url, document, serverContext) {
223
- if (serverComponentRequest(serverContext)) return await renderServerComponent(url, serverContext);
224
130
  resetComponentDefTViews();
225
131
  return await renderApplication(bootstrap, {
226
132
  document,
@@ -230,6 +136,672 @@ function render(rootComponent, config, platformProviders = []) {
230
136
  };
231
137
  }
232
138
  //#endregion
233
- export { injectStaticOutputs, injectStaticProps, provideServerContext, render, renderServerComponent, serverComponentRequest };
139
+ //#region packages/router/server/src/utils/stream-html.ts
140
+ /**
141
+ * Pure string helpers for slicing a fully rendered SSR document into the parts
142
+ * the streaming renderer flushes: the shell up to `<body>`, the authoritative
143
+ * `<body>` inner HTML for the tail, and the authoritative `<head>` inner HTML
144
+ * for the finalize-time head reconcile. Extracted from `render-stream` so they
145
+ * can be unit tested without driving the platform.
146
+ */
147
+ /** Byte offset just after the opening `<body>` tag, or 0 if none. */
148
+ function afterBodyOpen(html) {
149
+ const m = /<body[^>]*>/i.exec(html);
150
+ return m ? m.index + m[0].length : 0;
151
+ }
152
+ /** Inner HTML of `<body>` from a fully rendered document string. */
153
+ function bodyInner(html) {
154
+ const start = afterBodyOpen(html);
155
+ const end = html.lastIndexOf("</body>");
156
+ return html.slice(start, end > -1 ? end : html.length);
157
+ }
158
+ /** Inner HTML of `<head>` from a fully rendered document string. */
159
+ function headInner(html) {
160
+ const open = /<head[^>]*>/i.exec(html);
161
+ if (!open) return "";
162
+ const start = open.index + open[0].length;
163
+ const end = html.indexOf("</head>", start);
164
+ return html.slice(start, end > -1 ? end : start);
165
+ }
166
+ //#endregion
167
+ //#region packages/router/server/src/utils/stream-request.ts
168
+ /**
169
+ * Per-request decisions about whether the streaming renderer should fall back
170
+ * to a buffered render. Extracted from `render-stream` so they can be unit
171
+ * tested without driving the platform.
172
+ */
173
+ /**
174
+ * User agents that receive a fully buffered render (with a resolved `<head>`)
175
+ * instead of the streamed shell. Streaming flushes the head before the app has
176
+ * set a dynamic title/meta and reconciles it via a finalize script; a crawler
177
+ * that does not run that script would index the shell's static head. Mirrors
178
+ * Nuxt's bot bypass — streaming targets interactive clients, bots get the
179
+ * buffered path whose head is byte-identical to the classic `render()`.
180
+ */
181
+ var SSR_BOT_RE = /bot|crawl|spider|slurp|mediapartners|facebookexternalhit|embedly|quora link preview|outbrain|pinterest|vkshare|w3c_validator|whatsapp|telegrambot|lighthouse|google-inspectiontool|headlesschrome|bingpreview/i;
182
+ function isLikelyBot(serverContext) {
183
+ const ua = serverContext?.req?.headers?.["user-agent"];
184
+ return typeof ua === "string" && SSR_BOT_RE.test(ua);
185
+ }
186
+ /**
187
+ * Whether streaming is disabled for this request by a `streaming: false` route
188
+ * rule. The platform plugin translates that rule into an `x-analog-no-streaming`
189
+ * response header (mirroring how `ssr: false` becomes `x-analog-no-ssr`); when
190
+ * present, `renderStream` produces the buffered `render()` output for this
191
+ * route instead of streaming.
192
+ */
193
+ function streamingDisabledByRoute(serverContext) {
194
+ return serverContext?.res?.getHeader?.("x-analog-no-streaming") === "true";
195
+ }
196
+ //#endregion
197
+ //#region packages/router/server/src/defer-reconcile-runtime.ts
198
+ /**
199
+ * Tiny client runtime for progressive streaming SSR — EXPERIMENTAL.
200
+ *
201
+ * `renderStream` streams the document in three parts:
202
+ * 1. the head + this runtime + an empty `<div data-analog-stream>` region;
203
+ * 2. each `@defer` block, as it resolves on the server, as a
204
+ * `<template data-analog-defer="ID">…</template>` followed by a call to
205
+ * `window.__analogPaint("ID")` — this runtime paints the block into the
206
+ * streaming region immediately, so content appears progressively and out
207
+ * of document order;
208
+ * 3. the authoritative document tail: the app's resolved `<head>` in a
209
+ * `<template data-analog-head>` and the hydration-annotated body in a
210
+ * `<template data-analog-authoritative>`, followed by
211
+ * `window.__analogReconcileHead()` + `window.__analogFinalize()`. The head
212
+ * is reconciled first (a dynamically-set `<title>`/meta is applied to the
213
+ * live document, since the streamed shell head was flushed before the app
214
+ * ran), then the body is swapped to the exact document Angular's
215
+ * incremental hydration expects.
216
+ *
217
+ * Emitted into the document by `renderStream`. Exported as a string so it can
218
+ * be injected verbatim and unit-tested against a DOM.
219
+ */
220
+ var DEFER_RECONCILE_RUNTIME = `
221
+ (function () {
222
+ function region() {
223
+ return document.querySelector('[data-analog-stream]');
224
+ }
225
+ window.__analogPaint = function (id) {
226
+ var tpl = document.querySelector('template[data-analog-defer="' + id + '"]');
227
+ var r = region();
228
+ if (!tpl || !r) return;
229
+ r.appendChild(tpl.content.cloneNode(true));
230
+ tpl.remove();
231
+ };
232
+ window.__analogReconcileHead = function () {
233
+ // The shell head was flushed before the app rendered, so any title/meta the
234
+ // app set during render (Title/Meta services, route meta) is missing from
235
+ // the live document. Apply the authoritative head here, before hydration —
236
+ // matching how a buffered render would have produced the head. Idempotent:
237
+ // tags already present (charset, viewport, stylesheet/preload links) are
238
+ // matched and left as-is; only changed/added ones are updated.
239
+ var tpl = document.querySelector('template[data-analog-head]');
240
+ if (!tpl) return;
241
+ var frag = tpl.content;
242
+ var head = document.head;
243
+ var title = frag.querySelector('title');
244
+ if (title) document.title = title.textContent || '';
245
+ function metaKey(m) {
246
+ if (m.hasAttribute('charset')) return 'charset';
247
+ var attrs = ['name', 'property', 'http-equiv', 'itemprop'];
248
+ for (var i = 0; i < attrs.length; i++) {
249
+ if (m.hasAttribute(attrs[i])) return attrs[i] + '=' + m.getAttribute(attrs[i]);
250
+ }
251
+ return null;
252
+ }
253
+ var existingMeta = {};
254
+ var metas = head.querySelectorAll('meta');
255
+ for (var i = 0; i < metas.length; i++) {
256
+ var k = metaKey(metas[i]);
257
+ if (k) existingMeta[k] = metas[i];
258
+ }
259
+ frag.querySelectorAll('meta').forEach(function (m) {
260
+ var key = metaKey(m);
261
+ if (key == null) return;
262
+ if (existingMeta[key]) existingMeta[key].replaceWith(m.cloneNode(true));
263
+ else head.appendChild(m.cloneNode(true));
264
+ });
265
+ var existingHref = {};
266
+ var links = head.querySelectorAll('link[href]');
267
+ for (var j = 0; j < links.length; j++) {
268
+ existingHref[links[j].getAttribute('href')] = true;
269
+ }
270
+ frag.querySelectorAll('link').forEach(function (l) {
271
+ var href = l.getAttribute('href');
272
+ if (href && existingHref[href]) return;
273
+ head.appendChild(l.cloneNode(true));
274
+ if (href) existingHref[href] = true;
275
+ });
276
+ tpl.remove();
277
+ };
278
+ window.__analogFinalize = function () {
279
+ var auth = document.querySelector('template[data-analog-authoritative]');
280
+ if (!auth) return;
281
+ // Replace the entire body — preview region, block templates and runtime
282
+ // scripts — with just the authoritative body, so the reconciled DOM matches
283
+ // a buffered render byte-for-byte before hydration boots.
284
+ document.body.replaceChildren(auth.content.cloneNode(true));
285
+ };
286
+ })();
287
+ `;
288
+ //#endregion
289
+ //#region packages/router/server/src/render-stream.ts
290
+ /**
291
+ * Progressive streaming SSR renderer — EXPERIMENTAL.
292
+ *
293
+ * Returns a `ReadableStream<Uint8Array>` that flushes bytes DURING the render,
294
+ * not after it:
295
+ * 1. the document head + a client reconcile runtime are flushed immediately,
296
+ * before the app has finished rendering, so the browser starts fetching
297
+ * assets right away;
298
+ * 2. each `@defer (hydrate …)` block's content is flushed the moment it
299
+ * resolves on the server — out of document order — while later blocks are
300
+ * still pending (proven: a slow block does not hold back an early one);
301
+ * 3. once the app is stable, the authoritative, fully hydration-annotated
302
+ * document is flushed as the tail. This is byte-identical to a buffered
303
+ * `renderApplication`, and is what Angular's incremental hydration runs
304
+ * against on the client.
305
+ *
306
+ * Unlike a buffered renderer, this drives the platform directly
307
+ * (`platformServer` + `bootstrapApplication` + `ɵrenderInternal`) so it can
308
+ * interleave flushes with rendering. Angular's hydration annotation is
309
+ * whole-document (the root's `ngh` index references every `@defer` container),
310
+ * so the authoritative hydration payload is necessarily the tail: RENDERING
311
+ * streams progressively, and hydration begins once the tail arrives.
312
+ *
313
+ * Depends on an upstream Angular per-block resolution hook exposed on two
314
+ * globals (see {@link SsrStreamingGlobals}). When the primitive is absent,
315
+ * `renderStream` degrades to a single buffered chunk so behaviour matches the
316
+ * classic `render()` path, which is unchanged and remains the default.
317
+ */
318
+ enableProdMode();
319
+ function streamingPrimitiveAvailable() {
320
+ return typeof globalThis.__analogSsrInternals?.collectNativeNodesInLContainer === "function";
321
+ }
322
+ /**
323
+ * Per-render capture handlers live in async-local storage, not a single shared
324
+ * global slot, so concurrent renders in one process do not clobber each other.
325
+ * `globalThis.__analogSsrDeferCapture` is a stable dispatcher installed once; it
326
+ * routes each resolved `@defer` block to the handler of the render whose async
327
+ * context it fired in. A block that resolves outside any render (no store) is a
328
+ * no-op.
329
+ */
330
+ var captureStore = new AsyncLocalStorage();
331
+ function installCaptureDispatcher() {
332
+ const g = globalThis;
333
+ if (g.__analogSsrDeferCapture?.__analogDispatcher) return;
334
+ const dispatch = ((ev) => {
335
+ captureStore.getStore()?.(ev);
336
+ });
337
+ dispatch.__analogDispatcher = true;
338
+ g.__analogSsrDeferCapture = dispatch;
339
+ }
340
+ function warnMissingPrimitiveOnce() {}
341
+ /**
342
+ * Serialize a `@defer` block's live domino subtree to HTML. Called a macrotask
343
+ * after the block resolves, by which point change detection has filled in the
344
+ * block's interpolations.
345
+ */
346
+ function serializeLContainerHtml(lContainer) {
347
+ const collect = globalThis.__analogSsrInternals?.collectNativeNodesInLContainer;
348
+ if (!collect) return "";
349
+ const nodes = [];
350
+ collect(lContainer, nodes);
351
+ let html = "";
352
+ for (const n of nodes) html += n?.outerHTML ?? n?.data ?? n?.nodeValue ?? "";
353
+ return html;
354
+ }
355
+ /** Destroy the platform on a macrotask, matching `renderApplication`. */
356
+ function asyncDestroyPlatform(platformRef) {
357
+ return new Promise((resolve) => {
358
+ setTimeout(() => {
359
+ platformRef.destroy();
360
+ resolve();
361
+ }, 0);
362
+ });
363
+ }
364
+ /**
365
+ * Returns a function that renders a URL to a `ReadableStream<Uint8Array>`.
366
+ *
367
+ * Usage in main.server.ts:
368
+ * ```ts
369
+ * import { renderStream } from '@analogjs/router/server';
370
+ * export default renderStream(App, config);
371
+ * ```
372
+ */
373
+ function renderStream(rootComponent, config, platformProviders = []) {
374
+ function bootstrap(context) {
375
+ return bootstrapApplication(rootComponent, config, context);
376
+ }
377
+ return async function renderStream(url, document, serverContext) {
378
+ resetComponentDefTViews();
379
+ const primitiveAvailable = streamingPrimitiveAvailable();
380
+ const bot = isLikelyBot(serverContext);
381
+ const routeDisabled = streamingDisabledByRoute(serverContext);
382
+ if (bot || routeDisabled || !primitiveAvailable) {
383
+ if (!bot && !routeDisabled && !primitiveAvailable) warnMissingPrimitiveOnce();
384
+ const html = await renderApplication((context) => bootstrapApplication(rootComponent, config, context), {
385
+ document,
386
+ url,
387
+ platformProviders: [provideServerContext(serverContext), platformProviders]
388
+ });
389
+ return new ReadableStream({ start(controller) {
390
+ controller.enqueue(new TextEncoder().encode(html));
391
+ controller.close();
392
+ } });
393
+ }
394
+ installCaptureDispatcher();
395
+ const encoder = new TextEncoder();
396
+ return new ReadableStream({ async start(controller) {
397
+ const enqueue = (s) => controller.enqueue(encoder.encode(s));
398
+ let blockIndex = 0;
399
+ let capturing = true;
400
+ const seen = /* @__PURE__ */ new Set();
401
+ const pendingFlushes = [];
402
+ const onBlockResolved = (ev) => {
403
+ if (!capturing || seen.has(ev.lContainer)) return;
404
+ seen.add(ev.lContainer);
405
+ const id = `s${blockIndex++}`;
406
+ pendingFlushes.push(new Promise((resolve) => {
407
+ setTimeout(() => {
408
+ enqueue(`<template data-analog-defer="${id}">${serializeLContainerHtml(ev.lContainer)}</template><script>window.__analogPaint&&window.__analogPaint(${JSON.stringify(id)})<\/script>`);
409
+ resolve();
410
+ }, 0);
411
+ }));
412
+ };
413
+ await captureStore.run(onBlockResolved, async () => {
414
+ const platformRef = platformServer([
415
+ {
416
+ provide: INITIAL_CONFIG,
417
+ useValue: {
418
+ document,
419
+ url
420
+ }
421
+ },
422
+ provideServerContext(serverContext),
423
+ platformProviders
424
+ ]);
425
+ enqueue(document.slice(0, afterBodyOpen(document)) + `<script>${DEFER_RECONCILE_RUNTIME}<\/script><div data-analog-stream></div>`);
426
+ let appRef;
427
+ let errored = false;
428
+ try {
429
+ appRef = await bootstrap({ platformRef });
430
+ await appRef.whenStable();
431
+ await Promise.all(pendingFlushes);
432
+ capturing = false;
433
+ const authoritative = await ɵrenderInternal(platformRef, appRef);
434
+ enqueue(`<template data-analog-head>${headInner(authoritative)}</template><template data-analog-authoritative>${bodyInner(authoritative)}</template><script>window.__analogReconcileHead&&window.__analogReconcileHead();window.__analogFinalize&&window.__analogFinalize()<\/script></body></html>`);
435
+ } catch (err) {
436
+ errored = true;
437
+ console.error(`[@analogjs/router] renderStream failed for ${url} after ${blockIndex} block(s); response truncated.`, err);
438
+ controller.error(err);
439
+ } finally {
440
+ await asyncDestroyPlatform(platformRef);
441
+ if (!errored) controller.close();
442
+ }
443
+ });
444
+ } });
445
+ };
446
+ }
447
+ //#endregion
448
+ //#region packages/router/server/src/server-fn/registry.ts
449
+ /**
450
+ * Server-side registry of server functions, keyed by id. A `.server.ts` module
451
+ * populates it as a side effect of `serverFn(...)` running at import time; the
452
+ * Nitro dispatch route imports those modules to fill it, then looks up by id.
453
+ */
454
+ var serverFnRegistry = /* @__PURE__ */ new Map();
455
+ //#endregion
456
+ //#region packages/router/server/src/server-fn/server-fn.ts
457
+ function serverFn(arg1, arg2) {
458
+ const { config, handler } = normalizeArgs(arg1, arg2);
459
+ if (config.method === "GET" && config.input) throw new Error("[analog] a serverFn with `input` must use POST; GET is reserved for input-less reads.");
460
+ const ref = createServerFnRef(config);
461
+ serverFnRegistry.set(ref.id, {
462
+ id: ref.id,
463
+ method: ref.method,
464
+ config,
465
+ handler
466
+ });
467
+ return ref;
468
+ }
469
+ function normalizeArgs(arg1, arg2) {
470
+ if (typeof arg1 === "function") return {
471
+ config: {},
472
+ handler: arg1
473
+ };
474
+ if (isStandardSchema(arg1)) return {
475
+ config: { input: arg1 },
476
+ handler: arg2
477
+ };
478
+ return {
479
+ config: arg1 ?? {},
480
+ handler: arg2
481
+ };
482
+ }
483
+ function isStandardSchema(value) {
484
+ return typeof value === "object" && value !== null && "~standard" in value;
485
+ }
486
+ //#endregion
487
+ //#region packages/router/server/src/server-fn/app-injector.ts
488
+ /**
489
+ * Builds the parent injector the server-function dispatch endpoint runs handlers
490
+ * against, over HTTP.
491
+ *
492
+ * A plain `Injector.create({ providers })` resolves explicitly-listed providers
493
+ * but not tree-shakeable `providedIn: 'root'` services — those attach to a
494
+ * *bootstrapped* application's root injector, which `Injector.create` is not.
495
+ * So the in-process SSR leg (whose parent is the app's own bootstrapped
496
+ * injector) resolved `root` services while the HTTP leg did not — the same
497
+ * handler could work while rendering and fail when called from the browser.
498
+ *
499
+ * Bootstrapping a real application on the server platform closes that gap: the
500
+ * returned `appRef.injector` is a root environment injector, so both listed
501
+ * providers and `providedIn: 'root'` services resolve, matching SSR.
502
+ *
503
+ * The generated endpoint passes the app's own server `ApplicationConfig` (the
504
+ * one `main.server.ts` renders with), so a handler sees exactly the DI the app
505
+ * configured — services, tokens, and interceptors alike — with no second
506
+ * provider list to keep in sync. No root component is bootstrapped
507
+ * (`createApplication`, not `bootstrapApplication`), so nothing renders, no
508
+ * change detection runs, and the router registers but never navigates. It is a
509
+ * DI container with the app's providers, built once and reused for the process,
510
+ * with only `REQUEST`/`RESPONSE` rebuilt per call in the child.
511
+ *
512
+ * A bare provider array is also accepted (direct callers and tests without an
513
+ * app config); it is wrapped with `provideServerRendering` so the server tokens
514
+ * resolve the same way.
515
+ */
516
+ async function createServerFnAppInjector(configOrProviders = []) {
517
+ return (await createApplication(Array.isArray(configOrProviders) ? { providers: [provideServerRendering(), ...configOrProviders] } : configOrProviders, { platformRef: platformServer() })).injector;
518
+ }
519
+ //#endregion
520
+ //#region packages/router/server/src/server-fn/node-context.ts
521
+ /**
522
+ * Dispatch provides the Node request and response through `REQUEST` and
523
+ * `RESPONSE`, which are typed as Node primitives, so server functions only run
524
+ * on a Node runtime. h3 leaves `node` undefined on other runtimes.
525
+ */
526
+ function assertNodeContext(event) {
527
+ const node = event.node;
528
+ if (!node?.req || !node.res) throw new Error("@analogjs/router: server functions require a Node runtime.");
529
+ return node;
530
+ }
531
+ //#endregion
532
+ //#region packages/router/server/src/server-fn/interceptors.ts
533
+ var SERVER_FN_INTERCEPTORS = new InjectionToken("SERVER_FN_INTERCEPTORS");
534
+ /** `withServerFnInterceptors([...])` — registers the chain (DI, ordered). */
535
+ function withServerFnInterceptors(interceptors) {
536
+ return { providers: interceptors.map((fn) => ({
537
+ provide: SERVER_FN_INTERCEPTORS,
538
+ useValue: fn,
539
+ multi: true
540
+ })) };
541
+ }
542
+ /** `provideServerFns(withServerFnInterceptors(...))` — mirrors provideHttpClient. */
543
+ function provideServerFns(...features) {
544
+ return features.flatMap((f) => f.providers);
545
+ }
546
+ function makeCtx(input, context) {
547
+ return {
548
+ input,
549
+ context,
550
+ with(patch) {
551
+ return makeCtx(input, {
552
+ ...context,
553
+ ...patch
554
+ });
555
+ }
556
+ };
557
+ }
558
+ /**
559
+ * Run the interceptor chain, then the handler, threading the context.
560
+ *
561
+ * `runInCtx` re-establishes the DI injection context around each interceptor
562
+ * and the handler individually. This is what keeps `inject()` working in a
563
+ * handler even when an upstream interceptor `await`s before calling `next`
564
+ * (which would otherwise resume outside Angular's synchronous injection
565
+ * context). It defaults to a pass-through for non-DI callers/tests.
566
+ */
567
+ async function runInterceptors(interceptors, input, handler, runInCtx = (fn) => fn()) {
568
+ let i = -1;
569
+ const dispatch = async (ctx) => {
570
+ i += 1;
571
+ if (i < interceptors.length) return runInCtx(() => interceptors[i](ctx, dispatch));
572
+ return runInCtx(() => handler(ctx.input, ctx.context));
573
+ };
574
+ return dispatch(makeCtx(input, {}));
575
+ }
576
+ //#endregion
577
+ //#region packages/router/server/src/server-fn/same-origin.ts
578
+ /**
579
+ * Same-origin enforcement for the server-function HTTP transport.
580
+ *
581
+ * Server functions are same-origin RPC: a client proxy only ever calls the
582
+ * relative `/_analog/fn/:id` URL of its own app. A cross-origin page must not be
583
+ * able to invoke them against a logged-in user (a CSRF-shaped attack), so the
584
+ * transport rejects browser requests whose origin is not the app's own — out of
585
+ * the box, with no per-app configuration.
586
+ *
587
+ * The signals used (`Sec-Fetch-Site`, `Origin`) are added by the browser and
588
+ * cannot be forged by a cross-origin page's `fetch`. Non-browser callers (curl,
589
+ * server-to-server, SSR in-process) send neither, so they are unaffected: the
590
+ * guard blocks the cross-origin browser attack it is meant to, and nothing else.
591
+ */
592
+ /**
593
+ * Origins permitted beyond the app's own, registered through DI:
594
+ * `provideServerFns(withAllowedOrigins([...]))`. Empty by default — the
595
+ * transport is same-origin unless an app opts out explicitly.
596
+ */
597
+ var SERVER_FN_ALLOWED_ORIGINS = new InjectionToken("SERVER_FN_ALLOWED_ORIGINS");
598
+ /**
599
+ * `withAllowedOrigins([...])` — permit cross-origin browser calls from the
600
+ * listed origins, or pass `'*'` to disable the same-origin guard entirely.
601
+ * Server functions are frequently cookie-authenticated, so this is an explicit
602
+ * opt-out of CSRF protection: allow-list the exact origins you control.
603
+ */
604
+ function withAllowedOrigins(origins) {
605
+ return { providers: origins.map((origin) => ({
606
+ provide: SERVER_FN_ALLOWED_ORIGINS,
607
+ useValue: origin,
608
+ multi: true
609
+ })) };
610
+ }
611
+ function firstHeader(value) {
612
+ return Array.isArray(value) ? value[0] : value;
613
+ }
614
+ /**
615
+ * Whether an HTTP request to a server function may proceed.
616
+ *
617
+ * Allowed when the request is same-origin, carries no browser-origin signal at
618
+ * all (a non-browser client, or a same-origin GET that omits `Origin`), or its
619
+ * `Origin` is listed in `allowedOrigins`. Passing `'*'` in `allowedOrigins`
620
+ * disables the check — the explicit opt-in to cross-origin access.
621
+ *
622
+ * `Sec-Fetch-Site` is the authoritative signal when present: `same-origin` and
623
+ * `none` (a direct navigation, not a cross-site fetch) pass; `same-site` and
624
+ * `cross-site` require an explicit `allowedOrigins` entry. When the header is
625
+ * absent (older browsers, some proxies) the `Origin` host is compared to the
626
+ * request host as a fallback.
627
+ */
628
+ function isServerFnOriginAllowed(headers, allowedOrigins = []) {
629
+ if (allowedOrigins.includes("*")) return true;
630
+ const origin = firstHeader(headers["origin"]);
631
+ const originAllowlisted = origin !== void 0 && allowedOrigins.includes(origin);
632
+ const site = firstHeader(headers["sec-fetch-site"]);
633
+ if (site) {
634
+ if (site === "same-origin" || site === "none") return true;
635
+ return originAllowlisted;
636
+ }
637
+ if (!origin) return true;
638
+ if (originAllowlisted) return true;
639
+ const host = firstHeader(headers["x-forwarded-host"]) ?? firstHeader(headers["host"]);
640
+ if (!host) return false;
641
+ try {
642
+ return new URL(origin).host === host;
643
+ } catch {
644
+ return false;
645
+ }
646
+ }
647
+ //#endregion
648
+ //#region packages/router/server/src/server-fn/dispatch.ts
649
+ /**
650
+ * Server-side dispatch for a server function call.
651
+ *
652
+ * 1. reject cross-origin browser calls (403), unless allow-listed — HTTP
653
+ * transport only (in-process callers omit `method` and are exempt)
654
+ * 2. look up the function by id
655
+ * 3. enforce the configured HTTP method (405 on mismatch)
656
+ * 4. require a JSON body on input-bearing calls (415 otherwise)
657
+ * 5. validate `input` against the Standard-Schema (4xx on failure)
658
+ * 6. build a per-request injector (REQUEST/RESPONSE + app providers)
659
+ * 7. run the interceptor chain, then the handler, re-entering
660
+ * `runInInjectionContext` at every hop so `inject()` works even after an
661
+ * interceptor `await`s before calling `next`
662
+ * 8. a `Response` returned by an interceptor/handler (`fail`/`redirect`)
663
+ * short-circuits with its status AND headers
664
+ *
665
+ * `options.method` is the request's HTTP method; when provided it is enforced
666
+ * against the function's configured method AND it turns on the same-origin
667
+ * guard. Transports (the generated Nitro handler) always pass it; trusted
668
+ * in-process callers may omit it, which also exempts them from the origin guard.
669
+ */
670
+ async function dispatchServerFn(id, rawInput, event, options = {}) {
671
+ const { parent, providers = [], method, allowedOrigins = [] } = options;
672
+ const node = assertNodeContext(event);
673
+ const headers = node.req.headers ?? {};
674
+ if (method) {
675
+ if (!isServerFnOriginAllowed(headers, [...allowedOrigins, ...parent?.get(SERVER_FN_ALLOWED_ORIGINS, []) ?? []])) return {
676
+ status: 403,
677
+ body: { message: "Cross-origin server function call rejected" }
678
+ };
679
+ }
680
+ const def = serverFnRegistry.get(id);
681
+ if (!def) return {
682
+ status: 404,
683
+ body: { message: `Unknown server function: ${id}` }
684
+ };
685
+ if (method && method.toUpperCase() !== def.method) return {
686
+ status: 405,
687
+ body: { message: `Method ${method} not allowed for ${id}` },
688
+ headers: { Allow: def.method }
689
+ };
690
+ if (method && def.method === "POST" && !isJsonContentType(headers)) return {
691
+ status: 415,
692
+ body: { message: "Server functions accept an application/json body" }
693
+ };
694
+ let input = rawInput;
695
+ if (def.config.input) {
696
+ const result = await def.config.input["~standard"].validate(rawInput);
697
+ if ("issues" in result && result.issues) return {
698
+ status: 400,
699
+ body: { errors: result.issues }
700
+ };
701
+ input = result.value;
702
+ }
703
+ const req = node.req;
704
+ const locale = detectLocale(req);
705
+ const injector = Injector.create({
706
+ parent,
707
+ providers: [
708
+ {
709
+ provide: REQUEST,
710
+ useValue: node.req
711
+ },
712
+ {
713
+ provide: RESPONSE,
714
+ useValue: node.res
715
+ },
716
+ {
717
+ provide: BASE_URL,
718
+ useValue: getBaseUrl(req)
719
+ },
720
+ ...locale ? [{
721
+ provide: LOCALE,
722
+ useValue: locale
723
+ }] : [],
724
+ ...providers
725
+ ]
726
+ });
727
+ const runInCtx = (fn) => runInInjectionContext(injector, fn);
728
+ const outcome = await runInterceptors(injector.get(SERVER_FN_INTERCEPTORS, []), input, def.handler, runInCtx);
729
+ if (outcome instanceof Response) {
730
+ const text = await outcome.text();
731
+ const body = text ? safeJson(text) : null;
732
+ const headers = {};
733
+ outcome.headers.forEach((value, key) => {
734
+ if (key.toLowerCase() !== "set-cookie") headers[key] = value;
735
+ });
736
+ const setCookie = outcome.headers.getSetCookie?.() ?? [];
737
+ if (setCookie.length) headers["set-cookie"] = setCookie;
738
+ return {
739
+ status: outcome.status,
740
+ body,
741
+ headers: Object.keys(headers).length ? headers : void 0
742
+ };
743
+ }
744
+ return {
745
+ status: 200,
746
+ body: outcome
747
+ };
748
+ }
749
+ function isJsonContentType(headers) {
750
+ const contentType = headers["content-type"];
751
+ const value = Array.isArray(contentType) ? contentType[0] : contentType;
752
+ if (!value) return false;
753
+ const mediaType = value.split(";")[0].trim().toLowerCase();
754
+ return mediaType === "application/json" || mediaType.endsWith("+json");
755
+ }
756
+ function safeJson(text) {
757
+ try {
758
+ return JSON.parse(text);
759
+ } catch {
760
+ return text;
761
+ }
762
+ }
763
+ //#endregion
764
+ //#region packages/router/server/src/server-fn/event-handler.ts
765
+ /**
766
+ * The h3 request/response layer for the server-function dispatch route.
767
+ *
768
+ * `createServerFnAppInjector` bootstraps the parent injector once; this wraps
769
+ * that in the `/_analog/fn/:id` handler the Nitro build registers. Kept as a
770
+ * runtime function (rather than inlined into the generated module) so the
771
+ * transport behaviour — body decoding, the malformed-body contract, and header
772
+ * propagation — is unit-tested directly instead of by matching generated source.
773
+ *
774
+ * `appInjector` may be a promise: the generated module bootstraps the app at
775
+ * import time and passes the pending injector, which is awaited on first request
776
+ * and resolved instantly thereafter.
777
+ */
778
+ function createServerFnEventHandler(appInjector) {
779
+ return eventHandler((event) => handleServerFnRequest(event, appInjector));
780
+ }
781
+ /**
782
+ * Decode a server-function request, dispatch it, and write the result to the
783
+ * h3 response. Same-origin, method, content-type, validation, and interceptors
784
+ * are enforced inside `dispatchServerFn`; this owns only the h3 I/O around it.
785
+ */
786
+ async function handleServerFnRequest(event, appInjector) {
787
+ const node = assertNodeContext(event);
788
+ const id = getRouterParam(event, "id") ?? "";
789
+ let input;
790
+ if (event.method !== "GET") try {
791
+ input = await readBody(event);
792
+ } catch {
793
+ node.res.statusCode = 400;
794
+ return { message: "Malformed request body" };
795
+ }
796
+ const { status, body, headers } = await dispatchServerFn(id, input, event, {
797
+ parent: await appInjector,
798
+ method: event.method
799
+ });
800
+ node.res.statusCode = status;
801
+ if (headers) for (const [key, value] of Object.entries(headers)) node.res.setHeader(key, value);
802
+ return body;
803
+ }
804
+ //#endregion
805
+ export { SERVER_FN_ALLOWED_ORIGINS, SERVER_FN_INTERCEPTORS, createServerFnAppInjector, createServerFnEventHandler, dispatchServerFn, handleServerFnRequest, isServerFnOriginAllowed, provideServerContext, provideServerFns, render, renderStream, runInterceptors, serverFn, serverFnRegistry, withAllowedOrigins, withServerFnInterceptors };
234
806
 
235
807
  //# sourceMappingURL=analogjs-router-server.mjs.map