# lqsimooc-cli-dev

> - .js => module.exports/exports - .json => 内部会通过JSON.parse()返回一个对象 - .node => c++插件（一般用不到） - any => .js解析

Latest version **1.0.0** (published 2023-04-01) · ISC license · 0 weekly downloads

## Install

```sh
npm install lqsimooc-cli-dev
pnpm add lqsimooc-cli-dev
yarn add lqsimooc-cli-dev
bun add lqsimooc-cli-dev
```

## Health

**Score 15/100 (F)** — status: abandoned.

Positive: no vulnerabilities.

Warnings: low downloads; no types; no esm support.

Negative: abandoned; low maintenance score.

## Facts

| | |
|---|---|
| Version | 1.0.0 |
| Published | 2023-04-01 |
| First published | 2023-04-01 |
| Weekly downloads | 0 |
| License | ISC |
| TypeScript types | none |
| Module format | CommonJS |
| Dependencies | 0 |
| Unpacked size | 221.8 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | ashuiweb |

## Links

- npm: https://www.npmjs.com/package/lqsimooc-cli-dev
- npm.io page: https://npm.io/package/lqsimooc-cli-dev

## Recent versions

- 1.0.0 (latest) — 2023-04-01

## README

# 脚手架开发全流程记录
## 1. require能加载资源类型
- .js   => module.exports/exports
- .json => 内部会通过JSON.parse()返回一个对象
- .node => c++插件（一般用不到）
- any => .js解析

## 2. 封装@lqsimooc-cli-dev/log工具库
在utils/log中新建npm项目，在cli中引入这个包 `pnpm i @lqsimooc-cli-dev/log -r --filter @lqsimooc-cli-dev/core-cli`

然后就能使用了 
```js
const log = require("@lqsimooc-cli-dev/log");
log()
```
## 3. 使用semver进行版本号的比对

## 4. 使用colors来输出彩色文字

## 5. 使用root-check来root用户降级
root用户的操作一般都没有权限相关问题，为了规避一些文件权限问题，所以不让使用root用户
使用1.0.0版本支持require引入

## 6. 使用user-home获取用户家目录

## 7. 使用path-exists判断路径是否存在
4.0.0版本
```js
const pathExists = require("path-exists").sync;
const userHome = require("user-home");
pathExists(userHome)
```
## 8. 使用minimist@1.2.5检查命令参数
```js
function checkInputArgs() {
    const minimist = require("minimist");
    const args = minimist(process.argv.slice(2));
    console.log(args);
}
imooc-cli -wd
{ _: [], w: true, d: true }

imooc-cli --wd
{ _: [], wd: true }

```

## 9. 使用dotenv从.env文件中加载环境变量
该文件中的配置会自动挂载到process.env上
```js
function checkEnv() {
    const dotenv = require("dotenv");
    const dotenvPath = path.resolve(userHome, ".env");
    if (pathExists(dotenvPath)) {
        config = dotenv.config({
            path: dotenvPath
        });
    }
    log.verbose("环境变量", config, process.env.DB_PWD);
}
```
## 10. 开发get-npm-info 工具库
用来获取最新的版本号
### 10-1。 获取npm包信息
https://registry.npmjs.org/@lqsimooc-cli-dev/core-cli


### 10-2. 使用url-join@4.0.1来做url的拼接
然后用axios访问获取数据，拿到最新版本信息，如果有最新的版本就提示更新

## 11. 让Node支持ES Module
- 使用webpack
  ```js
  const path = require('path');
    module.exports = {
    entry: './bin/core.js',
    output: {
        path: path.join(__dirname, '/dist'),
        filename: 'core.js',
    },
    mode: 'development',
    target: 'node',
    module: {
        rules: [
        {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
            loader: 'babel-loader',
            options: {
                presets: [ '@babel/preset-env' ],
                plugins: [
                [
                    '@babel/plugin-transform-runtime',
                    {
                    corejs: 3,
                    regenerator: true,
                    useESModules: true,
                    helpers: true,
                    },
                ],
                ],
            },
            },
        },
        ],
    },
    };

  ```
- nodejs原生 后缀名.mjs 里面全都都用es modules 
  nodejs 14以前需要 `node --experimental-modules xxx.mjs`
## 12.使用commander@6.2.1来注册命令

## 12.1 在commands包中创建第一个命令 init

## 12.2 动态加载init命令
不同团队的init命令可能不同，为了增加灵活性，需要考虑动态加载init命令
### 12.2.1 全局指定本地调试文件路径
- 定义全局option：`program.option("-tp, --targetPath <targetPath>", "指定本地调试文件路径");`
- 监听 targetPath 把值放到环境变量
  ```js
  program.on("option:targetPath", function () {
        process.env.CLI_TARGET_PATH = program.targetPath;
  });
  ```
- 外部init命令库读取环境变量
  ```js
  function init(projectName, cmdObj) {
    console.log("init123123", projectName, cmdObj.force, process.env.CLI_TARGET_PATH);
    }
  ```
### 12.2.2 开发exec包用来执行js文件

```js
    // 1.targetPath => modulePath
    // 2.modulePath => Package(npm模块)
    // 3.package.getRootFile(获取入口文件)
    // 4.package.update()  package.install()
```
### 12.2.3 在models中开发Package类，来统一管理操作package
#### 12.2.3.1 使用pkg-dir来获取package的根路径
```js
 getRootFilePath() {
        // 1. 获取package.json所在目录 - pkg-dir
        const dir = pkgDir(this.targetPath);
        if (dir) {
            // 2. 读取package.json
            const pkgFile = require(path.resolve(dir, "package.json"));
            // 3. main/lib
            if (pkgFile && pkgFile.main) {
                return path.resolve(dir, pkgFile.main);
            }
            // 4. 路径兼容
        }
        return null;
    }
```

#### 12.2.3.2 在utils中开发utils包
```js
 isObject(o) {
        return Object.prototype.toString.call(o) === "[object Object]";
    }
```
#### 12.2.3.3 在utils中开发format-path包
用来兼容各平台路径,
```js
function formatPath(p) {
    if (p && typeof p === "string") {
        const sep = path.sep;
        if (sep === "/") return p;
        else { //windows
            return p.replace("/\\/g", "/");
        }
    }
    return p;
}
```
#### 12.2.3.4 使用npminstall 来安装package

#### 12.2.3.5 使用fs-extra 来进行文件相关操作

### 12.2.4 动态加载init 总结
- 之前init的命令直接执行init方法，现在通过执行一个中间层 exec方法
- exec中通过一个对象映射来找到对应方法的对应packageName
  ```js
    const SETTINGS = {
        init: "@lqsimooc-cli-dev/init"
    };
  ```
- 如果传入了targetPath `imooc-cli init -tp /Users/ashui/Desktop/架构师/01.脚手架/lqsimooc-cli-dev/commands/init/lib/index.js` 那么获取`rootFilePath`就会执行对应的文件
- 如果没有传入就会走默认缓存路径
  - 相关路径：`targetPath:path.resolve(homePath, CACHE_DIR)` `storeDir: path.resolve(targetPath, "node_modules");`  
  - 对应逻辑：
    - 如果包存在就更新包
    - 如果不存在就安装包
  - 执行`rootFilePath` 中的文件

## 13 node多进程
将之前的rootFilePath的执行(require执行)
```js
 if (rootFilePath) require(rootFilePath).apply(null, arguments);
```
改为：子进程调用
### 13.1 child_process 用法
具体查看 testDemo/process/child1.js
- 异步：
  - exec
  - execFile
  - fork
  - spawn
- 同步：
  - execSync
  - execFileSync
  - spawnSync
- 区别：
  - exec & execFile： exec主要用来执行shell命令，execFile用来执行shell文件， 通过回调机制返回信息（适合开销较小的任务,一次性把结果返回回调中）
  - spawn 没有回调，返回一个子进程，默认通过事件监听处理信息 适合耗时任务（比如：npm install）等 需要不断日志的任务 可以通过options中 stdio: "inherit" //实时在主进程打印
  - fork 使用node（开启一个子进程）去执行一个文件，返回一个子进程对象 可以和子进程双向通信
  ```js
    const child1 = cp.fork(path.resolve(__dirname, "fork.js"));
    //异步
    //child1.send("hello fork");
    child1.send("hello fork child process", () => {
        //向子进程发送消息
        console.log("向子进程发送消息");
    });

    child1.on("message", (msg) => {
        console.log(msg);
        //断开和子进程的连接
        child1.disconnect();
    });
  ```
#### 13.1.1 给文件添加可执行权限
`chmod +x ./test.shell`

#### 13.1.2 抹平操作系统通过cp.spawn执行node的差异
```js
// other: cp.spawn("node", ["-e", code],{})
// windows:  cp.spawn("cmd",["/c",'node','-e',code],{})
function spawn(command, args, options = {}) {
    const isWin32 = process.platform === "win32";
    const cmd = isWin32 ? "cmd" : command;
    const cmdArgs = isWin32 ? ["/c"].concat(command, args) : args;
    return cp.spawn(cmd, cmdArgs, options);
}

```

### 13.2 在models中新建command命令类
- 检查node版本 checkNodeVersion
- 格式化入参 initArgs
- 让子类实现 init
- 让子类实现 exec

## week5 脚手架创建项目流程设计和开发
### week5-1 主要内容
- 脚手架项目创建功能的架构设计
- 通过命令行交互获取项目基本信息
- eggjs+云MongoDB的集成
- 开发前端项目模板
- eggjs获取项目模板Api开发
- 项目模板下载功能开发
- 加餐：自己实现一个命令行交互（放弃）

### week5-2 prepare阶段
主要就是通过命令行交互收集项目信息，生成项目配置，为下一步模板的下载和安装做铺垫

![prepare阶段](./media/week5-1.png)

#### week5-2-1 判断当前目录是否为空
node判断目录是否为空
```js
  isCwdEmpty() {
        const currentpath = process.cwd();
        // console.log(currentpath);
        // console.log(path.resolve("."));
        let files = fs.readdirSync(currentpath);
        if (!files) return true;
        //过滤出 不以.开头 和 不是node_modules的文件或目录
        files = files.filter((file) => !file.startsWith(".") && ["node_modules"].indexOf(file) < 0);
        return files.length === 0;
    }
```
#### week5-2-2 使用inquirer库做命令行交互
```js
import inquirer from "inquirer";

inquirer
    .prompt([
        {
            type: "input", //input, number, confirm, list, rawlist, expand, checkbox, password, editor
            name: "yourName",
            message: "请输入你的名字",
            prefix: "ashuiweb:",
            suffix: "哎哟不错",
            //输入时的展现形式,不会改变结果
            transformer: (name) => `我的名字叫：${name}。`,
            //改变输入结果
            filter: (name) => (name += "!"),
            //validate: (v) => !!v
            //提示信息
            validate: function (v) {
                const done = this.async();
                if (!/^\d{1,2}\.\d{1,2}\.\d{1,2}$/.test(v)) {
                    done("参考格式:1.0.0");
                    return;
                }
                done(null, true);
            }
        },
        //when 根据用户之前输入来提问
        {
            type: "confirm",
            message: "你真的叫ashui吗",
            name: "confirm",
            when: (answers) => answers.yourName === "ashui"
        },
        {
            type: "list",
            name: "language",
            message: "your language",
            choices: [
                { value: 1, name: "php" },
                { value: 2, name: "node" },
                { value: 3, name: "js" }
            ]
        },
        //和list就是交互形式不同，基本差不多
        {
            type: "rawlist",
            name: "hobby",
            message: "your hobby",
            choices: [
                { value: 1, name: "php" },
                { value: 2, name: "node" },
                { value: 3, name: "js" }
            ]
        },
        //提供简写的形式 需要用户手动输入key key不能省
        {
            type: "expand",
            name: "color",
            message: "your color",
            default: "red",
            choices: [
                { key: "R", name: "红", value: "red" },
                { key: "G", name: "绿", value: "green" },
                { key: "B", name: "蓝", value: "blue" }
            ]
        },
        {
            type: "checkbox",
            name: "ball",
            message: "your ball",
            default: 0,
            choices: [
                { name: "足球", value: 1 },
                { name: "篮球", value: 2 },
                { name: "铅球", value: 3 }
            ]
        },
        {
            type: "editor",
            name: "message",
            message: "your message"
        }
    ])
    .then((answers) => {
        console.log(answers);
        // Use user feedback for... whatever!!
    })
    .catch((error) => {
        if (error.isTtyError) {
            // Prompt couldn't be rendered in the current environment
        } else {
            // Something else went wrong
        }
    });

```

### week5-3 eggjs项目创建
流程：
- 云mongoDb中插入数据
- eggjs中调用mongo库读取数据
- 控制器中返回数据

#### week5-3-1 安装 egg
`npm init egg --type=simple`
#### week5-3-2 执行npm init packageName
会下载执行 create-packageName

#### week5-3-3 通过egg.js创建api
#### week5-3-4 使用@pick-star/cli-mongodb 来快速使用momgodb

#### week5-3-5 在app目录下新建utils/mongo.js


### week5-4 utils中新建request包用来发请求

### week5-5 获取项目模板并添加选择项目模板流程
- 获取项目模板
```js
const request = require("@lqsimooc-cli-dev/request");

module.exports = () =>
    request({
        url: "/project/template"
    });

```
- 在inquire.propmt中插入
```js
{
    type: 'list',
    name: 'projectTemplateInfo',
    message: '请选择项目模板',
    choices: this.template.map((it) => ({ value: it.name, name: it.title })),
    //通过inquire中options的filter会修改最终返回结果 //最终返回
    /**
             {
                title: 'vue-element-admin模板',
                name: 'lqsimooc-cli-dev-tempate-vue-element-admin',
                version: '1.0.0'
            }
                */
    filter: (v) => this.template.find((it) => it.name === v),
    },
       
```
### week5-6 获取模板信息后，下载或更新模板包
使用我们之前创建的package类型来安装或更新包
```js
 async downloadTemplate() {
    //根据projectTemplateInfo下载
    const { projectTemplateInfo } = this.projectInfo;
    const { name, version } = projectTemplateInfo;
    const targetPath = path.resolve(process.env.HOME_PATH, process.env.CLI_HOME_PATH, 'template');
    const storeDir = path.resolve(targetPath, 'node_modules');
    const templatePkg = new Package({
      targetPath,
      storeDir,
      packageName: name,
      packageVersion: version,
    });
    if (!(await templatePkg.exists())) {
      log.info('模板下载中：', 'loading...');
      await templatePkg.install();
    } else {
      log.info('更新模板中：', 'loading...');
      await templatePkg.update();
    }
  }
```
### week5-7 下载时间有点长？使用cli-spinner给命令行加个loading效果吧

### week6 安装模板到项目

#### week6-1 ejs的用法
- ejs.compile返回渲染器，通过给渲染器传入数据来渲染结果
  ```js
  const compileHtml = ejs.compile(template, options);
  const compiledHtml = compileHtml(data);
  ```
- ejs.render
  ```js
  const renderedTHtml = ejs.render(template, data, options);
  console.log(renderedTHtml);
  ```
- ejs.renderFile
  - promise
  ```js
  const renderFile = ejs.renderFile(path.resolve(__dirname, 'temp.html'), data, options);
  renderFile.then(console.log);
  ```
  - 回调
  ```js
  ejs.renderFile(path.resolve(__dirname, 'temp.html'), data, options, function (err, data) {
  if (err) {
    console.log(err);
    return;
  }
  console.log(data);
  });
  ```

#### week6-2 ejs的标签含义
- <% '脚本' 标签，用于流程控制，无输出。
- <%= 输出数据到模板（输出是转义 HTML 标签）
- <%_ 删除其前面的空格符
- <%- 输出非转义的数据到模板
- <%# 注释标签，不执行、不输出内容
- <%% 输出字符串 '<%'
- %> 一般结束标签
- -%> 删除紧随其后的换行符
- _%> 将结束标签后面的空格符删除


#### week6-3 ejs其他功能
- 引入其他模板
  ```html
  <%# include 其他模板 _%> 
  <#- include('header.html', {user}); #>
  ```
- 配置自定义分隔符
  ```js
  const renderFile = ejs.renderFile(path.resolve(__dirname, 'temp.html'), data, {
     delimiter: '?', //自定义分隔符 默认 '#'
    }); 
  ```
  ```html
   <h1>版权所有：<?= user.copyRight ?> </h1>
  ```
- 自定义加载器 使用此功能，您可以在读取模板之前对其进行预处理。
  ```js
  let myFileLoader = function (filePath) {
  return 'myFileLoader: ' + fs.readFileSync(filePath);
  };
  ejs.fileLoader = myFileLoader;
  ```
#### week6-4 使用glob库来匹配文件路径
```js
const glob = require('glob');
//异步使用
(async function () {
  const jsfiles = await glob('**/*.js', { ignore: 'node_modules/**' });
  console.log(jsfiles);
})();
//同步使用
const jsfiles = glob.globSync('**/*.js', { ignore: 'node_modules/**' });
console.log(jsfiles);
//读取流
const filesStream = glob.globStream('**/*.js', { ignore: 'node_modules/**' });
const jsFilesArr = [];
filesStream.on('data', (data) => {
  //console.log(data);
  jsFilesArr.push(data);
});
filesStream.on('end', function () {
  console.log(jsFilesArr);
});

```
#### week6-5 安装模板
如果模板type是normal的就是普通安装
```js
    spinnerStart('模板安装中');
    //拷贝模板代码至当前目录
    const sourcePath = path.resolve(tempath, 'template');
    const targetPath = process.cwd(); //当前目录
    try {
      fse.copySync(sourcePath, targetPath);
    } catch (error) {
      throw error;
    } finally {
      spinnerEnd();
      log.success('模板安装成功');
    }
```

#### week6-6 安装依赖和执行
模板数据中配置installCommand 和 startCommand,将spawn抽离出来用来执行命令
```js
const {
      projectTemplateInfo: { installCommand, startCommand },
    } = this.projectInfo;
    //拿到命令和命令参数
    if (installCommand) {
      await this.execCommand(installCommand, '依赖安装过程失败');
    }
    if (startCommand) {
      await this.execCommand(startCommand, '启动命令失败');
    }
```
execAsync 抽离的spawn方法
```js
async execCommand(command, errMsg) {
    const [cmd, ...args] = command.split(' ');
    if (!WHITE_CMDS.includes(cmd)) throw new Error('远程模板存在非法命令');
    //执行命令
    const res = await execAsync(cmd, args, {
      stdio: 'inherit',
      cwd: process.cwd(),
    });
    if (res !== 0) throw new Error('errMsg');
  }
```
#### week6-7 使用kebab-case库来转化驼峰到classname形式
```js
var kebabCase = require('kebab-case');

console.log(kebabCase('webkitTransformDemo'));
// "webkit-transform-demo
console.log(kebabCase.reverse('-webkit-transform'));
// "WebkitTransform"

```
#### week6-8 重点 ejs替换模板内容
1. 先用glob获取项目文件
   ```js
    async ejsRender(_options = {}, data = {}) {
    const dir = process.cwd();
    const options = { ignore: ['node_modules/**'], cwd: dir, nodir: true, ..._options };
    const files = await require('glob')('**', options);
  }
   ```
2. 再用ejs处理对应的文件，处理好后重新写入
  ```JS
   async ejsRender(_options = {}, data = {}) {
    const dir = process.cwd();
    const options = { ignore: ['node_modules/**'], cwd: dir, nodir: true, ..._options };
    const files = await require('glob')('**', options);
     await Promise.all(
      files.map((file) => {
        const filePath = path.resolve(dir, file);
        //后缀名白名单
        if (EJS_RENDER_FILE_TYPE.includes(path.extname(filePath).substring(1))) {
          return ejs.renderFile(filePath, data).then((result) => {
            //重新写入
            return fse.writeFile(filePath, result);
          });
        }
      })
    );
  }
  
  ```
  #### week6-9 处理选择模板是组件以及是自定义模板
  如果创建的时候选择是组件，那么组件比项目多一个描述信息，需要用户手动去填写。
  
  什么是自定义模板，自定义模板就是我们只管下载，至于安装逻辑交由模板中的index.js去处理
  ```js
  const code = `require("${rootFilePath}")(${JSON.stringify(options)})`;
  log.verbose('code', code, options);
  await execAsync('node', ['-e', code], { stdio: 'inherit', cwd: process.cwd() });
  ```
  直接执行
  ```js
  require("/Users/ashui/.imooc-cli/template/node_modules/_lqsimooc-cli-dev-tempate-custom-vue3@1.0.3@lqsimooc-cli-dev-tempate-custom-vue3/index.js")
  ({"type":"project","projectName":"asd","version":"1.0.0","projectTemplateInfo":{"title":"vue3自定义模板","name":"lqsimooc-cli-dev-tempate-custom-vue3","version":"1.0.3","type":"custom","tag":["project"],"ejsIgnore":["public/**"],"path":"/Users/ashui/.imooc-cli/template/node_modules/_lqsimooc-cli-dev-tempate-custom-vue3@1.0.3@lqsimooc-cli-dev-tempate-custom-vue3"},"className":"asd","sourcePath":"/Users/ashui/.imooc-cli/template/node_modules/_lqsimooc-cli-dev-tempate-custom-vue3@1.0.3@lqsimooc-cli-dev-tempate-custom-vue3/template","targetPath":"/Users/ashui/Desktop/test/imooc-cli-test"})
  ```

  #### week6-10 当前小结
  目前为止，我们从0到1开发了一个脚手架，实现的功能如下：
  
 `imooc-cli init projectName  -tp /Users/ashui/Desktop/架构师/01.脚手架/lqsimooc-cli-dev/commands/init  -f -c -d`

- imooc-cli 是整体命令
- init 是 imooc-cli的一个子命令 
- -tp 和 -d 是immoc-cli的option 分别指代 本地调试路径和开启debug模式
- -f 和 -c 是init的option 分别指代是否强制初始化项目和是否读取上次缓存的远程模板数据。
- 如果传了tp 那么就用指定的包来执行init命令，否则就会从npm中下载@lqsimooc-cli-dev/init包进行inti命令

执行流程如下：
- prepare阶段
  - 检查版本号
  - root降级
  - 检查用户主目录
  - 检查环境变量
  - 检查更新
- 注册命令
  - 监听到init命令执行exec方法
- exec方法
  - 获取执行init的是哪个包
    - 如果传了targetPath就用这个包
    - 如果没有则需要安装或更新@lqsimooc-cli-dev/init
  - 子进程去执行这个包
    ```js
    const code = `require('${rootFilePath}').call(null, ${JSON.stringify(args)})`;
      //新的进程
      const child = spawn('node', ['-e', code], {
        cwd: process.cwd(),
        //将子进程的输出实时输出到父进程
        stdio: 'inherit',
      });
    ```
  - 执行init包
    - 初始化
      - 检查node版本
      - 检查参命令参数
    - 准备阶段
      - 判断项目模板是否存在（加入是否开启模板缓存判断的逻辑）
      - 判断目录是否是空（加入是否开启强制清空目录逻辑）
      - 再次提醒是否要清空目录（高危操作）
    - 收集项目信息
      - 项目类型：项目还是组件
      - 版本信息
      - 拉取对应类型的模板（前面已经在判断项目模板存在时已经做了，这里只要显示对应类型的模板即可）
      - 如果是组件还要填写项目描述
    - 下载模板
    - 安装模板
      - 普通模板
        - 将下载下来的模板拷贝到当前项目目录
        - 进行ejs模板渲染（替换掉需要ejs渲染的地方）
        - 执行install（如果有installCommand）
        - 执行start（如果有startCommand）
      - 自定义模板
        - 执行下载下来的模板的主文件（传入已经获取到项目信息等）

---
_Source: https://npm.io/package/lqsimooc-cli-dev · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
