@nestjs-ssr/react 0.3.22 → 0.3.23

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.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { P as PageContextProvider, a as PageProps, c as createSSRHooks, u as updatePageContext, b as useCookie, d as useCookies, e as useHeader, f as useHeaders, g as usePageContext, h as useParams, i as useQuery, j as useRequest } from './use-page-context-DXjw-66u.mjs';
1
+ export { a as PageContextProvider, P as PageProps, c as createSSRHooks, j as updatePageContext, u as useCookie, b as useCookies, d as useHeader, e as useHeaders, f as usePageContext, g as useParams, h as useQuery, i as useRequest } from './use-page-context-CmxWHIK3.mjs';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React from 'react';
4
4
  export { R as RenderContext } from './render-response.interface-ClWJXKL4.mjs';
@@ -67,4 +67,71 @@ declare function useNavigate(): (url: string, options?: NavigateOptions) => Prom
67
67
  */
68
68
  declare function useIsNavigating(): boolean;
69
69
 
70
- export { Link, type LinkProps, type NavigateOptions, NavigationProvider, navigate, useIsNavigating, useNavigate, useNavigation, useNavigationState };
70
+ /**
71
+ * Shared view-component resolution used by both the initial hydration entry
72
+ * (`entry-client.tsx`) and client-side segment navigation (`hydrate-segment.tsx`).
73
+ *
74
+ * Components are discovered with Vite's `import.meta.glob` over every `views`
75
+ * directory in the project (including those colocated inside feature modules,
76
+ * e.g. `@/products/views/list.tsx`). The server can only communicate *which*
77
+ * component to hydrate by its name (`displayName` || function name), because the
78
+ * component instance the server renders comes from the Nest build and is never
79
+ * identity-equal to the Vite-bundled module. That makes the name the contract,
80
+ * so it must be unique across all `views` directories.
81
+ *
82
+ * This module centralizes the matching logic so both call sites stay in sync
83
+ * (previously they normalized filenames differently — `recipe-list` resolved on
84
+ * first load but failed on client navigation) and so same-name collisions across
85
+ * submodules surface a clear error instead of silently hydrating the wrong view.
86
+ */
87
+ interface ViewModule {
88
+ default: React.ComponentType<any>;
89
+ }
90
+ type ViewModuleRegistry = Record<string, ViewModule>;
91
+ interface ComponentEntry {
92
+ /** The glob key, e.g. `/src/products/views/list.tsx`. Unique per file. */
93
+ path: string;
94
+ component: React.ComponentType<any>;
95
+ /** `displayName` || function name, if any. */
96
+ name?: string;
97
+ /** Filename without extension, e.g. `recipe-list`. */
98
+ filename?: string;
99
+ /** Filename converted to PascalCase, e.g. `RecipeList`. */
100
+ normalizedFilename?: string;
101
+ }
102
+ /**
103
+ * Build the list of resolvable view components from a glob module registry.
104
+ * Entry files and modules without a default export are excluded.
105
+ */
106
+ declare function buildComponentRegistry(modules: ViewModuleRegistry): ComponentEntry[];
107
+ interface ResolveOptions {
108
+ /**
109
+ * Called when a name resolves to more than one distinct component (a
110
+ * collision across `views` directories). Receives the requested name and the
111
+ * sorted list of colliding file paths. Defaults to `console.error`.
112
+ */
113
+ onCollision?: (name: string, paths: string[]) => void;
114
+ }
115
+ /**
116
+ * Resolve a view component by the name the server sent for hydration.
117
+ *
118
+ * Matching is tiered by precedence (mirrors the server's `displayName || name`
119
+ * identifier); the first non-empty tier wins:
120
+ * 1. exact `displayName` / function name
121
+ * 2. normalized (PascalCase) filename, e.g. "recipe-list.tsx" -> "RecipeList"
122
+ * 3. raw filename (case-insensitive)
123
+ * 4. minified anonymous defaults ("default", "default_1", ...)
124
+ *
125
+ * Tiering matters: a submodule view `recipe-list.tsx` (displayName "SpecialsList")
126
+ * normalizes to "RecipeList", so a flat match would tie it with the *real*
127
+ * `RecipeList` and mis-resolve. By preferring the exact-name tier, the explicit
128
+ * `displayName` always wins over an incidental filename collision.
129
+ *
130
+ * A collision is only reported when the *winning* tier itself contains more than
131
+ * one distinct component — i.e. the name is genuinely ambiguous (e.g. two files
132
+ * both `displayName = "RecipeList"`). In that case `onCollision` is invoked and
133
+ * resolution falls back to the first match by sorted path for determinism.
134
+ */
135
+ declare function resolveViewComponent(name: string, modules: ViewModuleRegistry, options?: ResolveOptions): React.ComponentType<any> | undefined;
136
+
137
+ export { type ComponentEntry, Link, type LinkProps, type NavigateOptions, NavigationProvider, type ResolveOptions, type ViewModule, type ViewModuleRegistry, buildComponentRegistry, navigate, resolveViewComponent, useIsNavigating, useNavigate, useNavigation, useNavigationState };
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { P as PageContextProvider, a as PageProps, c as createSSRHooks, u as updatePageContext, b as useCookie, d as useCookies, e as useHeader, f as useHeaders, g as usePageContext, h as useParams, i as useQuery, j as useRequest } from './use-page-context-Bo3Z0m-_.js';
1
+ export { a as PageContextProvider, P as PageProps, c as createSSRHooks, j as updatePageContext, u as useCookie, b as useCookies, d as useHeader, e as useHeaders, f as usePageContext, g as useParams, h as useQuery, i as useRequest } from './use-page-context-CUV31oda.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import React from 'react';
4
4
  export { R as RenderContext } from './render-response.interface-ClWJXKL4.js';
@@ -67,4 +67,71 @@ declare function useNavigate(): (url: string, options?: NavigateOptions) => Prom
67
67
  */
68
68
  declare function useIsNavigating(): boolean;
69
69
 
70
- export { Link, type LinkProps, type NavigateOptions, NavigationProvider, navigate, useIsNavigating, useNavigate, useNavigation, useNavigationState };
70
+ /**
71
+ * Shared view-component resolution used by both the initial hydration entry
72
+ * (`entry-client.tsx`) and client-side segment navigation (`hydrate-segment.tsx`).
73
+ *
74
+ * Components are discovered with Vite's `import.meta.glob` over every `views`
75
+ * directory in the project (including those colocated inside feature modules,
76
+ * e.g. `@/products/views/list.tsx`). The server can only communicate *which*
77
+ * component to hydrate by its name (`displayName` || function name), because the
78
+ * component instance the server renders comes from the Nest build and is never
79
+ * identity-equal to the Vite-bundled module. That makes the name the contract,
80
+ * so it must be unique across all `views` directories.
81
+ *
82
+ * This module centralizes the matching logic so both call sites stay in sync
83
+ * (previously they normalized filenames differently — `recipe-list` resolved on
84
+ * first load but failed on client navigation) and so same-name collisions across
85
+ * submodules surface a clear error instead of silently hydrating the wrong view.
86
+ */
87
+ interface ViewModule {
88
+ default: React.ComponentType<any>;
89
+ }
90
+ type ViewModuleRegistry = Record<string, ViewModule>;
91
+ interface ComponentEntry {
92
+ /** The glob key, e.g. `/src/products/views/list.tsx`. Unique per file. */
93
+ path: string;
94
+ component: React.ComponentType<any>;
95
+ /** `displayName` || function name, if any. */
96
+ name?: string;
97
+ /** Filename without extension, e.g. `recipe-list`. */
98
+ filename?: string;
99
+ /** Filename converted to PascalCase, e.g. `RecipeList`. */
100
+ normalizedFilename?: string;
101
+ }
102
+ /**
103
+ * Build the list of resolvable view components from a glob module registry.
104
+ * Entry files and modules without a default export are excluded.
105
+ */
106
+ declare function buildComponentRegistry(modules: ViewModuleRegistry): ComponentEntry[];
107
+ interface ResolveOptions {
108
+ /**
109
+ * Called when a name resolves to more than one distinct component (a
110
+ * collision across `views` directories). Receives the requested name and the
111
+ * sorted list of colliding file paths. Defaults to `console.error`.
112
+ */
113
+ onCollision?: (name: string, paths: string[]) => void;
114
+ }
115
+ /**
116
+ * Resolve a view component by the name the server sent for hydration.
117
+ *
118
+ * Matching is tiered by precedence (mirrors the server's `displayName || name`
119
+ * identifier); the first non-empty tier wins:
120
+ * 1. exact `displayName` / function name
121
+ * 2. normalized (PascalCase) filename, e.g. "recipe-list.tsx" -> "RecipeList"
122
+ * 3. raw filename (case-insensitive)
123
+ * 4. minified anonymous defaults ("default", "default_1", ...)
124
+ *
125
+ * Tiering matters: a submodule view `recipe-list.tsx` (displayName "SpecialsList")
126
+ * normalizes to "RecipeList", so a flat match would tie it with the *real*
127
+ * `RecipeList` and mis-resolve. By preferring the exact-name tier, the explicit
128
+ * `displayName` always wins over an incidental filename collision.
129
+ *
130
+ * A collision is only reported when the *winning* tier itself contains more than
131
+ * one distinct component — i.e. the name is genuinely ambiguous (e.g. two files
132
+ * both `displayName = "RecipeList"`). In that case `onCollision` is invoked and
133
+ * resolution falls back to the first match by sorted path for determinism.
134
+ */
135
+ declare function resolveViewComponent(name: string, modules: ViewModuleRegistry, options?: ResolveOptions): React.ComponentType<any> | undefined;
136
+
137
+ export { type ComponentEntry, Link, type LinkProps, type NavigateOptions, NavigationProvider, type ResolveOptions, type ViewModule, type ViewModuleRegistry, buildComponentRegistry, navigate, resolveViewComponent, useIsNavigating, useNavigate, useNavigation, useNavigationState };
package/dist/client.js CHANGED
@@ -258,6 +258,74 @@ var useHeaders = defaultHooks.useHeaders;
258
258
  var useHeader = defaultHooks.useHeader;
259
259
  var useCookies = defaultHooks.useCookies;
260
260
  var useCookie = defaultHooks.useCookie;
261
+
262
+ // src/react/navigation/resolve-component.ts
263
+ function toPascalCase(str) {
264
+ return str.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase());
265
+ }
266
+ __name(toPascalCase, "toPascalCase");
267
+ function isEntryFile(filename) {
268
+ return /^entry-/.test(filename);
269
+ }
270
+ __name(isEntryFile, "isEntryFile");
271
+ function buildComponentRegistry(modules) {
272
+ return Object.entries(modules).filter(([path, module]) => {
273
+ const filename = path.split("/").pop() ?? "";
274
+ if (isEntryFile(filename)) return false;
275
+ return module?.default !== void 0;
276
+ }).map(([path, module]) => {
277
+ const component = module.default;
278
+ const name = component.displayName || component.name || void 0;
279
+ const filename = path.split("/").pop()?.replace(/\.tsx?$/, "");
280
+ const normalizedFilename = filename ? toPascalCase(filename) : void 0;
281
+ return {
282
+ path,
283
+ component,
284
+ name,
285
+ filename,
286
+ normalizedFilename
287
+ };
288
+ });
289
+ }
290
+ __name(buildComponentRegistry, "buildComponentRegistry");
291
+ function reportCollision(name, paths) {
292
+ console.error(`[nestjs-ssr] Multiple view components resolve to the name "${name}":
293
+ ` + paths.map((p) => ` - ${p}`).join("\n") + `
294
+ View component names must be unique across all "views" directories. Give the components distinct names by setting a unique \`displayName\` (or renaming the files). Falling back to the first match by path.`);
295
+ }
296
+ __name(reportCollision, "reportCollision");
297
+ function resolveViewComponent(name, modules, options = {}) {
298
+ const registry = buildComponentRegistry(modules);
299
+ const lower = name.toLowerCase();
300
+ const tiers = [
301
+ registry.filter((c) => c.name === name),
302
+ registry.filter((c) => c.normalizedFilename === name),
303
+ registry.filter((c) => c.filename === lower)
304
+ ];
305
+ for (const tier of tiers) {
306
+ if (tier.length === 0) continue;
307
+ const distinct = new Set(tier.map((c) => c.component));
308
+ if (distinct.size > 1) {
309
+ const sorted = tier.slice().sort((a, b) => a.path.localeCompare(b.path));
310
+ (options.onCollision ?? reportCollision)(name, sorted.map((c) => c.path));
311
+ return sorted[0].component;
312
+ }
313
+ return tier[0].component;
314
+ }
315
+ if (/^default(_\d+)?$/.test(name)) {
316
+ if (registry.length === 1) {
317
+ return registry[0].component;
318
+ }
319
+ const indexMatch = name.match(/^default_(\d+)$/);
320
+ const index = indexMatch ? parseInt(indexMatch[1], 10) - 1 : 0;
321
+ const defaults = registry.filter((c) => c.name === "default").sort((a, b) => a.path.localeCompare(b.path));
322
+ return defaults[index]?.component;
323
+ }
324
+ return void 0;
325
+ }
326
+ __name(resolveViewComponent, "resolveViewComponent");
327
+
328
+ // src/react/navigation/hydrate-segment.tsx
261
329
  var rootRegistry = /* @__PURE__ */ new WeakMap();
262
330
  function hydrateSegment(outlet, componentName, props, layouts) {
263
331
  const modules = window.__MODULES__;
@@ -265,7 +333,7 @@ function hydrateSegment(outlet, componentName, props, layouts) {
265
333
  console.warn("[navigation] Module registry not available for segment hydration. Make sure entry-client.tsx exports window.__MODULES__.");
266
334
  return;
267
335
  }
268
- const ViewComponent = resolveComponent(componentName, modules);
336
+ const ViewComponent = resolveViewComponent(componentName, modules);
269
337
  if (!ViewComponent) {
270
338
  console.warn(`[navigation] Component "${componentName}" not found for hydration. Available components: ` + Object.keys(modules).map((path) => {
271
339
  const c = modules[path].default;
@@ -300,7 +368,7 @@ function composeWithLayouts(ViewComponent, props, layouts, context, modules) {
300
368
  let result = /* @__PURE__ */ React2__default.default.createElement(ViewComponent, props);
301
369
  for (let i = layouts.length - 1; i >= 0; i--) {
302
370
  const { name: layoutName, props: layoutProps } = layouts[i];
303
- const Layout = resolveComponent(layoutName, modules);
371
+ const Layout = resolveViewComponent(layoutName, modules);
304
372
  if (!Layout) {
305
373
  console.warn(`[navigation] Layout "${layoutName}" not found for hydration`);
306
374
  continue;
@@ -317,41 +385,6 @@ function composeWithLayouts(ViewComponent, props, layouts, context, modules) {
317
385
  return result;
318
386
  }
319
387
  __name(composeWithLayouts, "composeWithLayouts");
320
- function resolveComponent(name, modules) {
321
- const componentMap = Object.entries(modules).filter(([path, module]) => {
322
- const filename = path.split("/").pop();
323
- if (filename === "entry-client.tsx" || filename === "entry-server.tsx") {
324
- return false;
325
- }
326
- return module.default !== void 0;
327
- }).map(([path, module]) => {
328
- const component = module.default;
329
- const componentName = component.displayName || component.name;
330
- const filename = path.split("/").pop()?.replace(".tsx", "");
331
- const normalizedFilename = filename ? filename.charAt(0).toUpperCase() + filename.slice(1) : void 0;
332
- return {
333
- component,
334
- name: componentName,
335
- filename,
336
- normalizedFilename
337
- };
338
- });
339
- let match = componentMap.find((c) => c.name === name || c.normalizedFilename === name || c.filename === name.toLowerCase());
340
- if (!match && /^default(_\d+)?$/.test(name)) {
341
- if (componentMap.length === 1) {
342
- match = componentMap[0];
343
- } else {
344
- const indexMatch = name.match(/^default_(\d+)$/);
345
- const index = indexMatch ? parseInt(indexMatch[1], 10) - 1 : 0;
346
- const defaultComponents = componentMap.filter((c) => c.name === "default").sort((a, b) => (a.filename || "").localeCompare(b.filename || ""));
347
- if (defaultComponents[index]) {
348
- match = defaultComponents[index];
349
- }
350
- }
351
- }
352
- return match?.component;
353
- }
354
- __name(resolveComponent, "resolveComponent");
355
388
 
356
389
  // src/react/navigation/navigate.ts
357
390
  var setNavigationState = null;
@@ -579,8 +612,10 @@ __name(useIsNavigating, "useIsNavigating");
579
612
  exports.Link = Link;
580
613
  exports.NavigationProvider = NavigationProvider;
581
614
  exports.PageContextProvider = PageContextProvider;
615
+ exports.buildComponentRegistry = buildComponentRegistry;
582
616
  exports.createSSRHooks = createSSRHooks;
583
617
  exports.navigate = navigate;
618
+ exports.resolveViewComponent = resolveViewComponent;
584
619
  exports.updatePageContext = updatePageContext;
585
620
  exports.useCookie = useCookie;
586
621
  exports.useCookies = useCookies;
package/dist/client.mjs CHANGED
@@ -252,6 +252,74 @@ var useHeaders = defaultHooks.useHeaders;
252
252
  var useHeader = defaultHooks.useHeader;
253
253
  var useCookies = defaultHooks.useCookies;
254
254
  var useCookie = defaultHooks.useCookie;
255
+
256
+ // src/react/navigation/resolve-component.ts
257
+ function toPascalCase(str) {
258
+ return str.replace(/(^|-)([a-z])/g, (_, __, c) => c.toUpperCase());
259
+ }
260
+ __name(toPascalCase, "toPascalCase");
261
+ function isEntryFile(filename) {
262
+ return /^entry-/.test(filename);
263
+ }
264
+ __name(isEntryFile, "isEntryFile");
265
+ function buildComponentRegistry(modules) {
266
+ return Object.entries(modules).filter(([path, module]) => {
267
+ const filename = path.split("/").pop() ?? "";
268
+ if (isEntryFile(filename)) return false;
269
+ return module?.default !== void 0;
270
+ }).map(([path, module]) => {
271
+ const component = module.default;
272
+ const name = component.displayName || component.name || void 0;
273
+ const filename = path.split("/").pop()?.replace(/\.tsx?$/, "");
274
+ const normalizedFilename = filename ? toPascalCase(filename) : void 0;
275
+ return {
276
+ path,
277
+ component,
278
+ name,
279
+ filename,
280
+ normalizedFilename
281
+ };
282
+ });
283
+ }
284
+ __name(buildComponentRegistry, "buildComponentRegistry");
285
+ function reportCollision(name, paths) {
286
+ console.error(`[nestjs-ssr] Multiple view components resolve to the name "${name}":
287
+ ` + paths.map((p) => ` - ${p}`).join("\n") + `
288
+ View component names must be unique across all "views" directories. Give the components distinct names by setting a unique \`displayName\` (or renaming the files). Falling back to the first match by path.`);
289
+ }
290
+ __name(reportCollision, "reportCollision");
291
+ function resolveViewComponent(name, modules, options = {}) {
292
+ const registry = buildComponentRegistry(modules);
293
+ const lower = name.toLowerCase();
294
+ const tiers = [
295
+ registry.filter((c) => c.name === name),
296
+ registry.filter((c) => c.normalizedFilename === name),
297
+ registry.filter((c) => c.filename === lower)
298
+ ];
299
+ for (const tier of tiers) {
300
+ if (tier.length === 0) continue;
301
+ const distinct = new Set(tier.map((c) => c.component));
302
+ if (distinct.size > 1) {
303
+ const sorted = tier.slice().sort((a, b) => a.path.localeCompare(b.path));
304
+ (options.onCollision ?? reportCollision)(name, sorted.map((c) => c.path));
305
+ return sorted[0].component;
306
+ }
307
+ return tier[0].component;
308
+ }
309
+ if (/^default(_\d+)?$/.test(name)) {
310
+ if (registry.length === 1) {
311
+ return registry[0].component;
312
+ }
313
+ const indexMatch = name.match(/^default_(\d+)$/);
314
+ const index = indexMatch ? parseInt(indexMatch[1], 10) - 1 : 0;
315
+ const defaults = registry.filter((c) => c.name === "default").sort((a, b) => a.path.localeCompare(b.path));
316
+ return defaults[index]?.component;
317
+ }
318
+ return void 0;
319
+ }
320
+ __name(resolveViewComponent, "resolveViewComponent");
321
+
322
+ // src/react/navigation/hydrate-segment.tsx
255
323
  var rootRegistry = /* @__PURE__ */ new WeakMap();
256
324
  function hydrateSegment(outlet, componentName, props, layouts) {
257
325
  const modules = window.__MODULES__;
@@ -259,7 +327,7 @@ function hydrateSegment(outlet, componentName, props, layouts) {
259
327
  console.warn("[navigation] Module registry not available for segment hydration. Make sure entry-client.tsx exports window.__MODULES__.");
260
328
  return;
261
329
  }
262
- const ViewComponent = resolveComponent(componentName, modules);
330
+ const ViewComponent = resolveViewComponent(componentName, modules);
263
331
  if (!ViewComponent) {
264
332
  console.warn(`[navigation] Component "${componentName}" not found for hydration. Available components: ` + Object.keys(modules).map((path) => {
265
333
  const c = modules[path].default;
@@ -294,7 +362,7 @@ function composeWithLayouts(ViewComponent, props, layouts, context, modules) {
294
362
  let result = /* @__PURE__ */ React2.createElement(ViewComponent, props);
295
363
  for (let i = layouts.length - 1; i >= 0; i--) {
296
364
  const { name: layoutName, props: layoutProps } = layouts[i];
297
- const Layout = resolveComponent(layoutName, modules);
365
+ const Layout = resolveViewComponent(layoutName, modules);
298
366
  if (!Layout) {
299
367
  console.warn(`[navigation] Layout "${layoutName}" not found for hydration`);
300
368
  continue;
@@ -311,41 +379,6 @@ function composeWithLayouts(ViewComponent, props, layouts, context, modules) {
311
379
  return result;
312
380
  }
313
381
  __name(composeWithLayouts, "composeWithLayouts");
314
- function resolveComponent(name, modules) {
315
- const componentMap = Object.entries(modules).filter(([path, module]) => {
316
- const filename = path.split("/").pop();
317
- if (filename === "entry-client.tsx" || filename === "entry-server.tsx") {
318
- return false;
319
- }
320
- return module.default !== void 0;
321
- }).map(([path, module]) => {
322
- const component = module.default;
323
- const componentName = component.displayName || component.name;
324
- const filename = path.split("/").pop()?.replace(".tsx", "");
325
- const normalizedFilename = filename ? filename.charAt(0).toUpperCase() + filename.slice(1) : void 0;
326
- return {
327
- component,
328
- name: componentName,
329
- filename,
330
- normalizedFilename
331
- };
332
- });
333
- let match = componentMap.find((c) => c.name === name || c.normalizedFilename === name || c.filename === name.toLowerCase());
334
- if (!match && /^default(_\d+)?$/.test(name)) {
335
- if (componentMap.length === 1) {
336
- match = componentMap[0];
337
- } else {
338
- const indexMatch = name.match(/^default_(\d+)$/);
339
- const index = indexMatch ? parseInt(indexMatch[1], 10) - 1 : 0;
340
- const defaultComponents = componentMap.filter((c) => c.name === "default").sort((a, b) => (a.filename || "").localeCompare(b.filename || ""));
341
- if (defaultComponents[index]) {
342
- match = defaultComponents[index];
343
- }
344
- }
345
- }
346
- return match?.component;
347
- }
348
- __name(resolveComponent, "resolveComponent");
349
382
 
350
383
  // src/react/navigation/navigate.ts
351
384
  var setNavigationState = null;
@@ -570,4 +603,4 @@ function useIsNavigating() {
570
603
  }
571
604
  __name(useIsNavigating, "useIsNavigating");
572
605
 
573
- export { Link, NavigationProvider, PageContextProvider, createSSRHooks, navigate, updatePageContext, useCookie, useCookies, useHeader, useHeaders, useIsNavigating, useNavigate, useNavigation, useNavigationState, usePageContext, useParams, useQuery, useRequest };
606
+ export { Link, NavigationProvider, PageContextProvider, buildComponentRegistry, createSSRHooks, navigate, resolveViewComponent, updatePageContext, useCookie, useCookies, useHeader, useHeaders, useIsNavigating, useNavigate, useNavigation, useNavigationState, usePageContext, useParams, useQuery, useRequest };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { C as ContextFactory, E as ErrorPageDevelopment, a as ErrorPageProduction, R as RenderConfig, b as RenderInterceptor, c as RenderModule, d as RenderService, S as SSRMode, e as StreamingErrorHandler, T as TemplateParserService } from './index-CF5JpFZh.mjs';
2
2
  import React, { ComponentType, ReactNode } from 'react';
3
- import { a as PageProps } from './use-page-context-DXjw-66u.mjs';
4
- export { P as PageContextProvider, c as createSSRHooks, b as useCookie, d as useCookies, e as useHeader, f as useHeaders, g as usePageContext, h as useParams, i as useQuery, j as useRequest } from './use-page-context-DXjw-66u.mjs';
3
+ import { P as PageProps } from './use-page-context-CmxWHIK3.mjs';
4
+ export { a as PageContextProvider, c as createSSRHooks, u as useCookie, b as useCookies, d as useHeader, e as useHeaders, f as usePageContext, g as useParams, h as useQuery, i as useRequest } from './use-page-context-CmxWHIK3.mjs';
5
5
  import { R as RenderContext, H as HeadData, a as RenderResponse } from './render-response.interface-ClWJXKL4.mjs';
6
6
  import '@nestjs/common';
7
7
  import 'http';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { C as ContextFactory, E as ErrorPageDevelopment, a as ErrorPageProduction, R as RenderConfig, b as RenderInterceptor, c as RenderModule, d as RenderService, S as SSRMode, e as StreamingErrorHandler, T as TemplateParserService } from './index-Cd0UvVmK.js';
2
2
  import React, { ComponentType, ReactNode } from 'react';
3
- import { a as PageProps } from './use-page-context-Bo3Z0m-_.js';
4
- export { P as PageContextProvider, c as createSSRHooks, b as useCookie, d as useCookies, e as useHeader, f as useHeaders, g as usePageContext, h as useParams, i as useQuery, j as useRequest } from './use-page-context-Bo3Z0m-_.js';
3
+ import { P as PageProps } from './use-page-context-CUV31oda.js';
4
+ export { a as PageContextProvider, c as createSSRHooks, u as useCookie, b as useCookies, d as useHeader, e as useHeaders, f as usePageContext, g as useParams, h as useQuery, i as useRequest } from './use-page-context-CUV31oda.js';
5
5
  import { R as RenderContext, H as HeadData, a as RenderResponse } from './render-response.interface-ClWJXKL4.js';
6
6
  import '@nestjs/common';
7
7
  import 'http';
@@ -5,17 +5,14 @@ import { hydrateRoot } from 'react-dom/client';
5
5
  import {
6
6
  PageContextProvider,
7
7
  NavigationProvider,
8
+ buildComponentRegistry,
9
+ resolveViewComponent,
8
10
  } from '@nestjs-ssr/react/client';
9
11
 
10
12
  const componentName = window.__COMPONENT_NAME__;
11
13
  const initialProps = window.__INITIAL_STATE__ || {};
12
14
  const renderContext = window.__CONTEXT__ || {};
13
15
 
14
- /** Convert kebab-case filename to PascalCase (e.g., "login-page" → "LoginPage") */
15
- function toPascalCase(str: string): string {
16
- return str.replace(/(^|-)([a-z])/g, (_, __, c: string) => c.toUpperCase());
17
- }
18
-
19
16
  // Auto-discover root layout using Vite's glob import (must match server-side discovery)
20
17
  // @ts-ignore - Vite-specific API
21
18
  const layoutModules = import.meta.glob('@/views/layout.tsx', {
@@ -25,75 +22,26 @@ const layoutModules = import.meta.glob('@/views/layout.tsx', {
25
22
  const layoutPath = Object.keys(layoutModules)[0];
26
23
  const RootLayout = layoutPath ? layoutModules[layoutPath].default : null;
27
24
 
28
- // Auto-import all view components using Vite's glob feature
29
- // Exclude entry-client.tsx and entry-server.tsx from the glob
25
+ // Auto-import all view components using Vite's glob feature.
26
+ // Match any `views` directory under the source root (`@`), so views colocated
27
+ // inside feature modules (e.g. `@/products/views/list.tsx`) are discovered too,
28
+ // not just the top-level `@/views` folder. Exclude entry-* files in any views dir.
30
29
  // @ts-ignore - Vite-specific API
31
30
  const modules: Record<string, { default: React.ComponentType<any> }> =
32
- import.meta.glob(['@/views/**/*.tsx', '!@/views/entry-*.tsx'], {
31
+ import.meta.glob(['@/**/views/**/*.tsx', '!@/**/views/entry-*.tsx'], {
33
32
  eager: true,
34
33
  });
35
34
 
36
35
  // Export modules globally for segment hydration after client-side navigation
37
36
  window.__MODULES__ = modules;
38
37
 
39
- // Build a map of components with their metadata
40
- // Filter out entry files and modules without default exports
41
- const componentMap = Object.entries(modules)
42
- .filter(([path, module]) => {
43
- // Skip entry-client and entry-server files
44
- const filename = path.split('/').pop();
45
- if (filename === 'entry-client.tsx' || filename === 'entry-server.tsx') {
46
- return false;
47
- }
48
- // Only include modules with a default export
49
- return module.default !== undefined;
50
- })
51
- .map(([path, module]) => {
52
- const component = module.default;
53
- const name = component.displayName || component.name;
54
- const filename = path.split('/').pop()?.replace('.tsx', '');
55
- const normalizedFilename = filename ? toPascalCase(filename) : undefined;
56
-
57
- return { path, component, name, filename, normalizedFilename };
58
- });
38
+ // Build the component registry once (used below for layout lookups).
39
+ const componentMap = buildComponentRegistry(modules);
59
40
 
60
- // Find the component by matching in this order:
61
- // 1. Exact match by displayName or function name
62
- // 2. Match by normalized filename (e.g., "home.tsx" -> "Home")
63
- // 3. For minified names (default_N), match the Nth component with name "default"
64
- // 4. If only one component exists, use it (regardless of name)
65
- let ViewComponent: React.ComponentType<any> | undefined;
66
-
67
- // Try exact name match first
68
- ViewComponent = componentMap.find(
69
- (c) =>
70
- c.name === componentName ||
71
- c.normalizedFilename === componentName ||
72
- c.filename === componentName.toLowerCase(),
73
- )?.component;
74
-
75
- // If no match found and component name looks like a generic/minified name (default, default_1, etc.)
76
- if (!ViewComponent && /^default(_\d+)?$/.test(componentName)) {
77
- // If there's only one component, use it regardless of name
78
- if (componentMap.length === 1) {
79
- ViewComponent = componentMap[0].component;
80
- } else {
81
- // Handle minified anonymous functions: default_1, default_2, etc.
82
- // Extract the index from the name (default_1 -> 1, default_2 -> 2, default -> 0)
83
- const match = componentName.match(/^default_(\d+)$/);
84
- const index = match ? parseInt(match[1], 10) - 1 : 0;
85
-
86
- // Get all components with name "default" (anonymous functions), sorted by path for consistency
87
- const defaultComponents = componentMap
88
- .filter((c) => c.name === 'default')
89
- .sort((a, b) => a.path.localeCompare(b.path));
90
-
91
- // Try to match by index
92
- if (defaultComponents[index]) {
93
- ViewComponent = defaultComponents[index].component;
94
- }
95
- }
96
- }
41
+ // Resolve the page component the server rendered. The shared resolver matches by
42
+ // displayName/name, then normalized (PascalCase) filename, then minified default
43
+ // exports and logs a clear error if the name collides across views directories.
44
+ const ViewComponent = resolveViewComponent(componentName, modules);
97
45
 
98
46
  if (!ViewComponent) {
99
47
  const availableComponents = Object.entries(modules)
@@ -279,4 +279,4 @@ declare const useHeader: (name: string) => string | undefined;
279
279
  declare const useCookies: () => Record<string, string>;
280
280
  declare const useCookie: (name: string) => string | undefined;
281
281
 
282
- export { PageContextProvider as P, type PageProps as a, useCookie as b, createSSRHooks as c, useCookies as d, useHeader as e, useHeaders as f, usePageContext as g, useParams as h, useQuery as i, useRequest as j, updatePageContext as u };
282
+ export { type PageProps as P, PageContextProvider as a, useCookies as b, createSSRHooks as c, useHeader as d, useHeaders as e, usePageContext as f, useParams as g, useQuery as h, useRequest as i, updatePageContext as j, useCookie as u };
@@ -279,4 +279,4 @@ declare const useHeader: (name: string) => string | undefined;
279
279
  declare const useCookies: () => Record<string, string>;
280
280
  declare const useCookie: (name: string) => string | undefined;
281
281
 
282
- export { PageContextProvider as P, type PageProps as a, useCookie as b, createSSRHooks as c, useCookies as d, useHeader as e, useHeaders as f, usePageContext as g, useParams as h, useQuery as i, useRequest as j, updatePageContext as u };
282
+ export { type PageProps as P, PageContextProvider as a, useCookies as b, createSSRHooks as c, useHeader as d, useHeaders as e, usePageContext as f, useParams as g, useQuery as h, useRequest as i, updatePageContext as j, useCookie as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs-ssr/react",
3
- "version": "0.3.22",
3
+ "version": "0.3.23",
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",
@@ -5,17 +5,14 @@ import { hydrateRoot } from 'react-dom/client';
5
5
  import {
6
6
  PageContextProvider,
7
7
  NavigationProvider,
8
+ buildComponentRegistry,
9
+ resolveViewComponent,
8
10
  } from '@nestjs-ssr/react/client';
9
11
 
10
12
  const componentName = window.__COMPONENT_NAME__;
11
13
  const initialProps = window.__INITIAL_STATE__ || {};
12
14
  const renderContext = window.__CONTEXT__ || {};
13
15
 
14
- /** Convert kebab-case filename to PascalCase (e.g., "login-page" → "LoginPage") */
15
- function toPascalCase(str: string): string {
16
- return str.replace(/(^|-)([a-z])/g, (_, __, c: string) => c.toUpperCase());
17
- }
18
-
19
16
  // Auto-discover root layout using Vite's glob import (must match server-side discovery)
20
17
  // @ts-ignore - Vite-specific API
21
18
  const layoutModules = import.meta.glob('@/views/layout.tsx', {
@@ -25,75 +22,26 @@ const layoutModules = import.meta.glob('@/views/layout.tsx', {
25
22
  const layoutPath = Object.keys(layoutModules)[0];
26
23
  const RootLayout = layoutPath ? layoutModules[layoutPath].default : null;
27
24
 
28
- // Auto-import all view components using Vite's glob feature
29
- // Exclude entry-client.tsx and entry-server.tsx from the glob
25
+ // Auto-import all view components using Vite's glob feature.
26
+ // Match any `views` directory under the source root (`@`), so views colocated
27
+ // inside feature modules (e.g. `@/products/views/list.tsx`) are discovered too,
28
+ // not just the top-level `@/views` folder. Exclude entry-* files in any views dir.
30
29
  // @ts-ignore - Vite-specific API
31
30
  const modules: Record<string, { default: React.ComponentType<any> }> =
32
- import.meta.glob(['@/views/**/*.tsx', '!@/views/entry-*.tsx'], {
31
+ import.meta.glob(['@/**/views/**/*.tsx', '!@/**/views/entry-*.tsx'], {
33
32
  eager: true,
34
33
  });
35
34
 
36
35
  // Export modules globally for segment hydration after client-side navigation
37
36
  window.__MODULES__ = modules;
38
37
 
39
- // Build a map of components with their metadata
40
- // Filter out entry files and modules without default exports
41
- const componentMap = Object.entries(modules)
42
- .filter(([path, module]) => {
43
- // Skip entry-client and entry-server files
44
- const filename = path.split('/').pop();
45
- if (filename === 'entry-client.tsx' || filename === 'entry-server.tsx') {
46
- return false;
47
- }
48
- // Only include modules with a default export
49
- return module.default !== undefined;
50
- })
51
- .map(([path, module]) => {
52
- const component = module.default;
53
- const name = component.displayName || component.name;
54
- const filename = path.split('/').pop()?.replace('.tsx', '');
55
- const normalizedFilename = filename ? toPascalCase(filename) : undefined;
56
-
57
- return { path, component, name, filename, normalizedFilename };
58
- });
38
+ // Build the component registry once (used below for layout lookups).
39
+ const componentMap = buildComponentRegistry(modules);
59
40
 
60
- // Find the component by matching in this order:
61
- // 1. Exact match by displayName or function name
62
- // 2. Match by normalized filename (e.g., "home.tsx" -> "Home")
63
- // 3. For minified names (default_N), match the Nth component with name "default"
64
- // 4. If only one component exists, use it (regardless of name)
65
- let ViewComponent: React.ComponentType<any> | undefined;
66
-
67
- // Try exact name match first
68
- ViewComponent = componentMap.find(
69
- (c) =>
70
- c.name === componentName ||
71
- c.normalizedFilename === componentName ||
72
- c.filename === componentName.toLowerCase(),
73
- )?.component;
74
-
75
- // If no match found and component name looks like a generic/minified name (default, default_1, etc.)
76
- if (!ViewComponent && /^default(_\d+)?$/.test(componentName)) {
77
- // If there's only one component, use it regardless of name
78
- if (componentMap.length === 1) {
79
- ViewComponent = componentMap[0].component;
80
- } else {
81
- // Handle minified anonymous functions: default_1, default_2, etc.
82
- // Extract the index from the name (default_1 -> 1, default_2 -> 2, default -> 0)
83
- const match = componentName.match(/^default_(\d+)$/);
84
- const index = match ? parseInt(match[1], 10) - 1 : 0;
85
-
86
- // Get all components with name "default" (anonymous functions), sorted by path for consistency
87
- const defaultComponents = componentMap
88
- .filter((c) => c.name === 'default')
89
- .sort((a, b) => a.path.localeCompare(b.path));
90
-
91
- // Try to match by index
92
- if (defaultComponents[index]) {
93
- ViewComponent = defaultComponents[index].component;
94
- }
95
- }
96
- }
41
+ // Resolve the page component the server rendered. The shared resolver matches by
42
+ // displayName/name, then normalized (PascalCase) filename, then minified default
43
+ // exports and logs a clear error if the name collides across views directories.
44
+ const ViewComponent = resolveViewComponent(componentName, modules);
97
45
 
98
46
  if (!ViewComponent) {
99
47
  const availableComponents = Object.entries(modules)