Sign Up for Free

RunKit +

Try any Node.js package right in your browser

This is a playground to test code. It runs a full Node.js environment and already has all of npm’s 400,000 packages pre-installed, including gulp-chef with all npm packages installed. Try it out:

require("gulp/package.json"); // gulp is a peer dependency. var gulpChef = require("gulp-chef")

This service is provided by RunKit and is not affiliated with npm, Inc or the package authors.

gulp-chef v0.1.4

Cascading configurable recipes for gulp 4.0. An elegant, intuitive way to reuse gulp tasks.

gulp-chef

支援 Gulp 4.0,允许嵌套配置任务及组态。以优雅、直觉的方式,重复使用 gulp 任务。

编码的时候你遵守 DRY 原则,那编写 gulpfile.js 的时候,为什么不呢?

注意:此专案目前仍处于早期开发阶段,因此可能还存有错误。请协助回报问题并分享您的使用经验,谢谢!

[加入在https://gitter.im/gulp-cookery/gulp-chef 上的讨论](https:/ /gitter.im/gulp-cookery/gulp-chef?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

功能

  • 支援 Gulp 4.0,
  • 自动载入本地通用任务 (recipe),
  • 支援透过 npm 安装共享任務 (plugin),
  • 支援嵌套任务并且允许子任务继承组态配置,
  • 支援向前、向后参照任务,
  • 透过组态配置即可处理串流:譬如 合併merge, 序列 (queue), 或者 串接 (concat),
  • 透过组态配置即可控制子任务的执行: 並行 (parallel) 或者 序列 (series),
  • 支援条件式组态配置,
  • 支援命令行指令,查询可用的 recpies 及使用方式,以及
  • 支援命令行指令,查询可用的任务说明及其组态配置。

问与答

问: gulp-chef 违反了 gulp 的『编码优于组态配置 (preferring code over configuration)』哲学吗?

没有, 你还是像平常一样编码, 并且将可变动部份以组态配置的形式萃取出来。

Gulp-chef 透过简化以下的工作来提高使用弹性:

问: 有其它类似的替代方案吗?

: 有,像 gulp-cozy, gulp-load-subtasks, gulp-starter , elixir, 还有[更多其他方案](https://github.com/search?utf8=%E2%9C%93&q=gulp+recipes&type= Repositories&ref=searchresults)。

问: 那么,跟其它方案比起来,gulp-chef 的优势何在?

  • Gulp-chef 不是侵入式的。它不强迫也不限定你使用它的 API 来撰写通用任务 (recipe)。
  • Gulp-chef 强大且易用。它提供了最佳实务作法,如:合并串流、序列串流等。这表示,你可以让任务『只做一件事并做好(do one thing and do it well)』,然后使用组态配置来组合任务。
  • Gulp-chef 本身以及共享任务 (plugin) 都是标准的 node 模组。你可以透过 npm 安装并管理依赖关系,不再需要手动复制工具程式库或任务程式码,不再需要担心忘记更新某个专案的任务,或者担心专案之间的任务版本因各自修改而导致不一致的状况。
  • Gulp-chef 提供极大的弹性,让你依喜好方式决定如何使用它: 『最精简(minimal)』 或『最全面(maximal)』,随你选择。

入门

将 gulp cli 4.0 安装为公用程式 (全域安装)

Gulp-chef 目前仅支援 gulp 4.0。如果你还没开始使用 gulp 4.0,你需要先将全域安装的​​旧 gulp 版本替换为新的 gulp-cli。

npm uninstall -g gulp
npm install -g "gulpjs/gulp-cli#4.0"

不用担心,新的 gulp-cli 同时支援 gulp 4.0 与 gulp 3.x。所以你可以在既有的专案中继续使用 gulp 3.x。

将 gulp 4.0 安装为专案的 devDependencies

npm install --save-dev "gulpjs/gulp#4.0"

更详细的安装及 Gulp 4.0 入门请参阅 <<Gulp 4.0 前瞻>> 这篇文章。

将 gulp-chef 安装为专案的 devDependencies

$ npm install --save-dev gulp-chef

根据你的专案的需要,安装相关的 plugin 为专案的 devDependencies

npm install --save-dev gulp-ccr-browserify gulp-ccr-postcss browserify-shim stringify stylelint postcss-import postcss-cssnext lost cssnano
``

### 在专案根目录建立 gulpfile.js 档案

``` javascript
var gulp = require('gulp');
var chef = require('gulp-chef');

var ingredients = {
    src: 'src/',
    dest: 'dist/',
    clean: {},
    make: {
        postcss: {
            src: 'styles.css',
            processors: {
                stylelint: {},
                import: {},
                cssnext: {
                    features: {
                        autoprefixer: {
                            browser: 'last 2 versions'
                        }
                    }
                },
                lost: {},
                production: {
                    cssnano: {}
                }
            }
        },
        browserify: {
            bundle: {
                entry: 'main.js',
                file: 'scripts.js',
                transform: ['stringify', 'browserify-shim'],
                production: {
                    uglify: true
                }
            }
        },
        assets: {
            src: [
                'index.html',
                'favicon.ico',
                'opensearch.xml'
            ],
            recipe: 'copy'
        }
    },
    build: ['clean', 'make'],
    default: 'build'
};

var meals = chef(ingredients);

gulp.registry(meals);

执行 Gulp

$ gulp

参考范例

示范只将 gulp-chef 作为任务的黏合工具,所有的任务都是没有组态配置、单纯的 JavaScript 函数。

根据 gulp-cheatsheet 的范例,展示 gulp-chef 的能耐。通常不建议以这里采用的方式配置任务及组态。

根据现成完整可运作的范例程式 angularjs-gulp-example,示范如何将普通的 gulpfile.js 改用 gulp-chef 来撰写。同时不要错过了来自范例作者的好文章: "[A complete toolchain for AngularJs - Gulp, Browserify, Sass](http://blog.jhades.org/what-every-angular-project-likely-needs-and- a-gulp-build-to-provide-it/)"。

一个简单的 web app 种子专案。同时也可以当做是一个示范使用本地 recipe 的专案。

用语说明

Gulp Task

一个 gulp task 只是普通的 JavaScript 函数,函数可以回传 Promise, Observable, Stream, 子行程,或者是在完成任务时呼叫 done() 回呼函数。从 Gulp 4.0 开始,函数被呼叫时,其执行环境 (context),也就是 this 值,是 undefined

function gulpTask(done) {
    assert(this === null);
    // do things ...
    done();
}

必须使用 gulp.task() 函数注册之后,函数才会成为 gulp task。

gulp.task(gulpTask);

然后才能在命令行下执行。

$ gulp gulpTask

Configurable Task

一个可组态配置的 gulp 任务在 gulp-chef 中称为 configurable task,其函数参数配置与普通 gulp task 相同。但是被 gulp-chef 呼叫时,gulp-chef 将传递一个 { gulp, config, upstream } 物件做为其执行环境 (context)。

// 注意: configurable task 不能直接撰写,必须透过配置组态的方式来产生。
function configurableTask(done) {
    done();
}

你不能直接撰写 configurable task,而是必须透过定义组态配置,并呼叫 chef() 函数来产生。

var gulp = require('gulp');
var chef = require('gulp-chef');
var meals = chef({
    scripts: {
        src: 'src/**/*.js',
        dest: 'dist/'
    }
});

gulp.registry(meals);

在这个范例中,gulp-chef 为你建立了一个名为 "scripts" 的 configurable task。注意 chef() 函数回传一个 gulp registry 物件,你可以透过回传的 gulp registry 物件,以 meals.get('scripts') 的方式取得该 configurable task。但是通常你会呼叫 gulp.registry() 来注册所有包含在 registry 之中的任务。

gulp.registry(meals);

一旦你呼叫了 gulp.registry() 之后,你就可以在命令行下执行那些已注册的任务。

$ gulp scripts

当 configurable task 被呼叫时,将连同其一起配置的组态,经由其执行环境传入,大致上是以如下的方式呼叫:

scripts.call({
    gulp: gulp,
    config: {
        src: 'src/**/*.js',
        dest: 'dist/'
    }
}, done);

另外注意到在这个例子中,在组态中的 "scripts" 项目,实际上对应到一个 recipe 或 plugin 的模组名称。如果是 recipe 的话,该 recipe 模组档案必须位于专案的 "gulp" 目录下。如果对应的是 plugin 的话,该 plugin 必须先安装到专案中。更多细节请参考『撰写 recipe』及『使用 plugin』的说明。

Configurable Recipe

一个支援组态配置,可供gulp 重复使用的任务,在gulp-chef 中称为configurable recipe [注],其函数参数配置也与普通gulp task 相同,在被gulp-chef呼叫时,gulp-chef 也将传递一个{ gulp, config, upstream } 物件做为其执行环境(context)。这是你真正撰写,并且重复使用的函数。事实上,前面提到的 "configurable task",就是透过名称对应的方式,在组态配置中对应到真正的 configurable recipe,然后加以包装、注册为 gulp 任务。

function scripts(done) {
    // 注意:在 configurable recipe 中,你可以直接由 context 取得 gulp 实体。
    var gulp = this.gulp;
    // 注意:在 configurable recipe 中,你可以直接由 context 取得 config 组态。
    var config = this.config;

    // 用力 ...

    done();
}

注:在 gulp-chef 中,recipe 意指可重复使用的任务。就像一份『食谱』可以用来做出无数的菜肴一样。

撰写组态配置

组态配置只是普通的 JSON 物件。组态中的每个项目、项目的子项目,要嘛是属性 (property),要不然就是子任务。

嵌套任务

任务可以嵌套配置。子任务依照组态的语法结构 (lexically),或称静态语汇结构 (statically),以层叠结构 (cascading) 的形式继承 (inherit) 其父任务的组态。更棒的是,一些预先定义的属性,譬如 "src", "dest" 等路径性质的属性,gulp-chef 会自动帮你连接好路径。

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    build: {
        scripts: {
            src: '**/*.js'
        },
        styles: {
            src: '**/*.css'
        }
    }
});

这个例子建立了__三个__ configurable tasks 任务:build, scripts 以及 styles

并行任务

在上面的例子中,当你执行 build 任务时,它的子任务 scriptsstyles 会以并行 (parallel) 的方式同时执行。并且由于继承的关系,它们将获得如下的组态配置:

scripts: {
    src: 'src/**/*.js',
    dest: 'dist/'
},
styles: {
    src: 'src/**/*.css',
    dest: 'dist/'
}

序列任务

如果你希望任务以序列(series) 的顺序执行,你可以使用"series" __流程控制器(flow controller)__,并且在子任务的组态配置中,加上"order" 属性:

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    build: {
        series: {
            scripts: {
                src: '**/*.js',
                order: 0
            },
            styles: {
                src: '**/*.css',
                order: 1
            }
        }
    }
});

记住,你必须使用 "series" 流程控制器,子任务才会以序列的顺序执行,仅仅只是加上 "order" 属性并不会达到预期的效果。

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    build: {
        scripts: {
            src: '**/*.js',
            order: 0
        },
        styles: {
            src: '**/*.css',
            order: 1
        }
    }
});

在这个例子中,scriptsstyles 会以并行的方式同时执行。

其实有更简单的方式,可以使子任务以序列的顺序执行:使用数组。

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    build: [{
        name: 'scripts',
        src: '**/*.js'
    }, {
        name: 'styles',
        src: '**/*.css'
    }]
};

不过,看起来似乎有点可笑?别急,请继续往下看。

参照任务

你可以使用名称来参照其他任务。向前、向后参照皆可。

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    clean: {},
    scripts: {
        src: '**/*.js'
    },
    styles: {
        src: '**/*.css'
    },
    build: ['clean', 'scripts', 'styles']
};

在这个例子中,build 任务有三个子任务,分别参照到 clean, scripts 以及 styles 任务。参照任务并不会产生并注册新的任务,所以,在这个例子中,你无法直接执行 build 任务的子任务,但是你可以透过执行 build 任务执行它们。

前面提到过,子任务依照组态的语法结构 (lexically),或称静态语汇结构 (statically),以层叠结构 (cascading) 的形式继承 (inherit) 其父任务的组态。既然『被参照的任务』不是定义在『参照任务』之下,『被参照的任务』自然不会继承『参照任务』及其父任务的静态组态配置。不过,有另一种组态是执行时期动态产生的,动态组态会在执行时期注入到『被参照的任务』。更多细节请参考『动态组态』的说明。

在这个例子中,由于使用数组来指定参照 clean, scriptsstyles 的任务,所以是以序列的顺序执行。你可以使用 "parallel" 流程控制器改变这个缺省行为。

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    clean: {},
    scripts: {
        src: '**/*.js'
    },
    styles: {
        src: '**/*.css'
    },
    build: ['clean', { parallel: ['scripts', 'styles'] }]
};

或者,其实你可以将子任务以物件属性的方式,放在一个共同父任务之下,这样它们就会缺省以并行的方式执行。

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    clean: {},
    make: {
        scripts: {
            src: '**/*.js'
        },
        styles: {
            src: '**/*.css'
        }
    },
    build: ['clean', 'make']
});

你可以另外使用 "task" 关键字来引用『被参照的任务』,这样『参照任务』本身就可以同时拥有其他属性。

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    clean: {},
    make: {
        scripts: {
            src: '**/*.js'
        },
        styles: {
            src: '**/*.css'
        }
    },
    build: {
        description: 'Clean and make',
        task: ['clean', 'make']
    },
    watch: {
        description: 'Watch and run related task',
        options: {
            usePolling: true
        },
        task: ['scripts', 'styles']
    }
};

纯函数 / 内联函数

任务也可以以普通函数的方式定义并且直接引用,或以内联匿名函数的形式引用。

function clean() {
    return del(this.config.dest.path);
}

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    scripts: function (done) {
    },
    styles: function (done) {
    },
    build: [clean, { parallel: ['scripts', 'styles'] }]
};

注意在这个例子中,在组态配置中并未定义 clean 项目,所以clean 并不会被注册为 gulp task。

另外一个需要注意的地方是,即使只是纯函数,gulp-chef 呼叫时,总是会以 { gulp, config, upstream } 做为执行环境来呼叫。

你一样可以使用 "task" 关键字来引用函数,这样任务本身就可以同时拥有其他属性。

function clean() {
    return del(this.config.dest.path);
}

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    clean: {
        options: {
            dryRun: true
        },
        task: clean
    },
    make: {
        scripts: {
            src: '**/*.js',
            task: function (done) {
            }
        },
        styles: {
            src: '**/*.css',
            task: function (done) {
            }
        }
    },
    build: ['clean', 'make'],
    watch: {
        options: {
            usePolling: true
        },
        task: ['scripts', 'styles']
    }
};

注意到与上个例子相反地,在这里组态配置中定义了 clean 项目,因此 gulp-chef 会产生并注册 clean 任务,所以可以由命令行执行clean 任务。

隐藏任务

有时候,某些任务永远不需要单独在命令行下执行。隐藏任务可以让任务不要注册,同时不可被其它任务引用。隐藏一个任务不会影响到它的子任务,子任务仍然会继承它的组态配置并且注册为 gulp 任务。隐藏任务仍然是具有功能的,但是只能透过它的父任务执行。

要隐藏一个任务,可以在项目的组态中加入具有 "hidden" 值的 "visibility" 属性。

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    scripts: {
        concat: {
            visibility: 'hidden',
            file: 'bundle.js',
            src: 'lib/',
            coffee: {
                src: '**/*.coffee'
            },
            js: {
                src: '**/*.js'
            }
        }
    }
};

在这个例子中,concat 任务已经被隐藏了,然而它的子任务 coffeejs 依然可见。

为了简化组态配置,你也可以使用在任务名称前面附加上一个"." 字元的方式来隐藏任务,就像UNIX 系统的[dot-files](https://en.wikipedia.org /wiki/Dot-file) 一样。

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    scripts: {
        '.concat': {
            file: 'bundle.js',
            src: 'lib',
            coffee: {
                src: '**/*.coffee'
            },
            js: {
                src: '**/*.js'
            }
        }
    }
};

这将产出与上一个例子完全相同的结果。

停用任务

有时候,当你在调整 gulpfile.js 时,你可能需要暂时移除某些任务,找出发生问题的根源。这时候你可以停用任务。停用任务时,连同其全部的子任务都将被停用,就如同未曾定义过一样。

要停用一个任务,可以在项目的组态中加入具有 "disabled" 值的 "visibility" 属性。

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    scripts: {
        concat: {
            file: 'bundle.js',
            src: 'lib/',
            coffee: {
                visibility: 'disabled',
                src: '**/*.coffee'
            },
            js: {
                src: '**/*.js'
            }
        }
    }
};

在这个例子中,coffee 任务已经被停用了。

为了简化组态配置,你也可以使用在任务名称前面附加上一个 "#" 字元的方式来停用任务,就像 UNIX 系统的 bash 指令档的注解一样。

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    scripts: {
        concat: {
            file: 'bundle.js',
            src: 'lib',
            '#coffee': {
                src: '**/*.coffee'
            },
            js: {
                src: '**/*.js'
            }
        }
    }
};

这将产出与上一个例子完全相同的结果。

处理命名冲突问题

在使用 gulp-chef 时,建议你为所有的任务,分别取用唯一、容易区别的名称。

然而,如果你有非常多的任务,那么将有很高的机率,有一个以上的任务必须使用相同的 recipe 或 plugin。

在缺省情况下,任务名称必须与 recipe 名称相同,这样 gulp-chef 才有办法找到对应的 recipe。那么,当发生名称冲突时,gulp-chef 是怎么处理的呢? gulp-chef 会自动为发生冲突的的任务,在前方附加父任务的名称,像这样:"make:scripts:concat"。

事实上,你也可以将这个附加名称的行为变成缺省行为:在呼叫 chef() 函数时,在 settings 参数传入值为 true 的 "exposeWithPrefix" 属性即可。 "exposeWithPrefix" 属性的缺省值为 "auto"

var ingredients = { ... };
var settings = { exposeWithPrefix: true };
var meals = chef(ingredients, settings);

不是你的菜?没关系,你也可以使用其他办法。

引入新的父任务并隐藏名称冲突的任务

{
    scripts: {
        concatScripts: {
            '.concat': {
                file: 'bundle.js'
            }
        }
    },
    styles: {
        concatStyles: {
            '.concat': {
                file: 'main.css'
            }
        }
    }
}

使用 recipe 关键字

{
    scripts: {
        concatScripts: {
            recipe: 'concat',
            file: 'bundle.js'
        }
    },
    styles: {
        concatStyles: {
            recipe: 'concat',
            file: 'main.css'
        }
    }
}

注意:为了尽量避免发生名称冲突的可能性,并且简化任务树,某些特定种类的任务是缺省隐藏的。主要是『__串流处理器 (stream processor)__』及『__流程控制器 (flow controller)__』。请参考 撰写串流处理器 and 撰写流程控制器 的说明。

使用 Gulp Plugins

有时候,你所撰写的任务所做的,只不过是转呼叫一个 plugin。如果只是这样的话,事实上你完全可以不用费心写一个 recipe,你可以直接在组态配置中使用 "plugin" 关键字做为属性来引用 plugin。

{
    concat: {
        plugin: 'gulp-concat',
        options: 'bundle.js'
    }
}

这个 "plugin" 属性可以接受 stringfunction 类型的值。当指定的值不是 function 而是 string 类型时,gulp-chef 将以此字串做为模组名称,尝试去 "require()" 该模组。使用 "plugin" 属性时,另外还可以指定 "options" 属性,该属性的值将直接做为唯一参数,用来呼叫 plugin 函数。

任何gulp plugin,只要它只接受0 或1 个参数,并且回传一个Stream 或Promise 物件,就可以使用plugin" 关键字来加以引用。前提当然是plugin 已经使用npm install 指令先安装好了。

千万不要将 gulp plugin 与 gulp-chef 专用的 plugin 搞混了。 gulp-chef 专用的 plugin 称为 "Cascading Configurable Recipe for Gulp" 或简称 "gulp-ccr",意思是『可层叠组态配置、可重复使用的 Gulp 任务』。

传递组态值

如同你到目前为止所看到的,在组态配置中的项目,要嘛是任务的属性,要不然就是子任务。你要如何区别两者?基本的规则是,除了"config", "description", "dest", "name", "order", "parallel", "plugin", "recipe", "series", "spit", "src", "task" 以及"visibility" 这些关键字之外,其余的项目都将被视为子任务。

那么,你要如何传递组态值给你的 recipe 函数呢?其实,"config" 关键字就是特地为了这个目的而保留的。

{
    myPlugin: {
        config: {
            file: 'bundle.js'
        }
    }
}

这里"config" 属性连同其"file" 属性,将一起被传递给recipe 函数,而recipe 函数则透过执行环境依序取得"config" 属性及"file" 属性(在『 撰写recipe』中详细说明)。

function myPlugin(done) {
    var file = this.config.file;
    done();
}

module.exports = myPlugin;

只为了传递一个属性,就必须特地写一个"config" 项目来传递它,如果你觉得这样做太超过了,你也可以直接在任意属性名称前面附加一个"$" 字元,这样它们就会被视为是组态属性,而不再会被当作是子任务。

{
    myPlugin: {
        $file: 'bundle.js'
    }
}

这样 "$file" 项目就会被当作是组态属性,而你在组态配置及 recipe 中,可以透过 "file" 名称来存取它。 (注意,名称不是 "$file",这是为了允许使用者可以交换使用 "$" 字元和 "config" 项目来传递组态属性。)

Recipe / Plugin 专属组态属性

Recipe 以及 plugin 可以使用 JSON Schema 来定义它们的组态属性及架构。如果它们确实定义了组态架构,那么你就可以在组态配置项目中,直接列举专属的属性,而不需要透过 "$" 字元和 "config" 关键字。

举例,在"gulp-ccr-browserify" plugin 中,它定义了"bundles" 及"options" 属性,因此你可以在组态项目中直接使用这两个属性。

原本需要这样写:

{
    src: 'src/',
    dest: 'dest/',
    browserify: {
        config: {
            bundles: {
                entry: 'main.ts'
            },
            options: {
                plugins: 'tsify',
                sourcemaps: 'external'
            }
        }
    }
}

现在可以省略写成这样:

{
    src: 'src/',
    dest: 'dest/',
    browserify: {
        bundles: {
            entry: 'main.ts'
        },
        options: {
            plugins: 'tsify',
            sourcemaps: 'external'
        }
    }
}

自动识别属性

为了方便起见,当组态项目中包含有"task", "series", "parallel" 或"plugin" 关键字的时候,这时候除了保留属性之外,其余的属性都将自动认定为组态属性,而不是子任务。

动态组态属性 / 模板引值

有些『串流处理器』 (譬如"gulp-ccr-each-dir"),会以程序化或动态的方式产生新的组态属性。这些新产生的属性,将在执行时期,插入到子任务的的组态中。除了recipe 及plugin 可以透过"config" 属性取得这些值之外,子任务也可以透过使用模板的方式,以"{{var}}" 这样的语法,直接在组态中引用这些值。

{
    src: 'src/',
    dest: 'dist/',
    'each-dir': {
        dir: 'modules/',
        concat: {
            file: '{{dir}}',
            spit: true
        }
    }
}

这个例子里,"each-dir" plugin 会根据"dir" 属性指定的内容,也就是"modules" 目录,找出其下的所有子目录,然后产生新的"dir" 属性,透过这个属性将子目录资讯传递给每个子任务(这里只有"concat" 任务)。子任务可以透过 "config" 属性读取这个值。使用者也可以使用 "{{dir}}" 这样的语法,在组态配置中引用这个值。

条件式组态配置

Gulp-chef 支援条件式组态配置。可以透过设定执行时期环境的模式来启用不同的条件式组态配置。这个功能的实作是基于json-regulator 这个模组,可以参考该模组的说明以便获得更多的相关资讯。

缺省提供了 development, productionstaging 三个模式。你可以在组态配置中,将相关的组态内容,分别写在对应的 developmentdev, productionprod ,或 staging 项目之下。

譬如,如果将组态配置写成这样:

{
    scripts: {
        // common configs
        src: 'src/',

        development: {
            // development configs
            description: 'development mode',
            dest: 'build/',

            options: {
                // development opt​​ions
                debug: true
            },

            // sub tasks for development mode
            lint: {
            }
        },

        production: {
            // production configs
            description: 'production mode',
            dest: 'dist/',

            options: {
                // production options
                debug: false
            }
        },

        options: {
            // common options

            dev: {
                // development opt​​ions
                description: 'development mode',
                sourcemap: false
            },

            prod: {
                // production options
                description: 'production mode',
                sourcemap: 'external'
            }
        },

        // sub tasks
        pipe: [{
            typescript: {
                src: '**/*.ts'
            },

            js: {
                src: '**/*.js'
            }
        }, {
            production: {
                // production configs
                description: 'production mode',

                // sub tasks for production mode
                uglify: {
                }
            }
        }, {
            production: {
                // production configs
                description: 'production mode',

                // sub tasks for production mode
                concat: {
                }
            }
        }]
    }
}

当启用 development 模式时,组态配置将被转换为:

{
    scripts: {
        src: 'src/',
        description: 'development mode',
        dest: 'build/',
        options: {
            description: 'development mode',
            sourcemap: false,
            debug: true
        },
        lint: {
        },
        pipe: [{
            typescript: {
                src: '**/*.ts'
            },
            js: {
                src: '**/*.js'
            }
        }]
    }
}

而启用 production 模式时,组态配置将被转换为:

{
    scripts: {
        src: 'src/',
        description: 'production mode',
        dest: 'dist/',
        options: {
            description: 'production mode',
            sourcemap: 'external',
            debug: false
        },
        pipe: [{
            typescript: {
                src: '**/*.ts'
            },
            js: {
                src: '**/*.js'
            }
        }, {
            description: 'production mode',
            uglify: {
            }
        }, {
            description: 'production mode',
            concat: {
            }
        }]
    }
}

超强的!

以特定的执行时期环境模式启动 Gulp

经由命令行参数
$ gulp --development build

也可以使用简写:

$ gulp --dev build
经由环境变数

在 Linux/Unix 下:

$ NODE_ENV=development gulp build

同样地,若使用简写:

$ NODE_ENV=dev gulp build

自订执行时期环境模式

Gulp-chef 允许你自订执行时期环境模式。如果你崇尚极简主义,你甚至可以分别使用 d, ps 代表 development, productionstaging 模式。只是要记得,组态配置必须与执行时期环境模式配套才行。

var ingredients = {
    scripts: {
        src: 'src/',
        lint: {
        },
        d: {
            debug: true
        },
        p: {
            debug: false,
            sourcemap: 'external',
            uglify: {
            },
            concat: {
            }
        }
    }
};
var settings = {
    modes: {
        production: ['p'],
        development: ['d'],
        staging: ['s'],
        default: 'production'
    }
};
var meals = chef(ingredients, settings);

注意到在 settings.modes 之下的 default 属性。这个属性不会定义新的模式​​,它是用来指定缺省的模式。如果没有指定 settings.modes.default ,那么,缺省模式会成为列在 settings.modes 之下的第一个模式。建议最好不要省略。

除了改变模式的代号,你甚至可以设计自己的模式,并且还能一次提供多个代号。

var settings = {
    modes = {
        build: ['b', 'build'],
        compile: ['c', 'compile'],
        deploy: ['d', 'deploy', 'deployment'],
        review: ['r', 'review']
        default: 'build'
    }
};

但是要注意的是,不要使用到保留给任务使用的关键字

内建的 Recipe

clean

清除 dest 属性指定的目录。

copy

复制由 src 属性指定的档案,到由 dest 属性指定的目录,可以选择是否移除或改变档案的相对路径。

merge

这是一个串流处理器。回传一个新的串流,该串流只有在所有的子任务的串流都停止时才会停止。

更多资讯请参考 merge-stream

queue

这是一个串流处理器。可以汇集子任务所回传的串流,并回传一个新的串流,该串流会将子任务回传的串流,依照子任务的顺序排列在一起。

更多资讯请参考 streamqueue

pipe

这是一个串流处理器。提供与 stream.Readable.pipe() 相同的功能。方便在子任务之间递送 (pipe) 串流。

parallel

这是一个流程控制器。会以并行 (parallel) 的方式执行子任务,子任务之间不会互相等待。

series

这是一个流程控制器。会以序列 (series) 的方式执行子任务,前一个子任务结束之后才会执行下一个子任务。

watch

这是一个流程控制器。负责监看指定的子任务、以及其所有子任务的来源档案,当有任何档案异动时,执行对应的指定任务。

使用 Plugin

在你撰写自己的 recipe 之前,先看一下别人已经做了哪些东西,也许有现成的可以拿来用。你可以使用" gulp recipe",或者,更建议使用"gulp-ccr",在[github.com](https://github.com/search?utf8=%E2%9C%93&q=gulp -ccr) 和npmjs.com 上搜寻。这个 "gulp-ccr" 是 "Cascading Configurable Recipe for Gulp" 的简写,意思是『可层叠组态配置、可重复使用的 Gulp 任务』。

一旦你找到了,譬如,gulp-ccr-browserify ,将它安装为专案的 devDependencies:

$ npm install --save-dev gulp-ccr-browserify

Gulp-chef 会为你移除附加在前面的 "gulp-ccr-" 名称,所以你在使用 plugin 的时候,请移除 "gulp-ccr-" 部份。

{
    browserify: {
        description: 'Using the gulp-ccr-browserify plugin'
    }
}

撰写 Recipe

有三种 recipe: __任务型 (task)__、串流处理器 (stream processor) 以及 __流程控制器 (flow controller)__。

大多数时候,你想要写的是任务型 recipe。任务型 recipe 负责做苦工,而串流处理器及流程控制器则负责操弄其它 recipe。

更多关于串流处理器及流程控制器的说明,或者你乐于分享你的 recipe,你可以写成 plugin,请参考 撰写 Plugin 的说明。

如果你撰写的 recipe 只打算给特定专案使用,你可以将它们放在专案根目录下的特定子目录下:

类型目录
任务型gulp, gulp/tasks
串流处理器gulp/streams
流程控制器gulp/flows

如果你的 recipe 不需要组态配置,你可以像平常写 gulp task 一样的方式撰写 recipe。知道这代表什么意思吗?这代表你以前写的 gulp task 都可以直接拿来当作 recipe 用。你只需要将它们个别存放到专属的模组档案,然后放到专案根目录下的 "gulp" 目录下即可。

使用 recipe 的时候,在组态配置中,使用一个属性名称与 recipe 模组名称一模一样的项目来引用该 recipe。

譬如,假设你有一个 "my-recipe.js" recipe 放在 <your-project>/gulp 目录下。可以这样撰写组态配置来引用它:

var gulp = require('gulp');
var chef = require('gulp-chef');
var meals = chef({
    "my-recipe": {}
});
gulp.registry(meals);

就是这么简单。之后你就可以在命令行下,以 gulp my-recipe 指令执行它。

然而,提供组态配置的能力,才能最大化 recipe 的重复使用价值。

要让 recipe 可以处理组态内容,可以在 recipe 函数中,透过执行环境,也就是 this 变数,取得组态。

function scripts(done) {
    var gulp = this.gulp;
    var config = this.config;

    return gulp.src(config.src.globs)
        .pipe(eslint())
        .pipe(concat(config.file))
        .pipe(uglify())
        .pipe(gulp.dest(config.dest.path));
}

module.exports = scripts;

上面的 "scripts" recipe,在使用的时候可以像这样配置:

var meals = chef({
    src: 'src/',
    dest: 'dist/',
    scripts: {
        src: '**/*.js',
        file: 'bundle.js'
    }
});

Development / Production 模式

Gulp-chef 的 recipe 不需要自行处理条件式组态配置。组态配置在传递给 recipe 之前,已经先根据执行环境模式处理完毕。

撰写 Plugin

Gulp-chef 的 plugin,只是普通的 Node.js 模组,再加上一些必要的资讯。

Plugin 的类型

在前面撰写Recipe 的部份提到过,recipe 有三种:__任务型(task)__、串流处理器(stream processor) 以及__流程控制器( flow controller)__。 Gulp-chef 需要知道 plugin 的类型,才能安插必要的辅助功​​能。由于 plugin 必须使用 npm install 安装,gulp-chef 无法像本地的 recipe 一样,由目录决定 recipe 的类型,因此 plugin 必须自行提供类型资讯。

function myPlugin(done) {
    done();
}

module.exports = myPlugin;
module.exports.type = 'flow';

有效的类型为: "flow"、"stream" 以及 "task"。

组态架构 (Configuration Schema)

为了简化组态配置的处理过程,gulp-chef 鼓励使用 JSON Schema 来验证和转换组态配置。 Gulp-chef 使用json-normalizer 来为JSON Schema 提供扩充功能,并且协助将组态内容一般化(或称正规化),以提供最大的组态配置弹性。你可以为你的 plugin 定义组态架构,以提供属性别名、类型转换、缺省值等功能。同时,组态架构的定义内容还可以显示在命令行中,使用者可以使用指令 gulp --recipe <recipe-name> 查询,不必另外查阅文件,就可以了解如何撰写组态配置。请参考 json-normalizer 的说明,了解如何定义组态架构,甚至加以扩充。

以下是一个简单的 plugin,示范如何定义组态架构:

var gulpif = require('gulp-if');
var concat = require('gulp-concat');
var sourcemaps = require('gulp-sourcemaps');
var uglify = require('gulp-uglify');

function myPlugin() {
    var gulp = this.gulp;
    var config = this.config;
    var options = this.config.options || {};
    var maps = (options.sourcemaps === 'external') ? './' : null;

    return gulp.src(config.src.globs)
        .pipe(gulpif(config.sourcemaps, sourcemaps.init())
        .pipe(concat(config.file))
        .pipe(gulpif(options.uglify, uglify()))
        .pipe(gulpif(options.sourcemaps, sourcemaps.write(maps)))
        .pipe(gulp.dest(config.dest.path));
}

module.exports = myPlugin;
module.exports.type = 'task';
module.exports.schema = {
    title: 'My Plugin',
    description: 'My first plugin',
    type: 'object',
    properties: {
        src: {
            type: 'glob'
        },
        dest: {
            type: 'path'
        },
        file: {
            description: 'Output file name',
            type: 'string'
        },
        options: {
            type: 'object',
            properties: {
                sourcemaps: {
                    description: 'Sourcemap support',
                    alias: ['sourcemap'],
                    enum: [false, 'inline', 'external'],
                    default: false
                },
                uglify: {
                    description: 'Uglify bundle file',
                    type: 'boolean',
                    default: false
                }
            }
        }
    },
    required: ['file']
};

首先,注意到 "file" 被标示为『必须』,plugin 可以利用组态验证工具自动进行检查,因此在程式中就不须要再自行判断。

另外注意到"sourcemaps" 选项允许"sourcemap" 别名,因此使用者可以在组态配置中随意使用"sourcemaps" 或"sourcemap",但是同时在plugin 中,却只需要处理"sourcemaps" 即可。

扩充资料型别

Gulp-chef 提供两个扩充的 JSON Schema 资料型别: "glob" 及 "path"。

glob

一个属性如果是 "glob" 型别,它可以接受一个路径、一个路径匹配表达式 (glob),或者是一个由路径或路径匹配表达式组成的数组。另外还可以额外附带选项资料。

以下都是正确的 "glob" 数值:

// 一个路径字串
'src'
// 一个由路径字串组成的数组
['src', 'lib']
// 一个路径匹配表达式
'**/*.js'
// 一个由路径或路径匹配表达式组成的数组
['**/*.{js,ts}', '!test*']
// 非正规化的『物件表达形式』(注意 "glob" 属性)
{ glob: '**/*.js' }

上面所有的数值,都会被正规化为所谓的『物件表达形式』:

// 一个路径字串
{ globs: ['src'] }
// 一个由路径字串组成的数组
{ globs: ['src', 'lib'] }
// 一个路径匹配表达式
{ globs: ['**/*.js'] }
// 一个由路径或路径匹配表达式组成的数组
{ globs: ['**/*.{js,ts}', '!test*'] }
// 正规化之后的『物件表达形式』(注意 "glob" 属性已经正规化为 "globs")
{ globs: ['**/*.js'] }

注意到 "glob" 是 "globs" 属性的别名,在正规化之后,被更正为 "globs"。同时,"glob" 型别的 "globs" 属性的型态为数组,因此,所有的值都将自动被转换为数组。

当以『物件表达形式』呈现时,还可以使用 "options" 属性额外附带选项资料。

{
    globs: ['**/*.{js,ts}', '!test*'],
    options: {
        base: 'src',
        buffer: true,
        dot: true
    }
}

更多选项资料,请参考 node-glob 的说明。

在任务中,任何具有 "glob" 型别的组态属性,都会继承其父任务的 "src" 属性。这意谓着,当父任务定义了"src" 属性时,gulp-chef 会为子任务的"glob" 型别的组态属性,自动连接好父任务的"src" 属性的路径。

{
    src: 'src',
    browserify: {
        bundles: {
            entries: 'main.js'
        }
    }
}

在这个例子中,"browserify" plugin 具有一个"bundles" 属性,"bundles" 属性下又有一个" entries" 属性,而该属性为"glob" 型别。这个 "entries" 属性将继承外部的 "src" 属性,因而变成: { globs: "src/main.js" }

如果这不是你要的,你可以指定 "join" 选项来覆盖这个行为。

{
    src: 'src',
    browserify: {
        bundles: {
            entry: {
                glob: 'main.js',
                options: {
                    join: false
                }
            }
        }
    }
}

现在 "entries" 属性的值将成为: { globs: "main.js" }

选项​​ "join" 也可以接受字串,用来指定要从哪一个属性继承路径,该属性必须是 "glob" 或 "path" 型别。

在 plugin 中,也可以透过组态架构来定义要继承的缺省属性。请记住,除非有好的理由,请永远记得同时将 "options" 传递给呼叫的 API,以便允许使用者指定选项。像这样:

module.exports = function () {
    var gulp = this.gulp;
    var config = this.config;

    return gulp.src(config.src.globs, config.src.options)
        .pipe(...);
}
path

一个属性如果是 "path" 型别,它可以接受一个路径字串。另外还可以额外附带选项资料。

以下都是正确的 "path" 数值:

// 一个路径字串
'dist'
// 一个路径字串
'src/lib/'
// 『物件表达形式』
{ path: 'maps/' }

上面所有的数值,都会被正规化为所谓的『物件表达形式』:

// 一个路径字串
{ path: 'dist' }
// 一个路径字串
{ path: 'src/lib/' }
// 『物件表达形式』
{ path: 'maps/' }

当以『物件表达形式』呈现时,还可以使用 "options" 属性额外附带选项资料。

{
    path: 'dist/',
    options: {
        cwd: './',
        overwrite: true
    }
}

更多选项资料,请参考 gulp.dest() 的说明。

在任务中,任何具有 "path" 型别的组态属性,都会继承其父任务的 "dest" 属性。这意谓着,当父任务定义了"dest" 属性时,gulp-chef 会为子任务的"path" 型别的组态属性,自动连接好父任务的"dest" 属性的路径。

{
    dest: 'dist/',
    scripts: {
        file: 'bundle.js'
    }
}

假设这里的 "file" 属性是 "path" 型别,它将会继承外部的 "dest" 属性,而成为: "{ path: 'dist/bundle.js' }"。

如果这不是你要的,你可以指定 "join" 选项来覆盖这个行为。

{
    dest: 'dist/',
    scripts: {
        file: {
            path: 'bundle.js',
            options: {
                join: false
            }
        }
    }
}

现在 "file" 属性将成为: "{ path: 'bundle.js' }"。

选项​​ "join" 也可以接受字串,用来指定要从哪一个属性继承路径,该属性必须是 "path" 型别。

在 plugin 中,也可以透过组态架构来定义要继承的缺省属性。请记住,除非有好的理由,请永远记得同时将 "options" 传递给呼叫的 API,以便允许使用者指定选项。像这样:

module.exports = function () {
    var gulp = this.gulp;
    var config = this.config;

    return gulp.src(config.src.globs, config.src.options)
        .pipe(...)
        .pipe(gulp.dest(config.dest.path, config.dest.options));
}

撰写串流处理器

串流处理器负责操作它的子任务输入或输出串流。

串流处理器可以自己输出串流,或者由其中的子任务输出。串流处理器可以在子任务之间递送串流;或合并;或串接子任务的串流。任何你能想像得到的处理方式。唯一必要的要求就是:串流处理器必须回传一个串流。

串流处理器由执行环境中取得 "tasks" 属性,子任务即是经由此属性,以数组的方式传入。

当呼叫子任务时,串流处理器必须为子任务建立适当的执行环境。

module.exports = function () {
    var gulp = this.gulp;
    var config = this.config;
    var tasks = this.tasks;
    var context, stream;

    context = {
        gulp: gulp,
        // 传入获得的组态配置,以便将上层父任务动态插入的组态属性传递给子任务
        config: config
    };
    // 如果需要的话,可以额外插入新的组态属性
    context.config.injectedValue = 'hello!';
    stream = tasks[0].call(context);
    // ...
    return stream;
};

注意父任务可以动态给子任务插入新的组态属性。只有新的值可以成功插入,若子任务原本就配置了同名的属性,则新插入的属性不会覆盖原本的属性。

如果要传递串流给子任务,串流处理器必须透过 "upstream" 属性传递。

module.exports = function () {
    var gulp = this.gulp;
    var config = this.config;
    var tasks = this.tasks;
    var context, stream, i;

    context = {
        gulp: gulp,
        config: config
    };
    stream = gulp.src(config.src.globs, config.src.options);
    for (i = 0; i < tasks.length; ++i) {
        context.upstream = stream;
        stream = tasks[i].call(context);
    }
    return stream;
};

如果串流处理器期望子任务回传一个串流,然而子任务却没有,那么此时串流处理器必须抛出一个错误。

注意:官方关于撰写gulp plugin 的指导方针 中提到: "__不要在串流中抛出错误(do not throw errors inside a stream)__"。没错,你不应该在串流中抛出错误。但是在串流处理器中,如果不是位于处理串流的程式流程中,而是在处理流程之外,那么,抛出错误是没有问题的。

你可以使用 gulp-ccr-stream-helper 来协助呼叫子任务,并且检查其是否正确回传一个串流。

你可以从gulp-ccr-merge 以及[gulp-ccr-queue](https://github.com/gulp-cookery/ gulp-ccr-queue) 专案,参考串流处理器的实作。

撰写流程控制器

流程控制器负责控制子任务的执行时机,顺序等,而且并不关心子任务的输出、入串流。

流程控制器没有什么特别的限制,唯一的规则是,流程控制器必须正确处理子任务的结束事件。譬如,子任务可以呼叫 "done()" 回呼函数;回传一个串流或 Promise,等等。

你可以从gulp-ccr-parallel 、 [gulp-ccr-series](https://github.com/gulp-cookery/ gulp-ccr-series) 以及gulp-ccr-watch 专案,参考流程控制器的实作。

测试 Plugin

建议你可以先写供专案使用的本地 recipe,完成之后,再转换为 plugin。大多数的 recipe 测试都是资料导向的,如果你的 recipe 也是这样,也许你可以考虑使用我的另一个专案: mocha-cases

任务专用属性列表 (关键字)

以下的关键字保留给任务属性使用,你不能使用这些关键字做为你的任务或属性名称。

config

要传递给任务的组态配置。

description

描述任务的工作内容。

dest

要写出档案的路径。定义在子任务中的路径,缺省情形下会继承父任务定义的 dest 路径。属性值可以是字串,或者是如下的物件形式: { path: '', options: {} } 。实际传递给任务的是后者的形式。

name

任务名称。通常会自动由组态项目名称获得。除非任务是定义在数组中,而你仍然希望能够在命令行中执行。

order

任务的执行顺序。只有在你以物件属性的方式定义子任务时,又希望子任务能够依序执行时才需要。数值仅用来排序,因此不需要是连续值。需要配合 "series" 属性才能发挥作用。

parallel

要求子任务以并行的方式同时执行。缺省情形下,以物件属性的方式定义的子任务才会并行执行。使用此关键字时,子任务不论是以数组项目或物件属性的方式定义,都将并行执行。

plugin

要使用的原生 gulp plugin,可以是模组名称或函数。

recipe

任务所要对应的 recipe 模组名称。缺省情形下与任务名称 "name" 属性相同。

series

要求子任务以序列的方式逐一执行。缺省情形下,以数组项目的方式定义的子任务才会序列执行。使用此关键字时,子任务不论是以数组项目或物件属性的方式定义,都将序列执行。

spit

要求任务写出档案。任务允许使用者决定要不要写出档案时才有作用。

src

要读入的档案来源的路径或档案匹配表达式。由于缺省情形下会继承父任务的 "src" 属性,通常你会在父任务中定义路径,在终端任务中才定义档案匹配表达式。属性值可以是任意合格的档案匹配表达式,或由档案匹配表达式组成的数组,或者如下的物件形式: { globs: [], options: {} } 呈现。实际传递给任务的是后者的形式。

task

定义实际执行任务的方式。可以是普通函数的引用、内联函数或对其它任务的参照。子任务如果以数组的形式提供,子任务将以序列的顺序执行,否则子任务将以并行的方式同时执行。

visibility

任务的可见性。有效值为 normalhidden 以及 disabled

设定选项

设定选项可以改变 gulp-chef 的 缺省行为,以及用来定义自订条件式组态配置的执行时期环境模式。

设定选项是经由 chef() 方法的第二个参数传递:

var config = {
};
var settings = {
};
var meals = chef(config, settings);

settings.exposeWithPrefix

开关自动附加任务名称功能。 缺省值为 "auto",当发生名称冲突时,gulp-chef 会自动为发生冲突的的任务,在前方附加父任务的名称,像这样:"make:scripts:concat"。你可以设定为 true 强制开启。设定为 false 强制关闭,此时若遇到名称冲突时,会抛出错误。

settings.lookups

设定本地通用任务模组 (recipe) 的查找目录。 缺省值为:

{
    lookups: {
        flows: 'flows',
        streams: 'streams',
        tasks: 'tasks'
    }
}

settings.lookups.flows

设定本地流程控制器的查找目录。 缺省值为 "flows"

settings.lookups.streams

设定本地串流处理器的查找目录。 缺省值为 "streams"

settings.lookups.tasks

设定本地通用任务的查找目录。 缺省值为 "tasks"

settings.plugins

传递给 "gulp-load-plugins" 的选项。 Gulp-chef 使用 "gulp-load-plugins" 来载入共享任务模组,或称为 "gulp-ccr" 模组。 缺省情形下,不是以 "gulp-ccr" 名称开头的共享任务模组将不会被载入。你可以透过更改 "plugins" 选项来载入这些模组。

缺省选项为:

{
    plugins: {
        camelize: false,
        config: process.cwd() + '/package.json',
        pattern: ['gulp-ccr-*'],
        replaceString: /^gulp[-.]ccr[-.]/g
    }
}

settings.plugins.DEBUG

当设定为 true 时,"gulp-load-plugins" 将输出 log 讯息到 console。

settings.plugins.camelize

若设定为 true,使用 "-" 连接的名称将被改为驼峰形式。

settings.plugins.config

由何处查找共享任务模组的资讯。 缺省为专案的 package.json。

settings.plugins.pattern

共享任务模组的路径匹配表达式 (glob)。 缺省为 "gulp-ccr-*"

settings.plugins.scope

要查找哪些相依范围。 缺省为:

['dependencies', 'devDependencies', 'peerDependencies'].

settings.plugins.replaceString

要移除的模组附加名称。 缺省为: /^gulp[-.]ccr[-.]/g

settings.plugins.lazy

是否延迟载入模组。 缺省为 true

settings.plugins.rename

指定改名。必须为 hash 物件。键为原始名称,值为改名名称。

settings.plugins.renameFn

改名函数。

settings.modes

定义自订条件式组态配置的执行时期环境模式。

除了 default 属性是用来指定缺省模式之外,其余的属性名称定义新的模式​​,而值必须是数组,数组的项目是可用于组态配置及命令行的识别字代号。注意不要使用到保留给任务使用的关键字。 缺省为:

{
    modes: {
        production: ['production', 'prod'],
        development: ['development', 'dev'],
        staging: ['staging'],
        default: 'production'
    }
}

命令行选项列表

--task

查询任务并显示其工作内容说明以及组态配置内容。

$ gulp --task <task-name>

--recipe

列举可用的 recipe,包含内建的 recipe、本地的 recipe 以及已安装的 plugin 。

你可以任意使用 "--recipes" 、 "--recipe" 以及 "--r" 。

$ gulp --recipes

查询指定 recipe,显示其用途说明,以及,如果有定义的话,显示其组态架构

$ gulp --recipe <recipe-name>

专案建制与贡献

$ git clone https://github.com/gulp-cookery/gulp-chef.git
$ cd gulp-chef
$ npm install

问题提报

Issues

测试

测试是以 mocha 撰写,请在命令行下执行下列指令:

$ npm test

授权

MIT

作者

Amobiz

Metadata

RunKit is a free, in-browser JavaScript dev environment for prototyping Node.js code, with every npm package installed. Sign up to share your code.
Sign Up for Free