create-oven 0.2.4 → 0.3.1

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 +311 -439
  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.4');
91
+ console.log('create-oven v0.3.1');
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.4
98
+ ║ 🔥 Create Oven App v0.3.1
142
99
  ║ ║
143
100
  ║ Next.js-style framework for Bun ║
144
101
  ║ ║
@@ -148,113 +105,93 @@ ${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
- }
157
+ fs.mkdirSync(path.join(projectDir, 'app'), { recursive: true });
158
+ fs.mkdirSync(path.join(projectDir, 'public'), { recursive: true });
225
159
 
226
- spin.stop('Created project structure');
160
+ // ============ public/oven.svg ============
161
+ fs.writeFileSync(path.join(projectDir, 'public', 'oven.svg'), `<svg width="100" height="24" viewBox="0 0 100 24" fill="none" xmlns="http://www.w3.org/2000/svg">
162
+ <text x="0" y="20" font-family="system-ui, sans-serif" font-size="20" font-weight="bold" fill="currentColor">🔥 Oven</text>
163
+ </svg>`);
227
164
 
228
- // Create files
229
- const spin2 = spinner('Creating configuration files...');
165
+ // ============ public/github.svg ============
166
+ fs.writeFileSync(path.join(projectDir, 'public', 'github.svg'), `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
167
+ <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
168
+ </svg>`);
230
169
 
231
- // package.json
232
- const ext = useTypescript ? 'tsx' : 'js';
170
+ // ============ package.json ============
171
+ const spin1 = spinner('Creating package.json...');
233
172
  const pkg = {
234
173
  name: projectName,
235
174
  version: '0.1.0',
236
175
  private: true,
237
176
  scripts: {
238
- dev: `bun run --hot ${useSrcDir ? 'src/' : ''}server.${ext}`,
239
- build: `bun build ./${useSrcDir ? 'src/' : ''}server.${ext} --outdir ./dist --target bun`,
177
+ dev: 'bun run --hot server.tsx',
178
+ build: 'bun build ./server.tsx --outdir ./dist --target bun',
240
179
  start: 'bun run dist/server.js',
241
- lint: useEslint ? 'eslint . --ext .tsx' : undefined,
180
+ ...(useEslint && { lint: 'eslint .' }),
242
181
  },
243
182
  dependencies: {},
244
183
  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' }),
184
+ ...(useTypescript && { '@types/bun': 'latest', 'typescript': '^5' }),
185
+ ...(useTailwind && { '@tailwindcss/postcss': '^4', 'tailwindcss': '^4' }),
186
+ ...(useEslint && { 'eslint': '^9' }),
248
187
  },
249
188
  };
250
- // Remove undefined scripts
251
- Object.keys(pkg.scripts).forEach(key => {
252
- if (pkg.scripts[key] === undefined) delete pkg.scripts[key];
253
- });
254
189
  fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(pkg, null, 2));
190
+ spin1.stop('Created package.json');
255
191
 
256
- // tsconfig.json
192
+ // ============ tsconfig.json ============
257
193
  if (useTypescript) {
194
+ const spin2 = spinner('Creating tsconfig.json...');
258
195
  const tsconfig = {
259
196
  compilerOptions: {
260
197
  target: 'ES2017',
@@ -271,116 +208,66 @@ ${c.bold}${c.cyan} ╔═══════════════════
271
208
  jsx: 'preserve',
272
209
  incremental: true,
273
210
  types: ['bun-types'],
274
- paths: {
275
- [importAlias]: [useSrcDir ? './src/*' : './*'],
276
- },
277
- baseUrl: '.',
211
+ paths: { '@/*': ['./*'] },
278
212
  },
279
- include: [useSrcDir ? 'src/**/*.tsx' : '**/*.tsx'],
213
+ include: ['**/*.ts', '**/*.tsx'],
280
214
  exclude: ['node_modules', 'dist'],
281
215
  };
282
216
  fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
217
+ spin2.stop('Created tsconfig.json');
283
218
  }
284
219
 
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
220
+ // ============ postcss.config.mjs ============
305
221
  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 = {
222
+ const spin3 = spinner('Creating postcss.config.mjs...');
223
+ fs.writeFileSync(path.join(projectDir, 'postcss.config.mjs'), `const config = {
336
224
  plugins: {
337
- tailwindcss: {},
338
- autoprefixer: {},
225
+ "@tailwindcss/postcss": {},
339
226
  },
340
227
  };
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
228
 
355
229
  export default config;
356
- `;
357
- fs.writeFileSync(path.join(projectDir, `oven.config.${ext}`), ovenConfig);
358
-
359
- spin2.stop('Created configuration files');
230
+ `);
231
+ spin3.stop('Created postcss.config.mjs');
232
+ }
360
233
 
361
- // Create app files
362
- const spin3 = spinner('Creating app files...');
234
+ // ============ eslint.config.mjs ============
235
+ if (useEslint) {
236
+ const spin4 = spinner('Creating eslint.config.mjs...');
237
+ fs.writeFileSync(path.join(projectDir, 'eslint.config.mjs'), `import js from "@eslint/js";
238
+
239
+ export default [
240
+ js.configs.recommended,
241
+ {
242
+ rules: {
243
+ "no-unused-vars": "warn",
244
+ },
245
+ },
246
+ ];
247
+ `);
248
+ spin4.stop('Created eslint.config.mjs');
249
+ }
363
250
 
364
- // globals.css
365
- const globalsCss = useTailwind ? `@tailwind base;
366
- @tailwind components;
367
- @tailwind utilities;
251
+ // ============ app/globals.css ============
252
+ const spin5 = spinner('Creating app/globals.css...');
253
+ const globalsCss = useTailwind ? `@import "tailwindcss";
368
254
 
369
255
  :root {
370
- --foreground-rgb: 0, 0, 0;
371
- --background-rgb: 255, 255, 255;
256
+ --background: #ffffff;
257
+ --foreground: #171717;
372
258
  }
373
259
 
374
260
  @media (prefers-color-scheme: dark) {
375
261
  :root {
376
- --foreground-rgb: 255, 255, 255;
377
- --background-rgb: 10, 10, 10;
262
+ --background: #0a0a0a;
263
+ --foreground: #ededed;
378
264
  }
379
265
  }
380
266
 
381
267
  body {
382
- color: rgb(var(--foreground-rgb));
383
- background: rgb(var(--background-rgb));
268
+ background: var(--background);
269
+ color: var(--foreground);
270
+ font-family: Arial, Helvetica, sans-serif;
384
271
  }
385
272
  ` : `* {
386
273
  margin: 0;
@@ -389,193 +276,229 @@ body {
389
276
  }
390
277
 
391
278
  :root {
392
- --foreground-rgb: 0, 0, 0;
393
- --background-rgb: 255, 255, 255;
394
- --oven-primary: #ff6b35;
395
- --oven-secondary: #f7931e;
279
+ --background: #ffffff;
280
+ --foreground: #171717;
396
281
  }
397
282
 
398
283
  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;
284
+ background: var(--background);
285
+ color: var(--foreground);
286
+ font-family: Arial, Helvetica, sans-serif;
407
287
  }
408
288
  `;
409
- fs.writeFileSync(path.join(appDir, 'globals.css'), globalsCss);
410
-
411
- // favicon.ico (placeholder - just create the public dir marker)
412
- fs.writeFileSync(path.join(projectDir, 'public', '.gitkeep'), '');
289
+ fs.writeFileSync(path.join(projectDir, 'app', 'globals.css'), globalsCss);
290
+ spin5.stop('Created app/globals.css');
413
291
 
414
- // Root Layout
415
- const layoutContent = `import './globals.css';
292
+ // ============ app/layout.tsx ============
293
+ const spin6 = spinner(`Creating app/layout.${ext}...`);
294
+ const layoutContent = useTypescript ? `import "./globals.css";
416
295
 
417
- 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',
296
+ export const metadata${useTypescript ? ': { title: string; description: string }' : ''} = {
297
+ title: "Create Oven App",
298
+ description: "Generated by create-oven",
423
299
  };
424
300
 
425
- export default function RootLayout({ children }${useTypescript ? ': { children: string }' : ''}) {
426
- return \`
427
- <!DOCTYPE html>
301
+ export default function RootLayout({
302
+ children,
303
+ }: Readonly<{
304
+ children: string;
305
+ }>) {
306
+ return (
307
+ \`<!DOCTYPE html>
428
308
  <html lang="en">
429
309
  <head>
430
- <meta charset="UTF-8">
431
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
310
+ <meta charset="UTF-8" />
311
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
312
+ <title>\${metadata.title}</title>
313
+ <meta name="description" content="\${metadata.description}" />
432
314
  <link rel="icon" href="/favicon.ico" />
315
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
316
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
317
+ <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono&display=swap" rel="stylesheet" />
433
318
  </head>
434
- <body${useTailwind ? ' class="antialiased"' : ''}>
319
+ <body class="antialiased" style="font-family: 'Geist', sans-serif;">
435
320
  \${children}
436
321
  </body>
437
- </html>
438
- \`;
322
+ </html>\`
323
+ );
439
324
  }
440
- `;
441
- fs.writeFileSync(path.join(appDir, `layout.${ext}`), layoutContent);
325
+ ` : `import "./globals.css";
442
326
 
443
- // Home Page
444
- const homePageContent = `export const metadata = {
445
- title: 'Home',
446
- description: 'Welcome to ${projectName}',
327
+ export const metadata = {
328
+ title: "Create Oven App",
329
+ description: "Generated by create-oven",
447
330
  };
448
331
 
449
- export default function HomePage() {
450
- 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}
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 →
332
+ export default function RootLayout({ children }) {
333
+ return (
334
+ \`<!DOCTYPE html>
335
+ <html lang="en">
336
+ <head>
337
+ <meta charset="UTF-8" />
338
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
339
+ <title>\${metadata.title}</title>
340
+ <meta name="description" content="\${metadata.description}" />
341
+ <link rel="icon" href="/favicon.ico" />
342
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
343
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
344
+ <link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono&display=swap" rel="stylesheet" />
345
+ </head>
346
+ <body class="antialiased" style="font-family: 'Geist', sans-serif;">
347
+ \${children}
348
+ </body>
349
+ </html>\`
350
+ );
351
+ }
352
+ `;
353
+ fs.writeFileSync(path.join(projectDir, 'app', `layout.${ext}`), layoutContent);
354
+ spin6.stop(`Created app/layout.${ext}`);
355
+
356
+ // ============ app/page.tsx ============
357
+ const spin7 = spinner(`Creating app/page.${ext}...`);
358
+ const pageContent = useTailwind ? `export default function Home() {
359
+ return (
360
+ \`<div class="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
361
+ <main 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 sm:items-start">
362
+ <img
363
+ class="dark:invert"
364
+ src="/oven.svg"
365
+ alt="Oven logo"
366
+ width="100"
367
+ height="24"
368
+ />
369
+ <div class="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
370
+ <h1 class="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
371
+ Get started by editing app/page.${ext}
372
+ </h1>
373
+ <p class="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
374
+ Looking for a starting point? Head over to the{" "}
375
+ <a
376
+ href="https://github.com/oven-ttta/oven-framework"
377
+ class="font-medium text-zinc-950 dark:text-zinc-50"
378
+ >
379
+ Documentation
380
+ </a>{" "}
381
+ or{" "}
382
+ <a
383
+ href="https://bun.sh/docs"
384
+ class="font-medium text-zinc-950 dark:text-zinc-50"
385
+ >
386
+ Bun Docs
387
+ </a>.
388
+ </p>
389
+ </div>
390
+ <div class="flex flex-col gap-4 text-base font-medium sm:flex-row">
391
+ <a
392
+ class="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
393
+ href="https://github.com/oven-ttta/oven-framework"
394
+ target="_blank"
395
+ rel="noopener noreferrer"
396
+ style="background: #171717; color: white;"
397
+ >
398
+ <img
399
+ class="dark:invert"
400
+ src="/github.svg"
401
+ alt="GitHub"
402
+ width="16"
403
+ height="16"
404
+ />
405
+ GitHub
467
406
  </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
407
+ <a
408
+ class="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
409
+ href="https://bun.sh/docs"
410
+ target="_blank"
411
+ rel="noopener noreferrer"
412
+ >
413
+ Bun Docs
470
414
  </a>
471
415
  </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>
481
-
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.
416
+ </main>
417
+ </div>\`
418
+ );
419
+ }
420
+ ` : `export default function Home() {
421
+ return (
422
+ \`<div style="display: flex; min-height: 100vh; align-items: center; justify-content: center; background: #fafafa;">
423
+ <main style="display: flex; flex-direction: column; align-items: center; justify-content: space-between; max-width: 48rem; padding: 8rem 4rem; background: white;">
424
+ <div style="font-size: 4rem; margin-bottom: 2rem;">🔥</div>
425
+ <div style="display: flex; flex-direction: column; align-items: center; gap: 1.5rem; text-align: center;">
426
+ <h1 style="font-size: 1.875rem; font-weight: 600; line-height: 2.5rem; color: black;">
427
+ Get started by editing app/page.${ext}
428
+ </h1>
429
+ <p style="max-width: 28rem; font-size: 1.125rem; line-height: 2rem; color: #666;">
430
+ Looking for a starting point? Head over to the
431
+ <a href="https://github.com/oven-ttta/oven-framework" style="font-weight: 500; color: black;">Documentation</a>
432
+ or
433
+ <a href="https://bun.sh/docs" style="font-weight: 500; color: black;">Bun Docs</a>.
486
434
  </p>
487
435
  </div>
488
-
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>
436
+ <div style="display: flex; gap: 1rem; margin-top: 2rem;">
437
+ <a
438
+ href="https://github.com/oven-ttta/oven-framework"
439
+ target="_blank"
440
+ 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;"
441
+ >
442
+ GitHub
443
+ </a>
444
+ <a
445
+ href="https://bun.sh/docs"
446
+ target="_blank"
447
+ 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;"
448
+ >
449
+ Bun Docs
450
+ </a>
494
451
  </div>
495
- </div>
496
- </main>
497
- \`;
452
+ </main>
453
+ </div>\`
454
+ );
498
455
  }
499
456
  `;
500
- fs.writeFileSync(path.join(appDir, `page.${ext}`), homePageContent);
457
+ fs.writeFileSync(path.join(projectDir, 'app', `page.${ext}`), pageContent);
458
+ spin7.stop(`Created app/page.${ext}`);
501
459
 
502
- // Server file
460
+ // ============ server.tsx ============
461
+ const spin8 = spinner(`Creating server.${ext}...`);
503
462
  const serverContent = `/**
504
- * Oven Server - ${projectName}
505
- * A Next.js-style framework powered by Bun
463
+ * Oven Server
464
+ * Powered by Bun
506
465
  */
507
466
 
508
- const PORT = parseInt(process.env.PORT || '3000');
467
+ const PORT = parseInt(process.env.PORT || "3000");
509
468
 
510
- // Routes map
511
- const routes${useTypescript ? ': Map<string, (req: Request) => Promise<Response>>' : ''} = new Map();
469
+ // Simple router
470
+ const routes = new Map${useTypescript ? '<string, (req: Request) => Promise<Response>>' : ''}();
512
471
 
513
- // Scan and register routes
514
472
  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: {} });
473
+ const appDir = "./app";
531
474
 
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
- }
475
+ // Scan for page files
476
+ const glob = new Bun.Glob("**/page.{tsx,jsx,ts,js}");
556
477
 
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(/\\/$/, '');
478
+ for await (const file of glob.scan({ cwd: appDir })) {
479
+ const routePath = "/" + file
480
+ .replace(/\\/page\\.(tsx|jsx|ts|js)$/, "")
481
+ .replace(/^page\\.(tsx|jsx|ts|js)$/, "")
482
+ .replace(/\\/$/, "") || "/";
564
483
 
565
- routes.set(routePath, async (req${useTypescript ? ': Request' : ''}) => {
484
+ routes.set(routePath === "" ? "/" : routePath, async (req${useTypescript ? ': Request' : ''}) => {
566
485
  const module = await import(\`\${appDir}/\${file}\`);
567
- const method = req.method.toUpperCase();
568
- const handler = module[method];
486
+ const content = await module.default();
569
487
 
570
- if (handler) {
571
- return handler(req);
572
- }
573
- return new Response('Method not allowed', { status: 405 });
488
+ // Wrap with layout
489
+ let html = content;
490
+ try {
491
+ const layout = await import(\`\${appDir}/layout.tsx\`);
492
+ html = await layout.default({ children: content });
493
+ } catch {}
494
+
495
+ return new Response(html, {
496
+ headers: { "Content-Type": "text/html; charset=utf-8" },
497
+ });
574
498
  });
575
499
  }
576
500
  }
577
501
 
578
- // Start server
579
502
  async function main() {
580
503
  await scanRoutes();
581
504
 
@@ -585,94 +508,76 @@ async function main() {
585
508
  const url = new URL(req.url);
586
509
  let pathname = url.pathname;
587
510
 
588
- // Remove trailing slash
589
- if (pathname !== '/' && pathname.endsWith('/')) {
511
+ if (pathname !== "/" && pathname.endsWith("/")) {
590
512
  pathname = pathname.slice(0, -1);
591
513
  }
592
514
 
593
- // Match route
515
+ // Check routes
594
516
  const handler = routes.get(pathname);
595
517
  if (handler) {
596
518
  return handler(req);
597
519
  }
598
520
 
599
521
  // Static files
600
- const publicPath = './public' + pathname;
522
+ const publicPath = "./public" + pathname;
601
523
  const file = Bun.file(publicPath);
602
524
  if (await file.exists()) {
603
525
  return new Response(file);
604
526
  }
605
527
 
606
- // 404
607
- return new Response('Not Found', { status: 404 });
528
+ return new Response("Not Found", { status: 404 });
608
529
  },
609
530
  });
610
531
 
611
532
  console.log(\`
612
- ${c.green}✓${c.reset} Ready in \${Date.now() - startTime}ms
533
+ ${c.green}▲${c.reset} Ready in \${Date.now() - start}ms
613
534
 
614
535
  ${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
536
  \`);
617
537
  }
618
538
 
619
- const startTime = Date.now();
620
- main().catch(console.error);
539
+ const start = Date.now();
540
+ main();
621
541
  `;
622
- fs.writeFileSync(path.join(useSrcDir ? path.join(projectDir, 'src') : projectDir, `server.${ext}`), serverContent);
542
+ fs.writeFileSync(path.join(projectDir, `server.${ext}`), serverContent);
543
+ spin8.stop(`Created server.${ext}`);
623
544
 
624
- // .gitignore
625
- const gitignoreContent = `# Dependencies
545
+ // ============ .gitignore ============
546
+ const spin9 = spinner('Creating .gitignore...');
547
+ fs.writeFileSync(path.join(projectDir, '.gitignore'), `# Dependencies
626
548
  node_modules
627
549
  .pnpm-store
628
550
 
629
551
  # Build
630
552
  dist
631
553
  .oven
632
- .next
633
- out
634
554
 
635
- # Environment
555
+ # Env
636
556
  .env
637
557
  .env.local
638
558
  .env.*.local
639
559
 
640
560
  # Logs
641
561
  *.log
642
- npm-debug.log*
643
562
 
644
563
  # OS
645
564
  .DS_Store
646
- Thumbs.db
647
565
 
648
566
  # IDE
649
567
  .vscode
650
568
  .idea
651
- *.swp
652
- *.swo
653
-
654
- # Testing
655
- coverage
656
-
657
- # Misc
658
- *.tsbuildinfo
659
- `;
660
- fs.writeFileSync(path.join(projectDir, '.gitignore'), gitignoreContent);
569
+ `);
570
+ spin9.stop('Created .gitignore');
661
571
 
662
- // README.md
663
- const readmeContent = `# ${projectName}
572
+ // ============ README.md ============
573
+ const spin10 = spinner('Creating README.md...');
574
+ fs.writeFileSync(path.join(projectDir, 'README.md'), `# ${projectName}
664
575
 
665
- This is a [Oven](https://github.com/oven-ttta/oven-framework) project bootstrapped with \`create-oven\`.
576
+ This is an [Oven](https://github.com/oven-ttta/oven-framework) project bootstrapped with \`create-oven\`.
666
577
 
667
578
  ## Getting Started
668
579
 
669
- First, install dependencies:
670
-
671
- \`\`\`bash
672
- bun install
673
- \`\`\`
674
-
675
- Then, run the development server:
580
+ First, run the development server:
676
581
 
677
582
  \`\`\`bash
678
583
  bun run dev
@@ -680,68 +585,37 @@ bun run dev
680
585
 
681
586
  Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
682
587
 
683
- You can start editing the page by modifying \`${useSrcDir ? 'src/' : ''}app/page.${ext}\`. The page auto-updates as you edit the file.
684
-
685
- ## Project Structure
686
-
687
- \`\`\`
688
- ${projectName}/
689
- ├── ${useSrcDir ? 'src/' : ''}app/
690
- │ ├── layout.${ext} # Root layout
691
- │ ├── page.${ext} # 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
- \`\`\`
588
+ You can start editing the page by modifying \`app/page.${ext}\`. The page auto-updates as you edit the file.
697
589
 
698
590
  ## Learn More
699
591
 
700
- - [Oven Documentation](https://github.com/oven-ttta/oven-framework)
592
+ - [Oven GitHub](https://github.com/oven-ttta/oven-framework)
701
593
  - [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');
594
+ `);
595
+ spin10.stop('Created README.md');
723
596
 
724
- // Install dependencies (unless --no-install)
597
+ // ============ Install dependencies ============
725
598
  if (!noInstall) {
726
- const packageManager = hasFlag('--use-npm') ? 'npm' : 'bun';
727
- const spin4 = spinner(`Installing dependencies with ${packageManager}...`);
728
-
599
+ console.log();
600
+ const spin11 = spinner('Installing dependencies...');
729
601
  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.`);
602
+ execSync('bun install', { cwd: projectDir, stdio: 'pipe' });
603
+ spin11.stop('Installed dependencies');
604
+ } catch {
605
+ try {
606
+ execSync('npm install', { cwd: projectDir, stdio: 'pipe' });
607
+ spin11.stop('Installed dependencies');
608
+ } catch {
609
+ spin11.fail('Failed to install. Run "bun install" manually.');
610
+ }
737
611
  }
738
612
  }
739
613
 
740
- // Success message
614
+ // Success
741
615
  console.log(`
742
- ${c.green}${c.bold}Success!${c.reset} Created ${c.cyan}${projectName}${c.reset} at ${projectDir}
616
+ ${c.green}Success!${c.reset} Created ${c.cyan}${projectName}${c.reset} at ${projectDir}
743
617
 
744
- ${c.dim}Inside that directory, you can run several commands:${c.reset}
618
+ Inside that directory, you can run:
745
619
 
746
620
  ${c.cyan}bun run dev${c.reset}
747
621
  Starts the development server.
@@ -750,14 +624,12 @@ ${c.dim}Inside that directory, you can run several commands:${c.reset}
750
624
  Builds the app for production.
751
625
 
752
626
  ${c.cyan}bun run start${c.reset}
753
- Runs the built app in production mode.
627
+ Runs the built app.
754
628
 
755
- ${c.dim}We suggest that you begin by typing:${c.reset}
629
+ We suggest that you begin by typing:
756
630
 
757
631
  ${c.cyan}cd${c.reset} ${projectName}
758
632
  ${c.cyan}bun run dev${c.reset}
759
-
760
- ${c.bold}Happy coding! 🔥${c.reset}
761
633
  `);
762
634
 
763
635
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-oven",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "description": "Create a new Oven project - Next.js-style framework for Bun",
5
5
  "type": "module",
6
6
  "bin": {