create-base
Version:
快速构建项目基础框架,支持vue/uni-app/后台管理模板等
258 lines (226 loc) • 6.9 kB
JavaScript
const fs = require('fs')
const path = require('path')
// Avoids autoconversion to number of the project name by defining that the args
// non associated with an option ( _ ) needs to be parsed as a string.
const argv = require('minimist')(process.argv.slice(2), { string: ['_'] })
const prompts = require('prompts')
const { green, yellow, blue, gray, cyan, magenta, red, reset } = require('kolorist')
const cwd = process.cwd()
const FRAMEWORKS = [
{
name: 'vue',
color: green,
variants: [
// { name: 'vue2', color: gray },
// { name: 'vue', color: cyan },
{ name: 'vue-ts', color: magenta }
]
},
{
name: 'uniapp',
color: yellow,
variants: [
// { name: 'uniapp-vue2', color: gray },
// { name: 'uniapp', color: cyan },
{ name: 'uniapp-ts', color: magenta }
]
}
// {
// name: 'admin',
// color: blue,
// variants: [{ name: 'admin-element', color: gray }]
// }
]
const TEMPLATES = FRAMEWORKS.map(f => (f.variants && f.variants.map(v => v.name)) || [f.name]).reduce(
(a, b) => a.concat(b),
[]
)
const renameFiles = {
_gitignore: '.gitignore'
}
// 需要过滤的目录/文件名
const filterFiles = ['package.json', 'node_modules']
async function init() {
let targetDir = argv._[0]
let template = argv.template || argv.t
const defaultProjectName = !targetDir ? 'base-project' : targetDir
let result = {}
try {
result = await prompts(
[
{
type: targetDir ? null : 'text',
name: 'projectName',
message: reset('项目名称'),
initial: defaultProjectName,
onState: state => (targetDir = state.value.trim() || defaultProjectName)
},
{
type: () => (!fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm'),
name: 'overwrite',
message: () =>
(targetDir === '.' ? '当前目录' : `目标目录 "${targetDir}"`) + ' 不是空的。是否删除现有文件并继续?'
},
{
type: (_, { overwrite } = {}) => {
if (overwrite === false) {
throw new Error(red('✖') + ' 操作取消')
}
return null
},
name: 'overwriteChecker'
},
{
type: () => (isValidPackageName(targetDir) ? null : 'text'),
name: 'packageName',
message: reset('包名'),
initial: () => toValidPackageName(targetDir),
validate: dir => isValidPackageName(dir) || '无效的 package.json 名称'
},
{
type: template && TEMPLATES.includes(template) ? null : 'select',
name: 'framework',
message:
typeof template === 'string' && !TEMPLATES.includes(template)
? reset(`"${template}" 不是有效的模板。请从下面选择`)
: reset('选择一个框架'),
initial: 0,
choices: FRAMEWORKS.map(framework => {
const frameworkColor = framework.color
return {
title: frameworkColor(framework.name),
value: framework
}
})
},
{
type: framework => (framework && framework.variants ? 'select' : null),
name: 'variant',
message: reset('选择一个版本'),
choices: framework =>
framework.variants.map(variant => {
const variantColor = variant.color
return {
title: variantColor(variant.name),
value: variant.name
}
})
}
],
{
onCancel: () => {
throw new Error(red('✖') + ' 操作取消')
}
}
)
} catch (cancelled) {
console.log(cancelled.message)
return
}
// user choice associated with prompts
const { framework, overwrite, packageName, variant } = result
const root = path.join(cwd, targetDir)
if (overwrite) {
emptyDir(root)
} else if (!fs.existsSync(root)) {
fs.mkdirSync(root)
}
// determine template
template = variant || framework || template
console.log(`\n正在拉取项目文件到 ${root}...`)
const templateDir = path.join(__dirname, `template-${template}`)
const write = (file, content) => {
const targetPath = renameFiles[file] ? path.join(root, renameFiles[file]) : path.join(root, file)
if (content) {
fs.writeFileSync(targetPath, content)
} else {
copy(path.join(templateDir, file), targetPath)
}
}
const files = fs.readdirSync(templateDir)
for (const file of files.filter(f => !filterFiles.includes(f))) {
write(file)
}
const pkg = require(path.join(templateDir, `package.json`))
pkg.name = packageName || targetDir
write('package.json', JSON.stringify(pkg, null, 2))
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
console.log(`\n完成!现在运行下面命令\n`)
if (root !== cwd) {
console.log(` cd ${path.relative(cwd, root)}`)
}
switch (pkgManager) {
case 'yarn':
console.log(' yarn')
console.log(' yarn dev')
break
default:
console.log(` ${pkgManager} install`)
console.log(` ${pkgManager} run dev`)
break
}
console.log()
}
function copy(src, dest) {
const stat = fs.statSync(src)
if (stat.isDirectory()) {
copyDir(src, dest)
} else {
fs.copyFileSync(src, dest)
}
}
function isValidPackageName(projectName) {
return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(projectName)
}
function toValidPackageName(projectName) {
return projectName
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/^[._]/, '')
.replace(/[^a-z0-9-~]+/g, '-')
}
function copyDir(srcDir, destDir) {
fs.mkdirSync(destDir, { recursive: true })
for (const file of fs.readdirSync(srcDir)) {
const srcFile = path.resolve(srcDir, file)
const destFile = path.resolve(destDir, file)
copy(srcFile, destFile)
}
}
function isEmpty(path) {
return fs.readdirSync(path).length === 0
}
function emptyDir(dir) {
if (!fs.existsSync(dir)) {
return
}
for (const file of fs.readdirSync(dir)) {
const abs = path.resolve(dir, file)
// baseline is Node 12 so can't use rmSync :(
if (fs.lstatSync(abs).isDirectory()) {
emptyDir(abs)
fs.rmdirSync(abs)
} else {
fs.unlinkSync(abs)
}
}
}
/**
* @param {string | undefined} userAgent process.env.npm_config_user_agent
* @returns object | undefined
*/
function pkgFromUserAgent(userAgent) {
if (!userAgent) return undefined
const pkgSpec = userAgent.split(' ')[0]
const pkgSpecArr = pkgSpec.split('/')
return {
name: pkgSpecArr[0],
version: pkgSpecArr[1]
}
}
init().catch(e => {
console.error(e)
})