# sk2-gulp-cli

> [PROJECT HOME](https://gitee.com/caiyue823/sk2-gulp-cli) ## Intro A workflow cli with built-in gulp-cli & gulpfile & flexible config

Latest version **1.0.6** (published 2021-06-15) · MIT license · 0 weekly downloads

## Install

```sh
npm install sk2-gulp-cli
pnpm add sk2-gulp-cli
yarn add sk2-gulp-cli
bun add sk2-gulp-cli
```

Provides the command `sk2-gulp-cli`.

## 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.6 |
| Published | 2021-06-15 |
| First published | 2021-06-14 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | none |
| Module format | CommonJS |
| Dependencies | 20 |
| Unpacked size | 37.8 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | K |
| Maintainers | caiyue823 |

## Links

- npm: https://www.npmjs.com/package/sk2-gulp-cli
- Repository: https://gitee.com/caiyue823/sk2-gulp-cli
- npm.io page: https://npm.io/package/sk2-gulp-cli

## Dependencies (20)

- [del](https://npm.io/package/del.md) ^6.0.0
- [gulp](https://npm.io/package/gulp.md) ^4.0.2
- [eslint](https://npm.io/package/eslint.md) 6.8.0
- [gulp-if](https://npm.io/package/gulp-if.md) ^3.0.0
- [node-cmd](https://npm.io/package/node-cmd.md) ^4.0.0
- [gulp-sass](https://npm.io/package/gulp-sass.md) ^4.1.0
- [gulp-swig](https://npm.io/package/gulp-swig.md) ^0.9.1
- [gulp-babel](https://npm.io/package/gulp-babel.md) ^8.0.0
- [@babel/core](https://npm.io/package/@babel/core.md) ^7.14.5
- [gulp-eslint](https://npm.io/package/gulp-eslint.md) ^6.0.0
- [gulp-uglify](https://npm.io/package/gulp-uglify.md) ^3.0.2
- [gulp-useref](https://npm.io/package/gulp-useref.md) ^3.1.6
- [browser-sync](https://npm.io/package/browser-sync.md) ^2.26.7
- [gulp-htmlmin](https://npm.io/package/gulp-htmlmin.md) ^5.0.1
- [gulp-imagemin](https://npm.io/package/gulp-imagemin.md) ^6.1.0
- [gulp-clean-css](https://npm.io/package/gulp-clean-css.md) ^4.3.0
- [gulp-sass-lint](https://npm.io/package/gulp-sass-lint.md) ^1.4.0
- [node-args-parser](https://npm.io/package/node-args-parser.md) ^0.0.1
- [@babel/preset-env](https://npm.io/package/@babel/preset-env.md) ^7.14.5
- [gulp-load-plugins](https://npm.io/package/gulp-load-plugins.md) ^2.0.7

## Recent versions

- 1.0.6 (latest) — 2021-06-15
- 1.0.5 — 2021-06-14
- 1.0.4 — 2021-06-14
- 1.0.3 — 2021-06-14
- 1.0.2 — 2021-06-14

## README

# sk2-gulp-cli
[项目主页](https://gitee.com/caiyue823/sk2-gulp-cli)
## 介绍
一个内置gulp-cli & gulpfile & 灵活配置的工作流cli

## 项目结构

```
└── sk2-gulp-cli ······项目根目录
   ├─ bin ·············node命令目录
   │  ├─ index.js ·····bin入口文件
   ├─ lib ·············gulp相关文件
   │  ├─ cmdPromise.js ····将node-cmd.run函数包装成Promise函数exec
   │  ├─ config.js ····gulefile相关配置，主要为路径默认配置
   │  ├─ data.js ······页面相关默认配置
   │  ├─ gulpfile.js ··主gulpfile文件
   │  ├─ index.js ·····导出gulefile，main入口文件
   ├─ .eslintrc.js ····eslint默认配置，可以被项目的eslint配置覆盖
   ├─ .gitignore ······git忽略文件配置
   ├─ .npmrc ··········npm镜像下载地址
   ├─ LICENSE ········证书
   ├─ lint.yml ········暂不使用
   ├─ package.json ····npm包说明文件
   ├─ project.config.js ····项目默认配置样本，为lib中config和data的合集。
                            本项目中无用，需在使用项目中添加
   ├─ README.md ·······项目说明
   
```


## 使用说明

###安装
1.  `npm install sk2-gulp-cli -D` 或 `yarn add sk2-gulp-cli -D`
2.  `sk2-gulp-cli taskName`, 可用命令如下

###命令
### `sk2-gulp-cli lint`

scss/js文件lint检查

### `sk2-gulp-cli compile`

scss/js/html文件编译。编译后的css/js/html会被放入`temp`文件夹中。

### `sk2-gulp-cli serve`

使用内置服务器预览、监听、调试代码。编译后的css/js/html会被放入`temp`文件夹中。

#### 参数

- `open`: 是否在启动服务器时打开浏览器窗口, 默认: `false`
- `port`: 设置端口号, 默认: `2080`

### `sk2-gulp-cli build`

打包项目并将文件放入`dist`目录. 推荐使用production模式对代码/资源进行压缩以达到最佳效果。

#### options

- `production`: production模式, 默认: `false`
- `prod`:  等同于production

### `sk2-gulp-cli start`

使用内置服务器预览production模式打包的项目以获取真实的线上浏览体验。
这个命令其实是`sk2-gulp-cli serve --prod`的别名。
注意：在这个模式下，源文件监听/调试将被关闭。如果你想要调试，请使用`sk2-gulp-cli serve`

#### 参数

- `open`: 是否在启动服务器时打开浏览器窗口, 默认: `false`
- `port`: 设置端口号, 默认: `2080`

### `sk2-gulp-cli deploy`

将打包的`dist`文件夹push到git仓库

#### 参数
- `branch`: 要push的分支名, 默认: `'gh-pages'`

### `sk2-gulp-cli clean`

清空`dist` & `temp`文件

### 配置
你可以通过在自己的项目根目录新建一个project.config.js，根据自己的需要来覆盖这些配置。
默认配置如下。项目根目录提供了一份project.config.js，可以直接复制它到你的项目根目录进行修改即可。
```javascript
module.exports = {
  data: {
    menus: [
      {
        name: 'Home',
        icon: 'aperture',
        link: 'index.html'
      },
      {
        name: 'Features',
        link: 'features.html'
      },
      {
        name: 'About',
        link: 'about.html'
      },
      {
        name: 'Contact',
        link: '#',
        children: [
          {
            name: 'Twitter',
            link: 'https://twitter.com/w_zce'
          },
          {
            name: 'About',
            link: 'https://weibo.com/zceme'
          },
          {
            name: 'divider'
          },
          {
            name: 'About',
            link: 'https://github.com/zce'
          }
        ]
      }
    ],
    pkg: require('./package.json'),
    date: new Date(),
  },
  config: {
    SRC: 'src',
    DIST: 'dist',
    TEMP: '.tmp',
    PUBLIC: 'public',
    PATHS: {
      style: 'assets/styles/*.scss',
      script: 'assets/scripts/*.js',
      page: '*.html',
      image: 'assets/images/**',
      font: 'assets/fonts/**',
    }
  }
}
```
## 实现
### gulpfile相关任务的实现
#### lint的实现
lint任务分为styleLint和scriptLint两个子任务，任务的实现相对简单，
分别定义两个任务，引入对应的lint插件，调用相应api即可。以下是scriptLint的实现
```javascript
const scriptLint = () => {
    return src(PATHS.script, srcConfig)
        //执行lint
        .pipe(plugins.eslint())
        //输出lint结果至控制台
        .pipe(plugins.eslint.format())
        // 如果报错终止执行
        .pipe(plugins.eslint.failAfterError())
}
```
在使用eslint插件时需注意的是，当手动传入参数至gulp-eslint插件时，插件的参数格式与.eslintrc.js不匹配。
例如，gulp-eslint插件的envs,globals属性均为数组形式，而.eslintrc.js对应的属性为对象。
因此更好的方式应该是不处理参数，让插件在运行时自动读取运行时项目根目录的.eslintrc.js
```javascript
const lint = parallel(styleLint, scriptLint);
```
在实现lint命令时使用parallel并行执行，一是缩短时间，二是因为任何一项lint任务一旦报错都无需往下继续。
当然这样做也有一个缺点，子任务的报错日志会交替输出，阅读上有点麻烦。

#### compile的实现
compile任务分为css/js/html三个子任务，输出半成品的css/js/html文件，配合useref/watcher任务使用。
以js任务为例，实现如下
```javascript
// script编译
const script = () => {
    return src(PATHS.script, srcConfig)
        .pipe(plugins.babel({
            // 使用require引用，当变成cli时，require找到的是当前cli下的node_modules里的包
            presets: [require('@babel/preset-env')]
        }))
        .pipe(dest(TEMP))
        // 与watcher中的watch任务配合，实现浏览器刷新
        .pipe(server.reload({stream: true}))
}
```
在实现compile子任务时需要注意以下几点
* 子任务生成的文件均输出到临时文件夹`temp`而不是`dist`目录。这样做有以下几点原因
1. 因为compile生成的都是`半成品`文件，它们并不具备在生产环境运行的条件，比如生成的js和css文件没有经过压缩，而且没有生成依赖库文件(vendor.css/js)，
html文件在没有经过useref处理时也没有真正生成对应的依赖库引用。因此它们不能被放入意味着生产的`dist`包里
2. 这些`半成品`可以配合useref任务生成真正的生产环境代码。而在使用useref时，
从`temp`目录读取半成品文件，然后经过处理写入`dist`，可以避免读写冲突
* 这些半成品文件可以配合watcher任务使用，提高在调试时的编译效率。因此需要在子任务的最后添加`server.reload`
* compile任务采用并行执行，以提高效率
```javascript
// 编译，并行执行
const compile = parallel(style, script, page);
```
#### build的实现
build任务的实现如下
```javascript
// 打包命令，需要先clean，然后并行执行（css/js/html的编译与引用查找，以及其他静态资源的压缩）
const build = series(clean, parallel(series(compile, useref), image, font, extra));
```
由于build需要区分开发/生产模式，因此在useref/image/font等子任务中均需要做相应处理(不包括compile)。
以下代码通过插件从命令行中获取isProd。当isProd为true时，
需要对所有的资源进行压缩/混淆/查找引用等处理
```javascript
// 转化命令行参数为一个对象
const args = require('node-args-parser')(process.argv);
console.log('args', args);
// 处理相关命令行参数
// 提取prod参数供后续使用
const isProd = args['-production'] || args['-prod'] || false;
```
其中useref的实现如下
```javascript
const useref = () => {
    return src(PATHS.page, tempConfig)
    // tempConfig中的cwd：TEMP会将useref的工作目录改为TEMP，
    // 加上..才是项目根目录，从而找到node_modules
        .pipe(plugins.useref({searchPath: [TEMP, '.', '..']}))
        // 仅在prod模式下进行相应文件压缩
        .pipe(plugins.if(isProd && /\.css$/, plugins.cleanCss()))
        .pipe(plugins.if(isProd && /\.js$/, plugins.uglify()))
        .pipe(plugins.if(isProd && /\.html$/, plugins.htmlmin({
            collapseWhitespace: true,
            removeComments: true,
            minifyJS: true,
            minifyCSS: true,
        })))
        .pipe(dest(DIST));
}
```
image任务
```javascript
const image = () => {
    return src(PATHS.image, srcConfig)
    // 仅在prod模式下进行压缩
        .pipe(plugins.if(isProd, plugins.imagemin()))
        .pipe(dest(DIST));
}
```
### serve的实现
实现serve时需要考虑以下两点
1. 开发模式下，监听文件变化，编译调试，需要用到compile任务
2. 生产模式下，预览production模式打包的项目以获取真实的线上浏览体验，需要用到build任务
#### serveDev的实现
这是开发环境下的serve，由compile和watcher任务组成
```javascript
// 开发模式下的serve，编译加watcher即可
const serveDev = series(compile, watcher);
```
#### serveProd的实现
这是生产环境下的serve，由compile和watcher任务组成
```javascript
// prod模式下的serve，需要先打包，然后watcher即可。
// 这时候watcher中不监听任何文件变化，相关逻辑在watcher中处理
const serveProd = series(build, watcher);
```
在watcher任务中根据isProd处理相关逻辑，代码如下
```javascript
const watcher = () => {
    // 开发模式下监听文件变化；prod模式不监听
    if (!isProd) {
        // 监听相关css/js/html文件，并重新执行对应的编译
        watch(PATHS.style, srcConfig, style);
        watch(PATHS.script, srcConfig, script);
        watch(PATHS.page, srcConfig, page);
        // 监听图片/字体/其他静态资源文件，刷新浏览器
        watch([PATHS.image, PATHS.font], srcConfig, server.reload);
        watch('**', publicConfig, server.reload);
    }
    const serverCfg = {
        // prod模式只使用dist目录
        // 开发模式下，需使用temp里的css/js/html文件，src下的图片/字体文件，public下的其他静态资源文件
        baseDir: isProd ? [DIST] : [TEMP, SRC, PUBLIC],
        // prod模式下，不需要任何路由；开发模式下，需要通过路由找到/node_modules下的vendor文件
        routes: !isProd && {
            '/node_modules': 'node_modules',
        }
    }
    server.init({
        // 优先使用命令行参数的设置
        port: args['-port'] || 2080,
        open: args['-open'] || false,
        server: {
            ...serverCfg,
        },
    })
}
```
而最后向外暴露出的serve命令如下
```javascript
// 对外暴露的serve命令，根据命令行prod参数执行不同的serve
const serve = isProd ? serveProd : serveDev;
```
### deploy的实现
deploy的目标是将打包的dist文件夹git push到github上。这个任务很有意思，
个人思路其实很简单，通过&&连接命令来执行。但中间经过了几轮试错，在这里记录一下过程
##### round 1. 通过在gulpfile.js里往process.argv里push git相关命令，没有被执行，失败！
```javascript
function gitPush(branchName = 'gh-pages') {
    //git add foldername
    //git commit -m "commit operation"
    process.argv.push('deploy');
    process.argv.push('&&');
    process.argv.push('git');
    process.argv.push('add');
    process.argv.push('.');
```
##### round 2. 我就在想，是不是push argv的时机晚了，于是在bin/index.js里尝试如上方式。报了&&不是一个gulp task，找不到任务，失败！
##### round 3. 这时候脑子有点转过弯来了，可能跟push参数根本没有关系，需要寻找一个执行命令行的插件
##### 找到两款，分别叫node-cmd和node-run-cmd（Promise式）
##### 先试了node-cmd，中间经过几轮调试，终于成功push了！！！当时bin/index.js代码长这样
```javascript
const args = require('node-args-parser')(process.argv);
const gulpfilePath = require.resolve('..');
const cmd = require('node-cmd');
const cwd = process.cwd();
process.argv.push('--gulpfile');
process.argv.push(gulpfilePath);
process.argv.push('--cwd');
process.argv.push(cwd);

handleDeploy();
function handleDeploy() {
    // 如果非deploy命令，走require方式执行
    if (process.argv[2] !== 'deploy') {
        require('gulp/bin/gulp');
        return;
    }
    const branchName = args['-branch'] || 'gh-pages';
    console.log(gulpfilePath, cwd);
    // 如果是deploy，为了保证命令的先后执行，需要在这里手动执行gulp
    cmd.run(`gulp deploy --gulpfile ${gulpfilePath} --cwd ${cwd}`, (e, data) => {
        if (e) {
            console.log(e);
            return;
        }
        console.log(data);
        cmd.run('git add .', (e) => {
            if (e) {
                console.log(e);
                ...省略
```
##### 你以为这就完了？NO!在经过短暂的喜悦之后，我陷入了深深的长考。因为这里的实现有着很明显的问题。
1. 为了保证git命令在gulp后面执行，自己手动执行了gulp
2. 回调地狱的代码实在是太TMD丑了！！！
3. bin的代码太不纯洁了！！！
##### round 4. 既然这样的方式可行，就想着是不是可以把它放在gulpfile.js里，这不是有node-run-cmd支持Promise嘛，顺便优化一下写法
##### 然而node-run-cmd里面存在一些问题，比如只能打印exitcode，又比如它的Promise会在上一个命令出错的情况下继续往then里走，而不是catch
##### round 5. 切换回node-cmd，用Promise包装了一下
```javascript
// 将cmd run方法包装成一个Promise方法exec
const cmd = require('node-cmd');

function exec(line) {
    return new Promise(function (resolve, reject) {
        cmd.run(line, (err, data) => {
            if (err) {
                reject(err);
                return;
            }
            resolve(data);
        })
    })
}

module.exports = {
    exec,
}
```
##### 回到gulpfile里写git方法，这下就很好看了，并且能在命令行打印每一步该有的输出
```javascript
// 使用node-cmd插件异步执行git cmd
const git = (done) => {
    return exec(`git add ./${DIST}`)
        .then((data) => {
            console.log('git add success', data);
            return exec(`git commit -m "commit ${DIST} ${new Date().toLocaleString()}"`)
        })
        .then((data) => {
            console.log('git commit success', data);
            return exec(`git push origin ${args['-branch'] || 'gh-pages'}`)
        })
        .then((data) => {
            console.log('git push success', data);
        })
        .catch(e => {
            console.log(e);
            done(false);
        })
}
```
##### deploy命令水到渠成，搞定！
```javascript
// deploy命令，先打包，再git
const deploy = series(build, git);
```

整体gulpfile实现思路如上，具体代码及详细注释请参考lib/gulpfile.js

### 将gulpfile包装为cli
实现如下
#### 1. lib下放置config.js, data.js, gulpfile.js, index.js
config.js和data.js分别为gulpfile的相关配置文件，而index.js只是对gulpfile.js
做了一个导出
依次来看
* config.js
```javascript
// 默认的config参数，可以被项目根目录的project.config.js里的config对象覆盖
module.exports = {
    SRC: 'src',
    DIST: 'dist',
    TEMP: 'temp',
    PUBLIC: 'public',
    PATHS: {
        style: 'assets/styles/*.scss',
        script: 'assets/scripts/*.js',
        page: '*.html',
        image: 'assets/images/**',
        font: 'assets/fonts/**',
    }
}
```
* data.js
```javascript
// 默认的data参数，可以被项目根目录的project.config.js里的data对象覆盖
module.exports = {
    menus: [...省略],
    //因为被放在lib下，需要往上一级找到package.json
    pkg: require('../package.json'),
    date: new Date(),
}
```

* gulpfile.js
```javascript
// 获取命令行执行目录
const CWD = process.cwd();

// 设置配置文件名称
const configFileName = 'project.config.js';
// 读取gulp 编译相关默认配置
let cfg = {
    data: require('./data'),
    config: require('./config')
};
try {
    const projectCfg = require(resolve(CWD, configFileName));
    cfg = Object.assign({}, cfg, projectCfg);
} catch (e) {
    console.log(`read ${configFileName} error`);
}
```
* index.js 可以被省略
```javascript
module.exports = require('./gulpfile');
```
#### 2. 在package.json中添加main，指向lib/index.js
#### 3. 在bin下面创建index.js，代码及说明如下
```javascript
#!/usr/bin/env node

/**
 * 通过往命令行参数里添加--gulpfile，将运行项目的gulpfile指向当前cli包的lib/gulpfile
 * 通过往命令行参数里添加--cwd，并将运行项目的cwd传入，以防止gulp将运行目录重置为当前cli包目录
 */

process.argv.push('--gulpfile');
// require.resolve仅解析当前文件路径，并不执行当前文件代码，通过入参..的方式找到上一级目录（cli包目录）。
// 此时根据node查找模块的机制，会找到包目录的package.json里的main字段，从而定位到lib下的index文件
process.argv.push(require.resolve('..'));
process.argv.push('--cwd');
process.argv.push(process.cwd());
// console.log(process.argv);
// 这里使用了npm查找包的机制，会找到cli包下的node_modules里的gulp
// 而gulp中执行了require('gulp-cli')()，真正去执行gulpfile.js
require('gulp/bin/gulp');
```
#### 4. 在package.json中添加bin，指向bin/index.js
#### 5. 在package.json中添加files，声明要导出的bin和lib文件夹
```json
{"files": ["bin","lib"]}
```
#### 6. 最后yarn publish即可

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