create-ereo 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.
Files changed (3) hide show
  1. package/README.md +129 -0
  2. package/dist/index.js +1471 -0
  3. package/package.json +32 -0
package/dist/index.js ADDED
@@ -0,0 +1,1471 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/index.ts
5
+ import { join, resolve } from "path";
6
+ import { mkdir } from "fs/promises";
7
+ var defaultOptions = {
8
+ template: "tailwind",
9
+ typescript: true,
10
+ git: true,
11
+ install: true
12
+ };
13
+ function printBanner() {
14
+ console.log(`
15
+ \x1B[36m\u2B21\x1B[0m \x1B[1mCreate EreoJS App\x1B[0m
16
+
17
+ A React fullstack framework built on Bun.
18
+ `);
19
+ }
20
+ function printHelp() {
21
+ console.log(`
22
+ \x1B[1mUsage:\x1B[0m
23
+ bunx create-ereo <project-name> [options]
24
+
25
+ \x1B[1mOptions:\x1B[0m
26
+ -t, --template <name> Template to use (minimal, default, tailwind)
27
+ --no-typescript Use JavaScript instead of TypeScript
28
+ --no-git Skip git initialization
29
+ --no-install Skip package installation
30
+
31
+ \x1B[1mExamples:\x1B[0m
32
+ bunx create-ereo my-app
33
+ bunx create-ereo my-app --template minimal
34
+ bunx create-ereo my-app --no-typescript
35
+ `);
36
+ }
37
+ function parseArgs(args) {
38
+ const options = {};
39
+ let projectName = null;
40
+ for (let i = 0;i < args.length; i++) {
41
+ const arg = args[i];
42
+ if (arg === "-h" || arg === "--help") {
43
+ printHelp();
44
+ process.exit(0);
45
+ }
46
+ if (arg === "-t" || arg === "--template") {
47
+ options.template = args[++i];
48
+ } else if (arg === "--no-typescript") {
49
+ options.typescript = false;
50
+ } else if (arg === "--no-git") {
51
+ options.git = false;
52
+ } else if (arg === "--no-install") {
53
+ options.install = false;
54
+ } else if (!arg.startsWith("-") && !projectName) {
55
+ projectName = arg;
56
+ }
57
+ }
58
+ return { projectName, options };
59
+ }
60
+ async function generateMinimalProject(projectDir, projectName, typescript) {
61
+ const ext = typescript ? "tsx" : "jsx";
62
+ await mkdir(projectDir, { recursive: true });
63
+ await mkdir(join(projectDir, "app/routes"), { recursive: true });
64
+ await mkdir(join(projectDir, "public"), { recursive: true });
65
+ const packageJson = {
66
+ name: projectName,
67
+ version: "0.1.0",
68
+ type: "module",
69
+ scripts: {
70
+ dev: "ereo dev",
71
+ build: "ereo build",
72
+ start: "ereo start"
73
+ },
74
+ dependencies: {
75
+ "@ereo/core": "^0.1.0",
76
+ "@ereo/router": "^0.1.0",
77
+ "@ereo/server": "^0.1.0",
78
+ "@ereo/client": "^0.1.0",
79
+ "@ereo/data": "^0.1.0",
80
+ "@ereo/cli": "^0.1.0",
81
+ react: "^18.2.0",
82
+ "react-dom": "^18.2.0"
83
+ },
84
+ devDependencies: typescript ? {
85
+ "@types/bun": "^1.1.0",
86
+ "@types/react": "^18.2.0",
87
+ "@types/react-dom": "^18.2.0",
88
+ typescript: "^5.4.0"
89
+ } : {}
90
+ };
91
+ await Bun.write(join(projectDir, "package.json"), JSON.stringify(packageJson, null, 2));
92
+ const ereoConfig = `
93
+ import { defineConfig } from '@ereo/core';
94
+
95
+ export default defineConfig({
96
+ server: {
97
+ port: 3000,
98
+ },
99
+ });
100
+ `.trim();
101
+ await Bun.write(join(projectDir, `ereo.config.${typescript ? "ts" : "js"}`), ereoConfig);
102
+ const layout = `
103
+ export default function RootLayout({ children }${typescript ? ": { children: React.ReactNode }" : ""}) {
104
+ return (
105
+ <html lang="en">
106
+ <head>
107
+ <meta charSet="utf-8" />
108
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
109
+ <title>${projectName}</title>
110
+ </head>
111
+ <body>
112
+ {children}
113
+ </body>
114
+ </html>
115
+ );
116
+ }
117
+ `.trim();
118
+ await Bun.write(join(projectDir, `app/routes/_layout.${ext}`), layout);
119
+ const indexPage = `
120
+ export default function HomePage() {
121
+ return (
122
+ <main>
123
+ <h1>Welcome to EreoJS!</h1>
124
+ <p>Edit app/routes/index.${ext} to get started.</p>
125
+ </main>
126
+ );
127
+ }
128
+ `.trim();
129
+ await Bun.write(join(projectDir, `app/routes/index.${ext}`), indexPage);
130
+ if (typescript) {
131
+ const tsconfig = {
132
+ compilerOptions: {
133
+ target: "ESNext",
134
+ module: "ESNext",
135
+ moduleResolution: "bundler",
136
+ jsx: "react-jsx",
137
+ strict: true,
138
+ esModuleInterop: true,
139
+ skipLibCheck: true,
140
+ forceConsistentCasingInFileNames: true,
141
+ types: ["bun-types"]
142
+ },
143
+ include: ["app/**/*", "*.config.ts"]
144
+ };
145
+ await Bun.write(join(projectDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
146
+ }
147
+ await Bun.write(join(projectDir, ".gitignore"), `node_modules
148
+ .ereo
149
+ dist
150
+ *.log
151
+ .DS_Store
152
+ .env
153
+ .env.local`);
154
+ }
155
+ async function generateTailwindProject(projectDir, projectName, typescript) {
156
+ const ext = typescript ? "tsx" : "jsx";
157
+ const ts = typescript;
158
+ await mkdir(projectDir, { recursive: true });
159
+ await mkdir(join(projectDir, "app/routes/blog"), { recursive: true });
160
+ await mkdir(join(projectDir, "app/components"), { recursive: true });
161
+ await mkdir(join(projectDir, "app/lib"), { recursive: true });
162
+ await mkdir(join(projectDir, "public"), { recursive: true });
163
+ const packageJson = {
164
+ name: projectName,
165
+ version: "0.1.0",
166
+ type: "module",
167
+ scripts: {
168
+ dev: "ereo dev",
169
+ build: "ereo build",
170
+ start: "ereo start",
171
+ test: "bun test",
172
+ typecheck: "tsc --noEmit"
173
+ },
174
+ dependencies: {
175
+ "@ereo/core": "^0.1.0",
176
+ "@ereo/router": "^0.1.0",
177
+ "@ereo/server": "^0.1.0",
178
+ "@ereo/client": "^0.1.0",
179
+ "@ereo/data": "^0.1.0",
180
+ "@ereo/cli": "^0.1.0",
181
+ "@ereo/runtime-bun": "^0.1.0",
182
+ "@ereo/plugin-tailwind": "^0.1.0",
183
+ react: "^18.2.0",
184
+ "react-dom": "^18.2.0"
185
+ },
186
+ devDependencies: {
187
+ "@ereo/testing": "^0.1.0",
188
+ "@ereo/dev-inspector": "^0.1.0",
189
+ ...ts ? {
190
+ "@types/bun": "^1.1.0",
191
+ "@types/react": "^18.2.0",
192
+ "@types/react-dom": "^18.2.0",
193
+ typescript: "^5.4.0"
194
+ } : {},
195
+ tailwindcss: "^3.4.0"
196
+ }
197
+ };
198
+ await Bun.write(join(projectDir, "package.json"), JSON.stringify(packageJson, null, 2));
199
+ const ereoConfig = `
200
+ import { defineConfig, env } from '@ereo/core';
201
+ import tailwind from '@ereo/plugin-tailwind';
202
+
203
+ export default defineConfig({
204
+ server: {
205
+ port: 3000,
206
+ // Enable development features
207
+ development: process.env.NODE_ENV !== 'production',
208
+ },
209
+ build: {
210
+ target: 'bun',
211
+ },
212
+ // Environment variable validation
213
+ env: {
214
+ NODE_ENV: env.enum(['development', 'production', 'test'] as const).default('development'),
215
+ // Add your environment variables here:
216
+ // DATABASE_URL: env.string().required(),
217
+ // API_KEY: env.string(),
218
+ },
219
+ plugins: [
220
+ tailwind(),
221
+ ],
222
+ });
223
+ `.trim();
224
+ await Bun.write(join(projectDir, `ereo.config.${ts ? "ts" : "js"}`), ereoConfig);
225
+ if (ts) {
226
+ const tsconfig = {
227
+ compilerOptions: {
228
+ target: "ESNext",
229
+ module: "ESNext",
230
+ moduleResolution: "bundler",
231
+ jsx: "react-jsx",
232
+ strict: true,
233
+ esModuleInterop: true,
234
+ skipLibCheck: true,
235
+ forceConsistentCasingInFileNames: true,
236
+ types: ["bun-types"],
237
+ paths: {
238
+ "~/*": ["./app/*"]
239
+ }
240
+ },
241
+ include: ["app/**/*", "*.config.ts"]
242
+ };
243
+ await Bun.write(join(projectDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
244
+ }
245
+ const tailwindConfig = `
246
+ /** @type {import('tailwindcss').Config} */
247
+ export default {
248
+ content: ['./app/**/*.{js,ts,jsx,tsx}'],
249
+ darkMode: 'class',
250
+ theme: {
251
+ extend: {
252
+ colors: {
253
+ primary: {
254
+ 50: '#eff6ff',
255
+ 100: '#dbeafe',
256
+ 200: '#bfdbfe',
257
+ 300: '#93c5fd',
258
+ 400: '#60a5fa',
259
+ 500: '#3b82f6',
260
+ 600: '#2563eb',
261
+ 700: '#1d4ed8',
262
+ 800: '#1e40af',
263
+ 900: '#1e3a8a',
264
+ },
265
+ },
266
+ },
267
+ },
268
+ plugins: [],
269
+ };
270
+ `.trim();
271
+ await Bun.write(join(projectDir, "tailwind.config.js"), tailwindConfig);
272
+ const styles = `
273
+ @tailwind base;
274
+ @tailwind components;
275
+ @tailwind utilities;
276
+
277
+ @layer base {
278
+ body {
279
+ @apply antialiased;
280
+ }
281
+ }
282
+
283
+ @layer components {
284
+ .btn {
285
+ @apply px-4 py-2 rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2;
286
+ }
287
+ .btn-primary {
288
+ @apply bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500;
289
+ }
290
+ .btn-secondary {
291
+ @apply bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-600;
292
+ }
293
+ .input {
294
+ @apply w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent dark:bg-gray-800 dark:border-gray-600;
295
+ }
296
+ .card {
297
+ @apply bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6;
298
+ }
299
+ }
300
+ `.trim();
301
+ await Bun.write(join(projectDir, "app/styles.css"), styles);
302
+ if (ts) {
303
+ const types = `
304
+ /**
305
+ * Shared types for the application.
306
+ */
307
+
308
+ export interface Post {
309
+ slug: string;
310
+ title: string;
311
+ excerpt: string;
312
+ content: string;
313
+ author: string;
314
+ date: string;
315
+ readTime: string;
316
+ tags: string[];
317
+ }
318
+
319
+ export interface ContactFormData {
320
+ name: string;
321
+ email: string;
322
+ message: string;
323
+ }
324
+
325
+ export interface ActionResult<T = unknown> {
326
+ success: boolean;
327
+ data?: T;
328
+ error?: string;
329
+ }
330
+ `.trim();
331
+ await Bun.write(join(projectDir, "app/lib/types.ts"), types);
332
+ }
333
+ const mockData = `
334
+ ${ts ? `import type { Post } from './types';
335
+ ` : ""}
336
+ /**
337
+ * Mock blog posts data.
338
+ * In a real app, this would come from a database or CMS.
339
+ */
340
+ export const posts${ts ? ": Post[]" : ""} = [
341
+ {
342
+ slug: 'getting-started-with-ereo',
343
+ title: 'Getting Started with EreoJS',
344
+ excerpt: 'Learn how to build modern web applications with EreoJS, the React fullstack framework powered by Bun.',
345
+ content: \`
346
+ # Getting Started with EreoJS
347
+
348
+ EreoJS is a modern React fullstack framework that runs on Bun, offering exceptional performance and developer experience.
349
+
350
+ ## Key Features
351
+
352
+ - **Server-Side Rendering**: Fast initial page loads with SSR
353
+ - **File-Based Routing**: Intuitive routing with automatic code splitting
354
+ - **Data Loading**: Simple and powerful data fetching with loaders
355
+ - **Actions**: Handle form submissions and mutations easily
356
+ - **Islands Architecture**: Selective hydration for optimal performance
357
+
358
+ ## Quick Start
359
+
360
+ \\\`\\\`\\\`bash
361
+ bunx create-ereo my-app
362
+ cd my-app
363
+ bun run dev
364
+ \\\`\\\`\\\`
365
+
366
+ You're now ready to build amazing applications!
367
+ \`.trim(),
368
+ author: 'EreoJS Team',
369
+ date: '2024-01-15',
370
+ readTime: '5 min read',
371
+ tags: ['ereo', 'react', 'tutorial'],
372
+ },
373
+ {
374
+ slug: 'understanding-loaders-and-actions',
375
+ title: 'Understanding Loaders and Actions',
376
+ excerpt: 'Deep dive into EreoJS\\'s data loading and mutation patterns for building robust applications.',
377
+ content: \`
378
+ # Understanding Loaders and Actions
379
+
380
+ Loaders and actions are the core data primitives in EreoJS.
381
+
382
+ ## Loaders
383
+
384
+ Loaders run on the server before rendering and provide data to your components:
385
+
386
+ \\\`\\\`\\\`typescript
387
+ export async function loader({ params }) {
388
+ const user = await db.user.findUnique({
389
+ where: { id: params.id }
390
+ });
391
+ return { user };
392
+ }
393
+ \\\`\\\`\\\`
394
+
395
+ ## Actions
396
+
397
+ Actions handle form submissions and mutations:
398
+
399
+ \\\`\\\`\\\`typescript
400
+ export async function action({ request }) {
401
+ const formData = await request.formData();
402
+ await db.user.create({
403
+ data: Object.fromEntries(formData)
404
+ });
405
+ return { success: true };
406
+ }
407
+ \\\`\\\`\\\`
408
+ \`.trim(),
409
+ author: 'EreoJS Team',
410
+ date: '2024-01-20',
411
+ readTime: '8 min read',
412
+ tags: ['ereo', 'data', 'tutorial'],
413
+ },
414
+ {
415
+ slug: 'styling-with-tailwind',
416
+ title: 'Styling with Tailwind CSS',
417
+ excerpt: 'How to use Tailwind CSS effectively in your EreoJS applications for beautiful, responsive designs.',
418
+ content: \`
419
+ # Styling with Tailwind CSS
420
+
421
+ EreoJS comes with first-class Tailwind CSS support out of the box.
422
+
423
+ ## Setup
424
+
425
+ The Tailwind plugin is already configured when you create a new project:
426
+
427
+ \\\`\\\`\\\`typescript
428
+ import tailwind from '@ereo/plugin-tailwind';
429
+
430
+ export default defineConfig({
431
+ plugins: [tailwind()],
432
+ });
433
+ \\\`\\\`\\\`
434
+
435
+ ## Usage
436
+
437
+ Just use Tailwind classes in your components:
438
+
439
+ \\\`\\\`\\\`tsx
440
+ export default function Button({ children }) {
441
+ return (
442
+ <button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
443
+ {children}
444
+ </button>
445
+ );
446
+ }
447
+ \\\`\\\`\\\`
448
+ \`.trim(),
449
+ author: 'EreoJS Team',
450
+ date: '2024-01-25',
451
+ readTime: '4 min read',
452
+ tags: ['ereo', 'tailwind', 'css'],
453
+ },
454
+ ];
455
+
456
+ /**
457
+ * Get all posts.
458
+ */
459
+ export function getAllPosts()${ts ? ": Post[]" : ""} {
460
+ return posts;
461
+ }
462
+
463
+ /**
464
+ * Get a single post by slug.
465
+ */
466
+ export function getPostBySlug(slug${ts ? ": string" : ""})${ts ? ": Post | undefined" : ""} {
467
+ return posts.find((post) => post.slug === slug);
468
+ }
469
+
470
+ /**
471
+ * Simulate API delay for demo purposes.
472
+ */
473
+ export async function simulateDelay(ms${ts ? ": number" : ""} = 100)${ts ? ": Promise<void>" : ""} {
474
+ return new Promise((resolve) => setTimeout(resolve, ms));
475
+ }
476
+ `.trim();
477
+ await Bun.write(join(projectDir, `app/lib/data.${ts ? "ts" : "js"}`), mockData);
478
+ const navigation = `
479
+ 'use client';
480
+
481
+ import { useState } from 'react';
482
+
483
+ const navLinks = [
484
+ { href: '/', label: 'Home' },
485
+ { href: '/blog', label: 'Blog' },
486
+ { href: '/contact', label: 'Contact' },
487
+ { href: '/about', label: 'About' },
488
+ ];
489
+
490
+ export function Navigation() {
491
+ const [isOpen, setIsOpen] = useState(false);
492
+
493
+ return (
494
+ <nav className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
495
+ <div className="max-w-6xl mx-auto px-4">
496
+ <div className="flex items-center justify-between h-16">
497
+ {/* Logo */}
498
+ <a href="/" className="flex items-center space-x-2">
499
+ <span className="text-2xl">\u2B21</span>
500
+ <span className="font-bold text-xl">EreoJS</span>
501
+ </a>
502
+
503
+ {/* Desktop Navigation */}
504
+ <div className="hidden md:flex items-center space-x-8">
505
+ {navLinks.map((link) => (
506
+ <a
507
+ key={link.href}
508
+ href={link.href}
509
+ className="text-gray-600 dark:text-gray-300 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
510
+ >
511
+ {link.label}
512
+ </a>
513
+ ))}
514
+ </div>
515
+
516
+ {/* Mobile menu button */}
517
+ <button
518
+ onClick={() => setIsOpen(!isOpen)}
519
+ className="md:hidden p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"
520
+ aria-label="Toggle menu"
521
+ >
522
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
523
+ {isOpen ? (
524
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
525
+ ) : (
526
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
527
+ )}
528
+ </svg>
529
+ </button>
530
+ </div>
531
+
532
+ {/* Mobile Navigation */}
533
+ {isOpen && (
534
+ <div className="md:hidden py-4 border-t border-gray-200 dark:border-gray-800">
535
+ {navLinks.map((link) => (
536
+ <a
537
+ key={link.href}
538
+ href={link.href}
539
+ className="block py-2 text-gray-600 dark:text-gray-300 hover:text-primary-600"
540
+ onClick={() => setIsOpen(false)}
541
+ >
542
+ {link.label}
543
+ </a>
544
+ ))}
545
+ </div>
546
+ )}
547
+ </div>
548
+ </nav>
549
+ );
550
+ }
551
+ `.trim();
552
+ await Bun.write(join(projectDir, `app/components/Navigation.${ext}`), navigation);
553
+ const counter = `
554
+ 'use client';
555
+
556
+ import { useState } from 'react';
557
+
558
+ ${ts ? `interface CounterProps {
559
+ initialCount?: number;
560
+ }
561
+ ` : ""}
562
+ /**
563
+ * Interactive counter component.
564
+ * This demonstrates client-side interactivity with EreoJS's islands architecture.
565
+ * The 'use client' directive marks this component for hydration.
566
+ */
567
+ export function Counter({ initialCount = 0 }${ts ? ": CounterProps" : ""}) {
568
+ const [count, setCount] = useState(initialCount);
569
+
570
+ return (
571
+ <div className="flex items-center gap-4">
572
+ <button
573
+ onClick={() => setCount((c) => c - 1)}
574
+ className="btn btn-secondary w-10 h-10 flex items-center justify-center text-xl"
575
+ aria-label="Decrease count"
576
+ >
577
+ -
578
+ </button>
579
+ <span className="text-2xl font-bold w-12 text-center">{count}</span>
580
+ <button
581
+ onClick={() => setCount((c) => c + 1)}
582
+ className="btn btn-primary w-10 h-10 flex items-center justify-center text-xl"
583
+ aria-label="Increase count"
584
+ >
585
+ +
586
+ </button>
587
+ </div>
588
+ );
589
+ }
590
+ `.trim();
591
+ await Bun.write(join(projectDir, `app/components/Counter.${ext}`), counter);
592
+ const footer = `
593
+ export function Footer() {
594
+ const currentYear = new Date().getFullYear();
595
+
596
+ return (
597
+ <footer className="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800 mt-auto">
598
+ <div className="max-w-6xl mx-auto px-4 py-8">
599
+ <div className="flex flex-col md:flex-row items-center justify-between gap-4">
600
+ <div className="flex items-center space-x-2 text-gray-600 dark:text-gray-400">
601
+ <span className="text-xl">\u2B21</span>
602
+ <span>Built with EreoJS</span>
603
+ </div>
604
+ <div className="flex items-center gap-6 text-sm text-gray-500 dark:text-gray-500">
605
+ <a href="https://github.com/ereo-js/ereo" target="_blank" rel="noopener" className="hover:text-primary-600">
606
+ GitHub
607
+ </a>
608
+ <a href="https://ereo.dev/docs" target="_blank" rel="noopener" className="hover:text-primary-600">
609
+ Documentation
610
+ </a>
611
+ <span>&copy; {currentYear}</span>
612
+ </div>
613
+ </div>
614
+ </div>
615
+ </footer>
616
+ );
617
+ }
618
+ `.trim();
619
+ await Bun.write(join(projectDir, `app/components/Footer.${ext}`), footer);
620
+ const postCard = `
621
+ ${ts ? `import type { Post } from '~/lib/types';
622
+ ` : ""}
623
+ ${ts ? `interface PostCardProps {
624
+ post: Post;
625
+ }
626
+ ` : ""}
627
+ export function PostCard({ post }${ts ? ": PostCardProps" : ""}) {
628
+ return (
629
+ <article className="card hover:shadow-xl transition-shadow">
630
+ <div className="flex flex-wrap gap-2 mb-3">
631
+ {post.tags.map((tag) => (
632
+ <span
633
+ key={tag}
634
+ className="px-2 py-1 text-xs font-medium bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300 rounded"
635
+ >
636
+ {tag}
637
+ </span>
638
+ ))}
639
+ </div>
640
+ <h2 className="text-xl font-bold mb-2">
641
+ <a href={\`/blog/\${post.slug}\`} className="hover:text-primary-600 transition-colors">
642
+ {post.title}
643
+ </a>
644
+ </h2>
645
+ <p className="text-gray-600 dark:text-gray-400 mb-4">{post.excerpt}</p>
646
+ <div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-500">
647
+ <span>{post.author}</span>
648
+ <div className="flex items-center gap-3">
649
+ <span>{post.date}</span>
650
+ <span>{post.readTime}</span>
651
+ </div>
652
+ </div>
653
+ </article>
654
+ );
655
+ }
656
+ `.trim();
657
+ await Bun.write(join(projectDir, `app/components/PostCard.${ext}`), postCard);
658
+ const rootLayout = `
659
+ import { Navigation } from '~/components/Navigation';
660
+ import { Footer } from '~/components/Footer';
661
+
662
+ ${ts ? `interface RootLayoutProps {
663
+ children: React.ReactNode;
664
+ }
665
+ ` : ""}
666
+ export default function RootLayout({ children }${ts ? ": RootLayoutProps" : ""}) {
667
+ return (
668
+ <html lang="en">
669
+ <head>
670
+ <meta charSet="utf-8" />
671
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
672
+ <meta name="description" content="A modern web application built with EreoJS" />
673
+ <title>${projectName}</title>
674
+ <link rel="stylesheet" href="/app/styles.css" />
675
+ </head>
676
+ <body className="min-h-screen flex flex-col bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
677
+ <Navigation />
678
+ <main className="flex-1">
679
+ {children}
680
+ </main>
681
+ <Footer />
682
+ </body>
683
+ </html>
684
+ );
685
+ }
686
+ `.trim();
687
+ await Bun.write(join(projectDir, `app/routes/_layout.${ext}`), rootLayout);
688
+ const homePage = `
689
+ import { Counter } from '~/components/Counter';
690
+ import { getAllPosts, simulateDelay } from '~/lib/data';
691
+
692
+ /**
693
+ * Loader function - runs on the server before rendering.
694
+ * Fetches data and passes it to the component.
695
+ */
696
+ export async function loader() {
697
+ await simulateDelay(50);
698
+
699
+ const posts = getAllPosts();
700
+ const featuredPost = posts[0];
701
+
702
+ return {
703
+ featuredPost,
704
+ stats: {
705
+ posts: posts.length,
706
+ serverTime: new Date().toLocaleTimeString(),
707
+ },
708
+ };
709
+ }
710
+
711
+ ${ts ? `interface HomePageProps {
712
+ loaderData: {
713
+ featuredPost: {
714
+ slug: string;
715
+ title: string;
716
+ excerpt: string;
717
+ };
718
+ stats: {
719
+ posts: number;
720
+ serverTime: string;
721
+ };
722
+ };
723
+ }
724
+ ` : ""}
725
+ export default function HomePage({ loaderData }${ts ? ": HomePageProps" : ""}) {
726
+ const { featuredPost, stats } = loaderData;
727
+
728
+ return (
729
+ <div className="min-h-screen">
730
+ {/* Hero Section */}
731
+ <section className="py-20 px-4 bg-gradient-to-br from-primary-500 to-purple-600 text-white">
732
+ <div className="max-w-4xl mx-auto text-center">
733
+ <h1 className="text-5xl md:text-6xl font-bold mb-6">
734
+ Welcome to EreoJS
735
+ </h1>
736
+ <p className="text-xl md:text-2xl mb-8 text-primary-100">
737
+ A React fullstack framework built on Bun.
738
+ <br />
739
+ Fast, simple, and powerful.
740
+ </p>
741
+ <div className="flex flex-wrap gap-4 justify-center">
742
+ <a href="/blog" className="btn bg-white text-primary-600 hover:bg-primary-50">
743
+ Read the Blog
744
+ </a>
745
+ <a
746
+ href="https://github.com/ereo-js/ereo"
747
+ target="_blank"
748
+ rel="noopener"
749
+ className="btn border-2 border-white text-white hover:bg-white/10"
750
+ >
751
+ View on GitHub
752
+ </a>
753
+ </div>
754
+ </div>
755
+ </section>
756
+
757
+ {/* Features Section */}
758
+ <section className="py-16 px-4">
759
+ <div className="max-w-6xl mx-auto">
760
+ <h2 className="text-3xl font-bold text-center mb-12">Why EreoJS?</h2>
761
+ <div className="grid md:grid-cols-3 gap-8">
762
+ <div className="card text-center">
763
+ <div className="text-4xl mb-4">\u26A1</div>
764
+ <h3 className="text-xl font-bold mb-2">Blazing Fast</h3>
765
+ <p className="text-gray-600 dark:text-gray-400">
766
+ Built on Bun for exceptional performance. Server-side rendering with streaming support.
767
+ </p>
768
+ </div>
769
+ <div className="card text-center">
770
+ <div className="text-4xl mb-4">\uD83C\uDFAF</div>
771
+ <h3 className="text-xl font-bold mb-2">Simple Data Loading</h3>
772
+ <p className="text-gray-600 dark:text-gray-400">
773
+ One pattern for data fetching. Loaders and actions make it easy to build dynamic apps.
774
+ </p>
775
+ </div>
776
+ <div className="card text-center">
777
+ <div className="text-4xl mb-4">\uD83C\uDFDD\uFE0F</div>
778
+ <h3 className="text-xl font-bold mb-2">Islands Architecture</h3>
779
+ <p className="text-gray-600 dark:text-gray-400">
780
+ Selective hydration means smaller bundles and faster interactivity where it matters.
781
+ </p>
782
+ </div>
783
+ </div>
784
+ </div>
785
+ </section>
786
+
787
+ {/* Interactive Demo Section */}
788
+ <section className="py-16 px-4 bg-gray-50 dark:bg-gray-800">
789
+ <div className="max-w-4xl mx-auto text-center">
790
+ <h2 className="text-3xl font-bold mb-4">Interactive Islands</h2>
791
+ <p className="text-gray-600 dark:text-gray-400 mb-8">
792
+ This counter component is an "island" - only this part of the page is hydrated with JavaScript.
793
+ </p>
794
+ <div className="flex justify-center">
795
+ <Counter initialCount={0} />
796
+ </div>
797
+ </div>
798
+ </section>
799
+
800
+ {/* Server Data Section */}
801
+ <section className="py-16 px-4">
802
+ <div className="max-w-4xl mx-auto">
803
+ <div className="card">
804
+ <h2 className="text-2xl font-bold mb-6">Server-Side Data</h2>
805
+ <p className="text-gray-600 dark:text-gray-400 mb-6">
806
+ This data was loaded on the server using a loader function:
807
+ </p>
808
+ <div className="grid md:grid-cols-2 gap-6">
809
+ <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
810
+ <div className="text-sm text-gray-500 dark:text-gray-400 mb-1">Blog Posts</div>
811
+ <div className="text-3xl font-bold">{stats.posts}</div>
812
+ </div>
813
+ <div className="p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
814
+ <div className="text-sm text-gray-500 dark:text-gray-400 mb-1">Rendered At</div>
815
+ <div className="text-3xl font-bold">{stats.serverTime}</div>
816
+ </div>
817
+ </div>
818
+ {featuredPost && (
819
+ <div className="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
820
+ <div className="text-sm text-gray-500 dark:text-gray-400 mb-2">Featured Post</div>
821
+ <h3 className="text-xl font-bold mb-2">
822
+ <a href={\`/blog/\${featuredPost.slug}\`} className="hover:text-primary-600">
823
+ {featuredPost.title}
824
+ </a>
825
+ </h3>
826
+ <p className="text-gray-600 dark:text-gray-400">{featuredPost.excerpt}</p>
827
+ </div>
828
+ )}
829
+ </div>
830
+ </div>
831
+ </section>
832
+ </div>
833
+ );
834
+ }
835
+ `.trim();
836
+ await Bun.write(join(projectDir, `app/routes/index.${ext}`), homePage);
837
+ const blogLayout = `
838
+ ${ts ? `interface BlogLayoutProps {
839
+ children: React.ReactNode;
840
+ }
841
+ ` : ""}
842
+ export default function BlogLayout({ children }${ts ? ": BlogLayoutProps" : ""}) {
843
+ return (
844
+ <div className="min-h-screen">
845
+ {/* Blog Header */}
846
+ <div className="bg-gradient-to-r from-primary-600 to-purple-600 text-white py-12 px-4">
847
+ <div className="max-w-4xl mx-auto">
848
+ <h1 className="text-4xl font-bold mb-2">Blog</h1>
849
+ <p className="text-primary-100">Tutorials, guides, and updates from the EreoJS team</p>
850
+ </div>
851
+ </div>
852
+
853
+ {/* Blog Content */}
854
+ <div className="max-w-4xl mx-auto px-4 py-12">
855
+ {children}
856
+ </div>
857
+ </div>
858
+ );
859
+ }
860
+ `.trim();
861
+ await Bun.write(join(projectDir, `app/routes/blog/_layout.${ext}`), blogLayout);
862
+ const blogIndex = `
863
+ import { PostCard } from '~/components/PostCard';
864
+ import { getAllPosts, simulateDelay } from '~/lib/data';
865
+
866
+ /**
867
+ * Loader for the blog index page.
868
+ */
869
+ export async function loader() {
870
+ await simulateDelay(50);
871
+ const posts = getAllPosts();
872
+ return { posts };
873
+ }
874
+
875
+ ${ts ? `interface BlogIndexProps {
876
+ loaderData: {
877
+ posts: Array<{
878
+ slug: string;
879
+ title: string;
880
+ excerpt: string;
881
+ author: string;
882
+ date: string;
883
+ readTime: string;
884
+ tags: string[];
885
+ }>;
886
+ };
887
+ }
888
+ ` : ""}
889
+ export default function BlogIndex({ loaderData }${ts ? ": BlogIndexProps" : ""}) {
890
+ const { posts } = loaderData;
891
+
892
+ return (
893
+ <div>
894
+ <div className="grid gap-6">
895
+ {posts.map((post) => (
896
+ <PostCard key={post.slug} post={post} />
897
+ ))}
898
+ </div>
899
+ </div>
900
+ );
901
+ }
902
+ `.trim();
903
+ await Bun.write(join(projectDir, `app/routes/blog/index.${ext}`), blogIndex);
904
+ const blogPost = `
905
+ import { getPostBySlug, simulateDelay } from '~/lib/data';
906
+
907
+ /**
908
+ * Loader for individual blog posts.
909
+ * The [slug] in the filename creates a dynamic route parameter.
910
+ */
911
+ export async function loader({ params }${ts ? ": { params: { slug: string } }" : ""}) {
912
+ await simulateDelay(50);
913
+
914
+ const post = getPostBySlug(params.slug);
915
+
916
+ if (!post) {
917
+ throw new Response('Post not found', { status: 404 });
918
+ }
919
+
920
+ return { post };
921
+ }
922
+
923
+ ${ts ? `interface BlogPostProps {
924
+ loaderData: {
925
+ post: {
926
+ slug: string;
927
+ title: string;
928
+ content: string;
929
+ author: string;
930
+ date: string;
931
+ readTime: string;
932
+ tags: string[];
933
+ };
934
+ };
935
+ }
936
+ ` : ""}
937
+ export default function BlogPost({ loaderData }${ts ? ": BlogPostProps" : ""}) {
938
+ const { post } = loaderData;
939
+
940
+ return (
941
+ <article>
942
+ {/* Post Header */}
943
+ <header className="mb-8">
944
+ <div className="flex flex-wrap gap-2 mb-4">
945
+ {post.tags.map((tag) => (
946
+ <span
947
+ key={tag}
948
+ className="px-3 py-1 text-sm font-medium bg-primary-100 dark:bg-primary-900 text-primary-700 dark:text-primary-300 rounded-full"
949
+ >
950
+ {tag}
951
+ </span>
952
+ ))}
953
+ </div>
954
+ <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
955
+ <div className="flex items-center gap-4 text-gray-500 dark:text-gray-400">
956
+ <span>{post.author}</span>
957
+ <span>&bull;</span>
958
+ <span>{post.date}</span>
959
+ <span>&bull;</span>
960
+ <span>{post.readTime}</span>
961
+ </div>
962
+ </header>
963
+
964
+ {/* Post Content */}
965
+ <div className="prose dark:prose-invert prose-lg max-w-none">
966
+ {/* In a real app, you'd use a markdown renderer here */}
967
+ <div className="whitespace-pre-wrap font-serif leading-relaxed">
968
+ {post.content}
969
+ </div>
970
+ </div>
971
+
972
+ {/* Back Link */}
973
+ <div className="mt-12 pt-8 border-t border-gray-200 dark:border-gray-700">
974
+ <a href="/blog" className="text-primary-600 hover:underline">
975
+ &larr; Back to all posts
976
+ </a>
977
+ </div>
978
+ </article>
979
+ );
980
+ }
981
+
982
+ /**
983
+ * Error boundary for this route.
984
+ * Shown when the loader throws an error (e.g., post not found).
985
+ */
986
+ export function ErrorBoundary({ error }${ts ? ": { error: Error }" : ""}) {
987
+ return (
988
+ <div className="text-center py-12">
989
+ <h1 className="text-4xl font-bold mb-4">Post Not Found</h1>
990
+ <p className="text-gray-600 dark:text-gray-400 mb-8">
991
+ The blog post you're looking for doesn't exist.
992
+ </p>
993
+ <a href="/blog" className="btn btn-primary">
994
+ Back to Blog
995
+ </a>
996
+ </div>
997
+ );
998
+ }
999
+ `.trim();
1000
+ await Bun.write(join(projectDir, `app/routes/blog/[slug].${ext}`), blogPost);
1001
+ const contactPage = `
1002
+ 'use client';
1003
+
1004
+ import { useState } from 'react';
1005
+
1006
+ /**
1007
+ * Action handler for the contact form.
1008
+ * Runs on the server when the form is submitted.
1009
+ */
1010
+ export async function action({ request }${ts ? ": { request: Request }" : ""}) {
1011
+ const formData = await request.formData();
1012
+
1013
+ const name = formData.get('name')${ts ? " as string" : ""};
1014
+ const email = formData.get('email')${ts ? " as string" : ""};
1015
+ const message = formData.get('message')${ts ? " as string" : ""};
1016
+
1017
+ // Validate the form data
1018
+ const errors${ts ? ": Record<string, string>" : ""} = {};
1019
+
1020
+ if (!name || name.length < 2) {
1021
+ errors.name = 'Name must be at least 2 characters';
1022
+ }
1023
+ if (!email || !email.includes('@')) {
1024
+ errors.email = 'Please enter a valid email address';
1025
+ }
1026
+ if (!message || message.length < 10) {
1027
+ errors.message = 'Message must be at least 10 characters';
1028
+ }
1029
+
1030
+ if (Object.keys(errors).length > 0) {
1031
+ return { success: false, errors };
1032
+ }
1033
+
1034
+ // In a real app, you would:
1035
+ // - Save to database
1036
+ // - Send email notification
1037
+ // - etc.
1038
+
1039
+ console.log('Contact form submission:', { name, email, message });
1040
+
1041
+ // Simulate processing time
1042
+ await new Promise((resolve) => setTimeout(resolve, 500));
1043
+
1044
+ return { success: true, message: 'Thank you for your message! We\\'ll get back to you soon.' };
1045
+ }
1046
+
1047
+ ${ts ? `interface ContactPageProps {
1048
+ actionData?: {
1049
+ success: boolean;
1050
+ message?: string;
1051
+ errors?: Record<string, string>;
1052
+ };
1053
+ }
1054
+ ` : ""}
1055
+ export default function ContactPage({ actionData }${ts ? ": ContactPageProps" : ""}) {
1056
+ const [isSubmitting, setIsSubmitting] = useState(false);
1057
+
1058
+ const handleSubmit = async (e${ts ? ": React.FormEvent<HTMLFormElement>" : ""}) => {
1059
+ setIsSubmitting(true);
1060
+ // Form will be handled by the action
1061
+ };
1062
+
1063
+ return (
1064
+ <div className="min-h-screen py-12 px-4">
1065
+ <div className="max-w-2xl mx-auto">
1066
+ <h1 className="text-4xl font-bold mb-4">Contact Us</h1>
1067
+ <p className="text-gray-600 dark:text-gray-400 mb-8">
1068
+ Have a question or feedback? We'd love to hear from you.
1069
+ </p>
1070
+
1071
+ {actionData?.success ? (
1072
+ <div className="card bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800">
1073
+ <div className="flex items-center gap-3">
1074
+ <svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1075
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
1076
+ </svg>
1077
+ <p className="text-green-800 dark:text-green-200">{actionData.message}</p>
1078
+ </div>
1079
+ </div>
1080
+ ) : (
1081
+ <form method="POST" onSubmit={handleSubmit} className="space-y-6">
1082
+ <div>
1083
+ <label htmlFor="name" className="block text-sm font-medium mb-2">
1084
+ Name
1085
+ </label>
1086
+ <input
1087
+ type="text"
1088
+ id="name"
1089
+ name="name"
1090
+ required
1091
+ className="input"
1092
+ placeholder="Your name"
1093
+ />
1094
+ {actionData?.errors?.name && (
1095
+ <p className="mt-1 text-sm text-red-600">{actionData.errors.name}</p>
1096
+ )}
1097
+ </div>
1098
+
1099
+ <div>
1100
+ <label htmlFor="email" className="block text-sm font-medium mb-2">
1101
+ Email
1102
+ </label>
1103
+ <input
1104
+ type="email"
1105
+ id="email"
1106
+ name="email"
1107
+ required
1108
+ className="input"
1109
+ placeholder="you@example.com"
1110
+ />
1111
+ {actionData?.errors?.email && (
1112
+ <p className="mt-1 text-sm text-red-600">{actionData.errors.email}</p>
1113
+ )}
1114
+ </div>
1115
+
1116
+ <div>
1117
+ <label htmlFor="message" className="block text-sm font-medium mb-2">
1118
+ Message
1119
+ </label>
1120
+ <textarea
1121
+ id="message"
1122
+ name="message"
1123
+ rows={5}
1124
+ required
1125
+ className="input"
1126
+ placeholder="Your message..."
1127
+ />
1128
+ {actionData?.errors?.message && (
1129
+ <p className="mt-1 text-sm text-red-600">{actionData.errors.message}</p>
1130
+ )}
1131
+ </div>
1132
+
1133
+ <button
1134
+ type="submit"
1135
+ disabled={isSubmitting}
1136
+ className="btn btn-primary w-full disabled:opacity-50"
1137
+ >
1138
+ {isSubmitting ? 'Sending...' : 'Send Message'}
1139
+ </button>
1140
+ </form>
1141
+ )}
1142
+ </div>
1143
+ </div>
1144
+ );
1145
+ }
1146
+ `.trim();
1147
+ await Bun.write(join(projectDir, `app/routes/contact.${ext}`), contactPage);
1148
+ const aboutPage = `
1149
+ export default function AboutPage() {
1150
+ return (
1151
+ <div className="min-h-screen py-12 px-4">
1152
+ <div className="max-w-4xl mx-auto">
1153
+ <h1 className="text-4xl font-bold mb-8">About ${projectName}</h1>
1154
+
1155
+ <div className="prose dark:prose-invert prose-lg max-w-none">
1156
+ <p className="text-xl text-gray-600 dark:text-gray-400 mb-8">
1157
+ This project was created with EreoJS, a modern React fullstack framework built on Bun.
1158
+ </p>
1159
+
1160
+ <div className="grid md:grid-cols-2 gap-8 mb-12">
1161
+ <div className="card">
1162
+ <h3 className="text-xl font-bold mb-3">Features Demonstrated</h3>
1163
+ <ul className="space-y-2 text-gray-600 dark:text-gray-400">
1164
+ <li className="flex items-center gap-2">
1165
+ <span className="text-green-500">\u2713</span>
1166
+ Server-side rendering with loaders
1167
+ </li>
1168
+ <li className="flex items-center gap-2">
1169
+ <span className="text-green-500">\u2713</span>
1170
+ File-based routing
1171
+ </li>
1172
+ <li className="flex items-center gap-2">
1173
+ <span className="text-green-500">\u2713</span>
1174
+ Dynamic routes with [slug]
1175
+ </li>
1176
+ <li className="flex items-center gap-2">
1177
+ <span className="text-green-500">\u2713</span>
1178
+ Nested layouts
1179
+ </li>
1180
+ <li className="flex items-center gap-2">
1181
+ <span className="text-green-500">\u2713</span>
1182
+ Form actions
1183
+ </li>
1184
+ <li className="flex items-center gap-2">
1185
+ <span className="text-green-500">\u2713</span>
1186
+ Islands architecture
1187
+ </li>
1188
+ <li className="flex items-center gap-2">
1189
+ <span className="text-green-500">\u2713</span>
1190
+ Error boundaries
1191
+ </li>
1192
+ <li className="flex items-center gap-2">
1193
+ <span className="text-green-500">\u2713</span>
1194
+ Tailwind CSS styling
1195
+ </li>
1196
+ </ul>
1197
+ </div>
1198
+
1199
+ <div className="card">
1200
+ <h3 className="text-xl font-bold mb-3">Project Structure</h3>
1201
+ <pre className="text-sm bg-gray-100 dark:bg-gray-700 p-4 rounded-lg overflow-x-auto">
1202
+ {\`app/
1203
+ \u251C\u2500\u2500 components/
1204
+ \u2502 \u251C\u2500\u2500 Counter.tsx
1205
+ \u2502 \u251C\u2500\u2500 Footer.tsx
1206
+ \u2502 \u251C\u2500\u2500 Navigation.tsx
1207
+ \u2502 \u2514\u2500\u2500 PostCard.tsx
1208
+ \u251C\u2500\u2500 lib/
1209
+ \u2502 \u251C\u2500\u2500 data.ts
1210
+ \u2502 \u2514\u2500\u2500 types.ts
1211
+ \u251C\u2500\u2500 routes/
1212
+ \u2502 \u251C\u2500\u2500 _layout.tsx
1213
+ \u2502 \u251C\u2500\u2500 index.tsx
1214
+ \u2502 \u251C\u2500\u2500 about.tsx
1215
+ \u2502 \u251C\u2500\u2500 contact.tsx
1216
+ \u2502 \u2514\u2500\u2500 blog/
1217
+ \u2502 \u251C\u2500\u2500 _layout.tsx
1218
+ \u2502 \u251C\u2500\u2500 index.tsx
1219
+ \u2502 \u2514\u2500\u2500 [slug].tsx
1220
+ \u2514\u2500\u2500 styles.css\`}
1221
+ </pre>
1222
+ </div>
1223
+ </div>
1224
+
1225
+ <div className="card">
1226
+ <h3 className="text-xl font-bold mb-3">Learn More</h3>
1227
+ <p className="text-gray-600 dark:text-gray-400 mb-4">
1228
+ Check out the documentation and resources to learn how to build with EreoJS:
1229
+ </p>
1230
+ <div className="flex flex-wrap gap-4">
1231
+ <a
1232
+ href="https://ereo.dev/docs"
1233
+ target="_blank"
1234
+ rel="noopener"
1235
+ className="btn btn-primary"
1236
+ >
1237
+ Documentation
1238
+ </a>
1239
+ <a
1240
+ href="https://github.com/ereo-js/ereo"
1241
+ target="_blank"
1242
+ rel="noopener"
1243
+ className="btn btn-secondary"
1244
+ >
1245
+ GitHub Repository
1246
+ </a>
1247
+ </div>
1248
+ </div>
1249
+ </div>
1250
+ </div>
1251
+ </div>
1252
+ );
1253
+ }
1254
+ `.trim();
1255
+ await Bun.write(join(projectDir, `app/routes/about.${ext}`), aboutPage);
1256
+ const errorPage = `
1257
+ ${ts ? `interface ErrorPageProps {
1258
+ error: Error;
1259
+ }
1260
+ ` : ""}
1261
+ /**
1262
+ * Global error boundary.
1263
+ * This catches any unhandled errors in the app.
1264
+ */
1265
+ export default function ErrorPage({ error }${ts ? ": ErrorPageProps" : ""}) {
1266
+ return (
1267
+ <div className="min-h-screen flex items-center justify-center p-4">
1268
+ <div className="text-center">
1269
+ <div className="text-6xl mb-4">\uD83D\uDE35</div>
1270
+ <h1 className="text-4xl font-bold mb-4">Something went wrong</h1>
1271
+ <p className="text-gray-600 dark:text-gray-400 mb-8 max-w-md">
1272
+ {error?.message || 'An unexpected error occurred. Please try again.'}
1273
+ </p>
1274
+ <a href="/" className="btn btn-primary">
1275
+ Go Home
1276
+ </a>
1277
+ </div>
1278
+ </div>
1279
+ );
1280
+ }
1281
+ `.trim();
1282
+ await Bun.write(join(projectDir, `app/routes/_error.${ext}`), errorPage);
1283
+ const notFoundPage = `
1284
+ /**
1285
+ * Custom 404 page.
1286
+ * Shown when no route matches the URL.
1287
+ */
1288
+ export default function NotFoundPage() {
1289
+ return (
1290
+ <div className="min-h-screen flex items-center justify-center p-4">
1291
+ <div className="text-center">
1292
+ <div className="text-8xl font-bold text-gray-200 dark:text-gray-700 mb-4">404</div>
1293
+ <h1 className="text-4xl font-bold mb-4">Page Not Found</h1>
1294
+ <p className="text-gray-600 dark:text-gray-400 mb-8">
1295
+ The page you're looking for doesn't exist or has been moved.
1296
+ </p>
1297
+ <a href="/" className="btn btn-primary">
1298
+ Go Home
1299
+ </a>
1300
+ </div>
1301
+ </div>
1302
+ );
1303
+ }
1304
+ `.trim();
1305
+ await Bun.write(join(projectDir, `app/routes/_404.${ext}`), notFoundPage);
1306
+ await Bun.write(join(projectDir, ".gitignore"), `node_modules
1307
+ .ereo
1308
+ dist
1309
+ *.log
1310
+ .DS_Store
1311
+ .env
1312
+ .env.local
1313
+ .env.*.local`);
1314
+ await Bun.write(join(projectDir, ".env.example"), `# Environment Variables
1315
+ # Copy this file to .env and fill in your values
1316
+
1317
+ # Node environment
1318
+ NODE_ENV=development
1319
+
1320
+ # Server port (optional, defaults to 3000)
1321
+ # PORT=3000
1322
+
1323
+ # Database URL (if using database)
1324
+ # DATABASE_URL=
1325
+
1326
+ # API keys (if needed)
1327
+ # API_KEY=`);
1328
+ const readme = `# ${projectName}
1329
+
1330
+ A modern web application built with [EreoJS](https://github.com/ereo-js/ereo) - a React fullstack framework powered by Bun.
1331
+
1332
+ ## Features
1333
+
1334
+ This project demonstrates:
1335
+
1336
+ - **Server-Side Rendering** - Fast initial loads with SSR
1337
+ - **File-Based Routing** - Intuitive \`app/routes\` structure
1338
+ - **Data Loading** - Server loaders for data fetching
1339
+ - **Form Actions** - Handle mutations with actions
1340
+ - **Dynamic Routes** - \`[slug]\` parameters
1341
+ - **Nested Layouts** - Shared layouts per route segment
1342
+ - **Islands Architecture** - Selective hydration for interactivity
1343
+ - **Error Boundaries** - Graceful error handling
1344
+ - **Tailwind CSS** - Utility-first styling
1345
+
1346
+ ## Getting Started
1347
+
1348
+ \`\`\`bash
1349
+ # Install dependencies
1350
+ bun install
1351
+
1352
+ # Start development server
1353
+ bun run dev
1354
+
1355
+ # Open http://localhost:3000
1356
+ \`\`\`
1357
+
1358
+ ## Project Structure
1359
+
1360
+ \`\`\`
1361
+ app/
1362
+ \u251C\u2500\u2500 components/ # Reusable React components
1363
+ \u2502 \u251C\u2500\u2500 Counter.tsx # Interactive island example
1364
+ \u2502 \u251C\u2500\u2500 Footer.tsx
1365
+ \u2502 \u251C\u2500\u2500 Navigation.tsx
1366
+ \u2502 \u2514\u2500\u2500 PostCard.tsx
1367
+ \u251C\u2500\u2500 lib/ # Shared utilities and data
1368
+ \u2502 \u251C\u2500\u2500 data.ts # Mock data and helpers
1369
+ \u2502 \u2514\u2500\u2500 types.ts # TypeScript types
1370
+ \u251C\u2500\u2500 routes/ # File-based routes
1371
+ \u2502 \u251C\u2500\u2500 _layout.tsx # Root layout
1372
+ \u2502 \u251C\u2500\u2500 _error.tsx # Error boundary
1373
+ \u2502 \u251C\u2500\u2500 _404.tsx # Not found page
1374
+ \u2502 \u251C\u2500\u2500 index.tsx # Home page (/)
1375
+ \u2502 \u251C\u2500\u2500 about.tsx # About page (/about)
1376
+ \u2502 \u251C\u2500\u2500 contact.tsx # Contact form (/contact)
1377
+ \u2502 \u2514\u2500\u2500 blog/
1378
+ \u2502 \u251C\u2500\u2500 _layout.tsx # Blog layout
1379
+ \u2502 \u251C\u2500\u2500 index.tsx # Blog list (/blog)
1380
+ \u2502 \u2514\u2500\u2500 [slug].tsx # Blog post (/blog/:slug)
1381
+ \u2514\u2500\u2500 styles.css # Global styles with Tailwind
1382
+ \`\`\`
1383
+
1384
+ ## Scripts
1385
+
1386
+ - \`bun run dev\` - Start development server
1387
+ - \`bun run build\` - Build for production
1388
+ - \`bun run start\` - Start production server
1389
+ - \`bun test\` - Run tests
1390
+ - \`bun run typecheck\` - TypeScript type checking
1391
+
1392
+ ## Learn More
1393
+
1394
+ - [EreoJS Documentation](https://ereo.dev/docs)
1395
+ - [Bun Documentation](https://bun.sh/docs)
1396
+ - [Tailwind CSS](https://tailwindcss.com/docs)
1397
+ `;
1398
+ await Bun.write(join(projectDir, "README.md"), readme);
1399
+ }
1400
+ async function generateProject(projectDir, projectName, options) {
1401
+ const { template, typescript } = options;
1402
+ if (template === "minimal") {
1403
+ await generateMinimalProject(projectDir, projectName, typescript);
1404
+ } else {
1405
+ await generateTailwindProject(projectDir, projectName, typescript);
1406
+ }
1407
+ }
1408
+ async function initGit(projectDir) {
1409
+ try {
1410
+ const proc = Bun.spawn(["git", "init"], {
1411
+ cwd: projectDir,
1412
+ stdout: "pipe",
1413
+ stderr: "pipe"
1414
+ });
1415
+ await proc.exited;
1416
+ } catch {
1417
+ console.log(" \x1B[33m!\x1B[0m Git initialization skipped");
1418
+ }
1419
+ }
1420
+ async function installDeps(projectDir) {
1421
+ console.log(`
1422
+ Installing dependencies...
1423
+ `);
1424
+ const proc = Bun.spawn(["bun", "install"], {
1425
+ cwd: projectDir,
1426
+ stdout: "inherit",
1427
+ stderr: "inherit"
1428
+ });
1429
+ await proc.exited;
1430
+ }
1431
+ async function main() {
1432
+ printBanner();
1433
+ const args = process.argv.slice(2);
1434
+ const { projectName, options } = parseArgs(args);
1435
+ if (!projectName) {
1436
+ console.error(` \x1B[31m\u2717\x1B[0m Please provide a project name
1437
+ `);
1438
+ printHelp();
1439
+ process.exit(1);
1440
+ }
1441
+ const finalOptions = { ...defaultOptions, ...options };
1442
+ const projectDir = resolve(process.cwd(), projectName);
1443
+ console.log(` Creating \x1B[36m${projectName}\x1B[0m...
1444
+ `);
1445
+ console.log(` Template: ${finalOptions.template}`);
1446
+ console.log(` TypeScript: ${finalOptions.typescript ? "Yes" : "No"}
1447
+ `);
1448
+ await generateProject(projectDir, projectName, finalOptions);
1449
+ console.log(" \x1B[32m\u2713\x1B[0m Project files created");
1450
+ if (finalOptions.git) {
1451
+ await initGit(projectDir);
1452
+ console.log(" \x1B[32m\u2713\x1B[0m Git initialized");
1453
+ }
1454
+ if (finalOptions.install) {
1455
+ await installDeps(projectDir);
1456
+ }
1457
+ console.log(`
1458
+ \x1B[32m\u2713\x1B[0m Done! Your project is ready.
1459
+
1460
+ Next steps:
1461
+
1462
+ \x1B[36mcd ${projectName}\x1B[0m
1463
+ ${!finalOptions.install ? `\x1B[36mbun install\x1B[0m
1464
+ ` : ""}\x1B[36mbun run dev\x1B[0m
1465
+
1466
+ Open http://localhost:3000 to see your app.
1467
+
1468
+ Happy coding!
1469
+ `);
1470
+ }
1471
+ main().catch(console.error);