@useavalon/avalon 0.1.0
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 -0
- package/mod.ts +301 -0
- package/package.json +85 -0
- package/src/build/README.md +310 -0
- 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 +13 -0
- package/src/client/adapters/lit-adapter.ts +654 -0
- package/src/client/adapters/preact-adapter.ts +331 -0
- package/src/client/adapters/qwik-adapter.ts +345 -0
- package/src/client/adapters/react-adapter.ts +353 -0
- package/src/client/adapters/solid-adapter.ts +451 -0
- package/src/client/adapters/svelte-adapter.ts +524 -0
- package/src/client/adapters/vue-adapter.ts +467 -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 +816 -0
- package/src/client/tests/css-hmr-handler.test.ts +360 -0
- package/src/client/tests/framework-adapter.test.ts +519 -0
- package/src/client/tests/hmr-coordinator.test.ts +176 -0
- package/src/client/tests/hydration-option-parsing.test.ts +107 -0
- package/src/client/tests/lit-adapter.test.ts +427 -0
- package/src/client/tests/preact-adapter.test.ts +353 -0
- package/src/client/tests/qwik-adapter.test.ts +343 -0
- package/src/client/tests/react-adapter.test.ts +317 -0
- package/src/client/tests/solid-adapter.test.ts +396 -0
- package/src/client/tests/svelte-adapter.test.ts +387 -0
- package/src/client/tests/vue-adapter.test.ts +407 -0
- package/src/client/types/framework-runtime.d.ts +68 -0
- package/src/client/types/vite-hmr.d.ts +46 -0
- package/src/client/types/vite-virtual-modules.d.ts +60 -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/components/tests/component-analyzer.test.ts +96 -0
- package/src/components/tests/component-detection.test.ts +347 -0
- package/src/components/tests/persistent-islands.test.ts +398 -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/components/tests/enhanced-framework-detector.test.ts +577 -0
- package/src/core/components/tests/framework-registry.test.ts +465 -0
- package/src/core/content/mdx-processor.ts +46 -0
- package/src/core/integrations/README.md +282 -0
- package/src/core/integrations/index.ts +19 -0
- package/src/core/integrations/loader.ts +125 -0
- package/src/core/integrations/registry.ts +195 -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/layout/tests/enhanced-layout-resolver.test.ts +477 -0
- package/src/core/layout/tests/layout-cache-optimization.test.ts +149 -0
- package/src/core/layout/tests/layout-composer.test.ts +486 -0
- package/src/core/layout/tests/layout-data-loader.test.ts +443 -0
- package/src/core/layout/tests/layout-discovery.test.ts +253 -0
- package/src/core/layout/tests/layout-matcher.test.ts +480 -0
- package/src/core/modules/framework-module-resolver.ts +273 -0
- package/src/core/modules/tests/framework-module-resolver.test.ts +263 -0
- package/src/core/modules/tests/module-resolution-integration.test.ts +117 -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/tests/island-discovery.test.ts +881 -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.d.ts +592 -0
- package/src/layout-system.ts +218 -0
- package/src/middleware/__tests__/discovery.test.ts +107 -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 +1458 -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/image.d.ts +106 -0
- package/src/types/index.d.ts +22 -0
- package/src/types/island-jsx.d.ts +33 -0
- package/src/types/island-prop.d.ts +20 -0
- package/src/types/layout.ts +285 -0
- package/src/types/mdx.d.ts +6 -0
- package/src/types/routing.ts +555 -0
- package/src/types/tests/layout-types.test.ts +197 -0
- package/src/types/types.ts +5 -0
- package/src/types/urlpattern.d.ts +49 -0
- package/src/types/vite-env.d.ts +11 -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 +151 -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 +1334 -0
- package/src/vite-plugin/plugin.ts +329 -0
- package/src/vite-plugin/tests/image-optimization.test.ts +54 -0
- package/src/vite-plugin/types.ts +327 -0
- package/src/vite-plugin/validation.ts +228 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import type { ComponentType } from 'preact';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
EnhancedLayoutResolver,
|
|
9
|
+
createEnhancedLayoutResolver,
|
|
10
|
+
EnhancedLayoutResolverUtils,
|
|
11
|
+
} from '../enhanced-layout-resolver.ts';
|
|
12
|
+
import type { LayoutContext, LayoutConfig } from '../../../types/layout.ts';
|
|
13
|
+
|
|
14
|
+
// Mock components for testing
|
|
15
|
+
const MockPageComponent: ComponentType<any> = () => {
|
|
16
|
+
return { type: 'div', props: { children: 'Page Content' } };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Mock page module
|
|
20
|
+
interface MockPageModule {
|
|
21
|
+
default: ComponentType<any>;
|
|
22
|
+
layoutConfig?: LayoutConfig;
|
|
23
|
+
loader?: (ctx: any) => Promise<any>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Test fixtures
|
|
27
|
+
const createMockLayoutContext = (routePath = '/test'): LayoutContext => ({
|
|
28
|
+
request: new Request(`http://localhost${routePath}`),
|
|
29
|
+
params: {},
|
|
30
|
+
query: new URLSearchParams(),
|
|
31
|
+
state: new Map(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const createMockPageModule = (layoutConfig?: LayoutConfig): MockPageModule => ({
|
|
35
|
+
default: MockPageComponent,
|
|
36
|
+
layoutConfig,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('EnhancedLayoutResolver', () => {
|
|
40
|
+
let resolver: EnhancedLayoutResolver;
|
|
41
|
+
let tempDir: string;
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
process.env.NODE_ENV = 'test';
|
|
45
|
+
|
|
46
|
+
tempDir = await mkdtemp(join(tmpdir(), 'layout_resolver_test_'));
|
|
47
|
+
|
|
48
|
+
resolver = createEnhancedLayoutResolver({
|
|
49
|
+
baseDirectory: tempDir,
|
|
50
|
+
developmentMode: true,
|
|
51
|
+
enableCaching: true,
|
|
52
|
+
enableMetrics: true,
|
|
53
|
+
enableDebugInfo: true,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(async () => {
|
|
58
|
+
if (resolver) {
|
|
59
|
+
resolver.destroy();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await rm(tempDir, { recursive: true });
|
|
64
|
+
} catch {
|
|
65
|
+
// Ignore cleanup errors
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('constructor and configuration', () => {
|
|
70
|
+
it('should create resolver with default options', () => {
|
|
71
|
+
const defaultResolver = new EnhancedLayoutResolver({
|
|
72
|
+
baseDirectory: tempDir,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const options = defaultResolver.getOptions();
|
|
76
|
+
expect(options.baseDirectory).toEqual(tempDir);
|
|
77
|
+
expect(options.filePattern).toEqual('_layout.tsx');
|
|
78
|
+
expect(options.enableCaching).toEqual(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should create resolver with custom options', () => {
|
|
82
|
+
const customResolver = createEnhancedLayoutResolver({
|
|
83
|
+
baseDirectory: tempDir,
|
|
84
|
+
filePattern: 'layout.tsx',
|
|
85
|
+
enableCaching: false,
|
|
86
|
+
cacheTTL: 10000,
|
|
87
|
+
maxCacheSize: 500,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const options = customResolver.getOptions();
|
|
91
|
+
expect(options.filePattern).toEqual('layout.tsx');
|
|
92
|
+
expect(options.enableCaching).toEqual(false);
|
|
93
|
+
expect(options.cacheTTL).toEqual(10000);
|
|
94
|
+
expect(options.maxCacheSize).toEqual(500);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should update options after creation', () => {
|
|
98
|
+
resolver.updateOptions({
|
|
99
|
+
enableCaching: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const options = resolver.getOptions();
|
|
103
|
+
expect(options.enableCaching).toEqual(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('layout resolution pipeline', () => {
|
|
108
|
+
it('should resolve layouts with empty layout chain', async () => {
|
|
109
|
+
const context = createMockLayoutContext('/test');
|
|
110
|
+
const pageModule = createMockPageModule();
|
|
111
|
+
|
|
112
|
+
const result = await resolver.resolveLayouts('/test', pageModule, context);
|
|
113
|
+
|
|
114
|
+
expect(result).toBeDefined();
|
|
115
|
+
expect(result.handlers.length).toEqual(0);
|
|
116
|
+
expect(result.dataLoaders.length).toEqual(0);
|
|
117
|
+
expect(result.metadata.totalLayouts).toEqual(0);
|
|
118
|
+
expect(result.metadata.cacheHit).toEqual(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should handle layout resolution errors gracefully', async () => {
|
|
122
|
+
const context = createMockLayoutContext('/error');
|
|
123
|
+
const pageModule = createMockPageModule();
|
|
124
|
+
|
|
125
|
+
const result = await resolver.resolveLayouts('/error', pageModule, context);
|
|
126
|
+
|
|
127
|
+
expect(result).toBeDefined();
|
|
128
|
+
expect(result.handlers.length).toEqual(0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should collect performance metrics', async () => {
|
|
132
|
+
const context = createMockLayoutContext('/metrics');
|
|
133
|
+
const pageModule = createMockPageModule();
|
|
134
|
+
|
|
135
|
+
const result = await resolver.resolveLayouts('/metrics', pageModule, context);
|
|
136
|
+
|
|
137
|
+
expect(result.metadata).toBeDefined();
|
|
138
|
+
expect(typeof result.metadata.resolutionTime).toEqual('number');
|
|
139
|
+
expect(result.metadata.resolutionTime >= 0).toEqual(true);
|
|
140
|
+
expect(typeof result.metadata.totalLayouts).toEqual('number');
|
|
141
|
+
expect(typeof result.metadata.cacheHit).toEqual('boolean');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('caching system', () => {
|
|
146
|
+
it('should cache layout resolutions', async () => {
|
|
147
|
+
const context = createMockLayoutContext('/cache-test');
|
|
148
|
+
const pageModule = createMockPageModule();
|
|
149
|
+
|
|
150
|
+
const result1 = await resolver.resolveLayouts('/cache-test', pageModule, context);
|
|
151
|
+
expect(result1.metadata.cacheHit).toEqual(false);
|
|
152
|
+
|
|
153
|
+
const result2 = await resolver.resolveLayouts('/cache-test', pageModule, context);
|
|
154
|
+
expect(result2.metadata.cacheHit).toEqual(true);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should respect cache TTL', async () => {
|
|
158
|
+
const shortTTLResolver = createEnhancedLayoutResolver({
|
|
159
|
+
baseDirectory: tempDir,
|
|
160
|
+
enableCaching: true,
|
|
161
|
+
cacheTTL: 10,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const context = createMockLayoutContext('/ttl-test');
|
|
165
|
+
const pageModule = createMockPageModule();
|
|
166
|
+
|
|
167
|
+
await shortTTLResolver.resolveLayouts('/ttl-test', pageModule, context);
|
|
168
|
+
|
|
169
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
170
|
+
|
|
171
|
+
const result = await shortTTLResolver.resolveLayouts('/ttl-test', pageModule, context);
|
|
172
|
+
expect(result.metadata.cacheHit).toEqual(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should clear cache when requested', async () => {
|
|
176
|
+
const context = createMockLayoutContext('/clear-test');
|
|
177
|
+
const pageModule = createMockPageModule();
|
|
178
|
+
|
|
179
|
+
await resolver.resolveLayouts('/clear-test', pageModule, context);
|
|
180
|
+
|
|
181
|
+
resolver.clearCache();
|
|
182
|
+
|
|
183
|
+
const result = await resolver.resolveLayouts('/clear-test', pageModule, context);
|
|
184
|
+
expect(result.metadata.cacheHit).toEqual(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should disable caching when requested', async () => {
|
|
188
|
+
resolver.setCaching(false);
|
|
189
|
+
|
|
190
|
+
const context = createMockLayoutContext('/no-cache');
|
|
191
|
+
const pageModule = createMockPageModule();
|
|
192
|
+
|
|
193
|
+
const result1 = await resolver.resolveLayouts('/no-cache', pageModule, context);
|
|
194
|
+
expect(result1.metadata.cacheHit).toEqual(false);
|
|
195
|
+
|
|
196
|
+
const result2 = await resolver.resolveLayouts('/no-cache', pageModule, context);
|
|
197
|
+
expect(result2.metadata.cacheHit).toEqual(false);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('layout composition control', () => {
|
|
202
|
+
it('should handle replaceLayout configuration', async () => {
|
|
203
|
+
const context = createMockLayoutContext('/replace');
|
|
204
|
+
const pageModule = createMockPageModule({
|
|
205
|
+
replaceLayout: true,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const result = await resolver.resolveLayouts('/replace', pageModule, context);
|
|
209
|
+
|
|
210
|
+
expect(result).toBeDefined();
|
|
211
|
+
expect(result.handlers.length).toEqual(0);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should handle skipLayouts configuration', async () => {
|
|
215
|
+
const context = createMockLayoutContext('/skip');
|
|
216
|
+
const pageModule = createMockPageModule({
|
|
217
|
+
skipLayouts: ['root-layout', 'admin-layout'],
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const result = await resolver.resolveLayouts('/skip', pageModule, context);
|
|
221
|
+
|
|
222
|
+
expect(result).toBeDefined();
|
|
223
|
+
expect(result.metadata.totalLayouts).toEqual(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should handle onlyLayouts configuration', async () => {
|
|
227
|
+
const context = createMockLayoutContext('/only');
|
|
228
|
+
const pageModule = createMockPageModule({
|
|
229
|
+
onlyLayouts: ['specific-layout'],
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = await resolver.resolveLayouts('/only', pageModule, context);
|
|
233
|
+
|
|
234
|
+
expect(result).toBeDefined();
|
|
235
|
+
expect(result.metadata.totalLayouts).toEqual(0);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('component access', () => {
|
|
240
|
+
it('should provide access to layout discovery', () => {
|
|
241
|
+
const discovery = resolver.getLayoutDiscovery();
|
|
242
|
+
expect(discovery).toBeDefined();
|
|
243
|
+
expect(typeof discovery.discoverLayouts).toEqual('function');
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should provide access to layout matcher', () => {
|
|
247
|
+
const matcher = resolver.getLayoutMatcher();
|
|
248
|
+
expect(matcher).toBeDefined();
|
|
249
|
+
expect(typeof matcher.shouldApplyLayout).toEqual('function');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should provide access to layout composer', () => {
|
|
253
|
+
const composer = resolver.getLayoutComposer();
|
|
254
|
+
expect(composer).toBeDefined();
|
|
255
|
+
expect(typeof composer.resolveLayouts).toEqual('function');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should provide access to layout data loader', () => {
|
|
259
|
+
const dataLoader = resolver.getLayoutDataLoader();
|
|
260
|
+
expect(dataLoader).toBeDefined();
|
|
261
|
+
expect(typeof dataLoader.loadLayoutData).toEqual('function');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('should provide access to cache manager', () => {
|
|
265
|
+
const cacheManager = resolver.getCacheManager();
|
|
266
|
+
expect(cacheManager).toBeDefined();
|
|
267
|
+
expect(typeof cacheManager.getStats).toEqual('function');
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('resolver statistics', () => {
|
|
272
|
+
it('should provide resolver statistics', () => {
|
|
273
|
+
const stats = resolver.getResolverStats();
|
|
274
|
+
|
|
275
|
+
expect(stats).toBeDefined();
|
|
276
|
+
expect(typeof stats.cacheSize).toEqual('number');
|
|
277
|
+
expect(typeof stats.cacheHitRate).toEqual('number');
|
|
278
|
+
expect(typeof stats.totalResolutions).toEqual('number');
|
|
279
|
+
expect(typeof stats.averageResolutionTime).toEqual('number');
|
|
280
|
+
expect(typeof stats.errorCount).toEqual('number');
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('EnhancedLayoutResolverUtils', () => {
|
|
286
|
+
describe('configuration helpers', () => {
|
|
287
|
+
it('should create basic configuration', () => {
|
|
288
|
+
const config = EnhancedLayoutResolverUtils.createBasicConfig('/test', false);
|
|
289
|
+
|
|
290
|
+
expect(config.baseDirectory).toEqual('/test');
|
|
291
|
+
expect(config.developmentMode).toEqual(false);
|
|
292
|
+
expect(config.enableCaching).toEqual(true);
|
|
293
|
+
expect(config.enableMetrics).toEqual(false);
|
|
294
|
+
expect(config.enableDebugInfo).toEqual(false);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should create basic configuration in development mode', () => {
|
|
298
|
+
const config = EnhancedLayoutResolverUtils.createBasicConfig('/test', true);
|
|
299
|
+
|
|
300
|
+
expect(config.baseDirectory).toEqual('/test');
|
|
301
|
+
expect(config.developmentMode).toEqual(true);
|
|
302
|
+
expect(config.enableMetrics).toEqual(true);
|
|
303
|
+
expect(config.enableDebugInfo).toEqual(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should create production configuration', () => {
|
|
307
|
+
const config = EnhancedLayoutResolverUtils.createProductionConfig('/prod');
|
|
308
|
+
|
|
309
|
+
expect(config.baseDirectory).toEqual('/prod');
|
|
310
|
+
expect(config.developmentMode).toEqual(false);
|
|
311
|
+
expect(config.enableCaching).toEqual(true);
|
|
312
|
+
expect(config.enableMetrics).toEqual(false);
|
|
313
|
+
expect(config.enableDebugInfo).toEqual(false);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('Integration with layout system components', () => {
|
|
319
|
+
let resolver: EnhancedLayoutResolver;
|
|
320
|
+
let tempDir: string;
|
|
321
|
+
|
|
322
|
+
beforeEach(async () => {
|
|
323
|
+
tempDir = await mkdtemp(join(tmpdir(), 'layout_integration_test_'));
|
|
324
|
+
resolver = createEnhancedLayoutResolver({
|
|
325
|
+
baseDirectory: tempDir,
|
|
326
|
+
developmentMode: true,
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
afterEach(async () => {
|
|
331
|
+
await rm(tempDir, { recursive: true });
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('should integrate with layout matcher for conditional rendering', async () => {
|
|
335
|
+
const matcher = resolver.getLayoutMatcher();
|
|
336
|
+
|
|
337
|
+
matcher.addRule({
|
|
338
|
+
matches: route => route.path.startsWith('/api/'),
|
|
339
|
+
apply: false,
|
|
340
|
+
priority: 100,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const context = createMockLayoutContext('/api/test');
|
|
344
|
+
const pageModule = createMockPageModule();
|
|
345
|
+
|
|
346
|
+
const result = await resolver.resolveLayouts('/api/test', pageModule, context);
|
|
347
|
+
|
|
348
|
+
expect(result).toBeDefined();
|
|
349
|
+
expect(result.metadata.totalLayouts).toEqual(0);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should integrate with layout composer for composition control', async () => {
|
|
353
|
+
const composer = resolver.getLayoutComposer();
|
|
354
|
+
|
|
355
|
+
expect(composer).toBeDefined();
|
|
356
|
+
expect(typeof composer.resolveLayouts).toEqual('function');
|
|
357
|
+
|
|
358
|
+
const context = createMockLayoutContext('/compose');
|
|
359
|
+
const pageModule = createMockPageModule({
|
|
360
|
+
skipLayouts: ['unwanted-layout'],
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const result = await resolver.resolveLayouts('/compose', pageModule, context);
|
|
364
|
+
|
|
365
|
+
expect(result).toBeDefined();
|
|
366
|
+
expect(result.metadata.totalLayouts).toEqual(0);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('should integrate with data loader for layout data', async () => {
|
|
370
|
+
const dataLoader = resolver.getLayoutDataLoader();
|
|
371
|
+
|
|
372
|
+
expect(dataLoader).toBeDefined();
|
|
373
|
+
expect(typeof dataLoader.loadLayoutData).toEqual('function');
|
|
374
|
+
|
|
375
|
+
const context = createMockLayoutContext('/data');
|
|
376
|
+
const pageModule = createMockPageModule();
|
|
377
|
+
|
|
378
|
+
const result = await resolver.resolveLayouts('/data', pageModule, context);
|
|
379
|
+
|
|
380
|
+
expect(result).toBeDefined();
|
|
381
|
+
expect(result.dataLoaders.length).toEqual(0);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe('Error handling and recovery', () => {
|
|
386
|
+
let resolver: EnhancedLayoutResolver;
|
|
387
|
+
let tempDir: string;
|
|
388
|
+
|
|
389
|
+
beforeEach(async () => {
|
|
390
|
+
tempDir = await mkdtemp(join(tmpdir(), 'layout_error_test_'));
|
|
391
|
+
resolver = createEnhancedLayoutResolver({
|
|
392
|
+
baseDirectory: tempDir,
|
|
393
|
+
developmentMode: true,
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
afterEach(async () => {
|
|
398
|
+
await rm(tempDir, { recursive: true });
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should handle errors in discovery stage', async () => {
|
|
402
|
+
const invalidResolver = createEnhancedLayoutResolver({
|
|
403
|
+
baseDirectory: '/nonexistent/path',
|
|
404
|
+
developmentMode: true,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const context = createMockLayoutContext('/error');
|
|
408
|
+
const pageModule = createMockPageModule();
|
|
409
|
+
|
|
410
|
+
const result = await invalidResolver.resolveLayouts('/error', pageModule, context);
|
|
411
|
+
|
|
412
|
+
expect(result).toBeDefined();
|
|
413
|
+
expect(result.handlers.length).toEqual(0);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('should handle errors in composition stage', async () => {
|
|
417
|
+
const context = createMockLayoutContext('/composition-error');
|
|
418
|
+
const pageModule = createMockPageModule({
|
|
419
|
+
customLayout: '/nonexistent/layout.tsx',
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const result = await resolver.resolveLayouts('/composition-error', pageModule, context);
|
|
423
|
+
|
|
424
|
+
expect(result).toBeDefined();
|
|
425
|
+
expect(result.metadata.totalLayouts).toEqual(0);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe('Performance and metrics', () => {
|
|
430
|
+
let resolver: EnhancedLayoutResolver;
|
|
431
|
+
let tempDir: string;
|
|
432
|
+
|
|
433
|
+
beforeEach(async () => {
|
|
434
|
+
tempDir = await mkdtemp(join(tmpdir(), 'layout_perf_test_'));
|
|
435
|
+
resolver = createEnhancedLayoutResolver({
|
|
436
|
+
baseDirectory: tempDir,
|
|
437
|
+
developmentMode: true,
|
|
438
|
+
enableMetrics: true,
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
afterEach(async () => {
|
|
443
|
+
await rm(tempDir, { recursive: true });
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should collect timing metrics', async () => {
|
|
447
|
+
const context = createMockLayoutContext('/perf');
|
|
448
|
+
const pageModule = createMockPageModule();
|
|
449
|
+
|
|
450
|
+
const result = await resolver.resolveLayouts('/perf', pageModule, context);
|
|
451
|
+
|
|
452
|
+
expect(result.metadata).toBeDefined();
|
|
453
|
+
expect(typeof result.metadata.resolutionTime).toEqual('number');
|
|
454
|
+
expect(result.metadata.resolutionTime >= 0).toEqual(true);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('should handle multiple concurrent resolutions', async () => {
|
|
458
|
+
const context1 = createMockLayoutContext('/concurrent1');
|
|
459
|
+
const context2 = createMockLayoutContext('/concurrent2');
|
|
460
|
+
const context3 = createMockLayoutContext('/concurrent3');
|
|
461
|
+
const pageModule = createMockPageModule();
|
|
462
|
+
|
|
463
|
+
const [result1, result2, result3] = await Promise.all([
|
|
464
|
+
resolver.resolveLayouts('/concurrent1', pageModule, context1),
|
|
465
|
+
resolver.resolveLayouts('/concurrent2', pageModule, context2),
|
|
466
|
+
resolver.resolveLayouts('/concurrent3', pageModule, context3),
|
|
467
|
+
]);
|
|
468
|
+
|
|
469
|
+
expect(result1).toBeDefined();
|
|
470
|
+
expect(result2).toBeDefined();
|
|
471
|
+
expect(result3).toBeDefined();
|
|
472
|
+
|
|
473
|
+
expect(typeof result1.metadata.resolutionTime).toEqual('number');
|
|
474
|
+
expect(typeof result2.metadata.resolutionTime).toEqual('number');
|
|
475
|
+
expect(typeof result3.metadata.resolutionTime).toEqual('number');
|
|
476
|
+
});
|
|
477
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { LayoutCacheManager, defaultCacheConfig } from '../layout-cache-manager.ts';
|
|
3
|
+
import type { ResolvedLayout, LayoutHandler, LayoutData } from '../../../types/layout.ts';
|
|
4
|
+
|
|
5
|
+
describe('LayoutCacheManager - Intelligent Invalidation', () => {
|
|
6
|
+
it('should cache and invalidate by file path', () => {
|
|
7
|
+
const cacheManager = new LayoutCacheManager({
|
|
8
|
+
...defaultCacheConfig,
|
|
9
|
+
enableStats: true,
|
|
10
|
+
intelligentInvalidation: true,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const resolvedLayout: ResolvedLayout = {
|
|
14
|
+
handlers: [],
|
|
15
|
+
dataLoaders: [],
|
|
16
|
+
errorBoundaries: [],
|
|
17
|
+
streamingComponents: [],
|
|
18
|
+
metadata: {
|
|
19
|
+
totalLayouts: 1,
|
|
20
|
+
resolutionTime: 100,
|
|
21
|
+
cacheHit: false,
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const layoutHandler: LayoutHandler = {
|
|
26
|
+
component: () => null,
|
|
27
|
+
path: '/test/layout.tsx',
|
|
28
|
+
priority: 10,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const layoutData: LayoutData = {
|
|
32
|
+
title: 'Test Layout',
|
|
33
|
+
description: 'Test description',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const routeKey = '/test/route:/test/layout.tsx';
|
|
37
|
+
const handlerKey = '/test/layout.tsx';
|
|
38
|
+
const dataKey = '/test/layout.tsx:data';
|
|
39
|
+
|
|
40
|
+
cacheManager.setResolvedLayout(routeKey, resolvedLayout);
|
|
41
|
+
cacheManager.setLayoutHandler(handlerKey, layoutHandler);
|
|
42
|
+
cacheManager.setLayoutData(dataKey, layoutData);
|
|
43
|
+
|
|
44
|
+
const cachedLayout = cacheManager.getResolvedLayout(routeKey);
|
|
45
|
+
const cachedHandler = cacheManager.getLayoutHandler(handlerKey);
|
|
46
|
+
const cachedData = cacheManager.getLayoutData(dataKey);
|
|
47
|
+
|
|
48
|
+
expect(cachedLayout).toBeDefined();
|
|
49
|
+
expect(cachedHandler).toBeDefined();
|
|
50
|
+
expect(cachedData).toBeDefined();
|
|
51
|
+
|
|
52
|
+
cacheManager.addDependency(routeKey, '/test/layout.tsx');
|
|
53
|
+
|
|
54
|
+
const invalidatedCount = cacheManager.invalidateByFilePath('/test/layout.tsx');
|
|
55
|
+
expect(invalidatedCount >= 1).toEqual(true);
|
|
56
|
+
|
|
57
|
+
const invalidatedLayout = cacheManager.getResolvedLayout(routeKey);
|
|
58
|
+
expect(invalidatedLayout).toEqual(null);
|
|
59
|
+
|
|
60
|
+
const stats = cacheManager.getStats();
|
|
61
|
+
expect(stats).toBeDefined();
|
|
62
|
+
expect(typeof stats.hits).toEqual('number');
|
|
63
|
+
expect(typeof stats.misses).toEqual('number');
|
|
64
|
+
|
|
65
|
+
const hitRate = cacheManager.getHitRate();
|
|
66
|
+
expect(typeof hitRate).toEqual('number');
|
|
67
|
+
|
|
68
|
+
cacheManager.destroy();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('LayoutCacheManager - LRU Eviction', () => {
|
|
73
|
+
it('should evict least recently used entries', () => {
|
|
74
|
+
const cacheManager = new LayoutCacheManager({
|
|
75
|
+
...defaultCacheConfig,
|
|
76
|
+
maxEntries: 3,
|
|
77
|
+
enableStats: true,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const createResolvedLayout = (id: string): ResolvedLayout => ({
|
|
81
|
+
handlers: [],
|
|
82
|
+
dataLoaders: [],
|
|
83
|
+
errorBoundaries: [],
|
|
84
|
+
streamingComponents: [],
|
|
85
|
+
metadata: {
|
|
86
|
+
totalLayouts: 1,
|
|
87
|
+
resolutionTime: 100,
|
|
88
|
+
cacheHit: false,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
cacheManager.setResolvedLayout('route1', createResolvedLayout('1'));
|
|
93
|
+
cacheManager.setResolvedLayout('route2', createResolvedLayout('2'));
|
|
94
|
+
cacheManager.setResolvedLayout('route3', createResolvedLayout('3'));
|
|
95
|
+
|
|
96
|
+
cacheManager.getResolvedLayout('route1');
|
|
97
|
+
|
|
98
|
+
cacheManager.setResolvedLayout('route4', createResolvedLayout('4'));
|
|
99
|
+
|
|
100
|
+
const route1 = cacheManager.getResolvedLayout('route1');
|
|
101
|
+
const route2 = cacheManager.getResolvedLayout('route2');
|
|
102
|
+
const route3 = cacheManager.getResolvedLayout('route3');
|
|
103
|
+
const route4 = cacheManager.getResolvedLayout('route4');
|
|
104
|
+
|
|
105
|
+
expect(route1).toBeDefined();
|
|
106
|
+
expect(route2).toEqual(null);
|
|
107
|
+
expect(route3).toBeDefined();
|
|
108
|
+
expect(route4).toBeDefined();
|
|
109
|
+
|
|
110
|
+
cacheManager.destroy();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('LayoutCacheManager - Performance Metrics', () => {
|
|
115
|
+
it('should track hits and misses', () => {
|
|
116
|
+
const cacheManager = new LayoutCacheManager({
|
|
117
|
+
...defaultCacheConfig,
|
|
118
|
+
enableStats: true,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const resolvedLayout: ResolvedLayout = {
|
|
122
|
+
handlers: [],
|
|
123
|
+
dataLoaders: [],
|
|
124
|
+
errorBoundaries: [],
|
|
125
|
+
streamingComponents: [],
|
|
126
|
+
metadata: {
|
|
127
|
+
totalLayouts: 1,
|
|
128
|
+
resolutionTime: 100,
|
|
129
|
+
cacheHit: false,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const miss = cacheManager.getResolvedLayout('nonexistent');
|
|
134
|
+
expect(miss).toEqual(null);
|
|
135
|
+
|
|
136
|
+
cacheManager.setResolvedLayout('test', resolvedLayout);
|
|
137
|
+
const hit = cacheManager.getResolvedLayout('test');
|
|
138
|
+
expect(hit).toBeDefined();
|
|
139
|
+
|
|
140
|
+
const stats = cacheManager.getStats();
|
|
141
|
+
expect(stats.hits).toEqual(1);
|
|
142
|
+
expect(stats.misses).toEqual(1);
|
|
143
|
+
|
|
144
|
+
const hitRate = cacheManager.getHitRate();
|
|
145
|
+
expect(hitRate).toEqual(0.5);
|
|
146
|
+
|
|
147
|
+
cacheManager.destroy();
|
|
148
|
+
});
|
|
149
|
+
});
|