@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.
Files changed (159) hide show
  1. package/README.md +54 -0
  2. package/mod.ts +301 -0
  3. package/package.json +85 -0
  4. package/src/build/README.md +310 -0
  5. package/src/build/integration-bundler-plugin.ts +116 -0
  6. package/src/build/integration-config.ts +168 -0
  7. package/src/build/integration-detection-plugin.ts +117 -0
  8. package/src/build/integration-resolver-plugin.ts +90 -0
  9. package/src/build/island-manifest.ts +269 -0
  10. package/src/build/island-types-generator.ts +476 -0
  11. package/src/build/mdx-island-transform.ts +464 -0
  12. package/src/build/mdx-plugin.ts +98 -0
  13. package/src/build/page-island-transform.ts +598 -0
  14. package/src/build/prop-extractors/index.ts +21 -0
  15. package/src/build/prop-extractors/lit.ts +140 -0
  16. package/src/build/prop-extractors/qwik.ts +16 -0
  17. package/src/build/prop-extractors/solid.ts +125 -0
  18. package/src/build/prop-extractors/svelte.ts +194 -0
  19. package/src/build/prop-extractors/vue.ts +111 -0
  20. package/src/build/sidecar-file-manager.ts +104 -0
  21. package/src/build/sidecar-renderer.ts +30 -0
  22. package/src/client/adapters/index.ts +13 -0
  23. package/src/client/adapters/lit-adapter.ts +654 -0
  24. package/src/client/adapters/preact-adapter.ts +331 -0
  25. package/src/client/adapters/qwik-adapter.ts +345 -0
  26. package/src/client/adapters/react-adapter.ts +353 -0
  27. package/src/client/adapters/solid-adapter.ts +451 -0
  28. package/src/client/adapters/svelte-adapter.ts +524 -0
  29. package/src/client/adapters/vue-adapter.ts +467 -0
  30. package/src/client/components.ts +35 -0
  31. package/src/client/css-hmr-handler.ts +344 -0
  32. package/src/client/framework-adapter.ts +462 -0
  33. package/src/client/hmr-coordinator.ts +396 -0
  34. package/src/client/hmr-error-overlay.js +533 -0
  35. package/src/client/main.js +816 -0
  36. package/src/client/tests/css-hmr-handler.test.ts +360 -0
  37. package/src/client/tests/framework-adapter.test.ts +519 -0
  38. package/src/client/tests/hmr-coordinator.test.ts +176 -0
  39. package/src/client/tests/hydration-option-parsing.test.ts +107 -0
  40. package/src/client/tests/lit-adapter.test.ts +427 -0
  41. package/src/client/tests/preact-adapter.test.ts +353 -0
  42. package/src/client/tests/qwik-adapter.test.ts +343 -0
  43. package/src/client/tests/react-adapter.test.ts +317 -0
  44. package/src/client/tests/solid-adapter.test.ts +396 -0
  45. package/src/client/tests/svelte-adapter.test.ts +387 -0
  46. package/src/client/tests/vue-adapter.test.ts +407 -0
  47. package/src/client/types/framework-runtime.d.ts +68 -0
  48. package/src/client/types/vite-hmr.d.ts +46 -0
  49. package/src/client/types/vite-virtual-modules.d.ts +60 -0
  50. package/src/components/Image.tsx +123 -0
  51. package/src/components/IslandErrorBoundary.tsx +145 -0
  52. package/src/components/LayoutDataErrorBoundary.tsx +141 -0
  53. package/src/components/LayoutErrorBoundary.tsx +127 -0
  54. package/src/components/PersistentIsland.tsx +52 -0
  55. package/src/components/StreamingErrorBoundary.tsx +233 -0
  56. package/src/components/StreamingLayout.tsx +538 -0
  57. package/src/components/tests/component-analyzer.test.ts +96 -0
  58. package/src/components/tests/component-detection.test.ts +347 -0
  59. package/src/components/tests/persistent-islands.test.ts +398 -0
  60. package/src/core/components/component-analyzer.ts +192 -0
  61. package/src/core/components/component-detection.ts +508 -0
  62. package/src/core/components/enhanced-framework-detector.ts +500 -0
  63. package/src/core/components/framework-registry.ts +563 -0
  64. package/src/core/components/tests/enhanced-framework-detector.test.ts +577 -0
  65. package/src/core/components/tests/framework-registry.test.ts +465 -0
  66. package/src/core/content/mdx-processor.ts +46 -0
  67. package/src/core/integrations/README.md +282 -0
  68. package/src/core/integrations/index.ts +19 -0
  69. package/src/core/integrations/loader.ts +125 -0
  70. package/src/core/integrations/registry.ts +195 -0
  71. package/src/core/islands/island-persistence.ts +325 -0
  72. package/src/core/islands/island-state-serializer.ts +258 -0
  73. package/src/core/islands/persistent-island-context.tsx +80 -0
  74. package/src/core/islands/use-persistent-state.ts +68 -0
  75. package/src/core/layout/enhanced-layout-resolver.ts +322 -0
  76. package/src/core/layout/layout-cache-manager.ts +485 -0
  77. package/src/core/layout/layout-composer.ts +357 -0
  78. package/src/core/layout/layout-data-loader.ts +516 -0
  79. package/src/core/layout/layout-discovery.ts +243 -0
  80. package/src/core/layout/layout-matcher.ts +299 -0
  81. package/src/core/layout/layout-types.ts +110 -0
  82. package/src/core/layout/tests/enhanced-layout-resolver.test.ts +477 -0
  83. package/src/core/layout/tests/layout-cache-optimization.test.ts +149 -0
  84. package/src/core/layout/tests/layout-composer.test.ts +486 -0
  85. package/src/core/layout/tests/layout-data-loader.test.ts +443 -0
  86. package/src/core/layout/tests/layout-discovery.test.ts +253 -0
  87. package/src/core/layout/tests/layout-matcher.test.ts +480 -0
  88. package/src/core/modules/framework-module-resolver.ts +273 -0
  89. package/src/core/modules/tests/framework-module-resolver.test.ts +263 -0
  90. package/src/core/modules/tests/module-resolution-integration.test.ts +117 -0
  91. package/src/islands/component-analysis.ts +213 -0
  92. package/src/islands/css-utils.ts +565 -0
  93. package/src/islands/discovery/index.ts +80 -0
  94. package/src/islands/discovery/registry.ts +340 -0
  95. package/src/islands/discovery/resolver.ts +477 -0
  96. package/src/islands/discovery/scanner.ts +386 -0
  97. package/src/islands/discovery/tests/island-discovery.test.ts +881 -0
  98. package/src/islands/discovery/types.ts +117 -0
  99. package/src/islands/discovery/validator.ts +544 -0
  100. package/src/islands/discovery/watcher.ts +368 -0
  101. package/src/islands/framework-detection.ts +428 -0
  102. package/src/islands/integration-loader.ts +490 -0
  103. package/src/islands/island.tsx +565 -0
  104. package/src/islands/render-cache.ts +550 -0
  105. package/src/islands/types.ts +80 -0
  106. package/src/islands/universal-css-collector.ts +157 -0
  107. package/src/islands/universal-head-collector.ts +137 -0
  108. package/src/layout-system.d.ts +592 -0
  109. package/src/layout-system.ts +218 -0
  110. package/src/middleware/__tests__/discovery.test.ts +107 -0
  111. package/src/middleware/discovery.ts +268 -0
  112. package/src/middleware/executor.ts +315 -0
  113. package/src/middleware/index.ts +76 -0
  114. package/src/middleware/types.ts +99 -0
  115. package/src/nitro/build-config.ts +576 -0
  116. package/src/nitro/config.ts +483 -0
  117. package/src/nitro/error-handler.ts +636 -0
  118. package/src/nitro/index.ts +173 -0
  119. package/src/nitro/island-manifest.ts +584 -0
  120. package/src/nitro/middleware-adapter.ts +260 -0
  121. package/src/nitro/renderer.ts +1458 -0
  122. package/src/nitro/route-discovery.ts +439 -0
  123. package/src/nitro/types.ts +321 -0
  124. package/src/render/collect-css.ts +198 -0
  125. package/src/render/error-pages.ts +79 -0
  126. package/src/render/isolated-ssr-renderer.ts +654 -0
  127. package/src/render/ssr.ts +1030 -0
  128. package/src/schemas/api.ts +30 -0
  129. package/src/schemas/core.ts +64 -0
  130. package/src/schemas/index.ts +212 -0
  131. package/src/schemas/layout.ts +279 -0
  132. package/src/schemas/routing/index.ts +38 -0
  133. package/src/schemas/routing.ts +376 -0
  134. package/src/types/as-island.ts +20 -0
  135. package/src/types/image.d.ts +106 -0
  136. package/src/types/index.d.ts +22 -0
  137. package/src/types/island-jsx.d.ts +33 -0
  138. package/src/types/island-prop.d.ts +20 -0
  139. package/src/types/layout.ts +285 -0
  140. package/src/types/mdx.d.ts +6 -0
  141. package/src/types/routing.ts +555 -0
  142. package/src/types/tests/layout-types.test.ts +197 -0
  143. package/src/types/types.ts +5 -0
  144. package/src/types/urlpattern.d.ts +49 -0
  145. package/src/types/vite-env.d.ts +11 -0
  146. package/src/utils/dev-logger.ts +299 -0
  147. package/src/utils/fs.ts +151 -0
  148. package/src/vite-plugin/auto-discover.ts +551 -0
  149. package/src/vite-plugin/config.ts +266 -0
  150. package/src/vite-plugin/errors.ts +127 -0
  151. package/src/vite-plugin/image-optimization.ts +151 -0
  152. package/src/vite-plugin/integration-activator.ts +126 -0
  153. package/src/vite-plugin/island-sidecar-plugin.ts +176 -0
  154. package/src/vite-plugin/module-discovery.ts +189 -0
  155. package/src/vite-plugin/nitro-integration.ts +1334 -0
  156. package/src/vite-plugin/plugin.ts +329 -0
  157. package/src/vite-plugin/tests/image-optimization.test.ts +54 -0
  158. package/src/vite-plugin/types.ts +327 -0
  159. package/src/vite-plugin/validation.ts +228 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Advanced Layout System - Complete Export Index
3
+ *
4
+ * This file provides a comprehensive export index for the entire layout system,
5
+ * making it easy to import all layout-related functionality from a single location.
6
+ */
7
+
8
+ // === Core Layout System ===
9
+
10
+ // Layout Discovery
11
+ export { LayoutDiscovery } from './core/layout/layout-discovery.ts';
12
+ export type { LayoutDiscoveryOptions, LayoutRoute } from './schemas/layout.ts';
13
+
14
+ // Layout Data Loading
15
+ export {
16
+ LayoutDataLoader,
17
+ LayoutDataLoadingError,
18
+ loadSingleLayoutData,
19
+ mergeLayoutData,
20
+ getParentLayoutData,
21
+ defaultLayoutDataLoader,
22
+ } from './core/layout/layout-data-loader.ts';
23
+ export type { LayoutDataLoadingResult, LayoutDataLoadingOptions } from './core/layout/layout-data-loader.ts';
24
+
25
+ // Layout Matching and Conditional Rendering
26
+ export { LayoutMatcher as LayoutMatcherClass, BuiltInLayoutRules } from './core/layout/layout-matcher.ts';
27
+ export type { LayoutRule, RouteInfo } from './schemas/layout.ts';
28
+
29
+ // Layout Composition Control
30
+ export { LayoutComposer } from './core/layout/layout-composer.ts';
31
+ export type { LayoutConfig } from './schemas/layout.ts';
32
+
33
+ // Enhanced Layout Resolver
34
+ export {
35
+ EnhancedLayoutResolver,
36
+ createEnhancedLayoutResolver,
37
+ EnhancedLayoutResolverUtils,
38
+ } from './core/layout/enhanced-layout-resolver.ts';
39
+ export type { EnhancedLayoutResolverOptions } from './core/layout/enhanced-layout-resolver.ts';
40
+
41
+ // === Persistent Islands System ===
42
+
43
+ export { IslandPersistence, defaultIslandPersistence } from './core/islands/island-persistence.ts';
44
+ export { IslandStateSerializer } from './core/islands/island-state-serializer.ts';
45
+ export {
46
+ createPersistentIslandContext,
47
+ usePersistentIslandContext,
48
+ PersistentIslandProvider,
49
+ } from './core/islands/persistent-island-context.tsx';
50
+ export { PersistentIsland } from './components/PersistentIsland.tsx';
51
+ export type { IslandState, PersistentIslandProps, PersistentIslandContext } from './schemas/layout.ts';
52
+
53
+ // === Error Boundary System ===
54
+
55
+ export { LayoutErrorBoundary } from './components/LayoutErrorBoundary.tsx';
56
+ export { LayoutDataErrorBoundary } from './components/LayoutDataErrorBoundary.tsx';
57
+ export { IslandErrorBoundary, withIslandErrorBoundary } from './components/IslandErrorBoundary.tsx';
58
+ export { StreamingErrorBoundary, withStreamingErrorBoundary } from './components/StreamingErrorBoundary.tsx';
59
+ export type { LayoutErrorInfo, LayoutErrorBoundaryProps, ErrorRecoveryStrategy } from './schemas/layout.ts';
60
+
61
+ // === Streaming System ===
62
+
63
+ export { StreamingLayout, StreamingSuspense, withStreaming, useStreamingState } from './components/StreamingLayout.tsx';
64
+ export type { StreamingLayoutProps, StreamingComponent } from './schemas/layout.ts';
65
+
66
+ // === Layout Utilities (Essential Only) ===
67
+ // Debug and performance utilities are available via lazy import from './core/layout/layout-utilities.ts'
68
+ // when needed in development mode
69
+
70
+ export { LayoutCacheManager } from './core/layout/layout-cache-manager.ts';
71
+ export type {
72
+ CacheEntry,
73
+ CacheStats,
74
+ CacheConfig,
75
+ } from './core/layout/layout-cache-manager.ts';
76
+
77
+ // === Core Types and Schemas ===
78
+
79
+ export type {
80
+ LayoutContext,
81
+ LayoutData,
82
+ LayoutHandler,
83
+ LayoutProps,
84
+ LayoutLoader,
85
+ ResolvedLayout,
86
+ LayoutCache,
87
+ EnhancedLayoutContext,
88
+ LayoutMatcherFunction,
89
+ LayoutErrorHandler,
90
+ LayoutRetryFunction,
91
+ LayoutFallbackRenderer,
92
+ IslandStateSaver,
93
+ IslandStateLoader,
94
+ IslandStateClearer,
95
+ StreamingReadyCheck,
96
+ } from './schemas/layout.ts';
97
+
98
+ // === Advanced Interface Types ===
99
+
100
+ export type {
101
+ ILayoutDiscovery,
102
+ ILayoutMatcher,
103
+ ILayoutComposer,
104
+ IIslandPersistence,
105
+ ILayoutErrorRecovery,
106
+ ILayoutStreaming,
107
+ IEnhancedLayoutResolver,
108
+ ILayoutComponent,
109
+ IPersistentIslandComponent,
110
+ ILayoutErrorBoundaryComponent,
111
+ IStreamingLayoutComponent,
112
+ LayoutModule,
113
+ PageModule,
114
+ LayoutResolutionContext,
115
+ LayoutPerformanceMetrics,
116
+ LayoutDebugInfo,
117
+ LayoutEventType,
118
+ LayoutEventData,
119
+ LayoutEventHandler,
120
+ ILayoutEventEmitter,
121
+ } from './types/layout.ts';
122
+
123
+ // === Validation Schemas ===
124
+
125
+ export {
126
+ LayoutContextSchema,
127
+ LayoutDataSchema,
128
+ LayoutRouteSchema,
129
+ LayoutHandlerSchema,
130
+ LayoutPropsSchema,
131
+ LayoutDiscoveryOptionsSchema,
132
+ RouteInfoSchema,
133
+ LayoutRuleSchema,
134
+ LayoutConfigSchema,
135
+ IslandStateSchema,
136
+ PersistentIslandPropsSchema,
137
+ PersistentIslandContextSchema,
138
+ LayoutErrorInfoSchema,
139
+ LayoutErrorBoundaryPropsSchema,
140
+ ErrorRecoveryStrategySchema,
141
+ StreamingLayoutPropsSchema,
142
+ StreamingComponentSchema,
143
+ ResolvedLayoutSchema,
144
+ LayoutCacheSchema,
145
+ EnhancedLayoutContextSchema,
146
+ } from './schemas/layout.ts';
147
+
148
+ // === Convenience Re-exports ===
149
+
150
+ // Main layout system class for easy access
151
+ export { EnhancedLayoutResolver as LayoutSystem } from './core/layout/enhanced-layout-resolver.ts';
152
+
153
+ // Default instances for quick setup
154
+ export { defaultIslandPersistence as defaultPersistence } from './core/islands/island-persistence.ts';
155
+ // Note: defaultLayoutUtilities removed to reduce cold start overhead
156
+ // Import directly from './core/layout/layout-utilities.ts' if needed
157
+
158
+ // Factory functions for custom setups
159
+ export { createEnhancedLayoutResolver as createLayoutSystem } from './core/layout/enhanced-layout-resolver.ts';
160
+
161
+ /**
162
+ * Layout System Version Information
163
+ */
164
+ export const LAYOUT_SYSTEM_VERSION = '1.0.0';
165
+
166
+ /**
167
+ * Layout System Feature Flags
168
+ */
169
+ export const LAYOUT_SYSTEM_FEATURES = {
170
+ DISCOVERY: true,
171
+ DATA_LOADING: true,
172
+ CONDITIONAL_RENDERING: true,
173
+ COMPOSITION_CONTROL: true,
174
+ PERSISTENT_ISLANDS: true,
175
+ ERROR_BOUNDARIES: true,
176
+ STREAMING: true,
177
+ CACHING: true,
178
+ PERFORMANCE_MONITORING: true,
179
+ DEBUG_UTILITIES: true,
180
+ } as const;
181
+
182
+ /**
183
+ * Layout System Configuration Defaults
184
+ */
185
+ export const LAYOUT_SYSTEM_DEFAULTS = {
186
+ DISCOVERY: {
187
+ baseDirectory: 'src/pages',
188
+ filePattern: '_layout.tsx',
189
+ excludeDirectories: ['node_modules', '.git', 'dist'],
190
+ enableWatching: false,
191
+ developmentMode: false,
192
+ },
193
+ CACHING: {
194
+ enabled: true,
195
+ ttl: 300000, // 5 minutes
196
+ maxSize: 100,
197
+ cleanupInterval: 60000, // 1 minute
198
+ },
199
+ STREAMING: {
200
+ enabled: true,
201
+ priority: 'medium' as const,
202
+ timeout: 5000,
203
+ },
204
+ ERROR_BOUNDARIES: {
205
+ enabled: true,
206
+ maxRetries: 3,
207
+ fallbackStrategy: 'component' as const,
208
+ },
209
+ PERFORMANCE: {
210
+ monitoring: true,
211
+ thresholds: {
212
+ discoveryTime: 100,
213
+ dataLoadingTime: 500,
214
+ renderingTime: 200,
215
+ totalTime: 1000,
216
+ },
217
+ },
218
+ } as const;
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { join } from 'node:path';
3
+ import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { discoverScopedMiddleware, getMatchingMiddleware } from '../discovery.ts';
6
+
7
+ describe('discoverScopedMiddleware', () => {
8
+ let tempDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tempDir = await mkdtemp(join(tmpdir(), 'middleware-test-'));
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await rm(tempDir, { recursive: true, force: true });
16
+ });
17
+
18
+ it('discovers _middleware.ts files in pages subdirectories', async () => {
19
+ // Create pages directory structure with middleware files
20
+ await mkdir(join(tempDir, 'pages', 'admin'), { recursive: true });
21
+ await mkdir(join(tempDir, 'pages', 'blog'), { recursive: true });
22
+ await writeFile(join(tempDir, 'pages', 'admin', '_middleware.ts'), 'export default () => {};');
23
+ await writeFile(join(tempDir, 'pages', 'blog', '_middleware.ts'), 'export default () => {};');
24
+
25
+ const routes = await discoverScopedMiddleware({ baseDir: tempDir });
26
+
27
+ expect(routes).toHaveLength(2);
28
+ expect(routes.every(r => r.type === 'pages')).toBe(true);
29
+ expect(routes.map(r => r.filePath)).toEqual(
30
+ expect.arrayContaining([
31
+ join(tempDir, 'pages', 'admin', '_middleware.ts'),
32
+ join(tempDir, 'pages', 'blog', '_middleware.ts'),
33
+ ])
34
+ );
35
+ });
36
+
37
+ it('discovers nested _middleware.ts files with correct priority ordering', async () => {
38
+ await mkdir(join(tempDir, 'pages', 'admin', 'users'), { recursive: true });
39
+ await writeFile(join(tempDir, 'pages', 'admin', '_middleware.ts'), 'export default () => {};');
40
+ await writeFile(join(tempDir, 'pages', 'admin', 'users', '_middleware.ts'), 'export default () => {};');
41
+
42
+ const routes = await discoverScopedMiddleware({ baseDir: tempDir });
43
+
44
+ expect(routes).toHaveLength(2);
45
+ // Parent middleware (depth 1) should come before child (depth 2)
46
+ expect(routes[0].priority).toBeLessThan(routes[1].priority);
47
+ expect(routes[0].filePath).toContain(join('admin', '_middleware.ts'));
48
+ expect(routes[1].filePath).toContain(join('admin', 'users', '_middleware.ts'));
49
+ });
50
+
51
+ it('does NOT scan api directory for middleware', async () => {
52
+ await mkdir(join(tempDir, 'pages', 'admin'), { recursive: true });
53
+ await mkdir(join(tempDir, 'api', 'admin'), { recursive: true });
54
+ await writeFile(join(tempDir, 'pages', 'admin', '_middleware.ts'), 'export default () => {};');
55
+ await writeFile(join(tempDir, 'api', 'admin', '_middleware.ts'), 'export default () => {};');
56
+
57
+ const routes = await discoverScopedMiddleware({ baseDir: tempDir });
58
+
59
+ expect(routes).toHaveLength(1);
60
+ expect(routes[0].type).toBe('pages');
61
+ expect(routes[0].filePath).toContain(join('pages', 'admin', '_middleware.ts'));
62
+ });
63
+
64
+ it('returns empty array when no middleware files exist', async () => {
65
+ await mkdir(join(tempDir, 'pages'), { recursive: true });
66
+
67
+ const routes = await discoverScopedMiddleware({ baseDir: tempDir });
68
+
69
+ expect(routes).toHaveLength(0);
70
+ });
71
+
72
+ it('returns empty array when pages directory does not exist', async () => {
73
+ const routes = await discoverScopedMiddleware({ baseDir: tempDir });
74
+
75
+ expect(routes).toHaveLength(0);
76
+ });
77
+ });
78
+
79
+ describe('getMatchingMiddleware', () => {
80
+ it('matches page middleware for page routes', () => {
81
+ const routes = [
82
+ {
83
+ pattern: new URLPattern({ pathname: '/admin{/*}?' }),
84
+ filePath: '/src/pages/admin/_middleware.ts',
85
+ priority: 51,
86
+ type: 'pages' as const,
87
+ },
88
+ ];
89
+
90
+ const matches = getMatchingMiddleware(routes, new URL('http://localhost/admin/dashboard'));
91
+ expect(matches).toHaveLength(1);
92
+ });
93
+
94
+ it('does not match page middleware for API routes', () => {
95
+ const routes = [
96
+ {
97
+ pattern: new URLPattern({ pathname: '/*' }),
98
+ filePath: '/src/pages/_middleware.ts',
99
+ priority: 50,
100
+ type: 'pages' as const,
101
+ },
102
+ ];
103
+
104
+ const matches = getMatchingMiddleware(routes, new URL('http://localhost/api/users'));
105
+ expect(matches).toHaveLength(0);
106
+ });
107
+ });
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Simplified Middleware Discovery
3
+ *
4
+ * This module provides a streamlined middleware discovery system that scans
5
+ * for _middleware.ts files in src/pages/ directories.
6
+ *
7
+ * Key features:
8
+ * - Scans src/pages/ for page middleware
9
+ * - Calculates priority based on directory depth (parent before child)
10
+ * - Creates URL patterns for route matching
11
+ *
12
+ * Note: API middleware is handled natively by Nitro and is not scanned here.
13
+ *
14
+ * Requirements: 3.1, 3.2, 3.3, 3.4
15
+ */
16
+
17
+ import { join, relative, resolve } from 'node:path';
18
+ import { stat as fsStat, readdir } from 'node:fs/promises';
19
+ import type { MiddlewareRoute, MiddlewareDiscoveryOptions } from './types.ts';
20
+
21
+ /**
22
+ * Default options for middleware discovery
23
+ */
24
+ const DEFAULT_OPTIONS: Required<Omit<MiddlewareDiscoveryOptions, 'baseDir'>> = {
25
+ filePattern: '_middleware.ts',
26
+ excludeDirs: ['node_modules', '.git', 'dist', '.output', '.vite'],
27
+ devMode: false,
28
+ };
29
+
30
+ /**
31
+ * Base priority values for different middleware types
32
+ * Lower values execute first
33
+ */
34
+ const PRIORITY_BASE = {
35
+ /** Page middleware base priority */
36
+ pages: 50,
37
+ } as const;
38
+
39
+ /**
40
+ * Discovers route-scoped middleware files in the project
41
+ *
42
+ * Scans for _middleware.ts files in:
43
+ * - src/pages/ directory (and subdirectories) for page routes
44
+ *
45
+ * API middleware is handled natively by Nitro and is not scanned here.
46
+ *
47
+ * Middleware files are sorted by priority (directory depth), ensuring
48
+ * parent middleware executes before child middleware.
49
+ *
50
+ * @param options - Discovery options
51
+ * @returns Array of discovered middleware routes, sorted by priority
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * const routes = await discoverScopedMiddleware({
56
+ * baseDir: 'src',
57
+ * devMode: true,
58
+ * });
59
+ *
60
+ * // Returns routes like:
61
+ * // [
62
+ * // { pattern: URLPattern('/blog/*'), filePath: 'src/pages/blog/_middleware.ts', priority: 51, type: 'pages' },
63
+ * // ]
64
+ * ```
65
+ */
66
+ export async function discoverScopedMiddleware(
67
+ options: MiddlewareDiscoveryOptions
68
+ ): Promise<MiddlewareRoute[]> {
69
+ const {
70
+ baseDir,
71
+ filePattern = DEFAULT_OPTIONS.filePattern,
72
+ excludeDirs = DEFAULT_OPTIONS.excludeDirs,
73
+ devMode = DEFAULT_OPTIONS.devMode,
74
+ } = options;
75
+
76
+ const resolvedBaseDir = resolve(baseDir);
77
+ const routes: MiddlewareRoute[] = [];
78
+
79
+ // Scan pages directory for page middleware
80
+ const pagesDir = join(resolvedBaseDir, 'pages');
81
+ await scanDirectory(pagesDir, 'pages', filePattern, excludeDirs, routes, devMode);
82
+
83
+ // Sort by priority (lower numbers execute first)
84
+ routes.sort((a, b) => a.priority - b.priority);
85
+
86
+ if (devMode && routes.length > 0) {
87
+ console.log(`[middleware] Discovered ${routes.length} route-scoped middleware:`);
88
+ for (const route of routes) {
89
+ console.log(` - ${route.type}: ${route.filePath} (priority: ${route.priority})`);
90
+ }
91
+ }
92
+
93
+ return routes;
94
+ }
95
+
96
+ /**
97
+ * Scans a directory recursively for middleware files
98
+ *
99
+ * @param dir - Directory to scan
100
+ * @param type - Middleware type ('pages')
101
+ * @param filePattern - File pattern to match
102
+ * @param excludeDirs - Directories to exclude
103
+ * @param routes - Array to collect discovered routes
104
+ * @param devMode - Whether to log debug information
105
+ */
106
+ async function scanDirectory(
107
+ dir: string,
108
+ type: 'pages',
109
+ filePattern: string,
110
+ excludeDirs: string[],
111
+ routes: MiddlewareRoute[],
112
+ devMode: boolean
113
+ ): Promise<void> {
114
+ try {
115
+ // Check if directory exists
116
+ const dirInfo = await fsStat(dir).catch(() => null);
117
+ if (!dirInfo?.isDirectory()) {
118
+ return;
119
+ }
120
+
121
+ // Recursively scan directory
122
+ await scanDirectoryRecursive(dir, dir, type, filePattern, excludeDirs, routes, devMode);
123
+ } catch (error) {
124
+ // Directory doesn't exist or can't be read - skip silently
125
+ if (devMode && (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== 'ENOENT')) {
126
+ console.warn(`[middleware] Error scanning ${dir}: ${error instanceof Error ? error.message : String(error)}`);
127
+ }
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Recursively scans a directory for middleware files
133
+ *
134
+ * @param rootDir - Root directory being scanned
135
+ * @param currentDir - Current directory being scanned
136
+ * @param type - Middleware type
137
+ * @param filePattern - File pattern to match
138
+ * @param excludeDirs - Directories to exclude
139
+ * @param routes - Array to collect discovered routes
140
+ * @param devMode - Whether to log debug information
141
+ */
142
+ async function scanDirectoryRecursive(
143
+ rootDir: string,
144
+ currentDir: string,
145
+ type: 'pages',
146
+ filePattern: string,
147
+ excludeDirs: string[],
148
+ routes: MiddlewareRoute[],
149
+ devMode: boolean
150
+ ): Promise<void> {
151
+ try {
152
+ const entries = await readdir(currentDir, { withFileTypes: true });
153
+ for (const entry of entries) {
154
+ const fullPath = join(currentDir, entry.name);
155
+
156
+ // Skip excluded directories
157
+ if (entry.isDirectory() && excludeDirs.includes(entry.name)) {
158
+ continue;
159
+ }
160
+
161
+ if (entry.isDirectory()) {
162
+ // Recursively scan subdirectories
163
+ await scanDirectoryRecursive(rootDir, fullPath, type, filePattern, excludeDirs, routes, devMode);
164
+ } else if (entry.name === filePattern) {
165
+ // Found a middleware file
166
+ const relativePath = relative(rootDir, currentDir);
167
+ const route = createMiddlewareRoute(fullPath, relativePath, type);
168
+ routes.push(route);
169
+ }
170
+ }
171
+ } catch (error) {
172
+ if (devMode && (!(error instanceof Error) || (error as NodeJS.ErrnoException).code !== 'ENOENT')) {
173
+ console.warn(`[middleware] Error reading ${currentDir}: ${error instanceof Error ? error.message : String(error)}`);
174
+ }
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Creates a middleware route configuration from a file path
180
+ *
181
+ * @param filePath - Absolute path to the middleware file
182
+ * @param relativePath - Path relative to the pages directory
183
+ * @param type - Middleware type
184
+ * @returns Middleware route configuration
185
+ *
186
+ * @example
187
+ * ```ts
188
+ * // For src/pages/blog/_middleware.ts
189
+ * createMiddlewareRoute('/abs/path/src/pages/blog/_middleware.ts', 'blog', 'pages')
190
+ * // Returns: { pattern: URLPattern('/blog/*'), filePath: '...', priority: 51, type: 'pages' }
191
+ * ```
192
+ */
193
+ function createMiddlewareRoute(
194
+ filePath: string,
195
+ relativePath: string,
196
+ type: 'pages'
197
+ ): MiddlewareRoute {
198
+ // Calculate URL pattern from relative path
199
+ // relativePath is the directory path relative to pages/
200
+ // e.g., 'blog' for src/pages/blog/_middleware.ts
201
+ // e.g., '' for src/pages/_middleware.ts (root middleware)
202
+
203
+ // Page middleware: /* or /blog{/*}?
204
+ // Use {/*}? to match both /blog and /blog/anything
205
+ const urlPattern = relativePath ? `/${relativePath}{/*}?` : '/*';
206
+
207
+ // Calculate priority based on directory depth
208
+ // Shallower directories have lower priority (execute first)
209
+ const depth = relativePath ? relativePath.split('/').filter(Boolean).length : 0;
210
+ const priority = PRIORITY_BASE[type] + depth;
211
+
212
+ return {
213
+ pattern: new URLPattern({ pathname: urlPattern }),
214
+ filePath,
215
+ priority,
216
+ type,
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Gets middleware routes that match a given URL
222
+ *
223
+ * Filters discovered routes to only include those that match the URL,
224
+ * respecting type isolation (page middleware doesn't run for API routes
225
+ * and vice versa).
226
+ *
227
+ * @param routes - All discovered middleware routes
228
+ * @param url - URL to match against
229
+ * @returns Matching routes, sorted by priority
230
+ *
231
+ * @example
232
+ * ```ts
233
+ * const allRoutes = await discoverScopedMiddleware({ baseDir: 'src' });
234
+ * const matchingRoutes = getMatchingMiddleware(allRoutes, new URL('http://localhost/blog/post-1'));
235
+ * ```
236
+ */
237
+ export function getMatchingMiddleware(
238
+ routes: MiddlewareRoute[],
239
+ url: URL
240
+ ): MiddlewareRoute[] {
241
+ const isApiRoute = url.pathname.startsWith('/api');
242
+
243
+ return routes.filter(route => {
244
+ // Check if pattern matches the URL
245
+ if (!route.pattern.test(url)) {
246
+ return false;
247
+ }
248
+
249
+ // Type isolation: page middleware doesn't run for API routes
250
+ if (route.type === 'pages' && isApiRoute) {
251
+ return false;
252
+ }
253
+
254
+ return true;
255
+ });
256
+ }
257
+
258
+ /**
259
+ * Clears the middleware discovery cache
260
+ *
261
+ * This is a no-op in the simplified discovery module since we don't
262
+ * maintain a persistent cache. The function is provided for API
263
+ * compatibility with the executor module.
264
+ */
265
+ export function clearDiscoveryCache(): void {
266
+ // No-op: simplified discovery doesn't maintain a persistent cache
267
+ // Each call to discoverScopedMiddleware scans the file system
268
+ }