@flight-framework/cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +544 -544
  3. package/dist/bin.js +1433 -951
  4. package/dist/bin.js.map +1 -1
  5. package/dist/index.js +1433 -951
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/templates/angular/index.html +13 -13
  9. package/templates/angular/package.json.template +25 -25
  10. package/templates/angular/src/app.component.ts +13 -13
  11. package/templates/angular/src/main.server.ts +11 -11
  12. package/templates/angular/src/main.ts +4 -4
  13. package/templates/angular/tsconfig.json +16 -16
  14. package/templates/base/README.md.template +26 -26
  15. package/templates/base/_gitignore +25 -25
  16. package/templates/base/flight.config.ts.template +15 -15
  17. package/templates/base/styles/global.css +58 -58
  18. package/templates/htmx/index.html +18 -18
  19. package/templates/htmx/package.json.template +18 -18
  20. package/templates/htmx/vite.config.ts +6 -6
  21. package/templates/lit/index.html +14 -14
  22. package/templates/lit/package.json.template +21 -21
  23. package/templates/lit/src/app-root.ts +18 -18
  24. package/templates/lit/src/entry-client.ts +5 -5
  25. package/templates/lit/src/entry-server.ts +9 -9
  26. package/templates/lit/tsconfig.json +18 -18
  27. package/templates/lit/vite.config.ts +6 -6
  28. package/templates/preact/index.html +14 -14
  29. package/templates/preact/package.json.template +22 -22
  30. package/templates/preact/src/App.tsx +8 -8
  31. package/templates/preact/src/entry-client.tsx +11 -11
  32. package/templates/preact/src/entry-server.tsx +6 -6
  33. package/templates/preact/tsconfig.json +18 -18
  34. package/templates/preact/vite.config.ts +8 -8
  35. package/templates/qwik/index.html +14 -14
  36. package/templates/qwik/package.json.template +20 -20
  37. package/templates/qwik/src/App.tsx +10 -10
  38. package/templates/qwik/src/entry-client.tsx +4 -4
  39. package/templates/qwik/src/entry-server.tsx +9 -9
  40. package/templates/qwik/tsconfig.json +18 -18
  41. package/templates/qwik/vite.config.ts +8 -8
  42. package/templates/react/index.html +13 -13
  43. package/templates/react/package.json.template +24 -24
  44. package/templates/react/src/App.tsx +13 -13
  45. package/templates/react/src/context/RouterContext.tsx +63 -63
  46. package/templates/react/src/entry-client.tsx +19 -19
  47. package/templates/react/src/entry-server.tsx +17 -17
  48. package/templates/react/tsconfig.json +19 -19
  49. package/templates/react/vite.config.ts +12 -12
  50. package/templates/solid/index.html +14 -14
  51. package/templates/solid/package.json.template +21 -21
  52. package/templates/solid/src/App.tsx +8 -8
  53. package/templates/solid/src/entry-client.tsx +11 -11
  54. package/templates/solid/src/entry-server.tsx +6 -6
  55. package/templates/solid/tsconfig.json +18 -18
  56. package/templates/solid/vite.config.ts +8 -8
  57. package/templates/svelte/index.html +14 -14
  58. package/templates/svelte/package.json.template +21 -21
  59. package/templates/svelte/src/App.svelte +4 -4
  60. package/templates/svelte/src/entry-client.ts +7 -7
  61. package/templates/svelte/src/entry-server.ts +7 -7
  62. package/templates/svelte/tsconfig.json +17 -17
  63. package/templates/svelte/vite.config.ts +8 -8
  64. package/templates/use-cases/api/README.md +41 -41
  65. package/templates/use-cases/api/package.json.template +14 -14
  66. package/templates/use-cases/api/src/routes/api/health.get.ts.template +3 -3
  67. package/templates/use-cases/blog/README.md +47 -47
  68. package/templates/use-cases/blog/flight.config.ts.template +11 -11
  69. package/templates/use-cases/blog/package.json.template +15 -15
  70. package/templates/use-cases/blog/src/routes/blog/[slug].page.tsx.template +23 -23
  71. package/templates/use-cases/blog/src/routes/index.page.tsx.template +9 -9
  72. package/templates/use-cases/docs/README.md +49 -49
  73. package/templates/use-cases/docs/package.json.template +15 -15
  74. package/templates/use-cases/docs/src/content/index.md.template +16 -16
  75. package/templates/use-cases/ecommerce/README.md +32 -32
  76. package/templates/use-cases/ecommerce/package.json.template +16 -16
  77. package/templates/use-cases/ecommerce/src/routes/index.page.tsx.template +9 -9
  78. package/templates/use-cases/saas/README.md +34 -34
  79. package/templates/use-cases/saas/package.json.template +15 -15
  80. package/templates/use-cases/saas/src/routes/index.page.tsx.template +9 -9
  81. package/templates/vanilla/index.html +14 -14
  82. package/templates/vanilla/package.json.template +19 -19
  83. package/templates/vanilla/src/main.ts +10 -10
  84. package/templates/vanilla/tsconfig.json +16 -16
  85. package/templates/vanilla/vite.config.ts +6 -6
  86. package/templates/vue/index.html +14 -14
  87. package/templates/vue/package.json.template +21 -21
  88. package/templates/vue/src/App.vue +6 -6
  89. package/templates/vue/src/entry-client.ts +12 -12
  90. package/templates/vue/src/entry-server.ts +8 -8
  91. package/templates/vue/tsconfig.json +17 -17
  92. package/templates/vue/vite.config.ts +8 -8
package/dist/bin.js CHANGED
@@ -8,7 +8,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
8
8
 
9
9
  // src/index.ts
10
10
  import { cac } from "cac";
11
- import pc6 from "picocolors";
11
+ import pc7 from "picocolors";
12
12
 
13
13
  // src/version.ts
14
14
  var VERSION = "0.0.1";
@@ -509,1035 +509,1063 @@ ${pc.cyan("Happy flying!")}
509
509
  }
510
510
 
511
511
  // src/commands/dev.ts
512
- import { resolve as resolve2, join as join2 } from "path";
513
- import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
512
+ import { resolve as resolve3, join as join3 } from "path";
513
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
514
514
  import pc2 from "picocolors";
515
515
  import { loadConfig } from "@flight-framework/core/config";
516
- async function devCommand(options) {
517
- const startTime = Date.now();
518
- printLogo();
519
- console.log(pc2.cyan("\n\u2708\uFE0F Starting Flight development server...\n"));
520
- try {
521
- const root = resolve2(process.cwd());
522
- const config = await loadConfig(root);
523
- const port = options.port ? parseInt(options.port, 10) : config.dev.port;
524
- const host = options.host ?? config.dev.host;
525
- const open = options.open ?? config.dev.open;
526
- const ssrEnabled = options.ssr ?? config.rendering?.default === "ssr";
527
- const entryServerPath = join2(root, "src", "entry-server.tsx");
528
- const hasSSREntry = existsSync2(entryServerPath);
529
- const { createServer: createViteServer } = await import("vite");
530
- let flightHttpAvailable = false;
531
- let flightRouterAvailable = false;
532
- try {
533
- await import("@flight-framework/http");
534
- flightHttpAvailable = true;
535
- } catch {
516
+
517
+ // src/generators/typegen.ts
518
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, watch } from "fs";
519
+ import { join as join2 } from "path";
520
+ function generateHeader(command) {
521
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
522
+ return `/**
523
+ * Auto-generated by Flight CLI
524
+ * Do not edit manually - changes will be overwritten
525
+ *
526
+ * Command: ${command}
527
+ * Generated: ${timestamp}
528
+ */`;
529
+ }
530
+ function extractParams(routePath) {
531
+ const params = [];
532
+ const dynamicMatches = routePath.match(/:(\w+)/g);
533
+ if (dynamicMatches) {
534
+ params.push(...dynamicMatches.map((m) => m.slice(1)));
535
+ }
536
+ const catchAllMatches = routePath.match(/\*(\w+)/g);
537
+ if (catchAllMatches) {
538
+ params.push(...catchAllMatches.map((m) => m.slice(1)));
539
+ }
540
+ return params;
541
+ }
542
+ function isDynamicRoute(routePath) {
543
+ return routePath.includes(":") || routePath.includes("*");
544
+ }
545
+ function generateRouteTypes(routes) {
546
+ const pageRoutes = routes.filter((r) => !r.isApiRoute);
547
+ const apiRoutes = routes.filter((r) => r.isApiRoute);
548
+ const staticRoutes = pageRoutes.filter((r) => !r.isDynamic);
549
+ const dynamicRoutes = pageRoutes.filter((r) => r.isDynamic);
550
+ const appRoutesUnion = pageRoutes.length > 0 ? pageRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
551
+ const apiRoutesUnion = apiRoutes.length > 0 ? apiRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
552
+ const staticRoutesUnion = staticRoutes.length > 0 ? staticRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
553
+ const dynamicRoutesUnion = dynamicRoutes.length > 0 ? dynamicRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
554
+ const paramTypes = dynamicRoutes.map((r) => {
555
+ const params = extractParams(r.path);
556
+ const typeName = routePathToTypeName(r.path);
557
+ const paramDef = params.map((p) => `${p}: string`).join("; ");
558
+ return `export type ${typeName}Params = { ${paramDef} };`;
559
+ }).join("\n");
560
+ return `${generateHeader("flight types:generate --routes")}
561
+
562
+ // ============================================================================
563
+ // Route Types
564
+ // ============================================================================
565
+
566
+ /**
567
+ * All available page routes in the application
568
+ */
569
+ export type AppRoutes =
570
+ ${appRoutesUnion};
571
+
572
+ /**
573
+ * All available API routes in the application
574
+ */
575
+ export type ApiRoutes =
576
+ ${apiRoutesUnion};
577
+
578
+ /**
579
+ * Static routes (no dynamic parameters)
580
+ */
581
+ export type StaticRoutes =
582
+ ${staticRoutesUnion};
583
+
584
+ /**
585
+ * Dynamic routes (with parameters like :id or *slug)
586
+ */
587
+ export type DynamicRoutes =
588
+ ${dynamicRoutesUnion};
589
+
590
+ // ============================================================================
591
+ // Route Parameter Extraction
592
+ // ============================================================================
593
+
594
+ /**
595
+ * Extract route parameters from a route pattern using template literal types.
596
+ *
597
+ * @example
598
+ * type Params = ExtractRouteParams<'/users/:id'>; // { id: string }
599
+ * type BlogParams = ExtractRouteParams<'/blog/:year/:slug'>; // { year: string; slug: string }
600
+ */
601
+ type ExtractRouteParams<T extends string> =
602
+ // Handle :param/rest pattern
603
+ T extends \`\${infer _Start}:\${infer Param}/\${infer Rest}\`
604
+ ? { [K in Param]: string } & ExtractRouteParams<\`/\${Rest}\`>
605
+ // Handle :param at end
606
+ : T extends \`\${infer _Start}:\${infer Param}\`
607
+ ? { [K in Param]: string }
608
+ // Handle *param (catch-all)
609
+ : T extends \`\${infer _Start}*\${infer Param}\`
610
+ ? { [K in Param]: string }
611
+ // No params
612
+ : Record<string, never>;
613
+
614
+ /**
615
+ * Get typed parameters for a specific route.
616
+ *
617
+ * @example
618
+ * const params: RouteParams<'/users/:id'> = { id: '123' };
619
+ */
620
+ export type RouteParams<T extends AppRoutes | ApiRoutes> = ExtractRouteParams<T>;
621
+
622
+ // ============================================================================
623
+ // Helper Types
624
+ // ============================================================================
625
+
626
+ /**
627
+ * Check if a route requires parameters
628
+ */
629
+ export type RequiresParams<T extends AppRoutes | ApiRoutes> =
630
+ RouteParams<T> extends Record<string, never> ? false : true;
631
+
632
+ /**
633
+ * Props for a type-safe Link component
634
+ */
635
+ export type TypedLinkProps<T extends AppRoutes> =
636
+ RequiresParams<T> extends true
637
+ ? { to: T; params: RouteParams<T> }
638
+ : { to: T; params?: never };
639
+
640
+ /**
641
+ * Build a URL from a route pattern and parameters
642
+ */
643
+ export type BuildUrl<T extends AppRoutes> =
644
+ RequiresParams<T> extends true
645
+ ? (route: T, params: RouteParams<T>) => string
646
+ : (route: T) => string;
647
+
648
+ // ============================================================================
649
+ // Individual Route Parameter Types
650
+ // ============================================================================
651
+
652
+ ${paramTypes || "// No dynamic routes found"}
653
+ `;
654
+ }
655
+ function routePathToTypeName(routePath) {
656
+ return routePath.split("/").filter(Boolean).map((segment) => {
657
+ const clean = segment.replace(/^[:*]/, "");
658
+ return clean.charAt(0).toUpperCase() + clean.slice(1);
659
+ }).join("_") || "Root";
660
+ }
661
+ function parseEnvFile(content) {
662
+ const server = [];
663
+ const client = [];
664
+ const lines = content.split("\n");
665
+ for (const line of lines) {
666
+ const trimmed = line.trim();
667
+ if (!trimmed || trimmed.startsWith("#")) {
668
+ continue;
536
669
  }
537
- try {
538
- await import("@flight-framework/core/file-router");
539
- flightRouterAvailable = true;
540
- } catch {
670
+ const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)(\?)?=/i);
671
+ if (!match) {
672
+ continue;
541
673
  }
542
- const vite = await createViteServer({
543
- root,
544
- mode: "development",
545
- server: {
546
- middlewareMode: ssrEnabled && hasSSREntry,
547
- port: ssrEnabled && hasSSREntry ? void 0 : port,
548
- host: host === true ? "0.0.0.0" : host,
549
- open: ssrEnabled && hasSSREntry ? false : open,
550
- https: options.https ? {} : void 0
551
- },
552
- appType: ssrEnabled && hasSSREntry ? "custom" : "spa",
553
- plugins: [
554
- // Add Flight plugin when @flight-framework/http is available
555
- flightHttpAvailable ? flightDevPlugin(root) : null
556
- ].filter(Boolean)
557
- });
558
- if (ssrEnabled && hasSSREntry) {
559
- await startSSRServer(vite, root, port, host);
674
+ const key = match[1];
675
+ const optional = match[2] === "?";
676
+ const envVar = { key, optional };
677
+ if (key.startsWith("PUBLIC_")) {
678
+ client.push(envVar);
560
679
  } else {
561
- await vite.listen();
562
- }
563
- const elapsed = Date.now() - startTime;
564
- const isSSR = ssrEnabled && hasSSREntry;
565
- console.log(`
566
- ${pc2.green("\u2713")} Flight dev server ready in ${pc2.bold(elapsed + "ms")}
567
-
568
- ${pc2.cyan("\u279C")} Local: ${pc2.cyan(`http://localhost:${port}/`)}
569
- ${host === true || host === "0.0.0.0" ? ` ${pc2.cyan("\u279C")} Network: ${pc2.cyan(`http://${getNetworkAddress()}:${port}/`)}` : ""}
570
-
571
- ${isSSR ? pc2.green("\u2713") : pc2.yellow("\u25CB")} SSR ${isSSR ? "enabled (streaming)" : "disabled (CSR mode)"}
572
- ${flightHttpAvailable ? pc2.green("\u2713") : pc2.yellow("\u25CB")} @flight-framework/http ${flightHttpAvailable ? "enabled" : "not installed"}
573
- ${flightRouterAvailable ? pc2.green("\u2713") : pc2.yellow("\u25CB")} File-based routing ${flightRouterAvailable ? "enabled" : "not available"}
574
-
575
- ${pc2.dim("press")} ${pc2.bold("h")} ${pc2.dim("to show help")}
576
- `);
577
- if (!isSSR) {
578
- vite.bindCLIShortcuts({ print: true });
680
+ server.push(envVar);
579
681
  }
580
- } catch (error) {
581
- console.error(pc2.red("\nFailed to start dev server:"), error);
582
- process.exit(1);
583
682
  }
683
+ return { server, client };
584
684
  }
585
- async function startSSRServer(vite, root, port, host) {
586
- const { createServer: createHttpServer } = await import("http");
587
- let pageRouter = null;
588
- try {
589
- const { createFileRouter } = await import("@flight-framework/core/file-router");
590
- const moduleLoader = async (filePath) => {
591
- const normalizedFilePath = filePath.replace(/\\/g, "/");
592
- const normalizedRoot = root.replace(/\\/g, "/");
593
- const relativePath = normalizedFilePath.replace(normalizedRoot, "");
594
- return vite.ssrLoadModule(relativePath);
595
- };
596
- pageRouter = await createFileRouter({
597
- directory: join2(root, "src", "routes"),
598
- extensions: [".tsx", ".ts", ".jsx", ".js"],
599
- moduleLoader
600
- // Use Vite's ssrLoadModule
601
- });
602
- console.log(pc2.green(` \u2713 Page router loaded: ${pageRouter.routes.filter((r) => r.type === "page").length} pages`));
603
- } catch {
604
- }
605
- const server = createHttpServer(async (req, res) => {
606
- const url = req.url || "/";
607
- const pathname = url.split("?")[0];
608
- const isStaticAsset = url.startsWith("/@") || url.startsWith("/node_modules") || url.startsWith("/src") && !url.includes("entry-server") || // Static file extensions
609
- pathname.endsWith(".css") || pathname.endsWith(".js") || pathname.endsWith(".ts") || pathname.endsWith(".tsx") || pathname.endsWith(".svg") || pathname.endsWith(".png") || pathname.endsWith(".jpg") || pathname.endsWith(".jpeg") || pathname.endsWith(".gif") || pathname.endsWith(".ico") || pathname.endsWith(".woff") || pathname.endsWith(".woff2") || pathname.endsWith(".ttf") || pathname.endsWith(".eot") || pathname.endsWith(".json") || pathname.endsWith(".webp") || pathname.endsWith(".mp4") || pathname.endsWith(".webm");
610
- if (isStaticAsset) {
611
- vite.middlewares(req, res);
612
- return;
685
+ function loadEnvFiles(projectRoot) {
686
+ const envFiles = [".env", ".env.local", ".env.development", ".env.production"];
687
+ const merged = { server: [], client: [] };
688
+ const seenKeys = /* @__PURE__ */ new Set();
689
+ for (const envFile of envFiles) {
690
+ const envPath = join2(projectRoot, envFile);
691
+ if (!existsSync2(envPath)) {
692
+ continue;
613
693
  }
614
- if (pathname.startsWith("/api/")) {
615
- vite.middlewares(req, res);
616
- return;
617
- }
618
- try {
619
- let template = readFileSync2(
620
- join2(root, "index.html"),
621
- "utf-8"
622
- );
623
- template = await vite.transformIndexHtml(url, template);
624
- let appHtml = "";
625
- if (pageRouter) {
626
- const pageRoute = pageRouter.routes.find(
627
- (r) => r.type === "page" && matchPath(r.path, pathname)
628
- );
629
- if (pageRoute && pageRoute.component) {
630
- const normalizedFilePath = pageRoute.filePath.replace(/\\/g, "/");
631
- const normalizedRoot = root.replace(/\\/g, "/");
632
- const relativePath = normalizedFilePath.replace(normalizedRoot, "");
633
- const mod = await vite.ssrLoadModule(relativePath);
634
- const Component = mod.default;
635
- const { render } = await vite.ssrLoadModule("/src/entry-server.tsx");
636
- if (typeof render === "function" && Component) {
637
- appHtml = await render(url, { Component });
638
- }
639
- }
640
- }
641
- if (!appHtml) {
642
- const { render } = await vite.ssrLoadModule("/src/entry-server.tsx");
643
- if (typeof render === "function") {
644
- appHtml = await render(url);
645
- }
646
- }
647
- if (appHtml) {
648
- const html = template.replace("<!--ssr-outlet-->", appHtml);
649
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
650
- res.end(html);
651
- } else {
652
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
653
- res.end(template);
694
+ const content = readFileSync2(envPath, "utf-8");
695
+ const parsed = parseEnvFile(content);
696
+ for (const envVar of parsed.server) {
697
+ if (!seenKeys.has(envVar.key)) {
698
+ seenKeys.add(envVar.key);
699
+ merged.server.push(envVar);
654
700
  }
655
- } catch (e) {
656
- vite.ssrFixStacktrace(e);
657
- console.error(pc2.red("[SSR Error]"), e.stack);
658
- if (!res.headersSent) {
659
- res.writeHead(500, { "Content-Type": "text/plain" });
660
- res.end(`SSR Error: ${e.message}
661
-
662
- ${e.stack}`);
701
+ }
702
+ for (const envVar of parsed.client) {
703
+ if (!seenKeys.has(envVar.key)) {
704
+ seenKeys.add(envVar.key);
705
+ merged.client.push(envVar);
663
706
  }
664
707
  }
665
- });
666
- const listenHost = host === true ? "0.0.0.0" : host || "localhost";
667
- server.listen(port, listenHost, () => {
668
- console.log(pc2.green(` \u2713 SSR server listening`));
669
- });
708
+ }
709
+ return merged;
670
710
  }
671
- function flightDevPlugin(root) {
672
- return {
673
- name: "flight:dev",
674
- configureServer(server) {
675
- server.middlewares.use(async (req, res, next) => {
676
- const url = req.url || "/";
677
- if (url.startsWith("/__flight_action/")) {
678
- try {
679
- const { handleActionRequest } = await import("@flight-framework/core");
680
- const webRequest = await nodeToWebRequest(req);
681
- const response = await handleActionRequest(webRequest);
682
- res.statusCode = response.status;
683
- response.headers.forEach((value, key) => {
684
- res.setHeader(key, value);
685
- });
686
- const body = await response.text();
687
- res.end(body);
688
- } catch (error) {
689
- console.error("[Flight] Action error:", error);
690
- res.statusCode = 500;
691
- res.end(JSON.stringify({ error: "Internal server error" }));
692
- }
693
- return;
694
- }
695
- if (url.startsWith("/api/")) {
696
- try {
697
- const { createFileRouter } = await import("@flight-framework/core/file-router");
698
- const routesDir = join2(root, "src", "routes");
699
- const router = await createFileRouter({ directory: routesDir });
700
- const route = router.routes.find((r) => {
701
- return matchPath(r.path, url);
702
- });
703
- if (route && route.handler) {
704
- const webRequest = await nodeToWebRequest(req);
705
- const response = await route.handler({ req: webRequest, params: {} });
706
- res.statusCode = response.status;
707
- response.headers.forEach((value, key) => {
708
- res.setHeader(key, value);
709
- });
710
- const body = await response.text();
711
- res.end(body);
712
- return;
713
- }
714
- } catch (error) {
715
- }
716
- }
717
- next();
718
- });
711
+ function generateEnvTypes(env) {
712
+ const serverVars = env.server.sort((a, b) => a.key.localeCompare(b.key)).map((v) => ` ${v.key}${v.optional ? "?" : ""}: string;`).join("\n");
713
+ const clientVars = env.client.sort((a, b) => a.key.localeCompare(b.key)).map((v) => ` ${v.key}${v.optional ? "?" : ""}: string;`).join("\n");
714
+ return `${generateHeader("flight types:generate --env")}
715
+
716
+ // ============================================================================
717
+ // Server-side Environment Variables
718
+ // ============================================================================
719
+
720
+ /**
721
+ * Server-side environment variables accessible via process.env
722
+ * These are NOT exposed to the client.
723
+ */
724
+ declare namespace NodeJS {
725
+ interface ProcessEnv {
726
+ ${serverVars || " // No server environment variables defined"}
719
727
  }
720
- };
721
728
  }
722
- async function nodeToWebRequest(req) {
723
- const host = req.headers.host || "localhost";
724
- const protocol = "http";
725
- const url = new URL(req.url || "/", `${protocol}://${host}`);
726
- let body = null;
727
- if (req.method !== "GET" && req.method !== "HEAD") {
728
- const chunks = [];
729
- for await (const chunk of req) {
730
- chunks.push(chunk);
731
- }
732
- body = Buffer.concat(chunks);
729
+
730
+ // ============================================================================
731
+ // Client-side Environment Variables
732
+ // ============================================================================
733
+
734
+ /**
735
+ * Client-side environment variables accessible via import.meta.env
736
+ * Only variables with PUBLIC_ prefix are included.
737
+ */
738
+ interface ImportMetaEnv {
739
+ ${clientVars || " // No client environment variables defined"}
740
+ }
741
+
742
+ interface ImportMeta {
743
+ readonly env: ImportMetaEnv;
744
+ }
745
+ `;
746
+ }
747
+ function scanRoutesForTypes(routesDir) {
748
+ if (!existsSync2(routesDir)) {
749
+ return [];
733
750
  }
734
- const headers = new Headers();
735
- for (const [key, value] of Object.entries(req.headers)) {
736
- if (value) {
737
- if (Array.isArray(value)) {
738
- for (const v of value) {
739
- headers.append(key, v);
740
- }
741
- } else {
742
- headers.set(key, value);
751
+ const routes = [];
752
+ scanDirectoryRecursive(routesDir, "", routes);
753
+ return routes;
754
+ }
755
+ function scanDirectoryRecursive(dir, basePath, results) {
756
+ const entries = readdirSync2(dir, { withFileTypes: true });
757
+ for (const entry of entries) {
758
+ const fullPath = join2(dir, entry.name);
759
+ const relativePath = join2(basePath, entry.name);
760
+ if (entry.isDirectory()) {
761
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
762
+ continue;
763
+ }
764
+ scanDirectoryRecursive(fullPath, relativePath, results);
765
+ } else if (entry.isFile()) {
766
+ const route = parseRouteFile(entry.name, relativePath);
767
+ if (route) {
768
+ results.push(route);
743
769
  }
744
770
  }
745
771
  }
746
- return new Request(url.toString(), {
747
- method: req.method || "GET",
748
- headers,
749
- body
750
- });
751
- }
752
- function matchPath(pattern, path) {
753
- const cleanPath = path.split("?")[0];
754
- const regexPattern = pattern.replace(/:\w+/g, "[^/]+").replace(/\*/g, ".*");
755
- const regex = new RegExp(`^${regexPattern}$`);
756
- return regex.test(cleanPath || "/");
757
772
  }
758
- function getNetworkAddress() {
759
- try {
760
- const { networkInterfaces } = __require("os");
761
- const nets = networkInterfaces();
762
- for (const name of Object.keys(nets)) {
763
- for (const net of nets[name]) {
764
- if (net.family === "IPv4" && !net.internal) {
765
- return net.address;
766
- }
767
- }
768
- }
769
- } catch {
773
+ function parseRouteFile(filename, relativePath) {
774
+ if (/\.(page|route)\.(tsx?|jsx?)$/.test(filename)) {
775
+ const urlPath = filePathToUrlPath(relativePath);
776
+ return {
777
+ path: urlPath,
778
+ filePath: relativePath.replace(/\\/g, "/"),
779
+ isDynamic: isDynamicRoute(urlPath),
780
+ isApiRoute: false
781
+ };
770
782
  }
771
- return "0.0.0.0";
783
+ const apiMatch = filename.match(/\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/i);
784
+ if (apiMatch) {
785
+ const urlPath = filePathToUrlPath(relativePath);
786
+ return {
787
+ path: urlPath,
788
+ filePath: relativePath.replace(/\\/g, "/"),
789
+ isDynamic: isDynamicRoute(urlPath),
790
+ isApiRoute: true,
791
+ httpMethod: apiMatch[1].toUpperCase()
792
+ };
793
+ }
794
+ return null;
772
795
  }
773
-
774
- // src/commands/build.ts
775
- import { resolve as resolve3 } from "path";
776
- import { existsSync as existsSync3 } from "fs";
777
- import pc3 from "picocolors";
778
- import { loadConfig as loadConfig2 } from "@flight-framework/core/config";
779
- function findEntryServer(root, srcDir) {
780
- const tsxPath = resolve3(root, srcDir, "entry-server.tsx");
781
- const tsPath = resolve3(root, srcDir, "entry-server.ts");
782
- if (existsSync3(tsxPath)) return tsxPath;
783
- if (existsSync3(tsPath)) return tsPath;
784
- return tsxPath;
796
+ function filePathToUrlPath(filePath) {
797
+ let urlPath = filePath.replace(/\\/g, "/").replace(/\.(page|route)\.(tsx?|jsx?)$/, "").replace(/\.(get|post|put|patch|delete|options|head)$/i, "").replace(/\/index$/, "").replace(/^index$/, "").replace(/\/?\\?\([^)]+\\?\)/g, "").replace(/\[\.\.\.(\w+)\]/g, "*$1").replace(/\[(\w+)\]/g, ":$1");
798
+ if (!urlPath.startsWith("/")) {
799
+ urlPath = "/" + urlPath;
800
+ }
801
+ urlPath = urlPath.replace(/\/+/g, "/");
802
+ return urlPath || "/";
785
803
  }
786
- async function buildCommand(options) {
787
- const startTime = Date.now();
788
- printLogo();
789
- console.log(pc3.cyan("\n[*] Building Flight project for production...\n"));
790
- try {
791
- const root = resolve3(process.cwd());
792
- const config = await loadConfig2(root);
793
- const outDir = options.outDir ?? config.build.outDir;
794
- const sourcemap = options.sourcemap ?? config.build.sourcemap;
795
- const minify = options.minify ?? config.build.minify;
796
- const { build } = await import("vite");
797
- console.log(pc3.dim(`Output directory: ${outDir}`));
798
- console.log(pc3.dim(`Sourcemaps: ${sourcemap ? "enabled" : "disabled"}`));
799
- console.log(pc3.dim(`Minification: ${minify ? "enabled" : "disabled"}
800
- `));
801
- console.log(pc3.cyan("Building client..."));
802
- await build({
803
- root,
804
- mode: "production",
805
- build: {
806
- outDir: `${outDir}/client`,
807
- sourcemap,
808
- minify: minify ? "esbuild" : false,
809
- rollupOptions: {
810
- input: resolve3(root, "index.html")
811
- }
812
- }
813
- });
814
- console.log(pc3.green("\u2713") + " Client build complete");
815
- if (config.rendering.default !== "csr") {
816
- console.log(pc3.cyan("\nBuilding server..."));
817
- await build({
818
- root,
819
- mode: "production",
820
- build: {
821
- outDir: `${outDir}/server`,
822
- sourcemap,
823
- minify: minify ? "esbuild" : false,
824
- ssr: true,
825
- rollupOptions: {
826
- input: findEntryServer(root, config.build.srcDir)
827
- }
828
- }
829
- });
830
- console.log(pc3.green("\u2713") + " Server build complete");
804
+ async function generateTypes(options) {
805
+ const {
806
+ routesDir,
807
+ outputDir,
808
+ includeRoutes = true,
809
+ includeEnv = false,
810
+ projectRoot = process.cwd()
811
+ } = options;
812
+ const result = {
813
+ routeTypes: "",
814
+ envTypes: "",
815
+ filesWritten: [],
816
+ routeCount: 0,
817
+ envVarCount: 0
818
+ };
819
+ if (!existsSync2(outputDir)) {
820
+ mkdirSync2(outputDir, { recursive: true });
821
+ }
822
+ if (includeRoutes) {
823
+ const routes = scanRoutesForTypes(routesDir);
824
+ result.routeCount = routes.length;
825
+ result.routeTypes = generateRouteTypes(routes);
826
+ const routeTypesPath = join2(outputDir, "routes.d.ts");
827
+ writeFileSync2(routeTypesPath, result.routeTypes, "utf-8");
828
+ result.filesWritten.push(routeTypesPath);
829
+ console.log(`Generated route types: ${routes.length} routes`);
830
+ }
831
+ if (includeEnv) {
832
+ const env = loadEnvFiles(projectRoot);
833
+ result.envVarCount = env.server.length + env.client.length;
834
+ result.envTypes = generateEnvTypes(env);
835
+ const envTypesPath = join2(outputDir, "env.d.ts");
836
+ writeFileSync2(envTypesPath, result.envTypes, "utf-8");
837
+ result.filesWritten.push(envTypesPath);
838
+ console.log(`Generated env types: ${env.server.length} server, ${env.client.length} client`);
839
+ }
840
+ return result;
841
+ }
842
+ function watchAndGenerate(options) {
843
+ const { routesDir, debounce = 100, onRegenerate } = options;
844
+ let timeout = null;
845
+ const regenerate = async () => {
846
+ try {
847
+ const result = await generateTypes(options);
848
+ onRegenerate?.(result);
849
+ } catch (error) {
850
+ console.error("Type generation failed:", error.message);
831
851
  }
832
- if (config.adapter) {
833
- console.log(pc3.cyan(`
834
- Running ${config.adapter.name} adapter...`));
835
- console.log(pc3.green("\u2713") + ` ${config.adapter.name} adapter complete`);
852
+ };
853
+ const debouncedRegenerate = () => {
854
+ if (timeout) clearTimeout(timeout);
855
+ timeout = setTimeout(regenerate, debounce);
856
+ };
857
+ regenerate();
858
+ const watcher = watch(routesDir, { recursive: true }, (eventType, filename) => {
859
+ if (filename && /\.(tsx?|jsx?)$/.test(filename)) {
860
+ console.log(`Route file changed: ${filename}`);
861
+ debouncedRegenerate();
836
862
  }
837
- const elapsed = Date.now() - startTime;
838
- const elapsedSeconds = (elapsed / 1e3).toFixed(2);
839
- console.log(`
840
- ${pc3.green("[OK] Build complete!")} ${pc3.dim(`(${elapsedSeconds}s)`)}
841
-
842
- ${pc3.cyan("Output:")} ${resolve3(root, outDir)}
843
-
844
- ${pc3.dim("To preview the build:")}
845
- ${pc3.dim("$")} flight preview
846
-
847
- ${pc3.dim("To deploy:")}
848
- ${pc3.dim("\u2022")} Upload ${outDir}/ to your server
849
- ${pc3.dim("\u2022")} Or use your configured adapter
850
- `);
851
- } catch (error) {
852
- console.error(pc3.red("\nBuild failed:"), error);
853
- process.exit(1);
854
- }
863
+ });
864
+ return () => {
865
+ watcher.close();
866
+ if (timeout) clearTimeout(timeout);
867
+ };
855
868
  }
856
869
 
857
- // src/commands/preview.ts
858
- import { resolve as resolve4 } from "path";
859
- import pc4 from "picocolors";
860
- import { loadConfig as loadConfig3 } from "@flight-framework/core/config";
861
- async function previewCommand(options) {
870
+ // src/commands/dev.ts
871
+ async function devCommand(options) {
872
+ const startTime = Date.now();
862
873
  printLogo();
863
- console.log(pc4.cyan("\n\u2708\uFE0F Starting Flight preview server...\n"));
874
+ console.log(pc2.cyan("\n Starting Flight development server...\n"));
864
875
  try {
865
- const root = resolve4(process.cwd());
866
- const config = await loadConfig3(root);
867
- const port = options.port ? parseInt(options.port, 10) : config.dev.port + 1;
876
+ const root = resolve3(process.cwd());
877
+ const config = await loadConfig(root);
878
+ const routesDir = join3(root, "src", "routes");
879
+ const outputDir = join3(root, "src", ".flight");
880
+ if (existsSync3(routesDir)) {
881
+ try {
882
+ const result = await generateTypes({
883
+ routesDir,
884
+ outputDir,
885
+ includeRoutes: true,
886
+ includeEnv: false,
887
+ projectRoot: root
888
+ });
889
+ console.log(pc2.green(` Route types generated: ${result.routeCount} routes`));
890
+ const cleanup = watchAndGenerate({
891
+ routesDir,
892
+ outputDir,
893
+ includeRoutes: true,
894
+ includeEnv: false,
895
+ projectRoot: root,
896
+ onRegenerate: (watchResult) => {
897
+ console.log(pc2.blue(` Route types updated: ${watchResult.routeCount} routes`));
898
+ }
899
+ });
900
+ process.on("SIGINT", () => cleanup());
901
+ process.on("SIGTERM", () => cleanup());
902
+ } catch (typeError) {
903
+ console.log(pc2.yellow(` Route type generation skipped: ${typeError}`));
904
+ }
905
+ }
906
+ const port = options.port ? parseInt(options.port, 10) : config.dev.port;
868
907
  const host = options.host ?? config.dev.host;
869
- const open = options.open ?? false;
870
- const { preview } = await import("vite");
871
- const server = await preview({
908
+ const open = options.open ?? config.dev.open;
909
+ const ssrEnabled = options.ssr ?? config.rendering?.default === "ssr";
910
+ const entryServerPath = join3(root, "src", "entry-server.tsx");
911
+ const hasSSREntry = existsSync3(entryServerPath);
912
+ const { createServer: createViteServer } = await import("vite");
913
+ let flightHttpAvailable = false;
914
+ let flightRouterAvailable = false;
915
+ try {
916
+ await import("@flight-framework/http");
917
+ flightHttpAvailable = true;
918
+ } catch {
919
+ }
920
+ try {
921
+ await import("@flight-framework/core/file-router");
922
+ flightRouterAvailable = true;
923
+ } catch {
924
+ }
925
+ const vite = await createViteServer({
872
926
  root,
873
- preview: {
874
- port,
927
+ mode: "development",
928
+ server: {
929
+ middlewareMode: ssrEnabled && hasSSREntry,
930
+ port: ssrEnabled && hasSSREntry ? void 0 : port,
875
931
  host: host === true ? "0.0.0.0" : host,
876
- open
932
+ open: ssrEnabled && hasSSREntry ? false : open,
933
+ https: options.https ? {} : void 0
877
934
  },
878
- build: {
879
- outDir: config.build.outDir
880
- }
935
+ appType: ssrEnabled && hasSSREntry ? "custom" : "spa",
936
+ plugins: [
937
+ // Add Flight plugin when @flight-framework/http is available
938
+ flightHttpAvailable ? flightDevPlugin(root) : null
939
+ ].filter(Boolean)
881
940
  });
941
+ if (ssrEnabled && hasSSREntry) {
942
+ await startSSRServer(vite, root, port, host);
943
+ } else {
944
+ await vite.listen();
945
+ }
946
+ const elapsed = Date.now() - startTime;
947
+ const isSSR = ssrEnabled && hasSSREntry;
882
948
  console.log(`
883
- ${pc4.green("\u2713")} Flight preview server ready
949
+ ${pc2.green("\u2713")} Flight dev server ready in ${pc2.bold(elapsed + "ms")}
884
950
 
885
- ${pc4.cyan("\u279C")} Local: ${pc4.cyan(`http://localhost:${port}/`)}
886
- ${host === true || host === "0.0.0.0" ? ` ${pc4.cyan("\u279C")} Network: ${pc4.cyan(`http://0.0.0.0:${port}/`)}` : ""}
951
+ ${pc2.cyan("\u279C")} Local: ${pc2.cyan(`http://localhost:${port}/`)}
952
+ ${host === true || host === "0.0.0.0" ? ` ${pc2.cyan("\u279C")} Network: ${pc2.cyan(`http://${getNetworkAddress()}:${port}/`)}` : ""}
953
+
954
+ ${isSSR ? pc2.green("\u2713") : pc2.yellow("\u25CB")} SSR ${isSSR ? "enabled (streaming)" : "disabled (CSR mode)"}
955
+ ${flightHttpAvailable ? pc2.green("\u2713") : pc2.yellow("\u25CB")} @flight-framework/http ${flightHttpAvailable ? "enabled" : "not installed"}
956
+ ${flightRouterAvailable ? pc2.green("\u2713") : pc2.yellow("\u25CB")} File-based routing ${flightRouterAvailable ? "enabled" : "not available"}
887
957
 
888
- ${pc4.dim("This is a preview of your production build.")}
889
- ${pc4.dim("For development, use")} ${pc4.bold("flight dev")}
958
+ ${pc2.dim("press")} ${pc2.bold("h")} ${pc2.dim("to show help")}
890
959
  `);
891
- server.printUrls();
960
+ if (!isSSR) {
961
+ vite.bindCLIShortcuts({ print: true });
962
+ }
892
963
  } catch (error) {
893
- console.error(pc4.red("\nFailed to start preview server:"), error);
964
+ console.error(pc2.red("\nFailed to start dev server:"), error);
894
965
  process.exit(1);
895
966
  }
896
967
  }
897
-
898
- // src/commands/routes-generate.ts
899
- import { resolve as resolve5 } from "path";
900
-
901
- // src/generators/routes.ts
902
- import { readdirSync as readdirSync2, statSync as statSync2, existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
903
- import { join as join3, dirname as dirname2 } from "path";
904
- function filePathToUrlPath(filePath) {
905
- let urlPath = filePath.replace(/\.(page|route)\.(tsx?|jsx?)$/, "").replace(/\.(get|post|put|patch|delete|options|head)$/, "").replace(/\/index$/, "").replace(/\/?\([^)]+\)/g, "").replace(/\[\.\.\.(\w+)\]/g, "*$1").replace(/\[(\w+)\]/g, ":$1");
906
- if (!urlPath.startsWith("/")) {
907
- urlPath = "/" + urlPath;
908
- }
909
- if (urlPath === "" || urlPath === "/") {
910
- urlPath = "/";
911
- }
912
- urlPath = urlPath.replace(/\/+/g, "/");
913
- return urlPath;
914
- }
915
- function extractHttpMethod(filename) {
916
- const match = filename.match(/\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/i);
917
- return match ? match[1].toUpperCase() : void 0;
918
- }
919
- function isLayoutFile(filename) {
920
- return filename.startsWith("_layout.");
921
- }
922
- function isLoadingFile(filename) {
923
- return filename.startsWith("_loading.");
924
- }
925
- function isErrorFile(filename) {
926
- return filename.startsWith("_error.");
927
- }
928
- function isNotFoundFile(filename) {
929
- return filename.startsWith("_not-found.");
930
- }
931
- function isRouteFile(filename) {
932
- return /\.(page|route)\.(tsx?|jsx?)$/.test(filename);
933
- }
934
- function isApiRouteFile(filename) {
935
- return /\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/.test(filename);
936
- }
937
- function hasDynamicSegments(path) {
938
- return path.includes("[") && path.includes("]");
939
- }
940
- function scanDirectory(dir, basePath = "", results = []) {
941
- if (!existsSync4(dir)) {
942
- return results;
968
+ async function startSSRServer(vite, root, port, host) {
969
+ const { createServer: createHttpServer } = await import("http");
970
+ let pageRouter = null;
971
+ try {
972
+ const { createFileRouter } = await import("@flight-framework/core/file-router");
973
+ const moduleLoader = async (filePath) => {
974
+ const normalizedFilePath = filePath.replace(/\\/g, "/");
975
+ const normalizedRoot = root.replace(/\\/g, "/");
976
+ const relativePath = normalizedFilePath.replace(normalizedRoot, "");
977
+ return vite.ssrLoadModule(relativePath);
978
+ };
979
+ pageRouter = await createFileRouter({
980
+ directory: join3(root, "src", "routes"),
981
+ extensions: [".tsx", ".ts", ".jsx", ".js"],
982
+ moduleLoader
983
+ // Use Vite's ssrLoadModule
984
+ });
985
+ console.log(pc2.green(` \u2713 Page router loaded: ${pageRouter.routes.filter((r) => r.type === "page").length} pages`));
986
+ } catch {
943
987
  }
944
- const entries = readdirSync2(dir);
945
- for (const entry of entries) {
946
- const fullPath = join3(dir, entry);
947
- const relativePath = join3(basePath, entry);
948
- const stat = statSync2(fullPath);
949
- if (stat.isDirectory()) {
950
- if (entry.startsWith(".") || entry === "node_modules") {
951
- continue;
952
- }
953
- scanDirectory(fullPath, relativePath, results);
954
- } else if (stat.isFile()) {
955
- const routePath = filePathToUrlPath(dirname2(relativePath));
956
- const normalizedFilePath = relativePath.replace(/\\/g, "/");
957
- const isDynamic = hasDynamicSegments(relativePath);
958
- if (isLayoutFile(entry)) {
959
- results.push({
960
- path: routePath,
961
- filePath: normalizedFilePath,
962
- isLayout: true,
963
- isLoading: false,
964
- isError: false,
965
- isNotFound: false,
966
- isDynamic,
967
- isApiRoute: false
968
- });
969
- } else if (isLoadingFile(entry)) {
970
- results.push({
971
- path: routePath,
972
- filePath: normalizedFilePath,
973
- isLayout: false,
974
- isLoading: true,
975
- isError: false,
976
- isNotFound: false,
977
- isDynamic,
978
- isApiRoute: false
979
- });
980
- } else if (isErrorFile(entry)) {
981
- results.push({
982
- path: routePath,
983
- filePath: normalizedFilePath,
984
- isLayout: false,
985
- isLoading: false,
986
- isError: true,
987
- isNotFound: false,
988
- isDynamic,
989
- isApiRoute: false
990
- });
991
- } else if (isNotFoundFile(entry)) {
992
- results.push({
993
- path: routePath,
994
- filePath: normalizedFilePath,
995
- isLayout: false,
996
- isLoading: false,
997
- isError: false,
998
- isNotFound: true,
999
- isDynamic,
1000
- isApiRoute: false
1001
- });
1002
- } else if (isApiRouteFile(entry)) {
1003
- const method = extractHttpMethod(entry);
1004
- results.push({
1005
- path: filePathToUrlPath(relativePath),
1006
- filePath: normalizedFilePath,
1007
- isLayout: false,
1008
- isLoading: false,
1009
- isError: false,
1010
- isNotFound: false,
1011
- isDynamic,
1012
- isApiRoute: true,
1013
- httpMethod: method
1014
- });
1015
- } else if (isRouteFile(entry)) {
1016
- results.push({
1017
- path: filePathToUrlPath(relativePath),
1018
- filePath: normalizedFilePath,
1019
- isLayout: false,
1020
- isLoading: false,
1021
- isError: false,
1022
- isNotFound: false,
1023
- isDynamic,
1024
- isApiRoute: false
1025
- });
988
+ const server = createHttpServer(async (req, res) => {
989
+ const url = req.url || "/";
990
+ const pathname = url.split("?")[0];
991
+ const isStaticAsset = url.startsWith("/@") || url.startsWith("/node_modules") || url.startsWith("/src") && !url.includes("entry-server") || // Static file extensions
992
+ pathname.endsWith(".css") || pathname.endsWith(".js") || pathname.endsWith(".ts") || pathname.endsWith(".tsx") || pathname.endsWith(".svg") || pathname.endsWith(".png") || pathname.endsWith(".jpg") || pathname.endsWith(".jpeg") || pathname.endsWith(".gif") || pathname.endsWith(".ico") || pathname.endsWith(".woff") || pathname.endsWith(".woff2") || pathname.endsWith(".ttf") || pathname.endsWith(".eot") || pathname.endsWith(".json") || pathname.endsWith(".webp") || pathname.endsWith(".mp4") || pathname.endsWith(".webm");
993
+ if (isStaticAsset) {
994
+ vite.middlewares(req, res);
995
+ return;
996
+ }
997
+ if (pathname.startsWith("/api/")) {
998
+ vite.middlewares(req, res);
999
+ return;
1000
+ }
1001
+ try {
1002
+ let template = readFileSync3(
1003
+ join3(root, "index.html"),
1004
+ "utf-8"
1005
+ );
1006
+ template = await vite.transformIndexHtml(url, template);
1007
+ let appHtml = "";
1008
+ if (pageRouter) {
1009
+ const pageRoute = pageRouter.routes.find(
1010
+ (r) => r.type === "page" && matchPath(r.path, pathname)
1011
+ );
1012
+ if (pageRoute && pageRoute.component) {
1013
+ const normalizedFilePath = pageRoute.filePath.replace(/\\/g, "/");
1014
+ const normalizedRoot = root.replace(/\\/g, "/");
1015
+ const relativePath = normalizedFilePath.replace(normalizedRoot, "");
1016
+ const mod = await vite.ssrLoadModule(relativePath);
1017
+ const Component = mod.default;
1018
+ const { render } = await vite.ssrLoadModule("/src/entry-server.tsx");
1019
+ if (typeof render === "function" && Component) {
1020
+ appHtml = await render(url, { Component });
1021
+ }
1022
+ }
1023
+ }
1024
+ if (!appHtml) {
1025
+ const { render } = await vite.ssrLoadModule("/src/entry-server.tsx");
1026
+ if (typeof render === "function") {
1027
+ appHtml = await render(url);
1028
+ }
1029
+ }
1030
+ if (appHtml) {
1031
+ const html = template.replace("<!--ssr-outlet-->", appHtml);
1032
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1033
+ res.end(html);
1034
+ } else {
1035
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1036
+ res.end(template);
1037
+ }
1038
+ } catch (e) {
1039
+ vite.ssrFixStacktrace(e);
1040
+ console.error(pc2.red("[SSR Error]"), e.stack);
1041
+ if (!res.headersSent) {
1042
+ res.writeHead(500, { "Content-Type": "text/plain" });
1043
+ res.end(`SSR Error: ${e.message}
1044
+
1045
+ ${e.stack}`);
1026
1046
  }
1027
1047
  }
1028
- }
1029
- return results;
1030
- }
1031
- function sortRoutes(routes) {
1032
- return routes.sort((a, b) => {
1033
- if (!a.isDynamic && b.isDynamic) return -1;
1034
- if (a.isDynamic && !b.isDynamic) return 1;
1035
- const aSegments = a.path.split("/").length;
1036
- const bSegments = b.path.split("/").length;
1037
- if (aSegments !== bSegments) return aSegments - bSegments;
1038
- return a.path.localeCompare(b.path);
1048
+ });
1049
+ const listenHost = host === true ? "0.0.0.0" : host || "localhost";
1050
+ server.listen(port, listenHost, () => {
1051
+ console.log(pc2.green(` \u2713 SSR server listening`));
1039
1052
  });
1040
1053
  }
1041
- function generateRouteManifest(routesDir) {
1042
- const allRoutes = scanDirectory(routesDir);
1043
- const isPageRoute = (r) => !r.isLayout && !r.isLoading && !r.isError && !r.isNotFound && !r.isApiRoute;
1044
- const routes = sortRoutes(allRoutes.filter(isPageRoute));
1045
- const layouts = allRoutes.filter((r) => r.isLayout);
1046
- const loadingStates = allRoutes.filter((r) => r.isLoading);
1047
- const errorBoundaries = allRoutes.filter((r) => r.isError);
1048
- const notFoundPages = allRoutes.filter((r) => r.isNotFound);
1049
- const apiRoutes = sortRoutes(allRoutes.filter((r) => r.isApiRoute));
1054
+ function flightDevPlugin(root) {
1050
1055
  return {
1051
- routes,
1052
- layouts,
1053
- loadingStates,
1054
- errorBoundaries,
1055
- notFoundPages,
1056
- apiRoutes,
1057
- generated: (/* @__PURE__ */ new Date()).toISOString()
1056
+ name: "flight:dev",
1057
+ configureServer(server) {
1058
+ server.middlewares.use(async (req, res, next) => {
1059
+ const url = req.url || "/";
1060
+ if (url.startsWith("/__flight_action/")) {
1061
+ try {
1062
+ const { handleActionRequest } = await import("@flight-framework/core");
1063
+ const webRequest = await nodeToWebRequest(req);
1064
+ const response = await handleActionRequest(webRequest);
1065
+ res.statusCode = response.status;
1066
+ response.headers.forEach((value, key) => {
1067
+ res.setHeader(key, value);
1068
+ });
1069
+ const body = await response.text();
1070
+ res.end(body);
1071
+ } catch (error) {
1072
+ console.error("[Flight] Action error:", error);
1073
+ res.statusCode = 500;
1074
+ res.end(JSON.stringify({ error: "Internal server error" }));
1075
+ }
1076
+ return;
1077
+ }
1078
+ if (url.startsWith("/api/")) {
1079
+ try {
1080
+ const { createFileRouter } = await import("@flight-framework/core/file-router");
1081
+ const routesDir = join3(root, "src", "routes");
1082
+ const router = await createFileRouter({ directory: routesDir });
1083
+ const route = router.routes.find((r) => {
1084
+ return matchPath(r.path, url);
1085
+ });
1086
+ if (route && route.handler) {
1087
+ const webRequest = await nodeToWebRequest(req);
1088
+ const response = await route.handler({ req: webRequest, params: {} });
1089
+ res.statusCode = response.status;
1090
+ response.headers.forEach((value, key) => {
1091
+ res.setHeader(key, value);
1092
+ });
1093
+ const body = await response.text();
1094
+ res.end(body);
1095
+ return;
1096
+ }
1097
+ } catch (error) {
1098
+ }
1099
+ }
1100
+ next();
1101
+ });
1102
+ }
1058
1103
  };
1059
1104
  }
1060
- function generateRoutesFile(manifest, outputDir) {
1061
- if (!existsSync4(outputDir)) {
1062
- mkdirSync2(outputDir, { recursive: true });
1105
+ async function nodeToWebRequest(req) {
1106
+ const host = req.headers.host || "localhost";
1107
+ const protocol = "http";
1108
+ const url = new URL(req.url || "/", `${protocol}://${host}`);
1109
+ let body = null;
1110
+ if (req.method !== "GET" && req.method !== "HEAD") {
1111
+ const chunks = [];
1112
+ for await (const chunk of req) {
1113
+ chunks.push(chunk);
1114
+ }
1115
+ body = Buffer.concat(chunks);
1063
1116
  }
1064
- const routesContent = `/**
1065
- * Auto-generated by Flight CLI
1066
- * Do not edit manually
1067
- * Generated: ${manifest.generated}
1068
- */
1069
-
1070
- import type { RouteDefinition } from '@flight-framework/router';
1071
-
1072
- // Page Routes
1073
- export const routes: RouteDefinition[] = [
1074
- ${manifest.routes.map((r) => ` {
1075
- path: '${r.path}',
1076
- component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
1077
- },`).join("\n")}
1078
- ];
1079
-
1080
- // Layout Components
1081
- export const layouts = [
1082
- ${manifest.layouts.map((r) => ` {
1083
- path: '${r.path}',
1084
- component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
1085
- },`).join("\n")}
1086
- ];
1087
-
1088
- // Loading State Components
1089
- export const loadingStates = [
1090
- ${manifest.loadingStates.map((r) => ` {
1091
- path: '${r.path}',
1092
- component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
1093
- },`).join("\n")}
1094
- ];
1095
-
1096
- // Error Boundary Components
1097
- export const errorBoundaries = [
1098
- ${manifest.errorBoundaries.map((r) => ` {
1099
- path: '${r.path}',
1100
- component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
1101
- },`).join("\n")}
1102
- ];
1103
-
1104
- // Not Found Page Components
1105
- export const notFoundPages = [
1106
- ${manifest.notFoundPages.map((r) => ` {
1107
- path: '${r.path}',
1108
- component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
1109
- },`).join("\n")}
1110
- ];
1111
-
1112
- // API Routes
1113
- export const apiRoutes = [
1114
- ${manifest.apiRoutes.map((r) => ` {
1115
- path: '${r.path}',
1116
- method: '${r.httpMethod}',
1117
- handler: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
1118
- },`).join("\n")}
1119
- ];
1120
-
1121
- // Type-safe route paths
1122
- export type AppRoutes = ${manifest.routes.length > 0 ? manifest.routes.map((r) => `'${r.path}'`).join(" | ") : "never"};
1117
+ const headers = new Headers();
1118
+ for (const [key, value] of Object.entries(req.headers)) {
1119
+ if (value) {
1120
+ if (Array.isArray(value)) {
1121
+ for (const v of value) {
1122
+ headers.append(key, v);
1123
+ }
1124
+ } else {
1125
+ headers.set(key, value);
1126
+ }
1127
+ }
1128
+ }
1129
+ return new Request(url.toString(), {
1130
+ method: req.method || "GET",
1131
+ headers,
1132
+ body
1133
+ });
1134
+ }
1135
+ function matchPath(pattern, path) {
1136
+ const cleanPath = path.split("?")[0];
1137
+ const regexPattern = pattern.replace(/:\w+/g, "[^/]+").replace(/\*/g, ".*");
1138
+ const regex = new RegExp(`^${regexPattern}$`);
1139
+ return regex.test(cleanPath || "/");
1140
+ }
1141
+ function getNetworkAddress() {
1142
+ try {
1143
+ const { networkInterfaces } = __require("os");
1144
+ const nets = networkInterfaces();
1145
+ for (const name of Object.keys(nets)) {
1146
+ for (const net of nets[name]) {
1147
+ if (net.family === "IPv4" && !net.internal) {
1148
+ return net.address;
1149
+ }
1150
+ }
1151
+ }
1152
+ } catch {
1153
+ }
1154
+ return "0.0.0.0";
1155
+ }
1123
1156
 
1124
- // Type-safe API route paths
1125
- export type ApiRoutes = ${manifest.apiRoutes.length > 0 ? manifest.apiRoutes.map((r) => `'${r.path}'`).join(" | ") : "never"};
1126
- `;
1127
- writeFileSync2(join3(outputDir, "routes.ts"), routesContent, "utf-8");
1128
- const typesContent = `/**
1129
- * Auto-generated route types
1130
- * Generated: ${manifest.generated}
1131
- */
1157
+ // src/commands/build.ts
1158
+ import { resolve as resolve4 } from "path";
1159
+ import { existsSync as existsSync4 } from "fs";
1160
+ import pc3 from "picocolors";
1161
+ import { loadConfig as loadConfig2 } from "@flight-framework/core/config";
1162
+ function findEntryServer(root, srcDir) {
1163
+ const tsxPath = resolve4(root, srcDir, "entry-server.tsx");
1164
+ const tsPath = resolve4(root, srcDir, "entry-server.ts");
1165
+ if (existsSync4(tsxPath)) return tsxPath;
1166
+ if (existsSync4(tsPath)) return tsPath;
1167
+ return tsxPath;
1168
+ }
1169
+ async function buildCommand(options) {
1170
+ const startTime = Date.now();
1171
+ printLogo();
1172
+ console.log(pc3.cyan("\n[*] Building Flight project for production...\n"));
1173
+ try {
1174
+ const root = resolve4(process.cwd());
1175
+ const config = await loadConfig2(root);
1176
+ const outDir = options.outDir ?? config.build.outDir;
1177
+ const sourcemap = options.sourcemap ?? config.build.sourcemap;
1178
+ const minify = options.minify ?? config.build.minify;
1179
+ const { build } = await import("vite");
1180
+ console.log(pc3.dim(`Output directory: ${outDir}`));
1181
+ console.log(pc3.dim(`Sourcemaps: ${sourcemap ? "enabled" : "disabled"}`));
1182
+ console.log(pc3.dim(`Minification: ${minify ? "enabled" : "disabled"}
1183
+ `));
1184
+ console.log(pc3.cyan("Building client..."));
1185
+ await build({
1186
+ root,
1187
+ mode: "production",
1188
+ build: {
1189
+ outDir: `${outDir}/client`,
1190
+ sourcemap,
1191
+ minify: minify ? "esbuild" : false,
1192
+ rollupOptions: {
1193
+ input: resolve4(root, "index.html")
1194
+ }
1195
+ }
1196
+ });
1197
+ console.log(pc3.green("\u2713") + " Client build complete");
1198
+ if (config.rendering.default !== "csr") {
1199
+ console.log(pc3.cyan("\nBuilding server..."));
1200
+ await build({
1201
+ root,
1202
+ mode: "production",
1203
+ build: {
1204
+ outDir: `${outDir}/server`,
1205
+ sourcemap,
1206
+ minify: minify ? "esbuild" : false,
1207
+ ssr: true,
1208
+ rollupOptions: {
1209
+ input: findEntryServer(root, config.build.srcDir)
1210
+ }
1211
+ }
1212
+ });
1213
+ console.log(pc3.green("\u2713") + " Server build complete");
1214
+ }
1215
+ if (config.adapter) {
1216
+ console.log(pc3.cyan(`
1217
+ Running ${config.adapter.name} adapter...`));
1218
+ console.log(pc3.green("\u2713") + ` ${config.adapter.name} adapter complete`);
1219
+ }
1220
+ const elapsed = Date.now() - startTime;
1221
+ const elapsedSeconds = (elapsed / 1e3).toFixed(2);
1222
+ console.log(`
1223
+ ${pc3.green("[OK] Build complete!")} ${pc3.dim(`(${elapsedSeconds}s)`)}
1132
1224
 
1133
- import type { RouteParams } from '@flight-framework/router';
1225
+ ${pc3.cyan("Output:")} ${resolve4(root, outDir)}
1134
1226
 
1135
- // Extract params from route patterns
1136
- ${manifest.routes.filter((r) => r.isDynamic).map((r) => {
1137
- const paramMatches = r.path.match(/:(\w+)/g) || [];
1138
- const params = paramMatches.map((p) => p.slice(1));
1139
- const typeName = r.path.replace(/[/:]/g, "_").replace(/^_/, "").replace(/_$/, "") || "Root";
1140
- return `export type ${typeName}Params = { ${params.map((p) => `${p}: string`).join("; ")} };`;
1141
- }).join("\n")}
1142
- `;
1143
- writeFileSync2(join3(outputDir, "types.ts"), typesContent, "utf-8");
1144
- }
1145
- async function generateRoutes(options) {
1146
- const { routesDir, outputDir } = options;
1147
- console.log(`Scanning routes in: ${routesDir}`);
1148
- const manifest = generateRouteManifest(routesDir);
1149
- const stats = [
1150
- `${manifest.routes.length} pages`,
1151
- `${manifest.layouts.length} layouts`,
1152
- `${manifest.loadingStates.length} loading states`,
1153
- `${manifest.errorBoundaries.length} error boundaries`,
1154
- `${manifest.notFoundPages.length} not-found pages`,
1155
- `${manifest.apiRoutes.length} API routes`
1156
- ].join(", ");
1157
- console.log(`Found: ${stats}`);
1158
- generateRoutesFile(manifest, outputDir);
1159
- console.log(`Generated route manifest in: ${outputDir}`);
1160
- return manifest;
1227
+ ${pc3.dim("To preview the build:")}
1228
+ ${pc3.dim("$")} flight preview
1229
+
1230
+ ${pc3.dim("To deploy:")}
1231
+ ${pc3.dim("\u2022")} Upload ${outDir}/ to your server
1232
+ ${pc3.dim("\u2022")} Or use your configured adapter
1233
+ `);
1234
+ } catch (error) {
1235
+ console.error(pc3.red("\nBuild failed:"), error);
1236
+ process.exit(1);
1237
+ }
1161
1238
  }
1162
1239
 
1163
- // src/commands/routes-generate.ts
1164
- async function routesGenerateCommand(options = {}) {
1165
- const cwd = process.cwd();
1166
- const routesDir = options.routesDir ? resolve5(cwd, options.routesDir) : resolve5(cwd, "src/routes");
1167
- const outputDir = options.outputDir ? resolve5(cwd, options.outputDir) : resolve5(cwd, "src/.flight");
1240
+ // src/commands/preview.ts
1241
+ import { resolve as resolve5 } from "path";
1242
+ import pc4 from "picocolors";
1243
+ import { loadConfig as loadConfig3 } from "@flight-framework/core/config";
1244
+ async function previewCommand(options) {
1245
+ printLogo();
1246
+ console.log(pc4.cyan("\n\u2708\uFE0F Starting Flight preview server...\n"));
1168
1247
  try {
1169
- const manifest = await generateRoutes({
1170
- routesDir,
1171
- outputDir,
1172
- watch: options.watch
1248
+ const root = resolve5(process.cwd());
1249
+ const config = await loadConfig3(root);
1250
+ const port = options.port ? parseInt(options.port, 10) : config.dev.port + 1;
1251
+ const host = options.host ?? config.dev.host;
1252
+ const open = options.open ?? false;
1253
+ const { preview } = await import("vite");
1254
+ const server = await preview({
1255
+ root,
1256
+ preview: {
1257
+ port,
1258
+ host: host === true ? "0.0.0.0" : host,
1259
+ open
1260
+ },
1261
+ build: {
1262
+ outDir: config.build.outDir
1263
+ }
1173
1264
  });
1174
- console.log("\nRoute manifest generated successfully!");
1175
- console.log(` Pages: ${manifest.routes.length}`);
1176
- console.log(` API Routes: ${manifest.apiRoutes.length}`);
1177
- console.log(` Layouts: ${manifest.layouts.length}`);
1265
+ console.log(`
1266
+ ${pc4.green("\u2713")} Flight preview server ready
1267
+
1268
+ ${pc4.cyan("\u279C")} Local: ${pc4.cyan(`http://localhost:${port}/`)}
1269
+ ${host === true || host === "0.0.0.0" ? ` ${pc4.cyan("\u279C")} Network: ${pc4.cyan(`http://0.0.0.0:${port}/`)}` : ""}
1270
+
1271
+ ${pc4.dim("This is a preview of your production build.")}
1272
+ ${pc4.dim("For development, use")} ${pc4.bold("flight dev")}
1273
+ `);
1274
+ server.printUrls();
1178
1275
  } catch (error) {
1179
- console.error("Failed to generate routes:", error);
1276
+ console.error(pc4.red("\nFailed to start preview server:"), error);
1180
1277
  process.exit(1);
1181
1278
  }
1182
1279
  }
1183
1280
 
1184
- // src/commands/types-generate.ts
1185
- import { resolve as resolve7 } from "path";
1281
+ // src/commands/routes-generate.ts
1282
+ import { resolve as resolve6 } from "path";
1186
1283
 
1187
- // src/generators/typegen.ts
1188
- import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, watch } from "fs";
1189
- import { join as join4 } from "path";
1190
- function generateHeader(command) {
1191
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1192
- return `/**
1193
- * Auto-generated by Flight CLI
1194
- * Do not edit manually - changes will be overwritten
1195
- *
1196
- * Command: ${command}
1197
- * Generated: ${timestamp}
1198
- */`;
1199
- }
1200
- function extractParams(routePath) {
1201
- const params = [];
1202
- const dynamicMatches = routePath.match(/:(\w+)/g);
1203
- if (dynamicMatches) {
1204
- params.push(...dynamicMatches.map((m) => m.slice(1)));
1284
+ // src/generators/routes.ts
1285
+ import { readdirSync as readdirSync3, statSync as statSync2, existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
1286
+ import { join as join4, dirname as dirname3 } from "path";
1287
+ function filePathToUrlPath2(filePath) {
1288
+ let urlPath = filePath.replace(/\.(page|route)\.(tsx?|jsx?)$/, "").replace(/\.(get|post|put|patch|delete|options|head)$/, "").replace(/\/index$/, "").replace(/\/?\([^)]+\)/g, "").replace(/\[\.\.\.(\w+)\]/g, "*$1").replace(/\[(\w+)\]/g, ":$1");
1289
+ if (!urlPath.startsWith("/")) {
1290
+ urlPath = "/" + urlPath;
1205
1291
  }
1206
- const catchAllMatches = routePath.match(/\*(\w+)/g);
1207
- if (catchAllMatches) {
1208
- params.push(...catchAllMatches.map((m) => m.slice(1)));
1292
+ if (urlPath === "" || urlPath === "/") {
1293
+ urlPath = "/";
1209
1294
  }
1210
- return params;
1211
- }
1212
- function isDynamicRoute(routePath) {
1213
- return routePath.includes(":") || routePath.includes("*");
1214
- }
1215
- function generateRouteTypes(routes) {
1216
- const pageRoutes = routes.filter((r) => !r.isApiRoute);
1217
- const apiRoutes = routes.filter((r) => r.isApiRoute);
1218
- const staticRoutes = pageRoutes.filter((r) => !r.isDynamic);
1219
- const dynamicRoutes = pageRoutes.filter((r) => r.isDynamic);
1220
- const appRoutesUnion = pageRoutes.length > 0 ? pageRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
1221
- const apiRoutesUnion = apiRoutes.length > 0 ? apiRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
1222
- const staticRoutesUnion = staticRoutes.length > 0 ? staticRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
1223
- const dynamicRoutesUnion = dynamicRoutes.length > 0 ? dynamicRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
1224
- const paramTypes = dynamicRoutes.map((r) => {
1225
- const params = extractParams(r.path);
1226
- const typeName = routePathToTypeName(r.path);
1227
- const paramDef = params.map((p) => `${p}: string`).join("; ");
1228
- return `export type ${typeName}Params = { ${paramDef} };`;
1229
- }).join("\n");
1230
- return `${generateHeader("flight types:generate --routes")}
1231
-
1232
- // ============================================================================
1233
- // Route Types
1234
- // ============================================================================
1235
-
1236
- /**
1237
- * All available page routes in the application
1238
- */
1239
- export type AppRoutes =
1240
- ${appRoutesUnion};
1241
-
1242
- /**
1243
- * All available API routes in the application
1244
- */
1245
- export type ApiRoutes =
1246
- ${apiRoutesUnion};
1247
-
1248
- /**
1249
- * Static routes (no dynamic parameters)
1250
- */
1251
- export type StaticRoutes =
1252
- ${staticRoutesUnion};
1253
-
1254
- /**
1255
- * Dynamic routes (with parameters like :id or *slug)
1256
- */
1257
- export type DynamicRoutes =
1258
- ${dynamicRoutesUnion};
1259
-
1260
- // ============================================================================
1261
- // Route Parameter Extraction
1262
- // ============================================================================
1263
-
1264
- /**
1265
- * Extract route parameters from a route pattern using template literal types.
1266
- *
1267
- * @example
1268
- * type Params = ExtractRouteParams<'/users/:id'>; // { id: string }
1269
- * type BlogParams = ExtractRouteParams<'/blog/:year/:slug'>; // { year: string; slug: string }
1270
- */
1271
- type ExtractRouteParams<T extends string> =
1272
- // Handle :param/rest pattern
1273
- T extends \`\${infer _Start}:\${infer Param}/\${infer Rest}\`
1274
- ? { [K in Param]: string } & ExtractRouteParams<\`/\${Rest}\`>
1275
- // Handle :param at end
1276
- : T extends \`\${infer _Start}:\${infer Param}\`
1277
- ? { [K in Param]: string }
1278
- // Handle *param (catch-all)
1279
- : T extends \`\${infer _Start}*\${infer Param}\`
1280
- ? { [K in Param]: string }
1281
- // No params
1282
- : Record<string, never>;
1283
-
1284
- /**
1285
- * Get typed parameters for a specific route.
1286
- *
1287
- * @example
1288
- * const params: RouteParams<'/users/:id'> = { id: '123' };
1289
- */
1290
- export type RouteParams<T extends AppRoutes | ApiRoutes> = ExtractRouteParams<T>;
1291
-
1292
- // ============================================================================
1293
- // Helper Types
1294
- // ============================================================================
1295
-
1296
- /**
1297
- * Check if a route requires parameters
1298
- */
1299
- export type RequiresParams<T extends AppRoutes | ApiRoutes> =
1300
- RouteParams<T> extends Record<string, never> ? false : true;
1301
-
1302
- /**
1303
- * Props for a type-safe Link component
1304
- */
1305
- export type TypedLinkProps<T extends AppRoutes> =
1306
- RequiresParams<T> extends true
1307
- ? { to: T; params: RouteParams<T> }
1308
- : { to: T; params?: never };
1309
-
1310
- /**
1311
- * Build a URL from a route pattern and parameters
1312
- */
1313
- export type BuildUrl<T extends AppRoutes> =
1314
- RequiresParams<T> extends true
1315
- ? (route: T, params: RouteParams<T>) => string
1316
- : (route: T) => string;
1317
-
1318
- // ============================================================================
1319
- // Individual Route Parameter Types
1320
- // ============================================================================
1321
-
1322
- ${paramTypes || "// No dynamic routes found"}
1323
- `;
1324
- }
1325
- function routePathToTypeName(routePath) {
1326
- return routePath.split("/").filter(Boolean).map((segment) => {
1327
- const clean = segment.replace(/^[:*]/, "");
1328
- return clean.charAt(0).toUpperCase() + clean.slice(1);
1329
- }).join("_") || "Root";
1295
+ urlPath = urlPath.replace(/\/+/g, "/");
1296
+ return urlPath;
1330
1297
  }
1331
- function parseEnvFile(content) {
1332
- const server = [];
1333
- const client = [];
1334
- const lines = content.split("\n");
1335
- for (const line of lines) {
1336
- const trimmed = line.trim();
1337
- if (!trimmed || trimmed.startsWith("#")) {
1338
- continue;
1339
- }
1340
- const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)(\?)?=/i);
1341
- if (!match) {
1342
- continue;
1343
- }
1344
- const key = match[1];
1345
- const optional = match[2] === "?";
1346
- const envVar = { key, optional };
1347
- if (key.startsWith("PUBLIC_")) {
1348
- client.push(envVar);
1349
- } else {
1350
- server.push(envVar);
1351
- }
1352
- }
1353
- return { server, client };
1298
+ function extractHttpMethod(filename) {
1299
+ const match = filename.match(/\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/i);
1300
+ return match ? match[1].toUpperCase() : void 0;
1354
1301
  }
1355
- function loadEnvFiles(projectRoot) {
1356
- const envFiles = [".env", ".env.local", ".env.development", ".env.production"];
1357
- const merged = { server: [], client: [] };
1358
- const seenKeys = /* @__PURE__ */ new Set();
1359
- for (const envFile of envFiles) {
1360
- const envPath = join4(projectRoot, envFile);
1361
- if (!existsSync5(envPath)) {
1362
- continue;
1363
- }
1364
- const content = readFileSync3(envPath, "utf-8");
1365
- const parsed = parseEnvFile(content);
1366
- for (const envVar of parsed.server) {
1367
- if (!seenKeys.has(envVar.key)) {
1368
- seenKeys.add(envVar.key);
1369
- merged.server.push(envVar);
1370
- }
1371
- }
1372
- for (const envVar of parsed.client) {
1373
- if (!seenKeys.has(envVar.key)) {
1374
- seenKeys.add(envVar.key);
1375
- merged.client.push(envVar);
1376
- }
1377
- }
1378
- }
1379
- return merged;
1302
+ function isLayoutFile(filename) {
1303
+ return filename.startsWith("_layout.");
1380
1304
  }
1381
- function generateEnvTypes(env) {
1382
- const serverVars = env.server.sort((a, b) => a.key.localeCompare(b.key)).map((v) => ` ${v.key}${v.optional ? "?" : ""}: string;`).join("\n");
1383
- const clientVars = env.client.sort((a, b) => a.key.localeCompare(b.key)).map((v) => ` ${v.key}${v.optional ? "?" : ""}: string;`).join("\n");
1384
- return `${generateHeader("flight types:generate --env")}
1385
-
1386
- // ============================================================================
1387
- // Server-side Environment Variables
1388
- // ============================================================================
1389
-
1390
- /**
1391
- * Server-side environment variables accessible via process.env
1392
- * These are NOT exposed to the client.
1393
- */
1394
- declare namespace NodeJS {
1395
- interface ProcessEnv {
1396
- ${serverVars || " // No server environment variables defined"}
1397
- }
1305
+ function isLoadingFile(filename) {
1306
+ return filename.startsWith("_loading.");
1398
1307
  }
1399
-
1400
- // ============================================================================
1401
- // Client-side Environment Variables
1402
- // ============================================================================
1403
-
1404
- /**
1405
- * Client-side environment variables accessible via import.meta.env
1406
- * Only variables with PUBLIC_ prefix are included.
1407
- */
1408
- interface ImportMetaEnv {
1409
- ${clientVars || " // No client environment variables defined"}
1308
+ function isErrorFile(filename) {
1309
+ return filename.startsWith("_error.");
1410
1310
  }
1411
-
1412
- interface ImportMeta {
1413
- readonly env: ImportMetaEnv;
1311
+ function isNotFoundFile(filename) {
1312
+ return filename.startsWith("_not-found.");
1414
1313
  }
1415
- `;
1314
+ function isRouteFile(filename) {
1315
+ return /\.(page|route)\.(tsx?|jsx?)$/.test(filename);
1416
1316
  }
1417
- function scanRoutesForTypes(routesDir) {
1418
- if (!existsSync5(routesDir)) {
1419
- return [];
1420
- }
1421
- const routes = [];
1422
- scanDirectoryRecursive(routesDir, "", routes);
1423
- return routes;
1317
+ function isApiRouteFile(filename) {
1318
+ return /\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/.test(filename);
1424
1319
  }
1425
- function scanDirectoryRecursive(dir, basePath, results) {
1426
- const entries = readdirSync3(dir, { withFileTypes: true });
1320
+ function hasDynamicSegments(path) {
1321
+ return path.includes("[") && path.includes("]");
1322
+ }
1323
+ function scanDirectory(dir, basePath = "", results = []) {
1324
+ if (!existsSync5(dir)) {
1325
+ return results;
1326
+ }
1327
+ const entries = readdirSync3(dir);
1427
1328
  for (const entry of entries) {
1428
- const fullPath = join4(dir, entry.name);
1429
- const relativePath = join4(basePath, entry.name);
1430
- if (entry.isDirectory()) {
1431
- if (entry.name.startsWith(".") || entry.name === "node_modules") {
1329
+ const fullPath = join4(dir, entry);
1330
+ const relativePath = join4(basePath, entry);
1331
+ const stat = statSync2(fullPath);
1332
+ if (stat.isDirectory()) {
1333
+ if (entry.startsWith(".") || entry === "node_modules") {
1432
1334
  continue;
1433
1335
  }
1434
- scanDirectoryRecursive(fullPath, relativePath, results);
1435
- } else if (entry.isFile()) {
1436
- const route = parseRouteFile(entry.name, relativePath);
1437
- if (route) {
1438
- results.push(route);
1336
+ scanDirectory(fullPath, relativePath, results);
1337
+ } else if (stat.isFile()) {
1338
+ const routePath = filePathToUrlPath2(dirname3(relativePath));
1339
+ const normalizedFilePath = relativePath.replace(/\\/g, "/");
1340
+ const isDynamic = hasDynamicSegments(relativePath);
1341
+ if (isLayoutFile(entry)) {
1342
+ results.push({
1343
+ path: routePath,
1344
+ filePath: normalizedFilePath,
1345
+ isLayout: true,
1346
+ isLoading: false,
1347
+ isError: false,
1348
+ isNotFound: false,
1349
+ isDynamic,
1350
+ isApiRoute: false
1351
+ });
1352
+ } else if (isLoadingFile(entry)) {
1353
+ results.push({
1354
+ path: routePath,
1355
+ filePath: normalizedFilePath,
1356
+ isLayout: false,
1357
+ isLoading: true,
1358
+ isError: false,
1359
+ isNotFound: false,
1360
+ isDynamic,
1361
+ isApiRoute: false
1362
+ });
1363
+ } else if (isErrorFile(entry)) {
1364
+ results.push({
1365
+ path: routePath,
1366
+ filePath: normalizedFilePath,
1367
+ isLayout: false,
1368
+ isLoading: false,
1369
+ isError: true,
1370
+ isNotFound: false,
1371
+ isDynamic,
1372
+ isApiRoute: false
1373
+ });
1374
+ } else if (isNotFoundFile(entry)) {
1375
+ results.push({
1376
+ path: routePath,
1377
+ filePath: normalizedFilePath,
1378
+ isLayout: false,
1379
+ isLoading: false,
1380
+ isError: false,
1381
+ isNotFound: true,
1382
+ isDynamic,
1383
+ isApiRoute: false
1384
+ });
1385
+ } else if (isApiRouteFile(entry)) {
1386
+ const method = extractHttpMethod(entry);
1387
+ results.push({
1388
+ path: filePathToUrlPath2(relativePath),
1389
+ filePath: normalizedFilePath,
1390
+ isLayout: false,
1391
+ isLoading: false,
1392
+ isError: false,
1393
+ isNotFound: false,
1394
+ isDynamic,
1395
+ isApiRoute: true,
1396
+ httpMethod: method
1397
+ });
1398
+ } else if (isRouteFile(entry)) {
1399
+ results.push({
1400
+ path: filePathToUrlPath2(relativePath),
1401
+ filePath: normalizedFilePath,
1402
+ isLayout: false,
1403
+ isLoading: false,
1404
+ isError: false,
1405
+ isNotFound: false,
1406
+ isDynamic,
1407
+ isApiRoute: false
1408
+ });
1439
1409
  }
1440
1410
  }
1441
1411
  }
1412
+ return results;
1442
1413
  }
1443
- function parseRouteFile(filename, relativePath) {
1444
- if (/\.(page|route)\.(tsx?|jsx?)$/.test(filename)) {
1445
- const urlPath = filePathToUrlPath2(relativePath);
1446
- return {
1447
- path: urlPath,
1448
- filePath: relativePath.replace(/\\/g, "/"),
1449
- isDynamic: isDynamicRoute(urlPath),
1450
- isApiRoute: false
1451
- };
1452
- }
1453
- const apiMatch = filename.match(/\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/i);
1454
- if (apiMatch) {
1455
- const urlPath = filePathToUrlPath2(relativePath);
1456
- return {
1457
- path: urlPath,
1458
- filePath: relativePath.replace(/\\/g, "/"),
1459
- isDynamic: isDynamicRoute(urlPath),
1460
- isApiRoute: true,
1461
- httpMethod: apiMatch[1].toUpperCase()
1462
- };
1414
+ function sortRoutes(routes) {
1415
+ return routes.sort((a, b) => {
1416
+ if (!a.isDynamic && b.isDynamic) return -1;
1417
+ if (a.isDynamic && !b.isDynamic) return 1;
1418
+ const aSegments = a.path.split("/").length;
1419
+ const bSegments = b.path.split("/").length;
1420
+ if (aSegments !== bSegments) return aSegments - bSegments;
1421
+ return a.path.localeCompare(b.path);
1422
+ });
1423
+ }
1424
+ function generateRouteManifest(routesDir) {
1425
+ const allRoutes = scanDirectory(routesDir);
1426
+ const isPageRoute = (r) => !r.isLayout && !r.isLoading && !r.isError && !r.isNotFound && !r.isApiRoute;
1427
+ const routes = sortRoutes(allRoutes.filter(isPageRoute));
1428
+ const layouts = allRoutes.filter((r) => r.isLayout);
1429
+ const loadingStates = allRoutes.filter((r) => r.isLoading);
1430
+ const errorBoundaries = allRoutes.filter((r) => r.isError);
1431
+ const notFoundPages = allRoutes.filter((r) => r.isNotFound);
1432
+ const apiRoutes = sortRoutes(allRoutes.filter((r) => r.isApiRoute));
1433
+ return {
1434
+ routes,
1435
+ layouts,
1436
+ loadingStates,
1437
+ errorBoundaries,
1438
+ notFoundPages,
1439
+ apiRoutes,
1440
+ generated: (/* @__PURE__ */ new Date()).toISOString()
1441
+ };
1442
+ }
1443
+ function generateRoutesFile(manifest, outputDir) {
1444
+ if (!existsSync5(outputDir)) {
1445
+ mkdirSync3(outputDir, { recursive: true });
1463
1446
  }
1464
- return null;
1447
+ const routesContent = `/**
1448
+ * Auto-generated by Flight CLI
1449
+ * Do not edit manually
1450
+ * Generated: ${manifest.generated}
1451
+ */
1452
+
1453
+ import type { RouteDefinition } from '@flight-framework/router';
1454
+
1455
+ // Page Routes
1456
+ export const routes: RouteDefinition[] = [
1457
+ ${manifest.routes.map((r) => ` {
1458
+ path: '${r.path}',
1459
+ component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
1460
+ },`).join("\n")}
1461
+ ];
1462
+
1463
+ // Layout Components
1464
+ export const layouts = [
1465
+ ${manifest.layouts.map((r) => ` {
1466
+ path: '${r.path}',
1467
+ component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
1468
+ },`).join("\n")}
1469
+ ];
1470
+
1471
+ // Loading State Components
1472
+ export const loadingStates = [
1473
+ ${manifest.loadingStates.map((r) => ` {
1474
+ path: '${r.path}',
1475
+ component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
1476
+ },`).join("\n")}
1477
+ ];
1478
+
1479
+ // Error Boundary Components
1480
+ export const errorBoundaries = [
1481
+ ${manifest.errorBoundaries.map((r) => ` {
1482
+ path: '${r.path}',
1483
+ component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
1484
+ },`).join("\n")}
1485
+ ];
1486
+
1487
+ // Not Found Page Components
1488
+ export const notFoundPages = [
1489
+ ${manifest.notFoundPages.map((r) => ` {
1490
+ path: '${r.path}',
1491
+ component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
1492
+ },`).join("\n")}
1493
+ ];
1494
+
1495
+ // API Routes
1496
+ export const apiRoutes = [
1497
+ ${manifest.apiRoutes.map((r) => ` {
1498
+ path: '${r.path}',
1499
+ method: '${r.httpMethod}',
1500
+ handler: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
1501
+ },`).join("\n")}
1502
+ ];
1503
+
1504
+ // Type-safe route paths
1505
+ export type AppRoutes = ${manifest.routes.length > 0 ? manifest.routes.map((r) => `'${r.path}'`).join(" | ") : "never"};
1506
+
1507
+ // Type-safe API route paths
1508
+ export type ApiRoutes = ${manifest.apiRoutes.length > 0 ? manifest.apiRoutes.map((r) => `'${r.path}'`).join(" | ") : "never"};
1509
+ `;
1510
+ writeFileSync3(join4(outputDir, "routes.ts"), routesContent, "utf-8");
1511
+ const typesContent = `/**
1512
+ * Auto-generated route types
1513
+ * Generated: ${manifest.generated}
1514
+ */
1515
+
1516
+ import type { RouteParams } from '@flight-framework/router';
1517
+
1518
+ // Extract params from route patterns
1519
+ ${manifest.routes.filter((r) => r.isDynamic).map((r) => {
1520
+ const paramMatches = r.path.match(/:(\w+)/g) || [];
1521
+ const params = paramMatches.map((p) => p.slice(1));
1522
+ const typeName = r.path.replace(/[/:]/g, "_").replace(/^_/, "").replace(/_$/, "") || "Root";
1523
+ return `export type ${typeName}Params = { ${params.map((p) => `${p}: string`).join("; ")} };`;
1524
+ }).join("\n")}
1525
+ `;
1526
+ writeFileSync3(join4(outputDir, "types.ts"), typesContent, "utf-8");
1465
1527
  }
1466
- function filePathToUrlPath2(filePath) {
1467
- let urlPath = filePath.replace(/\\/g, "/").replace(/\.(page|route)\.(tsx?|jsx?)$/, "").replace(/\.(get|post|put|patch|delete|options|head)$/i, "").replace(/\/index$/, "").replace(/^index$/, "").replace(/\/?\\?\([^)]+\\?\)/g, "").replace(/\[\.\.\.(\w+)\]/g, "*$1").replace(/\[(\w+)\]/g, ":$1");
1468
- if (!urlPath.startsWith("/")) {
1469
- urlPath = "/" + urlPath;
1470
- }
1471
- urlPath = urlPath.replace(/\/+/g, "/");
1472
- return urlPath || "/";
1528
+ async function generateRoutes(options) {
1529
+ const { routesDir, outputDir } = options;
1530
+ console.log(`Scanning routes in: ${routesDir}`);
1531
+ const manifest = generateRouteManifest(routesDir);
1532
+ const stats = [
1533
+ `${manifest.routes.length} pages`,
1534
+ `${manifest.layouts.length} layouts`,
1535
+ `${manifest.loadingStates.length} loading states`,
1536
+ `${manifest.errorBoundaries.length} error boundaries`,
1537
+ `${manifest.notFoundPages.length} not-found pages`,
1538
+ `${manifest.apiRoutes.length} API routes`
1539
+ ].join(", ");
1540
+ console.log(`Found: ${stats}`);
1541
+ generateRoutesFile(manifest, outputDir);
1542
+ console.log(`Generated route manifest in: ${outputDir}`);
1543
+ return manifest;
1473
1544
  }
1474
- async function generateTypes(options) {
1475
- const {
1476
- routesDir,
1477
- outputDir,
1478
- includeRoutes = true,
1479
- includeEnv = false,
1480
- projectRoot = process.cwd()
1481
- } = options;
1482
- const result = {
1483
- routeTypes: "",
1484
- envTypes: "",
1485
- filesWritten: [],
1486
- routeCount: 0,
1487
- envVarCount: 0
1488
- };
1489
- if (!existsSync5(outputDir)) {
1490
- mkdirSync3(outputDir, { recursive: true });
1491
- }
1492
- if (includeRoutes) {
1493
- const routes = scanRoutesForTypes(routesDir);
1494
- result.routeCount = routes.length;
1495
- result.routeTypes = generateRouteTypes(routes);
1496
- const routeTypesPath = join4(outputDir, "routes.d.ts");
1497
- writeFileSync3(routeTypesPath, result.routeTypes, "utf-8");
1498
- result.filesWritten.push(routeTypesPath);
1499
- console.log(`Generated route types: ${routes.length} routes`);
1500
- }
1501
- if (includeEnv) {
1502
- const env = loadEnvFiles(projectRoot);
1503
- result.envVarCount = env.server.length + env.client.length;
1504
- result.envTypes = generateEnvTypes(env);
1505
- const envTypesPath = join4(outputDir, "env.d.ts");
1506
- writeFileSync3(envTypesPath, result.envTypes, "utf-8");
1507
- result.filesWritten.push(envTypesPath);
1508
- console.log(`Generated env types: ${env.server.length} server, ${env.client.length} client`);
1545
+
1546
+ // src/commands/routes-generate.ts
1547
+ async function routesGenerateCommand(options = {}) {
1548
+ const cwd = process.cwd();
1549
+ const routesDir = options.routesDir ? resolve6(cwd, options.routesDir) : resolve6(cwd, "src/routes");
1550
+ const outputDir = options.outputDir ? resolve6(cwd, options.outputDir) : resolve6(cwd, "src/.flight");
1551
+ try {
1552
+ const manifest = await generateRoutes({
1553
+ routesDir,
1554
+ outputDir,
1555
+ watch: options.watch
1556
+ });
1557
+ console.log("\nRoute manifest generated successfully!");
1558
+ console.log(` Pages: ${manifest.routes.length}`);
1559
+ console.log(` API Routes: ${manifest.apiRoutes.length}`);
1560
+ console.log(` Layouts: ${manifest.layouts.length}`);
1561
+ } catch (error) {
1562
+ console.error("Failed to generate routes:", error);
1563
+ process.exit(1);
1509
1564
  }
1510
- return result;
1511
- }
1512
- function watchAndGenerate(options) {
1513
- const { routesDir, debounce = 100, onRegenerate } = options;
1514
- let timeout = null;
1515
- const regenerate = async () => {
1516
- try {
1517
- const result = await generateTypes(options);
1518
- onRegenerate?.(result);
1519
- } catch (error) {
1520
- console.error("Type generation failed:", error.message);
1521
- }
1522
- };
1523
- const debouncedRegenerate = () => {
1524
- if (timeout) clearTimeout(timeout);
1525
- timeout = setTimeout(regenerate, debounce);
1526
- };
1527
- regenerate();
1528
- const watcher = watch(routesDir, { recursive: true }, (eventType, filename) => {
1529
- if (filename && /\.(tsx?|jsx?)$/.test(filename)) {
1530
- console.log(`Route file changed: ${filename}`);
1531
- debouncedRegenerate();
1532
- }
1533
- });
1534
- return () => {
1535
- watcher.close();
1536
- if (timeout) clearTimeout(timeout);
1537
- };
1538
1565
  }
1539
1566
 
1540
1567
  // src/commands/types-generate.ts
1568
+ import { resolve as resolve7 } from "path";
1541
1569
  async function typesGenerateCommand(options = {}) {
1542
1570
  const cwd = process.cwd();
1543
1571
  const routesDir = options.routesDir ? resolve7(cwd, options.routesDir) : resolve7(cwd, "src/routes");
@@ -1804,18 +1832,471 @@ ${pc5.cyan("Quick example:")}
1804
1832
  console.log(`${pc5.dim("Docs:")} https://flight.dev/docs/packages/${packageName}`);
1805
1833
  }
1806
1834
 
1835
+ // src/commands/adapter-create.ts
1836
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
1837
+ import { join as join6 } from "path";
1838
+ import pc6 from "picocolors";
1839
+ async function adapterCreateCommand(name, options = {}) {
1840
+ const cwd = process.cwd();
1841
+ const adapterName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
1842
+ const packageName = `@flight-framework/adapter-${adapterName}`;
1843
+ const outputDir = options.outputDir || join6(cwd, "packages", `adapter-${adapterName}`);
1844
+ const type = options.type || "node";
1845
+ const includeValidation = options.validation || false;
1846
+ console.log(pc6.cyan(`
1847
+ Creating adapter: ${packageName}
1848
+ `));
1849
+ if (existsSync7(outputDir)) {
1850
+ console.error(pc6.red(` Error: Directory already exists: ${outputDir}`));
1851
+ process.exit(1);
1852
+ }
1853
+ mkdirSync4(join6(outputDir, "src"), { recursive: true });
1854
+ mkdirSync4(join6(outputDir, "tests"), { recursive: true });
1855
+ const files = generateAdapterFiles(adapterName, packageName, type, includeValidation);
1856
+ for (const [filePath, content] of Object.entries(files)) {
1857
+ const fullPath = join6(outputDir, filePath);
1858
+ writeFileSync5(fullPath, content, "utf-8");
1859
+ console.log(pc6.green(` \u2713 Created ${filePath}`));
1860
+ }
1861
+ console.log(pc6.cyan(`
1862
+ Adapter created successfully!
1863
+
1864
+ Next steps:
1865
+ 1. cd packages/adapter-${adapterName}
1866
+ 2. Implement your adapter logic in src/index.ts
1867
+ 3. Run: pnpm build
1868
+ 4. Test: pnpm test
1869
+
1870
+ Documentation:
1871
+ https://flight.dev/docs/adapters/custom
1872
+ `));
1873
+ }
1874
+ function generateAdapterFiles(name, packageName, type, includeValidation) {
1875
+ const pascalName = name.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
1876
+ return {
1877
+ "package.json": generatePackageJson(packageName, includeValidation),
1878
+ "tsconfig.json": generateTsConfig(),
1879
+ "tsup.config.ts": generateTsupConfig(),
1880
+ "src/index.ts": generateAdapterSource(name, pascalName, type, includeValidation),
1881
+ "tests/adapter.test.ts": generateAdapterTest(name, pascalName),
1882
+ "README.md": generateReadme(name, packageName)
1883
+ };
1884
+ }
1885
+ function generatePackageJson(packageName, includeValidation) {
1886
+ const pkg = {
1887
+ name: packageName,
1888
+ version: "0.1.0",
1889
+ description: `Flight adapter for custom platform`,
1890
+ type: "module",
1891
+ main: "./dist/index.js",
1892
+ types: "./dist/index.d.ts",
1893
+ exports: {
1894
+ ".": {
1895
+ types: "./dist/index.d.ts",
1896
+ import: "./dist/index.js"
1897
+ }
1898
+ },
1899
+ scripts: {
1900
+ build: "tsup",
1901
+ test: "vitest run",
1902
+ "test:watch": "vitest"
1903
+ },
1904
+ dependencies: {
1905
+ "@flight-framework/core": "^0.4.0"
1906
+ },
1907
+ devDependencies: {
1908
+ tsup: "^8.0.0",
1909
+ typescript: "^5.0.0",
1910
+ vitest: "^2.0.0",
1911
+ ...includeValidation ? { zod: "^3.23.0" } : {}
1912
+ },
1913
+ peerDependencies: includeValidation ? {
1914
+ zod: "^3.0.0"
1915
+ } : void 0,
1916
+ peerDependenciesMeta: includeValidation ? {
1917
+ zod: { optional: true }
1918
+ } : void 0
1919
+ };
1920
+ return JSON.stringify(pkg, null, 2);
1921
+ }
1922
+ function generateTsConfig() {
1923
+ return JSON.stringify({
1924
+ extends: "../../tsconfig.base.json",
1925
+ compilerOptions: {
1926
+ outDir: "./dist",
1927
+ rootDir: "./src"
1928
+ },
1929
+ include: ["src"]
1930
+ }, null, 2);
1931
+ }
1932
+ function generateTsupConfig() {
1933
+ return `import { defineConfig } from 'tsup';
1934
+
1935
+ export default defineConfig({
1936
+ entry: ['src/index.ts'],
1937
+ format: ['esm'],
1938
+ dts: true,
1939
+ clean: true,
1940
+ target: 'node20',
1941
+ });
1942
+ `;
1943
+ }
1944
+ function generateAdapterSource(name, pascalName, type, includeValidation) {
1945
+ if (includeValidation) {
1946
+ return `/**
1947
+ * Flight Adapter - ${pascalName}
1948
+ *
1949
+ * Custom deployment adapter with optional Zod validation.
1950
+ */
1951
+
1952
+ import { createValidatedAdapter, type AdapterBuilder } from '@flight-framework/core/adapters';
1953
+ import { z } from 'zod';
1954
+
1955
+ // ============================================================================
1956
+ // Options Schema (Optional - Zod validation)
1957
+ // ============================================================================
1958
+
1959
+ const optionsSchema = z.object({
1960
+ /** Server port */
1961
+ port: z.number().default(3000),
1962
+ /** Enable health check endpoint */
1963
+ healthCheck: z.boolean().default(true),
1964
+ /** Custom environment variables */
1965
+ env: z.record(z.string()).optional(),
1966
+ });
1967
+
1968
+ export type ${pascalName}AdapterOptions = z.infer<typeof optionsSchema>;
1969
+
1970
+ // ============================================================================
1971
+ // Adapter Implementation
1972
+ // ============================================================================
1973
+
1974
+ export default createValidatedAdapter('${name}', optionsSchema, (options) => ({
1975
+ async adapt(builder: AdapterBuilder) {
1976
+ builder.log.info(\`Building for ${pascalName} platform...\`);
1977
+ builder.log.info(\`Port: \${options.port}\`);
1978
+
1979
+ // Generate server entry
1980
+ await generateServerEntry(builder, options);
1981
+
1982
+ // Generate platform-specific config
1983
+ await generatePlatformConfig(builder, options);
1984
+
1985
+ builder.log.info('Build complete!');
1986
+ },
1987
+
1988
+ supports: {
1989
+ node: () => ${type === "node" || type === "container"},
1990
+ edge: () => ${type === "edge" || type === "serverless"},
1991
+ streaming: () => ${type === "node"},
1992
+ websockets: () => ${type === "node"},
1993
+ },
1994
+
1995
+ emulate: () => ({
1996
+ env: {
1997
+ ${name.toUpperCase().replace(/-/g, "_")}_PLATFORM: 'true',
1998
+ PORT: String(options.port),
1999
+ },
2000
+ }),
2001
+ }));
2002
+
2003
+ // ============================================================================
2004
+ // Generator Functions
2005
+ // ============================================================================
2006
+
2007
+ async function generateServerEntry(builder: AdapterBuilder, options: ${pascalName}AdapterOptions): Promise<void> {
2008
+ const serverCode = \`
2009
+ import { createUniversalHandler } from '@flight-framework/core/adapters';
2010
+ import manifest from './manifest.js';
2011
+
2012
+ const handler = createUniversalHandler(manifest);
2013
+
2014
+ const server = Bun.serve({
2015
+ port: \${options.port},
2016
+ fetch: handler,
2017
+ });
2018
+
2019
+ console.log(\\\`Server running at http://localhost:\${options.port}\\\`);
2020
+ \`;
2021
+
2022
+ await builder.writeFile('server.ts', serverCode.trim());
2023
+ }
2024
+
2025
+ async function generatePlatformConfig(builder: AdapterBuilder, options: ${pascalName}AdapterOptions): Promise<void> {
2026
+ const config = {
2027
+ name: '${name}-app',
2028
+ port: options.port,
2029
+ healthCheck: options.healthCheck ? '/health' : null,
2030
+ };
2031
+
2032
+ await builder.writeFile('platform.json', JSON.stringify(config, null, 2));
2033
+ }
2034
+ `;
2035
+ }
2036
+ return `/**
2037
+ * Flight Adapter - ${pascalName}
2038
+ *
2039
+ * Custom deployment adapter for ${pascalName} platform.
2040
+ *
2041
+ * @example
2042
+ * \`\`\`typescript
2043
+ * import ${name.replace(/-/g, "")} from '${name.includes("@") ? name : `@flight-framework/adapter-${name}`}';
2044
+ *
2045
+ * export default defineConfig({
2046
+ * adapter: ${name.replace(/-/g, "")}({
2047
+ * port: 3000,
2048
+ * }),
2049
+ * });
2050
+ * \`\`\`
2051
+ */
2052
+
2053
+ import { createAdapter, type AdapterBuilder } from '@flight-framework/core/adapters';
2054
+
2055
+ // ============================================================================
2056
+ // Options Interface
2057
+ // ============================================================================
2058
+
2059
+ export interface ${pascalName}AdapterOptions {
2060
+ /** Server port (default: 3000) */
2061
+ port?: number;
2062
+ /** Enable health check endpoint (default: true) */
2063
+ healthCheck?: boolean;
2064
+ /** Custom environment variables */
2065
+ env?: Record<string, string>;
2066
+ }
2067
+
2068
+ // ============================================================================
2069
+ // Adapter Implementation
2070
+ // ============================================================================
2071
+
2072
+ export default function ${name.replace(/-/g, "")}Adapter(options: ${pascalName}AdapterOptions = {}) {
2073
+ const {
2074
+ port = 3000,
2075
+ healthCheck = true,
2076
+ env = {},
2077
+ } = options;
2078
+
2079
+ return createAdapter({
2080
+ name: '${name}',
2081
+
2082
+ async adapt(builder: AdapterBuilder) {
2083
+ builder.log.info(\`Building for ${pascalName} platform...\`);
2084
+ builder.log.info(\`Port: \${port}\`);
2085
+
2086
+ // Generate server entry
2087
+ await generateServerEntry(builder, port, healthCheck);
2088
+
2089
+ // Generate platform-specific config
2090
+ await generatePlatformConfig(builder, port, healthCheck);
2091
+
2092
+ builder.log.info('Build complete!');
2093
+ },
2094
+
2095
+ supports: {
2096
+ node: () => ${type === "node" || type === "container"},
2097
+ edge: () => ${type === "edge" || type === "serverless"},
2098
+ streaming: () => ${type === "node"},
2099
+ websockets: () => ${type === "node"},
2100
+ },
2101
+
2102
+ emulate: () => ({
2103
+ env: {
2104
+ ${name.toUpperCase().replace(/-/g, "_")}_PLATFORM: 'true',
2105
+ PORT: String(port),
2106
+ ...env,
2107
+ },
2108
+ }),
2109
+ });
2110
+ }
2111
+
2112
+ // ============================================================================
2113
+ // Generator Functions
2114
+ // ============================================================================
2115
+
2116
+ async function generateServerEntry(
2117
+ builder: AdapterBuilder,
2118
+ port: number,
2119
+ healthCheck: boolean
2120
+ ): Promise<void> {
2121
+ const serverCode = \`
2122
+ import { createServer } from 'node:http';
2123
+ import { createUniversalHandler } from '@flight-framework/core/adapters';
2124
+ import manifest from './manifest.js';
2125
+
2126
+ const handler = createUniversalHandler(manifest);
2127
+
2128
+ const server = createServer(async (req, res) => {
2129
+ const url = req.url || '/';
2130
+
2131
+ // Health check endpoint
2132
+ if (url === '/health' && \${healthCheck}) {
2133
+ res.writeHead(200, { 'Content-Type': 'application/json' });
2134
+ res.end(JSON.stringify({ status: 'ok' }));
2135
+ return;
2136
+ }
2137
+
2138
+ // Handle request with Flight
2139
+ const webReq = new Request(\\\`http://localhost\${url}\\\`, {
2140
+ method: req.method,
2141
+ headers: Object.fromEntries(
2142
+ Object.entries(req.headers).filter(([_, v]) => v != null).map(([k, v]) => [k, String(v)])
2143
+ ),
2144
+ });
2145
+
2146
+ const response = await handler(webReq);
2147
+
2148
+ res.writeHead(response.status, Object.fromEntries(response.headers));
2149
+ res.end(await response.text());
2150
+ });
2151
+
2152
+ server.listen(\${port}, () => {
2153
+ console.log(\\\`Server running at http://localhost:\${port}\\\`);
2154
+ });
2155
+ \`;
2156
+
2157
+ await builder.writeFile('server.mjs', serverCode.trim());
2158
+ }
2159
+
2160
+ async function generatePlatformConfig(
2161
+ builder: AdapterBuilder,
2162
+ port: number,
2163
+ healthCheck: boolean
2164
+ ): Promise<void> {
2165
+ const config = {
2166
+ name: '${name}-app',
2167
+ port,
2168
+ healthCheck: healthCheck ? '/health' : null,
2169
+ };
2170
+
2171
+ await builder.writeFile('platform.json', JSON.stringify(config, null, 2));
2172
+ }
2173
+ `;
2174
+ }
2175
+ function generateAdapterTest(name, pascalName) {
2176
+ return `/**
2177
+ * ${pascalName} Adapter Tests
2178
+ */
2179
+
2180
+ import { describe, it, expect, vi } from 'vitest';
2181
+ import adapter from '../src/index.js';
2182
+
2183
+ describe('adapter-${name}', () => {
2184
+ const createMockBuilder = () => ({
2185
+ manifest: {
2186
+ entries: {},
2187
+ files: [],
2188
+ routes: [],
2189
+ },
2190
+ root: '/test',
2191
+ outDir: '/test/dist',
2192
+ readFile: vi.fn(),
2193
+ writeFile: vi.fn(),
2194
+ copy: vi.fn(),
2195
+ glob: vi.fn(),
2196
+ log: {
2197
+ info: vi.fn(),
2198
+ warn: vi.fn(),
2199
+ error: vi.fn(),
2200
+ },
2201
+ });
2202
+
2203
+ it('should have correct name', () => {
2204
+ const instance = adapter();
2205
+ expect(instance.name).toBe('${name}');
2206
+ });
2207
+
2208
+ it('should generate server entry', async () => {
2209
+ const instance = adapter({ port: 4000 });
2210
+ const builder = createMockBuilder();
2211
+
2212
+ await instance.adapt(builder);
2213
+
2214
+ expect(builder.writeFile).toHaveBeenCalledWith(
2215
+ 'server.mjs',
2216
+ expect.stringContaining('4000')
2217
+ );
2218
+ });
2219
+
2220
+ it('should generate platform config', async () => {
2221
+ const instance = adapter();
2222
+ const builder = createMockBuilder();
2223
+
2224
+ await instance.adapt(builder);
2225
+
2226
+ expect(builder.writeFile).toHaveBeenCalledWith(
2227
+ 'platform.json',
2228
+ expect.any(String)
2229
+ );
2230
+ });
2231
+
2232
+ it('should declare supported features', () => {
2233
+ const instance = adapter();
2234
+
2235
+ expect(instance.supports?.node?.()).toBeDefined();
2236
+ expect(instance.supports?.streaming?.()).toBeDefined();
2237
+ });
2238
+
2239
+ it('should provide emulation environment', () => {
2240
+ const instance = adapter({ port: 5000 });
2241
+ const emulation = instance.emulate?.();
2242
+
2243
+ expect(emulation?.env?.PORT).toBe('5000');
2244
+ expect(emulation?.env?.${name.toUpperCase().replace(/-/g, "_")}_PLATFORM).toBe('true');
2245
+ });
2246
+ });
2247
+ `;
2248
+ }
2249
+ function generateReadme(name, packageName) {
2250
+ return `# ${packageName}
2251
+
2252
+ Flight adapter for ${name} platform.
2253
+
2254
+ ## Installation
2255
+
2256
+ \`\`\`bash
2257
+ npm install ${packageName}
2258
+ \`\`\`
2259
+
2260
+ ## Usage
2261
+
2262
+ \`\`\`typescript
2263
+ import { defineConfig } from '@flight-framework/core';
2264
+ import ${name.replace(/-/g, "")} from '${packageName}';
2265
+
2266
+ export default defineConfig({
2267
+ adapter: ${name.replace(/-/g, "")}({
2268
+ port: 3000,
2269
+ healthCheck: true,
2270
+ }),
2271
+ });
2272
+ \`\`\`
2273
+
2274
+ ## Options
2275
+
2276
+ | Option | Type | Default | Description |
2277
+ |--------|------|---------|-------------|
2278
+ | \`port\` | \`number\` | \`3000\` | Server port |
2279
+ | \`healthCheck\` | \`boolean\` | \`true\` | Enable health check endpoint |
2280
+ | \`env\` | \`Record<string, string>\` | \`{}\` | Custom environment variables |
2281
+
2282
+ ## License
2283
+
2284
+ MIT
2285
+ `;
2286
+ }
2287
+
1807
2288
  // src/index.ts
1808
2289
  var cli = cac("flight");
1809
2290
  var LOGO = `
1810
- ${pc6.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
1811
- ${pc6.cyan(" \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D")}
1812
- ${pc6.cyan(" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
1813
- ${pc6.cyan(" \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
1814
- ${pc6.cyan(" \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 ")}
1815
- ${pc6.cyan(" \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ")}
2291
+ ${pc7.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
2292
+ ${pc7.cyan(" \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D")}
2293
+ ${pc7.cyan(" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
2294
+ ${pc7.cyan(" \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
2295
+ ${pc7.cyan(" \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 ")}
2296
+ ${pc7.cyan(" \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ")}
1816
2297
 
1817
- ${pc6.dim("The Agnostic Full-Stack Framework")}
1818
- ${pc6.dim("Maximum Flexibility. Zero Lock-in.")}
2298
+ ${pc7.dim("The Agnostic Full-Stack Framework")}
2299
+ ${pc7.dim("Maximum Flexibility. Zero Lock-in.")}
1819
2300
  `;
1820
2301
  function printLogo() {
1821
2302
  console.log(LOGO);
@@ -1829,6 +2310,7 @@ cli.command("build", "Build for production").option("--outDir <dir>", "Output di
1829
2310
  cli.command("preview", "Preview production build").option("-p, --port <port>", "Port to listen on").option("-h, --host <host>", "Host to bind to").option("--open", "Open browser on start").action(previewCommand);
1830
2311
  cli.command("routes:generate", "Generate route manifest from routes directory").option("--routesDir <dir>", "Routes directory", { default: "src/routes" }).option("--outputDir <dir>", "Output directory", { default: "src/.flight" }).action(routesGenerateCommand);
1831
2312
  cli.command("types:generate", "Generate TypeScript types for routes and environment").option("--routesDir <dir>", "Routes directory", { default: "src/routes" }).option("--outputDir <dir>", "Output directory", { default: "src/.flight" }).option("--routes", "Generate route types", { default: true }).option("--env", "Generate environment variable types").option("--watch", "Watch for changes and regenerate").action(typesGenerateCommand);
2313
+ cli.command("adapter:create <name>", "Create a new adapter package").option("-t, --type <type>", "Platform type (node, edge, container, serverless)", { default: "node" }).option("--validation", "Include Zod validation example").option("--outputDir <dir>", "Output directory").action(adapterCreateCommand);
1832
2314
  function run() {
1833
2315
  try {
1834
2316
  cli.parse(process.argv, { run: false });
@@ -1837,7 +2319,7 @@ function run() {
1837
2319
  }
1838
2320
  cli.runMatchedCommand();
1839
2321
  } catch (error) {
1840
- console.error(pc6.red("Error:"), error instanceof Error ? error.message : error);
2322
+ console.error(pc7.red("Error:"), error instanceof Error ? error.message : error);
1841
2323
  process.exit(1);
1842
2324
  }
1843
2325
  }