@flight-framework/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +544 -544
- package/dist/bin.js +1433 -951
- package/dist/bin.js.map +1 -1
- package/dist/index.js +1433 -951
- 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
|
@@ -8,7 +8,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
8
8
|
|
|
9
9
|
// src/index.ts
|
|
10
10
|
import { cac } from "cac";
|
|
11
|
-
import
|
|
11
|
+
import pc7 from "picocolors";
|
|
12
12
|
|
|
13
13
|
// src/version.ts
|
|
14
14
|
var VERSION = "0.0.1";
|
|
@@ -509,1035 +509,1063 @@ ${pc.cyan("Happy flying!")}
|
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
// src/commands/dev.ts
|
|
512
|
-
import { resolve as
|
|
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
|
-
}
|
|
752
|
-
function matchPath(pattern, path) {
|
|
753
|
-
const cleanPath = path.split("?")[0];
|
|
754
|
-
const regexPattern = pattern.replace(/:\w+/g, "[^/]+").replace(/\*/g, ".*");
|
|
755
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
756
|
-
return regex.test(cleanPath || "/");
|
|
757
772
|
}
|
|
758
|
-
function
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
1056
|
+
name: "flight:dev",
|
|
1057
|
+
configureServer(server) {
|
|
1058
|
+
server.middlewares.use(async (req, res, next) => {
|
|
1059
|
+
const url = req.url || "/";
|
|
1060
|
+
if (url.startsWith("/__flight_action/")) {
|
|
1061
|
+
try {
|
|
1062
|
+
const { handleActionRequest } = await import("@flight-framework/core");
|
|
1063
|
+
const webRequest = await nodeToWebRequest(req);
|
|
1064
|
+
const response = await handleActionRequest(webRequest);
|
|
1065
|
+
res.statusCode = response.status;
|
|
1066
|
+
response.headers.forEach((value, key) => {
|
|
1067
|
+
res.setHeader(key, value);
|
|
1068
|
+
});
|
|
1069
|
+
const body = await response.text();
|
|
1070
|
+
res.end(body);
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
console.error("[Flight] Action error:", error);
|
|
1073
|
+
res.statusCode = 500;
|
|
1074
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
1075
|
+
}
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (url.startsWith("/api/")) {
|
|
1079
|
+
try {
|
|
1080
|
+
const { createFileRouter } = await import("@flight-framework/core/file-router");
|
|
1081
|
+
const routesDir = join3(root, "src", "routes");
|
|
1082
|
+
const router = await createFileRouter({ directory: routesDir });
|
|
1083
|
+
const route = router.routes.find((r) => {
|
|
1084
|
+
return matchPath(r.path, url);
|
|
1085
|
+
});
|
|
1086
|
+
if (route && route.handler) {
|
|
1087
|
+
const webRequest = await nodeToWebRequest(req);
|
|
1088
|
+
const response = await route.handler({ req: webRequest, params: {} });
|
|
1089
|
+
res.statusCode = response.status;
|
|
1090
|
+
response.headers.forEach((value, key) => {
|
|
1091
|
+
res.setHeader(key, value);
|
|
1092
|
+
});
|
|
1093
|
+
const body = await response.text();
|
|
1094
|
+
res.end(body);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
next();
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1058
1103
|
};
|
|
1059
1104
|
}
|
|
1060
|
-
function
|
|
1061
|
-
|
|
1062
|
-
|
|
1105
|
+
async function nodeToWebRequest(req) {
|
|
1106
|
+
const host = req.headers.host || "localhost";
|
|
1107
|
+
const protocol = "http";
|
|
1108
|
+
const url = new URL(req.url || "/", `${protocol}://${host}`);
|
|
1109
|
+
let body = null;
|
|
1110
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1111
|
+
const chunks = [];
|
|
1112
|
+
for await (const chunk of req) {
|
|
1113
|
+
chunks.push(chunk);
|
|
1114
|
+
}
|
|
1115
|
+
body = Buffer.concat(chunks);
|
|
1063
1116
|
}
|
|
1064
|
-
const
|
|
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
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
// Not Found Page Components
|
|
1105
|
-
export const notFoundPages = [
|
|
1106
|
-
${manifest.notFoundPages.map((r) => ` {
|
|
1107
|
-
path: '${r.path}',
|
|
1108
|
-
component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
1109
|
-
},`).join("\n")}
|
|
1110
|
-
];
|
|
1111
|
-
|
|
1112
|
-
// API Routes
|
|
1113
|
-
export const apiRoutes = [
|
|
1114
|
-
${manifest.apiRoutes.map((r) => ` {
|
|
1115
|
-
path: '${r.path}',
|
|
1116
|
-
method: '${r.httpMethod}',
|
|
1117
|
-
handler: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
1118
|
-
},`).join("\n")}
|
|
1119
|
-
];
|
|
1120
|
-
|
|
1121
|
-
// Type-safe route paths
|
|
1122
|
-
export type AppRoutes = ${manifest.routes.length > 0 ? manifest.routes.map((r) => `'${r.path}'`).join(" | ") : "never"};
|
|
1117
|
+
const headers = new Headers();
|
|
1118
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
1119
|
+
if (value) {
|
|
1120
|
+
if (Array.isArray(value)) {
|
|
1121
|
+
for (const v of value) {
|
|
1122
|
+
headers.append(key, v);
|
|
1123
|
+
}
|
|
1124
|
+
} else {
|
|
1125
|
+
headers.set(key, value);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return new Request(url.toString(), {
|
|
1130
|
+
method: req.method || "GET",
|
|
1131
|
+
headers,
|
|
1132
|
+
body
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
function matchPath(pattern, path) {
|
|
1136
|
+
const cleanPath = path.split("?")[0];
|
|
1137
|
+
const regexPattern = pattern.replace(/:\w+/g, "[^/]+").replace(/\*/g, ".*");
|
|
1138
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
1139
|
+
return regex.test(cleanPath || "/");
|
|
1140
|
+
}
|
|
1141
|
+
function getNetworkAddress() {
|
|
1142
|
+
try {
|
|
1143
|
+
const { networkInterfaces } = __require("os");
|
|
1144
|
+
const nets = networkInterfaces();
|
|
1145
|
+
for (const name of Object.keys(nets)) {
|
|
1146
|
+
for (const net of nets[name]) {
|
|
1147
|
+
if (net.family === "IPv4" && !net.internal) {
|
|
1148
|
+
return net.address;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
} catch {
|
|
1153
|
+
}
|
|
1154
|
+
return "0.0.0.0";
|
|
1155
|
+
}
|
|
1123
1156
|
|
|
1124
|
-
//
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1157
|
+
// src/commands/build.ts
|
|
1158
|
+
import { resolve as resolve4 } from "path";
|
|
1159
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1160
|
+
import pc3 from "picocolors";
|
|
1161
|
+
import { loadConfig as loadConfig2 } from "@flight-framework/core/config";
|
|
1162
|
+
function findEntryServer(root, srcDir) {
|
|
1163
|
+
const tsxPath = resolve4(root, srcDir, "entry-server.tsx");
|
|
1164
|
+
const tsPath = resolve4(root, srcDir, "entry-server.ts");
|
|
1165
|
+
if (existsSync4(tsxPath)) return tsxPath;
|
|
1166
|
+
if (existsSync4(tsPath)) return tsPath;
|
|
1167
|
+
return tsxPath;
|
|
1168
|
+
}
|
|
1169
|
+
async function buildCommand(options) {
|
|
1170
|
+
const startTime = Date.now();
|
|
1171
|
+
printLogo();
|
|
1172
|
+
console.log(pc3.cyan("\n[*] Building Flight project for production...\n"));
|
|
1173
|
+
try {
|
|
1174
|
+
const root = resolve4(process.cwd());
|
|
1175
|
+
const config = await loadConfig2(root);
|
|
1176
|
+
const outDir = options.outDir ?? config.build.outDir;
|
|
1177
|
+
const sourcemap = options.sourcemap ?? config.build.sourcemap;
|
|
1178
|
+
const minify = options.minify ?? config.build.minify;
|
|
1179
|
+
const { build } = await import("vite");
|
|
1180
|
+
console.log(pc3.dim(`Output directory: ${outDir}`));
|
|
1181
|
+
console.log(pc3.dim(`Sourcemaps: ${sourcemap ? "enabled" : "disabled"}`));
|
|
1182
|
+
console.log(pc3.dim(`Minification: ${minify ? "enabled" : "disabled"}
|
|
1183
|
+
`));
|
|
1184
|
+
console.log(pc3.cyan("Building client..."));
|
|
1185
|
+
await build({
|
|
1186
|
+
root,
|
|
1187
|
+
mode: "production",
|
|
1188
|
+
build: {
|
|
1189
|
+
outDir: `${outDir}/client`,
|
|
1190
|
+
sourcemap,
|
|
1191
|
+
minify: minify ? "esbuild" : false,
|
|
1192
|
+
rollupOptions: {
|
|
1193
|
+
input: resolve4(root, "index.html")
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
console.log(pc3.green("\u2713") + " Client build complete");
|
|
1198
|
+
if (config.rendering.default !== "csr") {
|
|
1199
|
+
console.log(pc3.cyan("\nBuilding server..."));
|
|
1200
|
+
await build({
|
|
1201
|
+
root,
|
|
1202
|
+
mode: "production",
|
|
1203
|
+
build: {
|
|
1204
|
+
outDir: `${outDir}/server`,
|
|
1205
|
+
sourcemap,
|
|
1206
|
+
minify: minify ? "esbuild" : false,
|
|
1207
|
+
ssr: true,
|
|
1208
|
+
rollupOptions: {
|
|
1209
|
+
input: findEntryServer(root, config.build.srcDir)
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
});
|
|
1213
|
+
console.log(pc3.green("\u2713") + " Server build complete");
|
|
1214
|
+
}
|
|
1215
|
+
if (config.adapter) {
|
|
1216
|
+
console.log(pc3.cyan(`
|
|
1217
|
+
Running ${config.adapter.name} adapter...`));
|
|
1218
|
+
console.log(pc3.green("\u2713") + ` ${config.adapter.name} adapter complete`);
|
|
1219
|
+
}
|
|
1220
|
+
const elapsed = Date.now() - startTime;
|
|
1221
|
+
const elapsedSeconds = (elapsed / 1e3).toFixed(2);
|
|
1222
|
+
console.log(`
|
|
1223
|
+
${pc3.green("[OK] Build complete!")} ${pc3.dim(`(${elapsedSeconds}s)`)}
|
|
1132
1224
|
|
|
1133
|
-
|
|
1225
|
+
${pc3.cyan("Output:")} ${resolve4(root, outDir)}
|
|
1134
1226
|
|
|
1135
|
-
|
|
1136
|
-
${
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
const { routesDir, outputDir } = options;
|
|
1147
|
-
console.log(`Scanning routes in: ${routesDir}`);
|
|
1148
|
-
const manifest = generateRouteManifest(routesDir);
|
|
1149
|
-
const stats = [
|
|
1150
|
-
`${manifest.routes.length} pages`,
|
|
1151
|
-
`${manifest.layouts.length} layouts`,
|
|
1152
|
-
`${manifest.loadingStates.length} loading states`,
|
|
1153
|
-
`${manifest.errorBoundaries.length} error boundaries`,
|
|
1154
|
-
`${manifest.notFoundPages.length} not-found pages`,
|
|
1155
|
-
`${manifest.apiRoutes.length} API routes`
|
|
1156
|
-
].join(", ");
|
|
1157
|
-
console.log(`Found: ${stats}`);
|
|
1158
|
-
generateRoutesFile(manifest, outputDir);
|
|
1159
|
-
console.log(`Generated route manifest in: ${outputDir}`);
|
|
1160
|
-
return manifest;
|
|
1227
|
+
${pc3.dim("To preview the build:")}
|
|
1228
|
+
${pc3.dim("$")} flight preview
|
|
1229
|
+
|
|
1230
|
+
${pc3.dim("To deploy:")}
|
|
1231
|
+
${pc3.dim("\u2022")} Upload ${outDir}/ to your server
|
|
1232
|
+
${pc3.dim("\u2022")} Or use your configured adapter
|
|
1233
|
+
`);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
console.error(pc3.red("\nBuild failed:"), error);
|
|
1236
|
+
process.exit(1);
|
|
1237
|
+
}
|
|
1161
1238
|
}
|
|
1162
1239
|
|
|
1163
|
-
// src/commands/
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1240
|
+
// src/commands/preview.ts
|
|
1241
|
+
import { resolve as resolve5 } from "path";
|
|
1242
|
+
import pc4 from "picocolors";
|
|
1243
|
+
import { loadConfig as loadConfig3 } from "@flight-framework/core/config";
|
|
1244
|
+
async function previewCommand(options) {
|
|
1245
|
+
printLogo();
|
|
1246
|
+
console.log(pc4.cyan("\n\u2708\uFE0F Starting Flight preview server...\n"));
|
|
1168
1247
|
try {
|
|
1169
|
-
const
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1248
|
+
const root = resolve5(process.cwd());
|
|
1249
|
+
const config = await loadConfig3(root);
|
|
1250
|
+
const port = options.port ? parseInt(options.port, 10) : config.dev.port + 1;
|
|
1251
|
+
const host = options.host ?? config.dev.host;
|
|
1252
|
+
const open = options.open ?? false;
|
|
1253
|
+
const { preview } = await import("vite");
|
|
1254
|
+
const server = await preview({
|
|
1255
|
+
root,
|
|
1256
|
+
preview: {
|
|
1257
|
+
port,
|
|
1258
|
+
host: host === true ? "0.0.0.0" : host,
|
|
1259
|
+
open
|
|
1260
|
+
},
|
|
1261
|
+
build: {
|
|
1262
|
+
outDir: config.build.outDir
|
|
1263
|
+
}
|
|
1173
1264
|
});
|
|
1174
|
-
console.log(
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1265
|
+
console.log(`
|
|
1266
|
+
${pc4.green("\u2713")} Flight preview server ready
|
|
1267
|
+
|
|
1268
|
+
${pc4.cyan("\u279C")} Local: ${pc4.cyan(`http://localhost:${port}/`)}
|
|
1269
|
+
${host === true || host === "0.0.0.0" ? ` ${pc4.cyan("\u279C")} Network: ${pc4.cyan(`http://0.0.0.0:${port}/`)}` : ""}
|
|
1270
|
+
|
|
1271
|
+
${pc4.dim("This is a preview of your production build.")}
|
|
1272
|
+
${pc4.dim("For development, use")} ${pc4.bold("flight dev")}
|
|
1273
|
+
`);
|
|
1274
|
+
server.printUrls();
|
|
1178
1275
|
} catch (error) {
|
|
1179
|
-
console.error("
|
|
1276
|
+
console.error(pc4.red("\nFailed to start preview server:"), error);
|
|
1180
1277
|
process.exit(1);
|
|
1181
1278
|
}
|
|
1182
1279
|
}
|
|
1183
1280
|
|
|
1184
|
-
// src/commands/
|
|
1185
|
-
import { resolve as
|
|
1281
|
+
// src/commands/routes-generate.ts
|
|
1282
|
+
import { resolve as resolve6 } from "path";
|
|
1186
1283
|
|
|
1187
|
-
// src/generators/
|
|
1188
|
-
import {
|
|
1189
|
-
import { join as join4 } from "path";
|
|
1190
|
-
function
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
* Do not edit manually - changes will be overwritten
|
|
1195
|
-
*
|
|
1196
|
-
* Command: ${command}
|
|
1197
|
-
* Generated: ${timestamp}
|
|
1198
|
-
*/`;
|
|
1199
|
-
}
|
|
1200
|
-
function extractParams(routePath) {
|
|
1201
|
-
const params = [];
|
|
1202
|
-
const dynamicMatches = routePath.match(/:(\w+)/g);
|
|
1203
|
-
if (dynamicMatches) {
|
|
1204
|
-
params.push(...dynamicMatches.map((m) => m.slice(1)));
|
|
1284
|
+
// src/generators/routes.ts
|
|
1285
|
+
import { readdirSync as readdirSync3, statSync as statSync2, existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
1286
|
+
import { join as join4, dirname as dirname3 } from "path";
|
|
1287
|
+
function filePathToUrlPath2(filePath) {
|
|
1288
|
+
let urlPath = filePath.replace(/\.(page|route)\.(tsx?|jsx?)$/, "").replace(/\.(get|post|put|patch|delete|options|head)$/, "").replace(/\/index$/, "").replace(/\/?\([^)]+\)/g, "").replace(/\[\.\.\.(\w+)\]/g, "*$1").replace(/\[(\w+)\]/g, ":$1");
|
|
1289
|
+
if (!urlPath.startsWith("/")) {
|
|
1290
|
+
urlPath = "/" + urlPath;
|
|
1205
1291
|
}
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
params.push(...catchAllMatches.map((m) => m.slice(1)));
|
|
1292
|
+
if (urlPath === "" || urlPath === "/") {
|
|
1293
|
+
urlPath = "/";
|
|
1209
1294
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
function isDynamicRoute(routePath) {
|
|
1213
|
-
return routePath.includes(":") || routePath.includes("*");
|
|
1214
|
-
}
|
|
1215
|
-
function generateRouteTypes(routes) {
|
|
1216
|
-
const pageRoutes = routes.filter((r) => !r.isApiRoute);
|
|
1217
|
-
const apiRoutes = routes.filter((r) => r.isApiRoute);
|
|
1218
|
-
const staticRoutes = pageRoutes.filter((r) => !r.isDynamic);
|
|
1219
|
-
const dynamicRoutes = pageRoutes.filter((r) => r.isDynamic);
|
|
1220
|
-
const appRoutesUnion = pageRoutes.length > 0 ? pageRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
|
|
1221
|
-
const apiRoutesUnion = apiRoutes.length > 0 ? apiRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
|
|
1222
|
-
const staticRoutesUnion = staticRoutes.length > 0 ? staticRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
|
|
1223
|
-
const dynamicRoutesUnion = dynamicRoutes.length > 0 ? dynamicRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
|
|
1224
|
-
const paramTypes = dynamicRoutes.map((r) => {
|
|
1225
|
-
const params = extractParams(r.path);
|
|
1226
|
-
const typeName = routePathToTypeName(r.path);
|
|
1227
|
-
const paramDef = params.map((p) => `${p}: string`).join("; ");
|
|
1228
|
-
return `export type ${typeName}Params = { ${paramDef} };`;
|
|
1229
|
-
}).join("\n");
|
|
1230
|
-
return `${generateHeader("flight types:generate --routes")}
|
|
1231
|
-
|
|
1232
|
-
// ============================================================================
|
|
1233
|
-
// Route Types
|
|
1234
|
-
// ============================================================================
|
|
1235
|
-
|
|
1236
|
-
/**
|
|
1237
|
-
* All available page routes in the application
|
|
1238
|
-
*/
|
|
1239
|
-
export type AppRoutes =
|
|
1240
|
-
${appRoutesUnion};
|
|
1241
|
-
|
|
1242
|
-
/**
|
|
1243
|
-
* All available API routes in the application
|
|
1244
|
-
*/
|
|
1245
|
-
export type ApiRoutes =
|
|
1246
|
-
${apiRoutesUnion};
|
|
1247
|
-
|
|
1248
|
-
/**
|
|
1249
|
-
* Static routes (no dynamic parameters)
|
|
1250
|
-
*/
|
|
1251
|
-
export type StaticRoutes =
|
|
1252
|
-
${staticRoutesUnion};
|
|
1253
|
-
|
|
1254
|
-
/**
|
|
1255
|
-
* Dynamic routes (with parameters like :id or *slug)
|
|
1256
|
-
*/
|
|
1257
|
-
export type DynamicRoutes =
|
|
1258
|
-
${dynamicRoutesUnion};
|
|
1259
|
-
|
|
1260
|
-
// ============================================================================
|
|
1261
|
-
// Route Parameter Extraction
|
|
1262
|
-
// ============================================================================
|
|
1263
|
-
|
|
1264
|
-
/**
|
|
1265
|
-
* Extract route parameters from a route pattern using template literal types.
|
|
1266
|
-
*
|
|
1267
|
-
* @example
|
|
1268
|
-
* type Params = ExtractRouteParams<'/users/:id'>; // { id: string }
|
|
1269
|
-
* type BlogParams = ExtractRouteParams<'/blog/:year/:slug'>; // { year: string; slug: string }
|
|
1270
|
-
*/
|
|
1271
|
-
type ExtractRouteParams<T extends string> =
|
|
1272
|
-
// Handle :param/rest pattern
|
|
1273
|
-
T extends \`\${infer _Start}:\${infer Param}/\${infer Rest}\`
|
|
1274
|
-
? { [K in Param]: string } & ExtractRouteParams<\`/\${Rest}\`>
|
|
1275
|
-
// Handle :param at end
|
|
1276
|
-
: T extends \`\${infer _Start}:\${infer Param}\`
|
|
1277
|
-
? { [K in Param]: string }
|
|
1278
|
-
// Handle *param (catch-all)
|
|
1279
|
-
: T extends \`\${infer _Start}*\${infer Param}\`
|
|
1280
|
-
? { [K in Param]: string }
|
|
1281
|
-
// No params
|
|
1282
|
-
: Record<string, never>;
|
|
1283
|
-
|
|
1284
|
-
/**
|
|
1285
|
-
* Get typed parameters for a specific route.
|
|
1286
|
-
*
|
|
1287
|
-
* @example
|
|
1288
|
-
* const params: RouteParams<'/users/:id'> = { id: '123' };
|
|
1289
|
-
*/
|
|
1290
|
-
export type RouteParams<T extends AppRoutes | ApiRoutes> = ExtractRouteParams<T>;
|
|
1291
|
-
|
|
1292
|
-
// ============================================================================
|
|
1293
|
-
// Helper Types
|
|
1294
|
-
// ============================================================================
|
|
1295
|
-
|
|
1296
|
-
/**
|
|
1297
|
-
* Check if a route requires parameters
|
|
1298
|
-
*/
|
|
1299
|
-
export type RequiresParams<T extends AppRoutes | ApiRoutes> =
|
|
1300
|
-
RouteParams<T> extends Record<string, never> ? false : true;
|
|
1301
|
-
|
|
1302
|
-
/**
|
|
1303
|
-
* Props for a type-safe Link component
|
|
1304
|
-
*/
|
|
1305
|
-
export type TypedLinkProps<T extends AppRoutes> =
|
|
1306
|
-
RequiresParams<T> extends true
|
|
1307
|
-
? { to: T; params: RouteParams<T> }
|
|
1308
|
-
: { to: T; params?: never };
|
|
1309
|
-
|
|
1310
|
-
/**
|
|
1311
|
-
* Build a URL from a route pattern and parameters
|
|
1312
|
-
*/
|
|
1313
|
-
export type BuildUrl<T extends AppRoutes> =
|
|
1314
|
-
RequiresParams<T> extends true
|
|
1315
|
-
? (route: T, params: RouteParams<T>) => string
|
|
1316
|
-
: (route: T) => string;
|
|
1317
|
-
|
|
1318
|
-
// ============================================================================
|
|
1319
|
-
// Individual Route Parameter Types
|
|
1320
|
-
// ============================================================================
|
|
1321
|
-
|
|
1322
|
-
${paramTypes || "// No dynamic routes found"}
|
|
1323
|
-
`;
|
|
1324
|
-
}
|
|
1325
|
-
function routePathToTypeName(routePath) {
|
|
1326
|
-
return routePath.split("/").filter(Boolean).map((segment) => {
|
|
1327
|
-
const clean = segment.replace(/^[:*]/, "");
|
|
1328
|
-
return clean.charAt(0).toUpperCase() + clean.slice(1);
|
|
1329
|
-
}).join("_") || "Root";
|
|
1295
|
+
urlPath = urlPath.replace(/\/+/g, "/");
|
|
1296
|
+
return urlPath;
|
|
1330
1297
|
}
|
|
1331
|
-
function
|
|
1332
|
-
const
|
|
1333
|
-
|
|
1334
|
-
const lines = content.split("\n");
|
|
1335
|
-
for (const line of lines) {
|
|
1336
|
-
const trimmed = line.trim();
|
|
1337
|
-
if (!trimmed || trimmed.startsWith("#")) {
|
|
1338
|
-
continue;
|
|
1339
|
-
}
|
|
1340
|
-
const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)(\?)?=/i);
|
|
1341
|
-
if (!match) {
|
|
1342
|
-
continue;
|
|
1343
|
-
}
|
|
1344
|
-
const key = match[1];
|
|
1345
|
-
const optional = match[2] === "?";
|
|
1346
|
-
const envVar = { key, optional };
|
|
1347
|
-
if (key.startsWith("PUBLIC_")) {
|
|
1348
|
-
client.push(envVar);
|
|
1349
|
-
} else {
|
|
1350
|
-
server.push(envVar);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
return { server, client };
|
|
1298
|
+
function extractHttpMethod(filename) {
|
|
1299
|
+
const match = filename.match(/\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/i);
|
|
1300
|
+
return match ? match[1].toUpperCase() : void 0;
|
|
1354
1301
|
}
|
|
1355
|
-
function
|
|
1356
|
-
|
|
1357
|
-
const merged = { server: [], client: [] };
|
|
1358
|
-
const seenKeys = /* @__PURE__ */ new Set();
|
|
1359
|
-
for (const envFile of envFiles) {
|
|
1360
|
-
const envPath = join4(projectRoot, envFile);
|
|
1361
|
-
if (!existsSync5(envPath)) {
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1364
|
-
const content = readFileSync3(envPath, "utf-8");
|
|
1365
|
-
const parsed = parseEnvFile(content);
|
|
1366
|
-
for (const envVar of parsed.server) {
|
|
1367
|
-
if (!seenKeys.has(envVar.key)) {
|
|
1368
|
-
seenKeys.add(envVar.key);
|
|
1369
|
-
merged.server.push(envVar);
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
for (const envVar of parsed.client) {
|
|
1373
|
-
if (!seenKeys.has(envVar.key)) {
|
|
1374
|
-
seenKeys.add(envVar.key);
|
|
1375
|
-
merged.client.push(envVar);
|
|
1376
|
-
}
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
return merged;
|
|
1302
|
+
function isLayoutFile(filename) {
|
|
1303
|
+
return filename.startsWith("_layout.");
|
|
1380
1304
|
}
|
|
1381
|
-
function
|
|
1382
|
-
|
|
1383
|
-
const clientVars = env.client.sort((a, b) => a.key.localeCompare(b.key)).map((v) => ` ${v.key}${v.optional ? "?" : ""}: string;`).join("\n");
|
|
1384
|
-
return `${generateHeader("flight types:generate --env")}
|
|
1385
|
-
|
|
1386
|
-
// ============================================================================
|
|
1387
|
-
// Server-side Environment Variables
|
|
1388
|
-
// ============================================================================
|
|
1389
|
-
|
|
1390
|
-
/**
|
|
1391
|
-
* Server-side environment variables accessible via process.env
|
|
1392
|
-
* These are NOT exposed to the client.
|
|
1393
|
-
*/
|
|
1394
|
-
declare namespace NodeJS {
|
|
1395
|
-
interface ProcessEnv {
|
|
1396
|
-
${serverVars || " // No server environment variables defined"}
|
|
1397
|
-
}
|
|
1305
|
+
function isLoadingFile(filename) {
|
|
1306
|
+
return filename.startsWith("_loading.");
|
|
1398
1307
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
// Client-side Environment Variables
|
|
1402
|
-
// ============================================================================
|
|
1403
|
-
|
|
1404
|
-
/**
|
|
1405
|
-
* Client-side environment variables accessible via import.meta.env
|
|
1406
|
-
* Only variables with PUBLIC_ prefix are included.
|
|
1407
|
-
*/
|
|
1408
|
-
interface ImportMetaEnv {
|
|
1409
|
-
${clientVars || " // No client environment variables defined"}
|
|
1308
|
+
function isErrorFile(filename) {
|
|
1309
|
+
return filename.startsWith("_error.");
|
|
1410
1310
|
}
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
readonly env: ImportMetaEnv;
|
|
1311
|
+
function isNotFoundFile(filename) {
|
|
1312
|
+
return filename.startsWith("_not-found.");
|
|
1414
1313
|
}
|
|
1415
|
-
|
|
1314
|
+
function isRouteFile(filename) {
|
|
1315
|
+
return /\.(page|route)\.(tsx?|jsx?)$/.test(filename);
|
|
1416
1316
|
}
|
|
1417
|
-
function
|
|
1418
|
-
|
|
1419
|
-
return [];
|
|
1420
|
-
}
|
|
1421
|
-
const routes = [];
|
|
1422
|
-
scanDirectoryRecursive(routesDir, "", routes);
|
|
1423
|
-
return routes;
|
|
1317
|
+
function isApiRouteFile(filename) {
|
|
1318
|
+
return /\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/.test(filename);
|
|
1424
1319
|
}
|
|
1425
|
-
function
|
|
1426
|
-
|
|
1320
|
+
function hasDynamicSegments(path) {
|
|
1321
|
+
return path.includes("[") && path.includes("]");
|
|
1322
|
+
}
|
|
1323
|
+
function scanDirectory(dir, basePath = "", results = []) {
|
|
1324
|
+
if (!existsSync5(dir)) {
|
|
1325
|
+
return results;
|
|
1326
|
+
}
|
|
1327
|
+
const entries = readdirSync3(dir);
|
|
1427
1328
|
for (const entry of entries) {
|
|
1428
|
-
const fullPath = join4(dir, entry
|
|
1429
|
-
const relativePath = join4(basePath, entry
|
|
1430
|
-
|
|
1431
|
-
|
|
1329
|
+
const fullPath = join4(dir, entry);
|
|
1330
|
+
const relativePath = join4(basePath, entry);
|
|
1331
|
+
const stat = statSync2(fullPath);
|
|
1332
|
+
if (stat.isDirectory()) {
|
|
1333
|
+
if (entry.startsWith(".") || entry === "node_modules") {
|
|
1432
1334
|
continue;
|
|
1433
1335
|
}
|
|
1434
|
-
|
|
1435
|
-
} else if (
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1438
|
-
|
|
1336
|
+
scanDirectory(fullPath, relativePath, results);
|
|
1337
|
+
} else if (stat.isFile()) {
|
|
1338
|
+
const routePath = filePathToUrlPath2(dirname3(relativePath));
|
|
1339
|
+
const normalizedFilePath = relativePath.replace(/\\/g, "/");
|
|
1340
|
+
const isDynamic = hasDynamicSegments(relativePath);
|
|
1341
|
+
if (isLayoutFile(entry)) {
|
|
1342
|
+
results.push({
|
|
1343
|
+
path: routePath,
|
|
1344
|
+
filePath: normalizedFilePath,
|
|
1345
|
+
isLayout: true,
|
|
1346
|
+
isLoading: false,
|
|
1347
|
+
isError: false,
|
|
1348
|
+
isNotFound: false,
|
|
1349
|
+
isDynamic,
|
|
1350
|
+
isApiRoute: false
|
|
1351
|
+
});
|
|
1352
|
+
} else if (isLoadingFile(entry)) {
|
|
1353
|
+
results.push({
|
|
1354
|
+
path: routePath,
|
|
1355
|
+
filePath: normalizedFilePath,
|
|
1356
|
+
isLayout: false,
|
|
1357
|
+
isLoading: true,
|
|
1358
|
+
isError: false,
|
|
1359
|
+
isNotFound: false,
|
|
1360
|
+
isDynamic,
|
|
1361
|
+
isApiRoute: false
|
|
1362
|
+
});
|
|
1363
|
+
} else if (isErrorFile(entry)) {
|
|
1364
|
+
results.push({
|
|
1365
|
+
path: routePath,
|
|
1366
|
+
filePath: normalizedFilePath,
|
|
1367
|
+
isLayout: false,
|
|
1368
|
+
isLoading: false,
|
|
1369
|
+
isError: true,
|
|
1370
|
+
isNotFound: false,
|
|
1371
|
+
isDynamic,
|
|
1372
|
+
isApiRoute: false
|
|
1373
|
+
});
|
|
1374
|
+
} else if (isNotFoundFile(entry)) {
|
|
1375
|
+
results.push({
|
|
1376
|
+
path: routePath,
|
|
1377
|
+
filePath: normalizedFilePath,
|
|
1378
|
+
isLayout: false,
|
|
1379
|
+
isLoading: false,
|
|
1380
|
+
isError: false,
|
|
1381
|
+
isNotFound: true,
|
|
1382
|
+
isDynamic,
|
|
1383
|
+
isApiRoute: false
|
|
1384
|
+
});
|
|
1385
|
+
} else if (isApiRouteFile(entry)) {
|
|
1386
|
+
const method = extractHttpMethod(entry);
|
|
1387
|
+
results.push({
|
|
1388
|
+
path: filePathToUrlPath2(relativePath),
|
|
1389
|
+
filePath: normalizedFilePath,
|
|
1390
|
+
isLayout: false,
|
|
1391
|
+
isLoading: false,
|
|
1392
|
+
isError: false,
|
|
1393
|
+
isNotFound: false,
|
|
1394
|
+
isDynamic,
|
|
1395
|
+
isApiRoute: true,
|
|
1396
|
+
httpMethod: method
|
|
1397
|
+
});
|
|
1398
|
+
} else if (isRouteFile(entry)) {
|
|
1399
|
+
results.push({
|
|
1400
|
+
path: filePathToUrlPath2(relativePath),
|
|
1401
|
+
filePath: normalizedFilePath,
|
|
1402
|
+
isLayout: false,
|
|
1403
|
+
isLoading: false,
|
|
1404
|
+
isError: false,
|
|
1405
|
+
isNotFound: false,
|
|
1406
|
+
isDynamic,
|
|
1407
|
+
isApiRoute: false
|
|
1408
|
+
});
|
|
1439
1409
|
}
|
|
1440
1410
|
}
|
|
1441
1411
|
}
|
|
1412
|
+
return results;
|
|
1442
1413
|
}
|
|
1443
|
-
function
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
return
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1414
|
+
function sortRoutes(routes) {
|
|
1415
|
+
return routes.sort((a, b) => {
|
|
1416
|
+
if (!a.isDynamic && b.isDynamic) return -1;
|
|
1417
|
+
if (a.isDynamic && !b.isDynamic) return 1;
|
|
1418
|
+
const aSegments = a.path.split("/").length;
|
|
1419
|
+
const bSegments = b.path.split("/").length;
|
|
1420
|
+
if (aSegments !== bSegments) return aSegments - bSegments;
|
|
1421
|
+
return a.path.localeCompare(b.path);
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
function generateRouteManifest(routesDir) {
|
|
1425
|
+
const allRoutes = scanDirectory(routesDir);
|
|
1426
|
+
const isPageRoute = (r) => !r.isLayout && !r.isLoading && !r.isError && !r.isNotFound && !r.isApiRoute;
|
|
1427
|
+
const routes = sortRoutes(allRoutes.filter(isPageRoute));
|
|
1428
|
+
const layouts = allRoutes.filter((r) => r.isLayout);
|
|
1429
|
+
const loadingStates = allRoutes.filter((r) => r.isLoading);
|
|
1430
|
+
const errorBoundaries = allRoutes.filter((r) => r.isError);
|
|
1431
|
+
const notFoundPages = allRoutes.filter((r) => r.isNotFound);
|
|
1432
|
+
const apiRoutes = sortRoutes(allRoutes.filter((r) => r.isApiRoute));
|
|
1433
|
+
return {
|
|
1434
|
+
routes,
|
|
1435
|
+
layouts,
|
|
1436
|
+
loadingStates,
|
|
1437
|
+
errorBoundaries,
|
|
1438
|
+
notFoundPages,
|
|
1439
|
+
apiRoutes,
|
|
1440
|
+
generated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
function generateRoutesFile(manifest, outputDir) {
|
|
1444
|
+
if (!existsSync5(outputDir)) {
|
|
1445
|
+
mkdirSync3(outputDir, { recursive: true });
|
|
1463
1446
|
}
|
|
1464
|
-
|
|
1447
|
+
const routesContent = `/**
|
|
1448
|
+
* Auto-generated by Flight CLI
|
|
1449
|
+
* Do not edit manually
|
|
1450
|
+
* Generated: ${manifest.generated}
|
|
1451
|
+
*/
|
|
1452
|
+
|
|
1453
|
+
import type { RouteDefinition } from '@flight-framework/router';
|
|
1454
|
+
|
|
1455
|
+
// Page Routes
|
|
1456
|
+
export const routes: RouteDefinition[] = [
|
|
1457
|
+
${manifest.routes.map((r) => ` {
|
|
1458
|
+
path: '${r.path}',
|
|
1459
|
+
component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
1460
|
+
},`).join("\n")}
|
|
1461
|
+
];
|
|
1462
|
+
|
|
1463
|
+
// Layout Components
|
|
1464
|
+
export const layouts = [
|
|
1465
|
+
${manifest.layouts.map((r) => ` {
|
|
1466
|
+
path: '${r.path}',
|
|
1467
|
+
component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
1468
|
+
},`).join("\n")}
|
|
1469
|
+
];
|
|
1470
|
+
|
|
1471
|
+
// Loading State Components
|
|
1472
|
+
export const loadingStates = [
|
|
1473
|
+
${manifest.loadingStates.map((r) => ` {
|
|
1474
|
+
path: '${r.path}',
|
|
1475
|
+
component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
1476
|
+
},`).join("\n")}
|
|
1477
|
+
];
|
|
1478
|
+
|
|
1479
|
+
// Error Boundary Components
|
|
1480
|
+
export const errorBoundaries = [
|
|
1481
|
+
${manifest.errorBoundaries.map((r) => ` {
|
|
1482
|
+
path: '${r.path}',
|
|
1483
|
+
component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
1484
|
+
},`).join("\n")}
|
|
1485
|
+
];
|
|
1486
|
+
|
|
1487
|
+
// Not Found Page Components
|
|
1488
|
+
export const notFoundPages = [
|
|
1489
|
+
${manifest.notFoundPages.map((r) => ` {
|
|
1490
|
+
path: '${r.path}',
|
|
1491
|
+
component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
1492
|
+
},`).join("\n")}
|
|
1493
|
+
];
|
|
1494
|
+
|
|
1495
|
+
// API Routes
|
|
1496
|
+
export const apiRoutes = [
|
|
1497
|
+
${manifest.apiRoutes.map((r) => ` {
|
|
1498
|
+
path: '${r.path}',
|
|
1499
|
+
method: '${r.httpMethod}',
|
|
1500
|
+
handler: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
1501
|
+
},`).join("\n")}
|
|
1502
|
+
];
|
|
1503
|
+
|
|
1504
|
+
// Type-safe route paths
|
|
1505
|
+
export type AppRoutes = ${manifest.routes.length > 0 ? manifest.routes.map((r) => `'${r.path}'`).join(" | ") : "never"};
|
|
1506
|
+
|
|
1507
|
+
// Type-safe API route paths
|
|
1508
|
+
export type ApiRoutes = ${manifest.apiRoutes.length > 0 ? manifest.apiRoutes.map((r) => `'${r.path}'`).join(" | ") : "never"};
|
|
1509
|
+
`;
|
|
1510
|
+
writeFileSync3(join4(outputDir, "routes.ts"), routesContent, "utf-8");
|
|
1511
|
+
const typesContent = `/**
|
|
1512
|
+
* Auto-generated route types
|
|
1513
|
+
* Generated: ${manifest.generated}
|
|
1514
|
+
*/
|
|
1515
|
+
|
|
1516
|
+
import type { RouteParams } from '@flight-framework/router';
|
|
1517
|
+
|
|
1518
|
+
// Extract params from route patterns
|
|
1519
|
+
${manifest.routes.filter((r) => r.isDynamic).map((r) => {
|
|
1520
|
+
const paramMatches = r.path.match(/:(\w+)/g) || [];
|
|
1521
|
+
const params = paramMatches.map((p) => p.slice(1));
|
|
1522
|
+
const typeName = r.path.replace(/[/:]/g, "_").replace(/^_/, "").replace(/_$/, "") || "Root";
|
|
1523
|
+
return `export type ${typeName}Params = { ${params.map((p) => `${p}: string`).join("; ")} };`;
|
|
1524
|
+
}).join("\n")}
|
|
1525
|
+
`;
|
|
1526
|
+
writeFileSync3(join4(outputDir, "types.ts"), typesContent, "utf-8");
|
|
1465
1527
|
}
|
|
1466
|
-
function
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
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");
|
|
@@ -1804,18 +1832,471 @@ ${pc5.cyan("Quick example:")}
|
|
|
1804
1832
|
console.log(`${pc5.dim("Docs:")} https://flight.dev/docs/packages/${packageName}`);
|
|
1805
1833
|
}
|
|
1806
1834
|
|
|
1835
|
+
// src/commands/adapter-create.ts
|
|
1836
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1837
|
+
import { join as join6 } from "path";
|
|
1838
|
+
import pc6 from "picocolors";
|
|
1839
|
+
async function adapterCreateCommand(name, options = {}) {
|
|
1840
|
+
const cwd = process.cwd();
|
|
1841
|
+
const adapterName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1842
|
+
const packageName = `@flight-framework/adapter-${adapterName}`;
|
|
1843
|
+
const outputDir = options.outputDir || join6(cwd, "packages", `adapter-${adapterName}`);
|
|
1844
|
+
const type = options.type || "node";
|
|
1845
|
+
const includeValidation = options.validation || false;
|
|
1846
|
+
console.log(pc6.cyan(`
|
|
1847
|
+
Creating adapter: ${packageName}
|
|
1848
|
+
`));
|
|
1849
|
+
if (existsSync7(outputDir)) {
|
|
1850
|
+
console.error(pc6.red(` Error: Directory already exists: ${outputDir}`));
|
|
1851
|
+
process.exit(1);
|
|
1852
|
+
}
|
|
1853
|
+
mkdirSync4(join6(outputDir, "src"), { recursive: true });
|
|
1854
|
+
mkdirSync4(join6(outputDir, "tests"), { recursive: true });
|
|
1855
|
+
const files = generateAdapterFiles(adapterName, packageName, type, includeValidation);
|
|
1856
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
1857
|
+
const fullPath = join6(outputDir, filePath);
|
|
1858
|
+
writeFileSync5(fullPath, content, "utf-8");
|
|
1859
|
+
console.log(pc6.green(` \u2713 Created ${filePath}`));
|
|
1860
|
+
}
|
|
1861
|
+
console.log(pc6.cyan(`
|
|
1862
|
+
Adapter created successfully!
|
|
1863
|
+
|
|
1864
|
+
Next steps:
|
|
1865
|
+
1. cd packages/adapter-${adapterName}
|
|
1866
|
+
2. Implement your adapter logic in src/index.ts
|
|
1867
|
+
3. Run: pnpm build
|
|
1868
|
+
4. Test: pnpm test
|
|
1869
|
+
|
|
1870
|
+
Documentation:
|
|
1871
|
+
https://flight.dev/docs/adapters/custom
|
|
1872
|
+
`));
|
|
1873
|
+
}
|
|
1874
|
+
function generateAdapterFiles(name, packageName, type, includeValidation) {
|
|
1875
|
+
const pascalName = name.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
1876
|
+
return {
|
|
1877
|
+
"package.json": generatePackageJson(packageName, includeValidation),
|
|
1878
|
+
"tsconfig.json": generateTsConfig(),
|
|
1879
|
+
"tsup.config.ts": generateTsupConfig(),
|
|
1880
|
+
"src/index.ts": generateAdapterSource(name, pascalName, type, includeValidation),
|
|
1881
|
+
"tests/adapter.test.ts": generateAdapterTest(name, pascalName),
|
|
1882
|
+
"README.md": generateReadme(name, packageName)
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
function generatePackageJson(packageName, includeValidation) {
|
|
1886
|
+
const pkg = {
|
|
1887
|
+
name: packageName,
|
|
1888
|
+
version: "0.1.0",
|
|
1889
|
+
description: `Flight adapter for custom platform`,
|
|
1890
|
+
type: "module",
|
|
1891
|
+
main: "./dist/index.js",
|
|
1892
|
+
types: "./dist/index.d.ts",
|
|
1893
|
+
exports: {
|
|
1894
|
+
".": {
|
|
1895
|
+
types: "./dist/index.d.ts",
|
|
1896
|
+
import: "./dist/index.js"
|
|
1897
|
+
}
|
|
1898
|
+
},
|
|
1899
|
+
scripts: {
|
|
1900
|
+
build: "tsup",
|
|
1901
|
+
test: "vitest run",
|
|
1902
|
+
"test:watch": "vitest"
|
|
1903
|
+
},
|
|
1904
|
+
dependencies: {
|
|
1905
|
+
"@flight-framework/core": "^0.4.0"
|
|
1906
|
+
},
|
|
1907
|
+
devDependencies: {
|
|
1908
|
+
tsup: "^8.0.0",
|
|
1909
|
+
typescript: "^5.0.0",
|
|
1910
|
+
vitest: "^2.0.0",
|
|
1911
|
+
...includeValidation ? { zod: "^3.23.0" } : {}
|
|
1912
|
+
},
|
|
1913
|
+
peerDependencies: includeValidation ? {
|
|
1914
|
+
zod: "^3.0.0"
|
|
1915
|
+
} : void 0,
|
|
1916
|
+
peerDependenciesMeta: includeValidation ? {
|
|
1917
|
+
zod: { optional: true }
|
|
1918
|
+
} : void 0
|
|
1919
|
+
};
|
|
1920
|
+
return JSON.stringify(pkg, null, 2);
|
|
1921
|
+
}
|
|
1922
|
+
function generateTsConfig() {
|
|
1923
|
+
return JSON.stringify({
|
|
1924
|
+
extends: "../../tsconfig.base.json",
|
|
1925
|
+
compilerOptions: {
|
|
1926
|
+
outDir: "./dist",
|
|
1927
|
+
rootDir: "./src"
|
|
1928
|
+
},
|
|
1929
|
+
include: ["src"]
|
|
1930
|
+
}, null, 2);
|
|
1931
|
+
}
|
|
1932
|
+
function generateTsupConfig() {
|
|
1933
|
+
return `import { defineConfig } from 'tsup';
|
|
1934
|
+
|
|
1935
|
+
export default defineConfig({
|
|
1936
|
+
entry: ['src/index.ts'],
|
|
1937
|
+
format: ['esm'],
|
|
1938
|
+
dts: true,
|
|
1939
|
+
clean: true,
|
|
1940
|
+
target: 'node20',
|
|
1941
|
+
});
|
|
1942
|
+
`;
|
|
1943
|
+
}
|
|
1944
|
+
function generateAdapterSource(name, pascalName, type, includeValidation) {
|
|
1945
|
+
if (includeValidation) {
|
|
1946
|
+
return `/**
|
|
1947
|
+
* Flight Adapter - ${pascalName}
|
|
1948
|
+
*
|
|
1949
|
+
* Custom deployment adapter with optional Zod validation.
|
|
1950
|
+
*/
|
|
1951
|
+
|
|
1952
|
+
import { createValidatedAdapter, type AdapterBuilder } from '@flight-framework/core/adapters';
|
|
1953
|
+
import { z } from 'zod';
|
|
1954
|
+
|
|
1955
|
+
// ============================================================================
|
|
1956
|
+
// Options Schema (Optional - Zod validation)
|
|
1957
|
+
// ============================================================================
|
|
1958
|
+
|
|
1959
|
+
const optionsSchema = z.object({
|
|
1960
|
+
/** Server port */
|
|
1961
|
+
port: z.number().default(3000),
|
|
1962
|
+
/** Enable health check endpoint */
|
|
1963
|
+
healthCheck: z.boolean().default(true),
|
|
1964
|
+
/** Custom environment variables */
|
|
1965
|
+
env: z.record(z.string()).optional(),
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
export type ${pascalName}AdapterOptions = z.infer<typeof optionsSchema>;
|
|
1969
|
+
|
|
1970
|
+
// ============================================================================
|
|
1971
|
+
// Adapter Implementation
|
|
1972
|
+
// ============================================================================
|
|
1973
|
+
|
|
1974
|
+
export default createValidatedAdapter('${name}', optionsSchema, (options) => ({
|
|
1975
|
+
async adapt(builder: AdapterBuilder) {
|
|
1976
|
+
builder.log.info(\`Building for ${pascalName} platform...\`);
|
|
1977
|
+
builder.log.info(\`Port: \${options.port}\`);
|
|
1978
|
+
|
|
1979
|
+
// Generate server entry
|
|
1980
|
+
await generateServerEntry(builder, options);
|
|
1981
|
+
|
|
1982
|
+
// Generate platform-specific config
|
|
1983
|
+
await generatePlatformConfig(builder, options);
|
|
1984
|
+
|
|
1985
|
+
builder.log.info('Build complete!');
|
|
1986
|
+
},
|
|
1987
|
+
|
|
1988
|
+
supports: {
|
|
1989
|
+
node: () => ${type === "node" || type === "container"},
|
|
1990
|
+
edge: () => ${type === "edge" || type === "serverless"},
|
|
1991
|
+
streaming: () => ${type === "node"},
|
|
1992
|
+
websockets: () => ${type === "node"},
|
|
1993
|
+
},
|
|
1994
|
+
|
|
1995
|
+
emulate: () => ({
|
|
1996
|
+
env: {
|
|
1997
|
+
${name.toUpperCase().replace(/-/g, "_")}_PLATFORM: 'true',
|
|
1998
|
+
PORT: String(options.port),
|
|
1999
|
+
},
|
|
2000
|
+
}),
|
|
2001
|
+
}));
|
|
2002
|
+
|
|
2003
|
+
// ============================================================================
|
|
2004
|
+
// Generator Functions
|
|
2005
|
+
// ============================================================================
|
|
2006
|
+
|
|
2007
|
+
async function generateServerEntry(builder: AdapterBuilder, options: ${pascalName}AdapterOptions): Promise<void> {
|
|
2008
|
+
const serverCode = \`
|
|
2009
|
+
import { createUniversalHandler } from '@flight-framework/core/adapters';
|
|
2010
|
+
import manifest from './manifest.js';
|
|
2011
|
+
|
|
2012
|
+
const handler = createUniversalHandler(manifest);
|
|
2013
|
+
|
|
2014
|
+
const server = Bun.serve({
|
|
2015
|
+
port: \${options.port},
|
|
2016
|
+
fetch: handler,
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
console.log(\\\`Server running at http://localhost:\${options.port}\\\`);
|
|
2020
|
+
\`;
|
|
2021
|
+
|
|
2022
|
+
await builder.writeFile('server.ts', serverCode.trim());
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
async function generatePlatformConfig(builder: AdapterBuilder, options: ${pascalName}AdapterOptions): Promise<void> {
|
|
2026
|
+
const config = {
|
|
2027
|
+
name: '${name}-app',
|
|
2028
|
+
port: options.port,
|
|
2029
|
+
healthCheck: options.healthCheck ? '/health' : null,
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
await builder.writeFile('platform.json', JSON.stringify(config, null, 2));
|
|
2033
|
+
}
|
|
2034
|
+
`;
|
|
2035
|
+
}
|
|
2036
|
+
return `/**
|
|
2037
|
+
* Flight Adapter - ${pascalName}
|
|
2038
|
+
*
|
|
2039
|
+
* Custom deployment adapter for ${pascalName} platform.
|
|
2040
|
+
*
|
|
2041
|
+
* @example
|
|
2042
|
+
* \`\`\`typescript
|
|
2043
|
+
* import ${name.replace(/-/g, "")} from '${name.includes("@") ? name : `@flight-framework/adapter-${name}`}';
|
|
2044
|
+
*
|
|
2045
|
+
* export default defineConfig({
|
|
2046
|
+
* adapter: ${name.replace(/-/g, "")}({
|
|
2047
|
+
* port: 3000,
|
|
2048
|
+
* }),
|
|
2049
|
+
* });
|
|
2050
|
+
* \`\`\`
|
|
2051
|
+
*/
|
|
2052
|
+
|
|
2053
|
+
import { createAdapter, type AdapterBuilder } from '@flight-framework/core/adapters';
|
|
2054
|
+
|
|
2055
|
+
// ============================================================================
|
|
2056
|
+
// Options Interface
|
|
2057
|
+
// ============================================================================
|
|
2058
|
+
|
|
2059
|
+
export interface ${pascalName}AdapterOptions {
|
|
2060
|
+
/** Server port (default: 3000) */
|
|
2061
|
+
port?: number;
|
|
2062
|
+
/** Enable health check endpoint (default: true) */
|
|
2063
|
+
healthCheck?: boolean;
|
|
2064
|
+
/** Custom environment variables */
|
|
2065
|
+
env?: Record<string, string>;
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// ============================================================================
|
|
2069
|
+
// Adapter Implementation
|
|
2070
|
+
// ============================================================================
|
|
2071
|
+
|
|
2072
|
+
export default function ${name.replace(/-/g, "")}Adapter(options: ${pascalName}AdapterOptions = {}) {
|
|
2073
|
+
const {
|
|
2074
|
+
port = 3000,
|
|
2075
|
+
healthCheck = true,
|
|
2076
|
+
env = {},
|
|
2077
|
+
} = options;
|
|
2078
|
+
|
|
2079
|
+
return createAdapter({
|
|
2080
|
+
name: '${name}',
|
|
2081
|
+
|
|
2082
|
+
async adapt(builder: AdapterBuilder) {
|
|
2083
|
+
builder.log.info(\`Building for ${pascalName} platform...\`);
|
|
2084
|
+
builder.log.info(\`Port: \${port}\`);
|
|
2085
|
+
|
|
2086
|
+
// Generate server entry
|
|
2087
|
+
await generateServerEntry(builder, port, healthCheck);
|
|
2088
|
+
|
|
2089
|
+
// Generate platform-specific config
|
|
2090
|
+
await generatePlatformConfig(builder, port, healthCheck);
|
|
2091
|
+
|
|
2092
|
+
builder.log.info('Build complete!');
|
|
2093
|
+
},
|
|
2094
|
+
|
|
2095
|
+
supports: {
|
|
2096
|
+
node: () => ${type === "node" || type === "container"},
|
|
2097
|
+
edge: () => ${type === "edge" || type === "serverless"},
|
|
2098
|
+
streaming: () => ${type === "node"},
|
|
2099
|
+
websockets: () => ${type === "node"},
|
|
2100
|
+
},
|
|
2101
|
+
|
|
2102
|
+
emulate: () => ({
|
|
2103
|
+
env: {
|
|
2104
|
+
${name.toUpperCase().replace(/-/g, "_")}_PLATFORM: 'true',
|
|
2105
|
+
PORT: String(port),
|
|
2106
|
+
...env,
|
|
2107
|
+
},
|
|
2108
|
+
}),
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// ============================================================================
|
|
2113
|
+
// Generator Functions
|
|
2114
|
+
// ============================================================================
|
|
2115
|
+
|
|
2116
|
+
async function generateServerEntry(
|
|
2117
|
+
builder: AdapterBuilder,
|
|
2118
|
+
port: number,
|
|
2119
|
+
healthCheck: boolean
|
|
2120
|
+
): Promise<void> {
|
|
2121
|
+
const serverCode = \`
|
|
2122
|
+
import { createServer } from 'node:http';
|
|
2123
|
+
import { createUniversalHandler } from '@flight-framework/core/adapters';
|
|
2124
|
+
import manifest from './manifest.js';
|
|
2125
|
+
|
|
2126
|
+
const handler = createUniversalHandler(manifest);
|
|
2127
|
+
|
|
2128
|
+
const server = createServer(async (req, res) => {
|
|
2129
|
+
const url = req.url || '/';
|
|
2130
|
+
|
|
2131
|
+
// Health check endpoint
|
|
2132
|
+
if (url === '/health' && \${healthCheck}) {
|
|
2133
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2134
|
+
res.end(JSON.stringify({ status: 'ok' }));
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
// Handle request with Flight
|
|
2139
|
+
const webReq = new Request(\\\`http://localhost\${url}\\\`, {
|
|
2140
|
+
method: req.method,
|
|
2141
|
+
headers: Object.fromEntries(
|
|
2142
|
+
Object.entries(req.headers).filter(([_, v]) => v != null).map(([k, v]) => [k, String(v)])
|
|
2143
|
+
),
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
const response = await handler(webReq);
|
|
2147
|
+
|
|
2148
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
2149
|
+
res.end(await response.text());
|
|
2150
|
+
});
|
|
2151
|
+
|
|
2152
|
+
server.listen(\${port}, () => {
|
|
2153
|
+
console.log(\\\`Server running at http://localhost:\${port}\\\`);
|
|
2154
|
+
});
|
|
2155
|
+
\`;
|
|
2156
|
+
|
|
2157
|
+
await builder.writeFile('server.mjs', serverCode.trim());
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
async function generatePlatformConfig(
|
|
2161
|
+
builder: AdapterBuilder,
|
|
2162
|
+
port: number,
|
|
2163
|
+
healthCheck: boolean
|
|
2164
|
+
): Promise<void> {
|
|
2165
|
+
const config = {
|
|
2166
|
+
name: '${name}-app',
|
|
2167
|
+
port,
|
|
2168
|
+
healthCheck: healthCheck ? '/health' : null,
|
|
2169
|
+
};
|
|
2170
|
+
|
|
2171
|
+
await builder.writeFile('platform.json', JSON.stringify(config, null, 2));
|
|
2172
|
+
}
|
|
2173
|
+
`;
|
|
2174
|
+
}
|
|
2175
|
+
function generateAdapterTest(name, pascalName) {
|
|
2176
|
+
return `/**
|
|
2177
|
+
* ${pascalName} Adapter Tests
|
|
2178
|
+
*/
|
|
2179
|
+
|
|
2180
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2181
|
+
import adapter from '../src/index.js';
|
|
2182
|
+
|
|
2183
|
+
describe('adapter-${name}', () => {
|
|
2184
|
+
const createMockBuilder = () => ({
|
|
2185
|
+
manifest: {
|
|
2186
|
+
entries: {},
|
|
2187
|
+
files: [],
|
|
2188
|
+
routes: [],
|
|
2189
|
+
},
|
|
2190
|
+
root: '/test',
|
|
2191
|
+
outDir: '/test/dist',
|
|
2192
|
+
readFile: vi.fn(),
|
|
2193
|
+
writeFile: vi.fn(),
|
|
2194
|
+
copy: vi.fn(),
|
|
2195
|
+
glob: vi.fn(),
|
|
2196
|
+
log: {
|
|
2197
|
+
info: vi.fn(),
|
|
2198
|
+
warn: vi.fn(),
|
|
2199
|
+
error: vi.fn(),
|
|
2200
|
+
},
|
|
2201
|
+
});
|
|
2202
|
+
|
|
2203
|
+
it('should have correct name', () => {
|
|
2204
|
+
const instance = adapter();
|
|
2205
|
+
expect(instance.name).toBe('${name}');
|
|
2206
|
+
});
|
|
2207
|
+
|
|
2208
|
+
it('should generate server entry', async () => {
|
|
2209
|
+
const instance = adapter({ port: 4000 });
|
|
2210
|
+
const builder = createMockBuilder();
|
|
2211
|
+
|
|
2212
|
+
await instance.adapt(builder);
|
|
2213
|
+
|
|
2214
|
+
expect(builder.writeFile).toHaveBeenCalledWith(
|
|
2215
|
+
'server.mjs',
|
|
2216
|
+
expect.stringContaining('4000')
|
|
2217
|
+
);
|
|
2218
|
+
});
|
|
2219
|
+
|
|
2220
|
+
it('should generate platform config', async () => {
|
|
2221
|
+
const instance = adapter();
|
|
2222
|
+
const builder = createMockBuilder();
|
|
2223
|
+
|
|
2224
|
+
await instance.adapt(builder);
|
|
2225
|
+
|
|
2226
|
+
expect(builder.writeFile).toHaveBeenCalledWith(
|
|
2227
|
+
'platform.json',
|
|
2228
|
+
expect.any(String)
|
|
2229
|
+
);
|
|
2230
|
+
});
|
|
2231
|
+
|
|
2232
|
+
it('should declare supported features', () => {
|
|
2233
|
+
const instance = adapter();
|
|
2234
|
+
|
|
2235
|
+
expect(instance.supports?.node?.()).toBeDefined();
|
|
2236
|
+
expect(instance.supports?.streaming?.()).toBeDefined();
|
|
2237
|
+
});
|
|
2238
|
+
|
|
2239
|
+
it('should provide emulation environment', () => {
|
|
2240
|
+
const instance = adapter({ port: 5000 });
|
|
2241
|
+
const emulation = instance.emulate?.();
|
|
2242
|
+
|
|
2243
|
+
expect(emulation?.env?.PORT).toBe('5000');
|
|
2244
|
+
expect(emulation?.env?.${name.toUpperCase().replace(/-/g, "_")}_PLATFORM).toBe('true');
|
|
2245
|
+
});
|
|
2246
|
+
});
|
|
2247
|
+
`;
|
|
2248
|
+
}
|
|
2249
|
+
function generateReadme(name, packageName) {
|
|
2250
|
+
return `# ${packageName}
|
|
2251
|
+
|
|
2252
|
+
Flight adapter for ${name} platform.
|
|
2253
|
+
|
|
2254
|
+
## Installation
|
|
2255
|
+
|
|
2256
|
+
\`\`\`bash
|
|
2257
|
+
npm install ${packageName}
|
|
2258
|
+
\`\`\`
|
|
2259
|
+
|
|
2260
|
+
## Usage
|
|
2261
|
+
|
|
2262
|
+
\`\`\`typescript
|
|
2263
|
+
import { defineConfig } from '@flight-framework/core';
|
|
2264
|
+
import ${name.replace(/-/g, "")} from '${packageName}';
|
|
2265
|
+
|
|
2266
|
+
export default defineConfig({
|
|
2267
|
+
adapter: ${name.replace(/-/g, "")}({
|
|
2268
|
+
port: 3000,
|
|
2269
|
+
healthCheck: true,
|
|
2270
|
+
}),
|
|
2271
|
+
});
|
|
2272
|
+
\`\`\`
|
|
2273
|
+
|
|
2274
|
+
## Options
|
|
2275
|
+
|
|
2276
|
+
| Option | Type | Default | Description |
|
|
2277
|
+
|--------|------|---------|-------------|
|
|
2278
|
+
| \`port\` | \`number\` | \`3000\` | Server port |
|
|
2279
|
+
| \`healthCheck\` | \`boolean\` | \`true\` | Enable health check endpoint |
|
|
2280
|
+
| \`env\` | \`Record<string, string>\` | \`{}\` | Custom environment variables |
|
|
2281
|
+
|
|
2282
|
+
## License
|
|
2283
|
+
|
|
2284
|
+
MIT
|
|
2285
|
+
`;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
1807
2288
|
// src/index.ts
|
|
1808
2289
|
var cli = cac("flight");
|
|
1809
2290
|
var LOGO = `
|
|
1810
|
-
${
|
|
1811
|
-
${
|
|
1812
|
-
${
|
|
1813
|
-
${
|
|
1814
|
-
${
|
|
1815
|
-
${
|
|
2291
|
+
${pc7.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
2292
|
+
${pc7.cyan(" \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D")}
|
|
2293
|
+
${pc7.cyan(" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
2294
|
+
${pc7.cyan(" \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
2295
|
+
${pc7.cyan(" \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
2296
|
+
${pc7.cyan(" \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ")}
|
|
1816
2297
|
|
|
1817
|
-
${
|
|
1818
|
-
${
|
|
2298
|
+
${pc7.dim("The Agnostic Full-Stack Framework")}
|
|
2299
|
+
${pc7.dim("Maximum Flexibility. Zero Lock-in.")}
|
|
1819
2300
|
`;
|
|
1820
2301
|
function printLogo() {
|
|
1821
2302
|
console.log(LOGO);
|
|
@@ -1829,6 +2310,7 @@ cli.command("build", "Build for production").option("--outDir <dir>", "Output di
|
|
|
1829
2310
|
cli.command("preview", "Preview production build").option("-p, --port <port>", "Port to listen on").option("-h, --host <host>", "Host to bind to").option("--open", "Open browser on start").action(previewCommand);
|
|
1830
2311
|
cli.command("routes:generate", "Generate route manifest from routes directory").option("--routesDir <dir>", "Routes directory", { default: "src/routes" }).option("--outputDir <dir>", "Output directory", { default: "src/.flight" }).action(routesGenerateCommand);
|
|
1831
2312
|
cli.command("types:generate", "Generate TypeScript types for routes and environment").option("--routesDir <dir>", "Routes directory", { default: "src/routes" }).option("--outputDir <dir>", "Output directory", { default: "src/.flight" }).option("--routes", "Generate route types", { default: true }).option("--env", "Generate environment variable types").option("--watch", "Watch for changes and regenerate").action(typesGenerateCommand);
|
|
2313
|
+
cli.command("adapter:create <name>", "Create a new adapter package").option("-t, --type <type>", "Platform type (node, edge, container, serverless)", { default: "node" }).option("--validation", "Include Zod validation example").option("--outputDir <dir>", "Output directory").action(adapterCreateCommand);
|
|
1832
2314
|
function run() {
|
|
1833
2315
|
try {
|
|
1834
2316
|
cli.parse(process.argv, { run: false });
|
|
@@ -1837,7 +2319,7 @@ function run() {
|
|
|
1837
2319
|
}
|
|
1838
2320
|
cli.runMatchedCommand();
|
|
1839
2321
|
} catch (error) {
|
|
1840
|
-
console.error(
|
|
2322
|
+
console.error(pc7.red("Error:"), error instanceof Error ? error.message : error);
|
|
1841
2323
|
process.exit(1);
|
|
1842
2324
|
}
|
|
1843
2325
|
}
|