create-oven 0.1.0 → 0.2.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.
- package/index.js +741 -360
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* create-oven - Create a new Oven project
|
|
5
|
+
* Like create-next-app but for Bun
|
|
5
6
|
*
|
|
6
7
|
* Usage:
|
|
7
8
|
* npx create-oven my-app
|
|
@@ -10,199 +11,678 @@
|
|
|
10
11
|
|
|
11
12
|
import fs from 'fs';
|
|
12
13
|
import path from 'path';
|
|
14
|
+
import readline from 'readline';
|
|
13
15
|
import { fileURLToPath } from 'url';
|
|
16
|
+
import { execSync } from 'child_process';
|
|
14
17
|
|
|
15
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
// Colors for terminal output
|
|
21
|
+
const c = {
|
|
18
22
|
reset: '\x1b[0m',
|
|
19
|
-
|
|
23
|
+
bold: '\x1b[1m',
|
|
24
|
+
dim: '\x1b[2m',
|
|
20
25
|
red: '\x1b[31m',
|
|
21
26
|
green: '\x1b[32m',
|
|
22
27
|
yellow: '\x1b[33m',
|
|
23
28
|
blue: '\x1b[34m',
|
|
29
|
+
magenta: '\x1b[35m',
|
|
24
30
|
cyan: '\x1b[36m',
|
|
31
|
+
white: '\x1b[37m',
|
|
32
|
+
bgBlue: '\x1b[44m',
|
|
25
33
|
};
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
35
|
+
// Create readline interface for prompts
|
|
36
|
+
function createPrompt() {
|
|
37
|
+
return readline.createInterface({
|
|
38
|
+
input: process.stdin,
|
|
39
|
+
output: process.stdout,
|
|
40
|
+
});
|
|
29
41
|
}
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
// Ask a yes/no question
|
|
44
|
+
async function askYesNo(rl, question, defaultValue = true) {
|
|
45
|
+
const defaultText = defaultValue ? 'Yes' : 'No';
|
|
46
|
+
const hint = defaultValue ? '(Y/n)' : '(y/N)';
|
|
47
|
+
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
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
|
+
});
|
|
33
57
|
}
|
|
34
58
|
|
|
35
|
-
|
|
36
|
-
|
|
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);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
37
68
|
}
|
|
38
69
|
|
|
39
|
-
|
|
40
|
-
|
|
70
|
+
// Display a spinner
|
|
71
|
+
function spinner(text) {
|
|
72
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
73
|
+
let i = 0;
|
|
74
|
+
const interval = setInterval(() => {
|
|
75
|
+
process.stdout.write(`\r${c.cyan}${frames[i]}${c.reset} ${text}`);
|
|
76
|
+
i = (i + 1) % frames.length;
|
|
77
|
+
}, 80);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
stop: (finalText) => {
|
|
81
|
+
clearInterval(interval);
|
|
82
|
+
process.stdout.write(`\r${c.green}✓${c.reset} ${finalText || text}\n`);
|
|
83
|
+
},
|
|
84
|
+
fail: (finalText) => {
|
|
85
|
+
clearInterval(interval);
|
|
86
|
+
process.stdout.write(`\r${c.red}✗${c.reset} ${finalText || text}\n`);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
41
89
|
}
|
|
42
90
|
|
|
91
|
+
// Main function
|
|
43
92
|
async function main() {
|
|
44
93
|
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
94
|
|
|
55
|
-
|
|
95
|
+
// Show help
|
|
96
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
97
|
+
console.log(`
|
|
98
|
+
${c.bold}${c.cyan}create-oven${c.reset} - Create a new Oven project
|
|
99
|
+
|
|
100
|
+
${c.yellow}Usage:${c.reset}
|
|
101
|
+
npx create-oven [project-name] [options]
|
|
102
|
+
bunx create-oven [project-name] [options]
|
|
103
|
+
|
|
104
|
+
${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
|
|
117
|
+
--no-install Skip installing dependencies
|
|
118
|
+
-y, --yes Use defaults without prompts
|
|
119
|
+
-h, --help Show this help message
|
|
120
|
+
-v, --version Show version
|
|
121
|
+
|
|
122
|
+
${c.yellow}Examples:${c.reset}
|
|
56
123
|
npx create-oven my-app
|
|
57
|
-
|
|
124
|
+
npx create-oven my-app --tailwind --eslint
|
|
125
|
+
bunx create-oven my-app --src-dir
|
|
58
126
|
|
|
59
|
-
${
|
|
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}
|
|
127
|
+
${c.cyan}Learn more: https://github.com/oven-ttta/oven-framework${c.reset}
|
|
64
128
|
`);
|
|
65
|
-
process.exit(
|
|
129
|
+
process.exit(0);
|
|
66
130
|
}
|
|
67
131
|
|
|
68
|
-
|
|
69
|
-
|
|
132
|
+
// Show version
|
|
133
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
134
|
+
console.log('create-oven v0.2.1');
|
|
70
135
|
process.exit(0);
|
|
71
136
|
}
|
|
72
137
|
|
|
73
|
-
log(`
|
|
74
|
-
|
|
75
|
-
|
|
138
|
+
console.log(`
|
|
139
|
+
${c.bold}${c.cyan} ╔═══════════════════════════════════════╗
|
|
140
|
+
║ ║
|
|
141
|
+
║ 🔥 Create Oven App v0.2.1 ║
|
|
142
|
+
║ ║
|
|
143
|
+
║ Next.js-style framework for Bun ║
|
|
144
|
+
║ ║
|
|
145
|
+
╚═══════════════════════════════════════╝${c.reset}
|
|
146
|
+
`);
|
|
147
|
+
|
|
148
|
+
const rl = createPrompt();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Get project name
|
|
152
|
+
let projectName = args.find(arg => !arg.startsWith('-'));
|
|
153
|
+
|
|
154
|
+
if (!projectName) {
|
|
155
|
+
projectName = await askText(rl, 'What is your project named?', 'my-app');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const projectDir = path.join(process.cwd(), projectName);
|
|
159
|
+
|
|
160
|
+
// Check if directory exists
|
|
161
|
+
if (fs.existsSync(projectDir)) {
|
|
162
|
+
console.log(`\n${c.red}✗${c.reset} Directory "${projectName}" already exists!`);
|
|
163
|
+
rl.close();
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
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
|
+
console.log();
|
|
174
|
+
|
|
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') :
|
|
178
|
+
skipPrompts ? true :
|
|
179
|
+
await askYesNo(rl, 'Would you like to use TypeScript?', true);
|
|
180
|
+
|
|
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') :
|
|
188
|
+
skipPrompts ? true :
|
|
189
|
+
await askYesNo(rl, 'Would you like to use Tailwind CSS?', true);
|
|
190
|
+
|
|
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') :
|
|
198
|
+
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?', '@/*'));
|
|
204
|
+
|
|
205
|
+
rl.close();
|
|
206
|
+
|
|
207
|
+
console.log();
|
|
208
|
+
|
|
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');
|
|
215
|
+
|
|
216
|
+
// Create directories
|
|
217
|
+
const dirs = [
|
|
218
|
+
appDir,
|
|
219
|
+
path.join(appDir, 'api', 'hello'),
|
|
220
|
+
path.join(appDir, 'about'),
|
|
221
|
+
path.join(projectDir, 'public'),
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
for (const dir of dirs) {
|
|
225
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
spin.stop('Created project structure');
|
|
229
|
+
|
|
230
|
+
// Create files
|
|
231
|
+
const spin2 = spinner('Creating configuration files...');
|
|
232
|
+
|
|
233
|
+
// package.json
|
|
234
|
+
const ext = useTypescript ? 'ts' : 'js';
|
|
235
|
+
const pkg = {
|
|
236
|
+
name: projectName,
|
|
237
|
+
version: '0.1.0',
|
|
238
|
+
private: true,
|
|
239
|
+
scripts: {
|
|
240
|
+
dev: `bun run --hot ${useSrcDir ? 'src/' : ''}server.${ext}`,
|
|
241
|
+
build: `bun build ./${useSrcDir ? 'src/' : ''}server.${ext} --outdir ./dist --target bun`,
|
|
242
|
+
start: 'bun run dist/server.js',
|
|
243
|
+
lint: useEslint ? 'eslint . --ext .ts,.tsx' : undefined,
|
|
244
|
+
},
|
|
245
|
+
dependencies: {},
|
|
246
|
+
devDependencies: {
|
|
247
|
+
...(useTypescript && { '@types/bun': 'latest', 'typescript': '^5.3.0' }),
|
|
248
|
+
...(useEslint && { 'eslint': '^8.0.0', '@typescript-eslint/eslint-plugin': '^6.0.0', '@typescript-eslint/parser': '^6.0.0' }),
|
|
249
|
+
...(useTailwind && { 'tailwindcss': '^3.4.0', 'postcss': '^8.4.0', 'autoprefixer': '^10.4.0' }),
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
// Remove undefined scripts
|
|
253
|
+
Object.keys(pkg.scripts).forEach(key => {
|
|
254
|
+
if (pkg.scripts[key] === undefined) delete pkg.scripts[key];
|
|
255
|
+
});
|
|
256
|
+
fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify(pkg, null, 2));
|
|
257
|
+
|
|
258
|
+
// tsconfig.json
|
|
259
|
+
if (useTypescript) {
|
|
260
|
+
const tsconfig = {
|
|
261
|
+
compilerOptions: {
|
|
262
|
+
target: 'ES2017',
|
|
263
|
+
lib: ['dom', 'dom.iterable', 'esnext'],
|
|
264
|
+
allowJs: true,
|
|
265
|
+
skipLibCheck: true,
|
|
266
|
+
strict: true,
|
|
267
|
+
noEmit: true,
|
|
268
|
+
esModuleInterop: true,
|
|
269
|
+
module: 'esnext',
|
|
270
|
+
moduleResolution: 'bundler',
|
|
271
|
+
resolveJsonModule: true,
|
|
272
|
+
isolatedModules: true,
|
|
273
|
+
jsx: 'preserve',
|
|
274
|
+
incremental: true,
|
|
275
|
+
types: ['bun-types'],
|
|
276
|
+
paths: {
|
|
277
|
+
[importAlias]: [useSrcDir ? './src/*' : './*'],
|
|
278
|
+
},
|
|
279
|
+
baseUrl: '.',
|
|
280
|
+
},
|
|
281
|
+
include: [useSrcDir ? 'src/**/*.ts' : '**/*.ts', useSrcDir ? 'src/**/*.tsx' : '**/*.tsx'],
|
|
282
|
+
exclude: ['node_modules', 'dist'],
|
|
283
|
+
};
|
|
284
|
+
fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ESLint config
|
|
288
|
+
if (useEslint) {
|
|
289
|
+
const eslintConfig = {
|
|
290
|
+
extends: [
|
|
291
|
+
'eslint:recommended',
|
|
292
|
+
...(useTypescript ? ['plugin:@typescript-eslint/recommended'] : []),
|
|
293
|
+
],
|
|
294
|
+
parser: useTypescript ? '@typescript-eslint/parser' : undefined,
|
|
295
|
+
plugins: useTypescript ? ['@typescript-eslint'] : [],
|
|
296
|
+
parserOptions: {
|
|
297
|
+
ecmaVersion: 'latest',
|
|
298
|
+
sourceType: 'module',
|
|
299
|
+
},
|
|
300
|
+
rules: {},
|
|
301
|
+
ignorePatterns: ['node_modules/', 'dist/'],
|
|
302
|
+
};
|
|
303
|
+
fs.writeFileSync(path.join(projectDir, '.eslintrc.json'), JSON.stringify(eslintConfig, null, 2));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Tailwind config
|
|
307
|
+
if (useTailwind) {
|
|
308
|
+
const tailwindConfig = `/** @type {import('tailwindcss').Config} */
|
|
309
|
+
module.exports = {
|
|
310
|
+
content: [
|
|
311
|
+
'./${useSrcDir ? 'src/' : ''}app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
312
|
+
'./${useSrcDir ? 'src/' : ''}components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
313
|
+
],
|
|
314
|
+
theme: {
|
|
315
|
+
extend: {
|
|
316
|
+
colors: {
|
|
317
|
+
oven: {
|
|
318
|
+
50: '#fff5f2',
|
|
319
|
+
100: '#ffe6de',
|
|
320
|
+
200: '#ffc9b8',
|
|
321
|
+
300: '#ffa088',
|
|
322
|
+
400: '#ff6b35',
|
|
323
|
+
500: '#f7531e',
|
|
324
|
+
600: '#e63d0a',
|
|
325
|
+
700: '#bf3009',
|
|
326
|
+
800: '#99290d',
|
|
327
|
+
900: '#7d2510',
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
plugins: [],
|
|
333
|
+
};
|
|
334
|
+
`;
|
|
335
|
+
fs.writeFileSync(path.join(projectDir, 'tailwind.config.js'), tailwindConfig);
|
|
76
336
|
|
|
77
|
-
|
|
337
|
+
const postcssConfig = `module.exports = {
|
|
338
|
+
plugins: {
|
|
339
|
+
tailwindcss: {},
|
|
340
|
+
autoprefixer: {},
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
`;
|
|
344
|
+
fs.writeFileSync(path.join(projectDir, 'postcss.config.js'), postcssConfig);
|
|
345
|
+
}
|
|
78
346
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
347
|
+
// oven.config.ts
|
|
348
|
+
const ovenConfig = `${useTypescript ? "import type { OvenConfig } from './app/types';\n\n" : ''}const config${useTypescript ? ': OvenConfig' : ''} = {
|
|
349
|
+
// Server configuration
|
|
350
|
+
port: 3000,
|
|
84
351
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
}
|
|
352
|
+
// Directories
|
|
353
|
+
appDir: '${useSrcDir ? 'src/' : ''}app',
|
|
354
|
+
publicDir: 'public',
|
|
355
|
+
};
|
|
101
356
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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'],
|
|
357
|
+
export default config;
|
|
358
|
+
`;
|
|
359
|
+
fs.writeFileSync(path.join(projectDir, `oven.config.${ext}`), ovenConfig);
|
|
360
|
+
|
|
361
|
+
spin2.stop('Created configuration files');
|
|
362
|
+
|
|
363
|
+
// Create app files
|
|
364
|
+
const spin3 = spinner('Creating app files...');
|
|
365
|
+
|
|
366
|
+
// Types
|
|
367
|
+
const typesContent = `export interface PageProps {
|
|
368
|
+
params: Record<string, string>;
|
|
369
|
+
searchParams: Record<string, string | string[] | undefined>;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export interface LayoutProps {
|
|
373
|
+
children: ${useTypescript ? 'string' : 'any'};
|
|
374
|
+
params: Record<string, string>;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export interface Metadata {
|
|
378
|
+
title?: string | { default: string; template?: string };
|
|
379
|
+
description?: string;
|
|
380
|
+
keywords?: string[];
|
|
381
|
+
openGraph?: {
|
|
382
|
+
title?: string;
|
|
383
|
+
description?: string;
|
|
384
|
+
images?: { url: string }[];
|
|
141
385
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export interface OvenConfig {
|
|
389
|
+
port?: number;
|
|
390
|
+
appDir?: string;
|
|
391
|
+
publicDir?: string;
|
|
392
|
+
}
|
|
393
|
+
`;
|
|
394
|
+
fs.writeFileSync(path.join(appDir, `types.${ext}`), typesContent);
|
|
395
|
+
|
|
396
|
+
// globals.css
|
|
397
|
+
const globalsCss = useTailwind ? `@tailwind base;
|
|
398
|
+
@tailwind components;
|
|
399
|
+
@tailwind utilities;
|
|
400
|
+
|
|
401
|
+
:root {
|
|
402
|
+
--foreground-rgb: 0, 0, 0;
|
|
403
|
+
--background-rgb: 255, 255, 255;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
@media (prefers-color-scheme: dark) {
|
|
407
|
+
:root {
|
|
408
|
+
--foreground-rgb: 255, 255, 255;
|
|
409
|
+
--background-rgb: 10, 10, 10;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
body {
|
|
414
|
+
color: rgb(var(--foreground-rgb));
|
|
415
|
+
background: rgb(var(--background-rgb));
|
|
416
|
+
}
|
|
417
|
+
` : `* {
|
|
418
|
+
margin: 0;
|
|
419
|
+
padding: 0;
|
|
420
|
+
box-sizing: border-box;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
:root {
|
|
424
|
+
--foreground-rgb: 0, 0, 0;
|
|
425
|
+
--background-rgb: 255, 255, 255;
|
|
426
|
+
--oven-primary: #ff6b35;
|
|
427
|
+
--oven-secondary: #f7931e;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
body {
|
|
431
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
432
|
+
color: rgb(var(--foreground-rgb));
|
|
433
|
+
background: rgb(var(--background-rgb));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
a {
|
|
437
|
+
color: inherit;
|
|
438
|
+
text-decoration: none;
|
|
439
|
+
}
|
|
440
|
+
`;
|
|
441
|
+
fs.writeFileSync(path.join(appDir, 'globals.css'), globalsCss);
|
|
442
|
+
|
|
443
|
+
// favicon.ico (placeholder - just create the public dir marker)
|
|
444
|
+
fs.writeFileSync(path.join(projectDir, 'public', '.gitkeep'), '');
|
|
445
|
+
|
|
446
|
+
// Root Layout
|
|
447
|
+
const layoutContent = `import${useTypescript ? ' type' : ''} { LayoutProps } from './types';
|
|
448
|
+
import './globals.css';
|
|
449
|
+
|
|
450
|
+
export const metadata = {
|
|
451
|
+
title: {
|
|
452
|
+
default: '${projectName}',
|
|
453
|
+
template: '%s | ${projectName}',
|
|
454
|
+
},
|
|
455
|
+
description: 'Built with Oven - A Next.js-style framework for Bun',
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
export default function RootLayout({ children }${useTypescript ? ': LayoutProps' : ''}) {
|
|
459
|
+
return \`
|
|
460
|
+
<!DOCTYPE html>
|
|
461
|
+
<html lang="en">
|
|
462
|
+
<head>
|
|
463
|
+
<meta charset="UTF-8">
|
|
464
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
465
|
+
<link rel="icon" href="/favicon.ico" />
|
|
466
|
+
</head>
|
|
467
|
+
<body${useTailwind ? ' class="antialiased"' : ''}>
|
|
468
|
+
\${children}
|
|
469
|
+
</body>
|
|
470
|
+
</html>
|
|
471
|
+
\`;
|
|
472
|
+
}
|
|
473
|
+
`;
|
|
474
|
+
fs.writeFileSync(path.join(appDir, `layout.${ext}x`), layoutContent);
|
|
475
|
+
|
|
476
|
+
// Home Page
|
|
477
|
+
const homePageContent = `import${useTypescript ? ' type' : ''} { PageProps, Metadata } from './types';
|
|
478
|
+
|
|
479
|
+
export const metadata${useTypescript ? ': Metadata' : ''} = {
|
|
480
|
+
title: 'Home',
|
|
481
|
+
description: 'Welcome to ${projectName}',
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
export default function HomePage({ searchParams }${useTypescript ? ': PageProps' : ''}) {
|
|
485
|
+
return \`
|
|
486
|
+
<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;"'}>
|
|
487
|
+
<div ${useTailwind ? 'class="text-center"' : 'style="text-align: center;"'}>
|
|
488
|
+
<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;"'}>
|
|
489
|
+
Welcome to ${projectName}
|
|
490
|
+
</h1>
|
|
491
|
+
|
|
492
|
+
<p ${useTailwind ? 'class="text-xl text-gray-600 mb-8"' : 'style="font-size: 1.25rem; color: #666; margin-bottom: 2rem;"'}>
|
|
493
|
+
Get started by editing
|
|
494
|
+
<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;"'}>
|
|
495
|
+
${useSrcDir ? 'src/' : ''}app/page.${ext}x
|
|
496
|
+
</code>
|
|
497
|
+
</p>
|
|
498
|
+
|
|
499
|
+
<div ${useTailwind ? 'class="flex gap-4 flex-wrap justify-center"' : 'style="display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center;"'}>
|
|
500
|
+
<a href="/about" ${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;"'}>
|
|
501
|
+
Learn More →
|
|
502
|
+
</a>
|
|
503
|
+
<a href="/api/hello" ${useTailwind ? 'class="px-6 py-3 bg-gray-900 text-white rounded-lg font-medium hover:bg-gray-800 transition"' : 'style="padding: 0.75rem 1.5rem; background: #1a1a1a; color: white; border-radius: 8px; font-weight: 500;"'}>
|
|
504
|
+
API Example
|
|
505
|
+
</a>
|
|
506
|
+
<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;"'}>
|
|
507
|
+
GitHub ⭐
|
|
508
|
+
</a>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<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;"'}>
|
|
513
|
+
<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;"'}>
|
|
514
|
+
<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>
|
|
515
|
+
<p ${useTailwind ? 'class="text-gray-600 text-sm"' : 'style="color: #666; font-size: 0.9rem;"'}>
|
|
516
|
+
Create routes by adding files to the app directory. Just like Next.js!
|
|
517
|
+
</p>
|
|
518
|
+
</div>
|
|
519
|
+
|
|
520
|
+
<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;"'}>
|
|
521
|
+
<h3 ${useTailwind ? 'class="text-lg font-semibold mb-2"' : 'style="font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem;"'}>⚡ Blazing Fast</h3>
|
|
522
|
+
<p ${useTailwind ? 'class="text-gray-600 text-sm"' : 'style="color: #666; font-size: 0.9rem;"'}>
|
|
523
|
+
Powered by Bun runtime. Up to 4x faster than Node.js.
|
|
524
|
+
</p>
|
|
525
|
+
</div>
|
|
526
|
+
|
|
527
|
+
<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;"'}>
|
|
528
|
+
<h3 ${useTailwind ? 'class="text-lg font-semibold mb-2"' : 'style="font-size: 1.1rem; font-weight: 600; margin-bottom: 0.5rem;"'}>🔌 API Routes</h3>
|
|
529
|
+
<p ${useTailwind ? 'class="text-gray-600 text-sm"' : 'style="color: #666; font-size: 0.9rem;"'}>
|
|
530
|
+
Build APIs with route handlers. Support GET, POST, PUT, DELETE.
|
|
531
|
+
</p>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
</main>
|
|
535
|
+
\`;
|
|
536
|
+
}
|
|
537
|
+
`;
|
|
538
|
+
fs.writeFileSync(path.join(appDir, `page.${ext}x`), homePageContent);
|
|
539
|
+
|
|
540
|
+
// About Page
|
|
541
|
+
const aboutPageContent = `import${useTypescript ? ' type' : ''} { PageProps, Metadata } from '../types';
|
|
542
|
+
|
|
543
|
+
export const metadata${useTypescript ? ': Metadata' : ''} = {
|
|
544
|
+
title: 'About',
|
|
545
|
+
description: 'About ${projectName}',
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
export default function AboutPage({ params }${useTypescript ? ': PageProps' : ''}) {
|
|
549
|
+
return \`
|
|
550
|
+
<main ${useTailwind ? 'class="max-w-4xl mx-auto px-6 py-16"' : 'style="max-width: 900px; margin: 0 auto; padding: 4rem 1.5rem;"'}>
|
|
551
|
+
<a href="/" ${useTailwind ? 'class="text-oven-400 hover:underline mb-8 inline-block"' : 'style="color: #ff6b35; margin-bottom: 2rem; display: inline-block;"'}>
|
|
552
|
+
← Back to Home
|
|
553
|
+
</a>
|
|
554
|
+
|
|
555
|
+
<h1 ${useTailwind ? 'class="text-4xl font-bold mb-6"' : 'style="font-size: 2.5rem; font-weight: bold; margin-bottom: 1.5rem;"'}>
|
|
556
|
+
About ${projectName}
|
|
557
|
+
</h1>
|
|
558
|
+
|
|
559
|
+
<p ${useTailwind ? 'class="text-lg text-gray-600 mb-8 leading-relaxed"' : 'style="font-size: 1.1rem; color: #666; margin-bottom: 2rem; line-height: 1.8;"'}>
|
|
560
|
+
This project was bootstrapped with <strong>create-oven</strong>,
|
|
561
|
+
a Next.js-style framework powered by the Bun runtime.
|
|
562
|
+
</p>
|
|
563
|
+
|
|
564
|
+
<div ${useTailwind ? 'class="bg-oven-50 border-l-4 border-oven-400 p-6 rounded-r-lg mb-8"' : 'style="background: #fff5f2; border-left: 4px solid #ff6b35; padding: 1.5rem; border-radius: 0 8px 8px 0; margin-bottom: 2rem;"'}>
|
|
565
|
+
<h2 ${useTailwind ? 'class="text-xl font-semibold mb-4"' : 'style="font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem;"'}>
|
|
566
|
+
🚀 Why Oven?
|
|
567
|
+
</h2>
|
|
568
|
+
<ul ${useTailwind ? 'class="space-y-2 text-gray-700"' : 'style="list-style: none;"'}>
|
|
569
|
+
<li ${useTailwind ? '' : 'style="margin-bottom: 0.5rem; color: #444;"'}>✓ Built for Bun runtime - 4x faster than Node.js</li>
|
|
570
|
+
<li ${useTailwind ? '' : 'style="margin-bottom: 0.5rem; color: #444;"'}>✓ Next.js-style file-based routing</li>
|
|
571
|
+
<li ${useTailwind ? '' : 'style="margin-bottom: 0.5rem; color: #444;"'}>✓ Zero configuration needed</li>
|
|
572
|
+
<li ${useTailwind ? '' : 'style="margin-bottom: 0.5rem; color: #444;"'}>✓ Full TypeScript support</li>
|
|
573
|
+
${useTailwind ? "<li>✓ Tailwind CSS integration</li>" : ""}
|
|
574
|
+
</ul>
|
|
575
|
+
</div>
|
|
146
576
|
|
|
147
|
-
|
|
148
|
-
|
|
577
|
+
<h2 ${useTailwind ? 'class="text-2xl font-semibold mb-4"' : 'style="font-size: 1.5rem; font-weight: 600; margin-bottom: 1rem;"'}>
|
|
578
|
+
Learn More
|
|
579
|
+
</h2>
|
|
580
|
+
|
|
581
|
+
<div ${useTailwind ? 'class="flex gap-4 flex-wrap"' : 'style="display: flex; gap: 1rem; flex-wrap: wrap;"'}>
|
|
582
|
+
<a href="https://github.com/oven-ttta/oven-framework" target="_blank"
|
|
583
|
+
${useTailwind ? 'class="px-4 py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800 transition"' : 'style="padding: 0.5rem 1rem; background: #1a1a1a; color: white; border-radius: 8px;"'}>
|
|
584
|
+
GitHub
|
|
585
|
+
</a>
|
|
586
|
+
<a href="https://bun.sh/docs" target="_blank"
|
|
587
|
+
${useTailwind ? 'class="px-4 py-2 border border-gray-300 rounded-lg hover:border-gray-400 transition"' : 'style="padding: 0.5rem 1rem; border: 1px solid #ddd; border-radius: 8px;"'}>
|
|
588
|
+
Bun Docs
|
|
589
|
+
</a>
|
|
590
|
+
</div>
|
|
591
|
+
</main>
|
|
592
|
+
\`;
|
|
593
|
+
}
|
|
594
|
+
`;
|
|
595
|
+
fs.writeFileSync(path.join(appDir, 'about', `page.${ext}x`), aboutPageContent);
|
|
596
|
+
|
|
597
|
+
// API Route
|
|
598
|
+
const apiRouteContent = `// API Route: GET /api/hello
|
|
599
|
+
|
|
600
|
+
export async function GET(request${useTypescript ? ': Request' : ''}) {
|
|
601
|
+
return Response.json({
|
|
602
|
+
message: 'Hello from ${projectName}!',
|
|
603
|
+
timestamp: new Date().toISOString(),
|
|
604
|
+
framework: 'Oven 🔥',
|
|
605
|
+
runtime: 'Bun',
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export async function POST(request${useTypescript ? ': Request' : ''}) {
|
|
610
|
+
const body = await request.json().catch(() => ({}));
|
|
611
|
+
|
|
612
|
+
return Response.json({
|
|
613
|
+
message: 'Data received!',
|
|
614
|
+
data: body,
|
|
615
|
+
timestamp: new Date().toISOString(),
|
|
616
|
+
}, { status: 201 });
|
|
617
|
+
}
|
|
618
|
+
`;
|
|
619
|
+
fs.writeFileSync(path.join(appDir, 'api', 'hello', `route.${ext}`), apiRouteContent);
|
|
620
|
+
|
|
621
|
+
// Server file
|
|
622
|
+
const serverContent = `/**
|
|
149
623
|
* Oven Server - ${projectName}
|
|
150
624
|
* A Next.js-style framework powered by Bun
|
|
151
625
|
*/
|
|
152
626
|
|
|
153
627
|
const PORT = parseInt(process.env.PORT || '3000');
|
|
154
628
|
|
|
155
|
-
//
|
|
156
|
-
const routes: Map<string, (req: Request) => Promise<Response>> = new Map();
|
|
629
|
+
// Routes map
|
|
630
|
+
const routes${useTypescript ? ': Map<string, (req: Request) => Promise<Response>>' : ''} = new Map();
|
|
157
631
|
|
|
158
|
-
// Scan
|
|
632
|
+
// Scan and register routes
|
|
159
633
|
async function scanRoutes() {
|
|
160
|
-
const
|
|
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;
|
|
634
|
+
const appDir = './${useSrcDir ? 'src/' : ''}app';
|
|
164
635
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
636
|
+
// Scan pages
|
|
637
|
+
const pageGlob = new Bun.Glob('**/page.{ts,tsx,js,jsx}');
|
|
638
|
+
for await (const file of pageGlob.scan({ cwd: appDir })) {
|
|
639
|
+
const routePath = '/' + file
|
|
640
|
+
.replace(/\\/page\\.(ts|tsx|js|jsx)$/, '')
|
|
641
|
+
.replace(/^page\\.(ts|tsx|js|jsx)$/, '')
|
|
642
|
+
.replace(/\\/$/, '') || '/';
|
|
168
643
|
|
|
169
|
-
|
|
170
|
-
|
|
644
|
+
const normalizedPath = routePath === '' ? '/' : routePath;
|
|
645
|
+
|
|
646
|
+
routes.set(normalizedPath, async (req${useTypescript ? ': Request' : ''}) => {
|
|
171
647
|
try {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
648
|
+
const module = await import(\`\${appDir}/\${file}\`);
|
|
649
|
+
const content = await module.default({ params: {}, searchParams: {} });
|
|
650
|
+
|
|
651
|
+
// Get layout
|
|
652
|
+
let html = content;
|
|
653
|
+
try {
|
|
654
|
+
const layoutModule = await import(\`\${appDir}/layout.tsx\`);
|
|
655
|
+
html = await layoutModule.default({ children: content, params: {} });
|
|
656
|
+
} catch {}
|
|
657
|
+
|
|
658
|
+
// Metadata
|
|
659
|
+
const metadata = module.metadata || {};
|
|
660
|
+
const title = typeof metadata.title === 'string'
|
|
661
|
+
? metadata.title
|
|
662
|
+
: metadata.title?.default || '${projectName}';
|
|
663
|
+
|
|
664
|
+
return new Response(html, {
|
|
665
|
+
headers: {
|
|
666
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
} catch (error) {
|
|
670
|
+
console.error('Page error:', error);
|
|
671
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
672
|
+
}
|
|
196
673
|
});
|
|
197
674
|
}
|
|
198
675
|
|
|
199
676
|
// Scan API routes
|
|
200
|
-
const apiGlob = new Bun.Glob('**/route.ts');
|
|
201
|
-
for await (const file of apiGlob.scan({ cwd:
|
|
202
|
-
const routePath = '/' + file
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
677
|
+
const apiGlob = new Bun.Glob('**/route.{ts,js}');
|
|
678
|
+
for await (const file of apiGlob.scan({ cwd: appDir })) {
|
|
679
|
+
const routePath = '/' + file
|
|
680
|
+
.replace(/\\/route\\.(ts|js)$/, '')
|
|
681
|
+
.replace(/^route\\.(ts|js)$/, '')
|
|
682
|
+
.replace(/\\/$/, '');
|
|
683
|
+
|
|
684
|
+
routes.set(routePath, async (req${useTypescript ? ': Request' : ''}) => {
|
|
685
|
+
const module = await import(\`\${appDir}/\${file}\`);
|
|
206
686
|
const method = req.method.toUpperCase();
|
|
207
687
|
const handler = module[method];
|
|
208
688
|
|
|
@@ -214,13 +694,13 @@ async function scanRoutes() {
|
|
|
214
694
|
}
|
|
215
695
|
}
|
|
216
696
|
|
|
217
|
-
//
|
|
697
|
+
// Start server
|
|
218
698
|
async function main() {
|
|
219
699
|
await scanRoutes();
|
|
220
700
|
|
|
221
701
|
Bun.serve({
|
|
222
702
|
port: PORT,
|
|
223
|
-
async fetch(req: Request) {
|
|
703
|
+
async fetch(req${useTypescript ? ': Request' : ''}) {
|
|
224
704
|
const url = new URL(req.url);
|
|
225
705
|
let pathname = url.pathname;
|
|
226
706
|
|
|
@@ -229,7 +709,7 @@ async function main() {
|
|
|
229
709
|
pathname = pathname.slice(0, -1);
|
|
230
710
|
}
|
|
231
711
|
|
|
232
|
-
//
|
|
712
|
+
// Match route
|
|
233
713
|
const handler = routes.get(pathname);
|
|
234
714
|
if (handler) {
|
|
235
715
|
return handler(req);
|
|
@@ -248,266 +728,167 @@ async function main() {
|
|
|
248
728
|
});
|
|
249
729
|
|
|
250
730
|
console.log(\`
|
|
251
|
-
|
|
731
|
+
${c.green}✓${c.reset} Ready in \${Date.now() - startTime}ms
|
|
252
732
|
|
|
253
|
-
|
|
254
|
-
|
|
733
|
+
${c.dim}➜${c.reset} Local: ${c.cyan}http://localhost:\${PORT}${c.reset}
|
|
734
|
+
${c.dim}➜${c.reset} Network: ${c.cyan}http://0.0.0.0:\${PORT}${c.reset}
|
|
255
735
|
\`);
|
|
256
736
|
}
|
|
257
737
|
|
|
258
|
-
|
|
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
|
-
}
|
|
738
|
+
const startTime = Date.now();
|
|
739
|
+
main().catch(console.error);
|
|
377
740
|
`;
|
|
378
|
-
|
|
741
|
+
fs.writeFileSync(path.join(useSrcDir ? path.join(projectDir, 'src') : projectDir, `server.${ext}`), serverContent);
|
|
379
742
|
|
|
380
|
-
|
|
381
|
-
|
|
743
|
+
// .gitignore
|
|
744
|
+
const gitignoreContent = `# Dependencies
|
|
745
|
+
node_modules
|
|
746
|
+
.pnpm-store
|
|
382
747
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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>
|
|
748
|
+
# Build
|
|
749
|
+
dist
|
|
750
|
+
.oven
|
|
751
|
+
.next
|
|
752
|
+
out
|
|
396
753
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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>
|
|
754
|
+
# Environment
|
|
755
|
+
.env
|
|
756
|
+
.env.local
|
|
757
|
+
.env.*.local
|
|
406
758
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
</div>
|
|
411
|
-
\`;
|
|
412
|
-
}
|
|
413
|
-
`;
|
|
414
|
-
fs.writeFileSync(path.join(projectDir, 'app', 'about', 'page.tsx'), aboutPage);
|
|
759
|
+
# Logs
|
|
760
|
+
*.log
|
|
761
|
+
npm-debug.log*
|
|
415
762
|
|
|
416
|
-
|
|
417
|
-
|
|
763
|
+
# OS
|
|
764
|
+
.DS_Store
|
|
765
|
+
Thumbs.db
|
|
418
766
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
});
|
|
425
|
-
}
|
|
767
|
+
# IDE
|
|
768
|
+
.vscode
|
|
769
|
+
.idea
|
|
770
|
+
*.swp
|
|
771
|
+
*.swo
|
|
426
772
|
|
|
427
|
-
|
|
428
|
-
|
|
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);
|
|
773
|
+
# Testing
|
|
774
|
+
coverage
|
|
436
775
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
dist
|
|
440
|
-
.oven
|
|
441
|
-
*.log
|
|
442
|
-
.DS_Store
|
|
443
|
-
.env
|
|
444
|
-
.env.local
|
|
776
|
+
# Misc
|
|
777
|
+
*.tsbuildinfo
|
|
445
778
|
`;
|
|
446
|
-
|
|
779
|
+
fs.writeFileSync(path.join(projectDir, '.gitignore'), gitignoreContent);
|
|
447
780
|
|
|
448
|
-
|
|
449
|
-
|
|
781
|
+
// README.md
|
|
782
|
+
const readmeContent = `# ${projectName}
|
|
450
783
|
|
|
451
|
-
|
|
784
|
+
This is a [Oven](https://github.com/oven-ttta/oven-framework) project bootstrapped with \`create-oven\`.
|
|
452
785
|
|
|
453
786
|
## Getting Started
|
|
454
787
|
|
|
788
|
+
First, install dependencies:
|
|
789
|
+
|
|
455
790
|
\`\`\`bash
|
|
456
|
-
# Install dependencies
|
|
457
791
|
bun install
|
|
792
|
+
\`\`\`
|
|
458
793
|
|
|
459
|
-
|
|
460
|
-
bun run dev
|
|
461
|
-
|
|
462
|
-
# Build for production
|
|
463
|
-
bun run build
|
|
794
|
+
Then, run the development server:
|
|
464
795
|
|
|
465
|
-
|
|
466
|
-
bun run
|
|
796
|
+
\`\`\`bash
|
|
797
|
+
bun run dev
|
|
467
798
|
\`\`\`
|
|
468
799
|
|
|
469
|
-
Open [http://localhost:3000](http://localhost:3000) to see
|
|
800
|
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
801
|
+
|
|
802
|
+
You can start editing the page by modifying \`${useSrcDir ? 'src/' : ''}app/page.${ext}x\`. The page auto-updates as you edit the file.
|
|
470
803
|
|
|
471
804
|
## Project Structure
|
|
472
805
|
|
|
473
806
|
\`\`\`
|
|
474
807
|
${projectName}/
|
|
475
|
-
├── app/
|
|
476
|
-
│ ├── layout
|
|
477
|
-
│ ├── page
|
|
808
|
+
├── ${useSrcDir ? 'src/' : ''}app/
|
|
809
|
+
│ ├── layout.${ext}x # Root layout
|
|
810
|
+
│ ├── page.${ext}x # Home page (/)
|
|
811
|
+
│ ├── globals.css # Global styles
|
|
478
812
|
│ ├── about/
|
|
479
|
-
│ │ └── page
|
|
813
|
+
│ │ └── page.${ext}x # About page (/about)
|
|
480
814
|
│ └── api/
|
|
481
815
|
│ └── hello/
|
|
482
|
-
│ └── route
|
|
483
|
-
├── public/
|
|
484
|
-
├──
|
|
816
|
+
│ └── route.${ext} # API route (/api/hello)
|
|
817
|
+
├── public/ # Static files
|
|
818
|
+
${useTailwind ? '├── tailwind.config.js # Tailwind config\n' : ''}├── oven.config.${ext} # Oven config
|
|
485
819
|
└── package.json
|
|
486
820
|
\`\`\`
|
|
487
821
|
|
|
488
822
|
## Learn More
|
|
489
823
|
|
|
490
|
-
- [Oven
|
|
824
|
+
- [Oven Documentation](https://github.com/oven-ttta/oven-framework)
|
|
491
825
|
- [Bun Documentation](https://bun.sh/docs)
|
|
826
|
+
${useTailwind ? '- [Tailwind CSS](https://tailwindcss.com/docs)\n' : ''}
|
|
827
|
+
## Deploy
|
|
828
|
+
|
|
829
|
+
Deploy your Oven app with Docker or Vercel.
|
|
830
|
+
|
|
831
|
+
### Docker
|
|
832
|
+
|
|
833
|
+
\`\`\`bash
|
|
834
|
+
docker build -t ${projectName} .
|
|
835
|
+
docker run -p 3000:3000 ${projectName}
|
|
836
|
+
\`\`\`
|
|
837
|
+
|
|
838
|
+
### Vercel
|
|
839
|
+
|
|
840
|
+
\`\`\`bash
|
|
841
|
+
vercel --prod
|
|
842
|
+
\`\`\`
|
|
492
843
|
`;
|
|
493
|
-
|
|
844
|
+
fs.writeFileSync(path.join(projectDir, 'README.md'), readmeContent);
|
|
845
|
+
|
|
846
|
+
spin3.stop('Created app files');
|
|
847
|
+
|
|
848
|
+
// Install dependencies (unless --no-install)
|
|
849
|
+
if (!noInstall) {
|
|
850
|
+
const packageManager = hasFlag('--use-npm') ? 'npm' : 'bun';
|
|
851
|
+
const spin4 = spinner(`Installing dependencies with ${packageManager}...`);
|
|
852
|
+
|
|
853
|
+
try {
|
|
854
|
+
execSync(`${packageManager} install`, {
|
|
855
|
+
cwd: projectDir,
|
|
856
|
+
stdio: 'pipe',
|
|
857
|
+
});
|
|
858
|
+
spin4.stop(`Installed dependencies with ${packageManager}`);
|
|
859
|
+
} catch (e) {
|
|
860
|
+
spin4.fail(`Failed to install dependencies. Run '${packageManager} install' manually.`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
494
863
|
|
|
495
|
-
|
|
496
|
-
|
|
864
|
+
// Success message
|
|
865
|
+
console.log(`
|
|
866
|
+
${c.green}${c.bold}Success!${c.reset} Created ${c.cyan}${projectName}${c.reset} at ${projectDir}
|
|
497
867
|
|
|
498
|
-
|
|
868
|
+
${c.dim}Inside that directory, you can run several commands:${c.reset}
|
|
499
869
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
bun run dev
|
|
870
|
+
${c.cyan}bun run dev${c.reset}
|
|
871
|
+
Starts the development server.
|
|
503
872
|
|
|
504
|
-
|
|
873
|
+
${c.cyan}bun run build${c.reset}
|
|
874
|
+
Builds the app for production.
|
|
505
875
|
|
|
506
|
-
${
|
|
507
|
-
|
|
876
|
+
${c.cyan}bun run start${c.reset}
|
|
877
|
+
Runs the built app in production mode.
|
|
878
|
+
|
|
879
|
+
${c.dim}We suggest that you begin by typing:${c.reset}
|
|
880
|
+
|
|
881
|
+
${c.cyan}cd${c.reset} ${projectName}
|
|
882
|
+
${c.cyan}bun run dev${c.reset}
|
|
883
|
+
|
|
884
|
+
${c.bold}Happy coding! 🔥${c.reset}
|
|
885
|
+
`);
|
|
886
|
+
|
|
887
|
+
} catch (err) {
|
|
888
|
+
console.error(`\n${c.red}Error: ${err.message}${c.reset}`);
|
|
889
|
+
rl.close();
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
508
892
|
}
|
|
509
893
|
|
|
510
|
-
main()
|
|
511
|
-
error(`Error: ${err.message}`);
|
|
512
|
-
process.exit(1);
|
|
513
|
-
});
|
|
894
|
+
main();
|