@analogjs/router 2.7.0-beta.1 → 2.7.0-beta.3

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.
@@ -1,10 +1,11 @@
1
- import { InjectionToken, Injector, runInInjectionContext, ɵresetCompiledComponents as _resetCompiledComponents, assertInInjectionContext, inject, TransferState, makeStateKey, reflectComponentType, APP_ID, ɵConsole as _Console, enableProdMode } from '@angular/core';
2
- import { ɵSERVER_CONTEXT as _SERVER_CONTEXT, provideServerRendering, renderApplication, platformServer } from '@angular/platform-server';
1
+ import { InjectionToken, Injector, runInInjectionContext, ɵresetCompiledComponents as _resetCompiledComponents, enableProdMode } from '@angular/core';
2
+ import { ɵSERVER_CONTEXT as _SERVER_CONTEXT, renderApplication, platformServer, INITIAL_CONFIG, ɵrenderInternal as _renderInternal, provideServerRendering } from '@angular/platform-server';
3
3
  import { REQUEST, RESPONSE, BASE_URL, LOCALE } from '@analogjs/router/tokens';
4
4
  import { SERVER_FN_DISPATCHER, createServerFnRef } from '@analogjs/router';
5
5
  import { HttpErrorResponse } from '@angular/common/http';
6
6
  import { bootstrapApplication, createApplication } from '@angular/platform-browser';
7
- import { getHeader, createEvent, readBody, eventHandler, getRouterParam } from 'h3';
7
+ import { AsyncLocalStorage } from 'node:async_hooks';
8
+ import { eventHandler, getRouterParam, readBody } from 'h3';
8
9
 
9
10
  /**
10
11
  * Server-side registry of server functions, keyed by id. A `.server.ts` module
@@ -392,145 +393,6 @@ function getRequestProtocol(req, opts = {}) {
392
393
  return req.connection?.encrypted ? 'https' : 'http';
393
394
  }
394
395
 
395
- const STATIC_PROPS = new InjectionToken('Static Props');
396
- function provideStaticProps(props) {
397
- return {
398
- provide: STATIC_PROPS,
399
- useFactory() {
400
- return props;
401
- },
402
- };
403
- }
404
- function injectStaticProps() {
405
- assertInInjectionContext(injectStaticProps);
406
- return inject(STATIC_PROPS);
407
- }
408
- function injectStaticOutputs() {
409
- const transferState = inject(TransferState);
410
- const outputsKey = makeStateKey('_analog_output');
411
- return {
412
- set(data) {
413
- transferState.set(outputsKey, data);
414
- },
415
- };
416
- }
417
-
418
- function serverComponentRequest(serverContext) {
419
- const serverComponentId = getHeader(createEvent(serverContext.req, serverContext.res), 'X-Analog-Component');
420
- if (!serverComponentId &&
421
- serverContext.req.url &&
422
- serverContext.req.url.startsWith('/_analog/components')) {
423
- const componentId = serverContext.req.url.split('/')?.[3];
424
- return componentId;
425
- }
426
- return serverComponentId;
427
- }
428
- // Deferred so the module can be imported outside a Vite context (e.g. by the
429
- // Nitro server-function dispatch path, or tooling) without eagerly evaluating
430
- // the Vite-only `import.meta.glob` macro at load time. Vite still statically
431
- // rewrites the macro; it is just invoked lazily on first server-component render.
432
- let _components;
433
- function serverComponents() {
434
- return (_components ??= import.meta.glob([
435
- '/src/server/components/**/*.{ts,analog,ag}',
436
- ]));
437
- }
438
- async function renderServerComponent(url, serverContext, config) {
439
- const componentReqId = serverComponentRequest(serverContext);
440
- const { componentLoader, componentId } = getComponentLoader(componentReqId);
441
- if (!componentLoader) {
442
- return new Response(`Server Component Not Found ${componentId}`, {
443
- status: 404,
444
- });
445
- }
446
- const component = (await componentLoader())['default'];
447
- if (!component) {
448
- return new Response(`No default export for ${componentId}`, {
449
- status: 422,
450
- });
451
- }
452
- const mirror = reflectComponentType(component);
453
- const selector = mirror?.selector.split(',')?.[0] || 'server-component';
454
- const event = createEvent(serverContext.req, serverContext.res);
455
- const body = (await readBody(event)) || {};
456
- const appId = `analog-server-${selector.toLowerCase()}-${new Date().getTime()}`;
457
- const bootstrap = (context) => bootstrapApplication(component, {
458
- providers: [
459
- provideServerRendering(),
460
- provideStaticProps(body),
461
- { provide: _SERVER_CONTEXT, useValue: 'analog-server-component' },
462
- {
463
- provide: APP_ID,
464
- useFactory() {
465
- return appId;
466
- },
467
- },
468
- ...(config?.providers || []),
469
- ],
470
- }, context);
471
- const html = await renderApplication(bootstrap, {
472
- url,
473
- document: `<${selector}></${selector}>`,
474
- platformProviders: [
475
- {
476
- provide: _Console,
477
- useFactory() {
478
- return {
479
- warn: () => { },
480
- log: () => { },
481
- };
482
- },
483
- },
484
- ],
485
- });
486
- const outputs = retrieveTransferredState(html, appId);
487
- const responseData = {
488
- html,
489
- outputs,
490
- };
491
- return new Response(JSON.stringify(responseData), {
492
- headers: {
493
- 'X-Analog-Component': 'true',
494
- },
495
- });
496
- }
497
- function getComponentLoader(componentReqId) {
498
- let _componentId = `/src/server/components/${componentReqId.toLowerCase()}`;
499
- let componentLoader = undefined;
500
- let componentId = _componentId;
501
- const components = serverComponents();
502
- if (components[`${_componentId}.ts`]) {
503
- componentId = `${_componentId}.ts`;
504
- componentLoader = components[componentId];
505
- }
506
- return { componentLoader, componentId };
507
- }
508
- function retrieveTransferredState(html, appId) {
509
- const regex = new RegExp(`<script id="${appId}-state" type="application/json">(.*?)<\/script>`);
510
- const match = html.match(regex);
511
- if (match) {
512
- const scriptContent = match[1];
513
- if (scriptContent) {
514
- try {
515
- const parsedContent = JSON.parse(scriptContent);
516
- return parsedContent._analog_output || {};
517
- }
518
- catch (e) {
519
- console.warn('Exception while parsing static outputs for ' + appId, e);
520
- }
521
- }
522
- return {};
523
- }
524
- else {
525
- return {};
526
- }
527
- }
528
-
529
- // Optional chaining: the server-function dispatch endpoint imports this entry
530
- // from a Nitro bundle, where `import.meta.env` is not defined at all.
531
- if (import.meta.env?.PROD) {
532
- enableProdMode();
533
- }
534
396
  /**
535
397
  * Nulls `def.tView` on every component definition that Angular has
536
398
  * compiled in this process. Angular caches the result of `consts()` on
@@ -552,6 +414,12 @@ function resetComponentDefTViews() {
552
414
  def.tView = null;
553
415
  }
554
416
  }
417
+
418
+ // Optional chaining: the server-function dispatch endpoint imports this entry
419
+ // from a Nitro bundle, where `import.meta.env` is not defined at all.
420
+ if (import.meta.env?.PROD) {
421
+ enableProdMode();
422
+ }
555
423
  /**
556
424
  * Returns a function that accepts the navigation URL,
557
425
  * the root HTML, and server context.
@@ -566,9 +434,6 @@ function render(rootComponent, config, platformProviders = []) {
566
434
  return bootstrapApplication(rootComponent, config, context);
567
435
  }
568
436
  return async function render(url, document, serverContext) {
569
- if (serverComponentRequest(serverContext)) {
570
- return await renderServerComponent(url, serverContext);
571
- }
572
437
  resetComponentDefTViews();
573
438
  const html = await renderApplication(bootstrap, {
574
439
  document,
@@ -582,6 +447,385 @@ function render(rootComponent, config, platformProviders = []) {
582
447
  };
583
448
  }
584
449
 
450
+ /**
451
+ * Pure string helpers for slicing a fully rendered SSR document into the parts
452
+ * the streaming renderer flushes: the shell up to `<body>`, the authoritative
453
+ * `<body>` inner HTML for the tail, and the authoritative `<head>` inner HTML
454
+ * for the finalize-time head reconcile. Extracted from `render-stream` so they
455
+ * can be unit tested without driving the platform.
456
+ */
457
+ /** Byte offset just after the opening `<body>` tag, or 0 if none. */
458
+ function afterBodyOpen(html) {
459
+ const m = /<body[^>]*>/i.exec(html);
460
+ return m ? m.index + m[0].length : 0;
461
+ }
462
+ /** Inner HTML of `<body>` from a fully rendered document string. */
463
+ function bodyInner(html) {
464
+ const start = afterBodyOpen(html);
465
+ const end = html.lastIndexOf('</body>');
466
+ return html.slice(start, end > -1 ? end : html.length);
467
+ }
468
+ /** Inner HTML of `<head>` from a fully rendered document string. */
469
+ function headInner(html) {
470
+ const open = /<head[^>]*>/i.exec(html);
471
+ if (!open)
472
+ return '';
473
+ const start = open.index + open[0].length;
474
+ const end = html.indexOf('</head>', start);
475
+ return html.slice(start, end > -1 ? end : start);
476
+ }
477
+
478
+ /**
479
+ * Per-request decisions about whether the streaming renderer should fall back
480
+ * to a buffered render. Extracted from `render-stream` so they can be unit
481
+ * tested without driving the platform.
482
+ */
483
+ /**
484
+ * User agents that receive a fully buffered render (with a resolved `<head>`)
485
+ * instead of the streamed shell. Streaming flushes the head before the app has
486
+ * set a dynamic title/meta and reconciles it via a finalize script; a crawler
487
+ * that does not run that script would index the shell's static head. Mirrors
488
+ * Nuxt's bot bypass — streaming targets interactive clients, bots get the
489
+ * buffered path whose head is byte-identical to the classic `render()`.
490
+ */
491
+ const 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;
492
+ function isLikelyBot(serverContext) {
493
+ const ua = serverContext?.req?.headers?.['user-agent'];
494
+ return typeof ua === 'string' && SSR_BOT_RE.test(ua);
495
+ }
496
+ /**
497
+ * Whether streaming is disabled for this request by a `streaming: false` route
498
+ * rule. The platform plugin translates that rule into an `x-analog-no-streaming`
499
+ * response header (mirroring how `ssr: false` becomes `x-analog-no-ssr`); when
500
+ * present, `renderStream` produces the buffered `render()` output for this
501
+ * route instead of streaming.
502
+ */
503
+ function streamingDisabledByRoute(serverContext) {
504
+ return serverContext?.res?.getHeader?.('x-analog-no-streaming') === 'true';
505
+ }
506
+
507
+ /**
508
+ * Tiny client runtime for progressive streaming SSR — EXPERIMENTAL.
509
+ *
510
+ * `renderStream` streams the document in three parts:
511
+ * 1. the head + this runtime + an empty `<div data-analog-stream>` region;
512
+ * 2. each `@defer` block, as it resolves on the server, as a
513
+ * `<template data-analog-defer="ID">…</template>` followed by a call to
514
+ * `window.__analogPaint("ID")` — this runtime paints the block into the
515
+ * streaming region immediately, so content appears progressively and out
516
+ * of document order;
517
+ * 3. the authoritative document tail: the app's resolved `<head>` in a
518
+ * `<template data-analog-head>` and the hydration-annotated body in a
519
+ * `<template data-analog-authoritative>`, followed by
520
+ * `window.__analogReconcileHead()` + `window.__analogFinalize()`. The head
521
+ * is reconciled first (a dynamically-set `<title>`/meta is applied to the
522
+ * live document, since the streamed shell head was flushed before the app
523
+ * ran), then the body is swapped to the exact document Angular's
524
+ * incremental hydration expects.
525
+ *
526
+ * Emitted into the document by `renderStream`. Exported as a string so it can
527
+ * be injected verbatim and unit-tested against a DOM.
528
+ */
529
+ const DEFER_RECONCILE_RUNTIME = /* js */ `
530
+ (function () {
531
+ function region() {
532
+ return document.querySelector('[data-analog-stream]');
533
+ }
534
+ window.__analogPaint = function (id) {
535
+ var tpl = document.querySelector('template[data-analog-defer="' + id + '"]');
536
+ var r = region();
537
+ if (!tpl || !r) return;
538
+ r.appendChild(tpl.content.cloneNode(true));
539
+ tpl.remove();
540
+ };
541
+ window.__analogReconcileHead = function () {
542
+ // The shell head was flushed before the app rendered, so any title/meta the
543
+ // app set during render (Title/Meta services, route meta) is missing from
544
+ // the live document. Apply the authoritative head here, before hydration —
545
+ // matching how a buffered render would have produced the head. Idempotent:
546
+ // tags already present (charset, viewport, stylesheet/preload links) are
547
+ // matched and left as-is; only changed/added ones are updated.
548
+ var tpl = document.querySelector('template[data-analog-head]');
549
+ if (!tpl) return;
550
+ var frag = tpl.content;
551
+ var head = document.head;
552
+ var title = frag.querySelector('title');
553
+ if (title) document.title = title.textContent || '';
554
+ function metaKey(m) {
555
+ if (m.hasAttribute('charset')) return 'charset';
556
+ var attrs = ['name', 'property', 'http-equiv', 'itemprop'];
557
+ for (var i = 0; i < attrs.length; i++) {
558
+ if (m.hasAttribute(attrs[i])) return attrs[i] + '=' + m.getAttribute(attrs[i]);
559
+ }
560
+ return null;
561
+ }
562
+ var existingMeta = {};
563
+ var metas = head.querySelectorAll('meta');
564
+ for (var i = 0; i < metas.length; i++) {
565
+ var k = metaKey(metas[i]);
566
+ if (k) existingMeta[k] = metas[i];
567
+ }
568
+ frag.querySelectorAll('meta').forEach(function (m) {
569
+ var key = metaKey(m);
570
+ if (key == null) return;
571
+ if (existingMeta[key]) existingMeta[key].replaceWith(m.cloneNode(true));
572
+ else head.appendChild(m.cloneNode(true));
573
+ });
574
+ var existingHref = {};
575
+ var links = head.querySelectorAll('link[href]');
576
+ for (var j = 0; j < links.length; j++) {
577
+ existingHref[links[j].getAttribute('href')] = true;
578
+ }
579
+ frag.querySelectorAll('link').forEach(function (l) {
580
+ var href = l.getAttribute('href');
581
+ if (href && existingHref[href]) return;
582
+ head.appendChild(l.cloneNode(true));
583
+ if (href) existingHref[href] = true;
584
+ });
585
+ tpl.remove();
586
+ };
587
+ window.__analogFinalize = function () {
588
+ var auth = document.querySelector('template[data-analog-authoritative]');
589
+ if (!auth) return;
590
+ // Replace the entire body — preview region, block templates and runtime
591
+ // scripts — with just the authoritative body, so the reconciled DOM matches
592
+ // a buffered render byte-for-byte before hydration boots.
593
+ document.body.replaceChildren(auth.content.cloneNode(true));
594
+ };
595
+ })();
596
+ `;
597
+
598
+ /**
599
+ * Progressive streaming SSR renderer — EXPERIMENTAL.
600
+ *
601
+ * Returns a `ReadableStream<Uint8Array>` that flushes bytes DURING the render,
602
+ * not after it:
603
+ * 1. the document head + a client reconcile runtime are flushed immediately,
604
+ * before the app has finished rendering, so the browser starts fetching
605
+ * assets right away;
606
+ * 2. each `@defer (hydrate …)` block's content is flushed the moment it
607
+ * resolves on the server — out of document order — while later blocks are
608
+ * still pending (proven: a slow block does not hold back an early one);
609
+ * 3. once the app is stable, the authoritative, fully hydration-annotated
610
+ * document is flushed as the tail. This is byte-identical to a buffered
611
+ * `renderApplication`, and is what Angular's incremental hydration runs
612
+ * against on the client.
613
+ *
614
+ * Unlike a buffered renderer, this drives the platform directly
615
+ * (`platformServer` + `bootstrapApplication` + `ɵrenderInternal`) so it can
616
+ * interleave flushes with rendering. Angular's hydration annotation is
617
+ * whole-document (the root's `ngh` index references every `@defer` container),
618
+ * so the authoritative hydration payload is necessarily the tail: RENDERING
619
+ * streams progressively, and hydration begins once the tail arrives.
620
+ *
621
+ * Depends on an upstream Angular per-block resolution hook exposed on two
622
+ * globals (see {@link SsrStreamingGlobals}). When the primitive is absent,
623
+ * `renderStream` degrades to a single buffered chunk so behaviour matches the
624
+ * classic `render()` path, which is unchanged and remains the default.
625
+ */
626
+ if (import.meta.env.PROD) {
627
+ enableProdMode();
628
+ }
629
+ function streamingPrimitiveAvailable() {
630
+ const g = globalThis;
631
+ return (typeof g.__analogSsrInternals?.collectNativeNodesInLContainer === 'function');
632
+ }
633
+ /**
634
+ * Per-render capture handlers live in async-local storage, not a single shared
635
+ * global slot, so concurrent renders in one process do not clobber each other.
636
+ * `globalThis.__analogSsrDeferCapture` is a stable dispatcher installed once; it
637
+ * routes each resolved `@defer` block to the handler of the render whose async
638
+ * context it fired in. A block that resolves outside any render (no store) is a
639
+ * no-op.
640
+ */
641
+ const captureStore = new AsyncLocalStorage();
642
+ function installCaptureDispatcher() {
643
+ const g = globalThis;
644
+ if (g.__analogSsrDeferCapture?.__analogDispatcher)
645
+ return;
646
+ const dispatch = ((ev) => {
647
+ captureStore.getStore()?.(ev);
648
+ });
649
+ dispatch.__analogDispatcher = true;
650
+ g.__analogSsrDeferCapture = dispatch;
651
+ }
652
+ let warnedMissingPrimitive = false;
653
+ function warnMissingPrimitiveOnce() {
654
+ if (warnedMissingPrimitive || !import.meta.env.DEV)
655
+ return;
656
+ warnedMissingPrimitive = true;
657
+ console.warn('[@analogjs/router] renderStream: the streaming hook was not found on ' +
658
+ '@angular/core, so rendering falls back to buffered. Enable ' +
659
+ '`experimental.streaming` in your Analog config; if it already is, your ' +
660
+ 'installed Angular version may be incompatible with the streaming patch.');
661
+ }
662
+ /**
663
+ * Serialize a `@defer` block's live domino subtree to HTML. Called a macrotask
664
+ * after the block resolves, by which point change detection has filled in the
665
+ * block's interpolations.
666
+ */
667
+ function serializeLContainerHtml(lContainer) {
668
+ const g = globalThis;
669
+ const collect = g.__analogSsrInternals?.collectNativeNodesInLContainer;
670
+ if (!collect)
671
+ return '';
672
+ const nodes = [];
673
+ collect(lContainer, nodes);
674
+ let html = '';
675
+ for (const n of nodes)
676
+ html += n?.outerHTML ?? n?.data ?? n?.nodeValue ?? '';
677
+ return html;
678
+ }
679
+ /** Destroy the platform on a macrotask, matching `renderApplication`. */
680
+ function asyncDestroyPlatform(platformRef) {
681
+ return new Promise((resolve) => {
682
+ setTimeout(() => {
683
+ platformRef.destroy();
684
+ resolve();
685
+ }, 0);
686
+ });
687
+ }
688
+ /**
689
+ * Returns a function that renders a URL to a `ReadableStream<Uint8Array>`.
690
+ *
691
+ * Usage in main.server.ts:
692
+ * ```ts
693
+ * import { renderStream } from '@analogjs/router/server';
694
+ * export default renderStream(App, config);
695
+ * ```
696
+ */
697
+ function renderStream(rootComponent, config, platformProviders = []) {
698
+ function bootstrap(context) {
699
+ return bootstrapApplication(rootComponent, config, context);
700
+ }
701
+ return async function renderStream(url, document, serverContext) {
702
+ // Reset before every render — both the buffered fallback below and the
703
+ // streaming path — so a prior render's locale/consts are not frozen for the
704
+ // process lifetime (parity with render.ts).
705
+ resetComponentDefTViews();
706
+ // Fall back to a single buffered chunk so output matches the classic path
707
+ // for:
708
+ // - crawlers, which may not run the finalize script that reconciles a
709
+ // dynamic <head>, so they get a buffered render with a resolved head;
710
+ // - routes with a `streaming: false` rule (opt out per route);
711
+ // - a missing streaming primitive.
712
+ const primitiveAvailable = streamingPrimitiveAvailable();
713
+ const bot = isLikelyBot(serverContext);
714
+ const routeDisabled = streamingDisabledByRoute(serverContext);
715
+ if (bot || routeDisabled || !primitiveAvailable) {
716
+ // Warn only when the primitive is genuinely absent — the bot and
717
+ // route-opt-out paths fall back to buffered by design, not by degradation.
718
+ if (!bot && !routeDisabled && !primitiveAvailable) {
719
+ warnMissingPrimitiveOnce();
720
+ }
721
+ const html = await renderApplication((context) => bootstrapApplication(rootComponent, config, context), {
722
+ document,
723
+ url,
724
+ platformProviders: [
725
+ provideServerContext(serverContext),
726
+ platformProviders,
727
+ ],
728
+ });
729
+ return new ReadableStream({
730
+ start(controller) {
731
+ controller.enqueue(new TextEncoder().encode(html));
732
+ controller.close();
733
+ },
734
+ });
735
+ }
736
+ installCaptureDispatcher();
737
+ const encoder = new TextEncoder();
738
+ // The stream is returned immediately; `start` fills it as the render
739
+ // progresses, so the consumer receives the head, then each @defer block the
740
+ // moment it resolves, then the authoritative tail — true streaming.
741
+ return new ReadableStream({
742
+ async start(controller) {
743
+ const enqueue = (s) => controller.enqueue(encoder.encode(s));
744
+ // The capture handler fires once per @defer block as it resolves. The
745
+ // block's DOM is not filled until the next change-detection tick, so we
746
+ // serialize + flush it a macrotask later — flushing DURING the render.
747
+ // It is scoped to this render via async-local storage (see
748
+ // installCaptureDispatcher) so concurrent renders never cross-talk.
749
+ let blockIndex = 0;
750
+ let capturing = true;
751
+ const seen = new Set();
752
+ const pendingFlushes = [];
753
+ const onBlockResolved = (ev) => {
754
+ // A block can reach `Complete` more than once during a render; only
755
+ // stream each container once. Also ignore any resolution that fires
756
+ // after the render has moved on to serializing the authoritative tail.
757
+ if (!capturing || seen.has(ev.lContainer))
758
+ return;
759
+ seen.add(ev.lContainer);
760
+ const id = `s${blockIndex++}`;
761
+ pendingFlushes.push(new Promise((resolve) => {
762
+ setTimeout(() => {
763
+ const html = serializeLContainerHtml(ev.lContainer);
764
+ enqueue(`<template data-analog-defer="${id}">${html}</template>` +
765
+ `<script>window.__analogPaint&&window.__analogPaint(${JSON.stringify(id)})</script>`);
766
+ resolve();
767
+ }, 0);
768
+ }));
769
+ };
770
+ // Run the whole render inside the async-local context so every
771
+ // change-detection tick and @defer resolution it schedules routes back
772
+ // to THIS render's handler.
773
+ await captureStore.run(onBlockResolved, async () => {
774
+ const platformRef = platformServer([
775
+ { provide: INITIAL_CONFIG, useValue: { document, url } },
776
+ provideServerContext(serverContext),
777
+ platformProviders,
778
+ ]);
779
+ // 1. Flush the head + reconcile runtime immediately (before the app is
780
+ // rendered), then open the live streaming region.
781
+ enqueue(document.slice(0, afterBodyOpen(document)) +
782
+ `<script>${DEFER_RECONCILE_RUNTIME}</script>` +
783
+ `<div data-analog-stream></div>`);
784
+ let appRef;
785
+ let errored = false;
786
+ try {
787
+ // 2. Bootstrap + render. Blocks resolve out of order during this
788
+ // phase and flush via the capture handler above.
789
+ appRef = await bootstrap({ platformRef });
790
+ await appRef.whenStable();
791
+ await Promise.all(pendingFlushes);
792
+ // Stop capturing before serializing the tail so late resolutions
793
+ // triggered by the hydration pass are not streamed as extra blocks.
794
+ capturing = false;
795
+ // 3. Flush the authoritative, fully hydration-annotated document as
796
+ // the tail. Carried in <template>s (their inert `ng-state`
797
+ // script survives). The app's resolved <head> ships alongside so
798
+ // a dynamically-set title/meta — set during render, after the
799
+ // shell head was already flushed — is reconciled onto the live
800
+ // document before the runtime swaps in the body and hydration
801
+ // boots.
802
+ const authoritative = await _renderInternal(platformRef, appRef);
803
+ enqueue(`<template data-analog-head>${headInner(authoritative)}</template>` +
804
+ `<template data-analog-authoritative>${bodyInner(authoritative)}</template>` +
805
+ `<script>window.__analogReconcileHead&&window.__analogReconcileHead();` +
806
+ `window.__analogFinalize&&window.__analogFinalize()</script>` +
807
+ `</body></html>`);
808
+ }
809
+ catch (err) {
810
+ // The head + runtime were already flushed, so the status/headers are
811
+ // committed; error the stream (a no-op silent close would hand the
812
+ // client a truncated, non-hydratable 200) and log with block context.
813
+ errored = true;
814
+ console.error(`[@analogjs/router] renderStream failed for ${url} after ` +
815
+ `${blockIndex} block(s); response truncated.`, err);
816
+ controller.error(err);
817
+ }
818
+ finally {
819
+ await asyncDestroyPlatform(platformRef);
820
+ if (!errored)
821
+ controller.close();
822
+ }
823
+ });
824
+ },
825
+ });
826
+ };
827
+ }
828
+
585
829
  function serverFn(arg1, arg2) {
586
830
  const { config, handler } = normalizeArgs(arg1, arg2);
587
831
  // GET is reserved for input-less reads; an input schema requires POST (the
@@ -715,5 +959,5 @@ async function handleServerFnRequest(event, appInjector) {
715
959
  * Generated bundle index. Do not edit.
716
960
  */
717
961
 
718
- export { SERVER_FN_ALLOWED_ORIGINS, SERVER_FN_INTERCEPTORS, createServerFnAppInjector, createServerFnEventHandler, dispatchServerFn, handleServerFnRequest, injectStaticOutputs, injectStaticProps, isServerFnOriginAllowed, provideServerContext, provideServerFns, render, renderServerComponent, runInterceptors, serverComponentRequest, serverFn, serverFnRegistry, withAllowedOrigins, withServerFnInterceptors };
962
+ export { SERVER_FN_ALLOWED_ORIGINS, SERVER_FN_INTERCEPTORS, createServerFnAppInjector, createServerFnEventHandler, dispatchServerFn, handleServerFnRequest, isServerFnOriginAllowed, provideServerContext, provideServerFns, render, renderStream, runInterceptors, serverFn, serverFnRegistry, withAllowedOrigins, withServerFnInterceptors };
719
963
  //# sourceMappingURL=analogjs-router-server.mjs.map