@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 +69 -2
- package/dist/client.d.ts +69 -2
- package/dist/client.js +72 -37
- package/dist/client.mjs +71 -38
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/templates/entry-client.tsx +13 -65
- package/dist/{use-page-context-Bo3Z0m-_.d.ts → use-page-context-CUV31oda.d.ts} +1 -1
- package/dist/{use-page-context-DXjw-66u.d.mts → use-page-context-CmxWHIK3.d.mts} +1 -1
- package/package.json +1 -1
- package/src/templates/entry-client.tsx +13 -65
package/dist/client.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 {
|
|
4
|
-
export {
|
|
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 {
|
|
4
|
-
export {
|
|
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
|
-
//
|
|
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(['
|
|
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
|
|
40
|
-
|
|
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
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
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 {
|
|
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 {
|
|
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
|
@@ -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
|
-
//
|
|
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(['
|
|
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
|
|
40
|
-
|
|
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
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
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)
|