create-oven 0.2.3 → 0.3.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 +228 -434
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -3,21 +3,14 @@
3
3
  /**
4
4
  * create-oven - Create a new Oven project
5
5
  * Like create-next-app but for Bun
6
- *
7
- * Usage:
8
- * npx create-oven my-app
9
- * bunx create-oven my-app
10
6
  */
11
7
 
12
8
  import fs from 'fs';
13
9
  import path from 'path';
14
10
  import readline from 'readline';
15
- import { fileURLToPath } from 'url';
16
11
  import { execSync } from 'child_process';
17
12
 
18
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
-
20
- // Colors for terminal output
13
+ // Colors
21
14
  const c = {
22
15
  reset: '\x1b[0m',
23
16
  bold: '\x1b[1m',
@@ -26,13 +19,9 @@ const c = {
26
19
  green: '\x1b[32m',
27
20
  yellow: '\x1b[33m',
28
21
  blue: '\x1b[34m',
29
- magenta: '\x1b[35m',
30
22
  cyan: '\x1b[36m',
31
- white: '\x1b[37m',
32
- bgBlue: '\x1b[44m',
33
23
  };
34
24
 
35
- // Create readline interface for prompts
36
25
  function createPrompt() {
37
26
  return readline.createInterface({
38
27
  input: process.stdin,
@@ -40,34 +29,16 @@ function createPrompt() {
40
29
  });
41
30
  }
42
31
 
43
- // Ask a yes/no question
44
32
  async function askYesNo(rl, question, defaultValue = true) {
45
- const defaultText = defaultValue ? 'Yes' : 'No';
46
33
  const hint = defaultValue ? '(Y/n)' : '(y/N)';
47
-
48
34
  return new Promise((resolve) => {
49
35
  rl.question(`${c.cyan}?${c.reset} ${question} ${c.dim}${hint}${c.reset} `, (answer) => {
50
- if (!answer.trim()) {
51
- resolve(defaultValue);
52
- } else {
53
- resolve(answer.toLowerCase().startsWith('y'));
54
- }
55
- });
56
- });
57
- }
58
-
59
- // Ask for text input
60
- async function askText(rl, question, defaultValue = '') {
61
- const hint = defaultValue ? ` ${c.dim}(${defaultValue})${c.reset}` : '';
62
-
63
- return new Promise((resolve) => {
64
- rl.question(`${c.cyan}?${c.reset} ${question}${hint} `, (answer) => {
65
- resolve(answer.trim() || defaultValue);
36
+ if (!answer.trim()) resolve(defaultValue);
37
+ else resolve(answer.toLowerCase().startsWith('y'));
66
38
  });
67
39
  });
68
40
  }
69
41
 
70
- // Display a spinner
71
42
  function spinner(text) {
72
43
  const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
73
44
  let i = 0;
@@ -75,7 +46,6 @@ function spinner(text) {
75
46
  process.stdout.write(`\r${c.cyan}${frames[i]}${c.reset} ${text}`);
76
47
  i = (i + 1) % frames.length;
77
48
  }, 80);
78
-
79
49
  return {
80
50
  stop: (finalText) => {
81
51
  clearInterval(interval);
@@ -88,57 +58,44 @@ function spinner(text) {
88
58
  };
89
59
  }
90
60
 
91
- // Main function
92
61
  async function main() {
93
62
  const args = process.argv.slice(2);
94
63
 
95
- // Show help
96
64
  if (args.includes('--help') || args.includes('-h')) {
97
65
  console.log(`
98
66
  ${c.bold}${c.cyan}create-oven${c.reset} - Create a new Oven project
99
67
 
100
68
  ${c.yellow}Usage:${c.reset}
101
69
  npx create-oven [project-name] [options]
102
- bunx create-oven [project-name] [options]
103
70
 
104
71
  ${c.yellow}Options:${c.reset}
105
- --typescript, --ts Use TypeScript (default: true)
106
- --no-typescript Don't use TypeScript
107
- --tailwind Add Tailwind CSS
108
- --no-tailwind Don't add Tailwind CSS
109
- --eslint Add ESLint
110
- --no-eslint Don't add ESLint
111
- --src-dir Use src/ directory
112
- --no-src-dir Don't use src/ directory (default)
113
- --app Use App Router (default: true)
114
- --import-alias <alias> Set import alias (default: @/*)
115
- --use-bun Use Bun for installing dependencies
116
- --use-npm Use npm for installing dependencies
72
+ --ts, --typescript Use TypeScript (default)
73
+ --js, --javascript Use JavaScript
74
+ --tailwind Use Tailwind CSS (default)
75
+ --eslint Use ESLint (default)
76
+ --no-tailwind Don't use Tailwind CSS
77
+ --no-eslint Don't use ESLint
117
78
  --no-install Skip installing dependencies
118
79
  -y, --yes Use defaults without prompts
119
- -h, --help Show this help message
80
+ -h, --help Show this help
120
81
  -v, --version Show version
121
82
 
122
83
  ${c.yellow}Examples:${c.reset}
123
84
  npx create-oven my-app
124
- npx create-oven my-app --tailwind --eslint
125
- bunx create-oven my-app --src-dir
126
-
127
- ${c.cyan}Learn more: https://github.com/oven-ttta/oven-framework${c.reset}
85
+ npx create-oven my-app --js --no-tailwind
128
86
  `);
129
87
  process.exit(0);
130
88
  }
131
89
 
132
- // Show version
133
90
  if (args.includes('--version') || args.includes('-v')) {
134
- console.log('create-oven v0.2.3');
91
+ console.log('create-oven v0.3.0');
135
92
  process.exit(0);
136
93
  }
137
94
 
138
95
  console.log(`
139
96
  ${c.bold}${c.cyan} ╔═══════════════════════════════════════╗
140
97
  ║ ║
141
- ║ 🔥 Create Oven App v0.2.3
98
+ ║ 🔥 Create Oven App v0.3.0
142
99
  ║ ║
143
100
  ║ Next.js-style framework for Bun ║
144
101
  ║ ║
@@ -148,113 +105,83 @@ ${c.bold}${c.cyan} ╔═══════════════════
148
105
  const rl = createPrompt();
149
106
 
150
107
  try {
108
+ const hasFlag = (flag) => args.includes(flag);
109
+ const skipPrompts = hasFlag('--yes') || hasFlag('-y');
110
+ const noInstall = hasFlag('--no-install');
111
+
151
112
  // Get project name
152
113
  let projectName = args.find(arg => !arg.startsWith('-'));
153
-
154
114
  if (!projectName) {
155
- projectName = await askText(rl, 'What is your project named?', 'my-app');
115
+ projectName = await new Promise((resolve) => {
116
+ rl.question(`${c.cyan}?${c.reset} What is your project named? ${c.dim}(my-app)${c.reset} `, (answer) => {
117
+ resolve(answer.trim() || 'my-app');
118
+ });
119
+ });
156
120
  }
157
121
 
158
122
  const projectDir = path.join(process.cwd(), projectName);
159
123
 
160
- // Check if directory exists
161
124
  if (fs.existsSync(projectDir)) {
162
125
  console.log(`\n${c.red}✗${c.reset} Directory "${projectName}" already exists!`);
163
126
  rl.close();
164
127
  process.exit(1);
165
128
  }
166
129
 
167
- // Parse CLI flags
168
- const hasFlag = (flag) => args.includes(flag);
169
- const skipPrompts = hasFlag('--yes') || hasFlag('-y');
170
- const noInstall = hasFlag('--no-install');
171
-
172
- // Interactive prompts (like create-next-app)
173
130
  console.log();
174
131
 
175
- // If --yes flag, use defaults. If specific flag, use that value. Otherwise ask.
176
- const useTypescript = hasFlag('--typescript') || hasFlag('--ts') ||
177
- hasFlag('--no-typescript') ? !hasFlag('--no-typescript') :
132
+ // Options
133
+ const useTypescript = hasFlag('--js') || hasFlag('--javascript') ? false :
134
+ hasFlag('--ts') || hasFlag('--typescript') ? true :
178
135
  skipPrompts ? true :
179
136
  await askYesNo(rl, 'Would you like to use TypeScript?', true);
180
137
 
181
- const useEslint = hasFlag('--eslint') ||
182
- hasFlag('--no-eslint') ? !hasFlag('--no-eslint') :
183
- skipPrompts ? true :
184
- await askYesNo(rl, 'Would you like to use ESLint?', true);
185
-
186
- const useTailwind = hasFlag('--tailwind') ||
187
- hasFlag('--no-tailwind') ? !hasFlag('--no-tailwind') :
138
+ const useTailwind = hasFlag('--no-tailwind') ? false :
139
+ hasFlag('--tailwind') ? true :
188
140
  skipPrompts ? true :
189
141
  await askYesNo(rl, 'Would you like to use Tailwind CSS?', true);
190
142
 
191
- const useSrcDir = hasFlag('--src-dir') ||
192
- hasFlag('--no-src-dir') ? !hasFlag('--no-src-dir') :
193
- skipPrompts ? false :
194
- await askYesNo(rl, 'Would you like to use `src/` directory?', false);
195
-
196
- const useAppRouter = hasFlag('--app') ||
197
- hasFlag('--no-app') ? !hasFlag('--no-app') :
143
+ const useEslint = hasFlag('--no-eslint') ? false :
144
+ hasFlag('--eslint') ? true :
198
145
  skipPrompts ? true :
199
- await askYesNo(rl, 'Would you like to use App Router? (recommended)', true);
200
-
201
- const importAliasArg = args.find((arg, i) => args[i - 1] === '--import-alias');
202
- const importAlias = importAliasArg ||
203
- (skipPrompts ? '@/*' : await askText(rl, 'What import alias would you like configured?', '@/*'));
146
+ await askYesNo(rl, 'Would you like to use ESLint?', true);
204
147
 
205
148
  rl.close();
206
149
 
150
+ console.log();
151
+ console.log(`Creating a new Oven app in ${c.green}${projectDir}${c.reset}.`);
207
152
  console.log();
208
153
 
209
- // Create project
210
- const spin = spinner('Creating project structure...');
211
-
212
- // Determine base directory
213
- const baseDir = useSrcDir ? path.join(projectDir, 'src') : projectDir;
214
- const appDir = path.join(baseDir, 'app');
154
+ const ext = useTypescript ? 'tsx' : 'js';
215
155
 
216
156
  // Create directories
217
- const dirs = [
218
- appDir,
219
- path.join(projectDir, 'public'),
220
- ];
221
-
222
- for (const dir of dirs) {
223
- fs.mkdirSync(dir, { recursive: true });
224
- }
225
-
226
- spin.stop('Created project structure');
227
-
228
- // Create files
229
- const spin2 = spinner('Creating configuration files...');
157
+ fs.mkdirSync(path.join(projectDir, 'app'), { recursive: true });
158
+ fs.mkdirSync(path.join(projectDir, 'public'), { recursive: true });
230
159
 
231
- // package.json
232
- const ext = useTypescript ? 'ts' : 'js';
160
+ // ============ package.json ============
161
+ const spin1 = spinner('Creating package.json...');
233
162
  const pkg = {
234
163
  name: projectName,
235
164
  version: '0.1.0',
236
165
  private: true,
237
166
  scripts: {
238
- dev: `bun run --hot ${useSrcDir ? 'src/' : ''}server.${ext}`,
239
- build: `bun build ./${useSrcDir ? 'src/' : ''}server.${ext} --outdir ./dist --target bun`,
167
+ dev: 'bun run --hot server.tsx',
168
+ build: 'bun build ./server.tsx --outdir ./dist --target bun',
240
169
  start: 'bun run dist/server.js',
241
- lint: useEslint ? 'eslint . --ext .ts,.tsx' : undefined,
170
+ ...(useEslint && { lint: 'eslint .' }),
242
171
  },
243
172
  dependencies: {},
244
173
  devDependencies: {
245
- ...(useTypescript && { '@types/bun': 'latest', 'typescript': '^5.3.0' }),
246
- ...(useEslint && { 'eslint': '^8.0.0', '@typescript-eslint/eslint-plugin': '^6.0.0', '@typescript-eslint/parser': '^6.0.0' }),
247
- ...(useTailwind && { 'tailwindcss': '^3.4.0', 'postcss': '^8.4.0', 'autoprefixer': '^10.4.0' }),
174
+ ...(useTypescript && { '@types/bun': 'latest', 'typescript': '^5' }),
175
+ ...(useTailwind && { '@tailwindcss/postcss': '^4', 'tailwindcss': '^4' }),
176
+ ...(useEslint && { 'eslint': '^9' }),
248
177
  },
249
178
  };
250
- // Remove undefined scripts
251
- Object.keys(pkg.scripts).forEach(key => {
252
- if (pkg.scripts[key] === undefined) delete pkg.scripts[key];
253
- });
254
179
  fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(pkg, null, 2));
180
+ spin1.stop('Created package.json');
255
181
 
256
- // tsconfig.json
182
+ // ============ tsconfig.json ============
257
183
  if (useTypescript) {
184
+ const spin2 = spinner('Creating tsconfig.json...');
258
185
  const tsconfig = {
259
186
  compilerOptions: {
260
187
  target: 'ES2017',
@@ -271,116 +198,66 @@ ${c.bold}${c.cyan} ╔═══════════════════
271
198
  jsx: 'preserve',
272
199
  incremental: true,
273
200
  types: ['bun-types'],
274
- paths: {
275
- [importAlias]: [useSrcDir ? './src/*' : './*'],
276
- },
277
- baseUrl: '.',
201
+ paths: { '@/*': ['./*'] },
278
202
  },
279
- include: [useSrcDir ? 'src/**/*.ts' : '**/*.ts', useSrcDir ? 'src/**/*.tsx' : '**/*.tsx'],
203
+ include: ['**/*.ts', '**/*.tsx'],
280
204
  exclude: ['node_modules', 'dist'],
281
205
  };
282
206
  fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
207
+ spin2.stop('Created tsconfig.json');
283
208
  }
284
209
 
285
- // ESLint config
286
- if (useEslint) {
287
- const eslintConfig = {
288
- extends: [
289
- 'eslint:recommended',
290
- ...(useTypescript ? ['plugin:@typescript-eslint/recommended'] : []),
291
- ],
292
- parser: useTypescript ? '@typescript-eslint/parser' : undefined,
293
- plugins: useTypescript ? ['@typescript-eslint'] : [],
294
- parserOptions: {
295
- ecmaVersion: 'latest',
296
- sourceType: 'module',
297
- },
298
- rules: {},
299
- ignorePatterns: ['node_modules/', 'dist/'],
300
- };
301
- fs.writeFileSync(path.join(projectDir, '.eslintrc.json'), JSON.stringify(eslintConfig, null, 2));
302
- }
303
-
304
- // Tailwind config
210
+ // ============ postcss.config.mjs ============
305
211
  if (useTailwind) {
306
- const tailwindConfig = `/** @type {import('tailwindcss').Config} */
307
- module.exports = {
308
- content: [
309
- './${useSrcDir ? 'src/' : ''}app/**/*.{js,ts,jsx,tsx,mdx}',
310
- './${useSrcDir ? 'src/' : ''}components/**/*.{js,ts,jsx,tsx,mdx}',
311
- ],
312
- theme: {
313
- extend: {
314
- colors: {
315
- oven: {
316
- 50: '#fff5f2',
317
- 100: '#ffe6de',
318
- 200: '#ffc9b8',
319
- 300: '#ffa088',
320
- 400: '#ff6b35',
321
- 500: '#f7531e',
322
- 600: '#e63d0a',
323
- 700: '#bf3009',
324
- 800: '#99290d',
325
- 900: '#7d2510',
326
- },
327
- },
328
- },
329
- },
330
- plugins: [],
331
- };
332
- `;
333
- fs.writeFileSync(path.join(projectDir, 'tailwind.config.js'), tailwindConfig);
334
-
335
- const postcssConfig = `module.exports = {
212
+ const spin3 = spinner('Creating postcss.config.mjs...');
213
+ fs.writeFileSync(path.join(projectDir, 'postcss.config.mjs'), `const config = {
336
214
  plugins: {
337
- tailwindcss: {},
338
- autoprefixer: {},
215
+ "@tailwindcss/postcss": {},
339
216
  },
340
217
  };
341
- `;
342
- fs.writeFileSync(path.join(projectDir, 'postcss.config.js'), postcssConfig);
343
- }
344
-
345
- // oven.config.ts
346
- const ovenConfig = `${useTypescript ? "import type { OvenConfig } from './app/types';\n\n" : ''}const config${useTypescript ? ': OvenConfig' : ''} = {
347
- // Server configuration
348
- port: 3000,
349
-
350
- // Directories
351
- appDir: '${useSrcDir ? 'src/' : ''}app',
352
- publicDir: 'public',
353
- };
354
218
 
355
219
  export default config;
356
- `;
357
- fs.writeFileSync(path.join(projectDir, `oven.config.${ext}`), ovenConfig);
358
-
359
- spin2.stop('Created configuration files');
220
+ `);
221
+ spin3.stop('Created postcss.config.mjs');
222
+ }
360
223
 
361
- // Create app files
362
- const spin3 = spinner('Creating app files...');
224
+ // ============ eslint.config.mjs ============
225
+ if (useEslint) {
226
+ const spin4 = spinner('Creating eslint.config.mjs...');
227
+ fs.writeFileSync(path.join(projectDir, 'eslint.config.mjs'), `import js from "@eslint/js";
228
+
229
+ export default [
230
+ js.configs.recommended,
231
+ {
232
+ rules: {
233
+ "no-unused-vars": "warn",
234
+ },
235
+ },
236
+ ];
237
+ `);
238
+ spin4.stop('Created eslint.config.mjs');
239
+ }
363
240
 
364
- // globals.css
365
- const globalsCss = useTailwind ? `@tailwind base;
366
- @tailwind components;
367
- @tailwind utilities;
241
+ // ============ app/globals.css ============
242
+ const spin5 = spinner('Creating app/globals.css...');
243
+ const globalsCss = useTailwind ? `@import "tailwindcss";
368
244
 
369
245
  :root {
370
- --foreground-rgb: 0, 0, 0;
371
- --background-rgb: 255, 255, 255;
246
+ --background: #ffffff;
247
+ --foreground: #171717;
372
248
  }
373
249
 
374
250
  @media (prefers-color-scheme: dark) {
375
251
  :root {
376
- --foreground-rgb: 255, 255, 255;
377
- --background-rgb: 10, 10, 10;
252
+ --background: #0a0a0a;
253
+ --foreground: #ededed;
378
254
  }
379
255
  }
380
256
 
381
257
  body {
382
- color: rgb(var(--foreground-rgb));
383
- background: rgb(var(--background-rgb));
258
+ background: var(--background);
259
+ color: var(--foreground);
260
+ font-family: Arial, Helvetica, sans-serif;
384
261
  }
385
262
  ` : `* {
386
263
  margin: 0;
@@ -389,193 +266,161 @@ body {
389
266
  }
390
267
 
391
268
  :root {
392
- --foreground-rgb: 0, 0, 0;
393
- --background-rgb: 255, 255, 255;
394
- --oven-primary: #ff6b35;
395
- --oven-secondary: #f7931e;
269
+ --background: #ffffff;
270
+ --foreground: #171717;
396
271
  }
397
272
 
398
273
  body {
399
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
400
- color: rgb(var(--foreground-rgb));
401
- background: rgb(var(--background-rgb));
402
- }
403
-
404
- a {
405
- color: inherit;
406
- text-decoration: none;
274
+ background: var(--background);
275
+ color: var(--foreground);
276
+ font-family: Arial, Helvetica, sans-serif;
407
277
  }
408
278
  `;
409
- fs.writeFileSync(path.join(appDir, 'globals.css'), globalsCss);
279
+ fs.writeFileSync(path.join(projectDir, 'app', 'globals.css'), globalsCss);
280
+ spin5.stop('Created app/globals.css');
410
281
 
411
- // favicon.ico (placeholder - just create the public dir marker)
412
- fs.writeFileSync(path.join(projectDir, 'public', '.gitkeep'), '');
413
-
414
- // Root Layout
415
- const layoutContent = `import './globals.css';
282
+ // ============ app/layout.tsx ============
283
+ const spin6 = spinner(`Creating app/layout.${ext}...`);
284
+ const layoutContent = useTypescript ? `import "./globals.css";
416
285
 
417
286
  export const metadata = {
418
- title: {
419
- default: '${projectName}',
420
- template: '%s | ${projectName}',
421
- },
422
- description: 'Built with Oven - A Next.js-style framework for Bun',
287
+ title: "Create Oven App",
288
+ description: "Generated by create-oven",
423
289
  };
424
290
 
425
- export default function RootLayout({ children }${useTypescript ? ': { children: string }' : ''}) {
291
+ export default function RootLayout({
292
+ children,
293
+ }: {
294
+ children: string;
295
+ }) {
426
296
  return \`
427
297
  <!DOCTYPE html>
428
298
  <html lang="en">
429
299
  <head>
430
300
  <meta charset="UTF-8">
431
301
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
432
- <link rel="icon" href="/favicon.ico" />
302
+ <title>\${metadata.title}</title>
303
+ <meta name="description" content="\${metadata.description}">
304
+ <link rel="icon" href="/favicon.ico">
433
305
  </head>
434
- <body${useTailwind ? ' class="antialiased"' : ''}>
306
+ <body>
435
307
  \${children}
436
308
  </body>
437
309
  </html>
438
310
  \`;
439
311
  }
440
- `;
441
- fs.writeFileSync(path.join(appDir, `layout.${ext}x`), layoutContent);
312
+ ` : `import "./globals.css";
442
313
 
443
- // Home Page
444
- const homePageContent = `export const metadata = {
445
- title: 'Home',
446
- description: 'Welcome to ${projectName}',
314
+ export const metadata = {
315
+ title: "Create Oven App",
316
+ description: "Generated by create-oven",
447
317
  };
448
318
 
449
- export default function HomePage() {
319
+ export default function RootLayout({ children }) {
450
320
  return \`
451
- <main ${useTailwind ? 'class="flex min-h-screen flex-col items-center justify-center p-24"' : 'style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; padding: 6rem;"'}>
452
- <div ${useTailwind ? 'class="text-center"' : 'style="text-align: center;"'}>
453
- <h1 ${useTailwind ? 'class="text-6xl font-bold mb-4 bg-gradient-to-r from-oven-400 to-oven-500 bg-clip-text text-transparent"' : 'style="font-size: 3.5rem; font-weight: bold; margin-bottom: 1rem; background: linear-gradient(135deg, #ff6b35, #f7931e); -webkit-background-clip: text; -webkit-text-fill-color: transparent;"'}>
454
- Welcome to ${projectName}
455
- </h1>
456
-
457
- <p ${useTailwind ? 'class="text-xl text-gray-600 mb-8"' : 'style="font-size: 1.25rem; color: #666; margin-bottom: 2rem;"'}>
458
- Get started by editing
459
- <code ${useTailwind ? 'class="px-2 py-1 bg-gray-100 rounded text-oven-400 font-mono"' : 'style="padding: 0.25rem 0.5rem; background: #f5f5f5; border-radius: 4px; color: #ff6b35; font-family: monospace;"'}>
460
- ${useSrcDir ? 'src/' : ''}app/page.${ext}x
461
- </code>
462
- </p>
463
-
464
- <div ${useTailwind ? 'class="flex gap-4 flex-wrap justify-center"' : 'style="display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center;"'}>
465
- <a href="https://github.com/oven-ttta/oven-framework" target="_blank" ${useTailwind ? 'class="px-6 py-3 bg-gradient-to-r from-oven-400 to-oven-500 text-white rounded-lg font-medium hover:opacity-90 transition"' : 'style="padding: 0.75rem 1.5rem; background: linear-gradient(135deg, #ff6b35, #f7931e); color: white; border-radius: 8px; font-weight: 500;"'}>
466
- Documentation →
467
- </a>
468
- <a href="https://github.com/oven-ttta/oven-framework" target="_blank" ${useTailwind ? 'class="px-6 py-3 border border-gray-300 rounded-lg font-medium hover:border-gray-400 transition"' : 'style="padding: 0.75rem 1.5rem; border: 1px solid #ddd; border-radius: 8px; font-weight: 500;"'}>
469
- GitHub ⭐
470
- </a>
471
- </div>
472
- </div>
473
-
474
- <div ${useTailwind ? 'class="mt-16 grid grid-cols-1 md:grid-cols-3 gap-6 max-w-4xl"' : 'style="margin-top: 4rem; display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; max-width: 900px;"'}>
475
- <div ${useTailwind ? 'class="p-6 bg-white border border-gray-200 rounded-xl hover:border-oven-400 transition"' : 'style="padding: 1.5rem; background: white; border: 1px solid #eee; border-radius: 12px;"'}>
476
- <h3 ${useTailwind ? 'class="text-lg font-semibold mb-2"' : 'style="font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem;"'}>📁 File-based Routing</h3>
477
- <p ${useTailwind ? 'class="text-gray-600 text-sm"' : 'style="color: #666; font-size: 0.9rem;"'}>
478
- Create routes by adding files to the app directory. Just like Next.js!
479
- </p>
480
- </div>
321
+ <!DOCTYPE html>
322
+ <html lang="en">
323
+ <head>
324
+ <meta charset="UTF-8">
325
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
326
+ <title>\${metadata.title}</title>
327
+ <meta name="description" content="\${metadata.description}">
328
+ <link rel="icon" href="/favicon.ico">
329
+ </head>
330
+ <body>
331
+ \${children}
332
+ </body>
333
+ </html>
334
+ \`;
335
+ }
336
+ `;
337
+ fs.writeFileSync(path.join(projectDir, 'app', `layout.${ext}`), layoutContent);
338
+ spin6.stop(`Created app/layout.${ext}`);
481
339
 
482
- <div ${useTailwind ? 'class="p-6 bg-white border border-gray-200 rounded-xl hover:border-oven-400 transition"' : 'style="padding: 1.5rem; background: white; border: 1px solid #eee; border-radius: 12px;"'}>
483
- <h3 ${useTailwind ? 'class="text-lg font-semibold mb-2"' : 'style="font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem;"'}>⚡ Blazing Fast</h3>
484
- <p ${useTailwind ? 'class="text-gray-600 text-sm"' : 'style="color: #666; font-size: 0.9rem;"'}>
485
- Powered by Bun runtime. Up to 4x faster than Node.js.
340
+ // ============ app/page.tsx ============
341
+ const spin7 = spinner(`Creating app/page.${ext}...`);
342
+ const tailwindClasses = useTailwind;
343
+ const pageContent = `export default function Home() {
344
+ return \`
345
+ <div ${tailwindClasses ? 'class="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black"' : 'style="display: flex; min-height: 100vh; align-items: center; justify-content: center; background: #fafafa;"'}>
346
+ <main ${tailwindClasses ? 'class="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black"' : 'style="display: flex; flex-direction: column; align-items: center; justify-content: center; max-width: 48rem; padding: 8rem 4rem; background: white;"'}>
347
+ <div ${tailwindClasses ? 'class="text-6xl mb-8"' : 'style="font-size: 4rem; margin-bottom: 2rem;"'}>🔥</div>
348
+
349
+ <div ${tailwindClasses ? 'class="flex flex-col items-center gap-6 text-center"' : 'style="display: flex; flex-direction: column; align-items: center; gap: 1.5rem; text-align: center;"'}>
350
+ <h1 ${tailwindClasses ? 'class="text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50"' : 'style="font-size: 1.875rem; font-weight: 600; line-height: 2.5rem; color: black;"'}>
351
+ Welcome to Oven
352
+ </h1>
353
+ <p ${tailwindClasses ? 'class="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400"' : 'style="max-width: 28rem; font-size: 1.125rem; line-height: 2rem; color: #666;"'}>
354
+ Get started by editing <code ${tailwindClasses ? 'class="bg-zinc-100 dark:bg-zinc-800 px-2 py-1 rounded"' : 'style="background: #f5f5f5; padding: 0.25rem 0.5rem; border-radius: 4px;"'}>app/page.${ext}</code>
486
355
  </p>
487
356
  </div>
488
357
 
489
- <div ${useTailwind ? 'class="p-6 bg-white border border-gray-200 rounded-xl hover:border-oven-400 transition"' : 'style="padding: 1.5rem; background: white; border: 1px solid #eee; border-radius: 12px;"'}>
490
- <h3 ${useTailwind ? 'class="text-lg font-semibold mb-2"' : 'style="font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem;"'}>🔌 API Routes</h3>
491
- <p ${useTailwind ? 'class="text-gray-600 text-sm"' : 'style="color: #666; font-size: 0.9rem;"'}>
492
- Build APIs with route handlers. Support GET, POST, PUT, DELETE.
493
- </p>
358
+ <div ${tailwindClasses ? 'class="flex flex-col gap-4 text-base font-medium sm:flex-row mt-8"' : 'style="display: flex; gap: 1rem; margin-top: 2rem;"'}>
359
+ <a
360
+ ${tailwindClasses ? 'class="flex h-12 items-center justify-center gap-2 rounded-full bg-black text-white px-6 hover:bg-zinc-800 transition-colors"' : 'style="display: flex; height: 3rem; align-items: center; justify-content: center; gap: 0.5rem; border-radius: 9999px; background: black; color: white; padding: 0 1.5rem; text-decoration: none;"'}
361
+ href="https://github.com/oven-ttta/oven-framework"
362
+ target="_blank"
363
+ >
364
+ GitHub
365
+ </a>
366
+ <a
367
+ ${tailwindClasses ? 'class="flex h-12 items-center justify-center rounded-full border border-zinc-200 dark:border-zinc-700 px-6 hover:bg-zinc-50 dark:hover:bg-zinc-900 transition-colors"' : 'style="display: flex; height: 3rem; align-items: center; justify-content: center; border-radius: 9999px; border: 1px solid #e5e5e5; padding: 0 1.5rem; text-decoration: none; color: inherit;"'}
368
+ href="https://bun.sh/docs"
369
+ target="_blank"
370
+ >
371
+ Bun Docs
372
+ </a>
494
373
  </div>
495
- </div>
496
- </main>
374
+ </main>
375
+ </div>
497
376
  \`;
498
377
  }
499
378
  `;
500
- fs.writeFileSync(path.join(appDir, `page.${ext}x`), homePageContent);
379
+ fs.writeFileSync(path.join(projectDir, 'app', `page.${ext}`), pageContent);
380
+ spin7.stop(`Created app/page.${ext}`);
501
381
 
502
- // Server file
382
+ // ============ server.tsx ============
383
+ const spin8 = spinner(`Creating server.${ext}...`);
503
384
  const serverContent = `/**
504
- * Oven Server - ${projectName}
505
- * A Next.js-style framework powered by Bun
385
+ * Oven Server
386
+ * Powered by Bun
506
387
  */
507
388
 
508
- const PORT = parseInt(process.env.PORT || '3000');
389
+ const PORT = parseInt(process.env.PORT || "3000");
509
390
 
510
- // Routes map
511
- const routes${useTypescript ? ': Map<string, (req: Request) => Promise<Response>>' : ''} = new Map();
391
+ // Simple router
392
+ const routes = new Map${useTypescript ? '<string, (req: Request) => Promise<Response>>' : ''}();
512
393
 
513
- // Scan and register routes
514
394
  async function scanRoutes() {
515
- const appDir = './${useSrcDir ? 'src/' : ''}app';
516
-
517
- // Scan pages
518
- const pageGlob = new Bun.Glob('**/page.{ts,tsx,js,jsx}');
519
- for await (const file of pageGlob.scan({ cwd: appDir })) {
520
- const routePath = '/' + file
521
- .replace(/\\/page\\.(ts|tsx|js|jsx)$/, '')
522
- .replace(/^page\\.(ts|tsx|js|jsx)$/, '')
523
- .replace(/\\/$/, '') || '/';
524
-
525
- const normalizedPath = routePath === '' ? '/' : routePath;
526
-
527
- routes.set(normalizedPath, async (req${useTypescript ? ': Request' : ''}) => {
528
- try {
529
- const module = await import(\`\${appDir}/\${file}\`);
530
- const content = await module.default({ params: {}, searchParams: {} });
395
+ const appDir = "./app";
531
396
 
532
- // Get layout
533
- let html = content;
534
- try {
535
- const layoutModule = await import(\`\${appDir}/layout.tsx\`);
536
- html = await layoutModule.default({ children: content, params: {} });
537
- } catch {}
538
-
539
- // Metadata
540
- const metadata = module.metadata || {};
541
- const title = typeof metadata.title === 'string'
542
- ? metadata.title
543
- : metadata.title?.default || '${projectName}';
544
-
545
- return new Response(html, {
546
- headers: {
547
- 'Content-Type': 'text/html; charset=utf-8',
548
- },
549
- });
550
- } catch (error) {
551
- console.error('Page error:', error);
552
- return new Response('Internal Server Error', { status: 500 });
553
- }
554
- });
555
- }
397
+ // Scan for page files
398
+ const glob = new Bun.Glob("**/page.{tsx,jsx,ts,js}");
556
399
 
557
- // Scan API routes
558
- const apiGlob = new Bun.Glob('**/route.{ts,js}');
559
- for await (const file of apiGlob.scan({ cwd: appDir })) {
560
- const routePath = '/' + file
561
- .replace(/\\/route\\.(ts|js)$/, '')
562
- .replace(/^route\\.(ts|js)$/, '')
563
- .replace(/\\/$/, '');
400
+ for await (const file of glob.scan({ cwd: appDir })) {
401
+ const routePath = "/" + file
402
+ .replace(/\\/page\\.(tsx|jsx|ts|js)$/, "")
403
+ .replace(/^page\\.(tsx|jsx|ts|js)$/, "")
404
+ .replace(/\\/$/, "") || "/";
564
405
 
565
- routes.set(routePath, async (req${useTypescript ? ': Request' : ''}) => {
406
+ routes.set(routePath === "" ? "/" : routePath, async (req${useTypescript ? ': Request' : ''}) => {
566
407
  const module = await import(\`\${appDir}/\${file}\`);
567
- const method = req.method.toUpperCase();
568
- const handler = module[method];
408
+ const content = await module.default();
569
409
 
570
- if (handler) {
571
- return handler(req);
572
- }
573
- return new Response('Method not allowed', { status: 405 });
410
+ // Wrap with layout
411
+ let html = content;
412
+ try {
413
+ const layout = await import(\`\${appDir}/layout.tsx\`);
414
+ html = await layout.default({ children: content });
415
+ } catch {}
416
+
417
+ return new Response(html, {
418
+ headers: { "Content-Type": "text/html; charset=utf-8" },
419
+ });
574
420
  });
575
421
  }
576
422
  }
577
423
 
578
- // Start server
579
424
  async function main() {
580
425
  await scanRoutes();
581
426
 
@@ -585,94 +430,76 @@ async function main() {
585
430
  const url = new URL(req.url);
586
431
  let pathname = url.pathname;
587
432
 
588
- // Remove trailing slash
589
- if (pathname !== '/' && pathname.endsWith('/')) {
433
+ if (pathname !== "/" && pathname.endsWith("/")) {
590
434
  pathname = pathname.slice(0, -1);
591
435
  }
592
436
 
593
- // Match route
437
+ // Check routes
594
438
  const handler = routes.get(pathname);
595
439
  if (handler) {
596
440
  return handler(req);
597
441
  }
598
442
 
599
443
  // Static files
600
- const publicPath = './public' + pathname;
444
+ const publicPath = "./public" + pathname;
601
445
  const file = Bun.file(publicPath);
602
446
  if (await file.exists()) {
603
447
  return new Response(file);
604
448
  }
605
449
 
606
- // 404
607
- return new Response('Not Found', { status: 404 });
450
+ return new Response("Not Found", { status: 404 });
608
451
  },
609
452
  });
610
453
 
611
454
  console.log(\`
612
- ${c.green}✓${c.reset} Ready in \${Date.now() - startTime}ms
455
+ ${c.green}▲${c.reset} Ready in \${Date.now() - start}ms
613
456
 
614
457
  ${c.dim}➜${c.reset} Local: ${c.cyan}http://localhost:\${PORT}${c.reset}
615
- ${c.dim}➜${c.reset} Network: ${c.cyan}http://0.0.0.0:\${PORT}${c.reset}
616
458
  \`);
617
459
  }
618
460
 
619
- const startTime = Date.now();
620
- main().catch(console.error);
461
+ const start = Date.now();
462
+ main();
621
463
  `;
622
- fs.writeFileSync(path.join(useSrcDir ? path.join(projectDir, 'src') : projectDir, `server.${ext}`), serverContent);
464
+ fs.writeFileSync(path.join(projectDir, `server.${ext}`), serverContent);
465
+ spin8.stop(`Created server.${ext}`);
623
466
 
624
- // .gitignore
625
- const gitignoreContent = `# Dependencies
467
+ // ============ .gitignore ============
468
+ const spin9 = spinner('Creating .gitignore...');
469
+ fs.writeFileSync(path.join(projectDir, '.gitignore'), `# Dependencies
626
470
  node_modules
627
471
  .pnpm-store
628
472
 
629
473
  # Build
630
474
  dist
631
475
  .oven
632
- .next
633
- out
634
476
 
635
- # Environment
477
+ # Env
636
478
  .env
637
479
  .env.local
638
480
  .env.*.local
639
481
 
640
482
  # Logs
641
483
  *.log
642
- npm-debug.log*
643
484
 
644
485
  # OS
645
486
  .DS_Store
646
- Thumbs.db
647
487
 
648
488
  # IDE
649
489
  .vscode
650
490
  .idea
651
- *.swp
652
- *.swo
653
-
654
- # Testing
655
- coverage
656
-
657
- # Misc
658
- *.tsbuildinfo
659
- `;
660
- fs.writeFileSync(path.join(projectDir, '.gitignore'), gitignoreContent);
491
+ `);
492
+ spin9.stop('Created .gitignore');
661
493
 
662
- // README.md
663
- const readmeContent = `# ${projectName}
494
+ // ============ README.md ============
495
+ const spin10 = spinner('Creating README.md...');
496
+ fs.writeFileSync(path.join(projectDir, 'README.md'), `# ${projectName}
664
497
 
665
- This is a [Oven](https://github.com/oven-ttta/oven-framework) project bootstrapped with \`create-oven\`.
498
+ This is an [Oven](https://github.com/oven-ttta/oven-framework) project bootstrapped with \`create-oven\`.
666
499
 
667
500
  ## Getting Started
668
501
 
669
- First, install dependencies:
670
-
671
- \`\`\`bash
672
- bun install
673
- \`\`\`
674
-
675
- Then, run the development server:
502
+ First, run the development server:
676
503
 
677
504
  \`\`\`bash
678
505
  bun run dev
@@ -680,68 +507,37 @@ bun run dev
680
507
 
681
508
  Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
682
509
 
683
- You can start editing the page by modifying \`${useSrcDir ? 'src/' : ''}app/page.${ext}x\`. The page auto-updates as you edit the file.
684
-
685
- ## Project Structure
686
-
687
- \`\`\`
688
- ${projectName}/
689
- ├── ${useSrcDir ? 'src/' : ''}app/
690
- │ ├── layout.${ext}x # Root layout
691
- │ ├── page.${ext}x # Home page (/)
692
- │ └── globals.css # Global styles
693
- ├── public/ # Static files
694
- ${useTailwind ? '├── tailwind.config.js # Tailwind config\n' : ''}├── oven.config.${ext} # Oven config
695
- └── package.json
696
- \`\`\`
510
+ You can start editing the page by modifying \`app/page.${ext}\`. The page auto-updates as you edit the file.
697
511
 
698
512
  ## Learn More
699
513
 
700
- - [Oven Documentation](https://github.com/oven-ttta/oven-framework)
514
+ - [Oven GitHub](https://github.com/oven-ttta/oven-framework)
701
515
  - [Bun Documentation](https://bun.sh/docs)
702
- ${useTailwind ? '- [Tailwind CSS](https://tailwindcss.com/docs)\n' : ''}
703
- ## Deploy
704
-
705
- Deploy your Oven app with Docker or Vercel.
706
-
707
- ### Docker
708
-
709
- \`\`\`bash
710
- docker build -t ${projectName} .
711
- docker run -p 3000:3000 ${projectName}
712
- \`\`\`
713
-
714
- ### Vercel
715
-
716
- \`\`\`bash
717
- vercel --prod
718
- \`\`\`
719
- `;
720
- fs.writeFileSync(path.join(projectDir, 'README.md'), readmeContent);
721
-
722
- spin3.stop('Created app files');
516
+ `);
517
+ spin10.stop('Created README.md');
723
518
 
724
- // Install dependencies (unless --no-install)
519
+ // ============ Install dependencies ============
725
520
  if (!noInstall) {
726
- const packageManager = hasFlag('--use-npm') ? 'npm' : 'bun';
727
- const spin4 = spinner(`Installing dependencies with ${packageManager}...`);
728
-
521
+ console.log();
522
+ const spin11 = spinner('Installing dependencies...');
729
523
  try {
730
- execSync(`${packageManager} install`, {
731
- cwd: projectDir,
732
- stdio: 'pipe',
733
- });
734
- spin4.stop(`Installed dependencies with ${packageManager}`);
735
- } catch (e) {
736
- spin4.fail(`Failed to install dependencies. Run '${packageManager} install' manually.`);
524
+ execSync('bun install', { cwd: projectDir, stdio: 'pipe' });
525
+ spin11.stop('Installed dependencies');
526
+ } catch {
527
+ try {
528
+ execSync('npm install', { cwd: projectDir, stdio: 'pipe' });
529
+ spin11.stop('Installed dependencies');
530
+ } catch {
531
+ spin11.fail('Failed to install. Run "bun install" manually.');
532
+ }
737
533
  }
738
534
  }
739
535
 
740
- // Success message
536
+ // Success
741
537
  console.log(`
742
- ${c.green}${c.bold}Success!${c.reset} Created ${c.cyan}${projectName}${c.reset} at ${projectDir}
538
+ ${c.green}Success!${c.reset} Created ${c.cyan}${projectName}${c.reset} at ${projectDir}
743
539
 
744
- ${c.dim}Inside that directory, you can run several commands:${c.reset}
540
+ Inside that directory, you can run:
745
541
 
746
542
  ${c.cyan}bun run dev${c.reset}
747
543
  Starts the development server.
@@ -750,14 +546,12 @@ ${c.dim}Inside that directory, you can run several commands:${c.reset}
750
546
  Builds the app for production.
751
547
 
752
548
  ${c.cyan}bun run start${c.reset}
753
- Runs the built app in production mode.
549
+ Runs the built app.
754
550
 
755
- ${c.dim}We suggest that you begin by typing:${c.reset}
551
+ We suggest that you begin by typing:
756
552
 
757
553
  ${c.cyan}cd${c.reset} ${projectName}
758
554
  ${c.cyan}bun run dev${c.reset}
759
-
760
- ${c.bold}Happy coding! 🔥${c.reset}
761
555
  `);
762
556
 
763
557
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-oven",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Create a new Oven project - Next.js-style framework for Bun",
5
5
  "type": "module",
6
6
  "bin": {