@flight-framework/core 0.0.2 → 0.0.3
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.
|
@@ -128,12 +128,17 @@ function convertToRoutePath(name) {
|
|
|
128
128
|
}
|
|
129
129
|
return name;
|
|
130
130
|
}
|
|
131
|
-
async function loadRoutes(scanResult) {
|
|
131
|
+
async function loadRoutes(scanResult, moduleLoader) {
|
|
132
132
|
const loadedRoutes = [];
|
|
133
133
|
for (const route of scanResult.routes) {
|
|
134
134
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
let module;
|
|
136
|
+
if (moduleLoader) {
|
|
137
|
+
module = await moduleLoader(route.filePath);
|
|
138
|
+
} else {
|
|
139
|
+
const fileUrl = pathToFileURL(route.filePath).href;
|
|
140
|
+
module = await import(fileUrl);
|
|
141
|
+
}
|
|
137
142
|
if (route.type === "page") {
|
|
138
143
|
const component = module.default;
|
|
139
144
|
const meta = module.meta || module.metadata || {};
|
|
@@ -181,12 +186,13 @@ async function loadRoutes(scanResult) {
|
|
|
181
186
|
}
|
|
182
187
|
async function createFileRouter(options) {
|
|
183
188
|
let routes = [];
|
|
189
|
+
const { moduleLoader } = options;
|
|
184
190
|
async function refresh() {
|
|
185
191
|
const scanResult = await scanRoutes(options);
|
|
186
192
|
if (scanResult.errors.length > 0) {
|
|
187
193
|
console.warn("[Flight] Route scan errors:", scanResult.errors);
|
|
188
194
|
}
|
|
189
|
-
routes = await loadRoutes(scanResult);
|
|
195
|
+
routes = await loadRoutes(scanResult, moduleLoader);
|
|
190
196
|
console.log(`[Flight] Loaded ${routes.length} routes from ${options.directory}`);
|
|
191
197
|
}
|
|
192
198
|
await refresh();
|
|
@@ -199,5 +205,5 @@ async function createFileRouter(options) {
|
|
|
199
205
|
}
|
|
200
206
|
|
|
201
207
|
export { createFileRouter, loadRoutes, scanRoutes };
|
|
202
|
-
//# sourceMappingURL=chunk-
|
|
203
|
-
//# sourceMappingURL=chunk-
|
|
208
|
+
//# sourceMappingURL=chunk-CLMFEKYM.js.map
|
|
209
|
+
//# sourceMappingURL=chunk-CLMFEKYM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/file-router/index.ts"],"names":[],"mappings":"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","file":"chunk-CLMFEKYM.js","sourcesContent":["/**\r\n * @flight-framework/core - File Router\r\n * \r\n * Auto-discovery of routes from file system.\r\n * Similar to Next.js App Router and Nuxt server/api patterns.\r\n */\r\n\r\nimport { readdir } from 'node:fs/promises';\r\nimport { join, basename, extname } from 'node:path';\r\nimport { pathToFileURL } from 'node:url';\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';\r\n\r\nexport type Handler = (context: unknown) => Response | Promise<Response>;\r\nexport type Middleware = (context: unknown, next: () => Promise<Response>) => Response | Promise<Response>;\r\n\r\nexport interface FileRoute {\r\n /** HTTP method (GET, POST, etc) or 'ALL' */\r\n method: HttpMethod | 'ALL';\r\n /** Route path with params (e.g., /users/:id) */\r\n path: string;\r\n /** Original file path */\r\n filePath: string;\r\n /** Handler function (for APIs) */\r\n handler?: Handler;\r\n /** Route-specific middleware */\r\n middleware?: Middleware[];\r\n /** Route type: 'page' for SSR pages, 'api' for API endpoints */\r\n type: 'page' | 'api';\r\n /** Component function (for pages) */\r\n component?: () => unknown;\r\n /** Page metadata (title, description, etc) */\r\n meta?: Record<string, unknown>;\r\n /** Parallel route slot name (for @folder convention) */\r\n slot?: string;\r\n}\r\n\r\nexport interface FileRouterOptions {\r\n /** Root directory to scan (default: src/routes) */\r\n directory: string;\r\n /** File extensions to consider (default: ['.ts', '.js']) */\r\n extensions?: string[];\r\n /** Whether to watch for changes (default: false in prod) */\r\n watch?: boolean;\r\n /** \r\n * Custom module loader for development with Vite.\r\n * Pass vite.ssrLoadModule to load TSX files correctly.\r\n * Falls back to native import() if not provided.\r\n */\r\n moduleLoader?: (filePath: string) => Promise<any>;\r\n}\r\n\r\nexport interface ScanResult {\r\n routes: FileRoute[];\r\n errors: string[];\r\n}\r\n\r\n// ============================================================================\r\n// File Scanner\r\n// ============================================================================\r\n\r\n/**\r\n * Scan a directory for route files\r\n */\r\nexport async function scanRoutes(options: FileRouterOptions): Promise<ScanResult> {\r\n const {\r\n directory,\r\n extensions = ['.ts', '.js'],\r\n } = options;\r\n\r\n const routes: FileRoute[] = [];\r\n const errors: string[] = [];\r\n\r\n async function scanDir(dir: string, basePath: string = ''): Promise<void> {\r\n try {\r\n const entries = await readdir(dir, { withFileTypes: true });\r\n\r\n for (const entry of entries) {\r\n const fullPath = join(dir, entry.name);\r\n\r\n if (entry.isDirectory()) {\r\n // Skip hidden directories and node_modules\r\n if (entry.name.startsWith('.') || entry.name === 'node_modules') {\r\n continue;\r\n }\r\n\r\n // Detect parallel route slots (@folder convention)\r\n if (entry.name.startsWith('@')) {\r\n const slotName = entry.name.slice(1);\r\n // Scan slot directory and mark routes with slot name\r\n const slotRoutes = await scanSlotDir(fullPath, basePath, slotName, extensions);\r\n routes.push(...slotRoutes);\r\n continue;\r\n }\r\n\r\n // Recurse into subdirectory\r\n await scanDir(fullPath, `${basePath}/${entry.name}`);\r\n } else if (entry.isFile()) {\r\n const ext = extname(entry.name);\r\n\r\n if (!extensions.includes(ext)) {\r\n continue;\r\n }\r\n\r\n // Parse route from filename\r\n const routeInfo = parseRouteFile(entry.name, basePath);\r\n\r\n if (routeInfo && routeInfo.method && routeInfo.path) {\r\n routes.push({\r\n method: routeInfo.method,\r\n path: routeInfo.path,\r\n filePath: fullPath,\r\n type: routeInfo.type,\r\n });\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n errors.push(`Failed to scan ${dir}: ${error}`);\r\n }\r\n }\r\n\r\n await scanDir(directory);\r\n\r\n return { routes, errors };\r\n}\r\n\r\n/**\r\n * Scan a parallel route slot directory\r\n */\r\nasync function scanSlotDir(\r\n dir: string,\r\n basePath: string,\r\n slotName: string,\r\n extensions: string[] = ['.ts', '.js', '.tsx', '.jsx']\r\n): Promise<FileRoute[]> {\r\n const routes: FileRoute[] = [];\r\n\r\n try {\r\n const entries = await readdir(dir, { withFileTypes: true });\r\n\r\n for (const entry of entries) {\r\n const fullPath = join(dir, entry.name);\r\n\r\n if (entry.isFile()) {\r\n const ext = extname(entry.name);\r\n\r\n if (!extensions.includes(ext)) {\r\n continue;\r\n }\r\n\r\n const routeInfo = parseRouteFile(entry.name, basePath);\r\n\r\n if (routeInfo && routeInfo.method && routeInfo.path) {\r\n routes.push({\r\n method: routeInfo.method,\r\n path: routeInfo.path,\r\n filePath: fullPath,\r\n type: routeInfo.type,\r\n slot: slotName,\r\n });\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n console.error(`[Flight] Failed to scan slot @${slotName}:`, error);\r\n }\r\n\r\n return routes;\r\n}\r\n\r\n// ============================================================================\r\n// Route Parser\r\n// ============================================================================\r\n\r\ninterface ParsedRoute {\r\n method: HttpMethod | 'ALL';\r\n path: string;\r\n type: 'page' | 'api';\r\n}\r\n\r\n/**\r\n * Parse route information from filename and path\r\n * \r\n * Patterns:\r\n * - index.ts → /\r\n * - users.ts → /users\r\n * - users.get.ts → GET /users (API)\r\n * - users.post.ts → POST /users (API)\r\n * - about.page.tsx → GET /about (Page)\r\n * - blog/[slug].page.tsx → GET /blog/:slug (Page)\r\n * - [id].ts → /:id\r\n * - [...slug].ts → /* (catch-all)\r\n * - [[...slug]].ts → /* (optional catch-all)\r\n */\r\nfunction parseRouteFile(filename: string, basePath: string): ParsedRoute | null {\r\n const ext = extname(filename);\r\n const nameWithoutExt = basename(filename, ext);\r\n\r\n // Skip files starting with underscore (private)\r\n if (nameWithoutExt.startsWith('_')) {\r\n return null;\r\n }\r\n\r\n // Detect route type\r\n let type: 'page' | 'api' = 'api';\r\n let routeName = nameWithoutExt;\r\n let method: HttpMethod | 'ALL' = 'ALL';\r\n\r\n // Check for page suffix (e.g., about.page.tsx, index.page.tsx)\r\n const pageMatch = nameWithoutExt.match(/^(.+)?\\.page$/i);\r\n if (pageMatch) {\r\n type = 'page';\r\n method = 'GET'; // Pages are always GET\r\n routeName = pageMatch[1] || 'index';\r\n } else {\r\n // Check for method suffix (e.g., users.get.ts)\r\n const methodMatch = nameWithoutExt.match(/^(.+)\\.(get|post|put|delete|patch|head|options)$/i);\r\n if (methodMatch) {\r\n routeName = methodMatch[1] || nameWithoutExt;\r\n method = (methodMatch[2] || 'ALL').toUpperCase() as HttpMethod;\r\n }\r\n }\r\n\r\n // Also check if file is in /api/ directory\r\n if (basePath.startsWith('/api') || basePath.includes('/api/')) {\r\n type = 'api';\r\n }\r\n\r\n // Build route path\r\n let path = basePath;\r\n\r\n if (routeName !== 'index') {\r\n path = `${basePath}/${convertToRoutePath(routeName)}`;\r\n }\r\n\r\n // Ensure path starts with /\r\n if (!path.startsWith('/')) {\r\n path = '/' + path;\r\n }\r\n\r\n // Remove trailing slash (except for root)\r\n if (path !== '/' && path.endsWith('/')) {\r\n path = path.slice(0, -1);\r\n }\r\n\r\n return { method, path, type };\r\n}\r\n\r\n/**\r\n * Convert filename segment to route path segment\r\n * \r\n * - [id] → :id\r\n * - [...slug] → *\r\n * - [[...slug]] → *?\r\n */\r\nfunction convertToRoutePath(name: string): string {\r\n // Optional catch-all: [[...slug]]\r\n if (name.startsWith('[[...') && name.endsWith(']]')) {\r\n const paramName = name.slice(5, -2);\r\n return `:${paramName}*`;\r\n }\r\n\r\n // Catch-all: [...slug]\r\n if (name.startsWith('[...') && name.endsWith(']')) {\r\n const paramName = name.slice(4, -1);\r\n return `:${paramName}+`;\r\n }\r\n\r\n // Dynamic param: [id]\r\n if (name.startsWith('[') && name.endsWith(']')) {\r\n const paramName = name.slice(1, -1);\r\n return `:${paramName}`;\r\n }\r\n\r\n return name;\r\n}\r\n\r\n// ============================================================================\r\n// Route Loader\r\n// ============================================================================\r\n\r\n/**\r\n * Load routes with their handlers or components\r\n * @param scanResult - Result from scanRoutes\r\n * @param moduleLoader - Optional custom loader (use vite.ssrLoadModule for dev)\r\n */\r\nexport async function loadRoutes(\r\n scanResult: ScanResult,\r\n moduleLoader?: (filePath: string) => Promise<any>\r\n): Promise<FileRoute[]> {\r\n const loadedRoutes: FileRoute[] = [];\r\n\r\n for (const route of scanResult.routes) {\r\n try {\r\n // Use custom loader if provided (Vite ssrLoadModule for TSX)\r\n // Otherwise fall back to native import() for production\r\n let module: any;\r\n if (moduleLoader) {\r\n module = await moduleLoader(route.filePath);\r\n } else {\r\n // Convert to file:// URL for Windows ESM compatibility\r\n const fileUrl = pathToFileURL(route.filePath).href;\r\n module = await import(fileUrl);\r\n }\r\n\r\n // Handle PAGE routes\r\n if (route.type === 'page') {\r\n // Pages export default component\r\n const component = module.default;\r\n const meta = module.meta || module.metadata || {};\r\n\r\n if (component) {\r\n loadedRoutes.push({\r\n ...route,\r\n component,\r\n meta,\r\n });\r\n }\r\n continue;\r\n }\r\n\r\n // Handle API routes\r\n if (route.method === 'ALL') {\r\n // Look for named exports: GET, POST, PUT, DELETE, etc\r\n const methods: HttpMethod[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];\r\n\r\n for (const method of methods) {\r\n if (typeof module[method] === 'function') {\r\n loadedRoutes.push({\r\n ...route,\r\n method,\r\n handler: module[method],\r\n });\r\n }\r\n }\r\n\r\n // Also check for default export as handler\r\n if (typeof module.default === 'function') {\r\n loadedRoutes.push({\r\n ...route,\r\n method: 'ALL',\r\n handler: module.default,\r\n });\r\n }\r\n } else {\r\n // Specific method from filename suffix\r\n const handler = module[route.method] || module.default;\r\n\r\n if (typeof handler === 'function') {\r\n loadedRoutes.push({\r\n ...route,\r\n handler,\r\n });\r\n }\r\n }\r\n } catch (error) {\r\n console.error(`[Flight] Failed to load route ${route.filePath}:`, error);\r\n }\r\n }\r\n\r\n return loadedRoutes;\r\n}\r\n\r\n// ============================================================================\r\n// File Router Factory\r\n// ============================================================================\r\n\r\nexport interface FileRouter {\r\n routes: FileRoute[];\r\n refresh: () => Promise<void>;\r\n}\r\n\r\n/**\r\n * Create a file-based router\r\n * \r\n * @example\r\n * ```typescript\r\n * import { createFileRouter } from '@flight-framework/core/file-router';\r\n * import { createServer } from '@flight-framework/http';\r\n * \r\n * const router = await createFileRouter({ directory: './src/routes' });\r\n * const app = createServer();\r\n * \r\n * // Register all discovered routes\r\n * for (const route of router.routes) {\r\n * app[route.method.toLowerCase()](route.path, route.handler);\r\n * }\r\n * ```\r\n */\r\nexport async function createFileRouter(options: FileRouterOptions): Promise<FileRouter> {\r\n let routes: FileRoute[] = [];\r\n const { moduleLoader } = options;\r\n\r\n async function refresh(): Promise<void> {\r\n const scanResult = await scanRoutes(options);\r\n\r\n if (scanResult.errors.length > 0) {\r\n console.warn('[Flight] Route scan errors:', scanResult.errors);\r\n }\r\n\r\n routes = await loadRoutes(scanResult, moduleLoader);\r\n\r\n console.log(`[Flight] Loaded ${routes.length} routes from ${options.directory}`);\r\n }\r\n\r\n // Initial load\r\n await refresh();\r\n\r\n return {\r\n get routes() {\r\n return routes;\r\n },\r\n refresh,\r\n };\r\n}\r\n"]}
|
|
@@ -34,6 +34,12 @@ interface FileRouterOptions {
|
|
|
34
34
|
extensions?: string[];
|
|
35
35
|
/** Whether to watch for changes (default: false in prod) */
|
|
36
36
|
watch?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Custom module loader for development with Vite.
|
|
39
|
+
* Pass vite.ssrLoadModule to load TSX files correctly.
|
|
40
|
+
* Falls back to native import() if not provided.
|
|
41
|
+
*/
|
|
42
|
+
moduleLoader?: (filePath: string) => Promise<any>;
|
|
37
43
|
}
|
|
38
44
|
interface ScanResult {
|
|
39
45
|
routes: FileRoute[];
|
|
@@ -45,8 +51,10 @@ interface ScanResult {
|
|
|
45
51
|
declare function scanRoutes(options: FileRouterOptions): Promise<ScanResult>;
|
|
46
52
|
/**
|
|
47
53
|
* Load routes with their handlers or components
|
|
54
|
+
* @param scanResult - Result from scanRoutes
|
|
55
|
+
* @param moduleLoader - Optional custom loader (use vite.ssrLoadModule for dev)
|
|
48
56
|
*/
|
|
49
|
-
declare function loadRoutes(scanResult: ScanResult): Promise<FileRoute[]>;
|
|
57
|
+
declare function loadRoutes(scanResult: ScanResult, moduleLoader?: (filePath: string) => Promise<any>): Promise<FileRoute[]>;
|
|
50
58
|
interface FileRouter {
|
|
51
59
|
routes: FileRoute[];
|
|
52
60
|
refresh: () => Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createFileRouter, loadRoutes, scanRoutes } from './chunk-
|
|
1
|
+
export { createFileRouter, loadRoutes, scanRoutes } from './chunk-CLMFEKYM.js';
|
|
2
2
|
export { RedirectError, redirect as actionRedirect, cookies, executeAction, executeFormAction, getAction, handleActionRequest, isRedirectError, parseFormData, registerAction } from './chunk-JIW55ZVD.js';
|
|
3
3
|
export { createRouteContext, error, json, parseBody, redirect } from './chunk-W6D62JCI.js';
|
|
4
4
|
export { createLazyContent, createStreamingResponse, createStreamingSSR, renderWithStreaming, streamParallel, streamSequential } from './chunk-ABNCAPQB.js';
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/file-router/index.ts"],"names":[],"mappings":"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","file":"chunk-WOMWIW7D.js","sourcesContent":["/**\r\n * @flight-framework/core - File Router\r\n * \r\n * Auto-discovery of routes from file system.\r\n * Similar to Next.js App Router and Nuxt server/api patterns.\r\n */\r\n\r\nimport { readdir } from 'node:fs/promises';\r\nimport { join, basename, extname } from 'node:path';\r\nimport { pathToFileURL } from 'node:url';\r\n\r\n// ============================================================================\r\n// Types\r\n// ============================================================================\r\n\r\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';\r\n\r\nexport type Handler = (context: unknown) => Response | Promise<Response>;\r\nexport type Middleware = (context: unknown, next: () => Promise<Response>) => Response | Promise<Response>;\r\n\r\nexport interface FileRoute {\r\n /** HTTP method (GET, POST, etc) or 'ALL' */\r\n method: HttpMethod | 'ALL';\r\n /** Route path with params (e.g., /users/:id) */\r\n path: string;\r\n /** Original file path */\r\n filePath: string;\r\n /** Handler function (for APIs) */\r\n handler?: Handler;\r\n /** Route-specific middleware */\r\n middleware?: Middleware[];\r\n /** Route type: 'page' for SSR pages, 'api' for API endpoints */\r\n type: 'page' | 'api';\r\n /** Component function (for pages) */\r\n component?: () => unknown;\r\n /** Page metadata (title, description, etc) */\r\n meta?: Record<string, unknown>;\r\n /** Parallel route slot name (for @folder convention) */\r\n slot?: string;\r\n}\r\n\r\nexport interface FileRouterOptions {\r\n /** Root directory to scan (default: src/routes) */\r\n directory: string;\r\n /** File extensions to consider (default: ['.ts', '.js']) */\r\n extensions?: string[];\r\n /** Whether to watch for changes (default: false in prod) */\r\n watch?: boolean;\r\n}\r\n\r\nexport interface ScanResult {\r\n routes: FileRoute[];\r\n errors: string[];\r\n}\r\n\r\n// ============================================================================\r\n// File Scanner\r\n// ============================================================================\r\n\r\n/**\r\n * Scan a directory for route files\r\n */\r\nexport async function scanRoutes(options: FileRouterOptions): Promise<ScanResult> {\r\n const {\r\n directory,\r\n extensions = ['.ts', '.js'],\r\n } = options;\r\n\r\n const routes: FileRoute[] = [];\r\n const errors: string[] = [];\r\n\r\n async function scanDir(dir: string, basePath: string = ''): Promise<void> {\r\n try {\r\n const entries = await readdir(dir, { withFileTypes: true });\r\n\r\n for (const entry of entries) {\r\n const fullPath = join(dir, entry.name);\r\n\r\n if (entry.isDirectory()) {\r\n // Skip hidden directories and node_modules\r\n if (entry.name.startsWith('.') || entry.name === 'node_modules') {\r\n continue;\r\n }\r\n\r\n // Detect parallel route slots (@folder convention)\r\n if (entry.name.startsWith('@')) {\r\n const slotName = entry.name.slice(1);\r\n // Scan slot directory and mark routes with slot name\r\n const slotRoutes = await scanSlotDir(fullPath, basePath, slotName, extensions);\r\n routes.push(...slotRoutes);\r\n continue;\r\n }\r\n\r\n // Recurse into subdirectory\r\n await scanDir(fullPath, `${basePath}/${entry.name}`);\r\n } else if (entry.isFile()) {\r\n const ext = extname(entry.name);\r\n\r\n if (!extensions.includes(ext)) {\r\n continue;\r\n }\r\n\r\n // Parse route from filename\r\n const routeInfo = parseRouteFile(entry.name, basePath);\r\n\r\n if (routeInfo && routeInfo.method && routeInfo.path) {\r\n routes.push({\r\n method: routeInfo.method,\r\n path: routeInfo.path,\r\n filePath: fullPath,\r\n type: routeInfo.type,\r\n });\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n errors.push(`Failed to scan ${dir}: ${error}`);\r\n }\r\n }\r\n\r\n await scanDir(directory);\r\n\r\n return { routes, errors };\r\n}\r\n\r\n/**\r\n * Scan a parallel route slot directory\r\n */\r\nasync function scanSlotDir(\r\n dir: string,\r\n basePath: string,\r\n slotName: string,\r\n extensions: string[] = ['.ts', '.js', '.tsx', '.jsx']\r\n): Promise<FileRoute[]> {\r\n const routes: FileRoute[] = [];\r\n\r\n try {\r\n const entries = await readdir(dir, { withFileTypes: true });\r\n\r\n for (const entry of entries) {\r\n const fullPath = join(dir, entry.name);\r\n\r\n if (entry.isFile()) {\r\n const ext = extname(entry.name);\r\n\r\n if (!extensions.includes(ext)) {\r\n continue;\r\n }\r\n\r\n const routeInfo = parseRouteFile(entry.name, basePath);\r\n\r\n if (routeInfo && routeInfo.method && routeInfo.path) {\r\n routes.push({\r\n method: routeInfo.method,\r\n path: routeInfo.path,\r\n filePath: fullPath,\r\n type: routeInfo.type,\r\n slot: slotName,\r\n });\r\n }\r\n }\r\n }\r\n } catch (error) {\r\n console.error(`[Flight] Failed to scan slot @${slotName}:`, error);\r\n }\r\n\r\n return routes;\r\n}\r\n\r\n// ============================================================================\r\n// Route Parser\r\n// ============================================================================\r\n\r\ninterface ParsedRoute {\r\n method: HttpMethod | 'ALL';\r\n path: string;\r\n type: 'page' | 'api';\r\n}\r\n\r\n/**\r\n * Parse route information from filename and path\r\n * \r\n * Patterns:\r\n * - index.ts → /\r\n * - users.ts → /users\r\n * - users.get.ts → GET /users (API)\r\n * - users.post.ts → POST /users (API)\r\n * - about.page.tsx → GET /about (Page)\r\n * - blog/[slug].page.tsx → GET /blog/:slug (Page)\r\n * - [id].ts → /:id\r\n * - [...slug].ts → /* (catch-all)\r\n * - [[...slug]].ts → /* (optional catch-all)\r\n */\r\nfunction parseRouteFile(filename: string, basePath: string): ParsedRoute | null {\r\n const ext = extname(filename);\r\n const nameWithoutExt = basename(filename, ext);\r\n\r\n // Skip files starting with underscore (private)\r\n if (nameWithoutExt.startsWith('_')) {\r\n return null;\r\n }\r\n\r\n // Detect route type\r\n let type: 'page' | 'api' = 'api';\r\n let routeName = nameWithoutExt;\r\n let method: HttpMethod | 'ALL' = 'ALL';\r\n\r\n // Check for page suffix (e.g., about.page.tsx, index.page.tsx)\r\n const pageMatch = nameWithoutExt.match(/^(.+)?\\.page$/i);\r\n if (pageMatch) {\r\n type = 'page';\r\n method = 'GET'; // Pages are always GET\r\n routeName = pageMatch[1] || 'index';\r\n } else {\r\n // Check for method suffix (e.g., users.get.ts)\r\n const methodMatch = nameWithoutExt.match(/^(.+)\\.(get|post|put|delete|patch|head|options)$/i);\r\n if (methodMatch) {\r\n routeName = methodMatch[1] || nameWithoutExt;\r\n method = (methodMatch[2] || 'ALL').toUpperCase() as HttpMethod;\r\n }\r\n }\r\n\r\n // Also check if file is in /api/ directory\r\n if (basePath.startsWith('/api') || basePath.includes('/api/')) {\r\n type = 'api';\r\n }\r\n\r\n // Build route path\r\n let path = basePath;\r\n\r\n if (routeName !== 'index') {\r\n path = `${basePath}/${convertToRoutePath(routeName)}`;\r\n }\r\n\r\n // Ensure path starts with /\r\n if (!path.startsWith('/')) {\r\n path = '/' + path;\r\n }\r\n\r\n // Remove trailing slash (except for root)\r\n if (path !== '/' && path.endsWith('/')) {\r\n path = path.slice(0, -1);\r\n }\r\n\r\n return { method, path, type };\r\n}\r\n\r\n/**\r\n * Convert filename segment to route path segment\r\n * \r\n * - [id] → :id\r\n * - [...slug] → *\r\n * - [[...slug]] → *?\r\n */\r\nfunction convertToRoutePath(name: string): string {\r\n // Optional catch-all: [[...slug]]\r\n if (name.startsWith('[[...') && name.endsWith(']]')) {\r\n const paramName = name.slice(5, -2);\r\n return `:${paramName}*`;\r\n }\r\n\r\n // Catch-all: [...slug]\r\n if (name.startsWith('[...') && name.endsWith(']')) {\r\n const paramName = name.slice(4, -1);\r\n return `:${paramName}+`;\r\n }\r\n\r\n // Dynamic param: [id]\r\n if (name.startsWith('[') && name.endsWith(']')) {\r\n const paramName = name.slice(1, -1);\r\n return `:${paramName}`;\r\n }\r\n\r\n return name;\r\n}\r\n\r\n// ============================================================================\r\n// Route Loader\r\n// ============================================================================\r\n\r\n/**\r\n * Load routes with their handlers or components\r\n */\r\nexport async function loadRoutes(scanResult: ScanResult): Promise<FileRoute[]> {\r\n const loadedRoutes: FileRoute[] = [];\r\n\r\n for (const route of scanResult.routes) {\r\n try {\r\n // Dynamic import of route file\r\n // Convert to file:// URL for Windows ESM compatibility\r\n const fileUrl = pathToFileURL(route.filePath).href;\r\n const module = await import(fileUrl);\r\n\r\n // Handle PAGE routes\r\n if (route.type === 'page') {\r\n // Pages export default component\r\n const component = module.default;\r\n const meta = module.meta || module.metadata || {};\r\n\r\n if (component) {\r\n loadedRoutes.push({\r\n ...route,\r\n component,\r\n meta,\r\n });\r\n }\r\n continue;\r\n }\r\n\r\n // Handle API routes\r\n if (route.method === 'ALL') {\r\n // Look for named exports: GET, POST, PUT, DELETE, etc\r\n const methods: HttpMethod[] = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];\r\n\r\n for (const method of methods) {\r\n if (typeof module[method] === 'function') {\r\n loadedRoutes.push({\r\n ...route,\r\n method,\r\n handler: module[method],\r\n });\r\n }\r\n }\r\n\r\n // Also check for default export as handler\r\n if (typeof module.default === 'function') {\r\n loadedRoutes.push({\r\n ...route,\r\n method: 'ALL',\r\n handler: module.default,\r\n });\r\n }\r\n } else {\r\n // Specific method from filename suffix\r\n const handler = module[route.method] || module.default;\r\n\r\n if (typeof handler === 'function') {\r\n loadedRoutes.push({\r\n ...route,\r\n handler,\r\n });\r\n }\r\n }\r\n } catch (error) {\r\n console.error(`[Flight] Failed to load route ${route.filePath}:`, error);\r\n }\r\n }\r\n\r\n return loadedRoutes;\r\n}\r\n\r\n// ============================================================================\r\n// File Router Factory\r\n// ============================================================================\r\n\r\nexport interface FileRouter {\r\n routes: FileRoute[];\r\n refresh: () => Promise<void>;\r\n}\r\n\r\n/**\r\n * Create a file-based router\r\n * \r\n * @example\r\n * ```typescript\r\n * import { createFileRouter } from '@flight-framework/core/file-router';\r\n * import { createServer } from '@flight-framework/http';\r\n * \r\n * const router = await createFileRouter({ directory: './src/routes' });\r\n * const app = createServer();\r\n * \r\n * // Register all discovered routes\r\n * for (const route of router.routes) {\r\n * app[route.method.toLowerCase()](route.path, route.handler);\r\n * }\r\n * ```\r\n */\r\nexport async function createFileRouter(options: FileRouterOptions): Promise<FileRouter> {\r\n let routes: FileRoute[] = [];\r\n\r\n async function refresh(): Promise<void> {\r\n const scanResult = await scanRoutes(options);\r\n\r\n if (scanResult.errors.length > 0) {\r\n console.warn('[Flight] Route scan errors:', scanResult.errors);\r\n }\r\n\r\n routes = await loadRoutes(scanResult);\r\n\r\n console.log(`[Flight] Loaded ${routes.length} routes from ${options.directory}`);\r\n }\r\n\r\n // Initial load\r\n await refresh();\r\n\r\n return {\r\n get routes() {\r\n return routes;\r\n },\r\n refresh,\r\n };\r\n}\r\n"]}
|