create-iking-admin
Version:
金合技术中台脚手架
377 lines (345 loc) • 10.4 kB
JavaScript
// @ts-check
import {
blue, green, red,
reset,
yellow
} from 'kolorist'
import minimist from 'minimist'
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import prompts from 'prompts'
// 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. See #4606
const argv = minimist(process.argv.slice(2), { string: ['_'] })
const cwd = process.cwd()
// https://datasource.tangyh.top/#/system/dict
const FRAMEWORKS = [
{
name: 'admin',
desc: '后台管理系统模板',
color: blue,
variants: [
{
name: 'v2',
desc: 'V2.0 完整模板,新UI已上线 [包含菜单、用户、角色、权限等页面](推荐使用)',
display: 'v2',
color: blue,
}
]
}, {
name: 'gis',
desc: 'GIS三维可视化模板[待开发]',
color: yellow,
variants: [
{
name: 'full',
desc: 'GIS',
display: 'all',
color: green
}
]
}, {
name: 'gis',
desc: 'uni-app小程序模板[待开发]',
color: yellow,
variants: [
{
name: 'full',
desc: 'GIS',
display: 'all',
color: green
}
]
},
]
const APPLY_TYPE = [
{
name: 'catalogue',
desc: '分目录方式(业务系统以单独目录形式接入)',
display: 'catalogue',
color: green
},
{
name: 'micro',
desc: '微服务方式(业务系统以微服务方式接入)',
display: 'micro',
color: green
}
]
const TEMPLATES = FRAMEWORKS.map(
(f) => f.variants && f.variants.map((fc) => fc.name) || [f.name])
.reduce((a, b) => a.concat(b), [])
const renameFiles = {
_gitignore: '.gitignore'
}
async function init() {
let targetDir = formatTargetDir(argv._[0])
let template = argv.template || argv.t
const defaultTargetDir = 'iking-admin'
const getProjectName = () =>
targetDir === '.' ? path.basename(path.resolve()) : targetDir
let result = {}
try {
result = await prompts(
[
{
type: targetDir ? null : 'text',
name: 'projectName',
message: reset('项目名称:'),
initial: defaultTargetDir,
onState: (state) => {
targetDir = formatTargetDir(state.value) || defaultTargetDir
}
},
{
type: () =>
// @ts-ignore
!fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm',
name: 'overwrite',
message: () =>
(targetDir === '.'
? 'Current directory'
: `目标目录 "${targetDir}"`) +
` 不是空文件夹. 删除现有文件并继续?`
},
{
// @ts-ignore
type: (_, { overwrite } = {}) => {
if (overwrite === false) {
throw new Error(red('✖') + ' Operation cancelled')
}
return null
},
name: 'overwriteChecker'
},
{
// @ts-ignore
type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
name: 'packageName',
message: reset('Package name:'),
// @ts-ignore
initial: () => toValidPackageName(getProjectName()),
validate: (dir) =>
isValidPackageName(dir) || 'Invalid package.json name'
},
{
type: template && TEMPLATES.includes(template) ? null : 'select',
name: 'framework',
message:
typeof template === 'string' && !TEMPLATES.includes(template)
? reset(
`"${template}" isn't a valid template. Please choose from below: `
)
: reset('选择项目模板:'),
initial: 0,
choices: FRAMEWORKS.map((framework) => {
// console.log('framework: ', framework);
const frameworkColor = framework.color
return {
title: frameworkColor(framework.desc),
value: framework
}
})
},
{
type: (framework) =>
framework && framework.variants ? 'select' : null,
name: 'framework',
message: reset('选择模板能力:'),
initial: 0,
// @ts-ignore
choices: (framework) =>
framework.variants.map((variant) => {
const variantColor = variant.color
return {
title: variantColor(variant.desc),
value: variant.name
}
})
}, {
type: (framework, variant) => {
return framework === 'v2' ? null : 'select'
},
name: 'apply',
message: '系统接入方式:',
initial: 0,
// @ts-ignore
choices: () =>
APPLY_TYPE.map((variant) => {
const variantColor = variant.color
return {
title: variantColor(variant.desc),
value: variant.name
}
})
},
],
{
onCancel: () => {
throw new Error(red('✖') + ' Operation cancelled')
}
}
)
} catch (cancelled) {
console.log(cancelled.message)
return
}
// user choice associated with prompts
const { framework, overwrite, packageName, variant, apply } = result
// @ts-ignore
const root = path.join(cwd, targetDir)
if (overwrite) {
emptyDir(root)
} else if (!fs.existsSync(root)) {
fs.mkdirSync(root, { recursive: true })
}
// determine template
template = apply || variant || framework || template
console.log(blue(`\n正在目录 [${root}] 创建项目...`))
const templateDir = path.resolve(
fileURLToPath(import.meta.url),
'..',
`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) => f !== 'package.json')) {
write(file)
}
if (apply === 'micro') {
const pkg = JSON.parse(
fs.readFileSync(path.join(templateDir, `iking-main-admin/package.json`), 'utf-8')
)
pkg.name = packageName || getProjectName()
write('iking-main-admin/package.json', JSON.stringify(pkg, null, 2))
const pkg1 = JSON.parse(
fs.readFileSync(path.join(templateDir, `iking-micro-admin/package.json`), 'utf-8')
)
pkg1.name = packageName || getProjectName()
write('iking-micro-admin/package.json', JSON.stringify(pkg1, null, 2))
} else {
console.log('templateDir', templateDir);
const pkg = JSON.parse(
fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8')
)
pkg.name = packageName || getProjectName()
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(red(`\n[注意] MainApp和global目录可以通过插件iking-global-mainapp自动生成和更新`))
console.log(red(`[注意] assets和locales目录可以通过插件iking-assets-locales自动生成和更新`))
console.log(red(`[注意] 可根据项目实际情况选择是否使用以上两个插件(强烈建议使用iking-global-mainapp,后续更新只需要更新插件即可)\n`))
console.log(green(`✔ 加载完成. 现在可以执行以下命令启动项目:\n`))
if (root !== cwd) {
console.log(blue(` cd ${path.relative(cwd, root)}`))
}
switch (pkgManager) {
case 'yarn':
console.log(blue(' yarn'))
console.log(blue(' yarn dev'))
break
default:
// console.log(blue(` ${pkgManager} install`))
// console.log(blue(` ${pkgManager} run dev`))
console.log(blue(` pnpm i`))
console.log(blue(` pnpm dev`))
break
}
if (apply === 'micro') {
console.log(yellow(`\n微服务模式请阅读iking-micro-admin下的README.md文档!\n`))
}
console.log(yellow(`\n版权所有 金合信息科技股份有限公司 [IKINGTECH]!\n`))
console.log(green(`感谢使用iking-admin!\n有任何问题请及时联系我们\n`))
console.log(blue(`问题反馈地址:\nhttps://www.tapd.cn/tapd_fe/46607310/bug/list\n`))
}
/**
* @param {string | undefined} targetDir
*/
function formatTargetDir(targetDir) {
return targetDir?.trim().replace(/\/+$/g, '')
}
function copy(src, dest) {
const stat = fs.statSync(src)
if (stat.isDirectory()) {
copyDir(src, dest)
} else {
fs.copyFileSync(src, dest)
}
}
/**
* @param {string} projectName
*/
function isValidPackageName(projectName) {
return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(
projectName
)
}
/**
* @param {string} projectName
*/
function toValidPackageName(projectName) {
return projectName
.trim()
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/^[._]/, '')
.replace(/[^a-z0-9-~]+/g, '-')
}
/**
* @param {string} srcDir
* @param {string} destDir
*/
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)
}
}
/**
* @param {string} path
*/
function isEmpty(path) {
const files = fs.readdirSync(path)
return files.length === 0 || (files.length === 1 && files[0] === '.git')
}
/**
* @param {string} dir
*/
function emptyDir(dir) {
if (!fs.existsSync(dir)) {
return
}
for (const file of fs.readdirSync(dir)) {
fs.rmSync(path.resolve(dir, file), { recursive: true, force: true })
}
}
/**
* @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)
})