@nestjs-ssr/react 0.2.6 → 0.3.1

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/client.mjs CHANGED
@@ -1,10 +1,37 @@
1
- import React, { createContext, useContext } from 'react';
1
+ import React2, { createContext, useContext, useState, useEffect, useCallback } from 'react';
2
+ import { createRoot } from 'react-dom/client';
2
3
 
3
4
  var __defProp = Object.defineProperty;
4
5
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
5
- var PageContext = /* @__PURE__ */ createContext(null);
6
- function PageContextProvider({ context, children }) {
7
- return /* @__PURE__ */ React.createElement(PageContext.Provider, {
6
+ var CONTEXT_KEY = /* @__PURE__ */ Symbol.for("nestjs-ssr.PageContext");
7
+ var globalStore = globalThis;
8
+ function getOrCreateContext() {
9
+ if (!globalStore[CONTEXT_KEY]) {
10
+ globalStore[CONTEXT_KEY] = /* @__PURE__ */ createContext(null);
11
+ }
12
+ return globalStore[CONTEXT_KEY];
13
+ }
14
+ __name(getOrCreateContext, "getOrCreateContext");
15
+ var PageContext = getOrCreateContext();
16
+ var setPageContextState = null;
17
+ function registerPageContextState(setter) {
18
+ setPageContextState = setter;
19
+ }
20
+ __name(registerPageContextState, "registerPageContextState");
21
+ function updatePageContext(context) {
22
+ setPageContextState?.(context);
23
+ }
24
+ __name(updatePageContext, "updatePageContext");
25
+ function PageContextProvider({ context: initialContext, children, isSegment = false }) {
26
+ const [context, setContext] = useState(initialContext);
27
+ useEffect(() => {
28
+ if (!isSegment) {
29
+ registerPageContextState(setContext);
30
+ }
31
+ }, [
32
+ isSegment
33
+ ]);
34
+ return /* @__PURE__ */ React2.createElement(PageContext.Provider, {
8
35
  value: context
9
36
  }, children);
10
37
  }
@@ -196,5 +223,309 @@ function createSSRHooks() {
196
223
  };
197
224
  }
198
225
  __name(createSSRHooks, "createSSRHooks");
226
+ var defaultHooks = createSSRHooks();
227
+ var usePageContext = defaultHooks.usePageContext;
228
+ var useParams = defaultHooks.useParams;
229
+ var useQuery = defaultHooks.useQuery;
230
+ var useRequest = defaultHooks.useRequest;
231
+ var useHeaders = defaultHooks.useHeaders;
232
+ var useHeader = defaultHooks.useHeader;
233
+ var useCookies = defaultHooks.useCookies;
234
+ var useCookie = defaultHooks.useCookie;
235
+ var rootRegistry = /* @__PURE__ */ new WeakMap();
236
+ function hydrateSegment(outlet, componentName, props) {
237
+ const modules = window.__MODULES__;
238
+ if (!modules) {
239
+ console.warn("[navigation] Module registry not available for segment hydration. Make sure entry-client.tsx exports window.__MODULES__.");
240
+ return;
241
+ }
242
+ const ViewComponent = resolveComponent(componentName, modules);
243
+ if (!ViewComponent) {
244
+ console.warn(`[navigation] Component "${componentName}" not found for hydration. Available components: ` + Object.keys(modules).map((path) => {
245
+ const c = modules[path].default;
246
+ return c?.displayName || c?.name || "anonymous";
247
+ }).join(", "));
248
+ return;
249
+ }
250
+ const context = window.__CONTEXT__ || {};
251
+ const element = /* @__PURE__ */ React2.createElement(PageContextProvider, {
252
+ context,
253
+ isSegment: true
254
+ }, /* @__PURE__ */ React2.createElement(ViewComponent, props));
255
+ let wrapper = outlet.querySelector("[data-segment-root]");
256
+ if (wrapper) {
257
+ const existingRoot = rootRegistry.get(wrapper);
258
+ if (existingRoot) {
259
+ existingRoot.unmount();
260
+ rootRegistry.delete(wrapper);
261
+ }
262
+ }
263
+ wrapper = document.createElement("div");
264
+ wrapper.setAttribute("data-segment-root", "true");
265
+ outlet.innerHTML = "";
266
+ outlet.appendChild(wrapper);
267
+ const root = createRoot(wrapper);
268
+ root.render(element);
269
+ rootRegistry.set(wrapper, root);
270
+ }
271
+ __name(hydrateSegment, "hydrateSegment");
272
+ function resolveComponent(name, modules) {
273
+ const componentMap = Object.entries(modules).filter(([path, module]) => {
274
+ const filename = path.split("/").pop();
275
+ if (filename === "entry-client.tsx" || filename === "entry-server.tsx") {
276
+ return false;
277
+ }
278
+ return module.default !== void 0;
279
+ }).map(([path, module]) => {
280
+ const component = module.default;
281
+ const componentName = component.displayName || component.name;
282
+ const filename = path.split("/").pop()?.replace(".tsx", "");
283
+ const normalizedFilename = filename ? filename.charAt(0).toUpperCase() + filename.slice(1) : void 0;
284
+ return {
285
+ component,
286
+ name: componentName,
287
+ filename,
288
+ normalizedFilename
289
+ };
290
+ });
291
+ let match = componentMap.find((c) => c.name === name || c.normalizedFilename === name || c.filename === name.toLowerCase());
292
+ if (!match && /^default(_\d+)?$/.test(name)) {
293
+ if (componentMap.length === 1) {
294
+ match = componentMap[0];
295
+ } else {
296
+ const indexMatch = name.match(/^default_(\d+)$/);
297
+ const index = indexMatch ? parseInt(indexMatch[1], 10) - 1 : 0;
298
+ const defaultComponents = componentMap.filter((c) => c.name === "default").sort((a, b) => (a.filename || "").localeCompare(b.filename || ""));
299
+ if (defaultComponents[index]) {
300
+ match = defaultComponents[index];
301
+ }
302
+ }
303
+ }
304
+ return match?.component;
305
+ }
306
+ __name(resolveComponent, "resolveComponent");
307
+
308
+ // src/react/navigation/navigate.ts
309
+ var setNavigationState = null;
310
+ function registerNavigationState(setter) {
311
+ setNavigationState = setter;
312
+ }
313
+ __name(registerNavigationState, "registerNavigationState");
314
+ async function navigate(url, options = {}) {
315
+ const { replace = false, scroll = true } = options;
316
+ setNavigationState?.("loading");
317
+ const parsedUrl = new URL(url, window.location.origin);
318
+ const currentContext = window.__CONTEXT__;
319
+ if (currentContext) {
320
+ const optimisticContext = {
321
+ ...currentContext,
322
+ path: parsedUrl.pathname,
323
+ url
324
+ };
325
+ updatePageContext(optimisticContext);
326
+ }
327
+ try {
328
+ const currentLayouts = getCurrentLayouts();
329
+ if (currentLayouts.length === 0) {
330
+ window.location.href = url;
331
+ return;
332
+ }
333
+ const response = await fetchSegment(url, currentLayouts);
334
+ if (!response.swapTarget) {
335
+ window.location.href = url;
336
+ return;
337
+ }
338
+ const outlet = await swapContent(response.html, response.swapTarget);
339
+ if (outlet) {
340
+ hydrateSegment(outlet, response.componentName, response.props);
341
+ }
342
+ if (replace) {
343
+ history.replaceState({
344
+ url
345
+ }, "", url);
346
+ } else {
347
+ history.pushState({
348
+ url
349
+ }, "", url);
350
+ }
351
+ if (response.context) {
352
+ updatePageContext(response.context);
353
+ window.__CONTEXT__ = response.context;
354
+ }
355
+ if (response.head) {
356
+ updateHead(response.head);
357
+ }
358
+ if (scroll) {
359
+ window.scrollTo(0, 0);
360
+ }
361
+ window.__COMPONENT_NAME__ = response.componentName;
362
+ window.__INITIAL_STATE__ = response.props;
363
+ } catch (error) {
364
+ console.error("Navigation failed:", error);
365
+ window.location.href = url;
366
+ } finally {
367
+ setNavigationState?.("idle");
368
+ }
369
+ }
370
+ __name(navigate, "navigate");
371
+ function getCurrentLayouts() {
372
+ return Array.from(document.querySelectorAll("[data-layout]")).map((el) => el.getAttribute("data-layout"));
373
+ }
374
+ __name(getCurrentLayouts, "getCurrentLayouts");
375
+ async function fetchSegment(url, currentLayouts) {
376
+ const res = await fetch(url, {
377
+ headers: {
378
+ "X-Current-Layouts": currentLayouts.join(",")
379
+ }
380
+ });
381
+ if (!res.ok) {
382
+ throw new Error(`Navigation failed: ${res.status}`);
383
+ }
384
+ return res.json();
385
+ }
386
+ __name(fetchSegment, "fetchSegment");
387
+ async function swapContent(html, swapTarget) {
388
+ const outlet = document.querySelector(`[data-outlet="${swapTarget}"]`);
389
+ if (!outlet) {
390
+ window.location.reload();
391
+ return null;
392
+ }
393
+ const swap = /* @__PURE__ */ __name(() => {
394
+ outlet.innerHTML = html;
395
+ }, "swap");
396
+ if ("startViewTransition" in document) {
397
+ try {
398
+ await document.startViewTransition(swap).finished;
399
+ } catch {
400
+ swap();
401
+ }
402
+ } else {
403
+ swap();
404
+ }
405
+ return outlet;
406
+ }
407
+ __name(swapContent, "swapContent");
408
+ function updateHead(head) {
409
+ if (head.title) {
410
+ document.title = head.title;
411
+ }
412
+ const updateMeta = /* @__PURE__ */ __name((name, content) => {
413
+ let el = document.querySelector(`meta[name="${name}"]`);
414
+ if (!el) {
415
+ el = document.createElement("meta");
416
+ el.setAttribute("name", name);
417
+ document.head.appendChild(el);
418
+ }
419
+ el.setAttribute("content", content);
420
+ }, "updateMeta");
421
+ if (head.description) {
422
+ updateMeta("description", head.description);
423
+ }
424
+ if (head.keywords) {
425
+ updateMeta("keywords", head.keywords);
426
+ }
427
+ const updateOgMeta = /* @__PURE__ */ __name((property, content) => {
428
+ let el = document.querySelector(`meta[property="${property}"]`);
429
+ if (!el) {
430
+ el = document.createElement("meta");
431
+ el.setAttribute("property", property);
432
+ document.head.appendChild(el);
433
+ }
434
+ el.setAttribute("content", content);
435
+ }, "updateOgMeta");
436
+ if (head.ogTitle) {
437
+ updateOgMeta("og:title", head.ogTitle);
438
+ }
439
+ if (head.ogDescription) {
440
+ updateOgMeta("og:description", head.ogDescription);
441
+ }
442
+ if (head.ogImage) {
443
+ updateOgMeta("og:image", head.ogImage);
444
+ }
445
+ if (head.canonical) {
446
+ let canonical = document.querySelector('link[rel="canonical"]');
447
+ if (!canonical) {
448
+ canonical = document.createElement("link");
449
+ canonical.setAttribute("rel", "canonical");
450
+ document.head.appendChild(canonical);
451
+ }
452
+ canonical.setAttribute("href", head.canonical);
453
+ }
454
+ }
455
+ __name(updateHead, "updateHead");
456
+
457
+ // src/react/navigation/navigation-context.tsx
458
+ var NavigationContext = /* @__PURE__ */ createContext(null);
459
+ function NavigationProvider({ children }) {
460
+ const [state, setState] = useState("idle");
461
+ useEffect(() => {
462
+ registerNavigationState(setState);
463
+ }, []);
464
+ return /* @__PURE__ */ React2.createElement(NavigationContext.Provider, {
465
+ value: {
466
+ state
467
+ }
468
+ }, children);
469
+ }
470
+ __name(NavigationProvider, "NavigationProvider");
471
+ function useNavigationContext() {
472
+ const context = useContext(NavigationContext);
473
+ if (!context) {
474
+ return {
475
+ state: "idle"
476
+ };
477
+ }
478
+ return context;
479
+ }
480
+ __name(useNavigationContext, "useNavigationContext");
481
+ function Link({ href, replace = false, scroll = true, children, onClick, ...props }) {
482
+ const handleClick = /* @__PURE__ */ __name((e) => {
483
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0 || props.target === "_blank" || !isSameOrigin(href)) {
484
+ onClick?.(e);
485
+ return;
486
+ }
487
+ e.preventDefault();
488
+ onClick?.(e);
489
+ navigate(href, {
490
+ replace,
491
+ scroll
492
+ });
493
+ }, "handleClick");
494
+ return /* @__PURE__ */ React2.createElement("a", {
495
+ href,
496
+ onClick: handleClick,
497
+ ...props
498
+ }, children);
499
+ }
500
+ __name(Link, "Link");
501
+ function isSameOrigin(url) {
502
+ try {
503
+ const parsed = new URL(url, window.location.origin);
504
+ return parsed.origin === window.location.origin;
505
+ } catch {
506
+ return true;
507
+ }
508
+ }
509
+ __name(isSameOrigin, "isSameOrigin");
510
+ function useNavigation() {
511
+ const { state } = useNavigationContext();
512
+ return {
513
+ state,
514
+ navigate
515
+ };
516
+ }
517
+ __name(useNavigation, "useNavigation");
518
+ function useNavigationState() {
519
+ return useNavigationContext().state;
520
+ }
521
+ __name(useNavigationState, "useNavigationState");
522
+ function useNavigate() {
523
+ return useCallback(navigate, []);
524
+ }
525
+ __name(useNavigate, "useNavigate");
526
+ function useIsNavigating() {
527
+ return useNavigationContext().state === "loading";
528
+ }
529
+ __name(useIsNavigating, "useIsNavigating");
199
530
 
200
- export { PageContextProvider, createSSRHooks };
531
+ export { Link, NavigationProvider, PageContextProvider, createSSRHooks, navigate, updatePageContext, useCookie, useCookies, useHeader, useHeaders, useIsNavigating, useNavigate, useNavigation, useNavigationState, usePageContext, useParams, useQuery, useRequest };
@@ -1,6 +1,6 @@
1
1
  import { DynamicModule, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
2
2
  import { ComponentType } from 'react';
3
- import { H as HeadData } from './render-response.interface-Dc-Kwb09.mjs';
3
+ import { H as HeadData, R as RenderContext } from './render-response.interface-CxbuKGnV.js';
4
4
  import { ViteDevServer } from 'vite';
5
5
  import { Response } from 'express';
6
6
  import { Reflector } from '@nestjs/core';
@@ -20,23 +20,14 @@ interface ErrorPageDevelopmentProps$1 {
20
20
  phase: 'shell' | 'streaming';
21
21
  }
22
22
  /**
23
- * Vite development mode configuration
24
- */
25
- type ViteMode = 'proxy' | 'embedded';
26
- /**
27
- * Vite configuration options
23
+ * Vite configuration options for development
24
+ *
25
+ * In development, NestJS proxies requests to an external Vite dev server
26
+ * which provides HMR (Hot Module Replacement) for React components.
28
27
  */
29
28
  interface ViteConfig {
30
29
  /**
31
- * Vite mode for development
32
- * - 'embedded': Vite runs inside NestJS (no HMR, simplest setup) - DEFAULT
33
- * - 'proxy': External Vite server with HMR support (requires running `vite` separately)
34
- *
35
- * @default 'embedded'
36
- */
37
- mode?: ViteMode;
38
- /**
39
- * Port where external Vite dev server is running (proxy mode only)
30
+ * Port where Vite dev server is running
40
31
  *
41
32
  * @default 5173
42
33
  */
@@ -48,8 +39,18 @@ interface ViteConfig {
48
39
  interface RenderConfig {
49
40
  /**
50
41
  * SSR rendering mode
51
- * - 'string': Traditional renderToString (simple, proven, easier debugging)
52
- * - 'stream': Modern renderToPipeableStream (better performance, progressive rendering)
42
+ * - 'string': Traditional renderToString - atomic responses, proper HTTP status codes (default)
43
+ * - 'stream': Modern renderToPipeableStream - better TTFB, Suspense support (advanced)
44
+ *
45
+ * String mode is recommended for most applications because:
46
+ * - Atomic responses: either complete HTML or error page, never partial
47
+ * - Proper HTTP status codes: 200 for success, 500 for errors
48
+ * - Simpler error handling and debugging
49
+ *
50
+ * Use stream mode only when you need:
51
+ * - Better Time to First Byte (TTFB) for performance-critical pages
52
+ * - Progressive rendering with Suspense boundaries
53
+ * - And you understand the trade-offs (errors after shell may result in HTTP 200 with partial content)
53
54
  *
54
55
  * @default 'string'
55
56
  */
@@ -64,20 +65,17 @@ interface RenderConfig {
64
65
  /**
65
66
  * Vite configuration for development
66
67
  *
68
+ * In development, run Vite separately (`pnpm dev:vite`) and NestJS will
69
+ * proxy requests to it. This enables HMR for React components.
70
+ *
67
71
  * @example
68
72
  * ```typescript
69
- * // Zero config - embedded mode by default (simplest)
70
- * @Module({
71
- * imports: [RenderModule],
72
- * })
73
+ * // Default port 5173
74
+ * RenderModule.forRoot()
73
75
  *
74
- * // Proxy mode - external Vite with HMR
75
- * @Module({
76
- * imports: [
77
- * RenderModule.forRoot({
78
- * vite: { mode: 'proxy', port: 5173 }
79
- * })
80
- * ],
76
+ * // Custom Vite port
77
+ * RenderModule.forRoot({
78
+ * vite: { port: 3001 }
81
79
  * })
82
80
  * ```
83
81
  */
@@ -191,6 +189,25 @@ interface TemplateParts {
191
189
  htmlEnd: string;
192
190
  }
193
191
 
192
+ /**
193
+ * Response format for segment rendering (client-side navigation).
194
+ * Returned when a GET request includes the X-Current-Layouts header.
195
+ */
196
+ interface SegmentResponse {
197
+ /** The rendered HTML for the segment (content below swapTarget layout) */
198
+ html: string;
199
+ /** Head metadata to update (title, description, etc.) */
200
+ head?: HeadData;
201
+ /** Component props for hydration */
202
+ props: any;
203
+ /** Which outlet to swap (the deepest common layout). Null if no common ancestor. */
204
+ swapTarget: string | null;
205
+ /** Component name for resolving in module registry during hydration */
206
+ componentName: string;
207
+ /** Page context for updating hooks (path, params, query, etc.) */
208
+ context?: RenderContext;
209
+ }
210
+
194
211
  declare class RenderModule {
195
212
  /**
196
213
  * Configure the render module
@@ -200,17 +217,17 @@ declare class RenderModule {
200
217
  *
201
218
  * @example
202
219
  * ```ts
203
- * // Zero config - uses defaults
220
+ * // Zero config - uses string mode (default, recommended)
204
221
  * RenderModule.forRoot()
205
222
  *
206
- * // Enable streaming SSR
207
- * RenderModule.forRoot({ mode: 'stream' })
208
- *
209
- * // Enable HMR with proxy mode
223
+ * // Custom Vite port
210
224
  * RenderModule.forRoot({
211
- * vite: { mode: 'proxy', port: 5173 }
225
+ * vite: { port: 3001 }
212
226
  * })
213
227
  *
228
+ * // Enable streaming SSR (advanced - see mode docs for trade-offs)
229
+ * RenderModule.forRoot({ mode: 'stream' })
230
+ *
214
231
  * // Custom error pages
215
232
  * RenderModule.forRoot({
216
233
  * errorPageDevelopment: DevErrorPage,
@@ -327,6 +344,47 @@ declare class TemplateParserService {
327
344
  private buildTag;
328
345
  }
329
346
 
347
+ interface ViteManifest$1 {
348
+ [key: string]: {
349
+ file: string;
350
+ src?: string;
351
+ isEntry?: boolean;
352
+ imports?: string[];
353
+ css?: string[];
354
+ };
355
+ }
356
+ interface StringRenderContext {
357
+ template: string;
358
+ vite: ViteDevServer | null;
359
+ manifest: ViteManifest$1 | null;
360
+ serverManifest: ViteManifest$1 | null;
361
+ entryServerPath: string;
362
+ isDevelopment: boolean;
363
+ }
364
+ /**
365
+ * String-based SSR renderer using React's renderToString
366
+ *
367
+ * This is the default renderer that provides:
368
+ * - Atomic responses (complete HTML or error page)
369
+ * - Proper HTTP status codes
370
+ * - Simple error handling
371
+ * - Easy debugging
372
+ */
373
+ declare class StringRenderer {
374
+ private readonly templateParser;
375
+ private readonly logger;
376
+ constructor(templateParser: TemplateParserService);
377
+ /**
378
+ * Render a React component to a complete HTML string
379
+ */
380
+ render(viewComponent: any, data: any, context: StringRenderContext, head?: HeadData): Promise<string>;
381
+ /**
382
+ * Render a segment for client-side navigation.
383
+ * Returns just the HTML and metadata without the full page template.
384
+ */
385
+ renderSegment(viewComponent: any, data: any, context: StringRenderContext, swapTarget: string, head?: HeadData): Promise<SegmentResponse>;
386
+ }
387
+
330
388
  /**
331
389
  * Error handling strategies for streaming SSR
332
390
  *
@@ -358,11 +416,86 @@ declare class StreamingErrorHandler {
358
416
  * Render production error page using React component
359
417
  */
360
418
  private renderProductionErrorPage;
419
+ /**
420
+ * Render inline error overlay for when headers are already sent
421
+ * This gets injected into the stream to show a visible error UI
422
+ */
423
+ private renderInlineErrorOverlay;
361
424
  }
362
425
 
363
- declare class RenderService {
426
+ interface ViteManifest {
427
+ [key: string]: {
428
+ file: string;
429
+ src?: string;
430
+ isEntry?: boolean;
431
+ imports?: string[];
432
+ css?: string[];
433
+ };
434
+ }
435
+ interface StreamRenderContext {
436
+ template: string;
437
+ vite: ViteDevServer | null;
438
+ manifest: ViteManifest | null;
439
+ serverManifest: ViteManifest | null;
440
+ entryServerPath: string;
441
+ isDevelopment: boolean;
442
+ }
443
+ /**
444
+ * Streaming SSR renderer using React's renderToPipeableStream
445
+ *
446
+ * This renderer provides:
447
+ * - Better TTFB (Time to First Byte)
448
+ * - Progressive rendering with Suspense support
449
+ * - Lower memory usage for large pages
450
+ *
451
+ * Trade-offs:
452
+ * - More complex error handling (shell vs streaming errors)
453
+ * - Errors after shell may result in partial responses with HTTP 200
454
+ * - Requires careful Suspense boundary design
455
+ *
456
+ * Use this mode when:
457
+ * - Performance is critical
458
+ * - You're using Suspense for data fetching
459
+ * - You understand the error handling implications
460
+ */
461
+ declare class StreamRenderer {
364
462
  private readonly templateParser;
365
463
  private readonly streamingErrorHandler;
464
+ private readonly logger;
465
+ constructor(templateParser: TemplateParserService, streamingErrorHandler: StreamingErrorHandler);
466
+ /**
467
+ * Render a React component using streaming SSR
468
+ *
469
+ * @param viewComponent - The React component to render
470
+ * @param data - Data to pass to the component
471
+ * @param res - Express response object (required for streaming)
472
+ * @param context - Render context with Vite and manifest info
473
+ * @param head - Head data for SEO tags
474
+ */
475
+ render(viewComponent: any, data: any, res: Response, context: StreamRenderContext, head?: HeadData): Promise<void>;
476
+ }
477
+
478
+ /**
479
+ * Main render service that orchestrates SSR rendering
480
+ *
481
+ * This service:
482
+ * - Loads and manages HTML templates
483
+ * - Handles Vite manifest loading for production
484
+ * - Discovers root layouts
485
+ * - Delegates rendering to StringRenderer (default) or StreamRenderer
486
+ *
487
+ * String mode is the default because it provides:
488
+ * - Atomic responses (complete HTML or error page)
489
+ * - Proper HTTP status codes always
490
+ * - Simpler error handling and debugging
491
+ *
492
+ * Stream mode is available for advanced use cases requiring:
493
+ * - Better TTFB (Time to First Byte)
494
+ * - Progressive rendering with Suspense
495
+ */
496
+ declare class RenderService {
497
+ private readonly stringRenderer;
498
+ private readonly streamRenderer;
366
499
  private readonly defaultHead?;
367
500
  private readonly logger;
368
501
  private vite;
@@ -374,7 +507,14 @@ declare class RenderService {
374
507
  private readonly entryServerPath;
375
508
  private rootLayout;
376
509
  private rootLayoutChecked;
377
- constructor(templateParser: TemplateParserService, streamingErrorHandler: StreamingErrorHandler, ssrMode?: SSRMode, defaultHead?: HeadData | undefined, customTemplate?: string);
510
+ constructor(stringRenderer: StringRenderer, streamRenderer: StreamRenderer, ssrMode?: SSRMode, defaultHead?: HeadData | undefined, customTemplate?: string);
511
+ /**
512
+ * Load HTML template from custom path, package, or local location
513
+ */
514
+ private loadTemplate;
515
+ private loadCustomTemplate;
516
+ private loadDefaultTemplate;
517
+ private loadManifests;
378
518
  setViteServer(vite: ViteDevServer): void;
379
519
  /**
380
520
  * Get the root layout component if it exists
@@ -386,21 +526,28 @@ declare class RenderService {
386
526
  getRootLayout(): Promise<any | null>;
387
527
  /**
388
528
  * Main render method that routes to string or stream mode
529
+ *
530
+ * String mode (default):
531
+ * - Returns complete HTML string
532
+ * - Atomic responses - works completely or fails completely
533
+ * - Proper HTTP status codes always
534
+ *
535
+ * Stream mode:
536
+ * - Writes directly to response
537
+ * - Better TTFB, progressive rendering
538
+ * - Requires response object
389
539
  */
390
540
  render(viewComponent: any, data?: any, res?: Response, head?: HeadData): Promise<string | void>;
541
+ /**
542
+ * Render a segment for client-side navigation.
543
+ * Always uses string mode (streaming not supported for segments).
544
+ */
545
+ renderSegment(viewComponent: any, data: any, swapTarget: string, head?: HeadData): Promise<SegmentResponse>;
391
546
  /**
392
547
  * Merge default head with page-specific head
393
548
  * Page-specific head values override defaults
394
549
  */
395
550
  private mergeHead;
396
- /**
397
- * Traditional string-based SSR using renderToString
398
- */
399
- private renderToString;
400
- /**
401
- * Modern streaming SSR using renderToPipeableStream
402
- */
403
- private renderToStream;
404
551
  }
405
552
 
406
553
  declare class RenderInterceptor implements NestInterceptor {
@@ -419,6 +566,22 @@ declare class RenderInterceptor implements NestInterceptor {
419
566
  * 3. Dynamic props from controller return (final override)
420
567
  */
421
568
  private resolveLayoutChain;
569
+ /**
570
+ * Detect request type based on headers.
571
+ * - If X-Current-Layouts header is present, this is a segment request
572
+ * - Only GET requests can be segments
573
+ */
574
+ private detectRequestType;
575
+ /**
576
+ * Determine swap target by finding deepest common layout.
577
+ * Returns null if no common ancestor (client should do full navigation).
578
+ */
579
+ private determineSwapTarget;
580
+ /**
581
+ * Filter layouts to only include those below the swap target.
582
+ * The swap target's outlet will contain the filtered layouts.
583
+ */
584
+ private filterLayoutsFromSwapTarget;
422
585
  intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
423
586
  }
424
587