Skip to content

Commit c61d3f8

Browse files
committed
feat: init command will scaffold new project
1 parent 794b38a commit c61d3f8

File tree

2 files changed

+253
-21
lines changed

2 files changed

+253
-21
lines changed

src/commands/init.js

Lines changed: 241 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import chalk from 'chalk';
33
import ora from 'ora';
44
import { promises as fs } from 'fs';
55
import path from 'path';
6-
import { generateAppName } from '../commons.js';
6+
import { generateAppName, getDefaultHomePage } from '../commons.js';
7+
8+
const JS_BUNDLERS = ['Vite', 'Webpack', 'Parcel', 'esbuild', 'Farm'];
9+
const FULLSTACK_FRAMEWORKS = ['Next', 'Nuxt', 'SvelteKit', 'Astro'];
10+
const JS_LIBRARIES = ['React', 'Vue', 'Angular', 'Svelte', 'jQuery'];
11+
const CSS_LIBRARIES = ['Bootstrap', 'Bulma', 'shadcn', 'Tailwind', 'Material-UI', 'Semantic UI', 'AntDesign', 'Element-Plus', 'PostCSS', 'AutoPrefixer'];
712

813
export async function init() {
914
const answers = await inquirer.prompt([
@@ -15,47 +20,223 @@ export async function init() {
1520
},
1621
{
1722
type: 'list',
18-
name: 'template',
19-
message: 'Select a template:',
20-
choices: ['basic', 'static-site', 'full-stack']
23+
name: 'useBundler',
24+
message: 'Do you want to use a JavaScript bundler?',
25+
choices: ['Yes', 'No (Use CDN)']
2126
}
2227
]);
2328

29+
let jsFiles = [];
30+
let cssFiles = [];
31+
let extraFiles = [];
32+
let bundlerAnswers = null;
33+
let frameworkAnswers = null;
34+
35+
if (answers.useBundler === 'Yes') {
36+
bundlerAnswers = await inquirer.prompt([
37+
{
38+
type: 'list',
39+
name: 'bundler',
40+
message: 'Select a JavaScript bundler:',
41+
choices: JS_BUNDLERS
42+
},
43+
{
44+
type: 'list',
45+
name: 'frameworkType',
46+
message: 'Do you want to use a full-stack framework or custom libraries?',
47+
choices: ['Full-stack framework', 'Custom libraries']
48+
}
49+
]);
50+
51+
if (bundlerAnswers.frameworkType === 'Full-stack framework') {
52+
frameworkAnswers = await inquirer.prompt([
53+
{
54+
type: 'list',
55+
name: 'framework',
56+
message: 'Select a full-stack framework:',
57+
choices: FULLSTACK_FRAMEWORKS
58+
}
59+
]);
60+
61+
switch (frameworkAnswers.framework) {
62+
case FULLSTACK_FRAMEWORKS[0]:
63+
jsFiles.push('next@latest');
64+
extraFiles.push({
65+
path: 'src/index.tsx',
66+
content: `export default function Home() { return (<h1>${answers.name}</h1>) }`
67+
});
68+
break;
69+
case FULLSTACK_FRAMEWORKS[1]:
70+
jsFiles.push('nuxt@latest');
71+
extraFiles.push({
72+
path: 'src/app.vue',
73+
content: `<template><h1>${answers.name}</h1></template>`
74+
});
75+
break;
76+
case FULLSTACK_FRAMEWORKS[2]:
77+
jsFiles.push('svelte@latest', 'sveltekit@latest');
78+
extraFiles.push({
79+
path: 'src/app.vue',
80+
content: `<template><h1>${answers.name}</h1></template>`
81+
});
82+
break;
83+
case FULLSTACK_FRAMEWORKS[3]:
84+
jsFiles.push('astro@latest', 'astro@latest');
85+
extraFiles.push({
86+
path: 'src/pages/index.astro',
87+
content: `---\n\n<Layout title="Welcome to ${answers.name}."><h1>${answers.name}</h1></Layout>`
88+
});
89+
break;
90+
}
91+
} else {
92+
const libraryAnswers = await inquirer.prompt([
93+
{
94+
type: 'list',
95+
name: 'library',
96+
message: 'Select a JavaScript library/framework:',
97+
choices: JS_LIBRARIES
98+
}
99+
]);
100+
101+
switch (libraryAnswers.library) {
102+
case JS_LIBRARIES[0]:
103+
jsFiles.push('react@latest', 'react-dom@latest');
104+
const reactLibs = await inquirer.prompt([
105+
{
106+
type: 'checkbox',
107+
name: 'reactLibraries',
108+
message: 'Select React libraries:',
109+
choices: CSS_LIBRARIES.concat(['react-router-dom', 'react-redux', 'react-bootstrap', '@chakra-ui/react', 'semantic-ui-react'])
110+
}
111+
]);
112+
jsFiles.push(...reactLibs.reactLibraries);
113+
extraFiles.push({
114+
path: 'src/App.jsx',
115+
content: `export default function Home() { return (<h1>${answers.name}</h1>) }`
116+
});
117+
break;
118+
case JS_LIBRARIES[1]:
119+
jsFiles.push('vue@latest');
120+
const vueLibs = await inquirer.prompt([
121+
{
122+
type: 'checkbox',
123+
name: 'vueLibraries',
124+
message: 'Select Vue libraries:',
125+
choices: CSS_LIBRARIES.concat(['shadcn-vue', 'UnoCSS', 'NaiveUI', 'bootstrap-vue-next', 'buefy', 'vue-router', 'pinia'])
126+
}
127+
]);
128+
jsFiles.push(...vueLibs.vueLibraries);
129+
extraFiles.push({
130+
path: 'src/App.vue',
131+
content: `<template><h1>${answers.name}</h1></template>`
132+
});
133+
break;
134+
case JS_LIBRARIES[2]:
135+
jsFiles.push('@angular/core@latest');
136+
extraFiles.push({
137+
path: 'src/index.controller.js',
138+
content: `(function () { angular.module('app', [])})`
139+
});
140+
break;
141+
case JS_LIBRARIES[3]:
142+
jsFiles.push('svelte@latest');
143+
break;
144+
case JS_LIBRARIES[4]:
145+
jsFiles.push('jquery@latest');
146+
extraFiles.push({
147+
path: 'src/main.js',
148+
content: `$(function(){})`
149+
});
150+
break;
151+
}
152+
}
153+
} else {
154+
155+
const cdnAnswers = await inquirer.prompt([
156+
{
157+
type: 'list',
158+
name: 'jsFramework',
159+
message: 'Select a JavaScript framework/library (CDN):',
160+
choices: JS_LIBRARIES
161+
},
162+
{
163+
type: 'list',
164+
name: 'cssFramework',
165+
message: 'Select a CSS framework/library (CDN):',
166+
choices: CSS_LIBRARIES //'Tailwind', 'Bootstrap', 'Bulma'...
167+
}
168+
]);
169+
170+
switch (cdnAnswers.jsFramework) {
171+
case JS_LIBRARIES[0]:
172+
jsFiles.push('https://unpkg.com/react@latest/umd/react.production.min.js');
173+
jsFiles.push('https://unpkg.com/react-dom@latest/umd/react-dom.production.min.js');
174+
break;
175+
case JS_LIBRARIES[1]:
176+
jsFiles.push('https://unpkg.com/vue@latest/dist/vue.global.js');
177+
break;
178+
case JS_LIBRARIES[2]:
179+
jsFiles.push('https://unpkg.com/@angular/core@latest/bundles/core.umd.js');
180+
break;
181+
case JS_LIBRARIES[3]:
182+
jsFiles.push('https://unpkg.com/svelte@latest/compiled/svelte.js');
183+
break;
184+
case JS_LIBRARIES[4]:
185+
jsFiles.push('https://code.jquery.com/jquery-latest.min.js');
186+
break;
187+
}
188+
189+
switch (cdnAnswers.cssFramework) {
190+
case CSS_LIBRARIES[0]:
191+
cssFiles.push('https://cdn.jsdelivr.net/npm/bootstrap@latest/dist/css/bootstrap.min.css');
192+
break;
193+
case CSS_LIBRARIES[1]:
194+
cssFiles.push('https://cdn.jsdelivr.net/npm/bulma@latest/css/bulma.min.css');
195+
break;
196+
case CSS_LIBRARIES[2]:
197+
cssFiles.push('https://cdn.tailwindcss.com');
198+
break;
199+
}
200+
}
201+
24202
const spinner = ora('Creating Puter app...').start();
25203

26204
try {
205+
const useBundler = answers.useBundler === 'Yes';
27206
// Create basic app structure
28-
await createAppStructure(answers);
207+
await createAppStructure(answers.name, useBundler, bundlerAnswers, frameworkAnswers, jsFiles, cssFiles, extraFiles);
29208
spinner.succeed(chalk.green('Successfully created Puter app!'));
30-
209+
31210
console.log('\nNext steps:');
32211
console.log(chalk.cyan('1. cd'), answers.name);
33-
console.log(chalk.cyan('2. npm install'));
34-
console.log(chalk.cyan('3. npm start'));
212+
if (useBundler) {
213+
console.log(chalk.cyan('2. npm install'));
214+
console.log(chalk.cyan('3. npm start'));
215+
} else {
216+
console.log(chalk.cyan('2. Open index.html in your browser'));
217+
}
35218
} catch (error) {
36219
spinner.fail(chalk.red('Failed to create app'));
37220
console.error(error);
38221
}
39222
}
40223

41-
async function createAppStructure({ name, template }) {
224+
async function createAppStructure(name, useBundler, bundlerAnswers, frameworkAnswers, jsFiles, cssFiles, extraFiles) {
42225
// Create project directory
43226
await fs.mkdir(name, { recursive: true });
44227

228+
// Generate default home page
229+
const homePage = useBundler?getDefaultHomePage(name): getDefaultHomePage(name, jsFiles, cssFiles);
230+
45231
// Create basic files
46232
const files = {
47233
'.env': `APP_NAME=${name}\nPUTER_API_KEY=`,
48-
'index.html': `<!DOCTYPE html>
49-
<html>
50-
<head>
51-
<title>${name}</title>
52-
</head>
53-
<body>
54-
<h1>Welcome to ${name}</h1>
55-
<script src="https://js.puter.com/v2/"></script>
56-
<script src="app.js"></script>
57-
</body>
58-
</html>`,
234+
'index.html': homePage,
235+
'styles.css': `body {
236+
font-family: 'Segoe UI', Roboto, sans-serif;
237+
margin: 0 auto;
238+
padding: 10px;
239+
}`,
59240
'app.js': `// Initialize Puter app
60241
console.log('Puter app initialized!');`,
61242
'README.md': `# ${name}\n\nA Puter app created with puter-cli`
@@ -64,4 +245,44 @@ console.log('Puter app initialized!');`,
64245
for (const [filename, content] of Object.entries(files)) {
65246
await fs.writeFile(path.join(name, filename), content);
66247
}
248+
249+
// If using a bundler, create a package.json
250+
// if (jsFiles.some(file => !file.startsWith('http'))) {
251+
if (useBundler) {
252+
253+
const useFullStackFramework = bundlerAnswers.frameworkType === 'Full-stack framework';
254+
const bundler = bundlerAnswers.bundler.toString().toLowerCase();
255+
const framework = useFullStackFramework?frameworkAnswers.framework.toLowerCase():null;
256+
257+
const scripts = {
258+
start: `${useFullStackFramework?`${framework} dev`:bundler} dev`,
259+
build: `${useFullStackFramework?`${framework} build`:bundler} build`,
260+
};
261+
262+
const packageJson = {
263+
name: name,
264+
version: '1.0.0',
265+
scripts,
266+
dependencies: {},
267+
devDependencies: {}
268+
};
269+
270+
jsFiles.forEach(lib => {
271+
if (!lib.startsWith('http')) {
272+
packageJson.dependencies[lib.split('@')[0].toString().toLowerCase()] = lib.split('@')[1] || 'latest';
273+
}
274+
});
275+
276+
packageJson.devDependencies[bundler] = 'latest';
277+
278+
await fs.writeFile(path.join(name, 'package.json'), JSON.stringify(packageJson, null, 2));
279+
280+
extraFiles.forEach(async (extraFile) => {
281+
const fullPath = path.join(name, extraFile.path);
282+
// Create directories recursively if they don't exist
283+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
284+
await fs.writeFile(fullPath, extraFile.content);
285+
});
286+
287+
}
67288
}

src/commons.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,19 @@ export function isValidAppName(name) {
190190
return true;
191191
}
192192

193-
export function getDefaultHomePage(appName) {
193+
/**
194+
* Generate the default home page for a new web application
195+
* @param {string} appName The name of the web application
196+
* @returns HTML template of the app
197+
*/
198+
export function getDefaultHomePage(appName, jsFiles = [], cssFiles= []) {
194199
const defaultIndexContent = `<!DOCTYPE html>
195200
<html lang="en">
196201
<head>
197202
<meta charset="UTF-8">
198203
<meta name="viewport" content="width=device-width, initial-scale=1.0">
199204
<title>${appName}</title>
205+
${cssFiles.map(css => `<link href="${css}" rel="stylesheet">`).join('\n ')}
200206
<style>
201207
body {
202208
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
@@ -292,6 +298,11 @@ export function getDefaultHomePage(appName) {
292298
<footer class="footer">
293299
&copy; 2025 ${appName}. All rights reserved.
294300
</footer>
301+
302+
<div id="${(jsFiles.length && jsFiles.some(f => f.includes('react'))) ? 'root' : 'app'}"></div>
303+
${jsFiles.map(js =>
304+
`<script ${js.endsWith('app.js') ? 'type="text/babel"' : ''} src="${js}"></script>`
305+
).join('\n ')}
295306
</body>
296307
</html>`;
297308

0 commit comments

Comments
 (0)