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.
- package/README.md +129 -0
- package/dist/index.js +1471 -0
- 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>© {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>•</span>
|
|
958
|
+
<span>{post.date}</span>
|
|
959
|
+
<span>•</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
|
+
← 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);
|