@flight-framework/cli 0.2.0 → 0.2.1

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 +975 -947
  4. package/dist/bin.js.map +1 -1
  5. package/dist/index.js +975 -947
  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
@@ -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
772
  }
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
- }
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()
1058
- };
1059
- }
1060
- function generateRoutesFile(manifest, outputDir) {
1061
- if (!existsSync4(outputDir)) {
1062
- mkdirSync2(outputDir, { recursive: true });
1063
- }
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"};
1123
-
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
- */
1132
-
1133
- import type { RouteParams } from '@flight-framework/router';
1134
-
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;
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
+ }
1103
+ };
1161
1104
  }
1162
-
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");
1168
- try {
1169
- const manifest = await generateRoutes({
1170
- routesDir,
1171
- outputDir,
1172
- watch: options.watch
1173
- });
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}`);
1178
- } catch (error) {
1179
- console.error("Failed to generate routes:", error);
1180
- process.exit(1);
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);
1116
+ }
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
+ }
1181
1128
  }
1129
+ return new Request(url.toString(), {
1130
+ method: req.method || "GET",
1131
+ headers,
1132
+ body
1133
+ });
1182
1134
  }
1183
-
1184
- // src/commands/types-generate.ts
1185
- import { resolve as resolve7 } from "path";
1186
-
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
- */`;
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 || "/");
1199
1140
  }
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)));
1205
- }
1206
- const catchAllMatches = routePath.match(/\*(\w+)/g);
1207
- if (catchAllMatches) {
1208
- params.push(...catchAllMatches.map((m) => m.slice(1)));
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 {
1209
1153
  }
1210
- return params;
1211
- }
1212
- function isDynamicRoute(routePath) {
1213
- return routePath.includes(":") || routePath.includes("*");
1154
+ return "0.0.0.0";
1214
1155
  }
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
1156
 
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>;
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)`)}
1291
1224
 
1292
- // ============================================================================
1293
- // Helper Types
1294
- // ============================================================================
1225
+ ${pc3.cyan("Output:")} ${resolve4(root, outDir)}
1295
1226
 
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;
1227
+ ${pc3.dim("To preview the build:")}
1228
+ ${pc3.dim("$")} flight preview
1301
1229
 
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 };
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
+ }
1238
+ }
1309
1239
 
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;
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"));
1247
+ try {
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
+ }
1264
+ });
1265
+ console.log(`
1266
+ ${pc4.green("\u2713")} Flight preview server ready
1317
1267
 
1318
- // ============================================================================
1319
- // Individual Route Parameter Types
1320
- // ============================================================================
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}/`)}` : ""}
1321
1270
 
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";
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();
1275
+ } catch (error) {
1276
+ console.error(pc4.red("\nFailed to start preview server:"), error);
1277
+ process.exit(1);
1278
+ }
1330
1279
  }
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
- }
1280
+
1281
+ // src/commands/routes-generate.ts
1282
+ import { resolve as resolve6 } from "path";
1283
+
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;
1352
1291
  }
1353
- return { server, client };
1292
+ if (urlPath === "" || urlPath === "/") {
1293
+ urlPath = "/";
1294
+ }
1295
+ urlPath = urlPath.replace(/\/+/g, "/");
1296
+ return urlPath;
1354
1297
  }
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);
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;
1301
+ }
1302
+ function isLayoutFile(filename) {
1303
+ return filename.startsWith("_layout.");
1304
+ }
1305
+ function isLoadingFile(filename) {
1306
+ return filename.startsWith("_loading.");
1307
+ }
1308
+ function isErrorFile(filename) {
1309
+ return filename.startsWith("_error.");
1310
+ }
1311
+ function isNotFoundFile(filename) {
1312
+ return filename.startsWith("_not-found.");
1313
+ }
1314
+ function isRouteFile(filename) {
1315
+ return /\.(page|route)\.(tsx?|jsx?)$/.test(filename);
1316
+ }
1317
+ function isApiRouteFile(filename) {
1318
+ return /\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/.test(filename);
1319
+ }
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);
1328
+ for (const entry of entries) {
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") {
1334
+ continue;
1370
1335
  }
1371
- }
1372
- for (const envVar of parsed.client) {
1373
- if (!seenKeys.has(envVar.key)) {
1374
- seenKeys.add(envVar.key);
1375
- merged.client.push(envVar);
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
+ });
1376
1409
  }
1377
1410
  }
1378
1411
  }
1379
- return merged;
1412
+ return results;
1413
+ }
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
+ };
1380
1442
  }
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")}
1443
+ function generateRoutesFile(manifest, outputDir) {
1444
+ if (!existsSync5(outputDir)) {
1445
+ mkdirSync3(outputDir, { recursive: true });
1446
+ }
1447
+ const routesContent = `/**
1448
+ * Auto-generated by Flight CLI
1449
+ * Do not edit manually
1450
+ * Generated: ${manifest.generated}
1451
+ */
1385
1452
 
1386
- // ============================================================================
1387
- // Server-side Environment Variables
1388
- // ============================================================================
1453
+ import type { RouteDefinition } from '@flight-framework/router';
1389
1454
 
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
- }
1398
- }
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
+ ];
1399
1462
 
1400
- // ============================================================================
1401
- // Client-side Environment Variables
1402
- // ============================================================================
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
+ ];
1403
1470
 
1404
- /**
1405
- * Client-side environment variables accessible via import.meta.env
1406
- * Only variables with PUBLIC_ prefix are included.
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}
1407
1514
  */
1408
- interface ImportMetaEnv {
1409
- ${clientVars || " // No client environment variables defined"}
1410
- }
1411
1515
 
1412
- interface ImportMeta {
1413
- readonly env: ImportMetaEnv;
1414
- }
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")}
1415
1525
  `;
1526
+ writeFileSync3(join4(outputDir, "types.ts"), typesContent, "utf-8");
1416
1527
  }
1417
- function scanRoutesForTypes(routesDir) {
1418
- if (!existsSync5(routesDir)) {
1419
- return [];
1420
- }
1421
- const routes = [];
1422
- scanDirectoryRecursive(routesDir, "", routes);
1423
- return routes;
1424
- }
1425
- function scanDirectoryRecursive(dir, basePath, results) {
1426
- const entries = readdirSync3(dir, { withFileTypes: true });
1427
- 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") {
1432
- continue;
1433
- }
1434
- scanDirectoryRecursive(fullPath, relativePath, results);
1435
- } else if (entry.isFile()) {
1436
- const route = parseRouteFile(entry.name, relativePath);
1437
- if (route) {
1438
- results.push(route);
1439
- }
1440
- }
1441
- }
1442
- }
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
- };
1463
- }
1464
- return null;
1465
- }
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");