@useavalon/avalon 0.1.11 → 0.1.13
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/README.md +54 -54
- package/mod.ts +302 -302
- package/package.json +49 -26
- package/src/build/integration-bundler-plugin.ts +116 -116
- package/src/build/integration-config.ts +168 -168
- package/src/build/integration-detection-plugin.ts +117 -117
- package/src/build/integration-resolver-plugin.ts +90 -90
- package/src/build/island-manifest.ts +269 -269
- package/src/build/island-types-generator.ts +476 -476
- package/src/build/mdx-island-transform.ts +464 -464
- package/src/build/mdx-plugin.ts +98 -98
- package/src/build/page-island-transform.ts +598 -598
- package/src/build/prop-extractors/index.ts +21 -21
- package/src/build/prop-extractors/lit.ts +140 -140
- package/src/build/prop-extractors/qwik.ts +16 -16
- package/src/build/prop-extractors/solid.ts +125 -125
- package/src/build/prop-extractors/svelte.ts +194 -194
- package/src/build/prop-extractors/vue.ts +111 -111
- package/src/build/sidecar-file-manager.ts +104 -104
- package/src/build/sidecar-renderer.ts +30 -30
- package/src/client/adapters/index.ts +21 -13
- package/src/client/components.ts +35 -35
- package/src/client/css-hmr-handler.ts +344 -344
- package/src/client/framework-adapter.ts +462 -462
- package/src/client/hmr-coordinator.ts +396 -396
- package/src/client/hmr-error-overlay.js +533 -533
- package/src/client/main.js +824 -816
- package/src/client/types/framework-runtime.d.ts +68 -68
- package/src/client/types/vite-hmr.d.ts +46 -46
- package/src/client/types/vite-virtual-modules.d.ts +70 -60
- package/src/components/Image.tsx +123 -123
- package/src/components/IslandErrorBoundary.tsx +145 -145
- package/src/components/LayoutDataErrorBoundary.tsx +141 -141
- package/src/components/LayoutErrorBoundary.tsx +127 -127
- package/src/components/PersistentIsland.tsx +52 -52
- package/src/components/StreamingErrorBoundary.tsx +233 -233
- package/src/components/StreamingLayout.tsx +538 -538
- package/src/core/components/component-analyzer.ts +192 -192
- package/src/core/components/component-detection.ts +508 -508
- package/src/core/components/enhanced-framework-detector.ts +500 -500
- package/src/core/components/framework-registry.ts +563 -563
- package/src/core/content/mdx-processor.ts +46 -46
- package/src/core/integrations/index.ts +19 -19
- package/src/core/integrations/loader.ts +125 -125
- package/src/core/integrations/registry.ts +175 -175
- package/src/core/islands/island-persistence.ts +325 -325
- package/src/core/islands/island-state-serializer.ts +258 -258
- package/src/core/islands/persistent-island-context.tsx +80 -80
- package/src/core/islands/use-persistent-state.ts +68 -68
- package/src/core/layout/enhanced-layout-resolver.ts +322 -322
- package/src/core/layout/layout-cache-manager.ts +485 -485
- package/src/core/layout/layout-composer.ts +357 -357
- package/src/core/layout/layout-data-loader.ts +516 -516
- package/src/core/layout/layout-discovery.ts +243 -243
- package/src/core/layout/layout-matcher.ts +299 -299
- package/src/core/layout/layout-types.ts +110 -110
- package/src/core/modules/framework-module-resolver.ts +273 -273
- package/src/islands/component-analysis.ts +213 -213
- package/src/islands/css-utils.ts +565 -565
- package/src/islands/discovery/index.ts +80 -80
- package/src/islands/discovery/registry.ts +340 -340
- package/src/islands/discovery/resolver.ts +477 -477
- package/src/islands/discovery/scanner.ts +386 -386
- package/src/islands/discovery/types.ts +117 -117
- package/src/islands/discovery/validator.ts +544 -544
- package/src/islands/discovery/watcher.ts +368 -368
- package/src/islands/framework-detection.ts +428 -428
- package/src/islands/integration-loader.ts +490 -490
- package/src/islands/island.tsx +565 -565
- package/src/islands/render-cache.ts +550 -550
- package/src/islands/types.ts +80 -80
- package/src/islands/universal-css-collector.ts +157 -157
- package/src/islands/universal-head-collector.ts +137 -137
- package/src/layout-system.d.ts +592 -592
- package/src/layout-system.ts +218 -218
- package/src/middleware/discovery.ts +268 -268
- package/src/middleware/executor.ts +315 -315
- package/src/middleware/index.ts +76 -76
- package/src/middleware/types.ts +99 -99
- package/src/nitro/build-config.ts +575 -575
- package/src/nitro/config.ts +483 -483
- package/src/nitro/error-handler.ts +636 -636
- package/src/nitro/index.ts +173 -173
- package/src/nitro/island-manifest.ts +584 -584
- package/src/nitro/middleware-adapter.ts +260 -260
- package/src/nitro/renderer.ts +1471 -1471
- package/src/nitro/route-discovery.ts +439 -439
- package/src/nitro/types.ts +321 -321
- package/src/render/collect-css.ts +198 -198
- package/src/render/error-pages.ts +79 -79
- package/src/render/isolated-ssr-renderer.ts +654 -654
- package/src/render/ssr.ts +1030 -1030
- package/src/schemas/api.ts +30 -30
- package/src/schemas/core.ts +64 -64
- package/src/schemas/index.ts +212 -212
- package/src/schemas/layout.ts +279 -279
- package/src/schemas/routing/index.ts +38 -38
- package/src/schemas/routing.ts +376 -376
- package/src/types/as-island.ts +20 -20
- package/src/types/image.d.ts +106 -106
- package/src/types/index.d.ts +22 -22
- package/src/types/island-jsx.d.ts +33 -33
- package/src/types/island-prop.d.ts +20 -20
- package/src/types/layout.ts +285 -285
- package/src/types/mdx.d.ts +6 -6
- package/src/types/routing.ts +555 -555
- package/src/types/types.ts +5 -5
- package/src/types/urlpattern.d.ts +49 -49
- package/src/types/vite-env.d.ts +11 -11
- package/src/utils/dev-logger.ts +299 -299
- package/src/utils/fs.ts +151 -151
- package/src/vite-plugin/auto-discover.ts +551 -551
- package/src/vite-plugin/config.ts +266 -266
- package/src/vite-plugin/errors.ts +127 -127
- package/src/vite-plugin/image-optimization.ts +156 -156
- package/src/vite-plugin/integration-activator.ts +126 -126
- package/src/vite-plugin/island-sidecar-plugin.ts +176 -176
- package/src/vite-plugin/module-discovery.ts +189 -189
- package/src/vite-plugin/nitro-integration.ts +1354 -1354
- package/src/vite-plugin/plugin.ts +403 -409
- package/src/vite-plugin/types.ts +327 -327
- package/src/vite-plugin/validation.ts +228 -228
- package/src/client/adapters/index.js +0 -12
- package/src/client/adapters/lit-adapter.js +0 -467
- package/src/client/adapters/lit-adapter.ts +0 -654
- package/src/client/adapters/preact-adapter.js +0 -223
- package/src/client/adapters/preact-adapter.ts +0 -331
- package/src/client/adapters/qwik-adapter.js +0 -259
- package/src/client/adapters/qwik-adapter.ts +0 -345
- package/src/client/adapters/react-adapter.js +0 -220
- package/src/client/adapters/react-adapter.ts +0 -353
- package/src/client/adapters/solid-adapter.js +0 -295
- package/src/client/adapters/solid-adapter.ts +0 -451
- package/src/client/adapters/svelte-adapter.js +0 -368
- package/src/client/adapters/svelte-adapter.ts +0 -524
- package/src/client/adapters/vue-adapter.js +0 -278
- package/src/client/adapters/vue-adapter.ts +0 -467
- package/src/client/components.js +0 -23
- package/src/client/css-hmr-handler.js +0 -263
- package/src/client/framework-adapter.js +0 -283
- package/src/client/hmr-coordinator.js +0 -274
|
@@ -1,243 +1,243 @@
|
|
|
1
|
-
import { join, resolve, relative } from 'node:path';
|
|
2
|
-
import { statSync } from 'node:fs';
|
|
3
|
-
|
|
4
|
-
import type {
|
|
5
|
-
ComponentType,
|
|
6
|
-
LayoutRoute,
|
|
7
|
-
LayoutHandler,
|
|
8
|
-
LayoutDiscoveryOptions,
|
|
9
|
-
LayoutContext,
|
|
10
|
-
LayoutData,
|
|
11
|
-
LayoutProps,
|
|
12
|
-
LayoutErrorInfo,
|
|
13
|
-
} from './layout-types.ts';
|
|
14
|
-
|
|
15
|
-
export type { LayoutDiscoveryOptions } from './layout-types.ts';
|
|
16
|
-
|
|
17
|
-
interface LayoutFileExport {
|
|
18
|
-
default: ComponentType<LayoutProps>;
|
|
19
|
-
layoutLoader?: (ctx: LayoutContext) => Promise<LayoutData>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Converts an absolute file path to a valid ESM import specifier.
|
|
24
|
-
* Windows absolute paths (C:\...) are converted to file:// URLs.
|
|
25
|
-
*/
|
|
26
|
-
function toImportSpecifier(filePath: string): string {
|
|
27
|
-
if (/^[A-Za-z]:[\\/]/.test(filePath)) {
|
|
28
|
-
return `file:///${filePath.replaceAll('\\', '/')}`;
|
|
29
|
-
}
|
|
30
|
-
return filePath;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Checks whether a file exists at the given path using statSync.
|
|
35
|
-
*/
|
|
36
|
-
function fileExists(filePath: string): boolean {
|
|
37
|
-
try {
|
|
38
|
-
statSync(filePath);
|
|
39
|
-
return true;
|
|
40
|
-
} catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Builds the path hierarchy for a route.
|
|
47
|
-
* For "/admin/users" returns ['', '/admin', '/admin/users'].
|
|
48
|
-
*/
|
|
49
|
-
function buildPathHierarchy(routePath: string): string[] {
|
|
50
|
-
const segments = routePath.split('/').filter(Boolean);
|
|
51
|
-
const paths: string[] = [''];
|
|
52
|
-
for (let i = 0; i < segments.length; i++) {
|
|
53
|
-
paths.push('/' + segments.slice(0, i + 1).join('/'));
|
|
54
|
-
}
|
|
55
|
-
return paths;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Simplified Layout Discovery System
|
|
60
|
-
*
|
|
61
|
-
* Looks for _layout.tsx files in the path hierarchy from root to the current route.
|
|
62
|
-
*
|
|
63
|
-
* For route /admin/users/123:
|
|
64
|
-
* - Check src/layouts/_layout.tsx (root layout)
|
|
65
|
-
* - Check src/layouts/admin/_layout.tsx
|
|
66
|
-
* - Check src/layouts/admin/users/_layout.tsx
|
|
67
|
-
*/
|
|
68
|
-
export class LayoutDiscovery {
|
|
69
|
-
private readonly layoutCache = new Map<string, LayoutHandler>();
|
|
70
|
-
private readonly routeCache = new Map<string, LayoutRoute[]>();
|
|
71
|
-
private readonly baseDirectory: string;
|
|
72
|
-
private readonly filePattern: string;
|
|
73
|
-
private readonly developmentMode: boolean;
|
|
74
|
-
|
|
75
|
-
constructor(options: LayoutDiscoveryOptions) {
|
|
76
|
-
this.baseDirectory = resolve(options.baseDirectory);
|
|
77
|
-
this.filePattern = options.filePattern || '_layout.tsx';
|
|
78
|
-
this.developmentMode = options.developmentMode || false;
|
|
79
|
-
|
|
80
|
-
if (this.developmentMode) {
|
|
81
|
-
console.log(`[LayoutDiscovery] baseDirectory=${this.baseDirectory}, filePattern=${this.filePattern}`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Discovers all layout files for a given route path by walking up the path hierarchy
|
|
87
|
-
*/
|
|
88
|
-
discoverLayouts(routePath: string): Promise<LayoutRoute[]> {
|
|
89
|
-
const cacheKey = `layouts-${routePath}`;
|
|
90
|
-
|
|
91
|
-
if (this.routeCache.has(cacheKey)) {
|
|
92
|
-
return Promise.resolve(this.routeCache.get(cacheKey)!);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const routes: LayoutRoute[] = [];
|
|
96
|
-
const pathsToCheck = buildPathHierarchy(routePath);
|
|
97
|
-
|
|
98
|
-
for (const pathToCheck of pathsToCheck) {
|
|
99
|
-
const fsPath = pathToCheck === '' ? this.baseDirectory : join(this.baseDirectory, pathToCheck);
|
|
100
|
-
const layoutFilePath = join(fsPath, this.filePattern);
|
|
101
|
-
|
|
102
|
-
if (fileExists(layoutFilePath)) {
|
|
103
|
-
const depth = pathToCheck === '' ? 0 : pathToCheck.split('/').filter(Boolean).length;
|
|
104
|
-
routes.push({
|
|
105
|
-
pattern: new URLPattern({ pathname: depth === 0 ? '*' : `${pathToCheck}/*` }),
|
|
106
|
-
layoutPath: layoutFilePath,
|
|
107
|
-
priority: depth * 10,
|
|
108
|
-
type: depth === 0 ? 'root' : 'nested',
|
|
109
|
-
depth,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
routes.sort((a, b) => a.priority - b.priority);
|
|
115
|
-
this.routeCache.set(cacheKey, routes);
|
|
116
|
-
return Promise.resolve(routes);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Builds a complete layout chain for a URL
|
|
121
|
-
*/
|
|
122
|
-
async buildLayoutChain(url: URL): Promise<LayoutHandler[]> {
|
|
123
|
-
const routes = await this.discoverLayouts(url.pathname);
|
|
124
|
-
const layoutChain: LayoutHandler[] = [];
|
|
125
|
-
|
|
126
|
-
for (const route of routes) {
|
|
127
|
-
try {
|
|
128
|
-
const handler = await this.loadLayout(route.layoutPath);
|
|
129
|
-
if (handler) {
|
|
130
|
-
layoutChain.push(handler);
|
|
131
|
-
}
|
|
132
|
-
} catch (error) {
|
|
133
|
-
if (this.developmentMode) {
|
|
134
|
-
console.warn(
|
|
135
|
-
`[Layout] Failed to load ${route.layoutPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return layoutChain;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Builds layout chain with data loading
|
|
146
|
-
*/
|
|
147
|
-
async buildLayoutChainWithData(
|
|
148
|
-
url: URL,
|
|
149
|
-
context: LayoutContext,
|
|
150
|
-
): Promise<{ handlers: LayoutHandler[]; data: LayoutData[]; errors: LayoutErrorInfo[] }> {
|
|
151
|
-
const handlers = await this.buildLayoutChain(url);
|
|
152
|
-
const data: LayoutData[] = [];
|
|
153
|
-
const errors: LayoutErrorInfo[] = [];
|
|
154
|
-
|
|
155
|
-
for (const handler of handlers) {
|
|
156
|
-
if (handler.loader) {
|
|
157
|
-
try {
|
|
158
|
-
const layoutData = await handler.loader(context);
|
|
159
|
-
data.push(layoutData);
|
|
160
|
-
} catch (error) {
|
|
161
|
-
if (this.developmentMode) {
|
|
162
|
-
console.warn(`[Layout] Data loader error for ${handler.path}:`, error);
|
|
163
|
-
}
|
|
164
|
-
errors.push({ layoutPath: handler.path, errorType: 'loader', timestamp: Date.now() });
|
|
165
|
-
data.push({});
|
|
166
|
-
}
|
|
167
|
-
} else {
|
|
168
|
-
data.push({});
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return { handlers, data, errors };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Loads layout handler from file
|
|
177
|
-
*/
|
|
178
|
-
private async loadLayout(filePath: string): Promise<LayoutHandler | null> {
|
|
179
|
-
if (this.layoutCache.has(filePath)) {
|
|
180
|
-
return this.layoutCache.get(filePath)!;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
const importPath = toImportSpecifier(filePath);
|
|
185
|
-
const layoutModule = (await import(/* @vite-ignore */ importPath)) as LayoutFileExport;
|
|
186
|
-
|
|
187
|
-
if (!layoutModule.default || typeof layoutModule.default !== 'function') {
|
|
188
|
-
if (this.developmentMode) {
|
|
189
|
-
console.warn(`[Layout] No default export in ${filePath}`);
|
|
190
|
-
}
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const relativePath = relative(this.baseDirectory, filePath);
|
|
195
|
-
const pathSegments = relativePath.split('/').filter(Boolean);
|
|
196
|
-
const priority = Math.max(0, (pathSegments.length - 1) * 10);
|
|
197
|
-
|
|
198
|
-
const handler: LayoutHandler = {
|
|
199
|
-
component: layoutModule.default,
|
|
200
|
-
loader: layoutModule.layoutLoader,
|
|
201
|
-
path: filePath,
|
|
202
|
-
priority,
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
this.layoutCache.set(filePath, handler);
|
|
206
|
-
return handler;
|
|
207
|
-
} catch (error) {
|
|
208
|
-
if (this.developmentMode) {
|
|
209
|
-
console.warn(`[Layout] Failed to load ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
210
|
-
}
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** Clears all caches */
|
|
216
|
-
clearCache(): void {
|
|
217
|
-
this.layoutCache.clear();
|
|
218
|
-
this.routeCache.clear();
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Clears cache for a specific layout file */
|
|
222
|
-
clearLayoutCache(filePath: string): void {
|
|
223
|
-
this.layoutCache.delete(filePath);
|
|
224
|
-
this.routeCache.clear();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** Gets cache statistics (for debugging) */
|
|
228
|
-
getCacheStats(): { layoutCount: number; routeCacheCount: number } {
|
|
229
|
-
return {
|
|
230
|
-
layoutCount: this.layoutCache.size,
|
|
231
|
-
routeCacheCount: this.routeCache.size,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** Gets the current discovery options */
|
|
236
|
-
getOptions(): LayoutDiscoveryOptions {
|
|
237
|
-
return {
|
|
238
|
-
baseDirectory: this.baseDirectory,
|
|
239
|
-
filePattern: this.filePattern,
|
|
240
|
-
developmentMode: this.developmentMode,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
}
|
|
1
|
+
import { join, resolve, relative } from 'node:path';
|
|
2
|
+
import { statSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
ComponentType,
|
|
6
|
+
LayoutRoute,
|
|
7
|
+
LayoutHandler,
|
|
8
|
+
LayoutDiscoveryOptions,
|
|
9
|
+
LayoutContext,
|
|
10
|
+
LayoutData,
|
|
11
|
+
LayoutProps,
|
|
12
|
+
LayoutErrorInfo,
|
|
13
|
+
} from './layout-types.ts';
|
|
14
|
+
|
|
15
|
+
export type { LayoutDiscoveryOptions } from './layout-types.ts';
|
|
16
|
+
|
|
17
|
+
interface LayoutFileExport {
|
|
18
|
+
default: ComponentType<LayoutProps>;
|
|
19
|
+
layoutLoader?: (ctx: LayoutContext) => Promise<LayoutData>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Converts an absolute file path to a valid ESM import specifier.
|
|
24
|
+
* Windows absolute paths (C:\...) are converted to file:// URLs.
|
|
25
|
+
*/
|
|
26
|
+
function toImportSpecifier(filePath: string): string {
|
|
27
|
+
if (/^[A-Za-z]:[\\/]/.test(filePath)) {
|
|
28
|
+
return `file:///${filePath.replaceAll('\\', '/')}`;
|
|
29
|
+
}
|
|
30
|
+
return filePath;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Checks whether a file exists at the given path using statSync.
|
|
35
|
+
*/
|
|
36
|
+
function fileExists(filePath: string): boolean {
|
|
37
|
+
try {
|
|
38
|
+
statSync(filePath);
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Builds the path hierarchy for a route.
|
|
47
|
+
* For "/admin/users" returns ['', '/admin', '/admin/users'].
|
|
48
|
+
*/
|
|
49
|
+
function buildPathHierarchy(routePath: string): string[] {
|
|
50
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
51
|
+
const paths: string[] = [''];
|
|
52
|
+
for (let i = 0; i < segments.length; i++) {
|
|
53
|
+
paths.push('/' + segments.slice(0, i + 1).join('/'));
|
|
54
|
+
}
|
|
55
|
+
return paths;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Simplified Layout Discovery System
|
|
60
|
+
*
|
|
61
|
+
* Looks for _layout.tsx files in the path hierarchy from root to the current route.
|
|
62
|
+
*
|
|
63
|
+
* For route /admin/users/123:
|
|
64
|
+
* - Check src/layouts/_layout.tsx (root layout)
|
|
65
|
+
* - Check src/layouts/admin/_layout.tsx
|
|
66
|
+
* - Check src/layouts/admin/users/_layout.tsx
|
|
67
|
+
*/
|
|
68
|
+
export class LayoutDiscovery {
|
|
69
|
+
private readonly layoutCache = new Map<string, LayoutHandler>();
|
|
70
|
+
private readonly routeCache = new Map<string, LayoutRoute[]>();
|
|
71
|
+
private readonly baseDirectory: string;
|
|
72
|
+
private readonly filePattern: string;
|
|
73
|
+
private readonly developmentMode: boolean;
|
|
74
|
+
|
|
75
|
+
constructor(options: LayoutDiscoveryOptions) {
|
|
76
|
+
this.baseDirectory = resolve(options.baseDirectory);
|
|
77
|
+
this.filePattern = options.filePattern || '_layout.tsx';
|
|
78
|
+
this.developmentMode = options.developmentMode || false;
|
|
79
|
+
|
|
80
|
+
if (this.developmentMode) {
|
|
81
|
+
console.log(`[LayoutDiscovery] baseDirectory=${this.baseDirectory}, filePattern=${this.filePattern}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Discovers all layout files for a given route path by walking up the path hierarchy
|
|
87
|
+
*/
|
|
88
|
+
discoverLayouts(routePath: string): Promise<LayoutRoute[]> {
|
|
89
|
+
const cacheKey = `layouts-${routePath}`;
|
|
90
|
+
|
|
91
|
+
if (this.routeCache.has(cacheKey)) {
|
|
92
|
+
return Promise.resolve(this.routeCache.get(cacheKey)!);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const routes: LayoutRoute[] = [];
|
|
96
|
+
const pathsToCheck = buildPathHierarchy(routePath);
|
|
97
|
+
|
|
98
|
+
for (const pathToCheck of pathsToCheck) {
|
|
99
|
+
const fsPath = pathToCheck === '' ? this.baseDirectory : join(this.baseDirectory, pathToCheck);
|
|
100
|
+
const layoutFilePath = join(fsPath, this.filePattern);
|
|
101
|
+
|
|
102
|
+
if (fileExists(layoutFilePath)) {
|
|
103
|
+
const depth = pathToCheck === '' ? 0 : pathToCheck.split('/').filter(Boolean).length;
|
|
104
|
+
routes.push({
|
|
105
|
+
pattern: new URLPattern({ pathname: depth === 0 ? '*' : `${pathToCheck}/*` }),
|
|
106
|
+
layoutPath: layoutFilePath,
|
|
107
|
+
priority: depth * 10,
|
|
108
|
+
type: depth === 0 ? 'root' : 'nested',
|
|
109
|
+
depth,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
routes.sort((a, b) => a.priority - b.priority);
|
|
115
|
+
this.routeCache.set(cacheKey, routes);
|
|
116
|
+
return Promise.resolve(routes);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Builds a complete layout chain for a URL
|
|
121
|
+
*/
|
|
122
|
+
async buildLayoutChain(url: URL): Promise<LayoutHandler[]> {
|
|
123
|
+
const routes = await this.discoverLayouts(url.pathname);
|
|
124
|
+
const layoutChain: LayoutHandler[] = [];
|
|
125
|
+
|
|
126
|
+
for (const route of routes) {
|
|
127
|
+
try {
|
|
128
|
+
const handler = await this.loadLayout(route.layoutPath);
|
|
129
|
+
if (handler) {
|
|
130
|
+
layoutChain.push(handler);
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (this.developmentMode) {
|
|
134
|
+
console.warn(
|
|
135
|
+
`[Layout] Failed to load ${route.layoutPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return layoutChain;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Builds layout chain with data loading
|
|
146
|
+
*/
|
|
147
|
+
async buildLayoutChainWithData(
|
|
148
|
+
url: URL,
|
|
149
|
+
context: LayoutContext,
|
|
150
|
+
): Promise<{ handlers: LayoutHandler[]; data: LayoutData[]; errors: LayoutErrorInfo[] }> {
|
|
151
|
+
const handlers = await this.buildLayoutChain(url);
|
|
152
|
+
const data: LayoutData[] = [];
|
|
153
|
+
const errors: LayoutErrorInfo[] = [];
|
|
154
|
+
|
|
155
|
+
for (const handler of handlers) {
|
|
156
|
+
if (handler.loader) {
|
|
157
|
+
try {
|
|
158
|
+
const layoutData = await handler.loader(context);
|
|
159
|
+
data.push(layoutData);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (this.developmentMode) {
|
|
162
|
+
console.warn(`[Layout] Data loader error for ${handler.path}:`, error);
|
|
163
|
+
}
|
|
164
|
+
errors.push({ layoutPath: handler.path, errorType: 'loader', timestamp: Date.now() });
|
|
165
|
+
data.push({});
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
data.push({});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { handlers, data, errors };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Loads layout handler from file
|
|
177
|
+
*/
|
|
178
|
+
private async loadLayout(filePath: string): Promise<LayoutHandler | null> {
|
|
179
|
+
if (this.layoutCache.has(filePath)) {
|
|
180
|
+
return this.layoutCache.get(filePath)!;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const importPath = toImportSpecifier(filePath);
|
|
185
|
+
const layoutModule = (await import(/* @vite-ignore */ importPath)) as LayoutFileExport;
|
|
186
|
+
|
|
187
|
+
if (!layoutModule.default || typeof layoutModule.default !== 'function') {
|
|
188
|
+
if (this.developmentMode) {
|
|
189
|
+
console.warn(`[Layout] No default export in ${filePath}`);
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const relativePath = relative(this.baseDirectory, filePath);
|
|
195
|
+
const pathSegments = relativePath.split('/').filter(Boolean);
|
|
196
|
+
const priority = Math.max(0, (pathSegments.length - 1) * 10);
|
|
197
|
+
|
|
198
|
+
const handler: LayoutHandler = {
|
|
199
|
+
component: layoutModule.default,
|
|
200
|
+
loader: layoutModule.layoutLoader,
|
|
201
|
+
path: filePath,
|
|
202
|
+
priority,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
this.layoutCache.set(filePath, handler);
|
|
206
|
+
return handler;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (this.developmentMode) {
|
|
209
|
+
console.warn(`[Layout] Failed to load ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Clears all caches */
|
|
216
|
+
clearCache(): void {
|
|
217
|
+
this.layoutCache.clear();
|
|
218
|
+
this.routeCache.clear();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Clears cache for a specific layout file */
|
|
222
|
+
clearLayoutCache(filePath: string): void {
|
|
223
|
+
this.layoutCache.delete(filePath);
|
|
224
|
+
this.routeCache.clear();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Gets cache statistics (for debugging) */
|
|
228
|
+
getCacheStats(): { layoutCount: number; routeCacheCount: number } {
|
|
229
|
+
return {
|
|
230
|
+
layoutCount: this.layoutCache.size,
|
|
231
|
+
routeCacheCount: this.routeCache.size,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Gets the current discovery options */
|
|
236
|
+
getOptions(): LayoutDiscoveryOptions {
|
|
237
|
+
return {
|
|
238
|
+
baseDirectory: this.baseDirectory,
|
|
239
|
+
filePattern: this.filePattern,
|
|
240
|
+
developmentMode: this.developmentMode,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|