@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.
- package/LICENSE +21 -21
- package/README.md +544 -544
- package/dist/bin.js +975 -947
- package/dist/bin.js.map +1 -1
- package/dist/index.js +975 -947
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/templates/angular/index.html +13 -13
- package/templates/angular/package.json.template +25 -25
- package/templates/angular/src/app.component.ts +13 -13
- package/templates/angular/src/main.server.ts +11 -11
- package/templates/angular/src/main.ts +4 -4
- package/templates/angular/tsconfig.json +16 -16
- package/templates/base/README.md.template +26 -26
- package/templates/base/_gitignore +25 -25
- package/templates/base/flight.config.ts.template +15 -15
- package/templates/base/styles/global.css +58 -58
- package/templates/htmx/index.html +18 -18
- package/templates/htmx/package.json.template +18 -18
- package/templates/htmx/vite.config.ts +6 -6
- package/templates/lit/index.html +14 -14
- package/templates/lit/package.json.template +21 -21
- package/templates/lit/src/app-root.ts +18 -18
- package/templates/lit/src/entry-client.ts +5 -5
- package/templates/lit/src/entry-server.ts +9 -9
- package/templates/lit/tsconfig.json +18 -18
- package/templates/lit/vite.config.ts +6 -6
- package/templates/preact/index.html +14 -14
- package/templates/preact/package.json.template +22 -22
- package/templates/preact/src/App.tsx +8 -8
- package/templates/preact/src/entry-client.tsx +11 -11
- package/templates/preact/src/entry-server.tsx +6 -6
- package/templates/preact/tsconfig.json +18 -18
- package/templates/preact/vite.config.ts +8 -8
- package/templates/qwik/index.html +14 -14
- package/templates/qwik/package.json.template +20 -20
- package/templates/qwik/src/App.tsx +10 -10
- package/templates/qwik/src/entry-client.tsx +4 -4
- package/templates/qwik/src/entry-server.tsx +9 -9
- package/templates/qwik/tsconfig.json +18 -18
- package/templates/qwik/vite.config.ts +8 -8
- package/templates/react/index.html +13 -13
- package/templates/react/package.json.template +24 -24
- package/templates/react/src/App.tsx +13 -13
- package/templates/react/src/context/RouterContext.tsx +63 -63
- package/templates/react/src/entry-client.tsx +19 -19
- package/templates/react/src/entry-server.tsx +17 -17
- package/templates/react/tsconfig.json +19 -19
- package/templates/react/vite.config.ts +12 -12
- package/templates/solid/index.html +14 -14
- package/templates/solid/package.json.template +21 -21
- package/templates/solid/src/App.tsx +8 -8
- package/templates/solid/src/entry-client.tsx +11 -11
- package/templates/solid/src/entry-server.tsx +6 -6
- package/templates/solid/tsconfig.json +18 -18
- package/templates/solid/vite.config.ts +8 -8
- package/templates/svelte/index.html +14 -14
- package/templates/svelte/package.json.template +21 -21
- package/templates/svelte/src/App.svelte +4 -4
- package/templates/svelte/src/entry-client.ts +7 -7
- package/templates/svelte/src/entry-server.ts +7 -7
- package/templates/svelte/tsconfig.json +17 -17
- package/templates/svelte/vite.config.ts +8 -8
- package/templates/use-cases/api/README.md +41 -41
- package/templates/use-cases/api/package.json.template +14 -14
- package/templates/use-cases/api/src/routes/api/health.get.ts.template +3 -3
- package/templates/use-cases/blog/README.md +47 -47
- package/templates/use-cases/blog/flight.config.ts.template +11 -11
- package/templates/use-cases/blog/package.json.template +15 -15
- package/templates/use-cases/blog/src/routes/blog/[slug].page.tsx.template +23 -23
- package/templates/use-cases/blog/src/routes/index.page.tsx.template +9 -9
- package/templates/use-cases/docs/README.md +49 -49
- package/templates/use-cases/docs/package.json.template +15 -15
- package/templates/use-cases/docs/src/content/index.md.template +16 -16
- package/templates/use-cases/ecommerce/README.md +32 -32
- package/templates/use-cases/ecommerce/package.json.template +16 -16
- package/templates/use-cases/ecommerce/src/routes/index.page.tsx.template +9 -9
- package/templates/use-cases/saas/README.md +34 -34
- package/templates/use-cases/saas/package.json.template +15 -15
- package/templates/use-cases/saas/src/routes/index.page.tsx.template +9 -9
- package/templates/vanilla/index.html +14 -14
- package/templates/vanilla/package.json.template +19 -19
- package/templates/vanilla/src/main.ts +10 -10
- package/templates/vanilla/tsconfig.json +16 -16
- package/templates/vanilla/vite.config.ts +6 -6
- package/templates/vue/index.html +14 -14
- package/templates/vue/package.json.template +21 -21
- package/templates/vue/src/App.vue +6 -6
- package/templates/vue/src/entry-client.ts +12 -12
- package/templates/vue/src/entry-server.ts +8 -8
- package/templates/vue/tsconfig.json +17 -17
- 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
|
|
513
|
-
import { readFileSync as
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
} catch {
|
|
670
|
+
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)(\?)?=/i);
|
|
671
|
+
if (!match) {
|
|
672
|
+
continue;
|
|
541
673
|
}
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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
|
-
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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
|
-
|
|
667
|
-
server.listen(port, listenHost, () => {
|
|
668
|
-
console.log(pc2.green(` \u2713 SSR server listening`));
|
|
669
|
-
});
|
|
708
|
+
}
|
|
709
|
+
return merged;
|
|
670
710
|
}
|
|
671
|
-
function
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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/
|
|
858
|
-
|
|
859
|
-
|
|
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(
|
|
874
|
+
console.log(pc2.cyan("\n Starting Flight development server...\n"));
|
|
864
875
|
try {
|
|
865
|
-
const root =
|
|
866
|
-
const config = await
|
|
867
|
-
const
|
|
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 ??
|
|
870
|
-
const
|
|
871
|
-
const
|
|
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
|
-
|
|
874
|
-
|
|
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
|
-
|
|
879
|
-
|
|
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
|
-
${
|
|
949
|
+
${pc2.green("\u2713")} Flight dev server ready in ${pc2.bold(elapsed + "ms")}
|
|
884
950
|
|
|
885
|
-
${
|
|
886
|
-
${host === true || host === "0.0.0.0" ? ` ${
|
|
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
|
-
${
|
|
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
|
-
|
|
960
|
+
if (!isSSR) {
|
|
961
|
+
vite.bindCLIShortcuts({ print: true });
|
|
962
|
+
}
|
|
892
963
|
} catch (error) {
|
|
893
|
-
console.error(
|
|
964
|
+
console.error(pc2.red("\nFailed to start dev server:"), error);
|
|
894
965
|
process.exit(1);
|
|
895
966
|
}
|
|
896
967
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
}
|
|
915
|
-
|
|
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
|
|
945
|
-
|
|
946
|
-
const
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
if (
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
|
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
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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
|
|
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
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
}
|
|
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
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
|
|
1292
|
+
if (urlPath === "" || urlPath === "/") {
|
|
1293
|
+
urlPath = "/";
|
|
1294
|
+
}
|
|
1295
|
+
urlPath = urlPath.replace(/\/+/g, "/");
|
|
1296
|
+
return urlPath;
|
|
1354
1297
|
}
|
|
1355
|
-
function
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
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
|
|
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
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1406
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
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
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
};
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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");
|