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