@ereo/cli 0.1.6

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/dist/index.js ADDED
@@ -0,0 +1,2335 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ var __defProp = Object.defineProperty;
4
+ var __export = (target, all) => {
5
+ for (var name in all)
6
+ __defProp(target, name, {
7
+ get: all[name],
8
+ enumerable: true,
9
+ configurable: true,
10
+ set: (newValue) => all[name] = () => newValue
11
+ });
12
+ };
13
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
+
15
+ // src/commands/build.ts
16
+ var exports_build = {};
17
+ __export(exports_build, {
18
+ build: () => build
19
+ });
20
+ import { join as join2 } from "path";
21
+ import { build as bundlerBuild, printBuildReport } from "@ereo/bundler";
22
+ async function build(options = {}) {
23
+ const root = process.cwd();
24
+ console.log(`
25
+ \x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m Production Build
26
+ `);
27
+ let config = {};
28
+ const configPath = join2(root, "ereo.config.ts");
29
+ try {
30
+ if (await Bun.file(configPath).exists()) {
31
+ const configModule = await import(configPath);
32
+ config = configModule.default || configModule;
33
+ }
34
+ } catch (error) {
35
+ console.warn("Could not load config:", error);
36
+ }
37
+ const configTarget = config.build?.target || "bun";
38
+ const bundlerTarget = ["bun", "node", "browser"].includes(configTarget) ? configTarget : "bun";
39
+ const buildOptions = {
40
+ root,
41
+ outDir: options.outDir || config.build?.outDir || ".ereo",
42
+ minify: options.minify ?? config.build?.minify ?? true,
43
+ sourcemap: options.sourcemap ?? config.build?.sourcemap ?? true,
44
+ target: bundlerTarget
45
+ };
46
+ console.log(` Target: ${buildOptions.target}`);
47
+ console.log(` Output: ${buildOptions.outDir}`);
48
+ console.log("");
49
+ const result = await bundlerBuild(buildOptions);
50
+ if (result.success) {
51
+ printBuildReport(result);
52
+ console.log(`
53
+ \x1B[32m\u2713\x1B[0m Build completed successfully
54
+ `);
55
+ } else {
56
+ console.error(`
57
+ \x1B[31m\u2717\x1B[0m Build failed
58
+ `);
59
+ if (result.errors) {
60
+ for (const error of result.errors) {
61
+ console.error(` ${error}`);
62
+ }
63
+ }
64
+ process.exit(1);
65
+ }
66
+ }
67
+ var init_build = () => {};
68
+
69
+ // src/commands/dev.ts
70
+ import { join } from "path";
71
+ import {
72
+ createApp,
73
+ setupEnv
74
+ } from "@ereo/core";
75
+ import { initFileRouter } from "@ereo/router";
76
+ import { createServer } from "@ereo/server";
77
+ import {
78
+ createHMRServer,
79
+ createHMRWatcher,
80
+ createHMRWebSocket,
81
+ HMR_CLIENT_CODE,
82
+ ERROR_OVERLAY_SCRIPT
83
+ } from "@ereo/bundler";
84
+ async function dev(options = {}) {
85
+ const port = options.port || 3000;
86
+ const hostname = options.host || "localhost";
87
+ const root = process.cwd();
88
+ console.log(`
89
+ \x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m Dev Server
90
+ `);
91
+ let config = {};
92
+ const configPath = join(root, "ereo.config.ts");
93
+ try {
94
+ if (await Bun.file(configPath).exists()) {
95
+ const configModule = await import(configPath);
96
+ config = configModule.default || configModule;
97
+ }
98
+ } catch (error) {
99
+ console.warn("Could not load config:", error);
100
+ }
101
+ if (config.env) {
102
+ console.log(" \x1B[2mLoading environment variables...\x1B[0m");
103
+ const envResult = await setupEnv(root, config.env, "development");
104
+ if (!envResult.valid) {
105
+ console.error(`
106
+ \x1B[31m\u2716\x1B[0m Environment validation failed
107
+ `);
108
+ process.exit(1);
109
+ }
110
+ console.log(` \x1B[32m\u2713\x1B[0m Loaded ${Object.keys(envResult.env).length} environment variables
111
+ `);
112
+ }
113
+ const app = createApp({
114
+ config: {
115
+ ...config,
116
+ server: {
117
+ port,
118
+ hostname,
119
+ development: true,
120
+ ...config.server
121
+ }
122
+ }
123
+ });
124
+ const router = await initFileRouter({
125
+ routesDir: config.routesDir || "app/routes",
126
+ watch: true
127
+ });
128
+ await router.loadAllModules();
129
+ const hmr = createHMRServer();
130
+ const hmrWatcher = createHMRWatcher(hmr);
131
+ hmrWatcher.watch(join(root, "app"));
132
+ router.on("reload", async () => {
133
+ console.log("\x1B[33m\u27F3\x1B[0m Routes reloaded");
134
+ await router.loadAllModules();
135
+ hmr.reload();
136
+ });
137
+ router.on("change", (route) => {
138
+ console.log(`\x1B[33m\u27F3\x1B[0m ${route.path} changed`);
139
+ hmr.jsUpdate(route.file);
140
+ });
141
+ const server = createServer({
142
+ port,
143
+ hostname,
144
+ development: true,
145
+ logging: true,
146
+ websocket: createHMRWebSocket(hmr)
147
+ });
148
+ server.setApp(app);
149
+ server.setRouter(router);
150
+ const pluginRegistry = app.getPluginRegistry();
151
+ await pluginRegistry.registerAll(config.plugins || []);
152
+ const devServer = {
153
+ ws: {
154
+ send: (data) => {
155
+ if (data && typeof data === "object") {
156
+ hmr.send(data);
157
+ }
158
+ },
159
+ on: (event, callback) => {}
160
+ },
161
+ restart: async () => {
162
+ console.log("\x1B[33m\u27F3\x1B[0m Restarting server...");
163
+ hmr.reload();
164
+ },
165
+ middlewares: [],
166
+ watcher: {
167
+ add: (path) => hmrWatcher.watch(path),
168
+ on: (event, callback) => {}
169
+ }
170
+ };
171
+ await pluginRegistry.configureServer(devServer);
172
+ for (const middleware of devServer.middlewares) {
173
+ server.use(middleware);
174
+ }
175
+ server.use(async (request, context, next) => {
176
+ const url = new URL(request.url);
177
+ if (url.pathname === "/__hmr-client.js") {
178
+ return new Response(HMR_CLIENT_CODE, {
179
+ headers: { "Content-Type": "text/javascript" }
180
+ });
181
+ }
182
+ try {
183
+ const response = await next();
184
+ if (response.headers.get("Content-Type")?.includes("text/html")) {
185
+ let html = await response.text();
186
+ const scripts = `
187
+ <script src="/__hmr-client.js"></script>
188
+ ${ERROR_OVERLAY_SCRIPT}
189
+ `;
190
+ html = html.replace("</body>", `${scripts}</body>`);
191
+ return new Response(html, {
192
+ status: response.status,
193
+ headers: response.headers
194
+ });
195
+ }
196
+ return response;
197
+ } catch (error) {
198
+ hmr.error(error instanceof Error ? error.message : String(error), error instanceof Error ? error.stack : undefined);
199
+ throw error;
200
+ }
201
+ });
202
+ await server.start();
203
+ console.log(` \x1B[32m\u279C\x1B[0m Local: \x1B[36mhttp://${hostname}:${port}/\x1B[0m`);
204
+ if (options.open) {
205
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
206
+ Bun.spawn([opener, `http://${hostname}:${port}`]);
207
+ }
208
+ console.log(`
209
+ \x1B[2mpress h to show help\x1B[0m
210
+ `);
211
+ if (process.stdin.isTTY) {
212
+ process.stdin.setRawMode(true);
213
+ process.stdin.resume();
214
+ process.stdin.on("data", async (data) => {
215
+ const key = data.toString();
216
+ switch (key) {
217
+ case "h":
218
+ console.log(`
219
+ Shortcuts:`);
220
+ console.log(" r - Reload routes");
221
+ console.log(" c - Clear console");
222
+ console.log(" q - Quit");
223
+ console.log("");
224
+ break;
225
+ case "r":
226
+ console.log("\x1B[33m\u27F3\x1B[0m Reloading routes...");
227
+ await router.discoverRoutes();
228
+ await router.loadAllModules();
229
+ hmr.reload();
230
+ break;
231
+ case "c":
232
+ console.clear();
233
+ console.log(`
234
+ \x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m Dev Server
235
+ `);
236
+ console.log(` \x1B[32m\u279C\x1B[0m Local: \x1B[36mhttp://${hostname}:${port}/\x1B[0m
237
+ `);
238
+ break;
239
+ case "q":
240
+ case "\x03":
241
+ console.log(`
242
+ Shutting down...
243
+ `);
244
+ server.stop();
245
+ hmrWatcher.stop();
246
+ process.exit(0);
247
+ break;
248
+ }
249
+ });
250
+ }
251
+ process.on("SIGINT", () => {
252
+ console.log(`
253
+ Shutting down...
254
+ `);
255
+ server.stop();
256
+ hmrWatcher.stop();
257
+ process.exit(0);
258
+ });
259
+ }
260
+
261
+ // src/index.ts
262
+ init_build();
263
+
264
+ // src/commands/start.ts
265
+ import { join as join3 } from "path";
266
+ import { createApp as createApp2 } from "@ereo/core";
267
+ import { initFileRouter as initFileRouter2 } from "@ereo/router";
268
+ import { createServer as createServer2 } from "@ereo/server";
269
+ async function start(options = {}) {
270
+ const root = process.cwd();
271
+ const buildDir = join3(root, ".ereo");
272
+ console.log(`
273
+ \x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m Production Server
274
+ `);
275
+ const manifestPath = join3(buildDir, "manifest.json");
276
+ if (!await Bun.file(manifestPath).exists()) {
277
+ console.error(" \x1B[31m\u2717\x1B[0m No build found. Run `ereo build` first.\n");
278
+ process.exit(1);
279
+ }
280
+ const manifest = await Bun.file(manifestPath).json();
281
+ let config = {};
282
+ const configPath = join3(root, "ereo.config.ts");
283
+ try {
284
+ if (await Bun.file(configPath).exists()) {
285
+ const configModule = await import(configPath);
286
+ config = configModule.default || configModule;
287
+ }
288
+ } catch {}
289
+ const port = options.port || config.server?.port || 3000;
290
+ const hostname = options.host || config.server?.hostname || "0.0.0.0";
291
+ const app = createApp2({
292
+ config: {
293
+ ...config,
294
+ server: {
295
+ port,
296
+ hostname,
297
+ development: false
298
+ }
299
+ }
300
+ });
301
+ const router = await initFileRouter2({
302
+ routesDir: config.routesDir || "app/routes",
303
+ watch: false
304
+ });
305
+ await router.loadAllModules();
306
+ const server = createServer2({
307
+ port,
308
+ hostname,
309
+ development: false,
310
+ logging: true,
311
+ static: {
312
+ root: join3(buildDir, "client"),
313
+ prefix: "/_ereo",
314
+ maxAge: 31536000,
315
+ immutable: true
316
+ }
317
+ });
318
+ server.setApp(app);
319
+ server.setRouter(router);
320
+ await server.start();
321
+ console.log(` \x1B[32m\u279C\x1B[0m Server running at \x1B[36mhttp://${hostname}:${port}/\x1B[0m
322
+ `);
323
+ process.on("SIGINT", () => {
324
+ console.log(`
325
+ Shutting down...
326
+ `);
327
+ server.stop();
328
+ process.exit(0);
329
+ });
330
+ process.on("SIGTERM", () => {
331
+ console.log(`
332
+ Shutting down...
333
+ `);
334
+ server.stop();
335
+ process.exit(0);
336
+ });
337
+ }
338
+
339
+ // src/commands/create.ts
340
+ import { join as join4 } from "path";
341
+ import { mkdir } from "fs/promises";
342
+ async function create(projectName, options = {}) {
343
+ const template = options.template || "tailwind";
344
+ const typescript = options.typescript !== false;
345
+ console.log(`
346
+ \x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m Create Project
347
+ `);
348
+ console.log(` Creating ${projectName} with ${template} template...
349
+ `);
350
+ const projectDir = join4(process.cwd(), projectName);
351
+ await mkdir(projectDir, { recursive: true });
352
+ await mkdir(join4(projectDir, "app/routes"), { recursive: true });
353
+ await mkdir(join4(projectDir, "app/components"), { recursive: true });
354
+ await mkdir(join4(projectDir, "app/middleware"), { recursive: true });
355
+ await mkdir(join4(projectDir, "public"), { recursive: true });
356
+ const files = generateTemplateFiles(template, typescript, projectName);
357
+ const sortedPaths = Object.keys(files).sort();
358
+ for (const path of sortedPaths) {
359
+ const content = files[path];
360
+ const fullPath = join4(projectDir, path);
361
+ await mkdir(join4(fullPath, ".."), { recursive: true });
362
+ await Bun.write(fullPath, content);
363
+ console.log(` \x1B[32m+\x1B[0m ${path}`);
364
+ }
365
+ console.log(`
366
+ \x1B[32m\u2713\x1B[0m Project created successfully!
367
+ `);
368
+ console.log(` Next steps:
369
+ `);
370
+ console.log(` cd ${projectName}`);
371
+ console.log(" bun install");
372
+ console.log(` bun run dev
373
+ `);
374
+ }
375
+ function generateTemplateFiles(template, typescript, projectName) {
376
+ const ext = typescript ? "tsx" : "jsx";
377
+ const files = {};
378
+ const dependencies = {
379
+ "@ereo/core": "^0.1.0",
380
+ "@ereo/router": "^0.1.0",
381
+ "@ereo/server": "^0.1.0",
382
+ "@ereo/client": "^0.1.0",
383
+ "@ereo/data": "^0.1.0",
384
+ "@ereo/cli": "^0.1.0",
385
+ react: "^18.2.0",
386
+ "react-dom": "^18.2.0"
387
+ };
388
+ if (template === "tailwind") {
389
+ dependencies["@ereo/plugin-tailwind"] = "^0.1.0";
390
+ }
391
+ files["package.json"] = JSON.stringify({
392
+ name: projectName,
393
+ version: "0.1.0",
394
+ type: "module",
395
+ scripts: {
396
+ dev: "ereo dev",
397
+ build: "ereo build",
398
+ start: "ereo start"
399
+ },
400
+ dependencies,
401
+ devDependencies: typescript ? {
402
+ "@types/react": "^18.2.0",
403
+ "@types/react-dom": "^18.2.0",
404
+ typescript: "^5.4.0"
405
+ } : {}
406
+ }, null, 2);
407
+ if (typescript) {
408
+ files["tsconfig.json"] = JSON.stringify({
409
+ compilerOptions: {
410
+ target: "ESNext",
411
+ module: "ESNext",
412
+ moduleResolution: "bundler",
413
+ jsx: "react-jsx",
414
+ strict: true,
415
+ esModuleInterop: true,
416
+ skipLibCheck: true,
417
+ forceConsistentCasingInFileNames: true,
418
+ types: ["bun-types"]
419
+ },
420
+ include: ["app/**/*", "ereo.config.ts"]
421
+ }, null, 2);
422
+ }
423
+ files[`ereo.config.${typescript ? "ts" : "js"}`] = generateEreoConfig(template);
424
+ files[".env"] = generateEnvFile();
425
+ files[".env.example"] = generateEnvFile();
426
+ files[`app/routes/_layout.${ext}`] = generateRootLayout(template, typescript);
427
+ files[`app/entry.client.${ext}`] = generateClientEntry(typescript);
428
+ files[`app/routes/index.${ext}`] = generateIndexPage(template, typescript);
429
+ files[`app/routes/about.${ext}`] = generateAboutPage(template, typescript);
430
+ files[`app/routes/contact.${ext}`] = generateContactPage(template, typescript);
431
+ files[`app/components/Counter.${ext}`] = generateCounterComponent(template, typescript);
432
+ files[`app/middleware/logger.${typescript ? "ts" : "js"}`] = generateLoggerMiddleware(typescript);
433
+ files[`app/routes/_error.${ext}`] = generateErrorBoundary(template, typescript);
434
+ files[`app/routes/blog/[slug].${ext}`] = generateDynamicRoute(template, typescript);
435
+ files[`app/routes/blog/index.${ext}`] = generateBlogIndex(template, typescript);
436
+ files[`app/routes/api/health.${typescript ? "ts" : "js"}`] = generateApiRoute(typescript);
437
+ if (template === "tailwind") {
438
+ files["tailwind.config.js"] = generateTailwindConfig();
439
+ files["app/globals.css"] = generateGlobalCSS();
440
+ }
441
+ files[".gitignore"] = generateGitignore();
442
+ return files;
443
+ }
444
+ function generateEreoConfig(template) {
445
+ const tailwindImport = template === "tailwind" ? `import tailwind from '@ereo/plugin-tailwind';
446
+ ` : "";
447
+ const tailwindPlugin = template === "tailwind" ? " tailwind()," : "";
448
+ return `import { defineConfig } from '@ereo/core';
449
+ ${tailwindImport}
450
+ export default defineConfig({
451
+ server: {
452
+ port: 3000,
453
+ },
454
+ build: {
455
+ target: 'bun',
456
+ },
457
+ plugins: [
458
+ ${tailwindPlugin}
459
+ ],
460
+ });
461
+ `.trim();
462
+ }
463
+ function generateEnvFile() {
464
+ return `# Environment Variables
465
+ # Prefix with EREO_PUBLIC_ to expose to the client
466
+
467
+ # Server-only (never sent to browser)
468
+ DATABASE_URL=postgresql://localhost:5432/mydb
469
+ API_SECRET=your-secret-key
470
+
471
+ # Public (available in client code)
472
+ EREO_PUBLIC_APP_NAME=EreoJS App
473
+ EREO_PUBLIC_API_URL=http://localhost:3000/api
474
+ `.trim();
475
+ }
476
+ function generateRootLayout(template, typescript) {
477
+ const imports = typescript ? `import type { ReactNode } from 'react';
478
+ import { Link } from '@ereo/client';` : `import { Link } from '@ereo/client';`;
479
+ const propsType = typescript ? ": { children: ReactNode }" : "";
480
+ const tailwindStyles = template === "tailwind";
481
+ const navClasses = tailwindStyles ? ' className="flex gap-4 p-4 border-b border-gray-200 dark:border-gray-700"' : "";
482
+ const linkClasses = tailwindStyles ? ' className="text-blue-600 hover:text-blue-800 dark:text-blue-400"' : "";
483
+ const bodyClasses = tailwindStyles ? ' className="min-h-screen bg-white dark:bg-gray-900 text-gray-900 dark:text-white"' : "";
484
+ const stylesheet = tailwindStyles ? `
485
+ <link rel="stylesheet" href="/app/globals.css" />` : "";
486
+ return `${imports}
487
+
488
+ export default function RootLayout({ children }${propsType}) {
489
+ return (
490
+ <html lang="en">
491
+ <head>
492
+ <meta charSet="utf-8" />
493
+ <meta name="viewport" content="width=device-width, initial-scale=1" />${stylesheet}
494
+ </head>
495
+ <body${bodyClasses}>
496
+ <nav${navClasses}>
497
+ <Link to="/"${linkClasses}>Home</Link>
498
+ <Link to="/about"${linkClasses}>About</Link>
499
+ <Link to="/blog"${linkClasses}>Blog</Link>
500
+ <Link to="/contact"${linkClasses}>Contact</Link>
501
+ </nav>
502
+ {children}
503
+ {/* Client-side hydration script - bundled by EreoJS */}
504
+ <script type="module" src="/@ereo/client-entry.js" />
505
+ </body>
506
+ </html>
507
+ );
508
+ }
509
+ `.trim();
510
+ }
511
+ function generateClientEntry(typescript) {
512
+ return `/**
513
+ * Client Entry Point
514
+ *
515
+ * This file initializes the client-side runtime:
516
+ * - Hydrates island components
517
+ * - Sets up client-side navigation
518
+ * - Enables link prefetching
519
+ */
520
+ import { initClient } from '@ereo/client';
521
+
522
+ // Initialize the EreoJS client runtime
523
+ initClient();
524
+
525
+ // You can also manually hydrate specific islands:
526
+ // import { hydrateIslands } from '@ereo/client';
527
+ // hydrateIslands();
528
+ `.trim();
529
+ }
530
+ function generateIndexPage(template, typescript) {
531
+ const imports = typescript ? `import type { LoaderArgs, MetaArgs, RouteConfig } from '@ereo/core';
532
+ import { Counter } from '../components/Counter';` : `import { Counter } from '../components/Counter';`;
533
+ const loaderType = typescript ? ": LoaderArgs" : "";
534
+ const loaderDataType = `{ message: string; timestamp: string; visitors: number }`;
535
+ const metaType = typescript ? `: MetaArgs<${loaderDataType}>` : "";
536
+ const tailwindStyles = template === "tailwind";
537
+ const mainClasses = tailwindStyles ? ' className="flex flex-col items-center justify-center min-h-[80vh] p-8"' : "";
538
+ const h1Classes = tailwindStyles ? ' className="text-4xl font-bold mb-4"' : "";
539
+ const pClasses = tailwindStyles ? ' className="text-gray-600 dark:text-gray-400 mb-2"' : "";
540
+ const sectionClasses = tailwindStyles ? ' className="mt-8 p-6 border border-gray-200 dark:border-gray-700 rounded-lg"' : "";
541
+ const h2Classes = tailwindStyles ? ' className="text-xl font-semibold mb-4"' : "";
542
+ const configType = typescript ? ": RouteConfig" : "";
543
+ return `${imports}
544
+
545
+ /**
546
+ * Route Configuration
547
+ *
548
+ * Export a config object to configure middleware, caching,
549
+ * rendering mode, and other route-level settings.
550
+ */
551
+ export const config${configType} = {
552
+ // Apply middleware to this route
553
+ middleware: ['logger'],
554
+ // Cache configuration
555
+ cache: {
556
+ edge: {
557
+ maxAge: 60,
558
+ staleWhileRevalidate: 300,
559
+ },
560
+ data: {
561
+ tags: ['homepage', 'content'],
562
+ },
563
+ },
564
+ };
565
+
566
+ /**
567
+ * Loader - Server-side data fetching
568
+ *
569
+ * Runs on the server for every request. Use context.cache
570
+ * for explicit cache control with tagged invalidation.
571
+ */
572
+ export async function loader({ request, params, context }${loaderType}) {
573
+ // Access environment variables
574
+ const appName = context.env.EREO_PUBLIC_APP_NAME || 'EreoJS App';
575
+
576
+ return {
577
+ message: \`Welcome to \${appName}!\`,
578
+ timestamp: new Date().toISOString(),
579
+ visitors: Math.floor(Math.random() * 1000),
580
+ };
581
+ }
582
+
583
+ /**
584
+ * Meta - Dynamic SEO metadata
585
+ *
586
+ * Generate meta tags based on loader data.
587
+ */
588
+ export function meta({ data }${metaType}) {
589
+ return [
590
+ { title: data.message },
591
+ { name: 'description', content: 'A blazing fast React framework built on Bun' },
592
+ { property: 'og:title', content: data.message },
593
+ ];
594
+ }
595
+
596
+ /**
597
+ * Page Component
598
+ *
599
+ * Receives loaderData from the loader function.
600
+ * For client components, use useLoaderData() hook from @ereo/client.
601
+ */
602
+ export default function HomePage({ loaderData }${typescript ? `: { loaderData: ${loaderDataType} }` : ""}) {
603
+ return (
604
+ <main${mainClasses}>
605
+ <h1${h1Classes}>
606
+ {loaderData.message}
607
+ </h1>
608
+ <p${pClasses}>
609
+ Server time: {loaderData.timestamp}
610
+ </p>
611
+ <p${pClasses}>
612
+ Today's visitors: {loaderData.visitors}
613
+ </p>
614
+
615
+ {/* Islands Architecture Example */}
616
+ <section${sectionClasses}>
617
+ <h2${h2Classes}>Interactive Island</h2>
618
+ <p${pClasses}>
619
+ This counter is an "island" - only this component hydrates on the client.
620
+ The rest of the page stays static HTML with zero JavaScript.
621
+ </p>
622
+ {/* client:load hydrates immediately */}
623
+ <Counter client:load initialCount={0} />
624
+ </section>
625
+ </main>
626
+ );
627
+ }
628
+ `.trim();
629
+ }
630
+ function generateAboutPage(template, typescript) {
631
+ const imports = typescript ? `import type { MetaFunction } from '@ereo/core';` : "";
632
+ const tailwindStyles = template === "tailwind";
633
+ const mainClasses = tailwindStyles ? ' className="flex flex-col items-center justify-center min-h-[80vh] p-8"' : "";
634
+ const h1Classes = tailwindStyles ? ' className="text-4xl font-bold mb-4"' : "";
635
+ const pClasses = tailwindStyles ? ' className="text-gray-600 dark:text-gray-400 max-w-2xl text-center"' : "";
636
+ const ulClasses = tailwindStyles ? ' className="mt-6 space-y-2 text-left"' : "";
637
+ const liClasses = tailwindStyles ? ' className="flex items-center gap-2"' : "";
638
+ const metaExport = typescript ? `
639
+ /**
640
+ * Static meta tags for this page
641
+ */
642
+ export const meta: MetaFunction = () => {
643
+ return [
644
+ { title: 'About - EreoJS App' },
645
+ { name: 'description', content: 'Learn about the EreoJS framework' },
646
+ ];
647
+ };
648
+ ` : `
649
+ /**
650
+ * Static meta tags for this page
651
+ */
652
+ export function meta() {
653
+ return [
654
+ { title: 'About - EreoJS App' },
655
+ { name: 'description', content: 'Learn about the EreoJS framework' },
656
+ ];
657
+ }
658
+ `;
659
+ return `${imports}
660
+ ${metaExport}
661
+ /**
662
+ * About Page - Static content (no loader needed)
663
+ *
664
+ * Pages without loaders are rendered as static HTML.
665
+ */
666
+ export default function AboutPage() {
667
+ return (
668
+ <main${mainClasses}>
669
+ <h1${h1Classes}>
670
+ About EreoJS
671
+ </h1>
672
+ <p${pClasses}>
673
+ EreoJS is a React fullstack framework built on Bun, designed for
674
+ simplicity and performance. It features islands architecture for
675
+ minimal JavaScript and explicit caching for predictable behavior.
676
+ </p>
677
+ <ul${ulClasses}>
678
+ <li${liClasses}>
679
+ <span>5-6x faster than Node.js</span>
680
+ </li>
681
+ <li${liClasses}>
682
+ <span>Islands architecture for minimal JS</span>
683
+ </li>
684
+ <li${liClasses}>
685
+ <span>One unified loader pattern</span>
686
+ </li>
687
+ <li${liClasses}>
688
+ <span>Explicit tagged cache invalidation</span>
689
+ </li>
690
+ </ul>
691
+ </main>
692
+ );
693
+ }
694
+ `.trim();
695
+ }
696
+ function generateContactPage(template, typescript) {
697
+ const imports = typescript ? `import type { ActionArgs, LoaderArgs, RouteConfig } from '@ereo/core';
698
+ import { json } from '@ereo/data';
699
+ import { Form, useActionData, useNavigation } from '@ereo/client';` : `import { json } from '@ereo/data';
700
+ import { Form, useActionData, useNavigation } from '@ereo/client';`;
701
+ const loaderType = typescript ? ": LoaderArgs" : "";
702
+ const actionType = typescript ? ": ActionArgs" : "";
703
+ const propsType = typescript ? ": { loaderData: { csrfToken: string } }" : "";
704
+ const actionDataType = typescript ? `
705
+ interface ActionData {
706
+ success: boolean;
707
+ message?: string;
708
+ errors?: Record<string, string>;
709
+ }` : "";
710
+ const tailwindStyles = template === "tailwind";
711
+ const mainClasses = tailwindStyles ? ' className="flex flex-col items-center justify-center min-h-[80vh] p-8"' : "";
712
+ const h1Classes = tailwindStyles ? ' className="text-4xl font-bold mb-4"' : "";
713
+ const formClasses = tailwindStyles ? ' className="w-full max-w-md space-y-4"' : "";
714
+ const labelClasses = tailwindStyles ? ' className="block text-sm font-medium mb-1"' : "";
715
+ const inputClasses = tailwindStyles ? ' className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800"' : "";
716
+ const textareaClasses = tailwindStyles ? ' className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 h-32"' : "";
717
+ const buttonClasses = tailwindStyles ? ' className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"' : "";
718
+ const errorClasses = tailwindStyles ? ' className="text-red-600 dark:text-red-400 text-sm mt-1"' : "";
719
+ const successClasses = tailwindStyles ? ' className="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 p-4 rounded-md mb-4"' : "";
720
+ return `${imports}
721
+ ${actionDataType}
722
+
723
+ /**
724
+ * Route Configuration
725
+ */
726
+ export const config${typescript ? ": RouteConfig" : ""} = {
727
+ // Progressive enhancement - form works without JS
728
+ progressive: {
729
+ forms: {
730
+ fallback: 'server',
731
+ },
732
+ },
733
+ };
734
+
735
+ /**
736
+ * Loader - Provide CSRF token for form security
737
+ */
738
+ export async function loader({ context }${loaderType}) {
739
+ return {
740
+ csrfToken: crypto.randomUUID(),
741
+ };
742
+ }
743
+
744
+ /**
745
+ * Action - Handle form submission
746
+ *
747
+ * Actions handle POST/PUT/DELETE requests.
748
+ * Use json() for responses, redirect() for redirects.
749
+ */
750
+ export async function action({ request, context }${actionType})${typescript ? ": Promise<Response>" : ""} {
751
+ const formData = await request.formData();
752
+
753
+ const name = formData.get('name') as string;
754
+ const email = formData.get('email') as string;
755
+ const message = formData.get('message') as string;
756
+
757
+ // Validate
758
+ const errors${typescript ? ": Record<string, string>" : ""} = {};
759
+ if (!name || name.length < 2) {
760
+ errors.name = 'Name must be at least 2 characters';
761
+ }
762
+ if (!email || !email.includes('@')) {
763
+ errors.email = 'Please enter a valid email';
764
+ }
765
+ if (!message || message.length < 10) {
766
+ errors.message = 'Message must be at least 10 characters';
767
+ }
768
+
769
+ if (Object.keys(errors).length > 0) {
770
+ return json({ success: false, errors }, { status: 400 });
771
+ }
772
+
773
+ // Process the submission (e.g., send email, save to DB)
774
+ console.log('Contact form submitted:', { name, email, message });
775
+
776
+ // Return success or redirect
777
+ return json({ success: true, message: 'Thank you for your message!' });
778
+ }
779
+
780
+ /**
781
+ * Contact Page with Enhanced Form
782
+ *
783
+ * Uses the Form component from @ereo/client for:
784
+ * - Automatic loading states
785
+ * - Client-side validation feedback
786
+ * - Progressive enhancement (works without JS)
787
+ *
788
+ * The useActionData hook provides access to the action response.
789
+ * The useNavigation hook provides loading state.
790
+ */
791
+ export default function ContactPage({ loaderData }${propsType}) {
792
+ // Get action response data (available after form submission)
793
+ const actionData = useActionData${typescript ? "<ActionData>" : ""}();
794
+ // Get navigation state for loading indicator
795
+ const navigation = useNavigation();
796
+ const isSubmitting = navigation.state === 'submitting';
797
+
798
+ return (
799
+ <main${mainClasses}>
800
+ <h1${h1Classes}>Contact Us</h1>
801
+
802
+ {actionData?.success && (
803
+ <div${successClasses}>
804
+ {actionData.message}
805
+ </div>
806
+ )}
807
+
808
+ <Form method="post"${formClasses}>
809
+ <input type="hidden" name="csrf" value={loaderData.csrfToken} />
810
+
811
+ <div>
812
+ <label htmlFor="name"${labelClasses}>Name</label>
813
+ <input
814
+ type="text"
815
+ id="name"
816
+ name="name"
817
+ required${inputClasses}
818
+ aria-invalid={actionData?.errors?.name ? 'true' : undefined}
819
+ />
820
+ {actionData?.errors?.name && (
821
+ <p${errorClasses}>{actionData.errors.name}</p>
822
+ )}
823
+ </div>
824
+
825
+ <div>
826
+ <label htmlFor="email"${labelClasses}>Email</label>
827
+ <input
828
+ type="email"
829
+ id="email"
830
+ name="email"
831
+ required${inputClasses}
832
+ aria-invalid={actionData?.errors?.email ? 'true' : undefined}
833
+ />
834
+ {actionData?.errors?.email && (
835
+ <p${errorClasses}>{actionData.errors.email}</p>
836
+ )}
837
+ </div>
838
+
839
+ <div>
840
+ <label htmlFor="message"${labelClasses}>Message</label>
841
+ <textarea
842
+ id="message"
843
+ name="message"
844
+ required${textareaClasses}
845
+ aria-invalid={actionData?.errors?.message ? 'true' : undefined}
846
+ />
847
+ {actionData?.errors?.message && (
848
+ <p${errorClasses}>{actionData.errors.message}</p>
849
+ )}
850
+ </div>
851
+
852
+ <button type="submit" disabled={isSubmitting}${buttonClasses}>
853
+ {isSubmitting ? 'Sending...' : 'Send Message'}
854
+ </button>
855
+ </Form>
856
+ </main>
857
+ );
858
+ }
859
+ `.trim();
860
+ }
861
+ function generateCounterComponent(template, typescript) {
862
+ const propsType = typescript ? ": { initialCount?: number }" : "";
863
+ const tailwindStyles = template === "tailwind";
864
+ const containerClasses = tailwindStyles ? ' className="flex items-center gap-4 mt-4"' : "";
865
+ const buttonClasses = tailwindStyles ? ' className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"' : "";
866
+ const countClasses = tailwindStyles ? ' className="text-2xl font-bold min-w-[3ch] text-center"' : "";
867
+ return `'use client';
868
+ /**
869
+ * Counter Component - Island Example
870
+ *
871
+ * This component demonstrates the islands architecture.
872
+ * Only components with 'use client' directive hydrate on the client.
873
+ *
874
+ * Hydration strategies:
875
+ * - client:load - Hydrate immediately on page load
876
+ * - client:idle - Hydrate when browser is idle
877
+ * - client:visible - Hydrate when element is visible (IntersectionObserver)
878
+ * - client:media - Hydrate when media query matches
879
+ *
880
+ * Usage:
881
+ * <Counter client:load initialCount={0} />
882
+ * <Counter client:visible initialCount={5} />
883
+ */
884
+ import { useState } from 'react';
885
+
886
+ export function Counter({ initialCount = 0 }${propsType}) {
887
+ const [count, setCount] = useState(initialCount);
888
+
889
+ return (
890
+ <div${containerClasses}>
891
+ <button onClick={() => setCount(c => c - 1)}${buttonClasses}>
892
+ -
893
+ </button>
894
+ <span${countClasses}>{count}</span>
895
+ <button onClick={() => setCount(c => c + 1)}${buttonClasses}>
896
+ +
897
+ </button>
898
+ </div>
899
+ );
900
+ }
901
+ `.trim();
902
+ }
903
+ function generateLoggerMiddleware(typescript) {
904
+ const imports = typescript ? `import type { MiddlewareHandler } from '@ereo/core';` : "";
905
+ const typeAnnotation = typescript ? ": MiddlewareHandler" : "";
906
+ return `${imports}
907
+ /**
908
+ * Logger Middleware
909
+ *
910
+ * Logs request information and timing.
911
+ *
912
+ * Register in ereo.config.ts or use the route config:
913
+ *
914
+ * export const config = {
915
+ * middleware: ['logger'],
916
+ * };
917
+ */
918
+ export const logger${typeAnnotation} = async (request, context, next) => {
919
+ const start = Date.now();
920
+ const url = new URL(request.url);
921
+
922
+ console.log(\`--> \${request.method} \${url.pathname}\`);
923
+
924
+ // Call next middleware/handler
925
+ const response = await next();
926
+
927
+ const duration = Date.now() - start;
928
+ console.log(\`<-- \${request.method} \${url.pathname} \${response.status} \${duration}ms\`);
929
+
930
+ return response;
931
+ };
932
+
933
+ export default logger;
934
+ `.trim();
935
+ }
936
+ function generateErrorBoundary(template, typescript) {
937
+ const imports = typescript ? `import type { RouteErrorComponentProps } from '@ereo/core';
938
+ import { Link } from '@ereo/client';
939
+ import { isRouteErrorResponse } from '@ereo/client';` : `import { Link } from '@ereo/client';
940
+ import { isRouteErrorResponse } from '@ereo/client';`;
941
+ const propsType = typescript ? ": RouteErrorComponentProps" : "";
942
+ const tailwindStyles = template === "tailwind";
943
+ const mainClasses = tailwindStyles ? ' className="flex flex-col items-center justify-center min-h-[80vh] p-8"' : "";
944
+ const h1Classes = tailwindStyles ? ' className="text-4xl font-bold text-red-600 mb-4"' : "";
945
+ const pClasses = tailwindStyles ? ' className="text-gray-600 dark:text-gray-400 mb-4"' : "";
946
+ const preClasses = tailwindStyles ? ' className="p-4 bg-gray-100 dark:bg-gray-800 rounded-md overflow-auto max-w-2xl text-sm"' : "";
947
+ const linkClasses = tailwindStyles ? ' className="mt-6 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 inline-block"' : "";
948
+ const statusClasses = tailwindStyles ? ' className="text-6xl font-bold text-gray-300 dark:text-gray-600 mb-4"' : "";
949
+ return `${imports}
950
+
951
+ /**
952
+ * Error Boundary Page
953
+ *
954
+ * This component renders when an error occurs in a route.
955
+ * It receives the error and route params.
956
+ *
957
+ * Error types:
958
+ * - Response errors (404, 500, etc.) - thrown via \`throw new Response()\`
959
+ * - JavaScript errors - unexpected exceptions
960
+ *
961
+ * File naming:
962
+ * - _error.tsx - Catches errors in current route and children
963
+ * - error.tsx - Same as above (alternative naming)
964
+ *
965
+ * The error boundary closest to the error will be used.
966
+ */
967
+ export default function ErrorBoundary({ error, params }${propsType}) {
968
+ // Check if this is a Response error (e.g., 404)
969
+ if (isRouteErrorResponse(error)) {
970
+ return (
971
+ <main${mainClasses}>
972
+ <div${statusClasses}>{error.status}</div>
973
+ <h1${h1Classes}>
974
+ {error.status === 404 ? 'Page Not Found' : 'Error'}
975
+ </h1>
976
+ <p${pClasses}>
977
+ {error.status === 404
978
+ ? "The page you're looking for doesn't exist."
979
+ : error.statusText || 'An error occurred.'}
980
+ </p>
981
+ <Link to="/"${linkClasses}>
982
+ Go back home
983
+ </Link>
984
+ </main>
985
+ );
986
+ }
987
+
988
+ // JavaScript/runtime error
989
+ return (
990
+ <main${mainClasses}>
991
+ <h1${h1Classes}>Something went wrong</h1>
992
+ <p${pClasses}>
993
+ We're sorry, but an error occurred while processing your request.
994
+ </p>
995
+
996
+ {process.env.NODE_ENV === 'development' && (
997
+ <pre${preClasses}>
998
+ <code>{error.message}</code>
999
+ {error.stack && (
1000
+ <>
1001
+ {'\\n\\n'}
1002
+ {error.stack}
1003
+ </>
1004
+ )}
1005
+ </pre>
1006
+ )}
1007
+
1008
+ <Link to="/"${linkClasses}>
1009
+ Go back home
1010
+ </Link>
1011
+ </main>
1012
+ );
1013
+ }
1014
+ `.trim();
1015
+ }
1016
+ function generateTailwindConfig() {
1017
+ return `/** @type {import('tailwindcss').Config} */
1018
+ export default {
1019
+ content: [
1020
+ './app/**/*.{js,ts,jsx,tsx}',
1021
+ './components/**/*.{js,ts,jsx,tsx}',
1022
+ ],
1023
+ darkMode: 'class',
1024
+ theme: {
1025
+ extend: {
1026
+ // Add your custom theme extensions here
1027
+ },
1028
+ },
1029
+ plugins: [],
1030
+ };
1031
+ `.trim();
1032
+ }
1033
+ function generateGlobalCSS() {
1034
+ return `@tailwind base;
1035
+ @tailwind components;
1036
+ @tailwind utilities;
1037
+
1038
+ /* Custom global styles */
1039
+ @layer base {
1040
+ html {
1041
+ @apply antialiased;
1042
+ }
1043
+
1044
+ body {
1045
+ @apply bg-white dark:bg-gray-900 text-gray-900 dark:text-white;
1046
+ }
1047
+ }
1048
+
1049
+ @layer components {
1050
+ /* Add reusable component styles here */
1051
+ }
1052
+
1053
+ @layer utilities {
1054
+ /* Add custom utilities here */
1055
+ }
1056
+ `.trim();
1057
+ }
1058
+ function generateGitignore() {
1059
+ return `# Dependencies
1060
+ node_modules
1061
+
1062
+ # Build output
1063
+ .ereo
1064
+ dist
1065
+
1066
+ # Environment
1067
+ .env
1068
+ .env.local
1069
+ .env.*.local
1070
+
1071
+ # Logs
1072
+ *.log
1073
+ npm-debug.log*
1074
+
1075
+ # OS
1076
+ .DS_Store
1077
+ Thumbs.db
1078
+
1079
+ # IDE
1080
+ .vscode
1081
+ .idea
1082
+ *.swp
1083
+ *.swo
1084
+
1085
+ # Bun
1086
+ bun.lockb
1087
+ `.trim();
1088
+ }
1089
+ function generateDynamicRoute(template, typescript) {
1090
+ const imports = typescript ? `import type { LoaderArgs, MetaArgs, RouteConfig } from '@ereo/core';
1091
+ import { Link } from '@ereo/client';` : `import { Link } from '@ereo/client';`;
1092
+ const loaderType = typescript ? ": LoaderArgs" : "";
1093
+ const configType = typescript ? ": RouteConfig" : "";
1094
+ const postType = typescript ? `
1095
+ interface Post {
1096
+ slug: string;
1097
+ title: string;
1098
+ content: string;
1099
+ author: string;
1100
+ publishedAt: string;
1101
+ }` : "";
1102
+ const metaType = typescript ? ": MetaArgs<Post>" : "";
1103
+ const propsType = typescript ? ": { loaderData: Post }" : "";
1104
+ const tailwindStyles = template === "tailwind";
1105
+ const articleClasses = tailwindStyles ? ' className="max-w-3xl mx-auto p-8"' : "";
1106
+ const backLinkClasses = tailwindStyles ? ' className="text-blue-600 hover:text-blue-800 dark:text-blue-400 mb-6 inline-block"' : "";
1107
+ const h1Classes = tailwindStyles ? ' className="text-4xl font-bold mb-4"' : "";
1108
+ const metaClasses = tailwindStyles ? ' className="text-gray-500 dark:text-gray-400 mb-8"' : "";
1109
+ const contentClasses = tailwindStyles ? ' className="prose dark:prose-invert max-w-none"' : "";
1110
+ return `${imports}
1111
+ ${postType}
1112
+
1113
+ /**
1114
+ * Route Configuration for dynamic routes
1115
+ *
1116
+ * Cache by slug parameter for efficient CDN caching.
1117
+ */
1118
+ export const config${configType} = {
1119
+ cache: {
1120
+ edge: {
1121
+ maxAge: 3600,
1122
+ staleWhileRevalidate: 86400,
1123
+ },
1124
+ data: {
1125
+ // Dynamic tags based on the slug parameter
1126
+ tags: (params) => ['blog', \`post:\${params.slug}\`],
1127
+ },
1128
+ },
1129
+ };
1130
+
1131
+ /**
1132
+ * Loader - Fetch blog post by slug
1133
+ *
1134
+ * The slug parameter comes from the [slug] in the filename.
1135
+ * Access via params.slug.
1136
+ */
1137
+ export async function loader({ params, context }${loaderType})${typescript ? ": Promise<Post>" : ""} {
1138
+ const { slug } = params;
1139
+
1140
+ // In a real app, fetch from database or CMS
1141
+ // Example: const post = await db.posts.findBySlug(slug);
1142
+
1143
+ // Mock data for demonstration
1144
+ const posts${typescript ? ": Record<string, Post>" : ""} = {
1145
+ 'hello-world': {
1146
+ slug: 'hello-world',
1147
+ title: 'Hello World',
1148
+ content: 'This is the first blog post using EreoJS framework. It demonstrates dynamic routing with [slug] parameters.',
1149
+ author: 'EreoJS Team',
1150
+ publishedAt: '2024-01-15',
1151
+ },
1152
+ 'getting-started': {
1153
+ slug: 'getting-started',
1154
+ title: 'Getting Started with EreoJS',
1155
+ content: 'Learn how to build blazing fast applications with EreoJS and Bun. This guide covers loaders, actions, and islands architecture.',
1156
+ author: 'EreoJS Team',
1157
+ publishedAt: '2024-01-20',
1158
+ },
1159
+ };
1160
+
1161
+ const post = posts[slug${typescript ? " as string" : ""}];
1162
+
1163
+ if (!post) {
1164
+ throw new Response('Post not found', { status: 404 });
1165
+ }
1166
+
1167
+ return post;
1168
+ }
1169
+
1170
+ /**
1171
+ * Meta - Generate SEO tags from post data
1172
+ */
1173
+ export function meta({ data }${metaType}) {
1174
+ return [
1175
+ { title: \`\${data.title} - Blog\` },
1176
+ { name: 'description', content: data.content.slice(0, 160) },
1177
+ { property: 'og:title', content: data.title },
1178
+ { property: 'og:type', content: 'article' },
1179
+ { property: 'article:author', content: data.author },
1180
+ { property: 'article:published_time', content: data.publishedAt },
1181
+ ];
1182
+ }
1183
+
1184
+ /**
1185
+ * Blog Post Page
1186
+ */
1187
+ export default function BlogPost({ loaderData: post }${propsType}) {
1188
+ return (
1189
+ <article${articleClasses}>
1190
+ <Link to="/blog"${backLinkClasses}>
1191
+ \u2190 Back to Blog
1192
+ </Link>
1193
+
1194
+ <h1${h1Classes}>{post.title}</h1>
1195
+
1196
+ <div${metaClasses}>
1197
+ By {post.author} \u2022 {new Date(post.publishedAt).toLocaleDateString()}
1198
+ </div>
1199
+
1200
+ <div${contentClasses}>
1201
+ <p>{post.content}</p>
1202
+ </div>
1203
+ </article>
1204
+ );
1205
+ }
1206
+ `.trim();
1207
+ }
1208
+ function generateBlogIndex(template, typescript) {
1209
+ const imports = typescript ? `import type { LoaderArgs, MetaArgs } from '@ereo/core';
1210
+ import { Link } from '@ereo/client';` : `import { Link } from '@ereo/client';`;
1211
+ const loaderType = typescript ? ": LoaderArgs" : "";
1212
+ const postType = typescript ? `
1213
+ interface PostSummary {
1214
+ slug: string;
1215
+ title: string;
1216
+ excerpt: string;
1217
+ publishedAt: string;
1218
+ }` : "";
1219
+ const metaType = typescript ? ": MetaArgs<PostSummary[]>" : "";
1220
+ const propsType = typescript ? ": { loaderData: PostSummary[] }" : "";
1221
+ const tailwindStyles = template === "tailwind";
1222
+ const mainClasses = tailwindStyles ? ' className="max-w-3xl mx-auto p-8"' : "";
1223
+ const h1Classes = tailwindStyles ? ' className="text-4xl font-bold mb-8"' : "";
1224
+ const listClasses = tailwindStyles ? ' className="space-y-6"' : "";
1225
+ const cardClasses = tailwindStyles ? ' className="border border-gray-200 dark:border-gray-700 rounded-lg p-6 hover:shadow-lg transition-shadow"' : "";
1226
+ const titleClasses = tailwindStyles ? ' className="text-xl font-semibold text-blue-600 dark:text-blue-400 hover:underline"' : "";
1227
+ const dateClasses = tailwindStyles ? ' className="text-sm text-gray-500 dark:text-gray-400 mt-1"' : "";
1228
+ const excerptClasses = tailwindStyles ? ' className="text-gray-600 dark:text-gray-300 mt-2"' : "";
1229
+ return `${imports}
1230
+ ${postType}
1231
+
1232
+ /**
1233
+ * Meta - Blog listing page
1234
+ */
1235
+ export function meta() {
1236
+ return [
1237
+ { title: 'Blog - EreoJS App' },
1238
+ { name: 'description', content: 'Read our latest blog posts' },
1239
+ ];
1240
+ }
1241
+
1242
+ /**
1243
+ * Loader - Fetch all blog posts
1244
+ */
1245
+ export async function loader({ context }${loaderType})${typescript ? ": Promise<PostSummary[]>" : ""} {
1246
+ // Set cache for the listing page
1247
+ context.cache.set({
1248
+ maxAge: 300,
1249
+ tags: ['blog', 'blog-list'],
1250
+ });
1251
+
1252
+ // In a real app, fetch from database
1253
+ // Example: const posts = await db.posts.findMany({ orderBy: { publishedAt: 'desc' } });
1254
+
1255
+ return [
1256
+ {
1257
+ slug: 'getting-started',
1258
+ title: 'Getting Started with EreoJS',
1259
+ excerpt: 'Learn how to build blazing fast applications with EreoJS and Bun.',
1260
+ publishedAt: '2024-01-20',
1261
+ },
1262
+ {
1263
+ slug: 'hello-world',
1264
+ title: 'Hello World',
1265
+ excerpt: 'This is the first blog post using EreoJS framework.',
1266
+ publishedAt: '2024-01-15',
1267
+ },
1268
+ ];
1269
+ }
1270
+
1271
+ /**
1272
+ * Blog Index Page
1273
+ */
1274
+ export default function BlogIndex({ loaderData: posts }${propsType}) {
1275
+ return (
1276
+ <main${mainClasses}>
1277
+ <h1${h1Classes}>Blog</h1>
1278
+
1279
+ <ul${listClasses}>
1280
+ {posts.map((post) => (
1281
+ <li key={post.slug}${cardClasses}>
1282
+ <Link to={\`/blog/\${post.slug}\`}${titleClasses}>
1283
+ {post.title}
1284
+ </Link>
1285
+ <p${dateClasses}>
1286
+ {new Date(post.publishedAt).toLocaleDateString()}
1287
+ </p>
1288
+ <p${excerptClasses}>{post.excerpt}</p>
1289
+ </li>
1290
+ ))}
1291
+ </ul>
1292
+ </main>
1293
+ );
1294
+ }
1295
+ `.trim();
1296
+ }
1297
+ function generateApiRoute(typescript) {
1298
+ const typeAnnotations = typescript ? `
1299
+ interface HealthResponse {
1300
+ status: 'ok' | 'error';
1301
+ timestamp: string;
1302
+ version: string;
1303
+ uptime: number;
1304
+ }
1305
+ ` : "";
1306
+ const loaderType = typescript ? ": LoaderArgs" : "";
1307
+ const returnType = typescript ? ": Promise<Response>" : "";
1308
+ return `/**
1309
+ * API Route Example - /api/health
1310
+ *
1311
+ * API routes return Response objects directly.
1312
+ * They don't render React components.
1313
+ *
1314
+ * Common patterns:
1315
+ * - GET /api/health -> Health check
1316
+ * - GET /api/users -> List users
1317
+ * - POST /api/users -> Create user
1318
+ * - GET /api/users/[id] -> Get user by ID
1319
+ */
1320
+ import type { LoaderArgs, ActionArgs } from '@ereo/core';
1321
+ import { json } from '@ereo/data';
1322
+ ${typeAnnotations}
1323
+ const startTime = Date.now();
1324
+
1325
+ /**
1326
+ * GET /api/health
1327
+ *
1328
+ * Health check endpoint for monitoring and load balancers.
1329
+ */
1330
+ export async function loader({ request, context }${loaderType})${returnType} {
1331
+ const health${typescript ? ": HealthResponse" : ""} = {
1332
+ status: 'ok',
1333
+ timestamp: new Date().toISOString(),
1334
+ version: '0.1.0',
1335
+ uptime: Math.floor((Date.now() - startTime) / 1000),
1336
+ };
1337
+
1338
+ return json(health, {
1339
+ headers: {
1340
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
1341
+ },
1342
+ });
1343
+ }
1344
+
1345
+ /**
1346
+ * POST /api/health
1347
+ *
1348
+ * Example of handling POST requests in API routes.
1349
+ * Use this pattern for webhooks, form submissions, etc.
1350
+ */
1351
+ export async function action({ request, context }${typescript ? ": ActionArgs" : ""})${returnType} {
1352
+ // Only allow POST
1353
+ if (request.method !== 'POST') {
1354
+ return json(
1355
+ { error: 'Method not allowed' },
1356
+ { status: 405 }
1357
+ );
1358
+ }
1359
+
1360
+ // Parse request body
1361
+ const body = await request.json().catch(() => ({}));
1362
+
1363
+ // Example: Log the health check ping
1364
+ console.log('Health check ping received:', body);
1365
+
1366
+ return json({ received: true, timestamp: new Date().toISOString() });
1367
+ }
1368
+ `.trim();
1369
+ }
1370
+
1371
+ // src/commands/deploy.ts
1372
+ import { join as join5 } from "path";
1373
+ async function deploy(options = {}) {
1374
+ const root = process.cwd();
1375
+ const target = options.target || await detectTarget(root);
1376
+ console.log(`
1377
+ \x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m Deploy
1378
+ `);
1379
+ console.log(` Target: \x1B[33m${target}\x1B[0m
1380
+ `);
1381
+ let config = {};
1382
+ const configPath = join5(root, "ereo.config.ts");
1383
+ try {
1384
+ if (await Bun.file(configPath).exists()) {
1385
+ const configModule = await import(configPath);
1386
+ config = configModule.default || configModule;
1387
+ }
1388
+ } catch (error) {
1389
+ console.warn("Could not load config:", error);
1390
+ }
1391
+ if (options.build !== false) {
1392
+ console.log(" \x1B[2mBuilding for production...\x1B[0m");
1393
+ const { build: build2 } = await Promise.resolve().then(() => (init_build(), exports_build));
1394
+ await build2({ production: true });
1395
+ console.log(` \x1B[32m\u2713\x1B[0m Build complete
1396
+ `);
1397
+ }
1398
+ if (options.dryRun) {
1399
+ console.log(` \x1B[33m\u26A0\x1B[0m Dry run - skipping actual deployment
1400
+ `);
1401
+ return generateDeployPreview(target, root, config);
1402
+ }
1403
+ try {
1404
+ switch (target) {
1405
+ case "vercel":
1406
+ return await deployToVercel(root, options);
1407
+ case "cloudflare":
1408
+ return await deployToCloudflare(root, options);
1409
+ case "fly":
1410
+ return await deployToFly(root, options);
1411
+ case "netlify":
1412
+ return await deployToNetlify(root, options);
1413
+ case "docker":
1414
+ return await deployToDocker(root, options);
1415
+ default:
1416
+ return {
1417
+ success: false,
1418
+ error: `Unknown deployment target: ${target}`
1419
+ };
1420
+ }
1421
+ } catch (error) {
1422
+ return {
1423
+ success: false,
1424
+ error: error instanceof Error ? error.message : String(error)
1425
+ };
1426
+ }
1427
+ }
1428
+ async function detectTarget(root) {
1429
+ if (await Bun.file(join5(root, "vercel.json")).exists()) {
1430
+ return "vercel";
1431
+ }
1432
+ if (await Bun.file(join5(root, "wrangler.toml")).exists()) {
1433
+ return "cloudflare";
1434
+ }
1435
+ if (await Bun.file(join5(root, "fly.toml")).exists()) {
1436
+ return "fly";
1437
+ }
1438
+ if (await Bun.file(join5(root, "netlify.toml")).exists()) {
1439
+ return "netlify";
1440
+ }
1441
+ if (await Bun.file(join5(root, "Dockerfile")).exists()) {
1442
+ return "docker";
1443
+ }
1444
+ return "vercel";
1445
+ }
1446
+ function generateDeployPreview(target, root, config) {
1447
+ const logs = [];
1448
+ logs.push(`Would deploy to ${target}`);
1449
+ logs.push(`Project root: ${root}`);
1450
+ logs.push(`Build target: ${config.build?.target || "bun"}`);
1451
+ logs.push(`Output directory: ${config.build?.outDir || "dist"}`);
1452
+ console.log(" Preview:");
1453
+ for (const log of logs) {
1454
+ console.log(` ${log}`);
1455
+ }
1456
+ console.log("");
1457
+ return {
1458
+ success: true,
1459
+ logs
1460
+ };
1461
+ }
1462
+ async function deployToVercel(root, options) {
1463
+ console.log(` Deploying to Vercel...
1464
+ `);
1465
+ const hasVercelCLI = await checkCommand("vercel");
1466
+ if (!hasVercelCLI) {
1467
+ console.log(` \x1B[33m\u26A0\x1B[0m Vercel CLI not found. Installing...
1468
+ `);
1469
+ await runCommand("bun", ["add", "-g", "vercel"]);
1470
+ }
1471
+ const vercelConfigPath = join5(root, "vercel.json");
1472
+ if (!await Bun.file(vercelConfigPath).exists()) {
1473
+ const vercelConfig = {
1474
+ buildCommand: "bun run build",
1475
+ outputDirectory: "dist",
1476
+ framework: null,
1477
+ functions: {
1478
+ "api/**/*.ts": {
1479
+ runtime: "@vercel/bun@0.1.0"
1480
+ }
1481
+ }
1482
+ };
1483
+ await Bun.write(vercelConfigPath, JSON.stringify(vercelConfig, null, 2));
1484
+ console.log(` \x1B[32m\u2713\x1B[0m Generated vercel.json
1485
+ `);
1486
+ }
1487
+ const args = ["deploy"];
1488
+ if (options.production) {
1489
+ args.push("--prod");
1490
+ }
1491
+ if (options.name) {
1492
+ args.push("--name", options.name);
1493
+ }
1494
+ const result = await runCommandWithOutput("vercel", args, root);
1495
+ if (result.success) {
1496
+ const urlMatch = result.output.match(/https:\/\/[^\s]+\.vercel\.app/);
1497
+ const url = urlMatch ? urlMatch[0] : undefined;
1498
+ console.log(`
1499
+ \x1B[32m\u2713\x1B[0m Deployed successfully!`);
1500
+ if (url) {
1501
+ console.log(` \x1B[36m\u279C\x1B[0m ${url}
1502
+ `);
1503
+ }
1504
+ return {
1505
+ success: true,
1506
+ url,
1507
+ logs: [result.output]
1508
+ };
1509
+ }
1510
+ return {
1511
+ success: false,
1512
+ error: result.error || "Deployment failed",
1513
+ logs: [result.output]
1514
+ };
1515
+ }
1516
+ async function deployToCloudflare(root, options) {
1517
+ console.log(` Deploying to Cloudflare...
1518
+ `);
1519
+ const hasWrangler = await checkCommand("wrangler");
1520
+ if (!hasWrangler) {
1521
+ console.log(` \x1B[33m\u26A0\x1B[0m Wrangler CLI not found. Installing...
1522
+ `);
1523
+ await runCommand("bun", ["add", "-g", "wrangler"]);
1524
+ }
1525
+ const wranglerConfigPath = join5(root, "wrangler.toml");
1526
+ if (!await Bun.file(wranglerConfigPath).exists()) {
1527
+ const projectName = options.name || "ereo-app";
1528
+ const wranglerConfig = `name = "${projectName}"
1529
+ main = "dist/server/index.js"
1530
+ compatibility_date = "2024-01-01"
1531
+
1532
+ [site]
1533
+ bucket = "./dist/client"
1534
+
1535
+ [build]
1536
+ command = "bun run build"
1537
+ `;
1538
+ await Bun.write(wranglerConfigPath, wranglerConfig);
1539
+ console.log(` \x1B[32m\u2713\x1B[0m Generated wrangler.toml
1540
+ `);
1541
+ }
1542
+ const result = await runCommandWithOutput("wrangler", ["deploy"], root);
1543
+ if (result.success) {
1544
+ const urlMatch = result.output.match(/https:\/\/[^\s]+\.workers\.dev/);
1545
+ const url = urlMatch ? urlMatch[0] : undefined;
1546
+ console.log(`
1547
+ \x1B[32m\u2713\x1B[0m Deployed successfully!`);
1548
+ if (url) {
1549
+ console.log(` \x1B[36m\u279C\x1B[0m ${url}
1550
+ `);
1551
+ }
1552
+ return {
1553
+ success: true,
1554
+ url,
1555
+ logs: [result.output]
1556
+ };
1557
+ }
1558
+ return {
1559
+ success: false,
1560
+ error: result.error || "Deployment failed",
1561
+ logs: [result.output]
1562
+ };
1563
+ }
1564
+ async function deployToFly(root, options) {
1565
+ console.log(` Deploying to Fly.io...
1566
+ `);
1567
+ const hasFly = await checkCommand("flyctl");
1568
+ if (!hasFly) {
1569
+ console.log(" \x1B[33m\u26A0\x1B[0m Fly CLI not found.");
1570
+ console.log(` Install from: https://fly.io/docs/hands-on/install-flyctl/
1571
+ `);
1572
+ return {
1573
+ success: false,
1574
+ error: "Fly CLI not installed"
1575
+ };
1576
+ }
1577
+ const flyConfigPath = join5(root, "fly.toml");
1578
+ if (!await Bun.file(flyConfigPath).exists()) {
1579
+ const projectName = options.name || "ereo-app";
1580
+ const flyConfig = `app = "${projectName}"
1581
+ primary_region = "iad"
1582
+
1583
+ [build]
1584
+ builder = "oven/bun"
1585
+
1586
+ [http_service]
1587
+ internal_port = 3000
1588
+ force_https = true
1589
+ auto_stop_machines = true
1590
+ auto_start_machines = true
1591
+ min_machines_running = 0
1592
+ `;
1593
+ await Bun.write(flyConfigPath, flyConfig);
1594
+ console.log(` \x1B[32m\u2713\x1B[0m Generated fly.toml
1595
+ `);
1596
+ }
1597
+ const result = await runCommandWithOutput("flyctl", ["deploy"], root);
1598
+ if (result.success) {
1599
+ const urlMatch = result.output.match(/https:\/\/[^\s]+\.fly\.dev/);
1600
+ const url = urlMatch ? urlMatch[0] : undefined;
1601
+ console.log(`
1602
+ \x1B[32m\u2713\x1B[0m Deployed successfully!`);
1603
+ if (url) {
1604
+ console.log(` \x1B[36m\u279C\x1B[0m ${url}
1605
+ `);
1606
+ }
1607
+ return {
1608
+ success: true,
1609
+ url,
1610
+ logs: [result.output]
1611
+ };
1612
+ }
1613
+ return {
1614
+ success: false,
1615
+ error: result.error || "Deployment failed",
1616
+ logs: [result.output]
1617
+ };
1618
+ }
1619
+ async function deployToNetlify(root, options) {
1620
+ console.log(` Deploying to Netlify...
1621
+ `);
1622
+ const hasNetlify = await checkCommand("netlify");
1623
+ if (!hasNetlify) {
1624
+ console.log(` \x1B[33m\u26A0\x1B[0m Netlify CLI not found. Installing...
1625
+ `);
1626
+ await runCommand("bun", ["add", "-g", "netlify-cli"]);
1627
+ }
1628
+ const netlifyConfigPath = join5(root, "netlify.toml");
1629
+ if (!await Bun.file(netlifyConfigPath).exists()) {
1630
+ const netlifyConfig = `[build]
1631
+ command = "bun run build"
1632
+ publish = "dist/client"
1633
+ functions = "dist/server"
1634
+
1635
+ [functions]
1636
+ node_bundler = "esbuild"
1637
+
1638
+ [[redirects]]
1639
+ from = "/api/*"
1640
+ to = "/.netlify/functions/api/:splat"
1641
+ status = 200
1642
+
1643
+ [[redirects]]
1644
+ from = "/*"
1645
+ to = "/index.html"
1646
+ status = 200
1647
+ `;
1648
+ await Bun.write(netlifyConfigPath, netlifyConfig);
1649
+ console.log(` \x1B[32m\u2713\x1B[0m Generated netlify.toml
1650
+ `);
1651
+ }
1652
+ const args = ["deploy", "--dir=dist/client"];
1653
+ if (options.production) {
1654
+ args.push("--prod");
1655
+ }
1656
+ const result = await runCommandWithOutput("netlify", args, root);
1657
+ if (result.success) {
1658
+ const urlMatch = result.output.match(/https:\/\/[^\s]+\.netlify\.app/);
1659
+ const url = urlMatch ? urlMatch[0] : undefined;
1660
+ console.log(`
1661
+ \x1B[32m\u2713\x1B[0m Deployed successfully!`);
1662
+ if (url) {
1663
+ console.log(` \x1B[36m\u279C\x1B[0m ${url}
1664
+ `);
1665
+ }
1666
+ return {
1667
+ success: true,
1668
+ url,
1669
+ logs: [result.output]
1670
+ };
1671
+ }
1672
+ return {
1673
+ success: false,
1674
+ error: result.error || "Deployment failed",
1675
+ logs: [result.output]
1676
+ };
1677
+ }
1678
+ async function deployToDocker(root, options) {
1679
+ console.log(` Building Docker image...
1680
+ `);
1681
+ const hasDocker = await checkCommand("docker");
1682
+ if (!hasDocker) {
1683
+ return {
1684
+ success: false,
1685
+ error: "Docker not installed"
1686
+ };
1687
+ }
1688
+ const dockerfilePath = join5(root, "Dockerfile");
1689
+ if (!await Bun.file(dockerfilePath).exists()) {
1690
+ const dockerfile = `# Build stage
1691
+ FROM oven/bun:1 as builder
1692
+ WORKDIR /app
1693
+ COPY package.json bun.lockb* ./
1694
+ RUN bun install --frozen-lockfile
1695
+ COPY . .
1696
+ RUN bun run build
1697
+
1698
+ # Production stage
1699
+ FROM oven/bun:1-slim
1700
+ WORKDIR /app
1701
+ COPY --from=builder /app/dist ./dist
1702
+ COPY --from=builder /app/package.json ./
1703
+ COPY --from=builder /app/node_modules ./node_modules
1704
+ ENV NODE_ENV=production
1705
+ EXPOSE 3000
1706
+ CMD ["bun", "run", "start"]
1707
+ `;
1708
+ await Bun.write(dockerfilePath, dockerfile);
1709
+ console.log(` \x1B[32m\u2713\x1B[0m Generated Dockerfile
1710
+ `);
1711
+ }
1712
+ const imageName = options.name || "ereo-app";
1713
+ const tag = options.production ? "latest" : "dev";
1714
+ const result = await runCommandWithOutput("docker", ["build", "-t", `${imageName}:${tag}`, "."], root);
1715
+ if (result.success) {
1716
+ console.log(`
1717
+ \x1B[32m\u2713\x1B[0m Docker image built: ${imageName}:${tag}`);
1718
+ console.log(` Run with: docker run -p 3000:3000 ${imageName}:${tag}
1719
+ `);
1720
+ return {
1721
+ success: true,
1722
+ deploymentId: `${imageName}:${tag}`,
1723
+ logs: [result.output]
1724
+ };
1725
+ }
1726
+ return {
1727
+ success: false,
1728
+ error: result.error || "Docker build failed",
1729
+ logs: [result.output]
1730
+ };
1731
+ }
1732
+ async function checkCommand(command) {
1733
+ try {
1734
+ const proc = Bun.spawn(["which", command], {
1735
+ stdout: "pipe",
1736
+ stderr: "pipe"
1737
+ });
1738
+ await proc.exited;
1739
+ return proc.exitCode === 0;
1740
+ } catch {
1741
+ return false;
1742
+ }
1743
+ }
1744
+ async function runCommand(command, args) {
1745
+ const proc = Bun.spawn([command, ...args], {
1746
+ stdout: "inherit",
1747
+ stderr: "inherit"
1748
+ });
1749
+ await proc.exited;
1750
+ return proc.exitCode === 0;
1751
+ }
1752
+ async function runCommandWithOutput(command, args, cwd) {
1753
+ try {
1754
+ const proc = Bun.spawn([command, ...args], {
1755
+ cwd,
1756
+ stdout: "pipe",
1757
+ stderr: "pipe"
1758
+ });
1759
+ const stdout = await new Response(proc.stdout).text();
1760
+ const stderr = await new Response(proc.stderr).text();
1761
+ await proc.exited;
1762
+ return {
1763
+ success: proc.exitCode === 0,
1764
+ output: stdout,
1765
+ error: stderr || undefined
1766
+ };
1767
+ } catch (error) {
1768
+ return {
1769
+ success: false,
1770
+ output: "",
1771
+ error: error instanceof Error ? error.message : String(error)
1772
+ };
1773
+ }
1774
+ }
1775
+ function printDeployHelp() {
1776
+ console.log(`
1777
+ Usage: ereo deploy [target] [options]
1778
+
1779
+ Targets:
1780
+ vercel Deploy to Vercel (default)
1781
+ cloudflare Deploy to Cloudflare Pages/Workers
1782
+ fly Deploy to Fly.io
1783
+ netlify Deploy to Netlify
1784
+ docker Build Docker image
1785
+
1786
+ Options:
1787
+ --prod Deploy to production
1788
+ --dry-run Preview deployment without actually deploying
1789
+ --name Project name for new deployments
1790
+ --no-build Skip build step
1791
+
1792
+ Examples:
1793
+ ereo deploy Deploy to auto-detected platform
1794
+ ereo deploy vercel --prod Deploy to Vercel production
1795
+ ereo deploy cloudflare Deploy to Cloudflare
1796
+ ereo deploy docker --name app Build Docker image named 'app'
1797
+ `);
1798
+ }
1799
+
1800
+ // src/commands/db.ts
1801
+ import { spawn } from "child_process";
1802
+ import { existsSync } from "fs";
1803
+ import { resolve, join as join6 } from "path";
1804
+ function findDrizzleConfig(customPath) {
1805
+ const cwd = process.cwd();
1806
+ if (customPath) {
1807
+ const absolutePath = resolve(cwd, customPath);
1808
+ if (existsSync(absolutePath)) {
1809
+ return absolutePath;
1810
+ }
1811
+ throw new Error(`Config file not found: ${customPath}`);
1812
+ }
1813
+ const configNames = [
1814
+ "drizzle.config.ts",
1815
+ "drizzle.config.js",
1816
+ "drizzle.config.mjs",
1817
+ "drizzle.config.json"
1818
+ ];
1819
+ for (const name of configNames) {
1820
+ const configPath = join6(cwd, name);
1821
+ if (existsSync(configPath)) {
1822
+ return configPath;
1823
+ }
1824
+ }
1825
+ throw new Error("Drizzle config not found. Create a drizzle.config.ts file or specify --config path.");
1826
+ }
1827
+ async function runDrizzleKit(command, args = []) {
1828
+ return new Promise((resolve2) => {
1829
+ console.log(`
1830
+ \x1B[36m\u25B6\x1B[0m Running: drizzle-kit ${command} ${args.join(" ")}
1831
+ `);
1832
+ const runner = existsSync(join6(process.cwd(), "bun.lockb")) ? "bunx" : "npx";
1833
+ const proc = spawn(runner, ["drizzle-kit", command, ...args], {
1834
+ cwd: process.cwd(),
1835
+ stdio: "inherit",
1836
+ shell: true
1837
+ });
1838
+ proc.on("close", (code) => {
1839
+ if (code === 0) {
1840
+ resolve2({ success: true });
1841
+ } else {
1842
+ resolve2({ success: false, error: `drizzle-kit ${command} failed with exit code ${code}` });
1843
+ }
1844
+ });
1845
+ proc.on("error", (error) => {
1846
+ resolve2({ success: false, error: error.message });
1847
+ });
1848
+ });
1849
+ }
1850
+ async function dbMigrate(options = {}) {
1851
+ console.log(`
1852
+ \x1B[36m\u2B21\x1B[0m \x1B[1mRunning database migrations...\x1B[0m`);
1853
+ try {
1854
+ const configPath = findDrizzleConfig(options.config);
1855
+ const args = ["--config", configPath];
1856
+ if (options.verbose) {
1857
+ args.push("--verbose");
1858
+ }
1859
+ const result = await runDrizzleKit("migrate", args);
1860
+ if (result.success) {
1861
+ console.log(`
1862
+ \x1B[32m\u2713\x1B[0m Migrations completed successfully
1863
+ `);
1864
+ } else {
1865
+ console.error(`
1866
+ \x1B[31m\u2717\x1B[0m ${result.error}
1867
+ `);
1868
+ process.exit(1);
1869
+ }
1870
+ } catch (error) {
1871
+ console.error(`
1872
+ \x1B[31m\u2717\x1B[0m ${error instanceof Error ? error.message : error}
1873
+ `);
1874
+ process.exit(1);
1875
+ }
1876
+ }
1877
+ async function dbGenerate(options) {
1878
+ console.log(`
1879
+ \x1B[36m\u2B21\x1B[0m \x1B[1mGenerating migration...\x1B[0m`);
1880
+ if (!options.name) {
1881
+ console.error(`
1882
+ \x1B[31m\u2717\x1B[0m Migration name is required (--name <name>)
1883
+ `);
1884
+ process.exit(1);
1885
+ }
1886
+ try {
1887
+ const configPath = findDrizzleConfig(options.config);
1888
+ const args = ["--config", configPath, "--name", options.name];
1889
+ if (options.out) {
1890
+ args.push("--out", options.out);
1891
+ }
1892
+ const result = await runDrizzleKit("generate", args);
1893
+ if (result.success) {
1894
+ console.log(`
1895
+ \x1B[32m\u2713\x1B[0m Migration "${options.name}" generated successfully
1896
+ `);
1897
+ } else {
1898
+ console.error(`
1899
+ \x1B[31m\u2717\x1B[0m ${result.error}
1900
+ `);
1901
+ process.exit(1);
1902
+ }
1903
+ } catch (error) {
1904
+ console.error(`
1905
+ \x1B[31m\u2717\x1B[0m ${error instanceof Error ? error.message : error}
1906
+ `);
1907
+ process.exit(1);
1908
+ }
1909
+ }
1910
+ async function dbStudio(options = {}) {
1911
+ console.log(`
1912
+ \x1B[36m\u2B21\x1B[0m \x1B[1mStarting Drizzle Studio...\x1B[0m`);
1913
+ try {
1914
+ const configPath = findDrizzleConfig(options.config);
1915
+ const args = ["--config", configPath];
1916
+ if (options.port) {
1917
+ args.push("--port", options.port.toString());
1918
+ }
1919
+ const result = await runDrizzleKit("studio", args);
1920
+ if (!result.success) {
1921
+ console.error(`
1922
+ \x1B[31m\u2717\x1B[0m ${result.error}
1923
+ `);
1924
+ process.exit(1);
1925
+ }
1926
+ } catch (error) {
1927
+ console.error(`
1928
+ \x1B[31m\u2717\x1B[0m ${error instanceof Error ? error.message : error}
1929
+ `);
1930
+ process.exit(1);
1931
+ }
1932
+ }
1933
+ async function dbPush(options = {}) {
1934
+ console.log(`
1935
+ \x1B[36m\u2B21\x1B[0m \x1B[1mPushing schema to database...\x1B[0m`);
1936
+ console.log(` \x1B[33m\u26A0\x1B[0m This should only be used in development
1937
+ `);
1938
+ try {
1939
+ const configPath = findDrizzleConfig(options.config);
1940
+ const args = ["--config", configPath];
1941
+ if (options.force) {
1942
+ args.push("--force");
1943
+ }
1944
+ if (options.verbose) {
1945
+ args.push("--verbose");
1946
+ }
1947
+ const result = await runDrizzleKit("push", args);
1948
+ if (result.success) {
1949
+ console.log(`
1950
+ \x1B[32m\u2713\x1B[0m Schema pushed successfully
1951
+ `);
1952
+ } else {
1953
+ console.error(`
1954
+ \x1B[31m\u2717\x1B[0m ${result.error}
1955
+ `);
1956
+ process.exit(1);
1957
+ }
1958
+ } catch (error) {
1959
+ console.error(`
1960
+ \x1B[31m\u2717\x1B[0m ${error instanceof Error ? error.message : error}
1961
+ `);
1962
+ process.exit(1);
1963
+ }
1964
+ }
1965
+ async function dbSeed(options = {}) {
1966
+ console.log(`
1967
+ \x1B[36m\u2B21\x1B[0m \x1B[1mRunning database seeders...\x1B[0m`);
1968
+ const cwd = process.cwd();
1969
+ const seedFile = options.file ?? findSeedFile(cwd);
1970
+ if (!seedFile) {
1971
+ console.error(`
1972
+ \x1B[31m\u2717\x1B[0m No seed file found.`);
1973
+ console.log(" Create a seed file at one of these locations:");
1974
+ console.log(" - db/seed.ts");
1975
+ console.log(" - src/db/seed.ts");
1976
+ console.log(" - seeds/index.ts");
1977
+ console.log(` Or specify a custom path with --file
1978
+ `);
1979
+ process.exit(1);
1980
+ }
1981
+ console.log(` Using seed file: ${seedFile}
1982
+ `);
1983
+ try {
1984
+ const runner = existsSync(join6(cwd, "bun.lockb")) ? "bun" : "npx";
1985
+ const runArgs = runner === "bun" ? ["run", seedFile] : ["tsx", seedFile];
1986
+ return new Promise((resolve2) => {
1987
+ const proc = spawn(runner, runArgs, {
1988
+ cwd,
1989
+ stdio: "inherit",
1990
+ shell: true,
1991
+ env: {
1992
+ ...process.env,
1993
+ DB_SEED_RESET: options.reset ? "1" : ""
1994
+ }
1995
+ });
1996
+ proc.on("close", (code) => {
1997
+ if (code === 0) {
1998
+ console.log(`
1999
+ \x1B[32m\u2713\x1B[0m Seeding completed successfully
2000
+ `);
2001
+ resolve2();
2002
+ } else {
2003
+ console.error(`
2004
+ \x1B[31m\u2717\x1B[0m Seeding failed with exit code ${code}
2005
+ `);
2006
+ process.exit(1);
2007
+ }
2008
+ });
2009
+ proc.on("error", (error) => {
2010
+ console.error(`
2011
+ \x1B[31m\u2717\x1B[0m ${error.message}
2012
+ `);
2013
+ process.exit(1);
2014
+ });
2015
+ });
2016
+ } catch (error) {
2017
+ console.error(`
2018
+ \x1B[31m\u2717\x1B[0m ${error instanceof Error ? error.message : error}
2019
+ `);
2020
+ process.exit(1);
2021
+ }
2022
+ }
2023
+ function findSeedFile(cwd) {
2024
+ const locations = [
2025
+ "db/seed.ts",
2026
+ "db/seed.js",
2027
+ "src/db/seed.ts",
2028
+ "src/db/seed.js",
2029
+ "seeds/index.ts",
2030
+ "seeds/index.js",
2031
+ "drizzle/seed.ts",
2032
+ "drizzle/seed.js"
2033
+ ];
2034
+ for (const loc of locations) {
2035
+ const fullPath = join6(cwd, loc);
2036
+ if (existsSync(fullPath)) {
2037
+ return fullPath;
2038
+ }
2039
+ }
2040
+ return null;
2041
+ }
2042
+ function printDbHelp() {
2043
+ console.log(`
2044
+ \x1B[36m\u2B21\x1B[0m \x1B[1mEreo Database Commands\x1B[0m
2045
+
2046
+ \x1B[1mUsage:\x1B[0m
2047
+ ereo db:<command> [options]
2048
+
2049
+ \x1B[1mCommands:\x1B[0m
2050
+ db:migrate Run pending database migrations
2051
+ db:generate --name <n> Generate migration from schema changes
2052
+ db:studio Open Drizzle Studio GUI
2053
+ db:push Push schema directly (dev only)
2054
+ db:seed Run database seeders
2055
+
2056
+ \x1B[1mMigrate Options:\x1B[0m
2057
+ --config <path> Path to drizzle config file
2058
+ --verbose Enable verbose output
2059
+
2060
+ \x1B[1mGenerate Options:\x1B[0m
2061
+ --name <name> Migration name (required)
2062
+ --config <path> Path to drizzle config file
2063
+ --out <dir> Output directory for migrations
2064
+
2065
+ \x1B[1mStudio Options:\x1B[0m
2066
+ --port <port> Port for Drizzle Studio
2067
+ --config <path> Path to drizzle config file
2068
+
2069
+ \x1B[1mPush Options:\x1B[0m
2070
+ --config <path> Path to drizzle config file
2071
+ --force Skip confirmation prompts
2072
+ --verbose Enable verbose output
2073
+
2074
+ \x1B[1mSeed Options:\x1B[0m
2075
+ --file <path> Path to seed file
2076
+ --reset Reset database before seeding
2077
+
2078
+ \x1B[1mExamples:\x1B[0m
2079
+ ereo db:generate --name add_users_table
2080
+ ereo db:migrate
2081
+ ereo db:studio --port 4000
2082
+ ereo db:push --force
2083
+ ereo db:seed --reset
2084
+ `);
2085
+ }
2086
+
2087
+ // src/index.ts
2088
+ var VERSION = "0.1.0";
2089
+ function printHelp() {
2090
+ console.log(`
2091
+ \x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m - React Fullstack Framework
2092
+
2093
+ \x1B[1mUsage:\x1B[0m
2094
+ ereo <command> [options]
2095
+
2096
+ \x1B[1mCommands:\x1B[0m
2097
+ dev Start development server
2098
+ build Build for production
2099
+ start Start production server
2100
+ create Create new project
2101
+ deploy Deploy to production
2102
+ db:* Database commands (db:migrate, db:generate, db:studio, db:push, db:seed)
2103
+
2104
+ \x1B[1mDev Options:\x1B[0m
2105
+ --port, -p Port number (default: 3000)
2106
+ --host, -h Host name (default: localhost)
2107
+ --open, -o Open browser
2108
+
2109
+ \x1B[1mBuild Options:\x1B[0m
2110
+ --outDir Output directory (default: .ereo)
2111
+ --minify Enable minification (default: true)
2112
+ --sourcemap Generate sourcemaps (default: true)
2113
+
2114
+ \x1B[1mStart Options:\x1B[0m
2115
+ --port, -p Port number (default: 3000)
2116
+ --host, -h Host name (default: 0.0.0.0)
2117
+
2118
+ \x1B[1mCreate Options:\x1B[0m
2119
+ --template, -t Template (minimal, default, tailwind)
2120
+ --typescript Use TypeScript (default: true)
2121
+
2122
+ \x1B[1mDeploy Options:\x1B[0m
2123
+ --prod Deploy to production
2124
+ --dry-run Preview deployment
2125
+ --name Project name
2126
+
2127
+ \x1B[1mDatabase Commands:\x1B[0m
2128
+ ereo db:migrate Run pending migrations
2129
+ ereo db:generate --name Generate migration from schema
2130
+ ereo db:studio Open Drizzle Studio
2131
+ ereo db:push Push schema (dev only)
2132
+ ereo db:seed Run database seeders
2133
+
2134
+ \x1B[1mExamples:\x1B[0m
2135
+ ereo dev --port 8080
2136
+ ereo build --minify
2137
+ ereo start --port 3001
2138
+ ereo create my-app --template tailwind
2139
+ ereo deploy vercel --prod
2140
+ ereo db:generate --name add_users
2141
+
2142
+ Version: ${VERSION}
2143
+ `);
2144
+ }
2145
+ function parseArgs(args) {
2146
+ const options = {};
2147
+ const positional = [];
2148
+ let command = "";
2149
+ for (let i = 0;i < args.length; i++) {
2150
+ const arg = args[i];
2151
+ if (!command && !arg.startsWith("-")) {
2152
+ command = arg;
2153
+ continue;
2154
+ }
2155
+ if (arg.startsWith("--")) {
2156
+ const [key, value] = arg.slice(2).split("=");
2157
+ if (value !== undefined) {
2158
+ options[key] = value;
2159
+ } else if (args[i + 1] && !args[i + 1].startsWith("-")) {
2160
+ options[key] = args[++i];
2161
+ } else {
2162
+ options[key] = true;
2163
+ }
2164
+ } else if (arg.startsWith("-")) {
2165
+ const key = arg.slice(1);
2166
+ if (args[i + 1] && !args[i + 1].startsWith("-")) {
2167
+ options[key] = args[++i];
2168
+ } else {
2169
+ options[key] = true;
2170
+ }
2171
+ } else {
2172
+ positional.push(arg);
2173
+ }
2174
+ }
2175
+ return { command, options, positional };
2176
+ }
2177
+ async function main() {
2178
+ const args = process.argv.slice(2);
2179
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
2180
+ printHelp();
2181
+ return;
2182
+ }
2183
+ if (args[0] === "--version" || args[0] === "-v") {
2184
+ console.log(VERSION);
2185
+ return;
2186
+ }
2187
+ const { command, options, positional } = parseArgs(args);
2188
+ try {
2189
+ switch (command) {
2190
+ case "dev": {
2191
+ const devOptions = {
2192
+ port: options.port ? parseInt(options.port) : undefined,
2193
+ host: options.host || options.h,
2194
+ open: !!(options.open || options.o)
2195
+ };
2196
+ await dev(devOptions);
2197
+ break;
2198
+ }
2199
+ case "build": {
2200
+ const buildOptions = {
2201
+ outDir: options.outDir,
2202
+ minify: options.minify === "false" ? false : true,
2203
+ sourcemap: options.sourcemap === "false" ? false : true
2204
+ };
2205
+ await build(buildOptions);
2206
+ break;
2207
+ }
2208
+ case "start": {
2209
+ const startOptions = {
2210
+ port: options.port ? parseInt(options.port) : undefined,
2211
+ host: options.host || options.h
2212
+ };
2213
+ await start(startOptions);
2214
+ break;
2215
+ }
2216
+ case "create": {
2217
+ const projectName = positional[0];
2218
+ if (!projectName) {
2219
+ console.error(`
2220
+ \x1B[31m\u2717\x1B[0m Please provide a project name
2221
+ `);
2222
+ console.log(` Usage: ereo create <project-name> [options]
2223
+ `);
2224
+ process.exit(1);
2225
+ }
2226
+ const createOptions = {
2227
+ template: options.template || options.t,
2228
+ typescript: options.typescript !== "false"
2229
+ };
2230
+ await create(projectName, createOptions);
2231
+ break;
2232
+ }
2233
+ case "deploy": {
2234
+ if (options.help || options.h) {
2235
+ printDeployHelp();
2236
+ break;
2237
+ }
2238
+ const target = positional[0];
2239
+ const deployOptions = {
2240
+ target,
2241
+ production: !!(options.prod || options.production),
2242
+ dryRun: !!(options["dry-run"] || options.dryRun),
2243
+ name: options.name,
2244
+ build: options["no-build"] ? false : true
2245
+ };
2246
+ const result = await deploy(deployOptions);
2247
+ if (!result.success) {
2248
+ console.error(`
2249
+ \x1B[31m\u2717\x1B[0m ${result.error}
2250
+ `);
2251
+ process.exit(1);
2252
+ }
2253
+ break;
2254
+ }
2255
+ case "db:migrate": {
2256
+ const migrateOptions = {
2257
+ config: options.config,
2258
+ verbose: !!(options.verbose || options.v)
2259
+ };
2260
+ await dbMigrate(migrateOptions);
2261
+ break;
2262
+ }
2263
+ case "db:generate": {
2264
+ const generateOptions = {
2265
+ name: options.name || positional[0],
2266
+ config: options.config,
2267
+ out: options.out
2268
+ };
2269
+ await dbGenerate(generateOptions);
2270
+ break;
2271
+ }
2272
+ case "db:studio": {
2273
+ const studioOptions = {
2274
+ port: options.port ? parseInt(options.port) : undefined,
2275
+ config: options.config,
2276
+ open: options.open !== false
2277
+ };
2278
+ await dbStudio(studioOptions);
2279
+ break;
2280
+ }
2281
+ case "db:push": {
2282
+ const pushOptions = {
2283
+ config: options.config,
2284
+ force: !!(options.force || options.f),
2285
+ verbose: !!(options.verbose || options.v)
2286
+ };
2287
+ await dbPush(pushOptions);
2288
+ break;
2289
+ }
2290
+ case "db:seed": {
2291
+ const seedOptions = {
2292
+ file: options.file,
2293
+ reset: !!(options.reset || options.r)
2294
+ };
2295
+ await dbSeed(seedOptions);
2296
+ break;
2297
+ }
2298
+ case "db":
2299
+ case "db:help": {
2300
+ printDbHelp();
2301
+ break;
2302
+ }
2303
+ default:
2304
+ if (command.startsWith("db:")) {
2305
+ console.error(`
2306
+ \x1B[31m\u2717\x1B[0m Unknown database command: ${command}
2307
+ `);
2308
+ printDbHelp();
2309
+ process.exit(1);
2310
+ }
2311
+ console.error(`
2312
+ \x1B[31m\u2717\x1B[0m Unknown command: ${command}
2313
+ `);
2314
+ printHelp();
2315
+ process.exit(1);
2316
+ }
2317
+ } catch (error) {
2318
+ console.error(`
2319
+ \x1B[31m\u2717\x1B[0m Error:`, error instanceof Error ? error.message : error);
2320
+ process.exit(1);
2321
+ }
2322
+ }
2323
+ main().catch(console.error);
2324
+ export {
2325
+ start,
2326
+ dev,
2327
+ deploy,
2328
+ dbStudio,
2329
+ dbSeed,
2330
+ dbPush,
2331
+ dbMigrate,
2332
+ dbGenerate,
2333
+ create,
2334
+ build
2335
+ };