@flight-framework/cli 0.0.11 → 0.0.13

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/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # @flight-framework/cli
2
+
3
+ Command-line interface for Flight Framework.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @flight-framework/cli
9
+ ```
10
+
11
+ Or use via npx:
12
+
13
+ ```bash
14
+ npx @flight-framework/cli <command>
15
+ ```
16
+
17
+ ## Commands
18
+
19
+ ### create
20
+
21
+ Create a new Flight project.
22
+
23
+ ```bash
24
+ flight create [project-name]
25
+ ```
26
+
27
+ Options:
28
+
29
+ | Option | Description |
30
+ |--------|-------------|
31
+ | `--ui <framework>` | UI framework: react, vue, svelte, solid, preact, qwik, lit, htmx |
32
+ | `--typescript` | Use TypeScript (default) |
33
+ | `--no-typescript` | Use JavaScript |
34
+
35
+ ### dev
36
+
37
+ Start the development server.
38
+
39
+ ```bash
40
+ flight dev
41
+ ```
42
+
43
+ Options:
44
+
45
+ | Option | Description | Default |
46
+ |--------|-------------|---------|
47
+ | `-p, --port` | Port number | `5173` |
48
+ | `-h, --host` | Host to bind | `localhost` |
49
+ | `--open` | Open browser | `false` |
50
+ | `--ssr` | Enable SSR | From config |
51
+
52
+ ### build
53
+
54
+ Build for production.
55
+
56
+ ```bash
57
+ flight build
58
+ ```
59
+
60
+ ### start
61
+
62
+ Start the production server.
63
+
64
+ ```bash
65
+ flight start
66
+ ```
67
+
68
+ Options:
69
+
70
+ | Option | Description | Default |
71
+ |--------|-------------|---------|
72
+ | `-p, --port` | Port number | `3000` |
73
+ | `-h, --host` | Host to bind | `0.0.0.0` |
74
+
75
+ ## License
76
+
77
+ MIT
package/dist/bin.js CHANGED
@@ -879,6 +879,421 @@ async function routesGenerateCommand(options = {}) {
879
879
  }
880
880
  }
881
881
 
882
+ // src/commands/types-generate.ts
883
+ import { resolve as resolve7 } from "path";
884
+
885
+ // src/generators/typegen.ts
886
+ import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, watch } from "fs";
887
+ import { join as join4 } from "path";
888
+ function generateHeader(command) {
889
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
890
+ return `/**
891
+ * Auto-generated by Flight CLI
892
+ * Do not edit manually - changes will be overwritten
893
+ *
894
+ * Command: ${command}
895
+ * Generated: ${timestamp}
896
+ */`;
897
+ }
898
+ function extractParams(routePath) {
899
+ const params = [];
900
+ const dynamicMatches = routePath.match(/:(\w+)/g);
901
+ if (dynamicMatches) {
902
+ params.push(...dynamicMatches.map((m) => m.slice(1)));
903
+ }
904
+ const catchAllMatches = routePath.match(/\*(\w+)/g);
905
+ if (catchAllMatches) {
906
+ params.push(...catchAllMatches.map((m) => m.slice(1)));
907
+ }
908
+ return params;
909
+ }
910
+ function isDynamicRoute(routePath) {
911
+ return routePath.includes(":") || routePath.includes("*");
912
+ }
913
+ function generateRouteTypes(routes) {
914
+ const pageRoutes = routes.filter((r) => !r.isApiRoute);
915
+ const apiRoutes = routes.filter((r) => r.isApiRoute);
916
+ const staticRoutes = pageRoutes.filter((r) => !r.isDynamic);
917
+ const dynamicRoutes = pageRoutes.filter((r) => r.isDynamic);
918
+ const appRoutesUnion = pageRoutes.length > 0 ? pageRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
919
+ const apiRoutesUnion = apiRoutes.length > 0 ? apiRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
920
+ const staticRoutesUnion = staticRoutes.length > 0 ? staticRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
921
+ const dynamicRoutesUnion = dynamicRoutes.length > 0 ? dynamicRoutes.map((r) => ` | '${r.path}'`).join("\n") : " | never";
922
+ const paramTypes = dynamicRoutes.map((r) => {
923
+ const params = extractParams(r.path);
924
+ const typeName = routePathToTypeName(r.path);
925
+ const paramDef = params.map((p) => `${p}: string`).join("; ");
926
+ return `export type ${typeName}Params = { ${paramDef} };`;
927
+ }).join("\n");
928
+ return `${generateHeader("flight types:generate --routes")}
929
+
930
+ // ============================================================================
931
+ // Route Types
932
+ // ============================================================================
933
+
934
+ /**
935
+ * All available page routes in the application
936
+ */
937
+ export type AppRoutes =
938
+ ${appRoutesUnion};
939
+
940
+ /**
941
+ * All available API routes in the application
942
+ */
943
+ export type ApiRoutes =
944
+ ${apiRoutesUnion};
945
+
946
+ /**
947
+ * Static routes (no dynamic parameters)
948
+ */
949
+ export type StaticRoutes =
950
+ ${staticRoutesUnion};
951
+
952
+ /**
953
+ * Dynamic routes (with parameters like :id or *slug)
954
+ */
955
+ export type DynamicRoutes =
956
+ ${dynamicRoutesUnion};
957
+
958
+ // ============================================================================
959
+ // Route Parameter Extraction
960
+ // ============================================================================
961
+
962
+ /**
963
+ * Extract route parameters from a route pattern using template literal types.
964
+ *
965
+ * @example
966
+ * type Params = ExtractRouteParams<'/users/:id'>; // { id: string }
967
+ * type BlogParams = ExtractRouteParams<'/blog/:year/:slug'>; // { year: string; slug: string }
968
+ */
969
+ type ExtractRouteParams<T extends string> =
970
+ // Handle :param/rest pattern
971
+ T extends \`\${infer _Start}:\${infer Param}/\${infer Rest}\`
972
+ ? { [K in Param]: string } & ExtractRouteParams<\`/\${Rest}\`>
973
+ // Handle :param at end
974
+ : T extends \`\${infer _Start}:\${infer Param}\`
975
+ ? { [K in Param]: string }
976
+ // Handle *param (catch-all)
977
+ : T extends \`\${infer _Start}*\${infer Param}\`
978
+ ? { [K in Param]: string }
979
+ // No params
980
+ : Record<string, never>;
981
+
982
+ /**
983
+ * Get typed parameters for a specific route.
984
+ *
985
+ * @example
986
+ * const params: RouteParams<'/users/:id'> = { id: '123' };
987
+ */
988
+ export type RouteParams<T extends AppRoutes | ApiRoutes> = ExtractRouteParams<T>;
989
+
990
+ // ============================================================================
991
+ // Helper Types
992
+ // ============================================================================
993
+
994
+ /**
995
+ * Check if a route requires parameters
996
+ */
997
+ export type RequiresParams<T extends AppRoutes | ApiRoutes> =
998
+ RouteParams<T> extends Record<string, never> ? false : true;
999
+
1000
+ /**
1001
+ * Props for a type-safe Link component
1002
+ */
1003
+ export type TypedLinkProps<T extends AppRoutes> =
1004
+ RequiresParams<T> extends true
1005
+ ? { to: T; params: RouteParams<T> }
1006
+ : { to: T; params?: never };
1007
+
1008
+ /**
1009
+ * Build a URL from a route pattern and parameters
1010
+ */
1011
+ export type BuildUrl<T extends AppRoutes> =
1012
+ RequiresParams<T> extends true
1013
+ ? (route: T, params: RouteParams<T>) => string
1014
+ : (route: T) => string;
1015
+
1016
+ // ============================================================================
1017
+ // Individual Route Parameter Types
1018
+ // ============================================================================
1019
+
1020
+ ${paramTypes || "// No dynamic routes found"}
1021
+ `;
1022
+ }
1023
+ function routePathToTypeName(routePath) {
1024
+ return routePath.split("/").filter(Boolean).map((segment) => {
1025
+ const clean = segment.replace(/^[:*]/, "");
1026
+ return clean.charAt(0).toUpperCase() + clean.slice(1);
1027
+ }).join("_") || "Root";
1028
+ }
1029
+ function parseEnvFile(content) {
1030
+ const server = [];
1031
+ const client = [];
1032
+ const lines = content.split("\n");
1033
+ for (const line of lines) {
1034
+ const trimmed = line.trim();
1035
+ if (!trimmed || trimmed.startsWith("#")) {
1036
+ continue;
1037
+ }
1038
+ const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)(\?)?=/i);
1039
+ if (!match) {
1040
+ continue;
1041
+ }
1042
+ const key = match[1];
1043
+ const optional = match[2] === "?";
1044
+ const envVar = { key, optional };
1045
+ if (key.startsWith("PUBLIC_")) {
1046
+ client.push(envVar);
1047
+ } else {
1048
+ server.push(envVar);
1049
+ }
1050
+ }
1051
+ return { server, client };
1052
+ }
1053
+ function loadEnvFiles(projectRoot) {
1054
+ const envFiles = [".env", ".env.local", ".env.development", ".env.production"];
1055
+ const merged = { server: [], client: [] };
1056
+ const seenKeys = /* @__PURE__ */ new Set();
1057
+ for (const envFile of envFiles) {
1058
+ const envPath = join4(projectRoot, envFile);
1059
+ if (!existsSync5(envPath)) {
1060
+ continue;
1061
+ }
1062
+ const content = readFileSync3(envPath, "utf-8");
1063
+ const parsed = parseEnvFile(content);
1064
+ for (const envVar of parsed.server) {
1065
+ if (!seenKeys.has(envVar.key)) {
1066
+ seenKeys.add(envVar.key);
1067
+ merged.server.push(envVar);
1068
+ }
1069
+ }
1070
+ for (const envVar of parsed.client) {
1071
+ if (!seenKeys.has(envVar.key)) {
1072
+ seenKeys.add(envVar.key);
1073
+ merged.client.push(envVar);
1074
+ }
1075
+ }
1076
+ }
1077
+ return merged;
1078
+ }
1079
+ function generateEnvTypes(env) {
1080
+ const serverVars = env.server.sort((a, b) => a.key.localeCompare(b.key)).map((v) => ` ${v.key}${v.optional ? "?" : ""}: string;`).join("\n");
1081
+ const clientVars = env.client.sort((a, b) => a.key.localeCompare(b.key)).map((v) => ` ${v.key}${v.optional ? "?" : ""}: string;`).join("\n");
1082
+ return `${generateHeader("flight types:generate --env")}
1083
+
1084
+ // ============================================================================
1085
+ // Server-side Environment Variables
1086
+ // ============================================================================
1087
+
1088
+ /**
1089
+ * Server-side environment variables accessible via process.env
1090
+ * These are NOT exposed to the client.
1091
+ */
1092
+ declare namespace NodeJS {
1093
+ interface ProcessEnv {
1094
+ ${serverVars || " // No server environment variables defined"}
1095
+ }
1096
+ }
1097
+
1098
+ // ============================================================================
1099
+ // Client-side Environment Variables
1100
+ // ============================================================================
1101
+
1102
+ /**
1103
+ * Client-side environment variables accessible via import.meta.env
1104
+ * Only variables with PUBLIC_ prefix are included.
1105
+ */
1106
+ interface ImportMetaEnv {
1107
+ ${clientVars || " // No client environment variables defined"}
1108
+ }
1109
+
1110
+ interface ImportMeta {
1111
+ readonly env: ImportMetaEnv;
1112
+ }
1113
+ `;
1114
+ }
1115
+ function scanRoutesForTypes(routesDir) {
1116
+ if (!existsSync5(routesDir)) {
1117
+ return [];
1118
+ }
1119
+ const routes = [];
1120
+ scanDirectoryRecursive(routesDir, "", routes);
1121
+ return routes;
1122
+ }
1123
+ function scanDirectoryRecursive(dir, basePath, results) {
1124
+ const entries = readdirSync3(dir, { withFileTypes: true });
1125
+ for (const entry of entries) {
1126
+ const fullPath = join4(dir, entry.name);
1127
+ const relativePath = join4(basePath, entry.name);
1128
+ if (entry.isDirectory()) {
1129
+ if (entry.name.startsWith(".") || entry.name === "node_modules") {
1130
+ continue;
1131
+ }
1132
+ scanDirectoryRecursive(fullPath, relativePath, results);
1133
+ } else if (entry.isFile()) {
1134
+ const route = parseRouteFile(entry.name, relativePath);
1135
+ if (route) {
1136
+ results.push(route);
1137
+ }
1138
+ }
1139
+ }
1140
+ }
1141
+ function parseRouteFile(filename, relativePath) {
1142
+ if (/\.(page|route)\.(tsx?|jsx?)$/.test(filename)) {
1143
+ const urlPath = filePathToUrlPath2(relativePath);
1144
+ return {
1145
+ path: urlPath,
1146
+ filePath: relativePath.replace(/\\/g, "/"),
1147
+ isDynamic: isDynamicRoute(urlPath),
1148
+ isApiRoute: false
1149
+ };
1150
+ }
1151
+ const apiMatch = filename.match(/\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/i);
1152
+ if (apiMatch) {
1153
+ const urlPath = filePathToUrlPath2(relativePath);
1154
+ return {
1155
+ path: urlPath,
1156
+ filePath: relativePath.replace(/\\/g, "/"),
1157
+ isDynamic: isDynamicRoute(urlPath),
1158
+ isApiRoute: true,
1159
+ httpMethod: apiMatch[1].toUpperCase()
1160
+ };
1161
+ }
1162
+ return null;
1163
+ }
1164
+ function filePathToUrlPath2(filePath) {
1165
+ 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");
1166
+ if (!urlPath.startsWith("/")) {
1167
+ urlPath = "/" + urlPath;
1168
+ }
1169
+ urlPath = urlPath.replace(/\/+/g, "/");
1170
+ return urlPath || "/";
1171
+ }
1172
+ async function generateTypes(options) {
1173
+ const {
1174
+ routesDir,
1175
+ outputDir,
1176
+ includeRoutes = true,
1177
+ includeEnv = false,
1178
+ projectRoot = process.cwd()
1179
+ } = options;
1180
+ const result = {
1181
+ routeTypes: "",
1182
+ envTypes: "",
1183
+ filesWritten: [],
1184
+ routeCount: 0,
1185
+ envVarCount: 0
1186
+ };
1187
+ if (!existsSync5(outputDir)) {
1188
+ mkdirSync3(outputDir, { recursive: true });
1189
+ }
1190
+ if (includeRoutes) {
1191
+ const routes = scanRoutesForTypes(routesDir);
1192
+ result.routeCount = routes.length;
1193
+ result.routeTypes = generateRouteTypes(routes);
1194
+ const routeTypesPath = join4(outputDir, "routes.d.ts");
1195
+ writeFileSync3(routeTypesPath, result.routeTypes, "utf-8");
1196
+ result.filesWritten.push(routeTypesPath);
1197
+ console.log(`Generated route types: ${routes.length} routes`);
1198
+ }
1199
+ if (includeEnv) {
1200
+ const env = loadEnvFiles(projectRoot);
1201
+ result.envVarCount = env.server.length + env.client.length;
1202
+ result.envTypes = generateEnvTypes(env);
1203
+ const envTypesPath = join4(outputDir, "env.d.ts");
1204
+ writeFileSync3(envTypesPath, result.envTypes, "utf-8");
1205
+ result.filesWritten.push(envTypesPath);
1206
+ console.log(`Generated env types: ${env.server.length} server, ${env.client.length} client`);
1207
+ }
1208
+ return result;
1209
+ }
1210
+ function watchAndGenerate(options) {
1211
+ const { routesDir, debounce = 100, onRegenerate } = options;
1212
+ let timeout = null;
1213
+ const regenerate = async () => {
1214
+ try {
1215
+ const result = await generateTypes(options);
1216
+ onRegenerate?.(result);
1217
+ } catch (error) {
1218
+ console.error("Type generation failed:", error.message);
1219
+ }
1220
+ };
1221
+ const debouncedRegenerate = () => {
1222
+ if (timeout) clearTimeout(timeout);
1223
+ timeout = setTimeout(regenerate, debounce);
1224
+ };
1225
+ regenerate();
1226
+ const watcher = watch(routesDir, { recursive: true }, (eventType, filename) => {
1227
+ if (filename && /\.(tsx?|jsx?)$/.test(filename)) {
1228
+ console.log(`Route file changed: ${filename}`);
1229
+ debouncedRegenerate();
1230
+ }
1231
+ });
1232
+ return () => {
1233
+ watcher.close();
1234
+ if (timeout) clearTimeout(timeout);
1235
+ };
1236
+ }
1237
+
1238
+ // src/commands/types-generate.ts
1239
+ async function typesGenerateCommand(options = {}) {
1240
+ const cwd = process.cwd();
1241
+ const routesDir = options.routesDir ? resolve7(cwd, options.routesDir) : resolve7(cwd, "src/routes");
1242
+ const outputDir = options.outputDir ? resolve7(cwd, options.outputDir) : resolve7(cwd, "src/.flight");
1243
+ const includeRoutes = options.routes ?? !options.env;
1244
+ const includeEnv = options.env ?? false;
1245
+ console.log("Flight Type Generator");
1246
+ console.log("---------------------");
1247
+ console.log(`Routes directory: ${routesDir}`);
1248
+ console.log(`Output directory: ${outputDir}`);
1249
+ console.log(`Generate routes: ${includeRoutes}`);
1250
+ console.log(`Generate env: ${includeEnv}`);
1251
+ console.log("");
1252
+ try {
1253
+ if (options.watch) {
1254
+ console.log("Watching for changes... (Ctrl+C to stop)");
1255
+ console.log("");
1256
+ const cleanup = watchAndGenerate({
1257
+ routesDir,
1258
+ outputDir,
1259
+ includeRoutes,
1260
+ includeEnv,
1261
+ projectRoot: cwd,
1262
+ onRegenerate: (result) => {
1263
+ console.log(`Regenerated: ${result.filesWritten.length} files`);
1264
+ }
1265
+ });
1266
+ process.on("SIGINT", () => {
1267
+ console.log("\nStopping watch mode...");
1268
+ cleanup();
1269
+ process.exit(0);
1270
+ });
1271
+ await new Promise(() => {
1272
+ });
1273
+ } else {
1274
+ const result = await generateTypes({
1275
+ routesDir,
1276
+ outputDir,
1277
+ includeRoutes,
1278
+ includeEnv,
1279
+ projectRoot: cwd
1280
+ });
1281
+ console.log("Type generation complete!");
1282
+ console.log("");
1283
+ console.log("Files written:");
1284
+ for (const file of result.filesWritten) {
1285
+ console.log(` - ${file}`);
1286
+ }
1287
+ console.log("");
1288
+ console.log("Add to your tsconfig.json includes:");
1289
+ console.log(' "include": ["src", "src/.flight/*.d.ts"]');
1290
+ }
1291
+ } catch (error) {
1292
+ console.error("Type generation failed:", error);
1293
+ process.exit(1);
1294
+ }
1295
+ }
1296
+
882
1297
  // src/index.ts
883
1298
  var cli = cac("flight");
884
1299
  var LOGO = `
@@ -902,6 +1317,7 @@ cli.command("dev", "Start development server").option("-p, --port <port>", "Port
902
1317
  cli.command("build", "Build for production").option("--outDir <dir>", "Output directory").option("--sourcemap", "Generate source maps").option("--minify", "Minify output", { default: true }).action(buildCommand);
903
1318
  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);
904
1319
  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);
1320
+ 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);
905
1321
  function run() {
906
1322
  try {
907
1323
  cli.parse(process.argv, { run: false });