create-oven 0.1.0

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 (2) hide show
  1. package/index.js +513 -0
  2. package/package.json +32 -0
package/index.js ADDED
@@ -0,0 +1,513 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * create-oven - Create a new Oven project
5
+ *
6
+ * Usage:
7
+ * npx create-oven my-app
8
+ * bunx create-oven my-app
9
+ */
10
+
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import { fileURLToPath } from 'url';
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+
17
+ const COLORS = {
18
+ reset: '\x1b[0m',
19
+ bright: '\x1b[1m',
20
+ red: '\x1b[31m',
21
+ green: '\x1b[32m',
22
+ yellow: '\x1b[33m',
23
+ blue: '\x1b[34m',
24
+ cyan: '\x1b[36m',
25
+ };
26
+
27
+ function log(msg) {
28
+ console.log(msg);
29
+ }
30
+
31
+ function success(msg) {
32
+ console.log(`${COLORS.green}${msg}${COLORS.reset}`);
33
+ }
34
+
35
+ function error(msg) {
36
+ console.error(`${COLORS.red}${msg}${COLORS.reset}`);
37
+ }
38
+
39
+ function info(msg) {
40
+ console.log(`${COLORS.cyan}${msg}${COLORS.reset}`);
41
+ }
42
+
43
+ async function main() {
44
+ const args = process.argv.slice(2);
45
+ const projectName = args[0];
46
+
47
+ if (!projectName || projectName === '--help' || projectName === '-h') {
48
+ log(`
49
+ 🔥 ${COLORS.bright}create-oven${COLORS.reset} - Create a new Oven project
50
+
51
+ ${COLORS.yellow}Usage:${COLORS.reset}
52
+ npx create-oven <project-name>
53
+ bunx create-oven <project-name>
54
+
55
+ ${COLORS.yellow}Examples:${COLORS.reset}
56
+ npx create-oven my-app
57
+ bunx create-oven my-awesome-app
58
+
59
+ ${COLORS.yellow}Options:${COLORS.reset}
60
+ -h, --help Show this help message
61
+ -v, --version Show version
62
+
63
+ ${COLORS.cyan}Learn more: https://github.com/oven-ttta/oven-framework${COLORS.reset}
64
+ `);
65
+ process.exit(projectName ? 0 : 1);
66
+ }
67
+
68
+ if (projectName === '--version' || projectName === '-v') {
69
+ log('create-oven v0.1.0');
70
+ process.exit(0);
71
+ }
72
+
73
+ log(`
74
+ 🔥 Creating new Oven project: ${COLORS.bright}${projectName}${COLORS.reset}
75
+ `);
76
+
77
+ const projectDir = path.join(process.cwd(), projectName);
78
+
79
+ // Check if directory exists
80
+ if (fs.existsSync(projectDir)) {
81
+ error(` ✗ Directory "${projectName}" already exists!`);
82
+ process.exit(1);
83
+ }
84
+
85
+ // Create project directory
86
+ fs.mkdirSync(projectDir, { recursive: true });
87
+
88
+ // Create project structure
89
+ const dirs = [
90
+ 'app',
91
+ 'app/api/hello',
92
+ 'app/about',
93
+ 'public',
94
+ 'components',
95
+ 'lib',
96
+ ];
97
+
98
+ for (const dir of dirs) {
99
+ fs.mkdirSync(path.join(projectDir, dir), { recursive: true });
100
+ }
101
+
102
+ info(' Creating project files...');
103
+
104
+ // package.json
105
+ const pkg = {
106
+ name: projectName,
107
+ version: '0.1.0',
108
+ type: 'module',
109
+ scripts: {
110
+ dev: 'bun run --hot server.ts',
111
+ build: 'bun build ./server.ts --outdir ./dist --target bun',
112
+ start: 'bun run dist/server.js',
113
+ },
114
+ dependencies: {},
115
+ devDependencies: {
116
+ '@types/bun': 'latest',
117
+ typescript: '^5.3.0',
118
+ },
119
+ };
120
+ fs.writeFileSync(
121
+ path.join(projectDir, 'package.json'),
122
+ JSON.stringify(pkg, null, 2)
123
+ );
124
+
125
+ // tsconfig.json
126
+ const tsconfig = {
127
+ compilerOptions: {
128
+ target: 'ESNext',
129
+ module: 'ESNext',
130
+ moduleResolution: 'bundler',
131
+ strict: true,
132
+ esModuleInterop: true,
133
+ skipLibCheck: true,
134
+ forceConsistentCasingInFileNames: true,
135
+ jsx: 'preserve',
136
+ lib: ['ESNext', 'DOM'],
137
+ types: ['bun-types'],
138
+ },
139
+ include: ['**/*.ts', '**/*.tsx'],
140
+ exclude: ['node_modules'],
141
+ };
142
+ fs.writeFileSync(
143
+ path.join(projectDir, 'tsconfig.json'),
144
+ JSON.stringify(tsconfig, null, 2)
145
+ );
146
+
147
+ // server.ts - Main server file
148
+ const serverFile = `/**
149
+ * Oven Server - ${projectName}
150
+ * A Next.js-style framework powered by Bun
151
+ */
152
+
153
+ const PORT = parseInt(process.env.PORT || '3000');
154
+
155
+ // Simple router
156
+ const routes: Map<string, (req: Request) => Promise<Response>> = new Map();
157
+
158
+ // Scan app directory for routes
159
+ async function scanRoutes() {
160
+ const glob = new Bun.Glob('**/page.tsx');
161
+ for await (const file of glob.scan({ cwd: './app' })) {
162
+ const routePath = '/' + file.replace(/\\/page\\.tsx$/, '').replace(/^page\\.tsx$/, '');
163
+ const normalizedPath = routePath === '/' ? '/' : routePath;
164
+
165
+ routes.set(normalizedPath, async (req: Request) => {
166
+ const module = await import(\`./app/\${file}\`);
167
+ const content = await module.default({ params: {}, searchParams: {} });
168
+
169
+ // Wrap with layout
170
+ let html = content;
171
+ try {
172
+ const layoutModule = await import('./app/layout.tsx');
173
+ html = await layoutModule.default({ children: content, params: {} });
174
+ } catch {}
175
+
176
+ // Generate metadata
177
+ const metadata = module.metadata || {};
178
+ const title = typeof metadata.title === 'string' ? metadata.title : metadata.title?.default || '${projectName}';
179
+
180
+ const fullHtml = \`<!DOCTYPE html>
181
+ <html lang="en">
182
+ <head>
183
+ <meta charset="UTF-8">
184
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
185
+ <title>\${title}</title>
186
+ \${metadata.description ? \`<meta name="description" content="\${metadata.description}">\` : ''}
187
+ </head>
188
+ <body>
189
+ \${html}
190
+ </body>
191
+ </html>\`;
192
+
193
+ return new Response(fullHtml, {
194
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
195
+ });
196
+ });
197
+ }
198
+
199
+ // Scan API routes
200
+ const apiGlob = new Bun.Glob('**/route.ts');
201
+ for await (const file of apiGlob.scan({ cwd: './app' })) {
202
+ const routePath = '/' + file.replace(/\\/route\\.ts$/, '').replace(/^route\\.ts$/, '');
203
+
204
+ routes.set(routePath, async (req: Request) => {
205
+ const module = await import(\`./app/\${file}\`);
206
+ const method = req.method.toUpperCase();
207
+ const handler = module[method];
208
+
209
+ if (handler) {
210
+ return handler(req);
211
+ }
212
+ return new Response('Method not allowed', { status: 405 });
213
+ });
214
+ }
215
+ }
216
+
217
+ // Main server
218
+ async function main() {
219
+ await scanRoutes();
220
+
221
+ Bun.serve({
222
+ port: PORT,
223
+ async fetch(req: Request) {
224
+ const url = new URL(req.url);
225
+ let pathname = url.pathname;
226
+
227
+ // Remove trailing slash
228
+ if (pathname !== '/' && pathname.endsWith('/')) {
229
+ pathname = pathname.slice(0, -1);
230
+ }
231
+
232
+ // Try to match route
233
+ const handler = routes.get(pathname);
234
+ if (handler) {
235
+ return handler(req);
236
+ }
237
+
238
+ // Static files
239
+ const publicPath = './public' + pathname;
240
+ const file = Bun.file(publicPath);
241
+ if (await file.exists()) {
242
+ return new Response(file);
243
+ }
244
+
245
+ // 404
246
+ return new Response('Not Found', { status: 404 });
247
+ },
248
+ });
249
+
250
+ console.log(\`
251
+ 🔥 Oven is ready!
252
+
253
+ ➜ Local: http://localhost:\${PORT}
254
+ ➜ Network: http://0.0.0.0:\${PORT}
255
+ \`);
256
+ }
257
+
258
+ main();
259
+ `;
260
+ fs.writeFileSync(path.join(projectDir, 'server.ts'), serverFile);
261
+
262
+ // app/layout.tsx
263
+ const rootLayout = `import type { LayoutProps } from './types';
264
+
265
+ export default function RootLayout({ children }: LayoutProps) {
266
+ return \`
267
+ <div id="__oven">
268
+ <nav style="
269
+ background: linear-gradient(135deg, #ff6b35 0%, #f7931e 100%);
270
+ padding: 1rem 2rem;
271
+ display: flex;
272
+ justify-content: space-between;
273
+ align-items: center;
274
+ ">
275
+ <a href="/" style="color: white; text-decoration: none; font-weight: bold; font-size: 1.25rem;">
276
+ 🔥 ${projectName}
277
+ </a>
278
+ <div style="display: flex; gap: 1.5rem;">
279
+ <a href="/" style="color: rgba(255,255,255,0.9); text-decoration: none;">Home</a>
280
+ <a href="/about" style="color: rgba(255,255,255,0.9); text-decoration: none;">About</a>
281
+ <a href="/api/hello" style="color: rgba(255,255,255,0.9); text-decoration: none;">API</a>
282
+ </div>
283
+ </nav>
284
+ <main style="min-height: calc(100vh - 60px);">
285
+ \${children}
286
+ </main>
287
+ <style>
288
+ * { margin: 0; padding: 0; box-sizing: border-box; }
289
+ body { font-family: system-ui, -apple-system, sans-serif; }
290
+ </style>
291
+ </div>
292
+ \`;
293
+ }
294
+ `;
295
+ fs.writeFileSync(path.join(projectDir, 'app', 'layout.tsx'), rootLayout);
296
+
297
+ // app/types.ts
298
+ const typesFile = `export interface PageProps {
299
+ params: Record<string, string>;
300
+ searchParams: Record<string, string>;
301
+ }
302
+
303
+ export interface LayoutProps {
304
+ children: string;
305
+ params: Record<string, string>;
306
+ }
307
+
308
+ export interface Metadata {
309
+ title?: string | { default: string; template?: string };
310
+ description?: string;
311
+ keywords?: string[];
312
+ }
313
+ `;
314
+ fs.writeFileSync(path.join(projectDir, 'app', 'types.ts'), typesFile);
315
+
316
+ // app/page.tsx
317
+ const homePage = `import type { PageProps, Metadata } from './types';
318
+
319
+ export const metadata: Metadata = {
320
+ title: '${projectName}',
321
+ description: 'Built with Oven - A Next.js-style framework for Bun',
322
+ };
323
+
324
+ export default function HomePage({ searchParams }: PageProps) {
325
+ return \`
326
+ <div style="max-width: 800px; margin: 0 auto; padding: 4rem 2rem; text-align: center;">
327
+ <h1 style="font-size: 3rem; margin-bottom: 1rem; background: linear-gradient(135deg, #ff6b35, #f7931e); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
328
+ Welcome to ${projectName} 🔥
329
+ </h1>
330
+ <p style="color: #666; font-size: 1.2rem; margin-bottom: 2rem;">
331
+ Get started by editing <code style="background: #f5f5f5; padding: 0.25rem 0.5rem; border-radius: 4px;">app/page.tsx</code>
332
+ </p>
333
+
334
+ <div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
335
+ <a href="/about" style="
336
+ background: linear-gradient(135deg, #ff6b35, #f7931e);
337
+ color: white;
338
+ padding: 0.75rem 1.5rem;
339
+ border-radius: 8px;
340
+ text-decoration: none;
341
+ font-weight: 500;
342
+ ">
343
+ About Page →
344
+ </a>
345
+ <a href="/api/hello" style="
346
+ background: #333;
347
+ color: white;
348
+ padding: 0.75rem 1.5rem;
349
+ border-radius: 8px;
350
+ text-decoration: none;
351
+ font-weight: 500;
352
+ ">
353
+ API Example
354
+ </a>
355
+ </div>
356
+
357
+ <div style="margin-top: 4rem; padding: 2rem; background: #f9f9f9; border-radius: 12px; text-align: left;">
358
+ <h3 style="margin-bottom: 1rem;">📁 Project Structure</h3>
359
+ <pre style="font-size: 0.9rem; color: #666;">
360
+ ${projectName}/
361
+ ├── app/
362
+ │ ├── layout.tsx # Root layout
363
+ │ ├── page.tsx # Home page (/)
364
+ │ ├── about/
365
+ │ │ └── page.tsx # About page (/about)
366
+ │ └── api/
367
+ │ └── hello/
368
+ │ └── route.ts # API route (/api/hello)
369
+ ├── public/ # Static files
370
+ ├── server.ts # Bun server
371
+ └── package.json
372
+ </pre>
373
+ </div>
374
+ </div>
375
+ \`;
376
+ }
377
+ `;
378
+ fs.writeFileSync(path.join(projectDir, 'app', 'page.tsx'), homePage);
379
+
380
+ // app/about/page.tsx
381
+ const aboutPage = `import type { PageProps, Metadata } from '../types';
382
+
383
+ export const metadata: Metadata = {
384
+ title: 'About',
385
+ description: 'About ${projectName}',
386
+ };
387
+
388
+ export default function AboutPage({ params }: PageProps) {
389
+ return \`
390
+ <div style="max-width: 800px; margin: 0 auto; padding: 4rem 2rem;">
391
+ <h1 style="font-size: 2.5rem; margin-bottom: 1rem;">About ${projectName}</h1>
392
+ <p style="color: #666; line-height: 1.8; margin-bottom: 1.5rem;">
393
+ This is the about page of your Oven application.
394
+ Built with Bun for maximum performance.
395
+ </p>
396
+
397
+ <div style="background: #fff5f2; padding: 1.5rem; border-radius: 12px; border-left: 4px solid #ff6b35;">
398
+ <h3 style="margin-bottom: 0.5rem;">🚀 Why Oven?</h3>
399
+ <ul style="color: #666; line-height: 1.8; margin-left: 1.5rem;">
400
+ <li>Built for Bun runtime - 4x faster than Node.js</li>
401
+ <li>Next.js-style file-based routing</li>
402
+ <li>Zero configuration needed</li>
403
+ <li>Full TypeScript support</li>
404
+ </ul>
405
+ </div>
406
+
407
+ <a href="/" style="color: #ff6b35; margin-top: 2rem; display: inline-block; text-decoration: none;">
408
+ ← Back to Home
409
+ </a>
410
+ </div>
411
+ \`;
412
+ }
413
+ `;
414
+ fs.writeFileSync(path.join(projectDir, 'app', 'about', 'page.tsx'), aboutPage);
415
+
416
+ // app/api/hello/route.ts
417
+ const apiRoute = `// API Route: /api/hello
418
+
419
+ export async function GET(request: Request) {
420
+ return Response.json({
421
+ message: 'Hello from ${projectName}!',
422
+ timestamp: new Date().toISOString(),
423
+ framework: 'Oven 🔥',
424
+ });
425
+ }
426
+
427
+ export async function POST(request: Request) {
428
+ const body = await request.json().catch(() => ({}));
429
+ return Response.json({
430
+ message: 'Data received!',
431
+ data: body,
432
+ });
433
+ }
434
+ `;
435
+ fs.writeFileSync(path.join(projectDir, 'app', 'api', 'hello', 'route.ts'), apiRoute);
436
+
437
+ // .gitignore
438
+ const gitignore = `node_modules
439
+ dist
440
+ .oven
441
+ *.log
442
+ .DS_Store
443
+ .env
444
+ .env.local
445
+ `;
446
+ fs.writeFileSync(path.join(projectDir, '.gitignore'), gitignore);
447
+
448
+ // README.md
449
+ const readme = `# ${projectName}
450
+
451
+ Built with [Oven](https://github.com/oven-ttta/oven-framework) - A Next.js-style framework for Bun 🔥
452
+
453
+ ## Getting Started
454
+
455
+ \`\`\`bash
456
+ # Install dependencies
457
+ bun install
458
+
459
+ # Start development server
460
+ bun run dev
461
+
462
+ # Build for production
463
+ bun run build
464
+
465
+ # Start production server
466
+ bun run start
467
+ \`\`\`
468
+
469
+ Open [http://localhost:3000](http://localhost:3000) to see your app.
470
+
471
+ ## Project Structure
472
+
473
+ \`\`\`
474
+ ${projectName}/
475
+ ├── app/
476
+ │ ├── layout.tsx # Root layout
477
+ │ ├── page.tsx # Home page (/)
478
+ │ ├── about/
479
+ │ │ └── page.tsx # About page (/about)
480
+ │ └── api/
481
+ │ └── hello/
482
+ │ └── route.ts # API route (/api/hello)
483
+ ├── public/ # Static files
484
+ ├── server.ts # Bun server
485
+ └── package.json
486
+ \`\`\`
487
+
488
+ ## Learn More
489
+
490
+ - [Oven GitHub](https://github.com/oven-ttta/oven-framework)
491
+ - [Bun Documentation](https://bun.sh/docs)
492
+ `;
493
+ fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
494
+
495
+ success(`
496
+ ✅ Project created successfully!
497
+
498
+ ${COLORS.yellow}Next steps:${COLORS.reset}
499
+
500
+ cd ${projectName}
501
+ bun install
502
+ bun run dev
503
+
504
+ Open ${COLORS.cyan}http://localhost:3000${COLORS.reset} in your browser.
505
+
506
+ ${COLORS.bright}Happy cooking! 🔥${COLORS.reset}
507
+ `);
508
+ }
509
+
510
+ main().catch((err) => {
511
+ error(`Error: ${err.message}`);
512
+ process.exit(1);
513
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "create-oven",
3
+ "version": "0.1.0",
4
+ "description": "Create a new Oven project - Next.js-style framework for Bun",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-oven": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "templates"
12
+ ],
13
+ "keywords": [
14
+ "bun",
15
+ "oven",
16
+ "framework",
17
+ "create",
18
+ "scaffold",
19
+ "typescript",
20
+ "fullstack"
21
+ ],
22
+ "author": "oven-ttta",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/oven-ttta/oven-framework"
27
+ },
28
+ "homepage": "https://github.com/oven-ttta/oven-framework#readme",
29
+ "engines": {
30
+ "node": ">=18.0.0"
31
+ }
32
+ }