1、封装异步执行命令

记得修改core/exec部分

  1. function exec(command, args, options = {}) {
  2. const win32 = process.platform === 'win32'
  3. const cmd = win32 ? 'cmd' : command
  4. const cmdArgs = win32 ? ['/c'].concat(command, args) : args
  5. return require('child_process').spawn(cmd, cmdArgs, options)
  6. }
  7. function execAysnc(command, args, options = {}) {
  8. return new Promise((resolve, reject) => {
  9. const cp = exec(command, args, options)
  10. cp.on('error', function (error) {
  11. reject(error)
  12. })
  13. cp.on('exit', e => {
  14. resolve(e)
  15. })
  16. })
  17. }
  18. module.exports = { sleep, execAysnc }

2、模板安装

  1. async exec() {
  2. try {
  3. //记得加上async,使同步运行,一个trycatch可以捕获方法内的错误
  4. //1、准备阶段
  5. this.projectInfo = await this.prepare()
  6. if (!this.projectInfo) return false
  7. //2、下载模板
  8. await this.downloadTemplate()
  9. //3、安装模板
  10. await this.installTemplate()
  11. } catch (e) {
  12. log.error(e.message)
  13. }
  14. }

1、安装模板时,根据模板类型选择安装标准模板或者自定义模板

  1. const templateTypes = {
  2. TEMPLATE_TYPE_NORMAL: 'normal',
  3. TEMPLATE_TYPE_CUSTOM: 'custom'
  4. }
  5. async installTemplate() {
  6. if (this.template) {
  7. if (!this.template.npmType) {
  8. this.template.npmType = templateTypes.TEMPLATE_TYPE_NORMAL
  9. }
  10. if (this.template.npmType === templateTypes.TEMPLATE_TYPE_CUSTOM) {
  11. await this.installCustomTemplate()
  12. } else if (this.template.npmType === templateTypes.TEMPLATE_TYPE_NORMAL) {
  13. await this.installNormalTemplate()
  14. } else {
  15. throw new Error('无法识别项目模板信息')
  16. }
  17. } else {
  18. throw new Error('项目模板信息不存在')
  19. }
  20. }

1、封装安装执行命令

  1. //白名单
  2. const WHITE_COMMAND = ['npm', 'cnpm', 'yarn']
  3. //白名单检测
  4. checkWhiteCommand(cmd) {
  5. if (WHITE_COMMAND.includes(cmd)) {
  6. return cmd
  7. }
  8. return null
  9. }
  10. //安装命令
  11. async execCommand(cmd, message) {
  12. if (cmd) {
  13. const tempCmd = cmd.split(' ')
  14. const mainCmd = this.checkWhiteCommand(tempCmd[0])
  15. if (!mainCmd) throw new Error('命令不存在')
  16. const args = tempCmd.slice(1)
  17. const installRet = await execAysnc(mainCmd, args, {
  18. cwd: dir, //cwd 子进程的当前工作目录
  19. stdio: 'inherit' //inherit 将相应的stdio传给父进程或者从父进程传入,相当于process.stdin,process.stout和process.stderr
  20. })
  21. if (installRet === 0) {
  22. log.success(message + '成功')
  23. } else {
  24. throw new Error(message + '失败')
  25. }
  26. }
  27. }

2、安装标准模板

  1. //标准安装
  2. async installNormalTemplate() {
  3. const spinner = spinnerStart()
  4. try {
  5. console.log(this.pkg)
  6. //拷贝模板代码到当前目录
  7. const templatePath = path.resolve(this.pkg.cacheFilePath, 'template')
  8. const dirs = fs.readdirSync(templatePath)
  9. const filePath = path.resolve(templatePath, dirs[0])
  10. targetPath = path.resolve(process.cwd(), this.projectInfo.project)
  11. fse.ensureDirSync(filePath)
  12. fse.ensureDirSync(targetPath)
  13. fse.copySync(filePath, targetPath)
  14. } catch (error) {
  15. throw error
  16. } finally {
  17. spinner.stop(true)
  18. log.success('模板安装成功')
  19. }
  20. const ignore = ['node_modules/**', 'public/**']
  21. await this.ejsRender(ignore)
  22. await this.execCommand(this.template.installCommand, '依赖安装')
  23. await this.execCommand(this.template.startCommand, '项目启动')
  24. }

3、自定义模板 暂不开发

  1. //自定义安装
  2. async installCustomTemplate() {}

3、修改模板代码

使用 ejs 修改 haha-cli-dev-template-admin-element-vue2、haha-cli-dev-template-vue2 这个模板 template 目录下的 package.json 文件,在npm publish

  1. "name": "<%= className %>",
  2. "version": "<%= version %>",

因为发布到 npm 的版本号必须是正常的版本号,所以才需要嵌套一层,将外层作为 npm 模块,内层作为模板。

4、使用ejs进行模板渲染

  1. //ejs渲染=>将模板中的转成模板中package.json定义的版本
  2. ejsRender(ignore) {
  3. return new Promise((resolve, reject) => {
  4. //得到文件夹下除去ignore的所有路径,
  5. require('glob')(
  6. '**',
  7. {
  8. ignore,
  9. cwd: targetPath,
  10. nodir: true
  11. },
  12. (err, files) => {
  13. if (err) {
  14. reject(err)
  15. }
  16. Promise.all(
  17. files.map(file => {
  18. //得到每一个文件的具体路径
  19. const filePath = path.join(targetPath, file)
  20. return new Promise((resolve1, reject1) => {
  21. //解析文件
  22. ejs.renderFile(filePath, this.projectInfo, {}, function (err, str) {
  23. if (err) {
  24. reject1(err)
  25. } else {
  26. //使用renderFile得到的str是字符串,需要转成文件
  27. fse.writeFileSync(filePath, str)
  28. resolve1(str)
  29. }
  30. })
  31. })
  32. })
  33. )
  34. .then(() => resolve(files))
  35. .catch(err => reject(err))
  36. }
  37. )
  38. })
  39. }

5、init命令直接传入项目名称

  1. //3、选择创建项目或组件
  2. async getBaseInfo() {
  3. let info = {}
  4. function isNamevalid(val) {
  5. return /^[a-zA-Z]+([-][a-zA-Z0-9]|[_][a-zA-Z0-9]|[a-zA-Z0-9])*$/.test(val)
  6. }
  7. const { type } = await inquirer.prompt({
  8. type: 'list',
  9. message: '请选择初始化类型',
  10. name: 'type',
  11. default: TYPE_PROJECT,
  12. choices: [
  13. {
  14. name: '项目',
  15. value: TYPE_PROJECT
  16. },
  17. {
  18. name: '组件',
  19. value: TYPE_COMPONENT
  20. }
  21. ]
  22. })
  23. const promptArr = [
  24. {
  25. type: 'input',
  26. message: '请输入项目版本号',
  27. name: 'version',
  28. default: '1.0.0',
  29. validate: function (val) {
  30. const done = this.async()
  31. setTimeout(function () {
  32. //!!semver.valid(val) !!转成Boolean类型
  33. if (!!!semver.valid(val)) {
  34. done('请输入合法的版本号')
  35. return
  36. }
  37. done(null, true)
  38. }, 0)
  39. },
  40. filter: val => {
  41. if (!!semver.valid(val)) {
  42. return semver.valid(val)
  43. }
  44. return val
  45. }
  46. },
  47. {
  48. type: 'list',
  49. message: '请选择项目模板',
  50. name: 'npmName',
  51. choices: this.createTemplateChoice()
  52. }
  53. ]
  54. if (type === TYPE_COMPONENT) {
  55. }
  56. if (type === TYPE_PROJECT) {
  57. const projectPromt = {
  58. type: 'input',
  59. message: '请输入项目名称',
  60. name: 'project',
  61. default: 'HahaDemo',
  62. validate: function (val) {
  63. //检查项目名称和版本号的合法性
  64. const done = this.async()
  65. setTimeout(function () {
  66. //1、必须首字母大写,
  67. //2、尾字符必须为英文或者数字,不能为字符
  68. //3、字符仅允许'-_'
  69. //类型合法有:a a-b a_b a-b-c a_b_c a1_b1_c1 a1 a1-b1-c1
  70. if (!isNamevalid(val)) {
  71. done('请输入合法的项目名称(要求英文字母开头,数字或字母结尾,字符只允许使用 - 以及 _)')
  72. return
  73. }
  74. done(null, true)
  75. }, 0)
  76. }
  77. }
  78. //命令行输入的projectName是否合法,不合法则重新填入项目名称
  79. if (!isNamevalid(this.projectName)) {
  80. promptArr.unshift(projectPromt)
  81. } else {
  82. info.project = this.projectName
  83. }
  84. const result = await inquirer.prompt(promptArr)
  85. if (result?.project) {
  86. info.project = result?.project
  87. }
  88. //4、获取项目的基本信息
  89. return {
  90. type,
  91. ...result,
  92. ...info,
  93. className: require('kebab-case')(info.project).replace(/^-/, '')
  94. }
  95. }
  96. }

6、完整代码

  1. 'use strict'
  2. const fs = require('fs')
  3. const path = require('path')
  4. const inquirer = require('inquirer')
  5. const fse = require('fs-extra')
  6. const semver = require('semver')
  7. const ejs = require('ejs')
  8. const Command = require('@haha-cli-dev/command')
  9. const log = require('@haha-cli-dev/log')
  10. const packages = require('@haha-cli-dev/packages')
  11. const { spinnerStart, execAysnc } = require('@haha-cli-dev/utils')
  12. const { getTemplate } = require('./template')
  13. const TYPE_PROJECT = 'project'
  14. const TYPE_COMPONENT = 'component'
  15. const templateTypes = {
  16. TEMPLATE_TYPE_NORMAL: 'normal',
  17. TEMPLATE_TYPE_CUSTOM: 'custom'
  18. }
  19. let targetPath
  20. //白名单
  21. const WHITE_COMMAND = ['npm', 'cnpm', 'yarn']
  22. class initCommand extends Command {
  23. init() {
  24. this.projectName = this._argv[0] || ''
  25. }
  26. /*
  27. 1、准备阶段
  28. 2、下载模板
  29. 3、安装模板
  30. */
  31. async exec() {
  32. try {
  33. //记得加上async,使同步运行,一个trycatch可以捕获方法内的错误
  34. //1、准备阶段
  35. this.projectInfo = await this.prepare()
  36. console.log('this.projectInfo', this.projectInfo)
  37. if (!this.projectInfo) return false
  38. //2、下载模板
  39. await this.downloadTemplate()
  40. //3、安装模板
  41. await this.installTemplate()
  42. } catch (e) {
  43. log.error(e?.message)
  44. if (process.env.LOG_LEVEL === 'verbose') {
  45. console.log('e', e)
  46. }
  47. }
  48. }
  49. async installTemplate() {
  50. if (this.template) {
  51. if (!this.template.npmType) {
  52. this.template.npmType = templateTypes.TEMPLATE_TYPE_NORMAL
  53. }
  54. if (this.template.npmType === templateTypes.TEMPLATE_TYPE_CUSTOM) {
  55. await this.installCustomTemplate()
  56. } else if (this.template.npmType === templateTypes.TEMPLATE_TYPE_NORMAL) {
  57. await this.installNormalTemplate()
  58. } else {
  59. throw new Error('无法识别项目模板信息')
  60. }
  61. } else {
  62. throw new Error('项目模板信息不存在')
  63. }
  64. }
  65. //自定义安装
  66. async installCustomTemplate() {}
  67. //标准安装
  68. async installNormalTemplate() {
  69. const spinner = spinnerStart('正在安装模板...')
  70. try {
  71. //拷贝模板代码到当前目录
  72. const templatePath = path.resolve(this.pkg.cacheFilePath, 'template')
  73. const dirs = fs.readdirSync(templatePath)
  74. const filePath = path.resolve(templatePath, dirs[0])
  75. targetPath = path.resolve(process.cwd(), this.projectInfo.project)
  76. fse.ensureDirSync(filePath)
  77. fse.ensureDirSync(targetPath)
  78. fse.copySync(filePath, targetPath)
  79. } catch (error) {
  80. throw error
  81. } finally {
  82. spinner.stop(true)
  83. log.success('模板安装成功')
  84. }
  85. const ignore = ['**/node_modules/**', ...this.template?.ignore.split(',')]
  86. await this.ejsRender(ignore)
  87. await this.execCommand(this.template.installCommand, '依赖安装')
  88. await this.execCommand(this.template.startCommand, '项目启动')
  89. }
  90. //ejs渲染=>将模板中的转成模板中package.json定义的版本
  91. ejsRender(ignore) {
  92. return new Promise((resolve, reject) => {
  93. //得到文件夹下除去ignore的所有路径,
  94. require('glob')(
  95. '**',
  96. {
  97. ignore,
  98. cwd: targetPath,
  99. nodir: true
  100. },
  101. (err, files) => {
  102. if (err) {
  103. reject(err)
  104. }
  105. Promise.all(
  106. files.map(file => {
  107. //得到每一个文件的具体路径
  108. const filePath = path.join(targetPath, file)
  109. return new Promise((resolve1, reject1) => {
  110. //解析文件
  111. ejs.renderFile(filePath, this.projectInfo, {}, function (err, str) {
  112. if (err) {
  113. reject1(err)
  114. } else {
  115. //使用renderFile得到的str是字符串,需要转成文件
  116. fse.writeFileSync(filePath, str)
  117. resolve1(str)
  118. }
  119. })
  120. })
  121. })
  122. )
  123. .then(() => resolve(files))
  124. .catch(err => reject(err))
  125. }
  126. )
  127. })
  128. }
  129. //白名单检测
  130. checkWhiteCommand(cmd) {
  131. if (WHITE_COMMAND.includes(cmd)) {
  132. return cmd
  133. }
  134. return null
  135. }
  136. //安装命令
  137. async execCommand(cmd, message) {
  138. if (cmd) {
  139. const tempCmd = cmd.split(' ')
  140. const mainCmd = this.checkWhiteCommand(tempCmd[0])
  141. if (!mainCmd) throw new Error('命令不存在')
  142. const args = tempCmd.slice(1)
  143. const installRet = await execAysnc(mainCmd, args, {
  144. cwd: targetPath, //cwd 子进程的当前工作目录
  145. stdio: 'inherit' //inherit 将相应的stdio传给父进程或者从父进程传入,相当于process.stdin,process.stout和process.stderr
  146. })
  147. if (installRet === 0) {
  148. log.success(message + '成功')
  149. } else {
  150. throw new Error(message + '失败')
  151. }
  152. }
  153. }
  154. /*
  155. 下载模板
  156. 1、通过项目模板API获取项目模板信息
  157. 1.1通过egg.js搭建后端系统
  158. 1.2通过npm存储项目模板
  159. 1.3将项目模板信息存储到mongoDB数据库中
  160. 1.4通过egg.js获取mongoDB的数据并通过API返回
  161. 3、选择创建组件或项目
  162. 4、获取项目的基本信息
  163. */
  164. async downloadTemplate() {
  165. const homePath = process.env.CLI_HOME_PATH
  166. const targetPath = path.resolve(homePath, 'templates')
  167. const storeDir = path.resolve(targetPath, 'node_modules')
  168. this.template = this.templates.find(item => item.npmName === this.projectInfo.npmName)
  169. const { npmName, version } = this.template
  170. this.pkg = new packages({
  171. targetPath,
  172. storeDir,
  173. packageName: npmName,
  174. packageVersion: version
  175. })
  176. if (await this.pkg.exists()) {
  177. const spinner = spinnerStart('模板更新中,请稍候')
  178. try {
  179. //更新
  180. await this.pkg.update()
  181. } catch (error) {
  182. //抛出异常,让上层捕获
  183. throw error
  184. } finally {
  185. //解决下载出错时,仍提示下载中的情况
  186. spinner.stop(true)
  187. if (await this.pkg.exists()) {
  188. log.success('模板更新成功')
  189. }
  190. }
  191. } else {
  192. const spinner = spinnerStart('模板下载中,请稍候')
  193. try {
  194. //初始化
  195. await this.pkg.install()
  196. } catch (error) {
  197. //抛出异常,让上层捕获
  198. throw error
  199. } finally {
  200. //解决下载出错时,仍提示下载中的情况
  201. spinner.stop(true)
  202. if (await this.pkg.exists()) {
  203. log.success('模板下载成功')
  204. }
  205. }
  206. }
  207. }
  208. /*
  209. 准备阶段
  210. 1、判断当前目录是否为空
  211. 1.1 询问是否继续创建
  212. 2、是否启动强制更新
  213. 3、选择创建组件或项目
  214. 4、获取项目的基本信息
  215. */
  216. async prepare() {
  217. //当前执行node命令时候的文件夹地址 ——工作目录
  218. const localPath = process.cwd()
  219. const force = this._argv[1]?.force
  220. let isContinue = false
  221. this.templates = await getTemplate()
  222. //判断模板是否存在
  223. if (!this.templates || this.templates.length === 0) {
  224. throw new Error('当前不存在项目模板')
  225. }
  226. if (this.isCmdEmpty(localPath)) {
  227. //1.1 询问是否继续创建
  228. if (!force) {
  229. const res = await inquirer.prompt({
  230. type: 'confirm',
  231. name: 'isContinue',
  232. message: '当前文件夹内容不为空,是否在此继续创建项目?',
  233. default: false
  234. })
  235. isContinue = res.isContinue
  236. if (!isContinue) {
  237. return false
  238. }
  239. }
  240. }
  241. // 2.是否启动强制安装
  242. if (isContinue || force) {
  243. const { isDelete } = await inquirer.prompt({
  244. type: 'confirm',
  245. name: 'isDelete',
  246. message: '是否清空当前目录下的文件?',
  247. default: false
  248. })
  249. if (isDelete) {
  250. // 清空当前目录
  251. fse.emptyDirSync(localPath)
  252. }
  253. }
  254. return this.getBaseInfo()
  255. }
  256. //3、选择创建项目或组件
  257. async getBaseInfo() {
  258. let info = {}
  259. function isNamevalid(val) {
  260. return /^[a-zA-Z]+([-][a-zA-Z0-9]|[_][a-zA-Z0-9]|[a-zA-Z0-9])*$/.test(val)
  261. }
  262. const { type } = await inquirer.prompt({
  263. type: 'list',
  264. message: '请选择初始化类型',
  265. name: 'type',
  266. default: TYPE_PROJECT,
  267. choices: [
  268. {
  269. name: '项目',
  270. value: TYPE_PROJECT
  271. },
  272. {
  273. name: '组件',
  274. value: TYPE_COMPONENT
  275. }
  276. ]
  277. })
  278. const title = TYPE_PROJECT === type ? '项目' : '组件'
  279. const promptArr = [
  280. {
  281. type: 'input',
  282. message: `请输入${title}版本号`,
  283. name: 'version',
  284. default: '1.0.0',
  285. validate: val => !!semver.valid(val) || '请输入合法的版本号',
  286. filter: val => {
  287. if (!!semver.valid(val)) {
  288. return semver.valid(val)
  289. }
  290. return val
  291. }
  292. },
  293. {
  294. type: 'list',
  295. message: `请选择${title}模板`,
  296. name: 'npmName',
  297. choices: this.createTemplateChoice(type)
  298. }
  299. ]
  300. const projectPromt = {
  301. type: 'input',
  302. message: `请输入${title}名称`,
  303. name: 'project',
  304. default: 'HahaDemo',
  305. validate: function (val) {
  306. //检查项目名称和版本号的合法性
  307. const done = this.async()
  308. setTimeout(function () {
  309. //1、必须首字母大写,
  310. //2、尾字符必须为英文或者数字,不能为字符
  311. //3、字符仅允许'-_'
  312. //类型合法有:a a-b a_b a-b-c a_b_c a1_b1_c1 a1 a1-b1-c1
  313. if (!isNamevalid(val)) {
  314. done(`请输入合法的${title}名称(要求英文字母开头,数字或字母结尾,字符只允许使用 - 以及 _)`)
  315. return
  316. }
  317. done(null, true)
  318. }, 0)
  319. }
  320. }
  321. //命令行输入的projectName是否合法,不合法则重新填入项目名称
  322. if (!isNamevalid(this.projectName)) {
  323. promptArr.unshift(projectPromt)
  324. } else {
  325. info.project = this.projectName
  326. }
  327. if (type === TYPE_COMPONENT) {
  328. const descriptPrompt = {
  329. type: 'input',
  330. message: `请输入${title}描述`,
  331. name: 'description',
  332. validate: val => {
  333. if (!val) {
  334. return '组件描述不可以为空'
  335. }
  336. return true
  337. }
  338. }
  339. promptArr.push(descriptPrompt)
  340. }
  341. const result = await inquirer.prompt(promptArr)
  342. if (result?.project) {
  343. info.project = result?.project
  344. }
  345. //4、获取项目的基本信息
  346. return {
  347. type,
  348. ...result,
  349. ...info,
  350. className: require('kebab-case')(info.project).replace(/^-/, '')
  351. }
  352. }
  353. createTemplateChoice(type) {
  354. return this.templates
  355. .filter(item => item.tag === type)
  356. ?.map(item => ({
  357. name: item.name,
  358. value: item.npmName
  359. }))
  360. }
  361. //判断当前路径是否不为空
  362. isCmdEmpty(localPath) {
  363. let fileList = fs.readdirSync(localPath)
  364. fileList = fileList.filter(item => !item.startsWith('.') && item !== 'node_modules')
  365. return fileList && fileList.length > 0
  366. }
  367. }
  368. function init(argv) {
  369. return new initCommand(argv)
  370. }
  371. module.exports = init
  372. module.exports.initCommand = initCommand
  373. 'use strict'
  374. const fs = require('fs')
  375. const path = require('path')
  376. const inquirer = require('inquirer')
  377. const fse = require('fs-extra')
  378. const semver = require('semver')
  379. const ejs = require('ejs')
  380. const Command = require('@haha-cli-dev/command')
  381. const log = require('@haha-cli-dev/log')
  382. const packages = require('@haha-cli-dev/packages')
  383. const { spinnerStart, execAysnc } = require('@haha-cli-dev/utils')
  384. const { getTemplate } = require('./template')
  385. const TYPE_PROJECT = 'project'
  386. const TYPE_COMPONENT = 'component'
  387. const templateTypes = {
  388. TEMPLATE_TYPE_NORMAL: 'normal',
  389. TEMPLATE_TYPE_CUSTOM: 'custom'
  390. }
  391. let targetPath
  392. //白名单
  393. const WHITE_COMMAND = ['npm', 'cnpm', 'yarn']
  394. class initCommand extends Command {
  395. init() {
  396. this.projectName = this._argv[0] || ''
  397. }
  398. /*
  399. 1、准备阶段
  400. 2、下载模板
  401. 3、安装模板
  402. */
  403. async exec() {
  404. try {
  405. //记得加上async,使同步运行,一个trycatch可以捕获方法内的错误
  406. //1、准备阶段
  407. this.projectInfo = await this.prepare()
  408. console.log('this.projectInfo', this.projectInfo)
  409. if (!this.projectInfo) return false
  410. //2、下载模板
  411. await this.downloadTemplate()
  412. //3、安装模板
  413. await this.installTemplate()
  414. } catch (e) {
  415. log.error(e?.message)
  416. if (process.env.LOG_LEVEL === 'verbose') {
  417. console.log('e', e)
  418. }
  419. }
  420. }
  421. async installTemplate() {
  422. if (this.template) {
  423. if (!this.template.npmType) {
  424. this.template.npmType = templateTypes.TEMPLATE_TYPE_NORMAL
  425. }
  426. if (this.template.npmType === templateTypes.TEMPLATE_TYPE_CUSTOM) {
  427. await this.installCustomTemplate()
  428. } else if (this.template.npmType === templateTypes.TEMPLATE_TYPE_NORMAL) {
  429. await this.installNormalTemplate()
  430. } else {
  431. throw new Error('无法识别项目模板信息')
  432. }
  433. } else {
  434. throw new Error('项目模板信息不存在')
  435. }
  436. }
  437. //自定义安装
  438. async installCustomTemplate() {}
  439. //标准安装
  440. async installNormalTemplate() {
  441. const spinner = spinnerStart('正在安装模板...')
  442. try {
  443. //拷贝模板代码到当前目录
  444. const templatePath = path.resolve(this.pkg.cacheFilePath, 'template')
  445. const dirs = fs.readdirSync(templatePath)
  446. const filePath = path.resolve(templatePath, dirs[0])
  447. targetPath = path.resolve(process.cwd(), this.projectInfo.project)
  448. fse.ensureDirSync(filePath)
  449. fse.ensureDirSync(targetPath)
  450. fse.copySync(filePath, targetPath)
  451. } catch (error) {
  452. throw error
  453. } finally {
  454. spinner.stop(true)
  455. log.success('模板安装成功')
  456. }
  457. const ignore = ['**/node_modules/**', ...this.template?.ignore.split(',')]
  458. await this.ejsRender(ignore)
  459. await this.execCommand(this.template.installCommand, '依赖安装')
  460. await this.execCommand(this.template.startCommand, '项目启动')
  461. }
  462. //ejs渲染=>将模板中的转成模板中package.json定义的版本
  463. ejsRender(ignore) {
  464. return new Promise((resolve, reject) => {
  465. //得到文件夹下除去ignore的所有路径,
  466. require('glob')(
  467. '**',
  468. {
  469. ignore,
  470. cwd: targetPath,
  471. nodir: true
  472. },
  473. (err, files) => {
  474. if (err) {
  475. reject(err)
  476. }
  477. Promise.all(
  478. files.map(file => {
  479. //得到每一个文件的具体路径
  480. const filePath = path.join(targetPath, file)
  481. return new Promise((resolve1, reject1) => {
  482. //解析文件
  483. ejs.renderFile(filePath, this.projectInfo, {}, function (err, str) {
  484. if (err) {
  485. reject1(err)
  486. } else {
  487. //使用renderFile得到的str是字符串,需要转成文件
  488. fse.writeFileSync(filePath, str)
  489. resolve1(str)
  490. }
  491. })
  492. })
  493. })
  494. )
  495. .then(() => resolve(files))
  496. .catch(err => reject(err))
  497. }
  498. )
  499. })
  500. }
  501. //白名单检测
  502. checkWhiteCommand(cmd) {
  503. if (WHITE_COMMAND.includes(cmd)) {
  504. return cmd
  505. }
  506. return null
  507. }
  508. //安装命令
  509. async execCommand(cmd, message) {
  510. if (cmd) {
  511. const tempCmd = cmd.split(' ')
  512. const mainCmd = this.checkWhiteCommand(tempCmd[0])
  513. if (!mainCmd) throw new Error('命令不存在')
  514. const args = tempCmd.slice(1)
  515. const installRet = await execAysnc(mainCmd, args, {
  516. cwd: targetPath, //cwd 子进程的当前工作目录
  517. stdio: 'inherit' //inherit 将相应的stdio传给父进程或者从父进程传入,相当于process.stdin,process.stout和process.stderr
  518. })
  519. if (installRet === 0) {
  520. log.success(message + '成功')
  521. } else {
  522. throw new Error(message + '失败')
  523. }
  524. }
  525. }
  526. /*
  527. 下载模板
  528. 1、通过项目模板API获取项目模板信息
  529. 1.1通过egg.js搭建后端系统
  530. 1.2通过npm存储项目模板
  531. 1.3将项目模板信息存储到mongoDB数据库中
  532. 1.4通过egg.js获取mongoDB的数据并通过API返回
  533. 3、选择创建组件或项目
  534. 4、获取项目的基本信息
  535. */
  536. async downloadTemplate() {
  537. const homePath = process.env.CLI_HOME_PATH
  538. const targetPath = path.resolve(homePath, 'templates')
  539. const storeDir = path.resolve(targetPath, 'node_modules')
  540. this.template = this.templates.find(item => item.npmName === this.projectInfo.npmName)
  541. const { npmName, version } = this.template
  542. this.pkg = new packages({
  543. targetPath,
  544. storeDir,
  545. packageName: npmName,
  546. packageVersion: version
  547. })
  548. if (await this.pkg.exists()) {
  549. const spinner = spinnerStart('模板更新中,请稍候')
  550. try {
  551. //更新
  552. await this.pkg.update()
  553. } catch (error) {
  554. //抛出异常,让上层捕获
  555. throw error
  556. } finally {
  557. //解决下载出错时,仍提示下载中的情况
  558. spinner.stop(true)
  559. if (await this.pkg.exists()) {
  560. log.success('模板更新成功')
  561. }
  562. }
  563. } else {
  564. const spinner = spinnerStart('模板下载中,请稍候')
  565. try {
  566. //初始化
  567. await this.pkg.install()
  568. } catch (error) {
  569. //抛出异常,让上层捕获
  570. throw error
  571. } finally {
  572. //解决下载出错时,仍提示下载中的情况
  573. spinner.stop(true)
  574. if (await this.pkg.exists()) {
  575. log.success('模板下载成功')
  576. }
  577. }
  578. }
  579. }
  580. /*
  581. 准备阶段
  582. 1、判断当前目录是否为空
  583. 1.1 询问是否继续创建
  584. 2、是否启动强制更新
  585. 3、选择创建组件或项目
  586. 4、获取项目的基本信息
  587. */
  588. async prepare() {
  589. //当前执行node命令时候的文件夹地址 ——工作目录
  590. const localPath = process.cwd()
  591. const force = this._argv[1]?.force
  592. let isContinue = false
  593. this.templates = await getTemplate()
  594. //判断模板是否存在
  595. if (!this.templates || this.templates.length === 0) {
  596. throw new Error('当前不存在项目模板')
  597. }
  598. if (this.isCmdEmpty(localPath)) {
  599. //1.1 询问是否继续创建
  600. if (!force) {
  601. const res = await inquirer.prompt({
  602. type: 'confirm',
  603. name: 'isContinue',
  604. message: '当前文件夹内容不为空,是否在此继续创建项目?',
  605. default: false
  606. })
  607. isContinue = res.isContinue
  608. if (!isContinue) {
  609. return false
  610. }
  611. }
  612. }
  613. // 2.是否启动强制安装
  614. if (isContinue || force) {
  615. const { isDelete } = await inquirer.prompt({
  616. type: 'confirm',
  617. name: 'isDelete',
  618. message: '是否清空当前目录下的文件?',
  619. default: false
  620. })
  621. if (isDelete) {
  622. // 清空当前目录
  623. fse.emptyDirSync(localPath)
  624. }
  625. }
  626. return this.getBaseInfo()
  627. }
  628. //3、选择创建项目或组件
  629. async getBaseInfo() {
  630. let info = {}
  631. function isNamevalid(val) {
  632. return /^[a-zA-Z]+([-][a-zA-Z0-9]|[_][a-zA-Z0-9]|[a-zA-Z0-9])*$/.test(val)
  633. }
  634. const { type } = await inquirer.prompt({
  635. type: 'list',
  636. message: '请选择初始化类型',
  637. name: 'type',
  638. default: TYPE_PROJECT,
  639. choices: [
  640. {
  641. name: '项目',
  642. value: TYPE_PROJECT
  643. },
  644. {
  645. name: '组件',
  646. value: TYPE_COMPONENT
  647. }
  648. ]
  649. })
  650. const title = TYPE_PROJECT === type ? '项目' : '组件'
  651. const promptArr = [
  652. {
  653. type: 'input',
  654. message: `请输入${title}版本号`,
  655. name: 'version',
  656. default: '1.0.0',
  657. validate: val => !!semver.valid(val) || '请输入合法的版本号',
  658. filter: val => {
  659. if (!!semver.valid(val)) {
  660. return semver.valid(val)
  661. }
  662. return val
  663. }
  664. },
  665. {
  666. type: 'list',
  667. message: `请选择${title}模板`,
  668. name: 'npmName',
  669. choices: this.createTemplateChoice(type)
  670. }
  671. ]
  672. const projectPromt = {
  673. type: 'input',
  674. message: `请输入${title}名称`,
  675. name: 'project',
  676. default: 'HahaDemo',
  677. validate: function (val) {
  678. //检查项目名称和版本号的合法性
  679. const done = this.async()
  680. setTimeout(function () {
  681. //1、必须首字母大写,
  682. //2、尾字符必须为英文或者数字,不能为字符
  683. //3、字符仅允许'-_'
  684. //类型合法有:a a-b a_b a-b-c a_b_c a1_b1_c1 a1 a1-b1-c1
  685. if (!isNamevalid(val)) {
  686. done(`请输入合法的${title}名称(要求英文字母开头,数字或字母结尾,字符只允许使用 - 以及 _)`)
  687. return
  688. }
  689. done(null, true)
  690. }, 0)
  691. }
  692. }
  693. //命令行输入的projectName是否合法,不合法则重新填入项目名称
  694. if (!isNamevalid(this.projectName)) {
  695. promptArr.unshift(projectPromt)
  696. } else {
  697. info.project = this.projectName
  698. }
  699. if (type === TYPE_COMPONENT) {
  700. const descriptPrompt = {
  701. type: 'input',
  702. message: `请输入${title}描述`,
  703. name: 'description',
  704. validate: val => {
  705. if (!val) {
  706. return '组件描述不可以为空'
  707. }
  708. return true
  709. }
  710. }
  711. promptArr.push(descriptPrompt)
  712. }
  713. const result = await inquirer.prompt(promptArr)
  714. if (result?.project) {
  715. info.project = result?.project
  716. }
  717. //4、获取项目的基本信息
  718. return {
  719. type,
  720. ...result,
  721. ...info,
  722. className: require('kebab-case')(info.project).replace(/^-/, '')
  723. }
  724. }
  725. createTemplateChoice(type) {
  726. return this.templates
  727. .filter(item => item.tag === type)
  728. ?.map(item => ({
  729. name: item.name,
  730. value: item.npmName
  731. }))
  732. }
  733. //判断当前路径是否不为空
  734. isCmdEmpty(localPath) {
  735. let fileList = fs.readdirSync(localPath)
  736. fileList = fileList.filter(item => !item.startsWith('.') && item !== 'node_modules')
  737. return fileList && fileList.length > 0
  738. }
  739. }
  740. function init(argv) {
  741. return new initCommand(argv)
  742. }
  743. module.exports = init
  744. module.exports.initCommand = initCommand
  745. 'use strict'
  746. const fs = require('fs')
  747. const path = require('path')
  748. const inquirer = require('inquirer')
  749. const fse = require('fs-extra')
  750. const semver = require('semver')
  751. const ejs = require('ejs')
  752. const Command = require('@haha-cli-dev/command')
  753. const log = require('@haha-cli-dev/log')
  754. const packages = require('@haha-cli-dev/packages')
  755. const { spinnerStart, execAysnc } = require('@haha-cli-dev/utils')
  756. const { getTemplate } = require('./template')
  757. const TYPE_PROJECT = 'project'
  758. const TYPE_COMPONENT = 'component'
  759. const templateTypes = {
  760. TEMPLATE_TYPE_NORMAL: 'normal',
  761. TEMPLATE_TYPE_CUSTOM: 'custom'
  762. }
  763. let dir = path.resolve(process.cwd(), 'vue-test2')
  764. //白名单
  765. const WHITE_COMMAND = ['npm', 'cnpm', 'yarn']
  766. class initCommand extends Command {
  767. init() {
  768. this.projectName = this._argv[0]
  769. }
  770. /*
  771. 1、准备阶段
  772. 2、下载模板
  773. 3、安装模板
  774. */
  775. async exec() {
  776. try {
  777. //记得加上async,使同步运行,一个trycatch可以捕获方法内的错误
  778. //1、准备阶段
  779. this.projectInfo = await this.prepare()
  780. if (!this.projectInfo) return false
  781. //2、下载模板
  782. await this.downloadTemplate()
  783. //3、安装模板
  784. await this.installTemplate()
  785. } catch (e) {
  786. log.error(e?.message)
  787. if (process.env.LOG_LEVEL === 'verbose') {
  788. console.log('e', e)
  789. }
  790. }
  791. }
  792. async installTemplate() {
  793. if (this.template) {
  794. if (!this.template.npmType) {
  795. this.template.npmType = templateTypes.TEMPLATE_TYPE_NORMAL
  796. }
  797. if (this.template.npmType === templateTypes.TEMPLATE_TYPE_CUSTOM) {
  798. await this.installCustomTemplate()
  799. } else if (this.template.npmType === templateTypes.TEMPLATE_TYPE_NORMAL) {
  800. await this.installNormalTemplate()
  801. } else {
  802. throw new Error('无法识别项目模板信息')
  803. }
  804. } else {
  805. throw new Error('项目模板信息不存在')
  806. }
  807. }
  808. //自定义安装
  809. async installCustomTemplate() {}
  810. //标准安装
  811. async installNormalTemplate() {
  812. const spinner = spinnerStart()
  813. try {
  814. console.log(this.pkg)
  815. //拷贝模板代码到当前目录
  816. const templatePath = path.resolve(this.pkg.cacheFilePath, 'template')
  817. const targetPath = process.cwd()
  818. fse.ensureDirSync(templatePath)
  819. fse.ensureDirSync(targetPath)
  820. fse.copySync(templatePath, targetPath)
  821. } catch (error) {
  822. throw error
  823. } finally {
  824. spinner.stop(true)
  825. log.success('模板安装成功')
  826. }
  827. const ignore = ['node_modules/**', 'public/**']
  828. await this.ejsRender(ignore)
  829. await this.execCommand(this.template.installCommand, '依赖安装')
  830. await this.execCommand(this.template.startCommand, '项目启动')
  831. }
  832. //ejs渲染=>将模板中的转成模板中package.json定义的版本
  833. ejsRender(ignore) {
  834. return new Promise((resolve, reject) => {
  835. //得到文件夹下除去ignore的所有路径,
  836. require('glob')(
  837. '**',
  838. {
  839. ignore,
  840. cwd: dir,
  841. nodir: true
  842. },
  843. (err, files) => {
  844. if (err) {
  845. reject(err)
  846. }
  847. Promise.all(
  848. files.map(file => {
  849. //得到每一个文件的具体路径
  850. const filePath = path.join(dir, file)
  851. return new Promise((resolve1, reject1) => {
  852. //解析文件
  853. ejs.renderFile(filePath, this.projectInfo, {}, function (err, str) {
  854. if (err) {
  855. reject1(err)
  856. } else {
  857. //使用renderFile得到的str是字符串,需要转成文件
  858. fse.writeFileSync(filePath, str)
  859. resolve1(str)
  860. }
  861. })
  862. })
  863. })
  864. )
  865. .then(() => resolve(files))
  866. .catch(err => reject(err))
  867. }
  868. )
  869. })
  870. }
  871. //白名单检测
  872. checkWhiteCommand(cmd) {
  873. if (WHITE_COMMAND.includes(cmd)) {
  874. return cmd
  875. }
  876. return null
  877. }
  878. //安装命令
  879. async execCommand(cmd, message) {
  880. if (cmd) {
  881. const tempCmd = cmd.split(' ')
  882. const mainCmd = this.checkWhiteCommand(tempCmd[0])
  883. if (!mainCmd) throw new Error('命令不存在')
  884. const args = tempCmd.slice(1)
  885. const installRet = await execAysnc(mainCmd, args, {
  886. cwd: dir, //cwd 子进程的当前工作目录
  887. stdio: 'inherit' //inherit 将相应的stdio传给父进程或者从父进程传入,相当于process.stdin,process.stout和process.stderr
  888. })
  889. if (installRet === 0) {
  890. log.success(message + '成功')
  891. } else {
  892. throw new Error(message + '失败')
  893. }
  894. }
  895. }
  896. /*
  897. 下载模板
  898. 1、通过项目模板API获取项目模板信息
  899. 1.1通过egg.js搭建后端系统
  900. 1.2通过npm存储项目模板
  901. 1.3将项目模板信息存储到mongoDB数据库中
  902. 1.4通过egg.js获取mongoDB的数据并通过API返回
  903. 3、选择创建组件或项目
  904. 4、获取项目的基本信息
  905. */
  906. async downloadTemplate() {
  907. const homePath = process.env.CLI_HOME_PATH
  908. const targetPath = path.resolve(homePath, 'templates')
  909. const storeDir = path.resolve(targetPath, 'node_modules')
  910. this.template = this.templates.find(item => item.npmName === this.projectInfo.npmName)
  911. const { npmName, version } = this.template
  912. this.pkg = new packages({
  913. targetPath,
  914. storeDir,
  915. packageName: npmName,
  916. packageVersion: version
  917. })
  918. if (await this.pkg.exists()) {
  919. const spinner = spinnerStart('模板更新中,请稍候')
  920. try {
  921. //更新
  922. await this.pkg.update()
  923. } catch (error) {
  924. //抛出异常,让上层捕获
  925. throw error
  926. } finally {
  927. //解决下载出错时,仍提示下载中的情况
  928. spinner.stop(true)
  929. if (await this.pkg.exists()) {
  930. log.success('模板更新成功')
  931. }
  932. }
  933. } else {
  934. const spinner = spinnerStart('模板下载中,请稍候')
  935. try {
  936. //初始化
  937. await this.pkg.install()
  938. } catch (error) {
  939. //抛出异常,让上层捕获
  940. throw error
  941. } finally {
  942. //解决下载出错时,仍提示下载中的情况
  943. spinner.stop(true)
  944. if (await this.pkg.exists()) {
  945. log.success('模板下载成功')
  946. }
  947. }
  948. }
  949. }
  950. /*
  951. 准备阶段
  952. 1、判断当前目录是否为空
  953. 1.1 询问是否继续创建
  954. 2、是否启动强制更新
  955. 3、选择创建组件或项目
  956. 4、获取项目的基本信息
  957. */
  958. async prepare() {
  959. //当前执行node命令时候的文件夹地址 ——工作目录
  960. const localPath = process.cwd()
  961. const force = this._argv[1]?.force
  962. let isContinue = false
  963. this.templates = await getTemplate()
  964. //判断模板是否存在
  965. if (!this.templates || this.templates.length === 0) {
  966. throw new Error('当前不存在项目模板')
  967. }
  968. if (this.isCmdEmpty(localPath)) {
  969. //1.1 询问是否继续创建
  970. if (!force) {
  971. const res = await inquirer.prompt({
  972. type: 'confirm',
  973. name: 'isContinue',
  974. message: '当前文件夹内容不为空,是否在此继续创建项目?',
  975. default: false
  976. })
  977. isContinue = res.isContinue
  978. if (!isContinue) {
  979. return false
  980. }
  981. }
  982. }
  983. // 2.是否启动强制安装
  984. if (isContinue || force) {
  985. const { isDelete } = await inquirer.prompt({
  986. type: 'confirm',
  987. name: 'isDelete',
  988. message: '是否清空当前目录下的文件?',
  989. default: false
  990. })
  991. if (isDelete) {
  992. // 清空当前目录
  993. fse.emptyDirSync(localPath)
  994. }
  995. }
  996. return this.getBaseInfo()
  997. }
  998. //3、选择创建项目或组件
  999. async getBaseInfo() {
  1000. let info = {}
  1001. function isNamevalid(val) {
  1002. return /^[a-zA-Z]+([-][a-zA-Z0-9]|[_][a-zA-Z0-9]|[a-zA-Z0-9])*$/.test(val)
  1003. }
  1004. const { type } = await inquirer.prompt({
  1005. type: 'list',
  1006. message: '请选择初始化类型',
  1007. name: 'type',
  1008. default: TYPE_PROJECT,
  1009. choices: [
  1010. {
  1011. name: '项目',
  1012. value: TYPE_PROJECT
  1013. },
  1014. {
  1015. name: '组件',
  1016. value: TYPE_COMPONENT
  1017. }
  1018. ]
  1019. })
  1020. const promptArr = [
  1021. {
  1022. type: 'input',
  1023. message: '请输入项目版本号',
  1024. name: 'version',
  1025. default: '1.0.0',
  1026. validate: function (val) {
  1027. const done = this.async()
  1028. setTimeout(function () {
  1029. //!!semver.valid(val) !!转成Boolean类型
  1030. if (!!!semver.valid(val)) {
  1031. done('请输入合法的版本号')
  1032. return
  1033. }
  1034. done(null, true)
  1035. }, 0)
  1036. },
  1037. filter: val => {
  1038. if (!!semver.valid(val)) {
  1039. return semver.valid(val)
  1040. }
  1041. return val
  1042. }
  1043. },
  1044. {
  1045. type: 'list',
  1046. message: '请选择项目模板',
  1047. name: 'npmName',
  1048. choices: this.createTemplateChoice()
  1049. }
  1050. ]
  1051. if (type === TYPE_COMPONENT) {
  1052. }
  1053. if (type === TYPE_PROJECT) {
  1054. const projectPromt = {
  1055. type: 'input',
  1056. message: '请输入项目名称',
  1057. name: 'project',
  1058. default: 'HahaDemo',
  1059. validate: function (val) {
  1060. //检查项目名称和版本号的合法性
  1061. const done = this.async()
  1062. setTimeout(function () {
  1063. //1、必须首字母大写,
  1064. //2、尾字符必须为英文或者数字,不能为字符
  1065. //3、字符仅允许'-_'
  1066. //类型合法有:a a-b a_b a-b-c a_b_c a1_b1_c1 a1 a1-b1-c1
  1067. if (!isNamevalid(val)) {
  1068. done('请输入合法的项目名称(要求英文字母开头,数字或字母结尾,字符只允许使用 - 以及 _)')
  1069. return
  1070. }
  1071. done(null, true)
  1072. }, 0)
  1073. }
  1074. }
  1075. //命令行输入的projectName是否合法,不合法则重新填入项目名称
  1076. if (!isNamevalid(this.projectName)) {
  1077. promptArr.unshift(projectPromt)
  1078. } else {
  1079. info.project = this.projectName
  1080. }
  1081. const result = await inquirer.prompt(promptArr)
  1082. if (result?.project) {
  1083. info.project = result?.project
  1084. }
  1085. //4、获取项目的基本信息
  1086. return {
  1087. type,
  1088. ...result,
  1089. ...info,
  1090. className: require('kebab-case')(info.project).replace(/^-/, '')
  1091. }
  1092. }
  1093. }
  1094. createTemplateChoice() {
  1095. return this.templates?.map(item => ({
  1096. name: item.name,
  1097. value: item.npmName
  1098. }))
  1099. }
  1100. //判断当前路径是否不为空
  1101. isCmdEmpty(localPath) {
  1102. let fileList = fs.readdirSync(localPath)
  1103. fileList = fileList.filter(item => !item.startsWith('.') && item !== 'node_modules')
  1104. return fileList && fileList.length > 0
  1105. }
  1106. }
  1107. function init(argv) {
  1108. return new initCommand(argv)
  1109. }
  1110. module.exports = init
  1111. module.exports.initCommand = initCommand