@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
- const fileUrl = pathToFileURL(route.filePath).href;
136
- const module = await import(fileUrl);
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-WOMWIW7D.js.map
203
- //# sourceMappingURL=chunk-WOMWIW7D.js.map
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>;
@@ -1,3 +1,3 @@
1
- export { createFileRouter, loadRoutes, scanRoutes } from '../chunk-WOMWIW7D.js';
1
+ export { createFileRouter, loadRoutes, scanRoutes } from '../chunk-CLMFEKYM.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- export { createFileRouter, loadRoutes, scanRoutes } from './chunk-WOMWIW7D.js';
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@flight-framework/core",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Core primitives for Flight Framework - routing, rendering, caching",
5
5
  "keywords": [
6
6
  "flight",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/file-router/index.ts"],"names":[],"mappings":";;;;;AA8DA,eAAsB,WAAW,OAAA,EAAiD;AAC9E,EAAA,MAAM;AAAA,IACF,SAAA;AAAA,IACA,UAAA,GAAa,CAAC,KAAA,EAAO,KAAK;AAAA,GAC9B,GAAI,OAAA;AAEJ,EAAA,MAAM,SAAsB,EAAC;AAC7B,EAAA,MAAM,SAAmB,EAAC;AAE1B,EAAA,eAAe,OAAA,CAAQ,GAAA,EAAa,QAAA,GAAmB,EAAA,EAAmB;AACtE,IAAA,IAAI;AACA,MAAA,MAAM,UAAU,MAAM,OAAA,CAAQ,KAAK,EAAE,aAAA,EAAe,MAAM,CAAA;AAE1D,MAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AACzB,QAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,EAAK,KAAA,CAAM,IAAI,CAAA;AAErC,QAAA,IAAI,KAAA,CAAM,aAAY,EAAG;AAErB,UAAA,IAAI,MAAM,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,IAAK,KAAA,CAAM,SAAS,cAAA,EAAgB;AAC7D,YAAA;AAAA,UACJ;AAGA,UAAA,IAAI,KAAA,CAAM,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,EAAG;AAC5B,YAAA,MAAM,QAAA,GAAW,KAAA,CAAM,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA;AAEnC,YAAA,MAAM,aAAa,MAAM,WAAA,CAAY,QAAA,EAAU,QAAA,EAAU,UAAU,UAAU,CAAA;AAC7E,YAAA,MAAA,CAAO,IAAA,CAAK,GAAG,UAAU,CAAA;AACzB,YAAA;AAAA,UACJ;AAGA,UAAA,MAAM,QAAQ,QAAA,EAAU,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,KAAA,CAAM,IAAI,CAAA,CAAE,CAAA;AAAA,QACvD,CAAA,MAAA,IAAW,KAAA,CAAM,MAAA,EAAO,EAAG;AACvB,UAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAA;AAE9B,UAAA,IAAI,CAAC,UAAA,CAAW,QAAA,CAAS,GAAG,CAAA,EAAG;AAC3B,YAAA;AAAA,UACJ;AAGA,UAAA,MAAM,SAAA,GAAY,cAAA,CAAe,KAAA,CAAM,IAAA,EAAM,QAAQ,CAAA;AAErD,UAAA,IAAI,SAAA,IAAa,SAAA,CAAU,MAAA,IAAU,SAAA,CAAU,IAAA,EAAM;AACjD,YAAA,MAAA,CAAO,IAAA,CAAK;AAAA,cACR,QAAQ,SAAA,CAAU,MAAA;AAAA,cAClB,MAAM,SAAA,CAAU,IAAA;AAAA,cAChB,QAAA,EAAU,QAAA;AAAA,cACV,MAAM,SAAA,CAAU;AAAA,aACnB,CAAA;AAAA,UACL;AAAA,QACJ;AAAA,MACJ;AAAA,IACJ,SAAS,KAAA,EAAO;AACZ,MAAA,MAAA,CAAO,IAAA,CAAK,CAAA,eAAA,EAAkB,GAAG,CAAA,EAAA,EAAK,KAAK,CAAA,CAAE,CAAA;AAAA,IACjD;AAAA,EACJ;AAEA,EAAA,MAAM,QAAQ,SAAS,CAAA;AAEvB,EAAA,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC5B;AAKA,eAAe,WAAA,CACX,GAAA,EACA,QAAA,EACA,QAAA,EACA,UAAA,GAAuB,CAAC,KAAA,EAAO,KAAA,EAAO,MAAA,EAAQ,MAAM,CAAA,EAChC;AACpB,EAAA,MAAM,SAAsB,EAAC;AAE7B,EAAA,IAAI;AACA,IAAA,MAAM,UAAU,MAAM,OAAA,CAAQ,KAAK,EAAE,aAAA,EAAe,MAAM,CAAA;AAE1D,IAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AACzB,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,EAAK,KAAA,CAAM,IAAI,CAAA;AAErC,MAAA,IAAI,KAAA,CAAM,QAAO,EAAG;AAChB,QAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAA;AAE9B,QAAA,IAAI,CAAC,UAAA,CAAW,QAAA,CAAS,GAAG,CAAA,EAAG;AAC3B,UAAA;AAAA,QACJ;AAEA,QAAA,MAAM,SAAA,GAAY,cAAA,CAAe,KAAA,CAAM,IAAA,EAAM,QAAQ,CAAA;AAErD,QAAA,IAAI,SAAA,IAAa,SAAA,CAAU,MAAA,IAAU,SAAA,CAAU,IAAA,EAAM;AACjD,UAAA,MAAA,CAAO,IAAA,CAAK;AAAA,YACR,QAAQ,SAAA,CAAU,MAAA;AAAA,YAClB,MAAM,SAAA,CAAU,IAAA;AAAA,YAChB,QAAA,EAAU,QAAA;AAAA,YACV,MAAM,SAAA,CAAU,IAAA;AAAA,YAChB,IAAA,EAAM;AAAA,WACT,CAAA;AAAA,QACL;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ,SAAS,KAAA,EAAO;AACZ,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,8BAAA,EAAiC,QAAQ,CAAA,CAAA,CAAA,EAAK,KAAK,CAAA;AAAA,EACrE;AAEA,EAAA,OAAO,MAAA;AACX;AA0BA,SAAS,cAAA,CAAe,UAAkB,QAAA,EAAsC;AAC5E,EAAA,MAAM,GAAA,GAAM,QAAQ,QAAQ,CAAA;AAC5B,EAAA,MAAM,cAAA,GAAiB,QAAA,CAAS,QAAA,EAAU,GAAG,CAAA;AAG7C,EAAA,IAAI,cAAA,CAAe,UAAA,CAAW,GAAG,CAAA,EAAG;AAChC,IAAA,OAAO,IAAA;AAAA,EACX;AAGA,EAAA,IAAI,IAAA,GAAuB,KAAA;AAC3B,EAAA,IAAI,SAAA,GAAY,cAAA;AAChB,EAAA,IAAI,MAAA,GAA6B,KAAA;AAGjC,EAAA,MAAM,SAAA,GAAY,cAAA,CAAe,KAAA,CAAM,gBAAgB,CAAA;AACvD,EAAA,IAAI,SAAA,EAAW;AACX,IAAA,IAAA,GAAO,MAAA;AACP,IAAA,MAAA,GAAS,KAAA;AACT,IAAA,SAAA,GAAY,SAAA,CAAU,CAAC,CAAA,IAAK,OAAA;AAAA,EAChC,CAAA,MAAO;AAEH,IAAA,MAAM,WAAA,GAAc,cAAA,CAAe,KAAA,CAAM,mDAAmD,CAAA;AAC5F,IAAA,IAAI,WAAA,EAAa;AACb,MAAA,SAAA,GAAY,WAAA,CAAY,CAAC,CAAA,IAAK,cAAA;AAC9B,MAAA,MAAA,GAAA,CAAU,WAAA,CAAY,CAAC,CAAA,IAAK,KAAA,EAAO,WAAA,EAAY;AAAA,IACnD;AAAA,EACJ;AAGA,EAAA,IAAI,SAAS,UAAA,CAAW,MAAM,KAAK,QAAA,CAAS,QAAA,CAAS,OAAO,CAAA,EAAG;AAC3D,IAAA,IAAA,GAAO,KAAA;AAAA,EACX;AAGA,EAAA,IAAI,IAAA,GAAO,QAAA;AAEX,EAAA,IAAI,cAAc,OAAA,EAAS;AACvB,IAAA,IAAA,GAAO,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,kBAAA,CAAmB,SAAS,CAAC,CAAA,CAAA;AAAA,EACvD;AAGA,EAAA,IAAI,CAAC,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,EAAG;AACvB,IAAA,IAAA,GAAO,GAAA,GAAM,IAAA;AAAA,EACjB;AAGA,EAAA,IAAI,IAAA,KAAS,GAAA,IAAO,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,EAAG;AACpC,IAAA,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAAA,EAC3B;AAEA,EAAA,OAAO,EAAE,MAAA,EAAQ,IAAA,EAAM,IAAA,EAAK;AAChC;AASA,SAAS,mBAAmB,IAAA,EAAsB;AAE9C,EAAA,IAAI,KAAK,UAAA,CAAW,OAAO,KAAK,IAAA,CAAK,QAAA,CAAS,IAAI,CAAA,EAAG;AACjD,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAClC,IAAA,OAAO,IAAI,SAAS,CAAA,CAAA,CAAA;AAAA,EACxB;AAGA,EAAA,IAAI,KAAK,UAAA,CAAW,MAAM,KAAK,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,EAAG;AAC/C,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAClC,IAAA,OAAO,IAAI,SAAS,CAAA,CAAA,CAAA;AAAA,EACxB;AAGA,EAAA,IAAI,KAAK,UAAA,CAAW,GAAG,KAAK,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,EAAG;AAC5C,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAClC,IAAA,OAAO,IAAI,SAAS,CAAA,CAAA;AAAA,EACxB;AAEA,EAAA,OAAO,IAAA;AACX;AASA,eAAsB,WAAW,UAAA,EAA8C;AAC3E,EAAA,MAAM,eAA4B,EAAC;AAEnC,EAAA,KAAA,MAAW,KAAA,IAAS,WAAW,MAAA,EAAQ;AACnC,IAAA,IAAI;AAGA,MAAA,MAAM,OAAA,GAAU,aAAA,CAAc,KAAA,CAAM,QAAQ,CAAA,CAAE,IAAA;AAC9C,MAAA,MAAM,MAAA,GAAS,MAAM,OAAO,OAAA,CAAA;AAG5B,MAAA,IAAI,KAAA,CAAM,SAAS,MAAA,EAAQ;AAEvB,QAAA,MAAM,YAAY,MAAA,CAAO,OAAA;AACzB,QAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,IAAQ,MAAA,CAAO,YAAY,EAAC;AAEhD,QAAA,IAAI,SAAA,EAAW;AACX,UAAA,YAAA,CAAa,IAAA,CAAK;AAAA,YACd,GAAG,KAAA;AAAA,YACH,SAAA;AAAA,YACA;AAAA,WACH,CAAA;AAAA,QACL;AACA,QAAA;AAAA,MACJ;AAGA,MAAA,IAAI,KAAA,CAAM,WAAW,KAAA,EAAO;AAExB,QAAA,MAAM,OAAA,GAAwB,CAAC,KAAA,EAAO,MAAA,EAAQ,OAAO,QAAA,EAAU,OAAA,EAAS,QAAQ,SAAS,CAAA;AAEzF,QAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC1B,UAAA,IAAI,OAAO,MAAA,CAAO,MAAM,CAAA,KAAM,UAAA,EAAY;AACtC,YAAA,YAAA,CAAa,IAAA,CAAK;AAAA,cACd,GAAG,KAAA;AAAA,cACH,MAAA;AAAA,cACA,OAAA,EAAS,OAAO,MAAM;AAAA,aACzB,CAAA;AAAA,UACL;AAAA,QACJ;AAGA,QAAA,IAAI,OAAO,MAAA,CAAO,OAAA,KAAY,UAAA,EAAY;AACtC,UAAA,YAAA,CAAa,IAAA,CAAK;AAAA,YACd,GAAG,KAAA;AAAA,YACH,MAAA,EAAQ,KAAA;AAAA,YACR,SAAS,MAAA,CAAO;AAAA,WACnB,CAAA;AAAA,QACL;AAAA,MACJ,CAAA,MAAO;AAEH,QAAA,MAAM,OAAA,GAAU,MAAA,CAAO,KAAA,CAAM,MAAM,KAAK,MAAA,CAAO,OAAA;AAE/C,QAAA,IAAI,OAAO,YAAY,UAAA,EAAY;AAC/B,UAAA,YAAA,CAAa,IAAA,CAAK;AAAA,YACd,GAAG,KAAA;AAAA,YACH;AAAA,WACH,CAAA;AAAA,QACL;AAAA,MACJ;AAAA,IACJ,SAAS,KAAA,EAAO;AACZ,MAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,8BAAA,EAAiC,KAAA,CAAM,QAAQ,KAAK,KAAK,CAAA;AAAA,IAC3E;AAAA,EACJ;AAEA,EAAA,OAAO,YAAA;AACX;AA4BA,eAAsB,iBAAiB,OAAA,EAAiD;AACpF,EAAA,IAAI,SAAsB,EAAC;AAE3B,EAAA,eAAe,OAAA,GAAyB;AACpC,IAAA,MAAM,UAAA,GAAa,MAAM,UAAA,CAAW,OAAO,CAAA;AAE3C,IAAA,IAAI,UAAA,CAAW,MAAA,CAAO,MAAA,GAAS,CAAA,EAAG;AAC9B,MAAA,OAAA,CAAQ,IAAA,CAAK,6BAAA,EAA+B,UAAA,CAAW,MAAM,CAAA;AAAA,IACjE;AAEA,IAAA,MAAA,GAAS,MAAM,WAAW,UAAU,CAAA;AAEpC,IAAA,OAAA,CAAQ,IAAI,CAAA,gBAAA,EAAmB,MAAA,CAAO,MAAM,CAAA,aAAA,EAAgB,OAAA,CAAQ,SAAS,CAAA,CAAE,CAAA;AAAA,EACnF;AAGA,EAAA,MAAM,OAAA,EAAQ;AAEd,EAAA,OAAO;AAAA,IACH,IAAI,MAAA,GAAS;AACT,MAAA,OAAO,MAAA;AAAA,IACX,CAAA;AAAA,IACA;AAAA,GACJ;AACJ","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"]}