Skip to content

Commit e3ed252

Browse files
committed
feat: implement the create-vue command (no readme or cli argument support yet)
1 parent b2ebbf0 commit e3ed252

File tree

5 files changed

+319
-42
lines changed

5 files changed

+319
-42
lines changed

emptyDir.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
4+
export default function emptyDir(dir) {
5+
if (!fs.existsSync(dir)) {
6+
return
7+
}
8+
for (const file of fs.readdirSync(dir)) {
9+
const abs = path.resolve(dir, file)
10+
// baseline is Node 12 so can't use rmSync :(
11+
if (fs.lstatSync(abs).isDirectory()) {
12+
emptyDir(abs)
13+
fs.rmdirSync(abs)
14+
} else {
15+
fs.unlinkSync(abs)
16+
}
17+
}
18+
}

index.js

100644100755
Lines changed: 222 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,226 @@
1+
#!/usr/bin/env node
2+
// @ts-check
3+
4+
import fs from 'fs'
5+
import minimist from 'minimist'
16
import prompts from 'prompts'
2-
import kolorist from 'kolorist'
7+
import { red, green, bold } from 'kolorist'
38

9+
import emptyDir from './emptyDir.js'
410
import renderTemplate from './renderTemplate.js'
11+
import path from 'path'
12+
13+
function isValidPackageName(projectName) {
14+
return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(
15+
projectName
16+
)
17+
}
18+
19+
function toValidPackageName(projectName) {
20+
return projectName
21+
.trim()
22+
.toLowerCase()
23+
.replace(/\s+/g, '-')
24+
.replace(/^[._]/, '')
25+
.replace(/[^a-z0-9-~]+/g, '-')
26+
}
27+
28+
function canSafelyOverwrite(dir) {
29+
return !fs.existsSync(dir) || fs.readdirSync(dir).length === 0
30+
}
31+
32+
async function init() {
33+
const cwd = process.cwd()
34+
const argv = minimist(process.argv.slice(2))
35+
36+
let targetDir = argv._[0]
37+
const defaultProjectName = !targetDir ? 'vue-project' : targetDir
38+
39+
let result = {}
40+
try {
41+
// Prompts:
42+
// - Project name:
43+
// - whether to overwrite the existing directory or not?
44+
// - enter a valid package name for package.json
45+
// - Project language: JavaScript / TypeScript
46+
// - Install Vue Router & Vuex for SPA development?
47+
// - Add Cypress for testing?
48+
result = await prompts([
49+
{
50+
name: 'projectName',
51+
type: targetDir ? null : 'text',
52+
message: 'Project name:',
53+
initial: defaultProjectName,
54+
onState: (state) =>
55+
(targetDir = String(state.value).trim() || defaultProjectName)
56+
},
57+
{
58+
name: 'shouldOverwrite',
59+
type: () => canSafelyOverwrite(targetDir) ? null : 'confirm',
60+
message: () => {
61+
const dirForPrompt = targetDir === '.'
62+
? 'Current directory'
63+
: `Target directory "${targetDir}"`
64+
65+
return `${dirForPrompt} is not empty. Remove existing files and continue?`
66+
}
67+
},
68+
{
69+
name: 'overwriteChecker',
70+
type: (prev, values = {}) => {
71+
if (values.shouldOverwrite === false) {
72+
throw new Error(red('✖') + ' Operation cancelled')
73+
}
74+
return null
75+
}
76+
},
77+
{
78+
name: 'packageName',
79+
type: () => (isValidPackageName(targetDir) ? null : 'text'),
80+
message: 'Package name:',
81+
initial: () => toValidPackageName(targetDir),
82+
validate: (dir) => isValidPackageName(dir) || 'Invalid package.json name'
83+
},
84+
{
85+
name: 'shouldUseTypeScript',
86+
type: 'toggle',
87+
message: 'Add TypeScript?',
88+
initial: false,
89+
active: 'Yes',
90+
inactive: 'No'
91+
},
92+
{
93+
name: 'isSPA',
94+
type: 'toggle',
95+
message: 'Install Vue Router & Vuex for Single Page Application development?',
96+
initial: false,
97+
active: 'Yes',
98+
inactive: 'No'
99+
},
100+
{
101+
name: 'shouldAddCypress',
102+
type: 'toggle',
103+
message: 'Add Cypress for testing?',
104+
initial: false,
105+
active: 'Yes',
106+
inactive: 'No'
107+
}
108+
], {
109+
onCancel: () => {
110+
throw new Error(red('✖') + ' Operation cancelled')
111+
}
112+
})
113+
} catch (cancelled) {
114+
console.log(cancelled.message)
115+
process.exit(1)
116+
}
117+
118+
const { packageName, shouldOverwrite, shouldUseTypeScript, isSPA, shouldAddCypress } = result
119+
const root = path.join(cwd, targetDir)
120+
121+
if (shouldOverwrite) {
122+
emptyDir(root)
123+
} else if (!fs.existsSync(root)) {
124+
fs.mkdirSync(root)
125+
}
126+
127+
// TODO:
128+
// Add command-line option as a template-shortcut,
129+
// so that we can generate them in playgrounds
130+
// e.g. `--template typescript-spa` and `--with-tests`
131+
132+
console.log(`\nScaffolding project in ${root}...`)
133+
134+
const pkg = { name: packageName, version: '0.0.0' }
135+
fs.writeFileSync(
136+
path.resolve(root, 'package.json'),
137+
JSON.stringify(pkg, null, 2)
138+
)
139+
140+
const templateRoot = new URL('./template', import.meta.url).pathname
141+
const render = function render(templateName) {
142+
const templateDir = path.resolve(templateRoot, templateName)
143+
renderTemplate(templateDir, root)
144+
}
145+
146+
// Add configs.
147+
render('config/base')
148+
if (shouldAddCypress) {
149+
render('config/cypress')
150+
}
151+
if (shouldUseTypeScript) {
152+
render('config/typescript')
153+
}
154+
155+
// Render code template.
156+
const codeTemplate =
157+
(shouldUseTypeScript ? 'typescript-' : '') +
158+
(isSPA ? 'spa' : 'default')
159+
render(`code/${codeTemplate}`)
160+
161+
// TODO: README generation
162+
163+
// Cleanup.
164+
165+
if (shouldUseTypeScript) {
166+
// Should remove the `vite.config.js` from the base config
167+
fs.unlinkSync(path.resolve(root, 'vite.config.js'))
168+
}
169+
170+
if (!shouldAddCypress) {
171+
// All templates assumes the need of tests.
172+
// If the user doesn't need it:
173+
// rm -rf cypress **/__tests__/
174+
function removeTestDirectories (dir) {
175+
for (const filename of fs.readdirSync(dir)) {
176+
const subdir = path.resolve(dir, filename)
177+
const stats = fs.lstatSync(subdir)
178+
179+
if (!stats.isDirectory()) { continue }
180+
181+
if (filename === 'cypress' || filename === '__tests__') {
182+
emptyDir(subdir)
183+
fs.rmdirSync(subdir)
184+
continue
185+
}
186+
187+
removeTestDirectories(subdir)
188+
}
189+
}
190+
191+
removeTestDirectories(root)
192+
}
193+
194+
// Instructions:
195+
// Supported package managers: pnpm > yarn > npm
196+
const packageManager = /pnpm/.test(process.env.npm_execpath)
197+
? 'pnpm'
198+
: /yarn/.test(process.env.npm_execpath)
199+
?'yarn'
200+
: 'npm'
201+
202+
const commandsMap = {
203+
install: {
204+
pnpm: 'pnpm install',
205+
yarn: 'yarn',
206+
npm: 'npm install'
207+
},
208+
dev: {
209+
pnpm: 'pnpm dev',
210+
yarn: 'yarn dev',
211+
npm: 'npm run dev'
212+
}
213+
}
214+
215+
console.log(`\nDone. Now run:\n`)
216+
if (root !== cwd) {
217+
console.log(` ${bold(green(`cd ${path.relative(cwd, root)}`))}`)
218+
}
219+
console.log(` ${bold(green(commandsMap.install[packageManager]))}`)
220+
console.log(` ${bold(green(commandsMap.dev[packageManager]))}`)
221+
console.log()
222+
}
5223

6-
// Prompts:
7-
// - Project name:
8-
// - Project language: JavaScript / TypeScript
9-
// - Install Vue Router & Vuex for Single Page Applications?
10-
// - Adding tests?
11-
12-
// TODO:
13-
// add command-line for all possible option combinations
14-
// so that we can generate them in playgrounds
15-
16-
// Add configs.
17-
// renderTemplate('config/base')
18-
// if (needs tests) {
19-
// renderTemplate('config/cypress')
20-
// }
21-
// if (is typescript) {
22-
// renderTemplate('config/typescript')
23-
// }
24-
25-
// templateName =
26-
// (isTs ? 'typescript-' : '') +
27-
// (isSPA ? 'spa' : 'default')
28-
// renderTemplate(`code/${templateName}`)
29-
30-
// Cleanup.
31-
32-
// All templates assumes the need of tests.
33-
// If the user doesn't need it:
34-
// rm -rf cypress **/__tests__/
35-
36-
// TS config template may add redundant tsconfig.json.
37-
// Should clean that too.
38-
39-
// Instructions:
40-
// Supported package managers: pnpm > yarn > npm
224+
init().catch((e) => {
225+
console.error(e)
226+
})

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"homepage": "https://github.com/vuejs/create-vue#readme",
3232
"dependencies": {
3333
"kolorist": "^1.5.0",
34+
"minimist": "^1.2.5",
3435
"prompts": "^2.4.1"
3536
}
3637
}

pnpm-lock.yaml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

renderTemplate.js

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,73 @@
1-
export default function renderTemplate(templateFolder) {
2-
// TODO:
3-
// Recursively copy all files under `template/${templateFolder}`,
4-
// with the following exception:
5-
// - `_filename` should be renamed to `.filename`
6-
// - Fields in `package.json` should be recursively merged
1+
import fs from 'fs'
2+
import path from 'path'
3+
4+
const isObject = val => val && typeof val === 'object'
5+
const mergeArrayWithDedupe = (a, b) => Array.from(new Set([...a, ...b]))
6+
7+
/**
8+
* Recursively merge the content of the new object to the existing one
9+
* @param {Object} target the existing object
10+
* @param {Object} obj the new object
11+
*/
12+
function deepMerge(target, obj) {
13+
for (const key of Object.keys(obj)) {
14+
const oldVal = target[key]
15+
const newVal = obj[key]
16+
17+
if (Array.isArray(oldVal) && Array.isArray(newVal)) {
18+
target[key] = mergeArrayWithDedupe(oldVal, newVal)
19+
} else if (isObject(oldVal) && isObject(newVal)) {
20+
target[key] = deepMerge(oldVal, newVal)
21+
} else {
22+
target[key] = newVal
23+
}
24+
}
25+
26+
return target
727
}
28+
29+
/**
30+
* Renders a template folder/file to the file system,
31+
* by recursively copying all files under the `src` directory,
32+
* with the following exception:
33+
* - `_filename` should be renamed to `.filename`
34+
* - Fields in `package.json` should be recursively merged
35+
* @param {string} src source filename to copy
36+
* @param {string} dest destination filename of the copy operation
37+
*/
38+
function renderTemplate(src, dest) {
39+
const stats = fs.statSync(src)
40+
41+
if (stats.isDirectory()) {
42+
// if it's a directory, render its subdirectories and files recusively
43+
fs.mkdirSync(dest, { recursive: true })
44+
for (const file of fs.readdirSync(src)) {
45+
renderTemplate(path.resolve(src, file), path.resolve(dest, file))
46+
}
47+
return
48+
}
49+
50+
const filename = path.basename(src)
51+
52+
if (filename === 'package.json' && fs.existsSync(dest)) {
53+
// merge instead of overwriting
54+
const pkg = deepMerge(
55+
JSON.parse(fs.readFileSync(dest)),
56+
JSON.parse(fs.readFileSync(src))
57+
)
58+
fs.writeFileSync(dest, JSON.stringify(pkg, null, 2))
59+
return
60+
}
61+
62+
if (filename.startsWith('_')) {
63+
// rename `_file` to `.file`
64+
dest = path.resolve(
65+
path.dirname(dest),
66+
filename.replace(/^_/, '.')
67+
)
68+
}
69+
70+
fs.copyFileSync(src, dest)
71+
}
72+
73+
export default renderTemplate

0 commit comments

Comments
 (0)