@useavalon/avalon 0.1.11 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -54
- package/mod.ts +302 -302
- package/package.json +49 -26
- package/src/build/integration-bundler-plugin.ts +116 -116
- package/src/build/integration-config.ts +168 -168
- package/src/build/integration-detection-plugin.ts +117 -117
- package/src/build/integration-resolver-plugin.ts +90 -90
- package/src/build/island-manifest.ts +269 -269
- package/src/build/island-types-generator.ts +476 -476
- package/src/build/mdx-island-transform.ts +464 -464
- package/src/build/mdx-plugin.ts +98 -98
- package/src/build/page-island-transform.ts +598 -598
- package/src/build/prop-extractors/index.ts +21 -21
- package/src/build/prop-extractors/lit.ts +140 -140
- package/src/build/prop-extractors/qwik.ts +16 -16
- package/src/build/prop-extractors/solid.ts +125 -125
- package/src/build/prop-extractors/svelte.ts +194 -194
- package/src/build/prop-extractors/vue.ts +111 -111
- package/src/build/sidecar-file-manager.ts +104 -104
- package/src/build/sidecar-renderer.ts +30 -30
- package/src/client/adapters/index.ts +21 -13
- package/src/client/components.ts +35 -35
- package/src/client/css-hmr-handler.ts +344 -344
- package/src/client/framework-adapter.ts +462 -462
- package/src/client/hmr-coordinator.ts +396 -396
- package/src/client/hmr-error-overlay.js +533 -533
- package/src/client/main.js +824 -816
- package/src/client/types/framework-runtime.d.ts +68 -68
- package/src/client/types/vite-hmr.d.ts +46 -46
- package/src/client/types/vite-virtual-modules.d.ts +70 -60
- package/src/components/Image.tsx +123 -123
- package/src/components/IslandErrorBoundary.tsx +145 -145
- package/src/components/LayoutDataErrorBoundary.tsx +141 -141
- package/src/components/LayoutErrorBoundary.tsx +127 -127
- package/src/components/PersistentIsland.tsx +52 -52
- package/src/components/StreamingErrorBoundary.tsx +233 -233
- package/src/components/StreamingLayout.tsx +538 -538
- package/src/core/components/component-analyzer.ts +192 -192
- package/src/core/components/component-detection.ts +508 -508
- package/src/core/components/enhanced-framework-detector.ts +500 -500
- package/src/core/components/framework-registry.ts +563 -563
- package/src/core/content/mdx-processor.ts +46 -46
- package/src/core/integrations/index.ts +19 -19
- package/src/core/integrations/loader.ts +125 -125
- package/src/core/integrations/registry.ts +175 -175
- package/src/core/islands/island-persistence.ts +325 -325
- package/src/core/islands/island-state-serializer.ts +258 -258
- package/src/core/islands/persistent-island-context.tsx +80 -80
- package/src/core/islands/use-persistent-state.ts +68 -68
- package/src/core/layout/enhanced-layout-resolver.ts +322 -322
- package/src/core/layout/layout-cache-manager.ts +485 -485
- package/src/core/layout/layout-composer.ts +357 -357
- package/src/core/layout/layout-data-loader.ts +516 -516
- package/src/core/layout/layout-discovery.ts +243 -243
- package/src/core/layout/layout-matcher.ts +299 -299
- package/src/core/layout/layout-types.ts +110 -110
- package/src/core/modules/framework-module-resolver.ts +273 -273
- package/src/islands/component-analysis.ts +213 -213
- package/src/islands/css-utils.ts +565 -565
- package/src/islands/discovery/index.ts +80 -80
- package/src/islands/discovery/registry.ts +340 -340
- package/src/islands/discovery/resolver.ts +477 -477
- package/src/islands/discovery/scanner.ts +386 -386
- package/src/islands/discovery/types.ts +117 -117
- package/src/islands/discovery/validator.ts +544 -544
- package/src/islands/discovery/watcher.ts +368 -368
- package/src/islands/framework-detection.ts +428 -428
- package/src/islands/integration-loader.ts +490 -490
- package/src/islands/island.tsx +565 -565
- package/src/islands/render-cache.ts +550 -550
- package/src/islands/types.ts +80 -80
- package/src/islands/universal-css-collector.ts +157 -157
- package/src/islands/universal-head-collector.ts +137 -137
- package/src/layout-system.d.ts +592 -592
- package/src/layout-system.ts +218 -218
- package/src/middleware/discovery.ts +268 -268
- package/src/middleware/executor.ts +315 -315
- package/src/middleware/index.ts +76 -76
- package/src/middleware/types.ts +99 -99
- package/src/nitro/build-config.ts +575 -575
- package/src/nitro/config.ts +483 -483
- package/src/nitro/error-handler.ts +636 -636
- package/src/nitro/index.ts +173 -173
- package/src/nitro/island-manifest.ts +584 -584
- package/src/nitro/middleware-adapter.ts +260 -260
- package/src/nitro/renderer.ts +1471 -1471
- package/src/nitro/route-discovery.ts +439 -439
- package/src/nitro/types.ts +321 -321
- package/src/render/collect-css.ts +198 -198
- package/src/render/error-pages.ts +79 -79
- package/src/render/isolated-ssr-renderer.ts +654 -654
- package/src/render/ssr.ts +1030 -1030
- package/src/schemas/api.ts +30 -30
- package/src/schemas/core.ts +64 -64
- package/src/schemas/index.ts +212 -212
- package/src/schemas/layout.ts +279 -279
- package/src/schemas/routing/index.ts +38 -38
- package/src/schemas/routing.ts +376 -376
- package/src/types/as-island.ts +20 -20
- package/src/types/image.d.ts +106 -106
- package/src/types/index.d.ts +22 -22
- package/src/types/island-jsx.d.ts +33 -33
- package/src/types/island-prop.d.ts +20 -20
- package/src/types/layout.ts +285 -285
- package/src/types/mdx.d.ts +6 -6
- package/src/types/routing.ts +555 -555
- package/src/types/types.ts +5 -5
- package/src/types/urlpattern.d.ts +49 -49
- package/src/types/vite-env.d.ts +11 -11
- package/src/utils/dev-logger.ts +299 -299
- package/src/utils/fs.ts +151 -151
- package/src/vite-plugin/auto-discover.ts +551 -551
- package/src/vite-plugin/config.ts +266 -266
- package/src/vite-plugin/errors.ts +127 -127
- package/src/vite-plugin/image-optimization.ts +156 -156
- package/src/vite-plugin/integration-activator.ts +126 -126
- package/src/vite-plugin/island-sidecar-plugin.ts +176 -176
- package/src/vite-plugin/module-discovery.ts +189 -189
- package/src/vite-plugin/nitro-integration.ts +1354 -1354
- package/src/vite-plugin/plugin.ts +403 -409
- package/src/vite-plugin/types.ts +327 -327
- package/src/vite-plugin/validation.ts +228 -228
- package/src/client/adapters/index.js +0 -12
- package/src/client/adapters/lit-adapter.js +0 -467
- package/src/client/adapters/lit-adapter.ts +0 -654
- package/src/client/adapters/preact-adapter.js +0 -223
- package/src/client/adapters/preact-adapter.ts +0 -331
- package/src/client/adapters/qwik-adapter.js +0 -259
- package/src/client/adapters/qwik-adapter.ts +0 -345
- package/src/client/adapters/react-adapter.js +0 -220
- package/src/client/adapters/react-adapter.ts +0 -353
- package/src/client/adapters/solid-adapter.js +0 -295
- package/src/client/adapters/solid-adapter.ts +0 -451
- package/src/client/adapters/svelte-adapter.js +0 -368
- package/src/client/adapters/svelte-adapter.ts +0 -524
- package/src/client/adapters/vue-adapter.js +0 -278
- package/src/client/adapters/vue-adapter.ts +0 -467
- package/src/client/components.js +0 -23
- package/src/client/css-hmr-handler.js +0 -263
- package/src/client/framework-adapter.js +0 -283
- package/src/client/hmr-coordinator.js +0 -274
|
@@ -1,315 +1,315 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simplified Middleware Executor
|
|
3
|
-
*
|
|
4
|
-
* This module provides a streamlined middleware execution system that aligns
|
|
5
|
-
* with Nitro's conventions while supporting Avalon's route-scoped middleware.
|
|
6
|
-
*
|
|
7
|
-
* Key features:
|
|
8
|
-
* - Nitro-style handler signature: return void to continue, return Response to terminate
|
|
9
|
-
* - Middleware caching for performance
|
|
10
|
-
|
|
11
|
-
* - Error propagation to Nitro's error handling
|
|
12
|
-
* - Context preservation: event.context modifications persist through the chain
|
|
13
|
-
* - Development logging for context changes
|
|
14
|
-
*
|
|
15
|
-
* Requirements: 1.2, 1.3, 1.4, 2.1, 2.2
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { H3Event } from 'h3';
|
|
19
|
-
import type { MiddlewareHandler, MiddlewareRoute, MiddlewareFileExport, MiddlewareExecutorOptions } from './types.ts';
|
|
20
|
-
import { getMatchingMiddleware } from './discovery.ts';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Captures a snapshot of the event context for comparison
|
|
24
|
-
* Used in development mode to log context changes
|
|
25
|
-
*/
|
|
26
|
-
function captureContextSnapshot(context: Record<string, unknown>): Map<string, unknown> {
|
|
27
|
-
const snapshot = new Map<string, unknown>();
|
|
28
|
-
for (const key of Object.keys(context)) {
|
|
29
|
-
snapshot.set(key, context[key]);
|
|
30
|
-
}
|
|
31
|
-
return snapshot;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Compares two context snapshots and returns the changes
|
|
36
|
-
* Used in development mode to log context modifications
|
|
37
|
-
*/
|
|
38
|
-
function getContextChanges(
|
|
39
|
-
before: Map<string, unknown>,
|
|
40
|
-
after: Record<string, unknown>,
|
|
41
|
-
): { added: string[]; modified: string[]; removed: string[] } {
|
|
42
|
-
const added: string[] = [];
|
|
43
|
-
const modified: string[] = [];
|
|
44
|
-
const removed: string[] = [];
|
|
45
|
-
|
|
46
|
-
for (const key of Object.keys(after)) {
|
|
47
|
-
if (!before.has(key)) {
|
|
48
|
-
added.push(key);
|
|
49
|
-
} else if (before.get(key) !== after[key]) {
|
|
50
|
-
modified.push(key);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
for (const key of before.keys()) {
|
|
55
|
-
if (!(key in after)) {
|
|
56
|
-
removed.push(key);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return { added, modified, removed };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Logs context changes in development mode
|
|
65
|
-
* Requirements: 2.1, 2.2
|
|
66
|
-
*
|
|
67
|
-
* Note: Intentionally minimal to reduce log noise.
|
|
68
|
-
* Uncomment the body for debugging middleware context issues.
|
|
69
|
-
*/
|
|
70
|
-
function logContextChanges(
|
|
71
|
-
_filePath: string,
|
|
72
|
-
_changes: { added: string[]; modified: string[]; removed: string[] },
|
|
73
|
-
): void {
|
|
74
|
-
// Uncomment for debugging middleware context issues:
|
|
75
|
-
// const hasChanges = _changes.added.length > 0 || _changes.modified.length > 0 || _changes.removed.length > 0;
|
|
76
|
-
// if (!hasChanges) return;
|
|
77
|
-
// console.log(`[middleware] Context changes in ${_filePath}: ...`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Default executor options */
|
|
81
|
-
const DEFAULT_OPTIONS: Required<MiddlewareExecutorOptions> = {
|
|
82
|
-
devMode: false,
|
|
83
|
-
timeout: 30000,
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
/** Middleware cache: maps file paths to loaded handlers */
|
|
87
|
-
const middlewareCache = new Map<string, MiddlewareHandler>();
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Executes a single middleware route handler with context tracking.
|
|
91
|
-
* Returns the Response if the chain should terminate, undefined to continue.
|
|
92
|
-
*/
|
|
93
|
-
async function executeMiddlewareRoute(
|
|
94
|
-
event: H3Event,
|
|
95
|
-
route: MiddlewareRoute,
|
|
96
|
-
devMode: boolean,
|
|
97
|
-
timeout: number,
|
|
98
|
-
): Promise<Response | undefined> {
|
|
99
|
-
const handler = await loadMiddleware(route.filePath, devMode);
|
|
100
|
-
if (!handler) return undefined;
|
|
101
|
-
|
|
102
|
-
const contextBefore = devMode ? captureContextSnapshot(event.context) : null;
|
|
103
|
-
|
|
104
|
-
const result = await executeWithTimeout(() => handler(event), timeout, route.filePath);
|
|
105
|
-
|
|
106
|
-
if (devMode && contextBefore) {
|
|
107
|
-
const changes = getContextChanges(contextBefore, event.context);
|
|
108
|
-
logContextChanges(route.filePath, changes);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const response = handleMiddlewareResult(result, route.filePath, devMode);
|
|
112
|
-
if (response && devMode) {
|
|
113
|
-
console.log(`[middleware] Chain terminated by ${route.filePath}`);
|
|
114
|
-
}
|
|
115
|
-
return response;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Executes route-scoped middleware chain for a request
|
|
120
|
-
*
|
|
121
|
-
* 1. Finds middleware that matches the request URL
|
|
122
|
-
* 2. Executes them in priority order (parent before child)
|
|
123
|
-
* 3. Handles void return (continue) and Response return (terminate)
|
|
124
|
-
* 4. Propagates errors to Nitro's error handling
|
|
125
|
-
* 5. Preserves event.context modifications across the chain
|
|
126
|
-
* 6. Logs context changes in development mode
|
|
127
|
-
*
|
|
128
|
-
* @param event - H3 event object from Nitro
|
|
129
|
-
* @param routes - Discovered middleware routes from discoverScopedMiddleware
|
|
130
|
-
* @param options - Execution options
|
|
131
|
-
* @returns Response if middleware terminated the chain, undefined otherwise
|
|
132
|
-
*/
|
|
133
|
-
export async function executeScopedMiddleware(
|
|
134
|
-
event: H3Event,
|
|
135
|
-
routes: MiddlewareRoute[],
|
|
136
|
-
options: MiddlewareExecutorOptions = {},
|
|
137
|
-
): Promise<Response | undefined> {
|
|
138
|
-
const { devMode, timeout } = { ...DEFAULT_OPTIONS, ...options };
|
|
139
|
-
|
|
140
|
-
const url = buildUrlFromEvent(event);
|
|
141
|
-
const matchingRoutes = getMatchingMiddleware(routes, url);
|
|
142
|
-
|
|
143
|
-
if (matchingRoutes.length === 0) {
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
for (const route of matchingRoutes) {
|
|
148
|
-
try {
|
|
149
|
-
const response = await executeMiddlewareRoute(event, route, devMode, timeout);
|
|
150
|
-
if (response) return response;
|
|
151
|
-
} catch (error) {
|
|
152
|
-
if (devMode) {
|
|
153
|
-
console.error(`[middleware] Error in ${route.filePath}:`, error);
|
|
154
|
-
}
|
|
155
|
-
throw error;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return undefined;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Builds a URL object from an H3 event.
|
|
164
|
-
* Supports both h3 v2 (event.url / event.req.url) and the dev-mode mock shape (event.path).
|
|
165
|
-
*/
|
|
166
|
-
function buildUrlFromEvent(event: H3Event): URL {
|
|
167
|
-
const base = 'http://localhost';
|
|
168
|
-
|
|
169
|
-
// h3 v2: event.url is a string
|
|
170
|
-
if (typeof (event as any).url === 'string') {
|
|
171
|
-
return new URL((event as any).url, base);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// h3 v2: event.req is a Web Request
|
|
175
|
-
if ((event as any).req?.url) {
|
|
176
|
-
return new URL((event as any).req.url, base);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Dev-mode mock: event.path
|
|
180
|
-
if ((event as any).path) {
|
|
181
|
-
return new URL((event as any).path, base);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Legacy h3 v1: event.node.req.url
|
|
185
|
-
if ((event as any).node?.req?.url) {
|
|
186
|
-
return new URL((event as any).node.req.url, base);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return new URL('/', base);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Converts an absolute file path to a valid ESM import specifier.
|
|
194
|
-
* Windows absolute paths (C:\...) are converted to file:// URLs.
|
|
195
|
-
* On Unix, the path is returned as-is (no-op).
|
|
196
|
-
*/
|
|
197
|
-
export function toImportSpecifier(filePath: string): string {
|
|
198
|
-
if (/^[A-Za-z]:[\\/]/.test(filePath)) {
|
|
199
|
-
return `file:///${filePath.replaceAll('\\', '/')}`;
|
|
200
|
-
}
|
|
201
|
-
return filePath;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Loads a middleware handler via Vite's ssrLoadModule (dev) or dynamic import (prod/worker).
|
|
206
|
-
*/
|
|
207
|
-
async function loadMiddleware(filePath: string, devMode: boolean): Promise<MiddlewareHandler | null> {
|
|
208
|
-
if (!devMode && middlewareCache.has(filePath)) {
|
|
209
|
-
return middlewareCache.get(filePath)!;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
let module: MiddlewareFileExport;
|
|
214
|
-
|
|
215
|
-
const viteServer = globalThis.__viteDevServer;
|
|
216
|
-
if (devMode && viteServer) {
|
|
217
|
-
const viteRoot = viteServer.config.root || '';
|
|
218
|
-
const vitePath = filePath.startsWith(viteRoot) ? '/' + filePath.slice(viteRoot.length + 1) : filePath;
|
|
219
|
-
module = (await viteServer.ssrLoadModule(vitePath)) as MiddlewareFileExport;
|
|
220
|
-
} else {
|
|
221
|
-
const importPath = toImportSpecifier(filePath);
|
|
222
|
-
module = (await import(/* @vite-ignore */ importPath)) as MiddlewareFileExport;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (!module.default || typeof module.default !== 'function') {
|
|
226
|
-
if (devMode) {
|
|
227
|
-
console.warn(`[middleware] ${filePath} does not export a default function`);
|
|
228
|
-
}
|
|
229
|
-
return null;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (!devMode) {
|
|
233
|
-
middlewareCache.set(filePath, module.default);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return module.default;
|
|
237
|
-
} catch (error) {
|
|
238
|
-
if (devMode) {
|
|
239
|
-
console.error(`[middleware] Failed to load ${filePath}:`, error);
|
|
240
|
-
}
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Executes a middleware handler with a timeout
|
|
247
|
-
*/
|
|
248
|
-
async function executeWithTimeout<T>(fn: () => T | Promise<T>, timeout: number, filePath: string): Promise<T> {
|
|
249
|
-
return Promise.race([
|
|
250
|
-
Promise.resolve(fn()),
|
|
251
|
-
new Promise<never>((_, reject) => {
|
|
252
|
-
setTimeout(() => {
|
|
253
|
-
reject(new Error(`Middleware timeout after ${timeout}ms: ${filePath}`));
|
|
254
|
-
}, timeout);
|
|
255
|
-
}),
|
|
256
|
-
]);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Handles the result of a middleware execution.
|
|
261
|
-
* Supports Nitro-style (void/Response) format.
|
|
262
|
-
*/
|
|
263
|
-
function handleMiddlewareResult(result: unknown, filePath: string, devMode: boolean): Response | undefined {
|
|
264
|
-
if (result === undefined || result === null) {
|
|
265
|
-
return undefined;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (result instanceof Response) {
|
|
269
|
-
return result;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (devMode) {
|
|
273
|
-
console.warn(
|
|
274
|
-
`[middleware] ${filePath} returned unexpected value: ${typeof result}. ` +
|
|
275
|
-
`Expected void or Response.`,
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return undefined;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/** Clears the middleware cache (e.g. during hot reload) */
|
|
283
|
-
export function clearMiddlewareCache(): void {
|
|
284
|
-
middlewareCache.clear();
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/** Removes a specific middleware from the cache */
|
|
288
|
-
export function invalidateMiddleware(filePath: string): boolean {
|
|
289
|
-
return middlewareCache.delete(filePath);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/** Gets the current size of the middleware cache */
|
|
293
|
-
export function getMiddlewareCacheSize(): number {
|
|
294
|
-
return middlewareCache.size;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** Checks if a key exists in event.context */
|
|
298
|
-
export function hasContextValue(event: H3Event, key: string): boolean {
|
|
299
|
-
return key in event.context;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/** Gets a typed value from event.context */
|
|
303
|
-
export function getContextValue<T>(event: H3Event, key: string): T | undefined {
|
|
304
|
-
return event.context[key] as T | undefined;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** Sets a value in event.context with optional development logging */
|
|
308
|
-
export function setContextValue<T>(event: H3Event, key: string, value: T, devMode: boolean = false): void {
|
|
309
|
-
const isNew = !(key in event.context);
|
|
310
|
-
event.context[key] = value;
|
|
311
|
-
|
|
312
|
-
if (devMode) {
|
|
313
|
-
console.log(`[middleware] Context ${isNew ? 'set' : 'updated'}: ${key}`);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Simplified Middleware Executor
|
|
3
|
+
*
|
|
4
|
+
* This module provides a streamlined middleware execution system that aligns
|
|
5
|
+
* with Nitro's conventions while supporting Avalon's route-scoped middleware.
|
|
6
|
+
*
|
|
7
|
+
* Key features:
|
|
8
|
+
* - Nitro-style handler signature: return void to continue, return Response to terminate
|
|
9
|
+
* - Middleware caching for performance
|
|
10
|
+
|
|
11
|
+
* - Error propagation to Nitro's error handling
|
|
12
|
+
* - Context preservation: event.context modifications persist through the chain
|
|
13
|
+
* - Development logging for context changes
|
|
14
|
+
*
|
|
15
|
+
* Requirements: 1.2, 1.3, 1.4, 2.1, 2.2
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { H3Event } from 'h3';
|
|
19
|
+
import type { MiddlewareHandler, MiddlewareRoute, MiddlewareFileExport, MiddlewareExecutorOptions } from './types.ts';
|
|
20
|
+
import { getMatchingMiddleware } from './discovery.ts';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Captures a snapshot of the event context for comparison
|
|
24
|
+
* Used in development mode to log context changes
|
|
25
|
+
*/
|
|
26
|
+
function captureContextSnapshot(context: Record<string, unknown>): Map<string, unknown> {
|
|
27
|
+
const snapshot = new Map<string, unknown>();
|
|
28
|
+
for (const key of Object.keys(context)) {
|
|
29
|
+
snapshot.set(key, context[key]);
|
|
30
|
+
}
|
|
31
|
+
return snapshot;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Compares two context snapshots and returns the changes
|
|
36
|
+
* Used in development mode to log context modifications
|
|
37
|
+
*/
|
|
38
|
+
function getContextChanges(
|
|
39
|
+
before: Map<string, unknown>,
|
|
40
|
+
after: Record<string, unknown>,
|
|
41
|
+
): { added: string[]; modified: string[]; removed: string[] } {
|
|
42
|
+
const added: string[] = [];
|
|
43
|
+
const modified: string[] = [];
|
|
44
|
+
const removed: string[] = [];
|
|
45
|
+
|
|
46
|
+
for (const key of Object.keys(after)) {
|
|
47
|
+
if (!before.has(key)) {
|
|
48
|
+
added.push(key);
|
|
49
|
+
} else if (before.get(key) !== after[key]) {
|
|
50
|
+
modified.push(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const key of before.keys()) {
|
|
55
|
+
if (!(key in after)) {
|
|
56
|
+
removed.push(key);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { added, modified, removed };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Logs context changes in development mode
|
|
65
|
+
* Requirements: 2.1, 2.2
|
|
66
|
+
*
|
|
67
|
+
* Note: Intentionally minimal to reduce log noise.
|
|
68
|
+
* Uncomment the body for debugging middleware context issues.
|
|
69
|
+
*/
|
|
70
|
+
function logContextChanges(
|
|
71
|
+
_filePath: string,
|
|
72
|
+
_changes: { added: string[]; modified: string[]; removed: string[] },
|
|
73
|
+
): void {
|
|
74
|
+
// Uncomment for debugging middleware context issues:
|
|
75
|
+
// const hasChanges = _changes.added.length > 0 || _changes.modified.length > 0 || _changes.removed.length > 0;
|
|
76
|
+
// if (!hasChanges) return;
|
|
77
|
+
// console.log(`[middleware] Context changes in ${_filePath}: ...`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Default executor options */
|
|
81
|
+
const DEFAULT_OPTIONS: Required<MiddlewareExecutorOptions> = {
|
|
82
|
+
devMode: false,
|
|
83
|
+
timeout: 30000,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Middleware cache: maps file paths to loaded handlers */
|
|
87
|
+
const middlewareCache = new Map<string, MiddlewareHandler>();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Executes a single middleware route handler with context tracking.
|
|
91
|
+
* Returns the Response if the chain should terminate, undefined to continue.
|
|
92
|
+
*/
|
|
93
|
+
async function executeMiddlewareRoute(
|
|
94
|
+
event: H3Event,
|
|
95
|
+
route: MiddlewareRoute,
|
|
96
|
+
devMode: boolean,
|
|
97
|
+
timeout: number,
|
|
98
|
+
): Promise<Response | undefined> {
|
|
99
|
+
const handler = await loadMiddleware(route.filePath, devMode);
|
|
100
|
+
if (!handler) return undefined;
|
|
101
|
+
|
|
102
|
+
const contextBefore = devMode ? captureContextSnapshot(event.context) : null;
|
|
103
|
+
|
|
104
|
+
const result = await executeWithTimeout(() => handler(event), timeout, route.filePath);
|
|
105
|
+
|
|
106
|
+
if (devMode && contextBefore) {
|
|
107
|
+
const changes = getContextChanges(contextBefore, event.context);
|
|
108
|
+
logContextChanges(route.filePath, changes);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const response = handleMiddlewareResult(result, route.filePath, devMode);
|
|
112
|
+
if (response && devMode) {
|
|
113
|
+
console.log(`[middleware] Chain terminated by ${route.filePath}`);
|
|
114
|
+
}
|
|
115
|
+
return response;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Executes route-scoped middleware chain for a request
|
|
120
|
+
*
|
|
121
|
+
* 1. Finds middleware that matches the request URL
|
|
122
|
+
* 2. Executes them in priority order (parent before child)
|
|
123
|
+
* 3. Handles void return (continue) and Response return (terminate)
|
|
124
|
+
* 4. Propagates errors to Nitro's error handling
|
|
125
|
+
* 5. Preserves event.context modifications across the chain
|
|
126
|
+
* 6. Logs context changes in development mode
|
|
127
|
+
*
|
|
128
|
+
* @param event - H3 event object from Nitro
|
|
129
|
+
* @param routes - Discovered middleware routes from discoverScopedMiddleware
|
|
130
|
+
* @param options - Execution options
|
|
131
|
+
* @returns Response if middleware terminated the chain, undefined otherwise
|
|
132
|
+
*/
|
|
133
|
+
export async function executeScopedMiddleware(
|
|
134
|
+
event: H3Event,
|
|
135
|
+
routes: MiddlewareRoute[],
|
|
136
|
+
options: MiddlewareExecutorOptions = {},
|
|
137
|
+
): Promise<Response | undefined> {
|
|
138
|
+
const { devMode, timeout } = { ...DEFAULT_OPTIONS, ...options };
|
|
139
|
+
|
|
140
|
+
const url = buildUrlFromEvent(event);
|
|
141
|
+
const matchingRoutes = getMatchingMiddleware(routes, url);
|
|
142
|
+
|
|
143
|
+
if (matchingRoutes.length === 0) {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const route of matchingRoutes) {
|
|
148
|
+
try {
|
|
149
|
+
const response = await executeMiddlewareRoute(event, route, devMode, timeout);
|
|
150
|
+
if (response) return response;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (devMode) {
|
|
153
|
+
console.error(`[middleware] Error in ${route.filePath}:`, error);
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Builds a URL object from an H3 event.
|
|
164
|
+
* Supports both h3 v2 (event.url / event.req.url) and the dev-mode mock shape (event.path).
|
|
165
|
+
*/
|
|
166
|
+
function buildUrlFromEvent(event: H3Event): URL {
|
|
167
|
+
const base = 'http://localhost';
|
|
168
|
+
|
|
169
|
+
// h3 v2: event.url is a string
|
|
170
|
+
if (typeof (event as any).url === 'string') {
|
|
171
|
+
return new URL((event as any).url, base);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// h3 v2: event.req is a Web Request
|
|
175
|
+
if ((event as any).req?.url) {
|
|
176
|
+
return new URL((event as any).req.url, base);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Dev-mode mock: event.path
|
|
180
|
+
if ((event as any).path) {
|
|
181
|
+
return new URL((event as any).path, base);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Legacy h3 v1: event.node.req.url
|
|
185
|
+
if ((event as any).node?.req?.url) {
|
|
186
|
+
return new URL((event as any).node.req.url, base);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return new URL('/', base);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Converts an absolute file path to a valid ESM import specifier.
|
|
194
|
+
* Windows absolute paths (C:\...) are converted to file:// URLs.
|
|
195
|
+
* On Unix, the path is returned as-is (no-op).
|
|
196
|
+
*/
|
|
197
|
+
export function toImportSpecifier(filePath: string): string {
|
|
198
|
+
if (/^[A-Za-z]:[\\/]/.test(filePath)) {
|
|
199
|
+
return `file:///${filePath.replaceAll('\\', '/')}`;
|
|
200
|
+
}
|
|
201
|
+
return filePath;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Loads a middleware handler via Vite's ssrLoadModule (dev) or dynamic import (prod/worker).
|
|
206
|
+
*/
|
|
207
|
+
async function loadMiddleware(filePath: string, devMode: boolean): Promise<MiddlewareHandler | null> {
|
|
208
|
+
if (!devMode && middlewareCache.has(filePath)) {
|
|
209
|
+
return middlewareCache.get(filePath)!;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
let module: MiddlewareFileExport;
|
|
214
|
+
|
|
215
|
+
const viteServer = globalThis.__viteDevServer;
|
|
216
|
+
if (devMode && viteServer) {
|
|
217
|
+
const viteRoot = viteServer.config.root || '';
|
|
218
|
+
const vitePath = filePath.startsWith(viteRoot) ? '/' + filePath.slice(viteRoot.length + 1) : filePath;
|
|
219
|
+
module = (await viteServer.ssrLoadModule(vitePath)) as MiddlewareFileExport;
|
|
220
|
+
} else {
|
|
221
|
+
const importPath = toImportSpecifier(filePath);
|
|
222
|
+
module = (await import(/* @vite-ignore */ importPath)) as MiddlewareFileExport;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!module.default || typeof module.default !== 'function') {
|
|
226
|
+
if (devMode) {
|
|
227
|
+
console.warn(`[middleware] ${filePath} does not export a default function`);
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!devMode) {
|
|
233
|
+
middlewareCache.set(filePath, module.default);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return module.default;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (devMode) {
|
|
239
|
+
console.error(`[middleware] Failed to load ${filePath}:`, error);
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Executes a middleware handler with a timeout
|
|
247
|
+
*/
|
|
248
|
+
async function executeWithTimeout<T>(fn: () => T | Promise<T>, timeout: number, filePath: string): Promise<T> {
|
|
249
|
+
return Promise.race([
|
|
250
|
+
Promise.resolve(fn()),
|
|
251
|
+
new Promise<never>((_, reject) => {
|
|
252
|
+
setTimeout(() => {
|
|
253
|
+
reject(new Error(`Middleware timeout after ${timeout}ms: ${filePath}`));
|
|
254
|
+
}, timeout);
|
|
255
|
+
}),
|
|
256
|
+
]);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Handles the result of a middleware execution.
|
|
261
|
+
* Supports Nitro-style (void/Response) format.
|
|
262
|
+
*/
|
|
263
|
+
function handleMiddlewareResult(result: unknown, filePath: string, devMode: boolean): Response | undefined {
|
|
264
|
+
if (result === undefined || result === null) {
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (result instanceof Response) {
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (devMode) {
|
|
273
|
+
console.warn(
|
|
274
|
+
`[middleware] ${filePath} returned unexpected value: ${typeof result}. ` +
|
|
275
|
+
`Expected void or Response.`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Clears the middleware cache (e.g. during hot reload) */
|
|
283
|
+
export function clearMiddlewareCache(): void {
|
|
284
|
+
middlewareCache.clear();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Removes a specific middleware from the cache */
|
|
288
|
+
export function invalidateMiddleware(filePath: string): boolean {
|
|
289
|
+
return middlewareCache.delete(filePath);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Gets the current size of the middleware cache */
|
|
293
|
+
export function getMiddlewareCacheSize(): number {
|
|
294
|
+
return middlewareCache.size;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Checks if a key exists in event.context */
|
|
298
|
+
export function hasContextValue(event: H3Event, key: string): boolean {
|
|
299
|
+
return key in event.context;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Gets a typed value from event.context */
|
|
303
|
+
export function getContextValue<T>(event: H3Event, key: string): T | undefined {
|
|
304
|
+
return event.context[key] as T | undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Sets a value in event.context with optional development logging */
|
|
308
|
+
export function setContextValue<T>(event: H3Event, key: string, value: T, devMode: boolean = false): void {
|
|
309
|
+
const isNew = !(key in event.context);
|
|
310
|
+
event.context[key] = value;
|
|
311
|
+
|
|
312
|
+
if (devMode) {
|
|
313
|
+
console.log(`[middleware] Context ${isNew ? 'set' : 'updated'}: ${key}`);
|
|
314
|
+
}
|
|
315
|
+
}
|