@flight-framework/cli 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +544 -474
- package/dist/bin.js +1352 -829
- package/dist/bin.js.map +1 -1
- package/dist/index.js +1352 -829
- 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 pc6 from "picocolors";
|
|
12
12
|
|
|
13
13
|
// src/version.ts
|
|
14
14
|
var VERSION = "0.0.1";
|
|
@@ -61,6 +61,21 @@ async function createCommand(name, options) {
|
|
|
61
61
|
console.log(pc.red("Project creation cancelled."));
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
|
+
if (options.raw) {
|
|
65
|
+
const projectPath2 = resolve(process.cwd(), projectName);
|
|
66
|
+
createRawProject(projectPath2, projectName, options);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (options.empty) {
|
|
70
|
+
const projectPath2 = resolve(process.cwd(), projectName);
|
|
71
|
+
createEmptyProject(projectPath2, projectName, options);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (options.minimal) {
|
|
75
|
+
const projectPath2 = resolve(process.cwd(), projectName);
|
|
76
|
+
createMinimalProject(projectPath2, projectName, options);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
64
79
|
if (!uiFramework) {
|
|
65
80
|
const response = await prompts({
|
|
66
81
|
type: "select",
|
|
@@ -221,710 +236,311 @@ function copyDirWithTemplates(srcDir, destDir, vars) {
|
|
|
221
236
|
}
|
|
222
237
|
}
|
|
223
238
|
}
|
|
239
|
+
function createRawProject(projectPath, projectName, options) {
|
|
240
|
+
mkdirSync(projectPath, { recursive: true });
|
|
241
|
+
const packageJson = {
|
|
242
|
+
name: projectName,
|
|
243
|
+
version: "0.0.1",
|
|
244
|
+
type: "module",
|
|
245
|
+
scripts: {
|
|
246
|
+
// Works with Bun, Node 22+, or Deno
|
|
247
|
+
"dev": "node --watch server.js",
|
|
248
|
+
"dev:bun": "bun --watch server.js",
|
|
249
|
+
"dev:deno": "deno run --allow-net server.js",
|
|
250
|
+
"start": "node server.js"
|
|
251
|
+
},
|
|
252
|
+
dependencies: {},
|
|
253
|
+
devDependencies: {}
|
|
254
|
+
};
|
|
255
|
+
writeFileSync(
|
|
256
|
+
join(projectPath, "package.json"),
|
|
257
|
+
JSON.stringify(packageJson, null, 2)
|
|
258
|
+
);
|
|
259
|
+
const serverCode = `/**
|
|
260
|
+
* ${projectName}
|
|
261
|
+
*
|
|
262
|
+
* 100% Web Standards server. Zero dependencies. Zero lock-in.
|
|
263
|
+
* Works on: Bun, Deno, Node 22+, Cloudflare Workers
|
|
264
|
+
*
|
|
265
|
+
* Run:
|
|
266
|
+
* bun server.js
|
|
267
|
+
* deno run --allow-net server.js
|
|
268
|
+
* node server.js (Node 22+)
|
|
269
|
+
*/
|
|
224
270
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const entryServerPath = join2(root, "src", "entry-server.tsx");
|
|
242
|
-
const hasSSREntry = existsSync2(entryServerPath);
|
|
243
|
-
const { createServer: createViteServer } = await import("vite");
|
|
244
|
-
let flightHttpAvailable = false;
|
|
245
|
-
let flightRouterAvailable = false;
|
|
246
|
-
try {
|
|
247
|
-
await import("@flight-framework/http");
|
|
248
|
-
flightHttpAvailable = true;
|
|
249
|
-
} catch {
|
|
271
|
+
/**
|
|
272
|
+
* Handle incoming requests using Web Standard APIs
|
|
273
|
+
* @param {Request} request
|
|
274
|
+
* @returns {Response}
|
|
275
|
+
*/
|
|
276
|
+
function handleRequest(request) {
|
|
277
|
+
const url = new URL(request.url);
|
|
278
|
+
const method = request.method;
|
|
279
|
+
|
|
280
|
+
// Router
|
|
281
|
+
if (method === 'GET' && url.pathname === '/') {
|
|
282
|
+
return Response.json({
|
|
283
|
+
message: 'Hello World!',
|
|
284
|
+
runtime: detectRuntime(),
|
|
285
|
+
docs: 'https://flight.dev/docs/quickstart-http',
|
|
286
|
+
});
|
|
250
287
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
288
|
+
|
|
289
|
+
if (method === 'GET' && url.pathname === '/health') {
|
|
290
|
+
return Response.json({
|
|
291
|
+
status: 'ok',
|
|
292
|
+
timestamp: Date.now(),
|
|
293
|
+
});
|
|
255
294
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
host: host === true ? "0.0.0.0" : host,
|
|
263
|
-
open: ssrEnabled && hasSSREntry ? false : open,
|
|
264
|
-
https: options.https ? {} : void 0
|
|
265
|
-
},
|
|
266
|
-
appType: ssrEnabled && hasSSREntry ? "custom" : "spa",
|
|
267
|
-
plugins: [
|
|
268
|
-
// Add Flight plugin when @flight-framework/http is available
|
|
269
|
-
flightHttpAvailable ? flightDevPlugin(root) : null
|
|
270
|
-
].filter(Boolean)
|
|
271
|
-
});
|
|
272
|
-
if (ssrEnabled && hasSSREntry) {
|
|
273
|
-
await startSSRServer(vite, root, port, host);
|
|
274
|
-
} else {
|
|
275
|
-
await vite.listen();
|
|
295
|
+
|
|
296
|
+
if (method === 'GET' && url.pathname.startsWith('/api/')) {
|
|
297
|
+
return Response.json({
|
|
298
|
+
path: url.pathname,
|
|
299
|
+
query: Object.fromEntries(url.searchParams),
|
|
300
|
+
});
|
|
276
301
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
302
|
+
|
|
303
|
+
// 404
|
|
304
|
+
return Response.json(
|
|
305
|
+
{ error: 'Not Found', path: url.pathname },
|
|
306
|
+
{ status: 404 }
|
|
307
|
+
);
|
|
308
|
+
}
|
|
281
309
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Detect which runtime we're running on
|
|
312
|
+
*/
|
|
313
|
+
function detectRuntime() {
|
|
314
|
+
if (typeof Bun !== 'undefined') return 'bun';
|
|
315
|
+
if (typeof Deno !== 'undefined') return 'deno';
|
|
316
|
+
return 'node';
|
|
317
|
+
}
|
|
288
318
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
319
|
+
// Export for different runtimes
|
|
320
|
+
const port = process.env.PORT || 3000;
|
|
321
|
+
|
|
322
|
+
// Bun / Cloudflare Workers style
|
|
323
|
+
export default {
|
|
324
|
+
port,
|
|
325
|
+
fetch: handleRequest,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Also start server for Node.js
|
|
329
|
+
if (detectRuntime() === 'node') {
|
|
330
|
+
const { serve } = await import('node:http');
|
|
331
|
+
serve({ port }, (req, res) => {
|
|
332
|
+
const url = 'http://localhost' + req.url;
|
|
333
|
+
const request = new Request(url, { method: req.method });
|
|
334
|
+
handleRequest(request).then(response => {
|
|
335
|
+
res.writeHead(response.status, Object.fromEntries(response.headers));
|
|
336
|
+
response.text().then(body => res.end(body));
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
console.log(\`Server running at http://localhost:\${port}\`);
|
|
340
|
+
}
|
|
341
|
+
`;
|
|
342
|
+
writeFileSync(join(projectPath, "server.js"), serverCode);
|
|
343
|
+
if (options.git) {
|
|
344
|
+
writeFileSync(
|
|
345
|
+
join(projectPath, ".gitignore"),
|
|
346
|
+
"node_modules\ndist\n.env\n.env.local\n"
|
|
347
|
+
);
|
|
348
|
+
try {
|
|
349
|
+
execSync("git init", { cwd: projectPath, stdio: "ignore" });
|
|
350
|
+
} catch {
|
|
293
351
|
}
|
|
294
|
-
} catch (error) {
|
|
295
|
-
console.error(pc2.red("\nFailed to start dev server:"), error);
|
|
296
|
-
process.exit(1);
|
|
297
352
|
}
|
|
353
|
+
console.log(`
|
|
354
|
+
${pc.green("[OK] Raw project created!")}
|
|
355
|
+
|
|
356
|
+
${pc.bold("ZERO dependencies. ZERO lock-in. 100% Web Standards.")}
|
|
357
|
+
|
|
358
|
+
${pc.cyan("Run with any runtime:")}
|
|
359
|
+
|
|
360
|
+
${pc.dim("# Bun (fastest)")}
|
|
361
|
+
bun server.js
|
|
362
|
+
|
|
363
|
+
${pc.dim("# Deno")}
|
|
364
|
+
deno run --allow-net server.js
|
|
365
|
+
|
|
366
|
+
${pc.dim("# Node.js 22+")}
|
|
367
|
+
node server.js
|
|
368
|
+
|
|
369
|
+
${pc.dim("Want to add Flight later? Just run:")}
|
|
370
|
+
flight add http ${pc.dim("# HTTP server with routing")}
|
|
371
|
+
flight add db ${pc.dim("# Database abstraction")}
|
|
372
|
+
flight add cache ${pc.dim("# Caching layer")}
|
|
373
|
+
|
|
374
|
+
${pc.dim("Path:")} ${projectPath}
|
|
375
|
+
`);
|
|
298
376
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const url = req.url || "/";
|
|
321
|
-
const pathname = url.split("?")[0];
|
|
322
|
-
const isStaticAsset = url.startsWith("/@") || url.startsWith("/node_modules") || url.startsWith("/src") && !url.includes("entry-server") || // Static file extensions
|
|
323
|
-
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");
|
|
324
|
-
if (isStaticAsset) {
|
|
325
|
-
vite.middlewares(req, res);
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
if (pathname.startsWith("/api/")) {
|
|
329
|
-
vite.middlewares(req, res);
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
377
|
+
function createEmptyProject(projectPath, projectName, options) {
|
|
378
|
+
mkdirSync(projectPath, { recursive: true });
|
|
379
|
+
const packageJson = {
|
|
380
|
+
name: projectName,
|
|
381
|
+
version: "0.0.1",
|
|
382
|
+
type: "module",
|
|
383
|
+
scripts: {
|
|
384
|
+
dev: "node server.js"
|
|
385
|
+
},
|
|
386
|
+
dependencies: {},
|
|
387
|
+
devDependencies: {}
|
|
388
|
+
};
|
|
389
|
+
writeFileSync(
|
|
390
|
+
join(projectPath, "package.json"),
|
|
391
|
+
JSON.stringify(packageJson, null, 2)
|
|
392
|
+
);
|
|
393
|
+
if (options.git) {
|
|
394
|
+
writeFileSync(
|
|
395
|
+
join(projectPath, ".gitignore"),
|
|
396
|
+
"node_modules\ndist\n.env\n.env.local\n"
|
|
397
|
+
);
|
|
332
398
|
try {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
"utf-8"
|
|
336
|
-
);
|
|
337
|
-
template = await vite.transformIndexHtml(url, template);
|
|
338
|
-
let appHtml = "";
|
|
339
|
-
if (pageRouter) {
|
|
340
|
-
const pageRoute = pageRouter.routes.find(
|
|
341
|
-
(r) => r.type === "page" && matchPath(r.path, pathname)
|
|
342
|
-
);
|
|
343
|
-
if (pageRoute && pageRoute.component) {
|
|
344
|
-
const normalizedFilePath = pageRoute.filePath.replace(/\\/g, "/");
|
|
345
|
-
const normalizedRoot = root.replace(/\\/g, "/");
|
|
346
|
-
const relativePath = normalizedFilePath.replace(normalizedRoot, "");
|
|
347
|
-
const mod = await vite.ssrLoadModule(relativePath);
|
|
348
|
-
const Component = mod.default;
|
|
349
|
-
const { render } = await vite.ssrLoadModule("/src/entry-server.tsx");
|
|
350
|
-
if (typeof render === "function" && Component) {
|
|
351
|
-
appHtml = await render(url, { Component });
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
if (!appHtml) {
|
|
356
|
-
const { render } = await vite.ssrLoadModule("/src/entry-server.tsx");
|
|
357
|
-
if (typeof render === "function") {
|
|
358
|
-
appHtml = await render(url);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (appHtml) {
|
|
362
|
-
const html = template.replace("<!--ssr-outlet-->", appHtml);
|
|
363
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
364
|
-
res.end(html);
|
|
365
|
-
} else {
|
|
366
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
367
|
-
res.end(template);
|
|
368
|
-
}
|
|
369
|
-
} catch (e) {
|
|
370
|
-
vite.ssrFixStacktrace(e);
|
|
371
|
-
console.error(pc2.red("[SSR Error]"), e.stack);
|
|
372
|
-
if (!res.headersSent) {
|
|
373
|
-
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
374
|
-
res.end(`SSR Error: ${e.message}
|
|
375
|
-
|
|
376
|
-
${e.stack}`);
|
|
377
|
-
}
|
|
399
|
+
execSync("git init", { cwd: projectPath, stdio: "ignore" });
|
|
400
|
+
} catch {
|
|
378
401
|
}
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
402
|
+
}
|
|
403
|
+
console.log(`
|
|
404
|
+
${pc.green("[OK] Empty project created!")}
|
|
405
|
+
|
|
406
|
+
${pc.cyan("Your project is a blank canvas.")} Add what you need:
|
|
407
|
+
|
|
408
|
+
${pc.dim("# HTTP server")}
|
|
409
|
+
npm install @flight-framework/http
|
|
410
|
+
|
|
411
|
+
${pc.dim("# Database")}
|
|
412
|
+
npm install @flight-framework/db
|
|
413
|
+
|
|
414
|
+
${pc.dim("# Cache")}
|
|
415
|
+
npm install @flight-framework/cache
|
|
416
|
+
|
|
417
|
+
${pc.dim("# Authentication")}
|
|
418
|
+
npm install @flight-framework/auth
|
|
419
|
+
|
|
420
|
+
${pc.dim("Path:")} ${projectPath}
|
|
421
|
+
`);
|
|
384
422
|
}
|
|
385
|
-
function
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
response.headers.forEach((value, key) => {
|
|
398
|
-
res.setHeader(key, value);
|
|
399
|
-
});
|
|
400
|
-
const body = await response.text();
|
|
401
|
-
res.end(body);
|
|
402
|
-
} catch (error) {
|
|
403
|
-
console.error("[Flight] Action error:", error);
|
|
404
|
-
res.statusCode = 500;
|
|
405
|
-
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
406
|
-
}
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
if (url.startsWith("/api/")) {
|
|
410
|
-
try {
|
|
411
|
-
const { createFileRouter } = await import("@flight-framework/core/file-router");
|
|
412
|
-
const routesDir = join2(root, "src", "routes");
|
|
413
|
-
const router = await createFileRouter({ directory: routesDir });
|
|
414
|
-
const route = router.routes.find((r) => {
|
|
415
|
-
return matchPath(r.path, url);
|
|
416
|
-
});
|
|
417
|
-
if (route && route.handler) {
|
|
418
|
-
const webRequest = await nodeToWebRequest(req);
|
|
419
|
-
const response = await route.handler({ req: webRequest, params: {} });
|
|
420
|
-
res.statusCode = response.status;
|
|
421
|
-
response.headers.forEach((value, key) => {
|
|
422
|
-
res.setHeader(key, value);
|
|
423
|
-
});
|
|
424
|
-
const body = await response.text();
|
|
425
|
-
res.end(body);
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
} catch (error) {
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
next();
|
|
432
|
-
});
|
|
423
|
+
function createMinimalProject(projectPath, projectName, options) {
|
|
424
|
+
mkdirSync(projectPath, { recursive: true });
|
|
425
|
+
const packageJson = {
|
|
426
|
+
name: projectName,
|
|
427
|
+
version: "0.0.1",
|
|
428
|
+
type: "module",
|
|
429
|
+
scripts: {
|
|
430
|
+
dev: "node --watch server.js",
|
|
431
|
+
start: "node server.js"
|
|
432
|
+
},
|
|
433
|
+
dependencies: {
|
|
434
|
+
"@flight-framework/http": "^0.0.1"
|
|
433
435
|
}
|
|
434
436
|
};
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
437
|
+
writeFileSync(
|
|
438
|
+
join(projectPath, "package.json"),
|
|
439
|
+
JSON.stringify(packageJson, null, 2)
|
|
440
|
+
);
|
|
441
|
+
const serverCode = `/**
|
|
442
|
+
* ${projectName} - Built with Flight
|
|
443
|
+
*
|
|
444
|
+
* This is a minimal Flight server. Add more as you need.
|
|
445
|
+
* Run: npm run dev
|
|
446
|
+
*/
|
|
447
|
+
|
|
448
|
+
import { createServer } from '@flight-framework/http';
|
|
449
|
+
|
|
450
|
+
const app = createServer();
|
|
451
|
+
|
|
452
|
+
// Your routes
|
|
453
|
+
app.get('/', (c) => c.json({
|
|
454
|
+
message: 'Hello from Flight!',
|
|
455
|
+
docs: 'https://flight.dev/docs/quickstart-http'
|
|
456
|
+
}));
|
|
457
|
+
|
|
458
|
+
app.get('/health', (c) => c.json({
|
|
459
|
+
status: 'ok',
|
|
460
|
+
timestamp: Date.now()
|
|
461
|
+
}));
|
|
462
|
+
|
|
463
|
+
// Start server
|
|
464
|
+
const port = process.env.PORT || 3000;
|
|
465
|
+
console.log(\`Server running at http://localhost:\${port}\`);
|
|
466
|
+
|
|
467
|
+
export default { port, fetch: app.fetch };
|
|
468
|
+
`;
|
|
469
|
+
writeFileSync(join(projectPath, "server.js"), serverCode);
|
|
470
|
+
if (options.git) {
|
|
471
|
+
writeFileSync(
|
|
472
|
+
join(projectPath, ".gitignore"),
|
|
473
|
+
"node_modules\ndist\n.env\n.env.local\n"
|
|
474
|
+
);
|
|
475
|
+
try {
|
|
476
|
+
execSync("git init", { cwd: projectPath, stdio: "ignore" });
|
|
477
|
+
} catch {
|
|
445
478
|
}
|
|
446
|
-
body = Buffer.concat(chunks);
|
|
447
479
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
} else {
|
|
456
|
-
headers.set(key, value);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
return new Request(url.toString(), {
|
|
461
|
-
method: req.method || "GET",
|
|
462
|
-
headers,
|
|
463
|
-
body
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
function matchPath(pattern, path) {
|
|
467
|
-
const cleanPath = path.split("?")[0];
|
|
468
|
-
const regexPattern = pattern.replace(/:\w+/g, "[^/]+").replace(/\*/g, ".*");
|
|
469
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
470
|
-
return regex.test(cleanPath || "/");
|
|
471
|
-
}
|
|
472
|
-
function getNetworkAddress() {
|
|
473
|
-
try {
|
|
474
|
-
const { networkInterfaces } = __require("os");
|
|
475
|
-
const nets = networkInterfaces();
|
|
476
|
-
for (const name of Object.keys(nets)) {
|
|
477
|
-
for (const net of nets[name]) {
|
|
478
|
-
if (net.family === "IPv4" && !net.internal) {
|
|
479
|
-
return net.address;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
480
|
+
if (options.install) {
|
|
481
|
+
console.log(pc.dim("\\nInstalling dependencies...\\n"));
|
|
482
|
+
try {
|
|
483
|
+
const pm = detectPackageManager();
|
|
484
|
+
execSync(`${pm} install`, { cwd: projectPath, stdio: "inherit" });
|
|
485
|
+
} catch {
|
|
482
486
|
}
|
|
483
|
-
} catch {
|
|
484
487
|
}
|
|
485
|
-
|
|
486
|
-
}
|
|
488
|
+
console.log(`
|
|
489
|
+
${pc.green("[OK] Minimal project created!")}
|
|
487
490
|
|
|
488
|
-
|
|
489
|
-
import { resolve as resolve3 } from "path";
|
|
490
|
-
import { existsSync as existsSync3 } from "fs";
|
|
491
|
-
import pc3 from "picocolors";
|
|
492
|
-
import { loadConfig as loadConfig2 } from "@flight-framework/core/config";
|
|
493
|
-
function findEntryServer(root, srcDir) {
|
|
494
|
-
const tsxPath = resolve3(root, srcDir, "entry-server.tsx");
|
|
495
|
-
const tsPath = resolve3(root, srcDir, "entry-server.ts");
|
|
496
|
-
if (existsSync3(tsxPath)) return tsxPath;
|
|
497
|
-
if (existsSync3(tsPath)) return tsPath;
|
|
498
|
-
return tsxPath;
|
|
499
|
-
}
|
|
500
|
-
async function buildCommand(options) {
|
|
501
|
-
const startTime = Date.now();
|
|
502
|
-
printLogo();
|
|
503
|
-
console.log(pc3.cyan("\n[*] Building Flight project for production...\n"));
|
|
504
|
-
try {
|
|
505
|
-
const root = resolve3(process.cwd());
|
|
506
|
-
const config = await loadConfig2(root);
|
|
507
|
-
const outDir = options.outDir ?? config.build.outDir;
|
|
508
|
-
const sourcemap = options.sourcemap ?? config.build.sourcemap;
|
|
509
|
-
const minify = options.minify ?? config.build.minify;
|
|
510
|
-
const { build } = await import("vite");
|
|
511
|
-
console.log(pc3.dim(`Output directory: ${outDir}`));
|
|
512
|
-
console.log(pc3.dim(`Sourcemaps: ${sourcemap ? "enabled" : "disabled"}`));
|
|
513
|
-
console.log(pc3.dim(`Minification: ${minify ? "enabled" : "disabled"}
|
|
514
|
-
`));
|
|
515
|
-
console.log(pc3.cyan("Building client..."));
|
|
516
|
-
await build({
|
|
517
|
-
root,
|
|
518
|
-
mode: "production",
|
|
519
|
-
build: {
|
|
520
|
-
outDir: `${outDir}/client`,
|
|
521
|
-
sourcemap,
|
|
522
|
-
minify: minify ? "esbuild" : false,
|
|
523
|
-
rollupOptions: {
|
|
524
|
-
input: resolve3(root, "index.html")
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
});
|
|
528
|
-
console.log(pc3.green("\u2713") + " Client build complete");
|
|
529
|
-
if (config.rendering.default !== "csr") {
|
|
530
|
-
console.log(pc3.cyan("\nBuilding server..."));
|
|
531
|
-
await build({
|
|
532
|
-
root,
|
|
533
|
-
mode: "production",
|
|
534
|
-
build: {
|
|
535
|
-
outDir: `${outDir}/server`,
|
|
536
|
-
sourcemap,
|
|
537
|
-
minify: minify ? "esbuild" : false,
|
|
538
|
-
ssr: true,
|
|
539
|
-
rollupOptions: {
|
|
540
|
-
input: findEntryServer(root, config.build.srcDir)
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
});
|
|
544
|
-
console.log(pc3.green("\u2713") + " Server build complete");
|
|
545
|
-
}
|
|
546
|
-
if (config.adapter) {
|
|
547
|
-
console.log(pc3.cyan(`
|
|
548
|
-
Running ${config.adapter.name} adapter...`));
|
|
549
|
-
console.log(pc3.green("\u2713") + ` ${config.adapter.name} adapter complete`);
|
|
550
|
-
}
|
|
551
|
-
const elapsed = Date.now() - startTime;
|
|
552
|
-
const elapsedSeconds = (elapsed / 1e3).toFixed(2);
|
|
553
|
-
console.log(`
|
|
554
|
-
${pc3.green("[OK] Build complete!")} ${pc3.dim(`(${elapsedSeconds}s)`)}
|
|
491
|
+
${pc.cyan("Next steps:")}
|
|
555
492
|
|
|
556
|
-
${
|
|
493
|
+
${pc.dim("$")} cd ${projectName}
|
|
494
|
+
${pc.dim("$")} ${options.install ? "" : "npm install && "}npm run dev
|
|
557
495
|
|
|
558
|
-
${
|
|
559
|
-
${pc3.dim("$")} flight preview
|
|
496
|
+
${pc.dim("Add more features as needed:")}
|
|
560
497
|
|
|
561
|
-
${
|
|
562
|
-
|
|
563
|
-
${pc3.dim("\u2022")} Or use your configured adapter
|
|
564
|
-
`);
|
|
565
|
-
} catch (error) {
|
|
566
|
-
console.error(pc3.red("\nBuild failed:"), error);
|
|
567
|
-
process.exit(1);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
498
|
+
${pc.dim("# File-based routing")}
|
|
499
|
+
npm install @flight-framework/core
|
|
570
500
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
import pc4 from "picocolors";
|
|
574
|
-
import { loadConfig as loadConfig3 } from "@flight-framework/core/config";
|
|
575
|
-
async function previewCommand(options) {
|
|
576
|
-
printLogo();
|
|
577
|
-
console.log(pc4.cyan("\n\u2708\uFE0F Starting Flight preview server...\n"));
|
|
578
|
-
try {
|
|
579
|
-
const root = resolve4(process.cwd());
|
|
580
|
-
const config = await loadConfig3(root);
|
|
581
|
-
const port = options.port ? parseInt(options.port, 10) : config.dev.port + 1;
|
|
582
|
-
const host = options.host ?? config.dev.host;
|
|
583
|
-
const open = options.open ?? false;
|
|
584
|
-
const { preview } = await import("vite");
|
|
585
|
-
const server = await preview({
|
|
586
|
-
root,
|
|
587
|
-
preview: {
|
|
588
|
-
port,
|
|
589
|
-
host: host === true ? "0.0.0.0" : host,
|
|
590
|
-
open
|
|
591
|
-
},
|
|
592
|
-
build: {
|
|
593
|
-
outDir: config.build.outDir
|
|
594
|
-
}
|
|
595
|
-
});
|
|
596
|
-
console.log(`
|
|
597
|
-
${pc4.green("\u2713")} Flight preview server ready
|
|
501
|
+
${pc.dim("# Database")}
|
|
502
|
+
npm install @flight-framework/db pg
|
|
598
503
|
|
|
599
|
-
${
|
|
600
|
-
|
|
504
|
+
${pc.dim("# Caching")}
|
|
505
|
+
npm install @flight-framework/cache
|
|
601
506
|
|
|
602
|
-
|
|
603
|
-
${pc4.dim("For development, use")} ${pc4.bold("flight dev")}
|
|
507
|
+
${pc.cyan("Happy flying!")}
|
|
604
508
|
`);
|
|
605
|
-
server.printUrls();
|
|
606
|
-
} catch (error) {
|
|
607
|
-
console.error(pc4.red("\nFailed to start preview server:"), error);
|
|
608
|
-
process.exit(1);
|
|
609
|
-
}
|
|
610
509
|
}
|
|
611
510
|
|
|
612
|
-
// src/commands/
|
|
613
|
-
import { resolve as
|
|
511
|
+
// src/commands/dev.ts
|
|
512
|
+
import { resolve as resolve3, join as join3 } from "path";
|
|
513
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
514
|
+
import pc2 from "picocolors";
|
|
515
|
+
import { loadConfig } from "@flight-framework/core/config";
|
|
614
516
|
|
|
615
|
-
// src/generators/
|
|
616
|
-
import {
|
|
617
|
-
import { join as
|
|
618
|
-
function
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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)));
|
|
622
535
|
}
|
|
623
|
-
|
|
624
|
-
|
|
536
|
+
const catchAllMatches = routePath.match(/\*(\w+)/g);
|
|
537
|
+
if (catchAllMatches) {
|
|
538
|
+
params.push(...catchAllMatches.map((m) => m.slice(1)));
|
|
625
539
|
}
|
|
626
|
-
|
|
627
|
-
return urlPath;
|
|
628
|
-
}
|
|
629
|
-
function extractHttpMethod(filename) {
|
|
630
|
-
const match = filename.match(/\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/i);
|
|
631
|
-
return match ? match[1].toUpperCase() : void 0;
|
|
632
|
-
}
|
|
633
|
-
function isLayoutFile(filename) {
|
|
634
|
-
return filename.startsWith("_layout.");
|
|
635
|
-
}
|
|
636
|
-
function isLoadingFile(filename) {
|
|
637
|
-
return filename.startsWith("_loading.");
|
|
638
|
-
}
|
|
639
|
-
function isErrorFile(filename) {
|
|
640
|
-
return filename.startsWith("_error.");
|
|
641
|
-
}
|
|
642
|
-
function isNotFoundFile(filename) {
|
|
643
|
-
return filename.startsWith("_not-found.");
|
|
644
|
-
}
|
|
645
|
-
function isRouteFile(filename) {
|
|
646
|
-
return /\.(page|route)\.(tsx?|jsx?)$/.test(filename);
|
|
647
|
-
}
|
|
648
|
-
function isApiRouteFile(filename) {
|
|
649
|
-
return /\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/.test(filename);
|
|
540
|
+
return params;
|
|
650
541
|
}
|
|
651
|
-
function
|
|
652
|
-
return
|
|
653
|
-
}
|
|
654
|
-
function scanDirectory(dir, basePath = "", results = []) {
|
|
655
|
-
if (!existsSync4(dir)) {
|
|
656
|
-
return results;
|
|
657
|
-
}
|
|
658
|
-
const entries = readdirSync2(dir);
|
|
659
|
-
for (const entry of entries) {
|
|
660
|
-
const fullPath = join3(dir, entry);
|
|
661
|
-
const relativePath = join3(basePath, entry);
|
|
662
|
-
const stat = statSync2(fullPath);
|
|
663
|
-
if (stat.isDirectory()) {
|
|
664
|
-
if (entry.startsWith(".") || entry === "node_modules") {
|
|
665
|
-
continue;
|
|
666
|
-
}
|
|
667
|
-
scanDirectory(fullPath, relativePath, results);
|
|
668
|
-
} else if (stat.isFile()) {
|
|
669
|
-
const routePath = filePathToUrlPath(dirname2(relativePath));
|
|
670
|
-
const normalizedFilePath = relativePath.replace(/\\/g, "/");
|
|
671
|
-
const isDynamic = hasDynamicSegments(relativePath);
|
|
672
|
-
if (isLayoutFile(entry)) {
|
|
673
|
-
results.push({
|
|
674
|
-
path: routePath,
|
|
675
|
-
filePath: normalizedFilePath,
|
|
676
|
-
isLayout: true,
|
|
677
|
-
isLoading: false,
|
|
678
|
-
isError: false,
|
|
679
|
-
isNotFound: false,
|
|
680
|
-
isDynamic,
|
|
681
|
-
isApiRoute: false
|
|
682
|
-
});
|
|
683
|
-
} else if (isLoadingFile(entry)) {
|
|
684
|
-
results.push({
|
|
685
|
-
path: routePath,
|
|
686
|
-
filePath: normalizedFilePath,
|
|
687
|
-
isLayout: false,
|
|
688
|
-
isLoading: true,
|
|
689
|
-
isError: false,
|
|
690
|
-
isNotFound: false,
|
|
691
|
-
isDynamic,
|
|
692
|
-
isApiRoute: false
|
|
693
|
-
});
|
|
694
|
-
} else if (isErrorFile(entry)) {
|
|
695
|
-
results.push({
|
|
696
|
-
path: routePath,
|
|
697
|
-
filePath: normalizedFilePath,
|
|
698
|
-
isLayout: false,
|
|
699
|
-
isLoading: false,
|
|
700
|
-
isError: true,
|
|
701
|
-
isNotFound: false,
|
|
702
|
-
isDynamic,
|
|
703
|
-
isApiRoute: false
|
|
704
|
-
});
|
|
705
|
-
} else if (isNotFoundFile(entry)) {
|
|
706
|
-
results.push({
|
|
707
|
-
path: routePath,
|
|
708
|
-
filePath: normalizedFilePath,
|
|
709
|
-
isLayout: false,
|
|
710
|
-
isLoading: false,
|
|
711
|
-
isError: false,
|
|
712
|
-
isNotFound: true,
|
|
713
|
-
isDynamic,
|
|
714
|
-
isApiRoute: false
|
|
715
|
-
});
|
|
716
|
-
} else if (isApiRouteFile(entry)) {
|
|
717
|
-
const method = extractHttpMethod(entry);
|
|
718
|
-
results.push({
|
|
719
|
-
path: filePathToUrlPath(relativePath),
|
|
720
|
-
filePath: normalizedFilePath,
|
|
721
|
-
isLayout: false,
|
|
722
|
-
isLoading: false,
|
|
723
|
-
isError: false,
|
|
724
|
-
isNotFound: false,
|
|
725
|
-
isDynamic,
|
|
726
|
-
isApiRoute: true,
|
|
727
|
-
httpMethod: method
|
|
728
|
-
});
|
|
729
|
-
} else if (isRouteFile(entry)) {
|
|
730
|
-
results.push({
|
|
731
|
-
path: filePathToUrlPath(relativePath),
|
|
732
|
-
filePath: normalizedFilePath,
|
|
733
|
-
isLayout: false,
|
|
734
|
-
isLoading: false,
|
|
735
|
-
isError: false,
|
|
736
|
-
isNotFound: false,
|
|
737
|
-
isDynamic,
|
|
738
|
-
isApiRoute: false
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
return results;
|
|
744
|
-
}
|
|
745
|
-
function sortRoutes(routes) {
|
|
746
|
-
return routes.sort((a, b) => {
|
|
747
|
-
if (!a.isDynamic && b.isDynamic) return -1;
|
|
748
|
-
if (a.isDynamic && !b.isDynamic) return 1;
|
|
749
|
-
const aSegments = a.path.split("/").length;
|
|
750
|
-
const bSegments = b.path.split("/").length;
|
|
751
|
-
if (aSegments !== bSegments) return aSegments - bSegments;
|
|
752
|
-
return a.path.localeCompare(b.path);
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
function generateRouteManifest(routesDir) {
|
|
756
|
-
const allRoutes = scanDirectory(routesDir);
|
|
757
|
-
const isPageRoute = (r) => !r.isLayout && !r.isLoading && !r.isError && !r.isNotFound && !r.isApiRoute;
|
|
758
|
-
const routes = sortRoutes(allRoutes.filter(isPageRoute));
|
|
759
|
-
const layouts = allRoutes.filter((r) => r.isLayout);
|
|
760
|
-
const loadingStates = allRoutes.filter((r) => r.isLoading);
|
|
761
|
-
const errorBoundaries = allRoutes.filter((r) => r.isError);
|
|
762
|
-
const notFoundPages = allRoutes.filter((r) => r.isNotFound);
|
|
763
|
-
const apiRoutes = sortRoutes(allRoutes.filter((r) => r.isApiRoute));
|
|
764
|
-
return {
|
|
765
|
-
routes,
|
|
766
|
-
layouts,
|
|
767
|
-
loadingStates,
|
|
768
|
-
errorBoundaries,
|
|
769
|
-
notFoundPages,
|
|
770
|
-
apiRoutes,
|
|
771
|
-
generated: (/* @__PURE__ */ new Date()).toISOString()
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
function generateRoutesFile(manifest, outputDir) {
|
|
775
|
-
if (!existsSync4(outputDir)) {
|
|
776
|
-
mkdirSync2(outputDir, { recursive: true });
|
|
777
|
-
}
|
|
778
|
-
const routesContent = `/**
|
|
779
|
-
* Auto-generated by Flight CLI
|
|
780
|
-
* Do not edit manually
|
|
781
|
-
* Generated: ${manifest.generated}
|
|
782
|
-
*/
|
|
783
|
-
|
|
784
|
-
import type { RouteDefinition } from '@flight-framework/router';
|
|
785
|
-
|
|
786
|
-
// Page Routes
|
|
787
|
-
export const routes: RouteDefinition[] = [
|
|
788
|
-
${manifest.routes.map((r) => ` {
|
|
789
|
-
path: '${r.path}',
|
|
790
|
-
component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
791
|
-
},`).join("\n")}
|
|
792
|
-
];
|
|
793
|
-
|
|
794
|
-
// Layout Components
|
|
795
|
-
export const layouts = [
|
|
796
|
-
${manifest.layouts.map((r) => ` {
|
|
797
|
-
path: '${r.path}',
|
|
798
|
-
component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
799
|
-
},`).join("\n")}
|
|
800
|
-
];
|
|
801
|
-
|
|
802
|
-
// Loading State Components
|
|
803
|
-
export const loadingStates = [
|
|
804
|
-
${manifest.loadingStates.map((r) => ` {
|
|
805
|
-
path: '${r.path}',
|
|
806
|
-
component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
807
|
-
},`).join("\n")}
|
|
808
|
-
];
|
|
809
|
-
|
|
810
|
-
// Error Boundary Components
|
|
811
|
-
export const errorBoundaries = [
|
|
812
|
-
${manifest.errorBoundaries.map((r) => ` {
|
|
813
|
-
path: '${r.path}',
|
|
814
|
-
component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
815
|
-
},`).join("\n")}
|
|
816
|
-
];
|
|
817
|
-
|
|
818
|
-
// Not Found Page Components
|
|
819
|
-
export const notFoundPages = [
|
|
820
|
-
${manifest.notFoundPages.map((r) => ` {
|
|
821
|
-
path: '${r.path}',
|
|
822
|
-
component: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
823
|
-
},`).join("\n")}
|
|
824
|
-
];
|
|
825
|
-
|
|
826
|
-
// API Routes
|
|
827
|
-
export const apiRoutes = [
|
|
828
|
-
${manifest.apiRoutes.map((r) => ` {
|
|
829
|
-
path: '${r.path}',
|
|
830
|
-
method: '${r.httpMethod}',
|
|
831
|
-
handler: () => import('../routes/${r.filePath.replace(/\.(tsx?|jsx?)$/, "")}'),
|
|
832
|
-
},`).join("\n")}
|
|
833
|
-
];
|
|
834
|
-
|
|
835
|
-
// Type-safe route paths
|
|
836
|
-
export type AppRoutes = ${manifest.routes.length > 0 ? manifest.routes.map((r) => `'${r.path}'`).join(" | ") : "never"};
|
|
837
|
-
|
|
838
|
-
// Type-safe API route paths
|
|
839
|
-
export type ApiRoutes = ${manifest.apiRoutes.length > 0 ? manifest.apiRoutes.map((r) => `'${r.path}'`).join(" | ") : "never"};
|
|
840
|
-
`;
|
|
841
|
-
writeFileSync2(join3(outputDir, "routes.ts"), routesContent, "utf-8");
|
|
842
|
-
const typesContent = `/**
|
|
843
|
-
* Auto-generated route types
|
|
844
|
-
* Generated: ${manifest.generated}
|
|
845
|
-
*/
|
|
846
|
-
|
|
847
|
-
import type { RouteParams } from '@flight-framework/router';
|
|
848
|
-
|
|
849
|
-
// Extract params from route patterns
|
|
850
|
-
${manifest.routes.filter((r) => r.isDynamic).map((r) => {
|
|
851
|
-
const paramMatches = r.path.match(/:(\w+)/g) || [];
|
|
852
|
-
const params = paramMatches.map((p) => p.slice(1));
|
|
853
|
-
const typeName = r.path.replace(/[/:]/g, "_").replace(/^_/, "").replace(/_$/, "") || "Root";
|
|
854
|
-
return `export type ${typeName}Params = { ${params.map((p) => `${p}: string`).join("; ")} };`;
|
|
855
|
-
}).join("\n")}
|
|
856
|
-
`;
|
|
857
|
-
writeFileSync2(join3(outputDir, "types.ts"), typesContent, "utf-8");
|
|
858
|
-
}
|
|
859
|
-
async function generateRoutes(options) {
|
|
860
|
-
const { routesDir, outputDir } = options;
|
|
861
|
-
console.log(`Scanning routes in: ${routesDir}`);
|
|
862
|
-
const manifest = generateRouteManifest(routesDir);
|
|
863
|
-
const stats = [
|
|
864
|
-
`${manifest.routes.length} pages`,
|
|
865
|
-
`${manifest.layouts.length} layouts`,
|
|
866
|
-
`${manifest.loadingStates.length} loading states`,
|
|
867
|
-
`${manifest.errorBoundaries.length} error boundaries`,
|
|
868
|
-
`${manifest.notFoundPages.length} not-found pages`,
|
|
869
|
-
`${manifest.apiRoutes.length} API routes`
|
|
870
|
-
].join(", ");
|
|
871
|
-
console.log(`Found: ${stats}`);
|
|
872
|
-
generateRoutesFile(manifest, outputDir);
|
|
873
|
-
console.log(`Generated route manifest in: ${outputDir}`);
|
|
874
|
-
return manifest;
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
// src/commands/routes-generate.ts
|
|
878
|
-
async function routesGenerateCommand(options = {}) {
|
|
879
|
-
const cwd = process.cwd();
|
|
880
|
-
const routesDir = options.routesDir ? resolve5(cwd, options.routesDir) : resolve5(cwd, "src/routes");
|
|
881
|
-
const outputDir = options.outputDir ? resolve5(cwd, options.outputDir) : resolve5(cwd, "src/.flight");
|
|
882
|
-
try {
|
|
883
|
-
const manifest = await generateRoutes({
|
|
884
|
-
routesDir,
|
|
885
|
-
outputDir,
|
|
886
|
-
watch: options.watch
|
|
887
|
-
});
|
|
888
|
-
console.log("\nRoute manifest generated successfully!");
|
|
889
|
-
console.log(` Pages: ${manifest.routes.length}`);
|
|
890
|
-
console.log(` API Routes: ${manifest.apiRoutes.length}`);
|
|
891
|
-
console.log(` Layouts: ${manifest.layouts.length}`);
|
|
892
|
-
} catch (error) {
|
|
893
|
-
console.error("Failed to generate routes:", error);
|
|
894
|
-
process.exit(1);
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// src/commands/types-generate.ts
|
|
899
|
-
import { resolve as resolve7 } from "path";
|
|
900
|
-
|
|
901
|
-
// src/generators/typegen.ts
|
|
902
|
-
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, readdirSync as readdirSync3, watch } from "fs";
|
|
903
|
-
import { join as join4 } from "path";
|
|
904
|
-
function generateHeader(command) {
|
|
905
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
906
|
-
return `/**
|
|
907
|
-
* Auto-generated by Flight CLI
|
|
908
|
-
* Do not edit manually - changes will be overwritten
|
|
909
|
-
*
|
|
910
|
-
* Command: ${command}
|
|
911
|
-
* Generated: ${timestamp}
|
|
912
|
-
*/`;
|
|
913
|
-
}
|
|
914
|
-
function extractParams(routePath) {
|
|
915
|
-
const params = [];
|
|
916
|
-
const dynamicMatches = routePath.match(/:(\w+)/g);
|
|
917
|
-
if (dynamicMatches) {
|
|
918
|
-
params.push(...dynamicMatches.map((m) => m.slice(1)));
|
|
919
|
-
}
|
|
920
|
-
const catchAllMatches = routePath.match(/\*(\w+)/g);
|
|
921
|
-
if (catchAllMatches) {
|
|
922
|
-
params.push(...catchAllMatches.map((m) => m.slice(1)));
|
|
923
|
-
}
|
|
924
|
-
return params;
|
|
925
|
-
}
|
|
926
|
-
function isDynamicRoute(routePath) {
|
|
927
|
-
return routePath.includes(":") || routePath.includes("*");
|
|
542
|
+
function isDynamicRoute(routePath) {
|
|
543
|
+
return routePath.includes(":") || routePath.includes("*");
|
|
928
544
|
}
|
|
929
545
|
function generateRouteTypes(routes) {
|
|
930
546
|
const pageRoutes = routes.filter((r) => !r.isApiRoute);
|
|
@@ -1071,11 +687,11 @@ function loadEnvFiles(projectRoot) {
|
|
|
1071
687
|
const merged = { server: [], client: [] };
|
|
1072
688
|
const seenKeys = /* @__PURE__ */ new Set();
|
|
1073
689
|
for (const envFile of envFiles) {
|
|
1074
|
-
const envPath =
|
|
1075
|
-
if (!
|
|
690
|
+
const envPath = join2(projectRoot, envFile);
|
|
691
|
+
if (!existsSync2(envPath)) {
|
|
1076
692
|
continue;
|
|
1077
693
|
}
|
|
1078
|
-
const content =
|
|
694
|
+
const content = readFileSync2(envPath, "utf-8");
|
|
1079
695
|
const parsed = parseEnvFile(content);
|
|
1080
696
|
for (const envVar of parsed.server) {
|
|
1081
697
|
if (!seenKeys.has(envVar.key)) {
|
|
@@ -1097,161 +713,859 @@ function generateEnvTypes(env) {
|
|
|
1097
713
|
const clientVars = env.client.sort((a, b) => a.key.localeCompare(b.key)).map((v) => ` ${v.key}${v.optional ? "?" : ""}: string;`).join("\n");
|
|
1098
714
|
return `${generateHeader("flight types:generate --env")}
|
|
1099
715
|
|
|
1100
|
-
// ============================================================================
|
|
1101
|
-
// Server-side Environment Variables
|
|
1102
|
-
// ============================================================================
|
|
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"}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
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 [];
|
|
750
|
+
}
|
|
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);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
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
|
+
};
|
|
782
|
+
}
|
|
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;
|
|
795
|
+
}
|
|
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 || "/";
|
|
803
|
+
}
|
|
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);
|
|
851
|
+
}
|
|
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();
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
return () => {
|
|
865
|
+
watcher.close();
|
|
866
|
+
if (timeout) clearTimeout(timeout);
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/commands/dev.ts
|
|
871
|
+
async function devCommand(options) {
|
|
872
|
+
const startTime = Date.now();
|
|
873
|
+
printLogo();
|
|
874
|
+
console.log(pc2.cyan("\n Starting Flight development server...\n"));
|
|
875
|
+
try {
|
|
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;
|
|
907
|
+
const host = options.host ?? config.dev.host;
|
|
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({
|
|
926
|
+
root,
|
|
927
|
+
mode: "development",
|
|
928
|
+
server: {
|
|
929
|
+
middlewareMode: ssrEnabled && hasSSREntry,
|
|
930
|
+
port: ssrEnabled && hasSSREntry ? void 0 : port,
|
|
931
|
+
host: host === true ? "0.0.0.0" : host,
|
|
932
|
+
open: ssrEnabled && hasSSREntry ? false : open,
|
|
933
|
+
https: options.https ? {} : void 0
|
|
934
|
+
},
|
|
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)
|
|
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;
|
|
948
|
+
console.log(`
|
|
949
|
+
${pc2.green("\u2713")} Flight dev server ready in ${pc2.bold(elapsed + "ms")}
|
|
950
|
+
|
|
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"}
|
|
957
|
+
|
|
958
|
+
${pc2.dim("press")} ${pc2.bold("h")} ${pc2.dim("to show help")}
|
|
959
|
+
`);
|
|
960
|
+
if (!isSSR) {
|
|
961
|
+
vite.bindCLIShortcuts({ print: true });
|
|
962
|
+
}
|
|
963
|
+
} catch (error) {
|
|
964
|
+
console.error(pc2.red("\nFailed to start dev server:"), error);
|
|
965
|
+
process.exit(1);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
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 {
|
|
987
|
+
}
|
|
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}`);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
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`));
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
function flightDevPlugin(root) {
|
|
1055
|
+
return {
|
|
1056
|
+
name: "flight:dev",
|
|
1057
|
+
configureServer(server) {
|
|
1058
|
+
server.middlewares.use(async (req, res, next) => {
|
|
1059
|
+
const url = req.url || "/";
|
|
1060
|
+
if (url.startsWith("/__flight_action/")) {
|
|
1061
|
+
try {
|
|
1062
|
+
const { handleActionRequest } = await import("@flight-framework/core");
|
|
1063
|
+
const webRequest = await nodeToWebRequest(req);
|
|
1064
|
+
const response = await handleActionRequest(webRequest);
|
|
1065
|
+
res.statusCode = response.status;
|
|
1066
|
+
response.headers.forEach((value, key) => {
|
|
1067
|
+
res.setHeader(key, value);
|
|
1068
|
+
});
|
|
1069
|
+
const body = await response.text();
|
|
1070
|
+
res.end(body);
|
|
1071
|
+
} catch (error) {
|
|
1072
|
+
console.error("[Flight] Action error:", error);
|
|
1073
|
+
res.statusCode = 500;
|
|
1074
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
1075
|
+
}
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (url.startsWith("/api/")) {
|
|
1079
|
+
try {
|
|
1080
|
+
const { createFileRouter } = await import("@flight-framework/core/file-router");
|
|
1081
|
+
const routesDir = join3(root, "src", "routes");
|
|
1082
|
+
const router = await createFileRouter({ directory: routesDir });
|
|
1083
|
+
const route = router.routes.find((r) => {
|
|
1084
|
+
return matchPath(r.path, url);
|
|
1085
|
+
});
|
|
1086
|
+
if (route && route.handler) {
|
|
1087
|
+
const webRequest = await nodeToWebRequest(req);
|
|
1088
|
+
const response = await route.handler({ req: webRequest, params: {} });
|
|
1089
|
+
res.statusCode = response.status;
|
|
1090
|
+
response.headers.forEach((value, key) => {
|
|
1091
|
+
res.setHeader(key, value);
|
|
1092
|
+
});
|
|
1093
|
+
const body = await response.text();
|
|
1094
|
+
res.end(body);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
next();
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
async function nodeToWebRequest(req) {
|
|
1106
|
+
const host = req.headers.host || "localhost";
|
|
1107
|
+
const protocol = "http";
|
|
1108
|
+
const url = new URL(req.url || "/", `${protocol}://${host}`);
|
|
1109
|
+
let body = null;
|
|
1110
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
1111
|
+
const chunks = [];
|
|
1112
|
+
for await (const chunk of req) {
|
|
1113
|
+
chunks.push(chunk);
|
|
1114
|
+
}
|
|
1115
|
+
body = Buffer.concat(chunks);
|
|
1116
|
+
}
|
|
1117
|
+
const headers = new Headers();
|
|
1118
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
1119
|
+
if (value) {
|
|
1120
|
+
if (Array.isArray(value)) {
|
|
1121
|
+
for (const v of value) {
|
|
1122
|
+
headers.append(key, v);
|
|
1123
|
+
}
|
|
1124
|
+
} else {
|
|
1125
|
+
headers.set(key, value);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
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
|
+
}
|
|
1156
|
+
|
|
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)`)}
|
|
1224
|
+
|
|
1225
|
+
${pc3.cyan("Output:")} ${resolve4(root, outDir)}
|
|
1226
|
+
|
|
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
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/commands/preview.ts
|
|
1241
|
+
import { resolve as resolve5 } from "path";
|
|
1242
|
+
import pc4 from "picocolors";
|
|
1243
|
+
import { loadConfig as loadConfig3 } from "@flight-framework/core/config";
|
|
1244
|
+
async function previewCommand(options) {
|
|
1245
|
+
printLogo();
|
|
1246
|
+
console.log(pc4.cyan("\n\u2708\uFE0F Starting Flight preview server...\n"));
|
|
1247
|
+
try {
|
|
1248
|
+
const root = resolve5(process.cwd());
|
|
1249
|
+
const config = await loadConfig3(root);
|
|
1250
|
+
const port = options.port ? parseInt(options.port, 10) : config.dev.port + 1;
|
|
1251
|
+
const host = options.host ?? config.dev.host;
|
|
1252
|
+
const open = options.open ?? false;
|
|
1253
|
+
const { preview } = await import("vite");
|
|
1254
|
+
const server = await preview({
|
|
1255
|
+
root,
|
|
1256
|
+
preview: {
|
|
1257
|
+
port,
|
|
1258
|
+
host: host === true ? "0.0.0.0" : host,
|
|
1259
|
+
open
|
|
1260
|
+
},
|
|
1261
|
+
build: {
|
|
1262
|
+
outDir: config.build.outDir
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
console.log(`
|
|
1266
|
+
${pc4.green("\u2713")} Flight preview server ready
|
|
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();
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
console.error(pc4.red("\nFailed to start preview server:"), error);
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// src/commands/routes-generate.ts
|
|
1282
|
+
import { resolve as resolve6 } from "path";
|
|
1283
|
+
|
|
1284
|
+
// src/generators/routes.ts
|
|
1285
|
+
import { readdirSync as readdirSync3, statSync as statSync2, existsSync as existsSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
|
|
1286
|
+
import { join as join4, dirname as dirname3 } from "path";
|
|
1287
|
+
function filePathToUrlPath2(filePath) {
|
|
1288
|
+
let urlPath = filePath.replace(/\.(page|route)\.(tsx?|jsx?)$/, "").replace(/\.(get|post|put|patch|delete|options|head)$/, "").replace(/\/index$/, "").replace(/\/?\([^)]+\)/g, "").replace(/\[\.\.\.(\w+)\]/g, "*$1").replace(/\[(\w+)\]/g, ":$1");
|
|
1289
|
+
if (!urlPath.startsWith("/")) {
|
|
1290
|
+
urlPath = "/" + urlPath;
|
|
1291
|
+
}
|
|
1292
|
+
if (urlPath === "" || urlPath === "/") {
|
|
1293
|
+
urlPath = "/";
|
|
1294
|
+
}
|
|
1295
|
+
urlPath = urlPath.replace(/\/+/g, "/");
|
|
1296
|
+
return urlPath;
|
|
1297
|
+
}
|
|
1298
|
+
function extractHttpMethod(filename) {
|
|
1299
|
+
const match = filename.match(/\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/i);
|
|
1300
|
+
return match ? match[1].toUpperCase() : void 0;
|
|
1301
|
+
}
|
|
1302
|
+
function isLayoutFile(filename) {
|
|
1303
|
+
return filename.startsWith("_layout.");
|
|
1304
|
+
}
|
|
1305
|
+
function isLoadingFile(filename) {
|
|
1306
|
+
return filename.startsWith("_loading.");
|
|
1307
|
+
}
|
|
1308
|
+
function isErrorFile(filename) {
|
|
1309
|
+
return filename.startsWith("_error.");
|
|
1310
|
+
}
|
|
1311
|
+
function isNotFoundFile(filename) {
|
|
1312
|
+
return filename.startsWith("_not-found.");
|
|
1313
|
+
}
|
|
1314
|
+
function isRouteFile(filename) {
|
|
1315
|
+
return /\.(page|route)\.(tsx?|jsx?)$/.test(filename);
|
|
1316
|
+
}
|
|
1317
|
+
function isApiRouteFile(filename) {
|
|
1318
|
+
return /\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/.test(filename);
|
|
1319
|
+
}
|
|
1320
|
+
function hasDynamicSegments(path) {
|
|
1321
|
+
return path.includes("[") && path.includes("]");
|
|
1322
|
+
}
|
|
1323
|
+
function scanDirectory(dir, basePath = "", results = []) {
|
|
1324
|
+
if (!existsSync5(dir)) {
|
|
1325
|
+
return results;
|
|
1326
|
+
}
|
|
1327
|
+
const entries = readdirSync3(dir);
|
|
1328
|
+
for (const entry of entries) {
|
|
1329
|
+
const fullPath = join4(dir, entry);
|
|
1330
|
+
const relativePath = join4(basePath, entry);
|
|
1331
|
+
const stat = statSync2(fullPath);
|
|
1332
|
+
if (stat.isDirectory()) {
|
|
1333
|
+
if (entry.startsWith(".") || entry === "node_modules") {
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
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
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
return results;
|
|
1413
|
+
}
|
|
1414
|
+
function sortRoutes(routes) {
|
|
1415
|
+
return routes.sort((a, b) => {
|
|
1416
|
+
if (!a.isDynamic && b.isDynamic) return -1;
|
|
1417
|
+
if (a.isDynamic && !b.isDynamic) return 1;
|
|
1418
|
+
const aSegments = a.path.split("/").length;
|
|
1419
|
+
const bSegments = b.path.split("/").length;
|
|
1420
|
+
if (aSegments !== bSegments) return aSegments - bSegments;
|
|
1421
|
+
return a.path.localeCompare(b.path);
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
function generateRouteManifest(routesDir) {
|
|
1425
|
+
const allRoutes = scanDirectory(routesDir);
|
|
1426
|
+
const isPageRoute = (r) => !r.isLayout && !r.isLoading && !r.isError && !r.isNotFound && !r.isApiRoute;
|
|
1427
|
+
const routes = sortRoutes(allRoutes.filter(isPageRoute));
|
|
1428
|
+
const layouts = allRoutes.filter((r) => r.isLayout);
|
|
1429
|
+
const loadingStates = allRoutes.filter((r) => r.isLoading);
|
|
1430
|
+
const errorBoundaries = allRoutes.filter((r) => r.isError);
|
|
1431
|
+
const notFoundPages = allRoutes.filter((r) => r.isNotFound);
|
|
1432
|
+
const apiRoutes = sortRoutes(allRoutes.filter((r) => r.isApiRoute));
|
|
1433
|
+
return {
|
|
1434
|
+
routes,
|
|
1435
|
+
layouts,
|
|
1436
|
+
loadingStates,
|
|
1437
|
+
errorBoundaries,
|
|
1438
|
+
notFoundPages,
|
|
1439
|
+
apiRoutes,
|
|
1440
|
+
generated: (/* @__PURE__ */ new Date()).toISOString()
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
function generateRoutesFile(manifest, outputDir) {
|
|
1444
|
+
if (!existsSync5(outputDir)) {
|
|
1445
|
+
mkdirSync3(outputDir, { recursive: true });
|
|
1446
|
+
}
|
|
1447
|
+
const routesContent = `/**
|
|
1448
|
+
* Auto-generated by Flight CLI
|
|
1449
|
+
* Do not edit manually
|
|
1450
|
+
* Generated: ${manifest.generated}
|
|
1451
|
+
*/
|
|
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
|
+
];
|
|
1103
1486
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
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
|
+
];
|
|
1113
1494
|
|
|
1114
|
-
//
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
+
];
|
|
1117
1503
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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}
|
|
1121
1514
|
*/
|
|
1122
|
-
interface ImportMetaEnv {
|
|
1123
|
-
${clientVars || " // No client environment variables defined"}
|
|
1124
|
-
}
|
|
1125
1515
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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")}
|
|
1129
1525
|
`;
|
|
1526
|
+
writeFileSync3(join4(outputDir, "types.ts"), typesContent, "utf-8");
|
|
1130
1527
|
}
|
|
1131
|
-
function
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
scanDirectoryRecursive(fullPath, relativePath, results);
|
|
1149
|
-
} else if (entry.isFile()) {
|
|
1150
|
-
const route = parseRouteFile(entry.name, relativePath);
|
|
1151
|
-
if (route) {
|
|
1152
|
-
results.push(route);
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
function parseRouteFile(filename, relativePath) {
|
|
1158
|
-
if (/\.(page|route)\.(tsx?|jsx?)$/.test(filename)) {
|
|
1159
|
-
const urlPath = filePathToUrlPath2(relativePath);
|
|
1160
|
-
return {
|
|
1161
|
-
path: urlPath,
|
|
1162
|
-
filePath: relativePath.replace(/\\/g, "/"),
|
|
1163
|
-
isDynamic: isDynamicRoute(urlPath),
|
|
1164
|
-
isApiRoute: false
|
|
1165
|
-
};
|
|
1166
|
-
}
|
|
1167
|
-
const apiMatch = filename.match(/\.(get|post|put|patch|delete|options|head)\.(tsx?|jsx?)$/i);
|
|
1168
|
-
if (apiMatch) {
|
|
1169
|
-
const urlPath = filePathToUrlPath2(relativePath);
|
|
1170
|
-
return {
|
|
1171
|
-
path: urlPath,
|
|
1172
|
-
filePath: relativePath.replace(/\\/g, "/"),
|
|
1173
|
-
isDynamic: isDynamicRoute(urlPath),
|
|
1174
|
-
isApiRoute: true,
|
|
1175
|
-
httpMethod: apiMatch[1].toUpperCase()
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
return null;
|
|
1179
|
-
}
|
|
1180
|
-
function filePathToUrlPath2(filePath) {
|
|
1181
|
-
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");
|
|
1182
|
-
if (!urlPath.startsWith("/")) {
|
|
1183
|
-
urlPath = "/" + urlPath;
|
|
1184
|
-
}
|
|
1185
|
-
urlPath = urlPath.replace(/\/+/g, "/");
|
|
1186
|
-
return urlPath || "/";
|
|
1528
|
+
async function generateRoutes(options) {
|
|
1529
|
+
const { routesDir, outputDir } = options;
|
|
1530
|
+
console.log(`Scanning routes in: ${routesDir}`);
|
|
1531
|
+
const manifest = generateRouteManifest(routesDir);
|
|
1532
|
+
const stats = [
|
|
1533
|
+
`${manifest.routes.length} pages`,
|
|
1534
|
+
`${manifest.layouts.length} layouts`,
|
|
1535
|
+
`${manifest.loadingStates.length} loading states`,
|
|
1536
|
+
`${manifest.errorBoundaries.length} error boundaries`,
|
|
1537
|
+
`${manifest.notFoundPages.length} not-found pages`,
|
|
1538
|
+
`${manifest.apiRoutes.length} API routes`
|
|
1539
|
+
].join(", ");
|
|
1540
|
+
console.log(`Found: ${stats}`);
|
|
1541
|
+
generateRoutesFile(manifest, outputDir);
|
|
1542
|
+
console.log(`Generated route manifest in: ${outputDir}`);
|
|
1543
|
+
return manifest;
|
|
1187
1544
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
};
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
const routes = scanRoutesForTypes(routesDir);
|
|
1208
|
-
result.routeCount = routes.length;
|
|
1209
|
-
result.routeTypes = generateRouteTypes(routes);
|
|
1210
|
-
const routeTypesPath = join4(outputDir, "routes.d.ts");
|
|
1211
|
-
writeFileSync3(routeTypesPath, result.routeTypes, "utf-8");
|
|
1212
|
-
result.filesWritten.push(routeTypesPath);
|
|
1213
|
-
console.log(`Generated route types: ${routes.length} routes`);
|
|
1214
|
-
}
|
|
1215
|
-
if (includeEnv) {
|
|
1216
|
-
const env = loadEnvFiles(projectRoot);
|
|
1217
|
-
result.envVarCount = env.server.length + env.client.length;
|
|
1218
|
-
result.envTypes = generateEnvTypes(env);
|
|
1219
|
-
const envTypesPath = join4(outputDir, "env.d.ts");
|
|
1220
|
-
writeFileSync3(envTypesPath, result.envTypes, "utf-8");
|
|
1221
|
-
result.filesWritten.push(envTypesPath);
|
|
1222
|
-
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);
|
|
1223
1564
|
}
|
|
1224
|
-
return result;
|
|
1225
|
-
}
|
|
1226
|
-
function watchAndGenerate(options) {
|
|
1227
|
-
const { routesDir, debounce = 100, onRegenerate } = options;
|
|
1228
|
-
let timeout = null;
|
|
1229
|
-
const regenerate = async () => {
|
|
1230
|
-
try {
|
|
1231
|
-
const result = await generateTypes(options);
|
|
1232
|
-
onRegenerate?.(result);
|
|
1233
|
-
} catch (error) {
|
|
1234
|
-
console.error("Type generation failed:", error.message);
|
|
1235
|
-
}
|
|
1236
|
-
};
|
|
1237
|
-
const debouncedRegenerate = () => {
|
|
1238
|
-
if (timeout) clearTimeout(timeout);
|
|
1239
|
-
timeout = setTimeout(regenerate, debounce);
|
|
1240
|
-
};
|
|
1241
|
-
regenerate();
|
|
1242
|
-
const watcher = watch(routesDir, { recursive: true }, (eventType, filename) => {
|
|
1243
|
-
if (filename && /\.(tsx?|jsx?)$/.test(filename)) {
|
|
1244
|
-
console.log(`Route file changed: ${filename}`);
|
|
1245
|
-
debouncedRegenerate();
|
|
1246
|
-
}
|
|
1247
|
-
});
|
|
1248
|
-
return () => {
|
|
1249
|
-
watcher.close();
|
|
1250
|
-
if (timeout) clearTimeout(timeout);
|
|
1251
|
-
};
|
|
1252
1565
|
}
|
|
1253
1566
|
|
|
1254
1567
|
// src/commands/types-generate.ts
|
|
1568
|
+
import { resolve as resolve7 } from "path";
|
|
1255
1569
|
async function typesGenerateCommand(options = {}) {
|
|
1256
1570
|
const cwd = process.cwd();
|
|
1257
1571
|
const routesDir = options.routesDir ? resolve7(cwd, options.routesDir) : resolve7(cwd, "src/routes");
|
|
@@ -1310,25 +1624,234 @@ async function typesGenerateCommand(options = {}) {
|
|
|
1310
1624
|
}
|
|
1311
1625
|
}
|
|
1312
1626
|
|
|
1627
|
+
// src/commands/add.ts
|
|
1628
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1629
|
+
import { join as join5 } from "path";
|
|
1630
|
+
import { execSync as execSync2 } from "child_process";
|
|
1631
|
+
import pc5 from "picocolors";
|
|
1632
|
+
var PACKAGES = {
|
|
1633
|
+
// Core
|
|
1634
|
+
"http": {
|
|
1635
|
+
name: "@flight-framework/http",
|
|
1636
|
+
description: "HTTP server with routing and middleware",
|
|
1637
|
+
category: "core"
|
|
1638
|
+
},
|
|
1639
|
+
"core": {
|
|
1640
|
+
name: "@flight-framework/core",
|
|
1641
|
+
description: "File-based routing and configuration",
|
|
1642
|
+
category: "core"
|
|
1643
|
+
},
|
|
1644
|
+
// Data
|
|
1645
|
+
"db": {
|
|
1646
|
+
name: "@flight-framework/db",
|
|
1647
|
+
description: "Database abstraction layer",
|
|
1648
|
+
category: "data",
|
|
1649
|
+
drivers: ["pg", "@libsql/client", "@neondatabase/serverless", "@supabase/supabase-js"]
|
|
1650
|
+
},
|
|
1651
|
+
"cache": {
|
|
1652
|
+
name: "@flight-framework/cache",
|
|
1653
|
+
description: "Caching with multiple adapters",
|
|
1654
|
+
category: "data"
|
|
1655
|
+
},
|
|
1656
|
+
// Auth
|
|
1657
|
+
"auth": {
|
|
1658
|
+
name: "@flight-framework/auth",
|
|
1659
|
+
description: "Authentication adapters",
|
|
1660
|
+
category: "auth"
|
|
1661
|
+
},
|
|
1662
|
+
// Frontend
|
|
1663
|
+
"forms": {
|
|
1664
|
+
name: "@flight-framework/forms",
|
|
1665
|
+
description: "Type-safe form handling",
|
|
1666
|
+
category: "frontend"
|
|
1667
|
+
},
|
|
1668
|
+
"i18n": {
|
|
1669
|
+
name: "@flight-framework/i18n",
|
|
1670
|
+
description: "Internationalization",
|
|
1671
|
+
category: "frontend"
|
|
1672
|
+
},
|
|
1673
|
+
"seo": {
|
|
1674
|
+
name: "@flight-framework/seo",
|
|
1675
|
+
description: "SEO utilities",
|
|
1676
|
+
category: "frontend"
|
|
1677
|
+
},
|
|
1678
|
+
"image": {
|
|
1679
|
+
name: "@flight-framework/image",
|
|
1680
|
+
description: "Image optimization",
|
|
1681
|
+
category: "frontend"
|
|
1682
|
+
},
|
|
1683
|
+
// Communication
|
|
1684
|
+
"email": {
|
|
1685
|
+
name: "@flight-framework/email",
|
|
1686
|
+
description: "Email sending",
|
|
1687
|
+
category: "communication"
|
|
1688
|
+
},
|
|
1689
|
+
"realtime": {
|
|
1690
|
+
name: "@flight-framework/realtime",
|
|
1691
|
+
description: "WebSocket and real-time features",
|
|
1692
|
+
category: "communication"
|
|
1693
|
+
},
|
|
1694
|
+
// Deployment
|
|
1695
|
+
"helpers": {
|
|
1696
|
+
name: "@flight-framework/helpers",
|
|
1697
|
+
description: "Optional helper utilities (detection, suggestions)",
|
|
1698
|
+
category: "utilities"
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
async function addCommand(packageName) {
|
|
1702
|
+
const cwd = process.cwd();
|
|
1703
|
+
if (!existsSync6(join5(cwd, "package.json"))) {
|
|
1704
|
+
console.log(pc5.red("No package.json found. Run this command from your project directory."));
|
|
1705
|
+
process.exit(1);
|
|
1706
|
+
}
|
|
1707
|
+
if (!packageName) {
|
|
1708
|
+
showAvailablePackages();
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
const pkg = PACKAGES[packageName];
|
|
1712
|
+
if (!pkg) {
|
|
1713
|
+
console.log(pc5.red(`Unknown package: ${packageName}`));
|
|
1714
|
+
console.log(pc5.dim("Run `flight add` to see available packages."));
|
|
1715
|
+
process.exit(1);
|
|
1716
|
+
}
|
|
1717
|
+
console.log(`
|
|
1718
|
+
${pc5.cyan("Adding")} ${pc5.bold(pkg.name)}...`);
|
|
1719
|
+
console.log(pc5.dim(pkg.description));
|
|
1720
|
+
console.log();
|
|
1721
|
+
const pm = detectPackageManager2();
|
|
1722
|
+
try {
|
|
1723
|
+
const installCmd = pm === "npm" ? "npm install" : `${pm} add`;
|
|
1724
|
+
execSync2(`${installCmd} ${pkg.name}`, { cwd, stdio: "inherit" });
|
|
1725
|
+
console.log(`
|
|
1726
|
+
${pc5.green("[OK]")} ${pkg.name} added successfully!`);
|
|
1727
|
+
showNextSteps(packageName, pkg);
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
console.log(pc5.red(`
|
|
1730
|
+
Failed to install ${pkg.name}`));
|
|
1731
|
+
process.exit(1);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
function detectPackageManager2() {
|
|
1735
|
+
const cwd = process.cwd();
|
|
1736
|
+
if (existsSync6(join5(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
1737
|
+
if (existsSync6(join5(cwd, "yarn.lock"))) return "yarn";
|
|
1738
|
+
if (existsSync6(join5(cwd, "bun.lockb"))) return "bun";
|
|
1739
|
+
return "npm";
|
|
1740
|
+
}
|
|
1741
|
+
function showAvailablePackages() {
|
|
1742
|
+
console.log(`
|
|
1743
|
+
${pc5.cyan("Available Flight packages:")}
|
|
1744
|
+
|
|
1745
|
+
${pc5.bold("Core")}
|
|
1746
|
+
${pc5.green("http")} ${pc5.dim("HTTP server with routing and middleware")}
|
|
1747
|
+
${pc5.green("core")} ${pc5.dim("File-based routing and configuration")}
|
|
1748
|
+
|
|
1749
|
+
${pc5.bold("Data")}
|
|
1750
|
+
${pc5.green("db")} ${pc5.dim("Database abstraction layer")}
|
|
1751
|
+
${pc5.green("cache")} ${pc5.dim("Caching with multiple adapters")}
|
|
1752
|
+
|
|
1753
|
+
${pc5.bold("Auth")}
|
|
1754
|
+
${pc5.green("auth")} ${pc5.dim("Authentication adapters")}
|
|
1755
|
+
|
|
1756
|
+
${pc5.bold("Frontend")}
|
|
1757
|
+
${pc5.green("forms")} ${pc5.dim("Type-safe form handling")}
|
|
1758
|
+
${pc5.green("i18n")} ${pc5.dim("Internationalization")}
|
|
1759
|
+
${pc5.green("seo")} ${pc5.dim("SEO utilities")}
|
|
1760
|
+
${pc5.green("image")} ${pc5.dim("Image optimization")}
|
|
1761
|
+
|
|
1762
|
+
${pc5.bold("Communication")}
|
|
1763
|
+
${pc5.green("email")} ${pc5.dim("Email sending")}
|
|
1764
|
+
${pc5.green("realtime")} ${pc5.dim("WebSocket and real-time features")}
|
|
1765
|
+
|
|
1766
|
+
${pc5.bold("Utilities")}
|
|
1767
|
+
${pc5.green("helpers")} ${pc5.dim("Optional helper utilities")}
|
|
1768
|
+
|
|
1769
|
+
${pc5.cyan("Usage:")}
|
|
1770
|
+
flight add http
|
|
1771
|
+
flight add db
|
|
1772
|
+
flight add auth
|
|
1773
|
+
`);
|
|
1774
|
+
}
|
|
1775
|
+
function showNextSteps(packageName, pkg) {
|
|
1776
|
+
const examples = {
|
|
1777
|
+
"http": `
|
|
1778
|
+
${pc5.cyan("Quick example:")}
|
|
1779
|
+
|
|
1780
|
+
import { createServer } from '@flight-framework/http';
|
|
1781
|
+
|
|
1782
|
+
const app = createServer();
|
|
1783
|
+
app.get('/', (c) => c.json({ hello: 'world' }));
|
|
1784
|
+
|
|
1785
|
+
export default { port: 3000, fetch: app.fetch };
|
|
1786
|
+
`,
|
|
1787
|
+
"db": `
|
|
1788
|
+
${pc5.cyan("Quick example:")}
|
|
1789
|
+
|
|
1790
|
+
import { createDb } from '@flight-framework/db';
|
|
1791
|
+
import { postgres } from '@flight-framework/db/postgres';
|
|
1792
|
+
|
|
1793
|
+
const db = createDb(postgres({ connectionString: process.env.DATABASE_URL }));
|
|
1794
|
+
const users = await db.query('SELECT * FROM users');
|
|
1795
|
+
|
|
1796
|
+
${pc5.dim("You may also need a driver:")}
|
|
1797
|
+
npm install pg ${pc5.dim("# PostgreSQL")}
|
|
1798
|
+
npm install @libsql/client ${pc5.dim("# Turso/SQLite")}
|
|
1799
|
+
npm install @neondatabase/serverless ${pc5.dim("# Neon")}
|
|
1800
|
+
`,
|
|
1801
|
+
"cache": `
|
|
1802
|
+
${pc5.cyan("Quick example:")}
|
|
1803
|
+
|
|
1804
|
+
import { createCache, lru } from '@flight-framework/cache';
|
|
1805
|
+
|
|
1806
|
+
const cache = createCache(lru({ max: 1000 }));
|
|
1807
|
+
await cache.set('key', { data: 'value' });
|
|
1808
|
+
const value = await cache.get('key');
|
|
1809
|
+
`,
|
|
1810
|
+
"auth": `
|
|
1811
|
+
${pc5.cyan("Quick example:")}
|
|
1812
|
+
|
|
1813
|
+
import { createAuth } from '@flight-framework/auth';
|
|
1814
|
+
import { betterAuth } from '@flight-framework/auth/better-auth';
|
|
1815
|
+
|
|
1816
|
+
const auth = createAuth(betterAuth({ db }));
|
|
1817
|
+
const user = await auth.getUser(request);
|
|
1818
|
+
`,
|
|
1819
|
+
"forms": `
|
|
1820
|
+
${pc5.cyan("Quick example:")}
|
|
1821
|
+
|
|
1822
|
+
import { createForm } from '@flight-framework/forms';
|
|
1823
|
+
import { zodAdapter } from '@flight-framework/forms/adapters/zod';
|
|
1824
|
+
|
|
1825
|
+
const form = createForm({ schema: zodAdapter(schema) });
|
|
1826
|
+
const result = form.validate(data);
|
|
1827
|
+
`
|
|
1828
|
+
};
|
|
1829
|
+
if (examples[packageName]) {
|
|
1830
|
+
console.log(examples[packageName]);
|
|
1831
|
+
}
|
|
1832
|
+
console.log(`${pc5.dim("Docs:")} https://flight.dev/docs/packages/${packageName}`);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1313
1835
|
// src/index.ts
|
|
1314
1836
|
var cli = cac("flight");
|
|
1315
1837
|
var LOGO = `
|
|
1316
|
-
${
|
|
1317
|
-
${
|
|
1318
|
-
${
|
|
1319
|
-
${
|
|
1320
|
-
${
|
|
1321
|
-
${
|
|
1838
|
+
${pc6.cyan(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557")}
|
|
1839
|
+
${pc6.cyan(" \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D")}
|
|
1840
|
+
${pc6.cyan(" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
1841
|
+
${pc6.cyan(" \u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
1842
|
+
${pc6.cyan(" \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 ")}
|
|
1843
|
+
${pc6.cyan(" \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ")}
|
|
1322
1844
|
|
|
1323
|
-
${
|
|
1324
|
-
${
|
|
1845
|
+
${pc6.dim("The Agnostic Full-Stack Framework")}
|
|
1846
|
+
${pc6.dim("Maximum Flexibility. Zero Lock-in.")}
|
|
1325
1847
|
`;
|
|
1326
1848
|
function printLogo() {
|
|
1327
1849
|
console.log(LOGO);
|
|
1328
1850
|
}
|
|
1329
1851
|
cli.version(VERSION);
|
|
1330
1852
|
cli.help();
|
|
1331
|
-
cli.command("create [name]", "Create a new Flight project").option("-t, --template <template>", "Project template to use", { default: "basic" }).option("--ui <framework>", "UI framework (react, vue, svelte, solid, vanilla)").option("--use-case <useCase>", "Use-case template (blog, ecommerce, saas, api, docs)").option("--ts", "Use TypeScript", { default: true }).option("--git", "Initialize git repository", { default: true }).option("--install", "Install dependencies", { default: true }).action(createCommand);
|
|
1853
|
+
cli.command("create [name]", "Create a new Flight project").option("-t, --template <template>", "Project template to use", { default: "basic" }).option("--ui <framework>", "UI framework (react, vue, svelte, solid, vanilla)").option("--use-case <useCase>", "Use-case template (blog, ecommerce, saas, api, docs)").option("--ts", "Use TypeScript", { default: true }).option("--git", "Initialize git repository", { default: true }).option("--install", "Install dependencies", { default: true }).option("--raw", "Create raw project (100% Web Standards, zero dependencies)").option("--empty", "Create empty project (just package.json)").option("--minimal", "Create minimal project (single server file with Flight)").action(createCommand);
|
|
1854
|
+
cli.command("add [package]", "Add a Flight package to your project").action(addCommand);
|
|
1332
1855
|
cli.command("dev", "Start development server").option("-p, --port <port>", "Port to listen on").option("-h, --host <host>", "Host to bind to").option("--open", "Open browser on start").option("--https", "Enable HTTPS").option("--ssr", "Enable Server-Side Rendering").action(devCommand);
|
|
1333
1856
|
cli.command("build", "Build for production").option("--outDir <dir>", "Output directory").option("--sourcemap", "Generate source maps").option("--minify", "Minify output", { default: true }).action(buildCommand);
|
|
1334
1857
|
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);
|
|
@@ -1342,7 +1865,7 @@ function run() {
|
|
|
1342
1865
|
}
|
|
1343
1866
|
cli.runMatchedCommand();
|
|
1344
1867
|
} catch (error) {
|
|
1345
|
-
console.error(
|
|
1868
|
+
console.error(pc6.red("Error:"), error instanceof Error ? error.message : error);
|
|
1346
1869
|
process.exit(1);
|
|
1347
1870
|
}
|
|
1348
1871
|
}
|