create-vite 脚手架-部分源码分析

前置知识

lerna

命令功能
lerna bootstrap安装依赖
lerna clean删除各个包下的node_modules
lerna init创建新的lerna库
lerna list查看本地包列表
lerna changed显示自上次release tag以来有修改的包, 选项通 list
lerna diff显示自上次release tag以来有修改的包的差异, 执行 git diff
lerna exec在每个包目录下执行任意命令
lerna run执行每个包package.json中的脚本命令
lerna add添加一个包的版本为各个包的依赖
lerna import引入package
lerna link链接互相引用的库
lerna create新建package
lerna publish发布

yarn

命令说明
yarn -v查看yarn版本
yarn config list查看yarn的所有配置
yarn config set registry https://registry.npm.taobao.org/修改yarn的源镜像为淘宝源
yarn config set global-folder “D:\RTE\Yarn\global”修改全局安装目录, 先创建好目录(global), 我放在了Yarn安装目录下(D:\RTE\Yarn\global)
yarn config set prefix “D:\RTE\Yarn\global”修改全局安装目录的bin目录位置
yarn config set cache-folder “D:\RTE\Yarn\cache”修改全局缓存目录, 先创建好目录(cache), 和global放在同一层目录下
yarn config list查看所有配置
yarn global bin查看当前yarn的bin的位置
yarn global dir查看当前yarn的全局安装位置

yarn workspace

  • yarn官网
  • yarn add <package…> [–ignore-workspace-root-check/-W]
  • yarn add <package…> [–dev/-D]
作用命令
查看工作空间信息yarn workspaces info
给所有的空间添加依赖yarn workspaces run add lodash
给根空间添加依赖yarn add -W -D typescript jest
给某个项目添加依赖yarn workspace create-react-app3 add commander
删除所有的 node_moduleslerna clean 等于 yarn workspaces run clean
安装和link所有的名yarn install 等于 lerna bootstrap --npm-client yarn --use-workspaces
重新获取所有的 node_modulesyarn install --force
查看缓存目录yarn cache dir
清除本地缓存yarn cache clean
在所有package中运行指定的命令yarn workspaces run

yargs

  • yargs帮助你构建交互命令行工具,可以解析参数生成优雅的用户界面
const yargs = require("yargs/yargs");
const cli = yargs();
cli
    .usage(`Usage: vite2 <command> [options]`)
    .demandCommand(1, "至少需要一个命令")
    .strict()
    .recommendCommands()
    .command({
        command: "create <name>",
        describe: "创建项目",
        builder: (yargs) => {
            yargs.positional("name", {
                type: "string",
                describe: "项目名称",
            });
        },
        handler: async function (argv) {
           console.log(argv);
           //{ _: [ 'create' ], '$0': 'doc\\1.yargs.js', name: 'p1' }
        }
    })
    .parse(process.argv.slice(2));

node -e

  • node -e可以直接执行一段js脚本并输入
  • -e, --eval “script”
  • 设置stdion: 'inherit',当执行代码时,子进程将会继承主进程的stdin、stdout和stderr
node -e "console.log(process.argv)" -- a b
node -e "console.log(JSON.parse(process.argv[1]))" -- "{\"name\":\"hs\"}"
node -e "console.log(process.cwd())"
const spawn = require("cross-spawn");
async function executeNodeScript({ cwd }, source, args) {
    return new Promise((resolve) => {
      const childProcess = spawn(
        process.execPath,
        ["-e", source, "--", JSON.stringify(args)],
        { cwd, stdio: "inherit" }
      );
      childProcess.on("close", resolve);
    });
}
module.exports = executeNodeScript;

clone

const clone = require('clone-git-repo');
let repository = 'gitee:hstemplate/template-react#v1.0';
clone(repository,'./output', {clone:true},function (err) {
    console.log(err);
})

create-vite介绍

最新版本为5.2.3

create-vite简介

使用

npm init vite
Need to install the following packages:
  create-vite
Ok to proceed? (y) y
√ Project name: ... vite-project
√ Select a framework: » react
√ Select a variant: » react

Scaffolding project in C:\aprepare\t1\vite-project...

Done. Now run:

  cd vite-project
  npm install
  npm run dev

create-vite源码调试

git clone https://github.com/vitejs/vite.git
cd vite
yarn install
packages\create-vite\index.js

.vscode\launch.json

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "pwa-node",
            "request": "launch",
            "name": "Launch Program",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "program": "${workspaceFolder}\\packages\\create-vite\\index.js",
            "args": ["create","vite-project"]
        }
    ]
}

create-vite功能

  • [√] 支持参数解析
  • [√] 支持自定义项目名
  • [√] 支持空目录检查
  • [√] 支持静态项目模板
  • [×] 不支持lerna lerna
  • [×] 不支持文件异步写入 create-react-app
  • [×] 不支持多进程执行命令 create-react-app
  • [×] 不支持执行动态node命令 create-react-app
  • [×] 不支持自动安装依赖 create-react-app
  • [×] 不支持自动启动服务 create-react-app
  • [×] 不支持参数配置 yarn
  • [×] 不支持gitubgitee仓库动态读取
  • [×] 不支持模板标签选择
  • [×] 不支持动态模板渲染
  • [×] 不支持插件化配置技术栈 vue-cli

npm create命令

用npm命令npm create vite创建工程npm createnpm init的alias,详情可查阅npm文档

npm create vite@lastest 相当于 => npx create-vite@lastestlatest 是版本号,目前最新版本可以通过以下命令查看。

npm dist-tag ls create-vite
beta: 5.0.0-beta.1
latest: 5.2.3

简单来说,不管是 npm create 还是 yarn createpnpm create,这三个命令的大致流程都是类似的:

  1. 解析命令行参数,得到 初始化器名称 initerName 和项目名称 projectName
  2. 解析 initerName,确定对应的依赖包名称和版本号
  3. 根据依赖包名称和版本号下载安装初始化器
  4. 执行初始化器的 create 方法
  5. 成功后打印提示信息,退出进程

核心

image-20240621151148746

  • 根据package.json中的typebin,type类型指定为module说明是ES Module。bin 可执行命令为 create-vite 或 别名 cva,主文件 index.js

    image-20240621141125272

  • 查看index.js

    image-20240621141222633

    • import './dist/index.mjs',说明它引入的是一个打包好的模块,根据 package.json 中的打包命令,可以看出使用了 unbuild 工具,在目录可以查找到相关的配置文件 build.config.tsunbuild 是一个统一的构建系统

      image-20240621141417374

src\index.ts

init初始化函数

  • 通过minimist解析命令行参数
  • 通过prompts 命令行交互
  • 通过kolorist 命令行输出彩色文本
  • 通过spawn跨平台开启子进程
  • 通过fileURLToPath把url 转文件路径模块
处理项目名称和项目创建的目标路径
  • 通过formatTargetDir函数,替换反斜杠 / 为空字符串

    image-20240621142953262

image-20240621143210096

通过prompts终端交互,进行参数确认

通过 prompts 询问用户并确认所用项目名称、所用框架等。

let result: prompts.Answers<
    'projectName' | 'overwrite' | 'packageName' | 'framework' | 'variant'
  >

  prompts.override({
    overwrite: argv.overwrite,
  })

  try {
    result = await prompts(
      [
        {
          type: argTargetDir ? null : 'text',
          name: 'projectName',
          message: reset('Project name:'),
          initial: defaultTargetDir,
          onState: (state) => {
            targetDir = formatTargetDir(state.value) || defaultTargetDir
          },
        },
        {
          type: () =>
            !fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'select',
          name: 'overwrite',
          message: () =>
            (targetDir === '.'
              ? 'Current directory'
              : `Target directory "${targetDir}"`) +
            ` is not empty. Please choose how to proceed:`,
          initial: 0,
          choices: [
            {
              title: 'Remove existing files and continue',
              value: 'yes',
            },
            {
              title: 'Cancel operation',
              value: 'no',
            },
            {
              title: 'Ignore files and continue',
              value: 'ignore',
            },
          ],
        },
        {
          type: (_, { overwrite }: { overwrite?: string }) => {
            if (overwrite === 'no') {
              throw new Error(red('✖') + ' Operation cancelled')
            }
            return null
          },
          name: 'overwriteChecker',
        },
        {
          type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
          name: 'packageName',
          message: reset('Package name:'),
          initial: () => toValidPackageName(getProjectName()),
          validate: (dir) =>
            isValidPackageName(dir) || 'Invalid package.json name',
        },
        {
          type:
            argTemplate && TEMPLATES.includes(argTemplate) ? null : 'select',
          name: 'framework',
          message:
            typeof argTemplate === 'string' && !TEMPLATES.includes(argTemplate)
              ? reset(
                `"${argTemplate}" isn't a valid template. Please choose from below: `,
              )
              : reset('Select a framework:'),
          initial: 0,
          choices: FRAMEWORKS.map((framework) => {
            const frameworkColor = framework.color
            return {
              title: frameworkColor(framework.display || framework.name),
              value: framework,
            }
          }),
        },
        {
          type: (framework: Framework) =>
            framework && framework.variants ? 'select' : null,
          name: 'variant',
          message: reset('Select a variant:'),
          choices: (framework: Framework) =>
            framework.variants.map((variant) => {
              const variantColor = variant.color
              return {
                title: variantColor(variant.display || variant.name),
                value: variant.name,
              }
            }),
        },
      ],
      {
        onCancel: () => {
          throw new Error(red('✖') + ' Operation cancelled')
        },
      },
    )
  } catch (cancelled: any) {
    console.log(cancelled.message)
    return
  }

  // user choice associated with prompts
  // framework 框架
  // overwrite 已有目录,是否重写
  // packageName 输入的项目名
  // variant 变体, 比如 react => react-ts
  const { framework, overwrite, packageName, variant } = result
清空已有目录或创建不存在的新目录
  • 通过emptyDir递归删除文件夹

    image-20240621143930468

image-20240621143828863

确定template
// determine template
  let template: string = variant || framework?.name || argTemplate
  let isReactSwc = false
  if (template.includes('-swc')) {
    isReactSwc = true
    template = template.replace('-swc', '')
  }

注意这里在 template 确定之后,还有一个 swc 的处理: isReactSwc

关于 swc(Speedy Web Compiler),官方释意是:“SWC is an extensible Rust-based platform for the next generation of fast developer tools. SWC can be used for both compilation and bundling. For compilation, it takes JavaScript / TypeScript files using modern JavaScript features and outputs valid code that is supported by all major browsers

翻译过来也就是:SWC 是基于 Rust 的下一代快速开发工具,可以用于代码编译和绑定;在代码编译上面,它可以使用现代 JavaScript 功能来处理 JavaScript/TypeScript 代码文件并输出适用于所有主流浏览器都支持的有效代码。

在代码编译上面,我们可以把它当成与 Babel 同等的代码处理插件,但是由于其基于 Rust 开发,所以在速度上比 Babel 快很多倍:

SWC在单线程上比 Babel 快 20 倍,在四核上 快 70 倍

如果用户选择的模板具有 swc 配置,则会将 isReactSwc 变量置为 true,并从 template 中移除 -swc 部分。

处理pkgManager

处理 node packages manager 包管理工具

image-20240621150015806

首先会通过 pkgFromUserAgent 来读取 用户代理字符串 中的相关信息,并返回 name 包管理工具名称、version 包管理工具版本。

然后通过 const pkgManager = pkgInfo ? pkgInfo.name : 'npm' 处理,如果上面的 pkgFromUserAgent 返回为空则会默认使用 npm

对于 yarn 的话,由于 yarn@1.x 版本不支持在 create 阶段指定版本,所以需要一个变量来保存它的版本信息以供后面进行初始化命令的拼接:

const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.')

此时,我们已经确定了用户当前的一个大致的工作环境了,包括工作目录、包管理工具等

customCommand

确认了用户的工作环境之后,就会在用户选择的 模板和变种 中提取出对应的 customCommand 自定义命令,然后根据上面确定的环境来 重新处理命令语句

  • 替换默认语句中的包管理工具,指定当前的工作目录,
  • 通过 spawn.sync 启动一个 子进程 执行脚本,并且设置 stdio: 'inherit' 让子进程继承当前进程的输入输出,将输出信息同步输出在当前进程上

image-20240621150419006

package.json单独处理(修改工程名)

package.json 文件单独处理。读取模板中的 package.json 最为一个 json 对象,修改其 name 属性为用户输入的项目名称 projectName

如果上面新增的 isReactSwctrue 的话,还会通过 editFile 方法在 vite.config.js/tspackage.json 中插入 swc 的相关插件,但是目前 仅支持 react 项目

image-20240621150744619

输出安装完成后的信息

image-20240621151021389

实现

1.初始化项目

1.1 lerna初始化

mkdir vite2
cd vite2
lerna init

1.2 使用yarn workspace

  • 开发多个互相依赖的package时,workspace会自动对package的引用设置软链接(symlink),比yarn link更加方便,且链接仅局限在当前workspace中,不会对整个系统造成影响
  • 所有package的依赖会安装在根目录的node_modules下,节省磁盘空间,且给了yarn更大的依赖优化空间
  • Yarn workspace只会在根目录安装一个node_modules,这有利于提升依赖的安装效率和不同package间的版本复用。而Lerna默认会进到每一个package中运行yarn/npm install,并在每个package中创建一个node_modules
  • yarn官方推荐的方案,是集成yarn workspace和lerna,使用yarn workspace来管理依赖,使用lerna来管理npm包的版本发布
1.2.1 lerna.json
{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0",
+ "npmClient": "yarn",
+ "useWorkspaces": true
}
1.2.2 package.json
{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^4.0.0"
  },
+  "workspaces": [
+    "packages/*"
+ ]  
}

1.3 创建子包

lerna create @vite2/config -y //配置项
lerna create @vite2/create -y //创建项目
lerna create vite2 -y  //核心命令
lerna create @vite2/settings -y  //常量定义
lerna create @vite2/utils -y //工具方法

1.4 安装依赖

1.4.1 config\package.json

packages\config\package.json

{
    "dependencies": {
    "@vite2/settings": "^0.0.0",
    "@vite2/utils": "^0.0.0",
    "fs-extra": "^10.0.0",
    "userhome": "^1.0.0"
   }
}
1.4.2 create\package.json

packages\create\package.json

{
  "dependencies": {
    "@vite2/settings": "^0.0.0",
    "@vite2/utils": "^0.0.0",
    "chalk": "^4.1.2",
    "clone-git-repo": "^0.0.2",
    "ejs": "^3.1.6",
    "execa": "^5.1.1",
    "fs-extra": "^10.0.0",
    "glob": "^7.1.7",
    "inquirer": "^8.1.2"
  },
}
1.4.3 utils\package.json

packages\utils\package.json

{
    "dependencies": {
    "@vite2/settings": "^0.0.0",
    "axios": "^0.21.2",
    "cross-spawn": "^7.0.3",
    "userhome": "^1.0.0"
  },
}
1.4.4 vite2\package.json

packages\vite2\package.json

{
  "dependencies": {
    "@vite2/config": "^0.0.0",
    "@vite2/create": "^0.0.0"
  }
}
1.4.5 publishConfig
{
  "publishConfig": {
    "access": "public",
    "registry": "http://registry.npmjs.org"
  }
}

1

1.5 配置命令

1.5.1 package.json

packages\vite2\package.json

{
  "name": "vite2",
  "version": "0.0.0",
  "dependencies": {
    "@vite2/config":"^0.0.0",
    "@vite2/create":"^0.0.0"
  },
+ "bin":{
+   "vite2": "index.js"
+ }
}
1.5.2 index.js

packages\vite2\index.js

#!/usr/bin/env node

async function main() {
   let argv = process.argv.slice(2);
   console.log(argv);
}

main().catch((err) => {
    console.error(err);
});
1.5.3 link
  • 一定要先添加#!/usr/bin/env node再link,否则会用文本编辑器打开
  • 这种情况可以vite2\packages\vite2目录中执行yarn unlink,再重新link
yarn link
yarn global bin 
C:\Users\admin\AppData\Local\Yarn\bin
C:\Users\admin\AppData\Local\Yarn\Data\link\vite2
npm bin -g
C:\Users\admin\AppData\Roaming\npm
vite2 create vite-project

2.实现配置命令

2.1 安装依赖

yarn workspace @vite2/config  add userhome fs-extra
yarn workspace @vite2/utils  add cross-spawn userhome fs-extra

2.2 settings\index.js

packages\settings\index.js

//执行命令脚本
exports.COMMAND_SOURCE = `
const args = JSON.parse(process.argv[1]);
const factory = require('.');
factory(args);
`
//配置文件名称
exports.RC_NAME = ".vite3rc";

2.3 config.js

packages\utils\config.js

const userhome = require("userhome");
const fs = require("fs-extra");
const { RC_NAME } = require("@vite5/settings");
const configPath = userhome(RC_NAME);
let config = {};
if (fs.existsSync(configPath)) {
  config = fs.readJSONSync(configPath);
}
config.configPath=configPath;
module.exports = config;

2.4 executeNodeScript.js

packages\utils\executeNodeScript.js

const spawn = require("cross-spawn");
async function executeNodeScript({ cwd }, source, args) {
    return new Promise((resolve) => {
      const childProcess = spawn(
        process.execPath,
        ["-e", source, "--", JSON.stringify(args)],
        { cwd, stdio: "inherit" }
      );
      childProcess.on("close", resolve);
    });
}
module.exports = executeNodeScript;

2.5 log.js

packages\utils\log.js

const log = require('npmlog');
log.heading = 'vite2';
module.exports = log;

2.6 utils\index.js

packages\utils\index.js

exports.log = require('./log');
exports.executeNodeScript = require('./executeNodeScript');
exports.config = require('./config');

2.7 packages\config\command.js

packages\config\command.js

const {executeNodeScript} = require('@vite2/utils');
const {COMMAND_SOURCE} = require('@vite2/settings');
const command = {
  command: "config [key] [value]",
  describe: "设置或查看配置项,比如GIT_TYPE设置仓库类型,ORG_NAME设置组织名",
  builder: (yargs) => {},
  handler:async function(argv){
    await executeNodeScript({ cwd: __dirname }, COMMAND_SOURCE,argv);
  },
};
module.exports = command;

2.8 config\index.js

packages\config\index.js

const fs = require("fs-extra");
const { log ,config} = require("@vite2/utils");
async function factory(argv) {
  const { key, value } = argv;
  console.log('config',config);
  if (key && value) {
    config[key] = value;
    await fs.writeJSON(config.configPath, config, { spaces: 2 });
    log.info("vite3","(%s=%s)配置成功保存至%s", key, value, config.configPath);
  }else if(key){
      console.log('%s=%s',key, config[key]);
  }else{
    console.log(config);
  }
}
module.exports = factory;

2.9 vite2\index.js

packages\vite2\index.js

#!/usr/bin/env node
const yargs = require("yargs/yargs");
const configCmd = require("@vite2/config/command");
async function main() {
    const cli = yargs();
    cli
    .usage(`Usage: vite2 <command> [options]`)
    .demandCommand(1, "至少需要一个命令")
    .strict() 
    .recommendCommands()
+   .command(configCmd)
    .parse(process.argv.slice(2));
}

main().catch((err) => {
    console.error(err);
});

3.实现创建命令

3.1 create\command.js

packages\create\command.js

const {COMMAND_SOURCE} = require('@vite2/settings');
const {executeNodeScript} = require('@vite2/utils');
const command = {
  command: "create <name>",
  describe: "创建项目",
  builder: (yargs) => {
    yargs.positional("name", {
      type: "string",
      describe: "项目名称",
    });
  },
  handler:async function(argv){
    let args = {name:argv.name,cwd:process.cwd()};
    await executeNodeScript({ cwd: __dirname }, COMMAND_SOURCE,args);
  }
};
module.exports = command;

3.2 create\index.js

packages\create\index.js

async function factory(argv) {
 console.log('create',argv);
}
module.exports = factory;

3.3 vite2\index.js

packages\vite2\index.js

#!/usr/bin/env node
const yargs = require("yargs/yargs");
const configCmd = require("@vite2/config/command");
+const createCmd = require("@vite2/create/command");
async function main() {
    const cli = yargs();
    cli
    .usage(`Usage: vite2 <command> [options]`)
    .demandCommand(1, "至少需要一个命令")
    .strict() 
    .recommendCommands()
    .command(configCmd)
+   .command(createCmd)
    .parse(process.argv.slice(2));
}
main().catch((err) => {
    console.error(err);
});

4.创建项目目录

4.1 安装

yarn workspace @vite2/create  add chalk fs-extra inquirer

4.2 create\index.js

packages\create\index.js

+const path = require("path");
+const { red } = require("chalk");
+const { prompt } = require("inquirer");
+const fs = require("fs-extra");
+const {config, log } = require("@vite2/utils");
async function factory(argv) {
+    const { cwd, name } = argv;
+    process.chdir(cwd);//切换为当前命令执行的工作目录
+    const { ORG_NAME } = config;
+    if (!ORG_NAME) {
+        throw new Error(red("X") + " 尚未配置组织名称!");
+    }
+    const targetDir = path.join(process.cwd(), name);
+    log.info("vite2", "创建的项目目录为%s", targetDir);
+    try {
+        await fs.access(targetDir);
+        const files = await fs.readdir(targetDir);
+        if (files.length > 0) {
+            const { overwrite } = await prompt({
+                type: "confirm",
+                name: "overwrite",
+                message: `目标目录非空,是否要移除存在的文件并继续?`,
+            });
+            if (overwrite) {
+                await fs.emptyDir(targetDir);
+            } else {
+                throw new Error(red("X") + " 操作被取消");
+            }
+        }
+    } catch (error) {
+        await fs.mkdirp(targetDir);
+    }
+    log.info("vite3", "%s目录已经准备就绪", targetDir);
}
module.exports = factory;

5.下载模板

5.1 安装

  • clone-git-repo

    下载仓库(GitHub, GitLab, Bitbucket,Gitee)

    yarn workspace @vite2/create  add clone-git-repo
    yarn workspace @vite2/utils  add  axios
    

5.2 settings\index.js

packages\settings\index.js

//执行命令脚本
exports.COMMAND_SOURCE = `
const args = JSON.parse(process.argv[1]);
const factory = require('.');
factory(args);
`
+//配置文件名称
+exports.RC_NAME = ".vite3rc";
+//组织的名称
+exports.ORG_NAME = "ORG_NAME";
+//git仓库类型
+exports.GIT_TYPE = "GIT_TYPE";
+//模板存放名称
+exports.TEMPLATES = ".vite3_templates";

5.3 utils\request.js

packages\utils\request.js

const axios = require("axios");
const { GIT_TYPE } = require("./config");
const GITEE = "https://gitee.com/api/v5";
const GITHUB = "https://api.github.com";

const BASE_URL = GIT_TYPE === "gitee" ? GITEE : GITHUB;
const request = axios.create({
    baseURL: BASE_URL,
    timeout: 5000,
});

request.interceptors.response.use(
    (response) => {
        return response.data;
    },
    (error) => {
        return Promise.reject(error);
    }
);
module.exports = request;

5.4 withLoading.js

packages\utils\withLoading.js

async function withLoading(message, fn, ...args) {
  const ora = await import("ora");
  const spinner = ora.default(message);
  spinner.start();
  const result = await fn(...args);
  spinner.succeed();
  return result;
}
module.exports = withLoading;

5.5 packages\utils\index.js

packages\utils\index.js

exports.log = require('./log');
exports.executeNodeScript = require('./executeNodeScript');
exports.config = require('./config');
+exports.withLoading = require('./withLoading');
+exports.request = require('./request');

5.6 create\index.js

packages\create\index.js

const path = require("path");
+const { promisify } = require("util");
const { red } = require("chalk");
const fs = require("fs-extra");
+const { prompt } = require("inquirer");
+const userhome = require("userhome");
+const clone = promisify(require('clone-git-repo'));
+const { TEMPLATES } = require("@vite2/settings");
+const { config, log,withLoading, request} = require("@vite2/utils");
async function factory(argv) {
    const { cwd, name } = argv;
    process.chdir(cwd);//切换为当前命令执行的工作目录
+   const { GIT_TYPE,ORG_NAME } = config;
    if (!ORG_NAME) {
        throw new Error(red("X") + " 尚未配置组织名称!");
    }
    const targetDir = path.join(process.cwd(), name);
    log.info("vite2", "创建的项目目录为%s", targetDir);
    try {
        await fs.access(targetDir);
        const files = await fs.readdir(targetDir);
        if (files.length > 0) {
            const { overwrite } = await prompt({
                type: "confirm",
                name: "overwrite",
                message: `目标目录非空,是否要移除存在的文件并继续?`,
            });
            if (overwrite) {
                await fs.emptyDir(targetDir);
            } else {
                throw new Error(red("X") + " 操作被取消");
            }
        }
    } catch (error) {
        await fs.mkdirp(targetDir);
    }
    log.info("vite3", "%s目录已经准备就绪", targetDir);
+    let repos = await withLoading("读取模板列表", async () =>
+        request.get(`/orgs/${ORG_NAME}/repos`)
+    );
+    let { repo } = await prompt({
+        name: "repo",
+        type: "list",
+        message: "请选择模板",
+        choices: repos.map((repo) => repo.name)
+    });
+    let tags = await withLoading("读取标签列表", async () =>
+        request.get(`/repos/${ORG_NAME}/${repo}/tags`)
+    );
+    let { tag } = await prompt({
+        name: "tag",
+        type: "list",
+        message: "请选择版本",
+        choices: tags,
+    });
+    let repository = GIT_TYPE + `:${ORG_NAME}/${repo}`;
+    if(tag)repository+=`#${tag}`;
+    const downloadDirectory = userhome(TEMPLATES);
+    const repoDirectory = `${downloadDirectory}/${repo}/${tag}`;
+    log.info("vite3", "准备下载模板到%s", repoDirectory);
+    try {
+        await fs.access(repoDirectory);
+    } catch (error) {
+        log.info("vite2", "从仓库下载%s", repository);
+        await clone(repository,repoDirectory, {clone:true});
+    }
}
module.exports = factory;

6.渲染模板

6.1 安装

yarn workspace @vite2/create  add ejs glob execa

ask.json

[
    {
        "name": "projectName",
        "type":"text",
        "message":"请输入项目名称"
    },
    {
        "name": "projectVersion",
        "type": "text",
        "message": "请输入项目版本"
    }
]

6.2 packages\settings\index.js

packages\settings\index.js

//执行命令脚本
exports.COMMAND_SOURCE = `
const args = JSON.parse(process.argv[1]);
const factory = require('.');
factory(args);
`
//配置文件名称
exports.RC_NAME = ".vite3rc";
//组织的名称
exports.ORG_NAME = "ORG_NAME";
//git仓库类型
exports.GIT_TYPE = "GIT_TYPE";
//模板存放名称
exports.TEMPLATES = ".vite3_templates";
+//重命名文件配置
+exports.RENAME_FILES = {
+    _gitignore: '.gitignore'
+}

6.3 packages\create\index.js

packages\create\index.js

const path = require("path");
const { promisify } = require("util");
const { red } = require("chalk");
const fs = require("fs-extra");
const { prompt } = require("inquirer");
const userhome = require("userhome");
const clone = promisify(require('clone-git-repo'));
+const glob = promisify(require("glob"));
+const { render } = require("ejs");
+const execa = require("execa");
+const { TEMPLATES,RENAME_FILES } = require("@vite2/settings");
const { config, log, withLoading, request } = require("@vite2/utils");
async function factory(argv) {
    const { cwd, name } = argv;
    process.chdir(cwd);//切换为当前命令执行的工作目录
    const { GIT_TYPE, ORG_NAME } = config;
    if (!ORG_NAME) {
        throw new Error(red("X") + " 尚未配置组织名称!");
    }
    const targetDir = path.join(process.cwd(), name);
    log.info("vite2", "创建的项目目录为%s", targetDir);
    try {
        await fs.access(targetDir);
        const files = await fs.readdir(targetDir);
        if (files.length > 0) {
            const { overwrite } = await prompt({
                type: "confirm",
                name: "overwrite",
                message: `目标目录非空,是否要移除存在的文件并继续?`,
            });
            if (overwrite) {
                await fs.emptyDir(targetDir);
            } else {
                throw new Error(red("X") + " 操作被取消");
            }
        }
    } catch (error) {
        await fs.mkdirp(targetDir);
    }
    log.info("vite3", "%s目录已经准备就绪", targetDir);
    let repos = await withLoading("读取模板列表", async () =>
        request.get(`/orgs/${ORG_NAME}/repos`)
    );
    let { repo } = await prompt({
        name: "repo",
        type: "list",
        message: "请选择模板",
        choices: repos.map((repo) => repo.name)
    });
    let tags = await withLoading("读取标签列表", async () =>
        request.get(`/repos/${ORG_NAME}/${repo}/tags`)
    );
    let { tag } = await prompt({
        name: "tag",
        type: "list",
        message: "请选择版本",
        choices: tags,
    });
    let repository = GIT_TYPE + `:${ORG_NAME}/${repo}`;
    if (tag) repository += `#${tag}`;
    const downloadDirectory = userhome(TEMPLATES);
    const repoDirectory = `${downloadDirectory}/${repo}/${tag}`;
    log.info("vite3", "准备下载模板到%s", repoDirectory);
    try {
        await fs.access(repoDirectory);
    } catch (error) {
        log.info("vite2", "从仓库下载%s", repository);
        await clone(repository, repoDirectory, { clone: true });
    }
+    let ask = path.join(repoDirectory, "ask.json");
+    if (fs.existsSync(ask)) {
+        const askOptions = await fs.readJSON(ask);
+        const result = await prompt(askOptions);
+        const files = await glob(`**/*`, {
+            cwd: repoDirectory,
+            ignore: ['ask.json'],
+            nodir: true
+        });
+        await Promise.all(files.map(file => new Promise(async function (resolve) {
+            let content = await fs.readFile(path.join(repoDirectory, file), 'utf8');
+            let renderContent = await render(content, result);
+            let targetName = RENAME_FILES[file] || file;
+            let targetFile = path.join(targetDir, targetName);
+            await fs.ensureDir(path.dirname(targetFile));
+            await fs.writeFile(targetFile, renderContent, 'utf8');
+            resolve();
+        })));
+    } else {
+        await fs.copy(repoDirectory, targetDir);
+    }
+    process.chdir(targetDir);
+    log.info("vite3", "在%s初始化 Git 仓库", targetDir);
+    await execa("git", ["init"], { stdio: "inherit" });
+    log.info("vite3", "在%s安装依赖", targetDir);
+    await execa("npm", ["install"], { stdio: "inherit" });
+    log.info("vite3", "启动服务");
+    await execa("node", ["./node_modules/esbuild/install.js"], {
+        stdio: "inherit",
+    });
+    await execa("npm", ["run", "dev"], { stdio: "inherit" });
}
module.exports = factory;

7.发布

发布

7.1 创建组织

7.2 发布

package.json

{
  "publishConfig": {
    "access": "public",
    "registry": "http://registry.npmjs.org"
  }
}
npm whoami
npm login
hscoder
lerna publish
  • 29
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值