@nestjs-ssr/react 0.3.21 → 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';
package/dist/index.js CHANGED
@@ -1408,6 +1408,7 @@ var ViteInitializerService = class _ViteInitializerService {
1408
1408
  vitePort;
1409
1409
  viteServer = null;
1410
1410
  isShuttingDown = false;
1411
+ trackedSockets = /* @__PURE__ */ new Set();
1411
1412
  constructor(renderService, httpAdapterHost, viteConfig) {
1412
1413
  this.renderService = renderService;
1413
1414
  this.httpAdapterHost = httpAdapterHost;
@@ -1470,6 +1471,15 @@ var ViteInitializerService = class _ViteInitializerService {
1470
1471
  });
1471
1472
  app.use(viteProxy);
1472
1473
  this.logger.log(`\u2713 Vite HMR proxy configured (Vite dev server on port ${this.vitePort})`);
1474
+ const httpServer = httpAdapter.getHttpServer?.();
1475
+ if (httpServer && typeof httpServer.on === "function") {
1476
+ const track = /* @__PURE__ */ __name((socket) => {
1477
+ this.trackedSockets.add(socket);
1478
+ socket.once("close", () => this.trackedSockets.delete(socket));
1479
+ }, "track");
1480
+ httpServer.on("connection", track);
1481
+ httpServer.on("upgrade", (_req, socket) => track(socket));
1482
+ }
1473
1483
  } catch (error) {
1474
1484
  this.logger.warn(`Failed to setup Vite proxy: ${error.message}. Make sure http-proxy-middleware is installed.`);
1475
1485
  }
@@ -1542,6 +1552,10 @@ var ViteInitializerService = class _ViteInitializerService {
1542
1552
  if (httpServer && typeof httpServer.closeAllConnections === "function") {
1543
1553
  httpServer.closeAllConnections();
1544
1554
  }
1555
+ for (const socket of this.trackedSockets) {
1556
+ socket.destroy();
1557
+ }
1558
+ this.trackedSockets.clear();
1545
1559
  }
1546
1560
  };
1547
1561
  ViteInitializerService = _ts_decorate7([
package/dist/index.mjs CHANGED
@@ -1401,6 +1401,7 @@ var ViteInitializerService = class _ViteInitializerService {
1401
1401
  vitePort;
1402
1402
  viteServer = null;
1403
1403
  isShuttingDown = false;
1404
+ trackedSockets = /* @__PURE__ */ new Set();
1404
1405
  constructor(renderService, httpAdapterHost, viteConfig) {
1405
1406
  this.renderService = renderService;
1406
1407
  this.httpAdapterHost = httpAdapterHost;
@@ -1463,6 +1464,15 @@ var ViteInitializerService = class _ViteInitializerService {
1463
1464
  });
1464
1465
  app.use(viteProxy);
1465
1466
  this.logger.log(`\u2713 Vite HMR proxy configured (Vite dev server on port ${this.vitePort})`);
1467
+ const httpServer = httpAdapter.getHttpServer?.();
1468
+ if (httpServer && typeof httpServer.on === "function") {
1469
+ const track = /* @__PURE__ */ __name((socket) => {
1470
+ this.trackedSockets.add(socket);
1471
+ socket.once("close", () => this.trackedSockets.delete(socket));
1472
+ }, "track");
1473
+ httpServer.on("connection", track);
1474
+ httpServer.on("upgrade", (_req, socket) => track(socket));
1475
+ }
1466
1476
  } catch (error) {
1467
1477
  this.logger.warn(`Failed to setup Vite proxy: ${error.message}. Make sure http-proxy-middleware is installed.`);
1468
1478
  }
@@ -1535,6 +1545,10 @@ var ViteInitializerService = class _ViteInitializerService {
1535
1545
  if (httpServer && typeof httpServer.closeAllConnections === "function") {
1536
1546
  httpServer.closeAllConnections();
1537
1547
  }
1548
+ for (const socket of this.trackedSockets) {
1549
+ socket.destroy();
1550
+ }
1551
+ this.trackedSockets.clear();
1538
1552
  }
1539
1553
  };
1540
1554
  ViteInitializerService = _ts_decorate7([
@@ -1389,6 +1389,7 @@ var ViteInitializerService = class _ViteInitializerService {
1389
1389
  vitePort;
1390
1390
  viteServer = null;
1391
1391
  isShuttingDown = false;
1392
+ trackedSockets = /* @__PURE__ */ new Set();
1392
1393
  constructor(renderService, httpAdapterHost, viteConfig) {
1393
1394
  this.renderService = renderService;
1394
1395
  this.httpAdapterHost = httpAdapterHost;
@@ -1451,6 +1452,15 @@ var ViteInitializerService = class _ViteInitializerService {
1451
1452
  });
1452
1453
  app.use(viteProxy);
1453
1454
  this.logger.log(`\u2713 Vite HMR proxy configured (Vite dev server on port ${this.vitePort})`);
1455
+ const httpServer = httpAdapter.getHttpServer?.();
1456
+ if (httpServer && typeof httpServer.on === "function") {
1457
+ const track = /* @__PURE__ */ __name((socket) => {
1458
+ this.trackedSockets.add(socket);
1459
+ socket.once("close", () => this.trackedSockets.delete(socket));
1460
+ }, "track");
1461
+ httpServer.on("connection", track);
1462
+ httpServer.on("upgrade", (_req, socket) => track(socket));
1463
+ }
1454
1464
  } catch (error) {
1455
1465
  this.logger.warn(`Failed to setup Vite proxy: ${error.message}. Make sure http-proxy-middleware is installed.`);
1456
1466
  }
@@ -1523,6 +1533,10 @@ var ViteInitializerService = class _ViteInitializerService {
1523
1533
  if (httpServer && typeof httpServer.closeAllConnections === "function") {
1524
1534
  httpServer.closeAllConnections();
1525
1535
  }
1536
+ for (const socket of this.trackedSockets) {
1537
+ socket.destroy();
1538
+ }
1539
+ this.trackedSockets.clear();
1526
1540
  }
1527
1541
  };
1528
1542
  ViteInitializerService = _ts_decorate7([
@@ -1383,6 +1383,7 @@ var ViteInitializerService = class _ViteInitializerService {
1383
1383
  vitePort;
1384
1384
  viteServer = null;
1385
1385
  isShuttingDown = false;
1386
+ trackedSockets = /* @__PURE__ */ new Set();
1386
1387
  constructor(renderService, httpAdapterHost, viteConfig) {
1387
1388
  this.renderService = renderService;
1388
1389
  this.httpAdapterHost = httpAdapterHost;
@@ -1445,6 +1446,15 @@ var ViteInitializerService = class _ViteInitializerService {
1445
1446
  });
1446
1447
  app.use(viteProxy);
1447
1448
  this.logger.log(`\u2713 Vite HMR proxy configured (Vite dev server on port ${this.vitePort})`);
1449
+ const httpServer = httpAdapter.getHttpServer?.();
1450
+ if (httpServer && typeof httpServer.on === "function") {
1451
+ const track = /* @__PURE__ */ __name((socket) => {
1452
+ this.trackedSockets.add(socket);
1453
+ socket.once("close", () => this.trackedSockets.delete(socket));
1454
+ }, "track");
1455
+ httpServer.on("connection", track);
1456
+ httpServer.on("upgrade", (_req, socket) => track(socket));
1457
+ }
1448
1458
  } catch (error) {
1449
1459
  this.logger.warn(`Failed to setup Vite proxy: ${error.message}. Make sure http-proxy-middleware is installed.`);
1450
1460
  }
@@ -1517,6 +1527,10 @@ var ViteInitializerService = class _ViteInitializerService {
1517
1527
  if (httpServer && typeof httpServer.closeAllConnections === "function") {
1518
1528
  httpServer.closeAllConnections();
1519
1529
  }
1530
+ for (const socket of this.trackedSockets) {
1531
+ socket.destroy();
1532
+ }
1533
+ this.trackedSockets.clear();
1520
1534
  }
1521
1535
  };
1522
1536
  ViteInitializerService = _ts_decorate7([
@@ -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.21",
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",
@@ -139,39 +139,39 @@
139
139
  }
140
140
  },
141
141
  "dependencies": {
142
- "citty": "^0.2.1",
142
+ "citty": "^0.2.2",
143
143
  "consola": "^3.4.2",
144
- "devalue": "^5.6.4",
144
+ "devalue": "^5.8.1",
145
145
  "escape-html": "^1.0.3"
146
146
  },
147
147
  "devDependencies": {
148
- "@microsoft/api-extractor": "^7.57.7",
148
+ "@microsoft/api-extractor": "^7.58.7",
149
149
  "@nestjs/common": "^11.1.19",
150
150
  "@nestjs/core": "^11.1.19",
151
151
  "@nestjs/platform-express": "^11.1.19",
152
- "@nestjs/testing": "^11.1.17",
153
- "@playwright/test": "^1.58.2",
154
- "@swc/core": "^1.15.21",
152
+ "@nestjs/testing": "^11.1.21",
153
+ "@playwright/test": "^1.60.0",
154
+ "@swc/core": "^1.15.33",
155
155
  "@testing-library/jest-dom": "^6.9.1",
156
156
  "@testing-library/react": "^16.3.2",
157
157
  "@types/escape-html": "^1.0.4",
158
158
  "@types/express": "^5.0.6",
159
- "@types/node": "^25.5.0",
159
+ "@types/node": "^25.8.0",
160
160
  "@types/react": "^19.2.14",
161
161
  "@types/react-dom": "^19.2.3",
162
162
  "@types/supertest": "^7.2.0",
163
- "@vitejs/plugin-react": "^6.0.1",
164
- "@vitest/coverage-v8": "^4.1.2",
165
- "@vitest/ui": "^4.1.2",
166
- "happy-dom": "^20.8.9",
167
- "react": "^19.2.5",
168
- "react-dom": "^19.2.5",
163
+ "@vitejs/plugin-react": "^6.0.2",
164
+ "@vitest/coverage-v8": "^4.1.6",
165
+ "@vitest/ui": "^4.1.6",
166
+ "happy-dom": "^20.9.0",
167
+ "react": "^19.2.6",
168
+ "react-dom": "^19.2.6",
169
169
  "rxjs": "^7.8.2",
170
170
  "supertest": "^7.2.2",
171
171
  "tsup": "^8.5.1",
172
172
  "typescript": "^5.9.3",
173
- "vite": "^8.0.9",
174
- "vitest": "^4.1.2"
173
+ "vite": "^8.0.13",
174
+ "vitest": "^4.1.6"
175
175
  },
176
176
  "publishConfig": {
177
177
  "access": "public"
@@ -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)