「Create-?」每个前端开发者都可以拥有属于自己的命令行脚手架

开发 前端
最近一直在搞Strve.js生态,在自己捣鼓框架的同时也学到了很多东西。所以就本篇文章给大家介绍一种更加方便灵活的命令行脚手架工具,以及如何发布到NPM上。

[[441093]]

前言

为什么要写这篇文章呢?是因为最近一直在搞Strve.js生态,在自己捣鼓框架的同时也学到了很多东西。所以就本篇文章给大家介绍一种更加方便灵活的命令行脚手架工具,以及如何发布到NPM上。

之前,我也写过类似的开发命令行工具的文章,但是核心思想都是通过代码远程拉取Git仓库中的项目模板代码。有时候会因为网速的原因导致拉取失败,进而会初始化项目失败。

那么,有没有比这个更好的方案呢?那么本篇就来了。

最近,使用Vite工具开发了很多项目。不得不佩服尤老师惊人的代码能力,创建了这么好的开发工具,开发体验非常丝滑。尤其是你刚初始化项目时,只需要执行一行命令,也不用全局安装什么工具。然后,自定义选择需要的模板进行初始化项目,就大功告成了!这种操作着实把我惊到了!我在想,如果我把create-vite的这种思路应用到我自己的脚手架工具中是不是很Nice!

实战

所以,二话不说,就抓紧打开ViteGitHub地址。

https://github.com/vitejs

找了大半天,终于找到了命令行工具核心代码。

https://github.com/vitejs/vite/tree/main/packages/create-vite

映入眼帘的是很多以template-开头的文件夹,打开几个都看了一下,都是框架项目模板。那么,可以先放在一边。

下一步,我们就打开index.js文件看下什么内容。我列下代码,大家可以简单看一下,不用深究。

  1. #!/usr/bin/env node 
  2.  
  3. // @ts-check 
  4. const fs = require('fs'
  5. const path = require('path'
  6. // Avoids autoconversion to number of the project name by defining that the args 
  7. // non associated with an option ( _ ) needs to be parsed as a string. See #4606 
  8. const argv = require('minimist')(process.argv.slice(2), { string: ['_'] }) 
  9. // eslint-disable-next-line node/no-restricted-require 
  10. const prompts = require('prompts'
  11. const { 
  12.   yellow, 
  13.   green, 
  14.   cyan, 
  15.   blue, 
  16.   magenta, 
  17.   lightRed, 
  18.   red 
  19. } = require('kolorist'
  20.  
  21. const cwd = process.cwd() 
  22.  
  23. const FRAMEWORKS = [ 
  24.   { 
  25.     name'vanilla'
  26.     color: yellow, 
  27.     variants: [ 
  28.       { 
  29.         name'vanilla'
  30.         display: 'JavaScript'
  31.         color: yellow 
  32.       }, 
  33.       { 
  34.         name'vanilla-ts'
  35.         display: 'TypeScript'
  36.         color: blue 
  37.       } 
  38.     ] 
  39.   }, 
  40.   { 
  41.     name'vue'
  42.     color: green, 
  43.     variants: [ 
  44.       { 
  45.         name'vue'
  46.         display: 'JavaScript'
  47.         color: yellow 
  48.       }, 
  49.       { 
  50.         name'vue-ts'
  51.         display: 'TypeScript'
  52.         color: blue 
  53.       } 
  54.     ] 
  55.   }, 
  56.   { 
  57.     name'react'
  58.     color: cyan, 
  59.     variants: [ 
  60.       { 
  61.         name'react'
  62.         display: 'JavaScript'
  63.         color: yellow 
  64.       }, 
  65.       { 
  66.         name'react-ts'
  67.         display: 'TypeScript'
  68.         color: blue 
  69.       } 
  70.     ] 
  71.   }, 
  72.   { 
  73.     name'preact'
  74.     color: magenta, 
  75.     variants: [ 
  76.       { 
  77.         name'preact'
  78.         display: 'JavaScript'
  79.         color: yellow 
  80.       }, 
  81.       { 
  82.         name'preact-ts'
  83.         display: 'TypeScript'
  84.         color: blue 
  85.       } 
  86.     ] 
  87.   }, 
  88.   { 
  89.     name'lit'
  90.     color: lightRed, 
  91.     variants: [ 
  92.       { 
  93.         name'lit'
  94.         display: 'JavaScript'
  95.         color: yellow 
  96.       }, 
  97.       { 
  98.         name'lit-ts'
  99.         display: 'TypeScript'
  100.         color: blue 
  101.       } 
  102.     ] 
  103.   }, 
  104.   { 
  105.     name'svelte'
  106.     color: red, 
  107.     variants: [ 
  108.       { 
  109.         name'svelte'
  110.         display: 'JavaScript'
  111.         color: yellow 
  112.       }, 
  113.       { 
  114.         name'svelte-ts'
  115.         display: 'TypeScript'
  116.         color: blue 
  117.       } 
  118.     ] 
  119.   } 
  120.  
  121. const TEMPLATES = FRAMEWORKS.map( 
  122.   (f) => (f.variants && f.variants.map((v) => v.name)) || [f.name
  123. ).reduce((a, b) => a.concat(b), []) 
  124.  
  125. const renameFiles = { 
  126.   _gitignore: '.gitignore' 
  127.  
  128. async function init() { 
  129.   let targetDir = argv._[0] 
  130.   let template = argv.template || argv.t 
  131.  
  132.   const defaultProjectName = !targetDir ? 'vite-project' : targetDir 
  133.  
  134.   let result = {} 
  135.  
  136.   try { 
  137.     result = await prompts( 
  138.       [ 
  139.         { 
  140.           type: targetDir ? null : 'text'
  141.           name'projectName'
  142.           message: 'Project name:'
  143.           initial: defaultProjectName, 
  144.           onState: (state) => 
  145.             (targetDir = state.value.trim() || defaultProjectName) 
  146.         }, 
  147.         { 
  148.           type: () => 
  149.             !fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm'
  150.           name'overwrite'
  151.           message: () => 
  152.             (targetDir === '.' 
  153.               ? 'Current directory' 
  154.               : `Target directory "${targetDir}"`) + 
  155.             ` is not empty. Remove existing files and continue?` 
  156.         }, 
  157.         { 
  158.           type: (_, { overwrite } = {}) => { 
  159.             if (overwrite === false) { 
  160.               throw new Error(red('✖') + ' Operation cancelled'
  161.             } 
  162.             return null 
  163.           }, 
  164.           name'overwriteChecker' 
  165.         }, 
  166.         { 
  167.           type: () => (isValidPackageName(targetDir) ? null : 'text'), 
  168.           name'packageName'
  169.           message: 'Package name:'
  170.           initial: () => toValidPackageName(targetDir), 
  171.           validate: (dir) => 
  172.             isValidPackageName(dir) || 'Invalid package.json name' 
  173.         }, 
  174.         { 
  175.           type: template && TEMPLATES.includes(template) ? null : 'select'
  176.           name'framework'
  177.           message: 
  178.             typeof template === 'string' && !TEMPLATES.includes(template) 
  179.               ? `"${template}" isn't a valid template. Please choose from below: ` 
  180.               : 'Select a framework:'
  181.           initial: 0, 
  182.           choices: FRAMEWORKS.map((framework) => { 
  183.             const frameworkColor = framework.color 
  184.             return { 
  185.               title: frameworkColor(framework.name), 
  186.               value: framework 
  187.             } 
  188.           }) 
  189.         }, 
  190.         { 
  191.           type: (framework) => 
  192.             framework && framework.variants ? 'select' : null
  193.           name'variant'
  194.           message: 'Select a variant:'
  195.           // @ts-ignore 
  196.           choices: (framework) => 
  197.             framework.variants.map((variant) => { 
  198.               const variantColor = variant.color 
  199.               return { 
  200.                 title: variantColor(variant.name), 
  201.                 value: variant.name 
  202.               } 
  203.             }) 
  204.         } 
  205.       ], 
  206.       { 
  207.         onCancel: () => { 
  208.           throw new Error(red('✖') + ' Operation cancelled'
  209.         } 
  210.       } 
  211.     ) 
  212.   } catch (cancelled) { 
  213.     console.log(cancelled.message) 
  214.     return 
  215.   } 
  216.  
  217.   // user choice associated with prompts 
  218.   const { framework, overwrite, packageName, variant } = result 
  219.  
  220.   const root = path.join(cwd, targetDir) 
  221.  
  222.   if (overwrite) { 
  223.     emptyDir(root) 
  224.   } else if (!fs.existsSync(root)) { 
  225.     fs.mkdirSync(root) 
  226.   } 
  227.  
  228.   // determine template 
  229.   template = variant || framework || template 
  230.  
  231.   console.log(`\nScaffolding project in ${root}...`) 
  232.  
  233.   const templateDir = path.join(__dirname, `template-${template}`) 
  234.  
  235.   const write = (file, content) => { 
  236.     const targetPath = renameFiles[file] 
  237.       ? path.join(root, renameFiles[file]) 
  238.       : path.join(root, file) 
  239.     if (content) { 
  240.       fs.writeFileSync(targetPath, content) 
  241.     } else { 
  242.       copy(path.join(templateDir, file), targetPath) 
  243.     } 
  244.   } 
  245.  
  246.   const files = fs.readdirSync(templateDir) 
  247.   for (const file of files.filter((f) => f !== 'package.json')) { 
  248.     write(file) 
  249.   } 
  250.  
  251.   const pkg = require(path.join(templateDir, `package.json`)) 
  252.  
  253.   pkg.name = packageName || targetDir 
  254.  
  255.   write('package.json', JSON.stringify(pkg, null, 2)) 
  256.  
  257.   const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent) 
  258.   const pkgManager = pkgInfo ? pkgInfo.name : 'npm' 
  259.  
  260.   console.log(`\nDone. Now run:\n`) 
  261.   if (root !== cwd) { 
  262.     console.log(`  cd ${path.relative(cwd, root)}`) 
  263.   } 
  264.   switch (pkgManager) { 
  265.     case 'yarn'
  266.       console.log('  yarn'
  267.       console.log('  yarn dev'
  268.       break 
  269.     default
  270.       console.log(`  ${pkgManager} install`) 
  271.       console.log(`  ${pkgManager} run dev`) 
  272.       break 
  273.   } 
  274.   console.log() 
  275.  
  276. function copy(src, dest) { 
  277.   const stat = fs.statSync(src) 
  278.   if (stat.isDirectory()) { 
  279.     copyDir(src, dest) 
  280.   } else { 
  281.     fs.copyFileSync(src, dest) 
  282.   } 
  283.  
  284. function isValidPackageName(projectName) { 
  285.   return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test( 
  286.     projectName 
  287.   ) 
  288.  
  289. function toValidPackageName(projectName) { 
  290.   return projectName 
  291.     .trim() 
  292.     .toLowerCase() 
  293.     .replace(/\s+/g, '-'
  294.     .replace(/^[._]/, ''
  295.     .replace(/[^a-z0-9-~]+/g, '-'
  296.  
  297. function copyDir(srcDir, destDir) { 
  298.   fs.mkdirSync(destDir, { recursive: true }) 
  299.   for (const file of fs.readdirSync(srcDir)) { 
  300.     const srcFile = path.resolve(srcDir, file) 
  301.     const destFile = path.resolve(destDir, file) 
  302.     copy(srcFile, destFile) 
  303.   } 
  304.  
  305. function isEmpty(path) { 
  306.   return fs.readdirSync(path).length === 0 
  307.  
  308. function emptyDir(dir) { 
  309.   if (!fs.existsSync(dir)) { 
  310.     return 
  311.   } 
  312.   for (const file of fs.readdirSync(dir)) { 
  313.     const abs = path.resolve(dir, file) 
  314.     // baseline is Node 12 so can't use rmSync :( 
  315.     if (fs.lstatSync(abs).isDirectory()) { 
  316.       emptyDir(abs
  317.       fs.rmdirSync(abs
  318.     } else { 
  319.       fs.unlinkSync(abs
  320.     } 
  321.   } 
  322.  
  323. /** 
  324.  * @param {string | undefined} userAgent process.env.npm_config_user_agent 
  325.  * @returns object | undefined 
  326.  */ 
  327. function pkgFromUserAgent(userAgent) { 
  328.   if (!userAgent) return undefined 
  329.   const pkgSpec = userAgent.split(' ')[0] 
  330.   const pkgSpecArr = pkgSpec.split('/'
  331.   return { 
  332.     name: pkgSpecArr[0], 
  333.     version: pkgSpecArr[1] 
  334.   } 
  335.  
  336. init().catch((e) => { 
  337.   console.error(e) 
  338. }) 

看到上面这么多代码是不是不想继续阅读下去了?不要慌!我们其实就用到里面几个地方,可以放心的继续阅读下去。

这些代码算是Create Vite核心代码了,我们会看到常量FRAMEWORKS定义了一个数组对象,另外数组对象中都是一些我们初始化项目时需要选择安装的框架。所以,我们可以先ViteGithub项目Clone下来,试试效果。

然后,将项目Clone下来之后,我们找到/packages/create-vite这个文件夹,我们现在就只关注这个文件夹。

我用的Yarn依赖管理工具,所以我首先使用命令初始化依赖。

  1. yarn  

然后,我们可以先打开根目录下的package.json文件,会发现有如下命令。

  1.   "bin": { 
  2.     "create-vite""index.js"
  3.     "cva""index.js" 
  4.   } 

我们可以在这里起一个自己模板的名字,比如我们就叫demo,

  1.   "bin": { 
  2.     "create-demo""index.js"
  3.     "cvd""index.js" 
  4.   } 

然后,我们先在这里使用yarn link命令来将此命令在本地可以运行。

然后再运行create-demo命令·。

 会显示一些交互文本,会发现非常熟悉,这正是我们创建Vite项目时所看到的。我们在前面说到我们想实现一个属于自己的项目模板,现在我们也找到了核心。所以就开始干起来吧!

我们会看到在根目录下有很多template-开头的文件夹,我们打开一个看一下。比如template-vue。

原来模板都在这!但是这些模板文件都是以template-开头,是不是有什么约定?所以,我们打算回头再去看下index.js文件。

  1. // determine template 
  2. template = variant || framework || template 
  3.  
  4. console.log(`\nScaffolding project in ${root}...`) 
  5.  
  6. const templateDir = path.join(__dirname, `template-${template}`) 

果真,所以模板都必须以template-开头。

那么,我们就在根目录下面建一个template-demo文件夹,里面再放一个index.js文件,作为示例模板。

我们在执行初始化项目时发现,需要选择对应的模板,那么这些选项是从哪里来的呢?我们决定再回去看下根目录下的index.js文件。

会发现有这么一个数组,里面正是我们要选择的框架模板。

  1. const FRAMEWORKS = [ 
  2.   { 
  3.     name'vanilla'
  4.     color: yellow, 
  5.     variants: [ 
  6.       { 
  7.         name'vanilla'
  8.         display: 'JavaScript'
  9.         color: yellow 
  10.       }, 
  11.       { 
  12.         name'vanilla-ts'
  13.         display: 'TypeScript'
  14.         color: blue 
  15.       } 
  16.     ] 
  17.   }, 
  18.   { 
  19.     name'vue'
  20.     color: green, 
  21.     variants: [ 
  22.       { 
  23.         name'vue'
  24.         display: 'JavaScript'
  25.         color: yellow 
  26.       }, 
  27.       { 
  28.         name'vue-ts'
  29.         display: 'TypeScript'
  30.         color: blue 
  31.       } 
  32.     ] 
  33.   }, 
  34.   { 
  35.     name'react'
  36.     color: cyan, 
  37.     variants: [ 
  38.       { 
  39.         name'react'
  40.         display: 'JavaScript'
  41.         color: yellow 
  42.       }, 
  43.       { 
  44.         name'react-ts'
  45.         display: 'TypeScript'
  46.         color: blue 
  47.       } 
  48.     ] 
  49.   }, 
  50.   { 
  51.     name'preact'
  52.     color: magenta, 
  53.     variants: [ 
  54.       { 
  55.         name'preact'
  56.         display: 'JavaScript'
  57.         color: yellow 
  58.       }, 
  59.       { 
  60.         name'preact-ts'
  61.         display: 'TypeScript'
  62.         color: blue 
  63.       } 
  64.     ] 
  65.   }, 
  66.   { 
  67.     name'lit'
  68.     color: lightRed, 
  69.     variants: [ 
  70.       { 
  71.         name'lit'
  72.         display: 'JavaScript'
  73.         color: yellow 
  74.       }, 
  75.       { 
  76.         name'lit-ts'
  77.         display: 'TypeScript'
  78.         color: blue 
  79.       } 
  80.     ] 
  81.   }, 
  82.   { 
  83.     name'svelte'
  84.     color: red, 
  85.     variants: [ 
  86.       { 
  87.         name'svelte'
  88.         display: 'JavaScript'
  89.         color: yellow 
  90.       }, 
  91.       { 
  92.         name'svelte-ts'
  93.         display: 'TypeScript'
  94.         color: blue 
  95.       } 
  96.     ] 
  97.   } 

所以,可以在后面数组后面再添加一个对象。

  1.     name'demo'
  2.     color: red, 
  3.     variants: [ 
  4.       { 
  5.         name'demo'
  6.         display: 'JavaScript'
  7.         color: yellow 
  8.       } 
  9.     ] 

好,你会发现我这里会有个color属性,并且有类似颜色值的属性值,这是依赖kolorist导出的常量。kolorist是一个将颜色放入标准输入/标准输出的小库。我们在之前那些模板交互文本会看到它们显示不同颜色,这正是它的功劳。

  1. const { 
  2.   yellow, 
  3.   green, 
  4.   cyan, 
  5.   blue, 
  6.   magenta, 
  7.   lightRed, 
  8.   red 
  9. } = require('kolorist'

我们,也将模板对象添加到数组里了,那么下一步我们执行命令看下效果。

 

会发现多了一个demo模板,这正是我们想要的。

我们继续执行下去。

我们会看到根目录下已经成功创建了demo1文件夹,并且里面正是我们想要的demo模板。

上图显示的Error,是因为我没有在demo模板上创建package.json文件,所以这里可以忽略。你可以在自己的模板里创建一个package.json文件。

虽然,我们成功在本地创建了自己的一个模板,但是,我们只能本地创建。也就是说你换台电脑,就没有办法执行这个创建模板的命令。

所以,我们要想办法去发布到云端,这里我们发布到NPM上。

首先,我们重新新建一个项目目录,将其他模板删除,只保留我们自己的模板。另外,将数组中的其他模板对象删除,保留一个自己的模板。

我以自己的模板create-strve-app为例。

然后,我们打开package.json文件,需要修改一些信息。

以create-strve-app为例:

  1.   "name""create-strve-app"
  2.   "version""1.3.3"
  3.   "license""MIT"
  4.   "author""maomincoding"
  5.   "bin": { 
  6.     "create-strve-app""index.js"
  7.     "cs-app""index.js" 
  8.   }, 
  9.   "files": [ 
  10.     "index.js"
  11.     "template-*" 
  12.   ], 
  13.   "main""index.js"
  14.   "private"false
  15.   "keywords": ["strve","strvejs","dom","mvvm","virtual dom","html","template","string","create-strve","create-strve-app"], 
  16.   "engines": { 
  17.     "node"">=12.0.0" 
  18.   }, 
  19.   "repository": { 
  20.     "type""git"
  21.     "url""git+https://github.com/maomincoding/create-strve-app.git" 
  22.   }, 
  23.   "bugs": { 
  24.     "url""https://github.com/maomincoding/create-strve-app/issues" 
  25.   }, 
  26.   "homepage""https://github.com/maomincoding/create-strve-app#readme"
  27.   "dependencies": { 
  28.     "kolorist""^1.5.0"
  29.     "minimist""^1.2.5"
  30.     "prompts""^2.4.2" 
  31.   } 

注意,每次发布前,version字段必须与之前不同,否则发布失败。

最后,我们依次运行如下命令。

切换到npm源

  1. npm config set registry=https://registry.npmjs.org 

登录NPM(如果已登录,可忽略此步)

  1. npm login 

发布NPM

  1. npm publish 

我们可以登录到NPM(https://www.npmjs.com/)

查看已经发布成功!

 

以后,我们就可以直接运行命令下载自定义模板。这在我们重复使用模板时非常有用,不仅可以提升效率,而且还可以避免犯很多不必要的错误。

结语

另外,此篇举例的 Create Strve App 是一套快速搭建Strve.js项目的命令行工具。如果你对此感兴趣,可以访问以下地址查看源码:

https://github.com/maomincoding/create-strve-app

熬夜奋战二个多月,Strve.js生态初步已经建成,以下是Strve.js 最新文档地址,欢迎浏览。

https://maomincoding.github.io/strvejs-doc/

 

责任编辑:姜华 来源: 前端历劫之路
相关推荐

2019-09-19 09:02:52

开发者技能工具

2021-12-23 06:07:21

前端技术编程

2023-11-21 17:36:04

OpenFeignSentinel

2022-04-20 06:56:33

Strve.js前端

2021-12-16 23:40:33

部署ReactTypeScript

2021-08-17 21:52:04

工具

2018-01-29 20:12:11

python翻译命令行

2021-05-21 05:22:52

脚手架工具项目

2019-12-25 15:20:48

前端脚手架命令

2022-03-07 10:27:03

Linux开源社区

2021-01-07 05:34:07

脚手架JDK缓存

2016-07-05 16:30:10

碳云智能数字化生命

2020-04-09 10:25:18

Java 开发者神器

2018-08-30 16:08:37

Node.js脚手架工具

2018-06-11 14:39:57

前端脚手架工具node.js

2014-08-15 09:36:06

2010-05-21 18:30:02

2016-06-30 11:25:52

VisualNET开发

2019-08-09 10:52:58

Linux内核Spark

2021-10-08 06:10:43

前端技术Vue
点赞
收藏

51CTO技术栈公众号