UNPKG

create-iking-admin

Version:

金合技术中台脚手架

377 lines (345 loc) 10.4 kB
#!/usr/bin/env node // @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) })