@useavalon/avalon 0.1.12 → 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/mod.ts +302 -0
- package/package.json +9 -17
- package/src/build/integration-bundler-plugin.ts +116 -0
- package/src/build/integration-config.ts +168 -0
- package/src/build/integration-detection-plugin.ts +117 -0
- package/src/build/integration-resolver-plugin.ts +90 -0
- package/src/build/island-manifest.ts +269 -0
- package/src/build/island-types-generator.ts +476 -0
- package/src/build/mdx-island-transform.ts +464 -0
- package/src/build/mdx-plugin.ts +98 -0
- package/src/build/page-island-transform.ts +598 -0
- package/src/build/prop-extractors/index.ts +21 -0
- package/src/build/prop-extractors/lit.ts +140 -0
- package/src/build/prop-extractors/qwik.ts +16 -0
- package/src/build/prop-extractors/solid.ts +125 -0
- package/src/build/prop-extractors/svelte.ts +194 -0
- package/src/build/prop-extractors/vue.ts +111 -0
- package/src/build/sidecar-file-manager.ts +104 -0
- package/src/build/sidecar-renderer.ts +30 -0
- package/src/client/adapters/index.ts +21 -0
- package/src/client/components.ts +35 -0
- package/src/client/css-hmr-handler.ts +344 -0
- package/src/client/framework-adapter.ts +462 -0
- package/src/client/hmr-coordinator.ts +396 -0
- package/src/client/hmr-error-overlay.js +533 -0
- package/src/client/main.js +824 -0
- package/src/components/Image.tsx +123 -0
- package/src/components/IslandErrorBoundary.tsx +145 -0
- package/src/components/LayoutDataErrorBoundary.tsx +141 -0
- package/src/components/LayoutErrorBoundary.tsx +127 -0
- package/src/components/PersistentIsland.tsx +52 -0
- package/src/components/StreamingErrorBoundary.tsx +233 -0
- package/src/components/StreamingLayout.tsx +538 -0
- package/src/core/components/component-analyzer.ts +192 -0
- package/src/core/components/component-detection.ts +508 -0
- package/src/core/components/enhanced-framework-detector.ts +500 -0
- package/src/core/components/framework-registry.ts +563 -0
- package/src/core/content/mdx-processor.ts +46 -0
- package/src/core/integrations/index.ts +19 -0
- package/src/core/integrations/loader.ts +125 -0
- package/src/core/integrations/registry.ts +175 -0
- package/src/core/islands/island-persistence.ts +325 -0
- package/src/core/islands/island-state-serializer.ts +258 -0
- package/src/core/islands/persistent-island-context.tsx +80 -0
- package/src/core/islands/use-persistent-state.ts +68 -0
- package/src/core/layout/enhanced-layout-resolver.ts +322 -0
- package/src/core/layout/layout-cache-manager.ts +485 -0
- package/src/core/layout/layout-composer.ts +357 -0
- package/src/core/layout/layout-data-loader.ts +516 -0
- package/src/core/layout/layout-discovery.ts +243 -0
- package/src/core/layout/layout-matcher.ts +299 -0
- package/src/core/layout/layout-types.ts +110 -0
- package/src/core/modules/framework-module-resolver.ts +273 -0
- package/src/islands/component-analysis.ts +213 -0
- package/src/islands/css-utils.ts +565 -0
- package/src/islands/discovery/index.ts +80 -0
- package/src/islands/discovery/registry.ts +340 -0
- package/src/islands/discovery/resolver.ts +477 -0
- package/src/islands/discovery/scanner.ts +386 -0
- package/src/islands/discovery/types.ts +117 -0
- package/src/islands/discovery/validator.ts +544 -0
- package/src/islands/discovery/watcher.ts +368 -0
- package/src/islands/framework-detection.ts +428 -0
- package/src/islands/integration-loader.ts +490 -0
- package/src/islands/island.tsx +565 -0
- package/src/islands/render-cache.ts +550 -0
- package/src/islands/types.ts +80 -0
- package/src/islands/universal-css-collector.ts +157 -0
- package/src/islands/universal-head-collector.ts +137 -0
- package/src/layout-system.ts +218 -0
- package/src/middleware/discovery.ts +268 -0
- package/src/middleware/executor.ts +315 -0
- package/src/middleware/index.ts +76 -0
- package/src/middleware/types.ts +99 -0
- package/src/nitro/build-config.ts +576 -0
- package/src/nitro/config.ts +483 -0
- package/src/nitro/error-handler.ts +636 -0
- package/src/nitro/index.ts +173 -0
- package/src/nitro/island-manifest.ts +584 -0
- package/src/nitro/middleware-adapter.ts +260 -0
- package/src/nitro/renderer.ts +1471 -0
- package/src/nitro/route-discovery.ts +439 -0
- package/src/nitro/types.ts +321 -0
- package/src/render/collect-css.ts +198 -0
- package/src/render/error-pages.ts +79 -0
- package/src/render/isolated-ssr-renderer.ts +654 -0
- package/src/render/ssr.ts +1030 -0
- package/src/schemas/api.ts +30 -0
- package/src/schemas/core.ts +64 -0
- package/src/schemas/index.ts +212 -0
- package/src/schemas/layout.ts +279 -0
- package/src/schemas/routing/index.ts +38 -0
- package/src/schemas/routing.ts +376 -0
- package/src/types/as-island.ts +20 -0
- package/src/types/layout.ts +285 -0
- package/src/types/routing.ts +555 -0
- package/src/types/types.ts +5 -0
- package/src/utils/dev-logger.ts +299 -0
- package/src/utils/fs.ts +151 -0
- package/src/vite-plugin/auto-discover.ts +551 -0
- package/src/vite-plugin/config.ts +266 -0
- package/src/vite-plugin/errors.ts +127 -0
- package/src/vite-plugin/image-optimization.ts +156 -0
- package/src/vite-plugin/integration-activator.ts +126 -0
- package/src/vite-plugin/island-sidecar-plugin.ts +176 -0
- package/src/vite-plugin/module-discovery.ts +189 -0
- package/src/vite-plugin/nitro-integration.ts +1354 -0
- package/src/vite-plugin/plugin.ts +403 -0
- package/src/vite-plugin/types.ts +327 -0
- package/src/vite-plugin/validation.ts +228 -0
- package/dist/mod.js +0 -1
- package/dist/src/build/integration-bundler-plugin.js +0 -1
- package/dist/src/build/integration-config.js +0 -1
- package/dist/src/build/integration-detection-plugin.js +0 -1
- package/dist/src/build/integration-resolver-plugin.js +0 -1
- package/dist/src/build/island-manifest.js +0 -1
- package/dist/src/build/island-types-generator.js +0 -5
- package/dist/src/build/mdx-island-transform.js +0 -2
- package/dist/src/build/mdx-plugin.js +0 -1
- package/dist/src/build/page-island-transform.js +0 -3
- package/dist/src/build/prop-extractors/index.js +0 -1
- package/dist/src/build/prop-extractors/lit.js +0 -1
- package/dist/src/build/prop-extractors/qwik.js +0 -1
- package/dist/src/build/prop-extractors/solid.js +0 -1
- package/dist/src/build/prop-extractors/svelte.js +0 -1
- package/dist/src/build/prop-extractors/vue.js +0 -1
- package/dist/src/build/sidecar-file-manager.js +0 -1
- package/dist/src/build/sidecar-renderer.js +0 -6
- package/dist/src/client/adapters/index.js +0 -1
- package/dist/src/client/components.js +0 -1
- package/dist/src/client/css-hmr-handler.js +0 -1
- package/dist/src/client/framework-adapter.js +0 -13
- package/dist/src/client/hmr-coordinator.js +0 -1
- package/dist/src/client/hmr-error-overlay.js +0 -214
- package/dist/src/client/main.js +0 -39
- package/dist/src/components/Image.js +0 -1
- package/dist/src/components/IslandErrorBoundary.js +0 -1
- package/dist/src/components/LayoutDataErrorBoundary.js +0 -1
- package/dist/src/components/LayoutErrorBoundary.js +0 -1
- package/dist/src/components/PersistentIsland.js +0 -1
- package/dist/src/components/StreamingErrorBoundary.js +0 -1
- package/dist/src/components/StreamingLayout.js +0 -29
- package/dist/src/core/components/component-analyzer.js +0 -1
- package/dist/src/core/components/component-detection.js +0 -5
- package/dist/src/core/components/enhanced-framework-detector.js +0 -1
- package/dist/src/core/components/framework-registry.js +0 -1
- package/dist/src/core/content/mdx-processor.js +0 -1
- package/dist/src/core/integrations/index.js +0 -1
- package/dist/src/core/integrations/loader.js +0 -1
- package/dist/src/core/integrations/registry.js +0 -1
- package/dist/src/core/islands/island-persistence.js +0 -1
- package/dist/src/core/islands/island-state-serializer.js +0 -1
- package/dist/src/core/islands/persistent-island-context.js +0 -1
- package/dist/src/core/islands/use-persistent-state.js +0 -1
- package/dist/src/core/layout/enhanced-layout-resolver.js +0 -1
- package/dist/src/core/layout/layout-cache-manager.js +0 -1
- package/dist/src/core/layout/layout-composer.js +0 -1
- package/dist/src/core/layout/layout-data-loader.js +0 -1
- package/dist/src/core/layout/layout-discovery.js +0 -1
- package/dist/src/core/layout/layout-matcher.js +0 -1
- package/dist/src/core/layout/layout-types.js +0 -1
- package/dist/src/core/modules/framework-module-resolver.js +0 -1
- package/dist/src/islands/component-analysis.js +0 -1
- package/dist/src/islands/css-utils.js +0 -17
- package/dist/src/islands/discovery/index.js +0 -1
- package/dist/src/islands/discovery/registry.js +0 -1
- package/dist/src/islands/discovery/resolver.js +0 -2
- package/dist/src/islands/discovery/scanner.js +0 -1
- package/dist/src/islands/discovery/types.js +0 -1
- package/dist/src/islands/discovery/validator.js +0 -18
- package/dist/src/islands/discovery/watcher.js +0 -1
- package/dist/src/islands/framework-detection.js +0 -1
- package/dist/src/islands/integration-loader.js +0 -1
- package/dist/src/islands/island.js +0 -1
- package/dist/src/islands/render-cache.js +0 -1
- package/dist/src/islands/types.js +0 -1
- package/dist/src/islands/universal-css-collector.js +0 -5
- package/dist/src/islands/universal-head-collector.js +0 -2
- package/dist/src/layout-system.js +0 -1
- package/dist/src/middleware/discovery.js +0 -1
- package/dist/src/middleware/executor.js +0 -1
- package/dist/src/middleware/index.js +0 -1
- package/dist/src/middleware/types.js +0 -1
- package/dist/src/nitro/build-config.js +0 -1
- package/dist/src/nitro/config.js +0 -1
- package/dist/src/nitro/error-handler.js +0 -198
- package/dist/src/nitro/index.js +0 -1
- package/dist/src/nitro/island-manifest.js +0 -2
- package/dist/src/nitro/middleware-adapter.js +0 -1
- package/dist/src/nitro/renderer.js +0 -183
- package/dist/src/nitro/route-discovery.js +0 -1
- package/dist/src/nitro/types.js +0 -1
- package/dist/src/render/collect-css.js +0 -3
- package/dist/src/render/error-pages.js +0 -48
- package/dist/src/render/isolated-ssr-renderer.js +0 -1
- package/dist/src/render/ssr.js +0 -90
- package/dist/src/schemas/api.js +0 -1
- package/dist/src/schemas/core.js +0 -1
- package/dist/src/schemas/index.js +0 -1
- package/dist/src/schemas/layout.js +0 -1
- package/dist/src/schemas/routing/index.js +0 -1
- package/dist/src/schemas/routing.js +0 -1
- package/dist/src/types/as-island.js +0 -1
- package/dist/src/types/layout.js +0 -1
- package/dist/src/types/routing.js +0 -1
- package/dist/src/types/types.js +0 -1
- package/dist/src/utils/dev-logger.js +0 -12
- package/dist/src/utils/fs.js +0 -1
- package/dist/src/vite-plugin/auto-discover.js +0 -1
- package/dist/src/vite-plugin/config.js +0 -1
- package/dist/src/vite-plugin/errors.js +0 -1
- package/dist/src/vite-plugin/image-optimization.js +0 -45
- package/dist/src/vite-plugin/integration-activator.js +0 -1
- package/dist/src/vite-plugin/island-sidecar-plugin.js +0 -1
- package/dist/src/vite-plugin/module-discovery.js +0 -1
- package/dist/src/vite-plugin/nitro-integration.js +0 -42
- package/dist/src/vite-plugin/plugin.js +0 -1
- package/dist/src/vite-plugin/types.js +0 -1
- package/dist/src/vite-plugin/validation.js +0 -2
- /package/{dist/src → src}/client/types/framework-runtime.d.ts +0 -0
- /package/{dist/src → src}/client/types/vite-hmr.d.ts +0 -0
- /package/{dist/src → src}/client/types/vite-virtual-modules.d.ts +0 -0
- /package/{dist/src → src}/layout-system.d.ts +0 -0
- /package/{dist/src → src}/types/image.d.ts +0 -0
- /package/{dist/src → src}/types/index.d.ts +0 -0
- /package/{dist/src → src}/types/island-jsx.d.ts +0 -0
- /package/{dist/src → src}/types/island-prop.d.ts +0 -0
- /package/{dist/src → src}/types/mdx.d.ts +0 -0
- /package/{dist/src → src}/types/urlpattern.d.ts +0 -0
- /package/{dist/src → src}/types/vite-env.d.ts +0 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type { LayoutRule, RouteInfo } from './layout-types.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in layout rules for common scenarios
|
|
5
|
+
*/
|
|
6
|
+
export class BuiltInLayoutRules {
|
|
7
|
+
/**
|
|
8
|
+
* Rule to skip HTML layouts for API routes
|
|
9
|
+
* Requirements: 4.1
|
|
10
|
+
*/
|
|
11
|
+
static readonly API_ROUTES_SKIP_LAYOUTS: LayoutRule = {
|
|
12
|
+
matches: (route: RouteInfo): boolean => {
|
|
13
|
+
return route.path.startsWith('/api/');
|
|
14
|
+
},
|
|
15
|
+
apply: false,
|
|
16
|
+
priority: 100,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Rule to apply mobile-specific layouts for mobile user agents
|
|
21
|
+
* Requirements: 4.2
|
|
22
|
+
*/
|
|
23
|
+
static readonly MOBILE_LAYOUT_DETECTION: LayoutRule = {
|
|
24
|
+
matches: (route: RouteInfo, layoutPath?: string): boolean => {
|
|
25
|
+
const userAgent = route.headers.get('user-agent')?.toLowerCase() || '';
|
|
26
|
+
const isMobile = /mobile|android|iphone|ipad|phone|tablet/i.test(userAgent);
|
|
27
|
+
const isMobileLayout = layoutPath?.includes('/mobile/') ?? false;
|
|
28
|
+
// Skip mobile layouts for non-mobile user agents; skip non-mobile layouts for mobile users
|
|
29
|
+
if (isMobileLayout) return !isMobile;
|
|
30
|
+
return isMobile;
|
|
31
|
+
},
|
|
32
|
+
apply: false,
|
|
33
|
+
priority: 50,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Rule to skip layouts based on specific headers
|
|
38
|
+
* Requirements: 4.3
|
|
39
|
+
*/
|
|
40
|
+
static readonly HEADER_BASED_SKIP: LayoutRule = {
|
|
41
|
+
matches: (route: RouteInfo): boolean => {
|
|
42
|
+
const skipLayout = route.headers.get('x-skip-layout');
|
|
43
|
+
return skipLayout === 'true' || skipLayout === '1';
|
|
44
|
+
},
|
|
45
|
+
apply: false,
|
|
46
|
+
priority: 90,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Rule to apply admin layouts only for admin routes
|
|
51
|
+
* Requirements: 4.3
|
|
52
|
+
*/
|
|
53
|
+
static readonly ADMIN_LAYOUT_RESTRICTION: LayoutRule = {
|
|
54
|
+
matches: (route: RouteInfo, layoutPath?: string): boolean => {
|
|
55
|
+
// Only restrict admin layouts — if no layoutPath or not an admin layout, don't match
|
|
56
|
+
if (!layoutPath?.includes('/admin/')) return false;
|
|
57
|
+
// Admin layout should only apply to admin routes
|
|
58
|
+
return !route.path.startsWith('/admin/');
|
|
59
|
+
},
|
|
60
|
+
apply: false,
|
|
61
|
+
priority: 60,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
static getAllRules(): LayoutRule[] {
|
|
65
|
+
return [
|
|
66
|
+
this.API_ROUTES_SKIP_LAYOUTS,
|
|
67
|
+
this.MOBILE_LAYOUT_DETECTION,
|
|
68
|
+
this.HEADER_BASED_SKIP,
|
|
69
|
+
this.ADMIN_LAYOUT_RESTRICTION,
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Layout matcher class that handles conditional layout rendering based on rules
|
|
76
|
+
* Requirements: 4.1, 4.2, 4.3, 4.4, 4.5
|
|
77
|
+
*/
|
|
78
|
+
export class LayoutMatcher {
|
|
79
|
+
private rules: LayoutRule[] = [];
|
|
80
|
+
private readonly developmentMode: boolean;
|
|
81
|
+
|
|
82
|
+
constructor(options: { developmentMode?: boolean } = {}) {
|
|
83
|
+
this.developmentMode = options.developmentMode || false;
|
|
84
|
+
this.addBuiltInRules();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Add a new layout rule
|
|
89
|
+
* Requirements: 4.3
|
|
90
|
+
*/
|
|
91
|
+
addRule(rule: LayoutRule): void {
|
|
92
|
+
if (!rule.matches || typeof rule.matches !== 'function') {
|
|
93
|
+
throw new Error('Layout rule must have a valid matches function');
|
|
94
|
+
}
|
|
95
|
+
if (typeof rule.apply !== 'boolean') {
|
|
96
|
+
throw new TypeError('Layout rule must have a boolean apply property');
|
|
97
|
+
}
|
|
98
|
+
if (typeof rule.priority !== 'number') {
|
|
99
|
+
throw new TypeError('Layout rule must have a numeric priority');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.rules.push(rule);
|
|
103
|
+
this.sortRulesByPriority();
|
|
104
|
+
|
|
105
|
+
if (this.developmentMode) {
|
|
106
|
+
console.log(`[LayoutMatcher] Added rule with priority ${rule.priority}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Remove a layout rule
|
|
112
|
+
* Requirements: 4.3
|
|
113
|
+
*/
|
|
114
|
+
removeRule(rule: LayoutRule): void {
|
|
115
|
+
const index = this.rules.indexOf(rule);
|
|
116
|
+
if (index > -1) {
|
|
117
|
+
this.rules.splice(index, 1);
|
|
118
|
+
if (this.developmentMode) {
|
|
119
|
+
console.log(`[LayoutMatcher] Removed rule with priority ${rule.priority}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Check if a layout should be applied based on all rules
|
|
126
|
+
* Requirements: 4.1, 4.2, 4.3, 4.4, 4.5
|
|
127
|
+
*/
|
|
128
|
+
shouldApplyLayout(layoutPath: string, route: RouteInfo): boolean {
|
|
129
|
+
try {
|
|
130
|
+
const matchingRules = this.getMatchingRules(route, layoutPath);
|
|
131
|
+
|
|
132
|
+
if (matchingRules.length === 0) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result = this.resolveRuleConflicts(matchingRules);
|
|
137
|
+
|
|
138
|
+
if (this.developmentMode) {
|
|
139
|
+
console.log(
|
|
140
|
+
`[LayoutMatcher] Layout ${layoutPath} for route ${route.path}: ${result ? 'APPLY' : 'SKIP'} ` +
|
|
141
|
+
`(${matchingRules.length} rules matched)`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return result;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (this.developmentMode) {
|
|
148
|
+
console.warn(
|
|
149
|
+
`[LayoutMatcher] Error evaluating rules for layout ${layoutPath}: ${
|
|
150
|
+
error instanceof Error ? error.message : String(error)
|
|
151
|
+
}`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getRules(): LayoutRule[] {
|
|
159
|
+
return [...this.rules];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
clearRules(): void {
|
|
163
|
+
this.rules = [];
|
|
164
|
+
if (this.developmentMode) {
|
|
165
|
+
console.log('[LayoutMatcher] Cleared all rules');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private addBuiltInRules(): void {
|
|
170
|
+
for (const rule of BuiltInLayoutRules.getAllRules()) {
|
|
171
|
+
this.rules.push(rule);
|
|
172
|
+
}
|
|
173
|
+
this.sortRulesByPriority();
|
|
174
|
+
if (this.developmentMode) {
|
|
175
|
+
console.log(`[LayoutMatcher] Added ${this.rules.length} built-in rules`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private sortRulesByPriority(): void {
|
|
180
|
+
this.rules.sort((a, b) => b.priority - a.priority);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private getMatchingRules(route: RouteInfo, layoutPath?: string): LayoutRule[] {
|
|
184
|
+
const matchingRules: LayoutRule[] = [];
|
|
185
|
+
for (const rule of this.rules) {
|
|
186
|
+
try {
|
|
187
|
+
if (rule.matches(route, layoutPath)) {
|
|
188
|
+
matchingRules.push(rule);
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (this.developmentMode) {
|
|
192
|
+
console.warn(
|
|
193
|
+
`[LayoutMatcher] Error in rule evaluation: ${error instanceof Error ? error.message : String(error)}`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return matchingRules;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private resolveRuleConflicts(matchingRules: LayoutRule[]): boolean {
|
|
202
|
+
if (matchingRules.length === 0) return true;
|
|
203
|
+
if (matchingRules.length === 1) return matchingRules[0].apply;
|
|
204
|
+
|
|
205
|
+
const rulesByPriority = new Map<number, LayoutRule[]>();
|
|
206
|
+
for (const rule of matchingRules) {
|
|
207
|
+
if (!rulesByPriority.has(rule.priority)) {
|
|
208
|
+
rulesByPriority.set(rule.priority, []);
|
|
209
|
+
}
|
|
210
|
+
rulesByPriority.get(rule.priority)!.push(rule);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const priorities = Array.from(rulesByPriority.keys()).sort((a, b) => b - a);
|
|
214
|
+
const highestPriorityRules = rulesByPriority.get(priorities[0])!;
|
|
215
|
+
|
|
216
|
+
if (highestPriorityRules.length === 1) {
|
|
217
|
+
return highestPriorityRules[0].apply;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return this.resolveEqualPriorityConflicts(highestPriorityRules);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private resolveEqualPriorityConflicts(rules: LayoutRule[]): boolean {
|
|
224
|
+
const applyCount = rules.filter(rule => rule.apply).length;
|
|
225
|
+
const skipCount = rules.filter(rule => !rule.apply).length;
|
|
226
|
+
|
|
227
|
+
if (skipCount > applyCount) {
|
|
228
|
+
if (this.developmentMode) console.log(`[LayoutMatcher] Conflict resolution: SKIP`);
|
|
229
|
+
return false;
|
|
230
|
+
} else if (applyCount > skipCount) {
|
|
231
|
+
if (this.developmentMode) console.log(`[LayoutMatcher] Conflict resolution: APPLY`);
|
|
232
|
+
return true;
|
|
233
|
+
} else {
|
|
234
|
+
if (this.developmentMode) console.log(`[LayoutMatcher] Conflict resolution: SKIP (tie-breaker)`);
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create a custom rule
|
|
241
|
+
* Requirements: 4.3
|
|
242
|
+
*/
|
|
243
|
+
static createCustomRule(matcher: (route: RouteInfo) => boolean, apply: boolean, priority: number = 10): LayoutRule {
|
|
244
|
+
return { matches: matcher, apply, priority };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
static createPathRule(pathPattern: string | RegExp, apply: boolean, priority: number = 10): LayoutRule {
|
|
248
|
+
const matches =
|
|
249
|
+
typeof pathPattern === 'string'
|
|
250
|
+
? (route: RouteInfo) => route.path.includes(pathPattern)
|
|
251
|
+
: (route: RouteInfo) => pathPattern.test(route.path);
|
|
252
|
+
return { matches, apply, priority };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
static createHeaderRule(
|
|
256
|
+
headerName: string,
|
|
257
|
+
headerValue: string | RegExp,
|
|
258
|
+
apply: boolean,
|
|
259
|
+
priority: number = 10,
|
|
260
|
+
): LayoutRule {
|
|
261
|
+
return {
|
|
262
|
+
matches: (route: RouteInfo) => {
|
|
263
|
+
const val = route.headers.get(headerName.toLowerCase());
|
|
264
|
+
if (!val) return false;
|
|
265
|
+
return typeof headerValue === 'string' ? val === headerValue : headerValue.test(val);
|
|
266
|
+
},
|
|
267
|
+
apply,
|
|
268
|
+
priority,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
static createMethodRule(methods: string | string[], apply: boolean, priority: number = 10): LayoutRule {
|
|
273
|
+
const normalizedMethods = new Set((Array.isArray(methods) ? methods : [methods]).map(m => m.toUpperCase()));
|
|
274
|
+
return {
|
|
275
|
+
matches: (route: RouteInfo) => normalizedMethods.has(route.method.toUpperCase()),
|
|
276
|
+
apply,
|
|
277
|
+
priority,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
getDebugInfo(
|
|
282
|
+
layoutPath: string,
|
|
283
|
+
route: RouteInfo,
|
|
284
|
+
): {
|
|
285
|
+
totalRules: number;
|
|
286
|
+
matchingRules: Array<{ priority: number; apply: boolean }>;
|
|
287
|
+
finalDecision: boolean;
|
|
288
|
+
conflictResolution?: string;
|
|
289
|
+
} {
|
|
290
|
+
const matchingRules = this.getMatchingRules(route, layoutPath);
|
|
291
|
+
const finalDecision = this.shouldApplyLayout(layoutPath, route);
|
|
292
|
+
return {
|
|
293
|
+
totalRules: this.rules.length,
|
|
294
|
+
matchingRules: matchingRules.map(rule => ({ priority: rule.priority, apply: rule.apply })),
|
|
295
|
+
finalDecision,
|
|
296
|
+
conflictResolution: matchingRules.length > 1 ? 'priority-based' : 'single-rule',
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared layout type definitions for the core/layout module.
|
|
3
|
+
*
|
|
4
|
+
* These are pure TypeScript interfaces with NO zod dependency,
|
|
5
|
+
* intentionally kept separate from schemas/layout.ts to avoid
|
|
6
|
+
* importing zod at cold start time.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
export type ComponentType<P = any> = ((props: P) => any) | (new (props: P) => any);
|
|
11
|
+
|
|
12
|
+
export interface LayoutContext {
|
|
13
|
+
request: Request;
|
|
14
|
+
params: Record<string, string>;
|
|
15
|
+
query: URLSearchParams;
|
|
16
|
+
state: Map<string, unknown>;
|
|
17
|
+
middlewareContext?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type LayoutData = Record<string, unknown>;
|
|
21
|
+
|
|
22
|
+
export type LayoutLoader = (ctx: LayoutContext) => Promise<LayoutData>;
|
|
23
|
+
|
|
24
|
+
export interface LayoutProps {
|
|
25
|
+
children: import('preact').ComponentChildren;
|
|
26
|
+
data: LayoutData;
|
|
27
|
+
frontmatter?: Record<string, unknown>;
|
|
28
|
+
route: {
|
|
29
|
+
path: string;
|
|
30
|
+
params: Record<string, string>;
|
|
31
|
+
query: URLSearchParams;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LayoutHandler {
|
|
36
|
+
component: ComponentType<LayoutProps>;
|
|
37
|
+
loader?: LayoutLoader;
|
|
38
|
+
path: string;
|
|
39
|
+
priority: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LayoutRoute {
|
|
43
|
+
pattern: URLPattern;
|
|
44
|
+
layoutPath: string;
|
|
45
|
+
priority: number;
|
|
46
|
+
type: 'root' | 'nested';
|
|
47
|
+
depth: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface LayoutDiscoveryOptions {
|
|
51
|
+
baseDirectory: string;
|
|
52
|
+
filePattern?: string;
|
|
53
|
+
excludeDirectories?: string[];
|
|
54
|
+
enableWatching?: boolean;
|
|
55
|
+
developmentMode?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface LayoutConfig {
|
|
59
|
+
skipLayouts?: string[];
|
|
60
|
+
replaceLayout?: boolean;
|
|
61
|
+
onlyLayouts?: string[];
|
|
62
|
+
customLayout?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface RouteInfo {
|
|
66
|
+
path: string;
|
|
67
|
+
params: Record<string, string>;
|
|
68
|
+
method: string;
|
|
69
|
+
headers: Headers;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface LayoutRule {
|
|
73
|
+
matches: (route: RouteInfo, layoutPath?: string) => boolean;
|
|
74
|
+
apply: boolean;
|
|
75
|
+
priority: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface LayoutErrorInfo {
|
|
79
|
+
layoutPath: string;
|
|
80
|
+
errorType: 'component' | 'loader' | 'rendering' | 'island';
|
|
81
|
+
timestamp: number;
|
|
82
|
+
componentStack?: string;
|
|
83
|
+
errorBoundary?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ResolvedLayout {
|
|
87
|
+
handlers: LayoutHandler[];
|
|
88
|
+
dataLoaders: LayoutLoader[];
|
|
89
|
+
errorBoundaries: unknown[];
|
|
90
|
+
streamingComponents: unknown[];
|
|
91
|
+
metadata: {
|
|
92
|
+
totalLayouts: number;
|
|
93
|
+
resolutionTime: number;
|
|
94
|
+
cacheHit: boolean;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface LayoutCache {
|
|
99
|
+
resolved: Map<string, ResolvedLayout>;
|
|
100
|
+
handlers: Map<string, LayoutHandler>;
|
|
101
|
+
data: Map<string, LayoutData>;
|
|
102
|
+
ttl: Map<string, number>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface PageModule {
|
|
106
|
+
default: ComponentType<Record<string, unknown>>;
|
|
107
|
+
layoutConfig?: LayoutConfig;
|
|
108
|
+
loader?: LayoutLoader;
|
|
109
|
+
frontmatter?: Record<string, unknown>;
|
|
110
|
+
}
|