@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.
@@ -0,0 +1,281 @@
1
+ import { H as HeadData, R as RenderContext } from './render-response.interface-CxbuKGnV.mjs';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+ import React from 'react';
4
+
5
+ /**
6
+ * Generic type for React page component props.
7
+ * Spreads controller data directly as props (React-standard pattern).
8
+ *
9
+ * Request context is available via typed hooks created with createSSRHooks().
10
+ *
11
+ * @template TProps - The shape of props returned by the controller
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * // src/lib/ssr-hooks.ts
16
+ * import { createSSRHooks, RenderContext } from '@nestjs-ssr/react';
17
+ *
18
+ * interface AppRenderContext extends RenderContext {
19
+ * user?: User;
20
+ * }
21
+ *
22
+ * export const { usePageContext } = createSSRHooks<AppRenderContext>();
23
+ *
24
+ * // src/views/product.tsx
25
+ * import { usePageContext } from '@/lib/ssr-hooks';
26
+ *
27
+ * interface ProductPageProps {
28
+ * product: Product;
29
+ * relatedProducts: Product[];
30
+ * }
31
+ *
32
+ * export default function ProductDetail(props: PageProps<ProductPageProps>) {
33
+ * const { product, relatedProducts, head } = props;
34
+ * const context = usePageContext(); // Fully typed!
35
+ *
36
+ * return (
37
+ * <html>
38
+ * <head>
39
+ * <title>{head?.title || product.name}</title>
40
+ * </head>
41
+ * <body>
42
+ * <h1>{product.name}</h1>
43
+ * <p>Current path: {context.path}</p>
44
+ * </body>
45
+ * </html>
46
+ * );
47
+ * }
48
+ * ```
49
+ */
50
+ type PageProps<TProps = {}> = TProps & {
51
+ /**
52
+ * Optional head metadata for SEO (title, description, og tags, etc.)
53
+ * Pass from controller to populate meta tags, Open Graph, etc.
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * // In controller:
58
+ * return {
59
+ * product,
60
+ * head: {
61
+ * title: product.name,
62
+ * description: product.description,
63
+ * }
64
+ * };
65
+ *
66
+ * // In component:
67
+ * <head>
68
+ * <title>{props.head?.title}</title>
69
+ * <meta name="description" content={props.head?.description} />
70
+ * </head>
71
+ * ```
72
+ */
73
+ head?: HeadData;
74
+ };
75
+
76
+ /**
77
+ * Update the page context during client-side navigation.
78
+ * Called by navigate() after successful navigation.
79
+ */
80
+ declare function updatePageContext(context: RenderContext): void;
81
+ /**
82
+ * Provider component that makes page context available to all child components.
83
+ * Should wrap the entire app in entry-server and entry-client.
84
+ * On the client, this provider is stateful and updates during navigation.
85
+ *
86
+ * @param isSegment - If true, this is a segment provider (for hydrated segments)
87
+ * and won't register its setter to avoid overwriting the root provider's.
88
+ */
89
+ declare function PageContextProvider({ context: initialContext, children, isSegment, }: {
90
+ context: RenderContext;
91
+ children: React.ReactNode;
92
+ isSegment?: boolean;
93
+ }): react_jsx_runtime.JSX.Element;
94
+ /**
95
+ * Factory function to create typed SSR hooks bound to your app's context type.
96
+ * Use this once in your app to create hooks with full type safety.
97
+ *
98
+ * This eliminates the need to pass generic types to every hook call,
99
+ * providing excellent DX with full IntelliSense support.
100
+ *
101
+ * @template T - Your extended RenderContext type with app-specific properties
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * // src/lib/ssr-hooks.ts - Define once
106
+ * import { createSSRHooks, RenderContext } from '@nestjs-ssr/react';
107
+ *
108
+ * interface AppRenderContext extends RenderContext {
109
+ * user?: {
110
+ * id: string;
111
+ * name: string;
112
+ * email: string;
113
+ * };
114
+ * tenant?: { id: string; name: string };
115
+ * featureFlags?: Record<string, boolean>;
116
+ * theme?: string; // From cookie
117
+ * }
118
+ *
119
+ * export const {
120
+ * usePageContext,
121
+ * useParams,
122
+ * useQuery,
123
+ * useRequest,
124
+ * useHeaders,
125
+ * useHeader,
126
+ * useCookies,
127
+ * useCookie,
128
+ * } = createSSRHooks<AppRenderContext>();
129
+ *
130
+ * // Create custom helper hooks
131
+ * export const useUser = () => usePageContext().user;
132
+ * export const useTheme = () => useCookie('theme');
133
+ * export const useUserAgent = () => useHeader('user-agent');
134
+ * ```
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * // src/views/home.tsx - Use everywhere with full types
139
+ * import { usePageContext, useUser, useTheme, useCookie, useHeader } from '@/lib/ssr-hooks';
140
+ *
141
+ * export default function Home() {
142
+ * const { user, featureFlags } = usePageContext(); // ✅ Fully typed!
143
+ * const user = useUser(); // ✅ Also typed!
144
+ * const theme = useTheme(); // ✅ From cookie
145
+ * const locale = useCookie('locale'); // ✅ Access specific cookie
146
+ * const tenantId = useHeader('x-tenant-id'); // ✅ Access specific header
147
+ *
148
+ * return (
149
+ * <div>
150
+ * <h1>Welcome {user?.name}</h1>
151
+ * <p>Theme: {theme}</p>
152
+ * <p>Locale: {locale}</p>
153
+ * <p>Tenant: {tenantId}</p>
154
+ * </div>
155
+ * );
156
+ * }
157
+ * ```
158
+ */
159
+ declare function createSSRHooks<T extends RenderContext = RenderContext>(): {
160
+ /**
161
+ * Hook to access the full page context with your app's type.
162
+ * Contains URL metadata, headers, and any custom properties you've added.
163
+ */
164
+ usePageContext: () => T;
165
+ /**
166
+ * Hook to access route parameters.
167
+ *
168
+ * @example
169
+ * ```tsx
170
+ * // Route: /users/:id
171
+ * const params = useParams();
172
+ * console.log(params.id); // '123'
173
+ * ```
174
+ */
175
+ useParams: () => Record<string, string>;
176
+ /**
177
+ * Hook to access query string parameters.
178
+ *
179
+ * @example
180
+ * ```tsx
181
+ * // URL: /search?q=react&sort=date
182
+ * const query = useQuery();
183
+ * console.log(query.q); // 'react'
184
+ * console.log(query.sort); // 'date'
185
+ * ```
186
+ */
187
+ useQuery: () => Record<string, string | string[]>;
188
+ /**
189
+ * Alias for usePageContext() with a more intuitive name.
190
+ * Returns the full request context with your app's type.
191
+ *
192
+ * @example
193
+ * ```tsx
194
+ * const request = useRequest();
195
+ * console.log(request.path); // '/users/123'
196
+ * console.log(request.method); // 'GET'
197
+ * console.log(request.params); // { id: '123' }
198
+ * console.log(request.query); // { search: 'foo' }
199
+ * ```
200
+ */
201
+ useRequest: () => T;
202
+ /**
203
+ * Hook to access headers configured via allowedHeaders.
204
+ * Returns all headers as a Record.
205
+ *
206
+ * Configure in module registration:
207
+ * ```typescript
208
+ * RenderModule.forRoot({
209
+ * allowedHeaders: ['user-agent', 'x-tenant-id', 'x-api-version']
210
+ * })
211
+ * ```
212
+ *
213
+ * @example
214
+ * ```tsx
215
+ * const headers = useHeaders();
216
+ * console.log(headers['user-agent']); // 'Mozilla/5.0...'
217
+ * console.log(headers['x-tenant-id']); // 'tenant-123'
218
+ * console.log(headers['x-api-version']); // 'v2'
219
+ * ```
220
+ */
221
+ useHeaders: () => Record<string, string>;
222
+ /**
223
+ * Hook to access a specific custom header by name.
224
+ * Returns undefined if the header is not configured or not present.
225
+ *
226
+ * @param name - The header name (as configured in allowedHeaders)
227
+ *
228
+ * @example
229
+ * ```tsx
230
+ * const tenantId = useHeader('x-tenant-id');
231
+ * if (tenantId) {
232
+ * console.log(`Tenant: ${tenantId}`);
233
+ * }
234
+ * ```
235
+ */
236
+ useHeader: (name: string) => string | undefined;
237
+ /**
238
+ * Hook to access cookies configured via allowedCookies.
239
+ * Returns all allowed cookies as a Record.
240
+ *
241
+ * Configure in module registration:
242
+ * ```typescript
243
+ * RenderModule.forRoot({
244
+ * allowedCookies: ['theme', 'locale', 'consent']
245
+ * })
246
+ * ```
247
+ *
248
+ * @example
249
+ * ```tsx
250
+ * const cookies = useCookies();
251
+ * console.log(cookies.theme); // 'dark'
252
+ * console.log(cookies.locale); // 'en-US'
253
+ * ```
254
+ */
255
+ useCookies: () => Record<string, string>;
256
+ /**
257
+ * Hook to access a specific cookie by name.
258
+ * Returns undefined if the cookie is not configured or not present.
259
+ *
260
+ * @param name - The cookie name (as configured in allowedCookies)
261
+ *
262
+ * @example
263
+ * ```tsx
264
+ * const theme = useCookie('theme');
265
+ * if (theme === 'dark') {
266
+ * console.log('Dark mode enabled');
267
+ * }
268
+ * ```
269
+ */
270
+ useCookie: (name: string) => string | undefined;
271
+ };
272
+ declare const usePageContext: () => RenderContext;
273
+ declare const useParams: () => Record<string, string>;
274
+ declare const useQuery: () => Record<string, string | string[]>;
275
+ declare const useRequest: () => RenderContext;
276
+ declare const useHeaders: () => Record<string, string>;
277
+ declare const useHeader: (name: string) => string | undefined;
278
+ declare const useCookies: () => Record<string, string>;
279
+ declare const useCookie: (name: string) => string | undefined;
280
+
281
+ export { type PageProps as P, PageContextProvider as a, useParams as b, createSSRHooks as c, useQuery as d, useRequest as e, useHeaders as f, useHeader as g, useCookies as h, useCookie as i, updatePageContext as j, usePageContext as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs-ssr/react",
3
- "version": "0.2.6",
3
+ "version": "0.3.1",
4
4
  "description": "React SSR for NestJS that respects Clean Architecture. Proper DI, SOLID principles, clear separation of concerns.",
5
5
  "keywords": [
6
6
  "nestjs",
package/src/global.d.ts CHANGED
@@ -18,6 +18,17 @@ declare global {
18
18
  * Component name for the current page
19
19
  */
20
20
  __COMPONENT_NAME__: string;
21
+
22
+ /**
23
+ * Layout metadata from the server for navigation
24
+ */
25
+ __LAYOUTS__: Array<{ name: string; props?: any }>;
26
+
27
+ /**
28
+ * Module registry for segment hydration after client-side navigation.
29
+ * Set by entry-client.tsx using Vite's import.meta.glob.
30
+ */
31
+ __MODULES__: Record<string, { default: React.ComponentType<any> }>;
21
32
  }
22
33
 
23
34
  interface ImportMeta {
@@ -1,7 +1,10 @@
1
1
  /// <reference types="@nestjs-ssr/react/global" />
2
2
  import React, { StrictMode } from 'react';
3
3
  import { hydrateRoot } from 'react-dom/client';
4
- import { PageContextProvider } from '@nestjs-ssr/react/client';
4
+ import {
5
+ PageContextProvider,
6
+ NavigationProvider,
7
+ } from '@nestjs-ssr/react/client';
5
8
 
6
9
  const componentName = window.__COMPONENT_NAME__;
7
10
  const initialProps = window.__INITIAL_STATE__ || {};
@@ -15,6 +18,9 @@ const modules: Record<string, { default: React.ComponentType<any> }> =
15
18
  eager: true,
16
19
  });
17
20
 
21
+ // Export modules globally for segment hydration after client-side navigation
22
+ window.__MODULES__ = modules;
23
+
18
24
  // Build a map of components with their metadata
19
25
  // Filter out entry files and modules without default exports
20
26
  const componentMap = Object.entries(modules)
@@ -140,14 +146,24 @@ function composeWithLayout(
140
146
  // Compose the component with its layout (if any)
141
147
  const composedElement = composeWithLayout(ViewComponent, initialProps);
142
148
 
143
- // Wrap with PageContextProvider to make context available via hooks
149
+ // Wrap with providers to make context and navigation state available via hooks
144
150
  const wrappedElement = (
145
- <PageContextProvider context={renderContext}>
146
- {composedElement}
147
- </PageContextProvider>
151
+ <NavigationProvider>
152
+ <PageContextProvider context={renderContext}>
153
+ {composedElement}
154
+ </PageContextProvider>
155
+ </NavigationProvider>
148
156
  );
149
157
 
150
158
  hydrateRoot(
151
159
  document.getElementById('root')!,
152
160
  <StrictMode>{wrappedElement}</StrictMode>,
153
161
  );
162
+
163
+ // Handle browser back/forward navigation
164
+ window.addEventListener('popstate', async () => {
165
+ // Dynamically import navigate to avoid circular dependency with hydrate-segment
166
+ const { navigate } = await import('@nestjs-ssr/react/client');
167
+ // Re-navigate to the current URL (browser already updated location)
168
+ navigate(location.href, { replace: true, scroll: false });
169
+ });
@@ -1,23 +1,35 @@
1
1
  import React from 'react';
2
2
  import { renderToString, renderToPipeableStream } from 'react-dom/server';
3
- import { PageContextProvider } from '@nestjs-ssr/react';
3
+ import { PageContextProvider } from '@nestjs-ssr/react/client';
4
4
 
5
5
  /**
6
- * Compose a component with its layouts from the interceptor
7
- * Layouts are passed from the RenderInterceptor based on decorators
6
+ * Compose a component with its layouts from the interceptor.
7
+ * Layouts are passed from the RenderInterceptor based on decorators.
8
+ * Each layout is wrapped with data-layout and data-outlet attributes
9
+ * for client-side navigation segment swapping.
8
10
  */
9
11
  function composeWithLayouts(
10
12
  ViewComponent: React.ComponentType<any>,
11
13
  props: any,
12
14
  layouts: Array<{ layout: React.ComponentType<any>; props?: any }> = [],
15
+ context?: any,
13
16
  ): React.ReactElement {
14
17
  // Start with the page component
15
18
  let result = <ViewComponent {...props} />;
16
19
 
17
20
  // Wrap with each layout in the chain (outermost to innermost in array)
18
21
  // We iterate normally because layouts are already in correct order from interceptor
22
+ // Pass context to layouts so they can access path, params, etc. for navigation
23
+ // Each layout gets data-layout attribute and children are wrapped in data-outlet
19
24
  for (const { layout: Layout, props: layoutProps } of layouts) {
20
- result = <Layout layoutProps={layoutProps}>{result}</Layout>;
25
+ const layoutName = Layout.displayName || Layout.name || 'Layout';
26
+ result = (
27
+ <div data-layout={layoutName}>
28
+ <Layout context={context} layoutProps={layoutProps}>
29
+ <div data-outlet={layoutName}>{result}</div>
30
+ </Layout>
31
+ </div>
32
+ );
21
33
  }
22
34
 
23
35
  return result;
@@ -32,7 +44,12 @@ export function renderComponent(
32
44
  data: any,
33
45
  ) {
34
46
  const { data: pageData, __context: context, __layouts: layouts } = data;
35
- const composedElement = composeWithLayouts(ViewComponent, pageData, layouts);
47
+ const composedElement = composeWithLayouts(
48
+ ViewComponent,
49
+ pageData,
50
+ layouts,
51
+ context,
52
+ );
36
53
 
37
54
  // Wrap with PageContextProvider to make context available via hooks
38
55
  const wrappedElement = (
@@ -44,6 +61,26 @@ export function renderComponent(
44
61
  return renderToString(wrappedElement);
45
62
  }
46
63
 
64
+ /**
65
+ * Render just the page component for segment navigation.
66
+ * No layout wrappers - the layout already exists on the client.
67
+ */
68
+ export function renderSegment(
69
+ ViewComponent: React.ComponentType<any>,
70
+ data: any,
71
+ ) {
72
+ const { data: pageData, __context: context } = data;
73
+
74
+ // Render just the page component, no layout wrappers
75
+ const element = (
76
+ <PageContextProvider context={context}>
77
+ <ViewComponent {...pageData} />
78
+ </PageContextProvider>
79
+ );
80
+
81
+ return renderToString(element);
82
+ }
83
+
47
84
  /**
48
85
  * Streaming SSR (mode: 'stream' - default)
49
86
  * Modern approach with progressive rendering and Suspense support
@@ -59,7 +96,12 @@ export function renderComponentStream(
59
96
  },
60
97
  ) {
61
98
  const { data: pageData, __context: context, __layouts: layouts } = data;
62
- const composedElement = composeWithLayouts(ViewComponent, pageData, layouts);
99
+ const composedElement = composeWithLayouts(
100
+ ViewComponent,
101
+ pageData,
102
+ layouts,
103
+ context,
104
+ );
63
105
 
64
106
  // Wrap with PageContextProvider to make context available via hooks
65
107
  const wrappedElement = (