├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README.zh_CN.md ├── README.zh_TW.md ├── gulpfile.js ├── index.js ├── lib ├── cli │ ├── exit.js │ ├── index.js │ ├── recipes.js │ └── tasks.js ├── configuration │ ├── defaults.js │ ├── glob.js │ ├── index.js │ ├── path.js │ ├── realize.js │ └── sort.js ├── configure.js ├── helpers │ ├── dataflow.js │ ├── globs.js │ ├── observable.js │ ├── safe_require_dir.js │ └── settings.js ├── recipe │ ├── factory.js │ └── registry.js ├── regulator.js ├── schema │ ├── glob.json │ ├── path.json │ └── task.json ├── stuff.js └── task │ ├── expose.js │ ├── factory.js │ ├── metadata.js │ ├── profile.js │ └── registry.js ├── package.json └── test ├── .eslintrc.json ├── fake ├── factory.js ├── gulp.js └── stuff.js └── specs ├── configuration ├── glob_test.js ├── path_test.js ├── realize_test.js └── sort_test.js ├── prerequisite_test.js ├── recipe ├── factory_test.js └── registry_test.js └── task └── factory_test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = tab 12 | indent_size = 4 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | indent_style = space 23 | indent_size = 4 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true 5 | }, 6 | "globals": { 7 | }, 8 | "rules": { 9 | "strict": [2, "global"], 10 | "init-declarations": 0, 11 | "no-catch-shadow": 0, 12 | "no-delete-var": 1, 13 | "no-label-var": 2, 14 | "no-shadow-restricted-names": 2, 15 | "no-shadow": [2, { 16 | "builtinGlobals": false, 17 | "hoist": "all" 18 | }], 19 | "no-undef-init": 1, 20 | "no-undef": 2, 21 | "no-undefined": 2, 22 | "no-unused-vars": [1, { 23 | "vars": "all", 24 | "args": "after-used" 25 | }], 26 | "no-use-before-define": 0, 27 | "comma-dangle": [2, "never"], 28 | "no-cond-assign": [2, "except-parens"], 29 | "no-console": 1, 30 | "no-constant-condition": 2, 31 | "no-control-regex": 2, 32 | "no-debugger": 2, 33 | "no-dupe-args": 2, 34 | "no-dupe-keys": 2, 35 | "no-duplicate-case": 2, 36 | "no-empty-character-class": 2, 37 | "no-empty": 2, 38 | "no-ex-assign": 2, 39 | "no-extra-boolean-cast": 2, 40 | "no-extra-parens": [1, "functions"], 41 | "no-extra-semi": 1, 42 | "no-func-assign": 2, 43 | "no-inner-declarations": [2, "both"], 44 | "no-invalid-regexp": 2, 45 | "no-irregular-whitespace": 2, 46 | "no-negated-in-lhs": 2, 47 | "no-obj-calls": 2, 48 | "no-regex-spaces": 2, 49 | "no-sparse-arrays": 2, 50 | "no-unreachable": 2, 51 | "use-isnan": 2, 52 | "valid-jsdoc": 0, 53 | "valid-typeof": 2, 54 | "no-unexpected-multiline": 2, 55 | "accessor-pairs": [2, { 56 | "getWithoutSet": false, 57 | "setWithoutGet": true 58 | }], 59 | "block-scoped-var": 2, 60 | "complexity": [1, 10], 61 | "consistent-return": 2, 62 | "curly": [2, "all"], 63 | "default-case": 2, 64 | "dot-notation": [2, { 65 | "allowKeywords": true, 66 | "allowPattern": "" 67 | }], 68 | "dot-location": [2, "property"], 69 | "eqeqeq": 2, 70 | "guard-for-in": 2, 71 | "no-alert": 1, 72 | "no-caller": 2, 73 | "no-div-regex": 2, 74 | "no-else-return": 2, 75 | "no-empty-label": 2, 76 | "no-eq-null": 2, 77 | "no-eval": 2, 78 | "no-extend-native": 2, 79 | "no-extra-bind": 2, 80 | "no-fallthrough": 1, 81 | "no-floating-decimal": 1, 82 | "no-implicit-coercion": [1, { 83 | "boolean": false, 84 | "number": true, 85 | "string": false 86 | }], 87 | "no-implied-eval": 2, 88 | "no-invalid-this": 0, 89 | "no-iterator": 2, 90 | "no-labels": 2, 91 | "no-lone-blocks": 2, 92 | "no-loop-func": 2, 93 | "no-multi-spaces": [2, { 94 | "exceptions": { 95 | "VariableDeclarator": true, 96 | "ImportDeclaration": true, 97 | "AssignmentExpression": true, 98 | "ObjectExpression": true 99 | } 100 | }], 101 | "no-multi-str": 2, 102 | "no-native-reassign": 2, 103 | "no-new-func": 2, 104 | "no-new-wrappers": 2, 105 | "no-new": 2, 106 | "no-octal-escape": 2, 107 | "no-octal": 2, 108 | "no-param-reassign": 1, 109 | "no-process-env": 1, 110 | "no-proto": 2, 111 | "no-redeclare": [2, { 112 | "builtinGlobals": true 113 | }], 114 | "no-return-assign": [2, "except-parens"], 115 | "no-script-url": 2, 116 | "no-self-compare": 2, 117 | "no-sequences": 2, 118 | "no-throw-literal": 2, 119 | "no-unused-expressions": [2, { "allowShortCircuit": true, "allowTernary": true }], 120 | "no-useless-call": 1, 121 | "no-useless-concat": 2, 122 | "no-void": 2, 123 | "no-warning-comments": [1, { 124 | "terms": ["todo", "fixme"], 125 | "location": "start" 126 | }], 127 | "no-with": 2, 128 | "radix": 1, 129 | "vars-on-top": 1, 130 | "wrap-iife": [2, "inside"], 131 | "yoda": [1, "never", { "exceptRange": true }], 132 | "array-bracket-spacing": [1, "never"], 133 | "block-spacing": [1, "always"], 134 | "brace-style": [1, "1tbs", { 135 | "allowSingleLine": false 136 | }], 137 | "camelcase": [1, { 138 | "properties": "always" 139 | }], 140 | "comma-spacing": [1, { 141 | "before": false, 142 | "after": true 143 | }], 144 | "comma-style": [1, "last"], 145 | "computed-property-spacing": [1, "never"], 146 | "consistent-this": [2, "self"], 147 | "eol-last": 1, 148 | "func-names": 0, 149 | "func-style": 0, 150 | "id-length": 0, 151 | "id-match": 0, 152 | "indent": [1, "tab"], 153 | "jsx-quotes": [1, "prefer-double"], 154 | "key-spacing": [1, { 155 | "beforeColon": false, 156 | "afterColon": true, 157 | "mode": "minimum" 158 | }], 159 | "lines-around-comment": 0, 160 | "linebreak-style": 0, 161 | "max-nested-callbacks": [1, 3], 162 | "new-cap": [1, { 163 | "newIsCap": true, 164 | "capIsNew": true 165 | }], 166 | "new-parens": 1, 167 | "newline-after-var": [1, "always"], 168 | "no-array-constructor": 1, 169 | "no-continue": 0, 170 | "no-inline-comments": 0, 171 | "no-lonely-if": 1, 172 | "no-mixed-spaces-and-tabs": 1, 173 | "no-multiple-empty-lines": [1, { 174 | "max": 1 175 | }], 176 | "no-nested-ternary": 1, 177 | "no-new-object": 1, 178 | "no-restricted-syntax": 0, 179 | "no-spaced-func": 1, 180 | "no-ternary": 0, 181 | "no-trailing-spaces": [1, { 182 | "skipBlankLines": false 183 | }], 184 | "no-underscore-dangle": 0, 185 | "no-unneeded-ternary": [1, { 186 | "defaultAssignment": true 187 | }], 188 | "object-curly-spacing": [1, "always"], 189 | "one-var": [1, { 190 | "uninitialized": "always", 191 | "initialized": "never" 192 | }], 193 | "operator-assignment": 0, 194 | "operator-linebreak": [1, "after"], 195 | "padded-blocks": [1, "never"], 196 | "quote-props": [1, "as-needed", { 197 | "keywords": false, 198 | "unnecessary": false, 199 | "numbers": true 200 | }], 201 | "quotes": [1, "single", "avoid-escape"], 202 | "require-jsdoc": 0, 203 | "semi-spacing": [1, { 204 | "before": false, 205 | "after": true 206 | }], 207 | "semi": [1, "always"], 208 | "sort-vars": 0, 209 | "space-after-keywords": [1, "always"], 210 | "space-before-keywords": [1, "always"], 211 | "space-before-blocks": [1, "always"], 212 | "space-before-function-paren": [1, { 213 | "anonymous": "always", 214 | "named": "never" 215 | }], 216 | "space-in-parens": [1, "never"], 217 | "space-infix-ops": [1, { 218 | "int32Hint": false 219 | }], 220 | "space-return-throw-case": 1, 221 | "space-unary-ops": [1, { 222 | "words": true, 223 | "nonwords": false 224 | }], 225 | "spaced-comment": [1, "always", { 226 | "exceptions": ["/", "!"] 227 | }], 228 | "wrap-regex": 1, 229 | "arrow-parens": [2, "always"], 230 | "arrow-spacing": [2, { 231 | "before": true, 232 | "after": true 233 | }], 234 | "constructor-super": 2, 235 | "generator-star-spacing": [2, { 236 | "before": false, 237 | "after": true 238 | }], 239 | "no-class-assign": 2, 240 | "no-const-assign": 2, 241 | "no-dupe-class-members": 0, 242 | "no-this-before-super": 2, 243 | "no-var": 0, 244 | "object-shorthand": 0, 245 | "prefer-arrow-callback": 0, 246 | "prefer-const": 1, 247 | "prefer-spread": 0, 248 | "prefer-reflect": 0, 249 | "prefer-template": 0, 250 | "require-yield": 2, 251 | "callback-return": 0, 252 | "global-require": 0, 253 | "handle-callback-err": 0, 254 | "no-mixed-requires": 0, 255 | "no-new-require": 0, 256 | "no-path-concat": 0, 257 | "no-process-exit": 0, 258 | "no-restricted-modules": 0, 259 | "no-sync": 0, 260 | "max-depth": 0, 261 | "max-len": 0, 262 | "max-params": 0, 263 | "max-statements": 0, 264 | "no-bitwise": 1, 265 | "no-plusplus": 0 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | dist 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # IDE settings 31 | .vscode 32 | .idea 33 | .settings 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Build 2 | test 3 | .eslintrc.json 4 | gruntfile.js 5 | gulpfile.js 6 | jsdoc.conf.json 7 | 8 | # Examples 9 | examples 10 | 11 | # IDE Settings 12 | .editorconfig 13 | .idea 14 | .settings 15 | .vscode 16 | 17 | # Logs 18 | logs 19 | *.log 20 | 21 | # NPM 22 | .npmignore 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## gulp-chef changelog 2 | 3 | ### 2016-06-20: 0.1.4 4 | 5 | * Revert Changes: function evaluating at realizing stage. 6 | 7 | ### 2016-03-08: 0.1.3 8 | 9 | * Bug Fix: Update json-regulator to avoid processing non plain objects. 10 | 11 | ### 2016-02-08: 0.1.2 12 | 13 | * Remove function evaluating at realizing stage. 14 | 15 | ### 2016-02-08: 0.1.1 16 | 17 | * Allow custom local recipe folder via settings.lookups. 18 | 19 | ### 2016-01-31: 0.1.0 20 | 21 | * Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Amobiz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.zh_CN.md: -------------------------------------------------------------------------------- 1 | # gulp-chef 2 | 3 | 支援 Gulp 4.0,允许嵌套配置任务及组态。以优雅、直觉的方式,重复使用 gulp 任务。 4 | 5 | 编码的时候你遵守 DRY 原则,那编写 gulpfile.js 的时候,为什么不呢? 6 | 7 | 注意:此专案目前仍处于早期开发阶段,因此可能还存有错误。请协助回报问题并分享您的使用经验,谢谢! 8 | 9 | [![加入在https://gitter.im/gulp-cookery/gulp-chef 上的讨论](https://badges.gitter.im/gulp-cookery/gulp-chef.svg)](https:/ /gitter.im/gulp-cookery/gulp-chef?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 10 | 11 | ## 功能 12 | 13 | * 支援 Gulp 4.0, 14 | * 自动载入本地通用任务 (recipe), 15 | * 支援透过 npm 安装共享任務 (plugin), 16 | * 支援嵌套任务并且允许子任务继承组态配置, 17 | * 支援向前、向后参照任务, 18 | * 透过组态配置即可处理串流:譬如 合併merge, 序列 (queue), 或者 串接 (concat), 19 | * 透过组态配置即可控制子任务的执行: 並行 (parallel) 或者 序列 (series), 20 | * 支援条件式组态配置, 21 | * 支援命令行指令,查询可用的 recpies 及使用方式,以及 22 | * 支援命令行指令,查询可用的任务说明及其组态配置。 23 | 24 | ## 问与答 25 | 26 | ### 问: gulp-chef 违反了 gulp 的『编码优于组态配置 (preferring code over configuration)』哲学吗? 27 | 28 | __答__ 没有, 你还是像平常一样编码, 并且将可变动部份以组态配置的形式萃取出来。 29 | 30 | Gulp-chef 透过简化以下的工作来提高使用弹性: 31 | 32 | * [分割任务到不同的档案](https://github.com/gulpjs/gulp/blob/master/docs/recipes/split-tasks-across-multiple-files.md),以及 33 | * [让任务可分享并立即可用](https://github.com/gulpjs/gulp/tree/master/docs/recipes)。 34 | 35 | ### 问: 有其它类似的替代方案吗? 36 | 37 | __答__: 有,像 [gulp-cozy](https://github.com/lmammino/gulp-cozy), [gulp-load-subtasks](https://github.com/skorlir/gulp-load-subtasks), [gulp-starter](https://github.com/vigetlabs/gulp-starter) , [elixir](https://github.com/laravel/elixir), 还有[更多其他方案](https://github.com/search?utf8=%E2%9C%93&q=gulp+recipes&type= Repositories&ref=searchresults)。 38 | 39 | ### 问: 那么,跟其它方案比起来,gulp-chef 的优势何在? 40 | 41 | __答__: 42 | 43 | * Gulp-chef 不是侵入式的。它不强迫也不限定你使用它的 API 来撰写通用任务 (recipe)。 44 | * Gulp-chef 强大且易用。它提供了最佳实务作法,如:合并串流、序列串流等。这表示,你可以让任务『[只做一件事并做好(do one thing and do it well)](https://en.wikipedia.org/wiki/Unix_philosophy)』,然后使用组态配置来组合任务。 45 | * Gulp-chef 本身以及共享任务 (plugin) 都是标准的 node 模组。你可以透过 npm 安装并管理依赖关系,不再需要手动复制工具程式库或任务程式码,不再需要担心忘记更新某个专案的任务,或者担心专案之间的任务版本因各自修改而导致不一致的状况。 46 | * Gulp-chef 提供极大的弹性,让你依喜好方式决定如何使用它: 『[最精简(minimal)](https://github.com/gulp-cookery/example-minimal-configuration)』 或『[最全面(maximal)](https://github.com/gulp-cookery/example-recipes-demo)』,随你选择。 47 | 48 | ## 入门 49 | 50 | ### 将 gulp cli 4.0 安装为公用程式 (全域安装) 51 | 52 | Gulp-chef 目前仅支援 gulp 4.0。如果你还没开始使用 gulp 4.0,你需要先将全域安装的​​旧 gulp 版本替换为新的 gulp-cli。 53 | 54 | ``` bash 55 | npm uninstall -g gulp 56 | ``` 57 | 58 | ``` bash 59 | npm install -g "gulpjs/gulp-cli#4.0" 60 | ``` 61 | 62 | 不用担心,新的 gulp-cli 同时支援 gulp 4.0 与 gulp 3.x。所以你可以在既有的专案中继续使用 gulp 3.x。 63 | 64 | ### 将 gulp 4.0 安装为专案的 devDependencies 65 | 66 | ``` bash 67 | npm install --save-dev "gulpjs/gulp#4.0" 68 | ``` 69 | 70 | 更详细的安装及 Gulp 4.0 入门请参阅 <<[Gulp 4.0 前瞻](http://segmentfault.com/a/1190000002528547)>> 这篇文章。 71 | 72 | ### 将 gulp-chef 安装为专案的 devDependencies 73 | 74 | ``` bash 75 | $ npm install --save-dev gulp-chef 76 | ``` 77 | 78 | ### 根据你的专案的需要,安装相关的 plugin 为专案的 devDependencies 79 | 80 | ``` bash 81 | npm install --save-dev gulp-ccr-browserify gulp-ccr-postcss browserify-shim stringify stylelint postcss-import postcss-cssnext lost cssnano 82 | `` 83 | 84 | ### 在专案根目录建立 gulpfile.js 档案 85 | 86 | ``` javascript 87 | var gulp = require('gulp'); 88 | var chef = require('gulp-chef'); 89 | 90 | var ingredients = { 91 | src: 'src/', 92 | dest: 'dist/', 93 | clean: {}, 94 | make: { 95 | postcss: { 96 | src: 'styles.css', 97 | processors: { 98 | stylelint: {}, 99 | import: {}, 100 | cssnext: { 101 | features: { 102 | autoprefixer: { 103 | browser: 'last 2 versions' 104 | } 105 | } 106 | }, 107 | lost: {}, 108 | production: { 109 | cssnano: {} 110 | } 111 | } 112 | }, 113 | browserify: { 114 | bundle: { 115 | entry: 'main.js', 116 | file: 'scripts.js', 117 | transform: ['stringify', 'browserify-shim'], 118 | production: { 119 | uglify: true 120 | } 121 | } 122 | }, 123 | assets: { 124 | src: [ 125 | 'index.html', 126 | 'favicon.ico', 127 | 'opensearch.xml' 128 | ], 129 | recipe: 'copy' 130 | } 131 | }, 132 | build: ['clean', 'make'], 133 | default: 'build' 134 | }; 135 | 136 | var meals = chef(ingredients); 137 | 138 | gulp.registry(meals); 139 | ``` 140 | 141 | ### 执行 Gulp 142 | ``` bash 143 | $ gulp 144 | ``` 145 | 146 | ## 参考范例 147 | 148 | * [example-minimal-configuration](https://github.com/gulp-cookery/example-minimal-configuration) 149 | 150 | 示范只将 gulp-chef 作为任务的黏合工具,所有的任务都是没有组态配置、单纯的 JavaScript 函数。 151 | 152 | * [example-recipes-demo](https://github.com/gulp-cookery/example-recipes-demo) 153 | 154 | 根据 [gulp-cheatsheet](https://github.com/osscafe/gulp-cheatsheet) 的范例,展示 gulp-chef 的能耐。通常不建议以这里采用的方式配置任务及组态。 155 | 156 | * [example-todomvc-angularjs-browserify](https://github.com/gulp-cookery/example-todomvc-angularjs-browserify) 157 | 158 | 根据现成完整可运作的范例程式 [angularjs-gulp-example](https://github.com/jhades/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/)"。 159 | 160 | * [example-webapp-seed](https://github.com/gulp-cookery/example-webapp-seed) 161 | 162 | 一个简单的 web app 种子专案。同时也可以当做是一个示范使用本地 recipe 的专案。 163 | 164 | 165 | ## 用语说明 166 | 167 | ### Gulp Task 168 | 169 | 一个 gulp task 只是普通的 JavaScript 函数,函数可以回传 Promise, Observable, Stream, 子行程,或者是在完成任务时呼叫 `done()` 回呼函数。从 Gulp 4.0 开始,函数被呼叫时,其执行环境 (context),也就是 `this` 值,是 `undefined`。 170 | 171 | ``` javascript 172 | function gulpTask(done) { 173 |     assert(this === null); 174 |     // do things ... 175 |     done(); 176 | } 177 | ``` 178 | 179 | 必须使用 `gulp.task()` 函数注册之后,函数才会成为 gulp task。 180 | 181 | ``` javascript 182 | gulp.task(gulpTask); 183 | ``` 184 | 185 | 然后才能在命令行下执行。 186 | 187 | ``` bash 188 | $ gulp gulpTask 189 | ``` 190 | 191 | ### Configurable Task 192 | 193 | 一个可组态配置的 gulp 任务在 gulp-chef 中称为 configurable task,其函数参数配置与普通 gulp task 相同。但是被 gulp-chef 呼叫时,gulp-chef 将传递一个 `{ gulp, config, upstream }` 物件做为其执行环境 (context)。 194 | 195 | ``` javascript 196 | // 注意: configurable task 不能直接撰写,必须透过配置组态的方式来产生。 197 | function configurableTask(done) { 198 |     done(); 199 | } 200 | ``` 201 | 202 | 你不能直接撰写 configurable task,而是必须透过定义组态配置,并呼叫 `chef()` 函数来产生。 203 | 204 | ``` javascript 205 | var gulp = require('gulp'); 206 | var chef = require('gulp-chef'); 207 | var meals = chef({ 208 |     scripts: { 209 |         src: 'src/**/*.js', 210 |         dest: 'dist/' 211 |     } 212 | }); 213 | 214 | gulp.registry(meals); 215 | ``` 216 | 217 | 在这个范例中,gulp-chef 为你建立了一个名为 "`scripts`" 的 configurable task。注意 `chef()` 函数回传一个 gulp registry 物件,你可以透过回传的 gulp registry 物件,以 `meals.get('scripts')` 的方式取得该 configurable task。但是通常你会呼叫 `gulp.registry()` 来注册所有包含在 registry 之中的任务。 218 | 219 | ``` javascript 220 | gulp.registry(meals); 221 | ``` 222 | 223 | 一旦你呼叫了 `gulp.registry()` 之后,你就可以在命令行下执行那些已注册的任务。 224 | 225 | ``` bash 226 | $ gulp scripts 227 | ``` 228 | 229 | 当 configurable task 被呼叫时,将连同其一起配置的组态,经由其执行环境传入,大致上是以如下的方式呼叫: 230 | 231 | ``` javascript 232 | scripts.call({ 233 |     gulp: gulp, 234 |     config: { 235 |         src: 'src/**/*.js', 236 |         dest: 'dist/' 237 |     } 238 | }, done); 239 | ``` 240 | 241 | 另外注意到在这个例子中,在组态中的 "`scripts`" 项目,实际上对应到一个 recipe 或 plugin 的模组名称。如果是 recipe 的话,该 recipe 模组档案必须位于专案的 "`gulp`" 目录下。如果对应的是 plugin 的话,该 plugin 必须先安装到专案中。更多细节请参考『[撰写 recipe](#writing-recipes)』及『[使用 plugin](#using-plugins)』的说明。 242 | 243 | ### Configurable Recipe 244 | 245 | 一个支援组态配置,可供gulp 重复使用的任务,在gulp-chef 中称为configurable recipe [注],其函数参数配置也与普通gulp task 相同,在被gulp-chef呼叫时,gulp-chef 也将传递一个`{ gulp, config, upstream }` 物件做为其执行环境(context)。这是你真正撰写,并且重复使用的函数。事实上,前面提到的 "[configurable task](#configurable-task)",就是透过名称对应的方式,在组态配置中对应到真正的 configurable recipe,然后加以包装、注册为 gulp 任务。 246 | 247 | ``` javascript 248 | function scripts(done) { 249 |     // 注意:在 configurable recipe 中,你可以直接由 context 取得 gulp 实体。 250 |     var gulp = this.gulp; 251 |     // 注意:在 configurable recipe 中,你可以直接由 context 取得 config 组态。 252 |     var config = this.config; 253 | 254 |     // 用力 ... 255 | 256 |     done(); 257 | } 258 | ``` 259 | 260 | 注:在 gulp-chef 中,recipe 意指可重复使用的任务。就像一份『食谱』可以用来做出无数的菜肴一样。 261 | 262 | ## 撰写组态配置 263 | 264 | 组态配置只是普通的 JSON 物件。组态中的每个项目、项目的子项目,要嘛是属性 (property),要不然就是子任务。 265 | 266 | ### 嵌套任务 267 | 268 | 任务可以嵌套配置。子任务依照组态的语法结构 (lexically),或称静态语汇结构 (statically),以层叠结构 (cascading) 的形式继承 (inherit) 其父任务的组态。更棒的是,一些预先定义的属性,譬如 "`src`", "`dest`" 等路径性质的属性,gulp-chef 会自动帮你连接好路径。 269 | 270 | ``` javascript 271 | var meals = chef({ 272 |     src: 'src/', 273 |     dest: 'dist/', 274 |     build: { 275 |         scripts: { 276 |             src: '**/*.js' 277 |         }, 278 |         styles: { 279 |             src: '**/*.css' 280 |         } 281 |     } 282 | }); 283 | ``` 284 | 285 | 这个例子建立了__三个__ configurable tasks 任务:`build`, `scripts` 以及 `styles`。 286 | 287 | ### 并行任务 288 | 289 | 在上面的例子中,当你执行 `build` 任务时,它的子任务 `scripts` 和 `styles` 会以并行 (parallel) 的方式同时执行。并且由于继承的关系,它们将获得如下的组态配置: 290 | 291 | ``` javascript 292 | scripts: { 293 |     src: 'src/**/*.js', 294 |     dest: 'dist/' 295 | }, 296 | styles: { 297 |     src: 'src/**/*.css', 298 |     dest: 'dist/' 299 | } 300 | ``` 301 | 302 | ### 序列任务 303 | 304 | 如果你希望任务以序列(series) 的顺序执行,你可以使用"`series`" __流程控制器(flow controller)__,并且在子任务的组态配置中,加上"`order`" 属性: 305 | 306 | ``` javascript 307 | var meals = chef({ 308 |     src: 'src/', 309 |     dest: 'dist/', 310 |     build: { 311 |         series: { 312 |             scripts: { 313 |                 src: '**/*.js', 314 |                 order: 0 315 |             }, 316 |             styles: { 317 |                 src: '**/*.css', 318 |                 order: 1 319 |             } 320 |         } 321 |     } 322 | }); 323 | ``` 324 | 325 | 记住,你必须使用 "`series`" 流程控制器,子任务才会以序列的顺序执行,仅仅只是加上 "`order`" 属性并不会达到预期的效果。 326 | 327 | ``` javascript 328 | var meals = chef({ 329 |     src: 'src/', 330 |     dest: 'dist/', 331 |     build: { 332 |         scripts: { 333 |             src: '**/*.js', 334 |             order: 0 335 |         }, 336 |         styles: { 337 |             src: '**/*.css', 338 |             order: 1 339 |         } 340 |     } 341 | }); 342 | ``` 343 | 344 | 在这个例子中,`scripts` 和 `styles` 会以并行的方式同时执行。 345 | 346 | 其实有更简单的方式,可以使子任务以序列的顺序执行:使用数组。 347 | 348 | ``` javascript 349 | var meals = chef({ 350 |     src: 'src/', 351 |     dest: 'dist/', 352 |     build: [{ 353 |         name: 'scripts', 354 |         src: '**/*.js' 355 |     }, { 356 |         name: 'styles', 357 |         src: '**/*.css' 358 |     }] 359 | }; 360 | ``` 361 | 362 | 不过,看起来似乎有点可笑?别急,请继续往下看。 363 | 364 | ### 参照任务 365 | 366 | 你可以使用名称来参照其他任务。向前、向后参照皆可。 367 | 368 | ``` javascript 369 | var meals = chef({ 370 |     src: 'src/', 371 |     dest: 'dist/', 372 |     clean: {}, 373 |     scripts: { 374 |         src: '**/*.js' 375 |     }, 376 |     styles: { 377 |         src: '**/*.css' 378 |     }, 379 |     build: ['clean', 'scripts', 'styles'] 380 | }; 381 | ``` 382 | 383 | 在这个例子中,`build` 任务有三个子任务,分别参照到 `clean`, `scripts` 以及 `styles` 任务。参照任务并不会产生并注册新的任务,所以,在这个例子中,你无法直接执行 `build` 任务的子任务,但是你可以透过执行 `build` 任务执行它们。 384 | 385 | 前面提到过,子任务依照组态的语法结构 (lexically),或称静态语汇结构 (statically),以层叠结构 (cascading) 的形式继承 (inherit) 其父任务的组态。既然『被参照的任务』不是定义在『参照任务』之下,『被参照的任务』自然不会继承『参照任务』及其父任务的静态组态配置。不过,有另一种组态是执行时期动态产生的,动态组态会在执行时期注入到『被参照的任务』。更多细节请参考『[动态组态](#dynamic-configuration)』的说明。 386 | 387 | 在这个例子中,由于使用数组来指定参照 `clean`, `scripts` 及 `styles` 的任务,所以是以序列的顺序执行。你可以使用 "`parallel`" 流程控制器改变这个缺省行为。 388 | 389 | ``` javascript 390 | var meals = chef({ 391 |     src: 'src/', 392 |     dest: 'dist/', 393 |     clean: {}, 394 |     scripts: { 395 |         src: '**/*.js' 396 |     }, 397 |     styles: { 398 |         src: '**/*.css' 399 |     }, 400 |     build: ['clean', { parallel: ['scripts', 'styles'] }] 401 | }; 402 | ``` 403 | 404 | 或者,其实你可以将子任务以物件属性的方式,放在一个共同父任务之下,这样它们就会缺省以并行的方式执行。 405 | 406 | ``` javascript 407 | var meals = chef({ 408 |     src: 'src/', 409 |     dest: 'dist/', 410 |     clean: {}, 411 |     make: { 412 |         scripts: { 413 |             src: '**/*.js' 414 |         }, 415 |         styles: { 416 |             src: '**/*.css' 417 |         } 418 |     }, 419 |     build: ['clean', 'make'] 420 | }); 421 | ``` 422 | 423 | 你可以另外使用 "`task`" 关键字来引用『被参照的任务』,这样『参照任务』本身就可以同时拥有其他属性。 424 | 425 | ``` javascript 426 | var meals = chef({ 427 |     src: 'src/', 428 |     dest: 'dist/', 429 |     clean: {}, 430 |     make: { 431 |         scripts: { 432 |             src: '**/*.js' 433 |         }, 434 |         styles: { 435 |             src: '**/*.css' 436 |         } 437 |     }, 438 |     build: { 439 |         description: 'Clean and make', 440 |         task: ['clean', 'make'] 441 |     }, 442 |     watch: { 443 |         description: 'Watch and run related task', 444 |         options: { 445 |             usePolling: true 446 |         }, 447 |         task: ['scripts', 'styles'] 448 |     } 449 | }; 450 | ``` 451 | 452 | ### 纯函数 / 内联函数 453 | 454 | 任务也可以以普通函数的方式定义并且直接引用,或以内联匿名函数的形式引用。 455 | 456 | ``` javascript 457 | function clean() { 458 |     return del(this.config.dest.path); 459 | } 460 | 461 | var meals = chef({ 462 |     src: 'src/', 463 |     dest: 'dist/', 464 |     scripts: function (done) { 465 |     }, 466 |     styles: function (done) { 467 |     }, 468 |     build: [clean, { parallel: ['scripts', 'styles'] }] 469 | }; 470 | ``` 471 | 472 | 注意在这个例子中,在组态配置中并未定义 `clean` 项目,所以`clean` 并不会被注册为 gulp task。 473 | 474 | 另外一个需要注意的地方是,即使只是纯函数,gulp-chef 呼叫时,总是会以 `{ gulp, config, upstream }` 做为执行环境来呼叫。 475 | 476 | 你一样可以使用 "`task`" 关键字来引用函数,这样任务本身就可以同时拥有其他属性。 477 | 478 | ``` javascript 479 | function clean() { 480 |     return del(this.config.dest.path); 481 | } 482 | 483 | var meals = chef({ 484 |     src: 'src/', 485 |     dest: 'dist/', 486 |     clean: { 487 |         options: { 488 |             dryRun: true 489 |         }, 490 |         task: clean 491 |     }, 492 |     make: { 493 |         scripts: { 494 |             src: '**/*.js', 495 |             task: function (done) { 496 |             } 497 |         }, 498 |         styles: { 499 |             src: '**/*.css', 500 |             task: function (done) { 501 |             } 502 |         } 503 |     }, 504 |     build: ['clean', 'make'], 505 |     watch: { 506 |         options: { 507 |             usePolling: true 508 |         }, 509 |         task: ['scripts', 'styles'] 510 |     } 511 | }; 512 | ``` 513 | 514 | 注意到与上个例子相反地,在这里组态配置中定义了 `clean` 项目,因此 gulp-chef 会产生并注册 `clean` 任务,所以可以由命令行执行`clean` 任务。 515 | 516 | ### 隐藏任务 517 | 518 | 有时候,某些任务永远不需要单独在命令行下执行。隐藏任务可以让任务不要注册,同时不可被其它任务引用。隐藏一个任务不会影响到它的子任务,子任务仍然会继承它的组态配置并且注册为 gulp 任务。隐藏任务仍然是具有功能的,但是只能透过它的父任务执行。 519 | 520 | 要隐藏一个任务,可以在项目的组态中加入具有 "`hidden`" 值的 "`visibility`" 属性。 521 | 522 | ``` javascript 523 | var meals = chef({ 524 |     src: 'src/', 525 |     dest: 'dist/', 526 |     scripts: { 527 |         concat: { 528 |             visibility: 'hidden', 529 |             file: 'bundle.js', 530 |             src: 'lib/', 531 |             coffee: { 532 |                 src: '**/*.coffee' 533 |             }, 534 |             js: { 535 |                 src: '**/*.js' 536 |             } 537 |         } 538 |     } 539 | }; 540 | ``` 541 | 542 | 在这个例子中,`concat` 任务已经被隐藏了,然而它的子任务 `coffee` 和 `js` 依然可见。 543 | 544 | 为了简化组态配置,你也可以使用在任务名称前面附加上一个"`.`" 字元的方式来隐藏任务,就像UNIX 系统的[dot-files](https://en.wikipedia.org /wiki/Dot-file) 一样。 545 | 546 | ``` javascript 547 | var meals = chef({ 548 |     src: 'src/', 549 |     dest: 'dist/', 550 |     scripts: { 551 |         '.concat': { 552 |             file: 'bundle.js', 553 |             src: 'lib', 554 |             coffee: { 555 |                 src: '**/*.coffee' 556 |             }, 557 |             js: { 558 |                 src: '**/*.js' 559 |             } 560 |         } 561 |     } 562 | }; 563 | ``` 564 | 565 | 这将产出与上一个例子完全相同的结果。 566 | 567 | ### 停用任务 568 | 569 | 有时候,当你在调整 gulpfile.js 时,你可能需要暂时移除某些任务,找出发生问题的根源。这时候你可以停用任务。停用任务时,连同其全部的子任务都将被停用,就如同未曾定义过一样。 570 | 571 | 要停用一个任务,可以在项目的组态中加入具有 "`disabled`" 值的 "`visibility`" 属性。 572 | 573 | ``` javascript 574 | var meals = chef({ 575 |     src: 'src/', 576 |     dest: 'dist/', 577 |     scripts: { 578 |         concat: { 579 |             file: 'bundle.js', 580 |             src: 'lib/', 581 |             coffee: { 582 |                 visibility: 'disabled', 583 |                 src: '**/*.coffee' 584 |             }, 585 |             js: { 586 |                 src: '**/*.js' 587 |             } 588 |         } 589 |     } 590 | }; 591 | ``` 592 | 593 | 在这个例子中,`coffee` 任务已经被停用了。 594 | 595 | 为了简化组态配置,你也可以使用在任务名称前面附加上一个 "`#`" 字元的方式来停用任务,就像 UNIX 系统的 bash 指令档的注解一样。 596 | 597 | ``` javascript 598 | var meals = chef({ 599 |     src: 'src/', 600 |     dest: 'dist/', 601 |     scripts: { 602 |         concat: { 603 |             file: 'bundle.js', 604 |             src: 'lib', 605 |             '#coffee': { 606 |                 src: '**/*.coffee' 607 |             }, 608 |             js: { 609 |                 src: '**/*.js' 610 |             } 611 |         } 612 |     } 613 | }; 614 | ``` 615 | 616 | 这将产出与上一个例子完全相同的结果。 617 | 618 | ### 处理命名冲突问题 619 | 620 | 在使用 gulp-chef 时,建议你为所有的任务,分别取用唯一、容易区别的名称。 621 | 622 | 然而,如果你有非常多的任务,那么将有很高的机率,有一个以上的任务必须使用相同的 recipe 或 plugin。 623 | 624 | 在缺省情况下,任务名称必须与 recipe 名称相同,这样 gulp-chef 才有办法找到对应的 recipe。那么,当发生名称冲突时,gulp-chef 是怎么处理的呢? gulp-chef 会自动为发生冲突的的任务,在前方附加父任务的名称,像这样:"`make:scripts:concat`"。 625 | 626 | 事实上,你也可以将这个附加名称的行为变成缺省行为:在呼叫 `chef()` 函数时,在 `settings` 参数传入值为 `true` 的 "`exposeWithPrefix`" 属性即可。 "`exposeWithPrefix`" 属性的缺省值为 `"auto"`。 627 | 628 | ``` javascript 629 | var ingredients = { ... }; 630 | var settings = { exposeWithPrefix: true }; 631 | var meals = chef(ingredients, settings); 632 | ``` 633 | 634 | 不是你的菜?没关系,你也可以使用其他办法。 635 | 636 | #### 引入新的父任务并隐藏名称冲突的任务 637 | 638 | ``` javascript 639 | { 640 |     scripts: { 641 |         concatScripts: { 642 |             '.concat': { 643 |                 file: 'bundle.js' 644 |             } 645 |         } 646 |     }, 647 |     styles: { 648 |         concatStyles: { 649 |             '.concat': { 650 |                 file: 'main.css' 651 |             } 652 |         } 653 |     } 654 | } 655 | ``` 656 | 657 | #### 使用 `recipe` 关键字 658 | 659 | ``` javascript 660 | { 661 |     scripts: { 662 |         concatScripts: { 663 |             recipe: 'concat', 664 |             file: 'bundle.js' 665 |         } 666 |     }, 667 |     styles: { 668 |         concatStyles: { 669 |             recipe: 'concat', 670 |             file: 'main.css' 671 |         } 672 |     } 673 | } 674 | ``` 675 | 676 | 注意:为了尽量避免发生名称冲突的可能性,并且简化任务树,某些特定种类的任务是缺省隐藏的。主要是『__串流处理器 (stream processor)__』及『__流程控制器 (flow controller)__』。请参考 [撰写串流处理器](#writing-stream-processor) and [撰写流程控制器](#writing-flow-controller) 的说明。 677 | 678 | ### 使用 Gulp Plugins 679 | 680 | 有时候,你所撰写的任务所做的,只不过是转呼叫一个 plugin。如果只是这样的话,事实上你完全可以不用费心写一个 recipe,你可以直接在组态配置中使用 "`plugin`" 关键字做为属性来引用 plugin。 681 | 682 | ``` javascript 683 | { 684 |     concat: { 685 |         plugin: 'gulp-concat', 686 |         options: 'bundle.js' 687 |     } 688 | } 689 | ``` 690 | 691 | 这个 "`plugin`" 属性可以接受 `string` 和 `function` 类型的值。当指定的值不是 `function` 而是 `string` 类型时,gulp-chef 将以此字串做为模组名称,尝试去 "`require()`" 该模组。使用 "`plugin`" 属性时,另外还可以指定 "`options`" 属性,该属性的值将直接做为唯一参数,用来呼叫 plugin 函数。 692 | 693 | 任何gulp plugin,只要它只接受0 或1 个参数,并且回传一个Stream 或Promise 物件,就可以使用`plugin`" 关键字来加以引用。前提当然是plugin 已经使用`npm install` 指令先安装好了。 694 | 695 | 千万不要将 gulp plugin 与 [gulp-chef 专用的 plugin](#using-plugins) 搞混了。 gulp-chef 专用的 plugin 称为 "Cascading Configurable Recipe for Gulp" 或简称 "gulp-ccr",意思是『可层叠组态配置、可重复使用的 Gulp 任务』。 696 | 697 | ### 传递组态值 698 | 699 | 如同你到目前为止所看到的,在组态配置中的项目,要嘛是任务的属性,要不然就是子任务。你要如何区别两者?基本的规则是,除了"`config`", "`description`", "`dest`", "`name`", "`order`", "`parallel`", "`plugin`", "` recipe`", "`series`", "`spit`", "`src`", "`task`" 以及"`visibility`" 这些[关键字](#keywords)之外,其余的项目都将被视为子任务。 700 | 701 | 那么,你要如何传递组态值给你的 recipe 函数呢?其实,"`config`" 关键字就是特地为了这个目的而保留的。 702 | 703 | ``` javascript 704 | { 705 |     myPlugin: { 706 |         config: { 707 |             file: 'bundle.js' 708 |         } 709 |     } 710 | } 711 | ``` 712 | 713 | 这里"`config`" 属性连同其"`file`" 属性,将一起被传递给recipe 函数,而recipe 函数则透过执行环境依序取得"`config`" 属性及"`file`" 属性(在『 [撰写recipe](#writing-recipes)』中详细说明)。 714 | 715 | ``` javascript 716 | function myPlugin(done) { 717 |     var file = this.config.file; 718 |     done(); 719 | } 720 | 721 | module.exports = myPlugin; 722 | ``` 723 | 724 | 只为了传递一个属性,就必须特地写一个"`config`" 项目来传递它,如果你觉得这样做太超过了,你也可以直接在任意属性名称前面附加一个"`$`" 字元,这样它们就会被视为是组态属性,而不再会被当作是子任务。 725 | 726 | ``` javascript 727 | { 728 |     myPlugin: { 729 |         $file: 'bundle.js' 730 |     } 731 | } 732 | ``` 733 | 734 | 这样 "`$file`" 项目就会被当作是组态属性,而你在组态配置及 recipe 中,可以透过 "`file`" 名称来存取它。 (注意,名称不是 "`$file`",这是为了允许使用者可以交换使用 "`$`" 字元和 "`config`" 项目来传递组态属性。) 735 | 736 | #### Recipe / Plugin 专属组态属性 737 | 738 | Recipe 以及 plugin 可以使用 [JSON Schema](http://json-schema.org/) 来定义它们的组态属性及架构。如果它们确实定义了组态架构,那么你就可以在组态配置项目中,直接列举专属的属性,而不需要透过 "`$`" 字元和 "`config`" 关键字。 739 | 740 | 举例,在"[gulp-ccr-browserify](https://github.com/gulp-cookery/gulp-ccr-browserify)" plugin 中,它定义了"`bundles`" 及"`options`" 属性,因此你可以在组态项目中直接使用这两个属性。 741 | 742 | 原本需要这样写: 743 | 744 | ``` javascript 745 | { 746 |     src: 'src/', 747 |     dest: 'dest/', 748 |     browserify: { 749 |         config: { 750 |             bundles: { 751 |                 entry: 'main.ts' 752 |             }, 753 |             options: { 754 |                 plugins: 'tsify', 755 |                 sourcemaps: 'external' 756 |             } 757 |         } 758 |     } 759 | } 760 | ``` 761 | 762 | 现在可以省略写成这样: 763 | 764 | ``` javascript 765 | { 766 |     src: 'src/', 767 |     dest: 'dest/', 768 |     browserify: { 769 |         bundles: { 770 |             entry: 'main.ts' 771 |         }, 772 |         options: { 773 |             plugins: 'tsify', 774 |             sourcemaps: 'external' 775 |         } 776 |     } 777 | } 778 | ``` 779 | 780 | #### 自动识别属性 781 | 782 | 为了方便起见,当组态项目中包含有"`task`", "`series`", "`parallel`" 或"`plugin`" 关键字的时候,这时候除了保留属性之外,其余的属性都将自动认定为组态属性,而不是子任务。 783 | 784 | ### 动态组态属性 / 模板引值 785 | 786 | 有些『串流处理器』 (譬如"[gulp-ccr-each-dir](https://github.com/gulp-cookery/gulp-ccr-each-dir)"),会以程序化或动态的方式产生新的组态属性。这些新产生的属性,将在执行时期,插入到子任务的的组态中。除了recipe 及plugin 可以透过"`config`" 属性取得这些值之外,子任务也可以透过使用模板的方式,以"`{{var}}`" 这样的语法,直接在组态中引用这些值。 787 | 788 | ``` javascript 789 | { 790 |     src: 'src/', 791 |     dest: 'dist/', 792 |     'each-dir': { 793 |         dir: 'modules/', 794 |         concat: { 795 |             file: '{{dir}}', 796 |             spit: true 797 |         } 798 |     } 799 | } 800 | ``` 801 | 802 | 这个例子里,"[each-dir](https://github.com/gulp-cookery/gulp-ccr-each-dir)" plugin 会根据"`dir`" 属性指定的内容,也就是"`modules `" 目录,找出其下的所有子目录,然后产生新的"`dir`" 属性,透过这个属性将子目录资讯传递给每个子任务(这里只有"concat" 任务)。子任务可以透过 "`config`" 属性读取这个值。使用者也可以使用 "`{{dir}}`" 这样的语法,在组态配置中引用这个值。 803 | 804 | ### 条件式组态配置 805 | 806 | Gulp-chef 支援条件式组态配置。可以透过设定执行时期环境的模式来启用不同的条件式组态配置。这个功能的实作是基于[json-regulator](https://github.com/amobiz/json-regulator?utm_referer="gulp-chef") 这个模组,可以参考该模组的说明以便获得更多的相关资讯。 807 | 808 | 缺省提供了 `development`, `production` 及 `staging` 三个模式。你可以在组态配置中,将相关的组态内容,分别写在对应的 `development` 或 `dev`, `production` 或 `prod` ,或 `staging` 项目之下。 809 | 810 | 譬如,如果将组态配置写成这样: 811 | 812 | ``` javascript 813 | { 814 |     scripts: { 815 |         // common configs 816 |         src: 'src/', 817 | 818 |         development: { 819 |             // development configs 820 |             description: 'development mode', 821 |             dest: 'build/', 822 | 823 |             options: { 824 |                 // development opt​​ions 825 |                 debug: true 826 |             }, 827 | 828 |             // sub tasks for development mode 829 |             lint: { 830 |             } 831 |         }, 832 | 833 |         production: { 834 |             // production configs 835 |             description: 'production mode', 836 |             dest: 'dist/', 837 | 838 |             options: { 839 |                 // production options 840 |                 debug: false 841 |             } 842 |         }, 843 | 844 |         options: { 845 |             // common options 846 | 847 |             dev: { 848 |                 // development opt​​ions 849 |                 description: 'development mode', 850 |                 sourcemap: false 851 |             }, 852 | 853 |             prod: { 854 |                 // production options 855 |                 description: 'production mode', 856 |                 sourcemap: 'external' 857 |             } 858 |         }, 859 | 860 |         // sub tasks 861 |         pipe: [{ 862 |             typescript: { 863 |                 src: '**/*.ts' 864 |             }, 865 | 866 |             js: { 867 |                 src: '**/*.js' 868 |             } 869 |         }, { 870 |             production: { 871 |                 // production configs 872 |                 description: 'production mode', 873 | 874 |                 // sub tasks for production mode 875 |                 uglify: { 876 |                 } 877 |             } 878 |         }, { 879 |             production: { 880 |                 // production configs 881 |                 description: 'production mode', 882 | 883 |                 // sub tasks for production mode 884 |                 concat: { 885 |                 } 886 |             } 887 |         }] 888 |     } 889 | } 890 | ``` 891 | 892 | 当启用 `development` 模式时,组态配置将被转换为: 893 | 894 | ``` javascript 895 | { 896 |     scripts: { 897 |         src: 'src/', 898 |         description: 'development mode', 899 |         dest: 'build/', 900 |         options: { 901 |             description: 'development mode', 902 |             sourcemap: false, 903 |             debug: true 904 |         }, 905 |         lint: { 906 |         }, 907 |         pipe: [{ 908 |             typescript: { 909 |                 src: '**/*.ts' 910 |             }, 911 |             js: { 912 |                 src: '**/*.js' 913 |             } 914 |         }] 915 |     } 916 | } 917 | ``` 918 | 919 | 而启用 `production` 模式时,组态配置将被转换为: 920 | 921 | ``` javascript 922 | { 923 |     scripts: { 924 |         src: 'src/', 925 |         description: 'production mode', 926 |         dest: 'dist/', 927 |         options: { 928 |             description: 'production mode', 929 |             sourcemap: 'external', 930 |             debug: false 931 |         }, 932 |         pipe: [{ 933 |             typescript: { 934 |                 src: '**/*.ts' 935 |             }, 936 |             js: { 937 |                 src: '**/*.js' 938 |             } 939 |         }, { 940 |             description: 'production mode', 941 |             uglify: { 942 |             } 943 |         }, { 944 |             description: 'production mode', 945 |             concat: { 946 |             } 947 |         }] 948 |     } 949 | } 950 | ``` 951 | 952 | 超强的! 953 | 954 | #### 以特定的执行时期环境模式启动 Gulp 955 | 956 | ##### 经由命令行参数 957 | 958 | ``` bash 959 | $ gulp --development build 960 | ``` 961 | 962 | 也可以使用简写: 963 | 964 | ``` bash 965 | $ gulp --dev build 966 | ``` 967 | 968 | ##### 经由环境变数 969 | 970 | 在 Linux/Unix 下: 971 | 972 | ``` bash 973 | $ NODE_ENV=development gulp build 974 | ``` 975 | 976 | 同样地,若使用简写: 977 | 978 | ``` bash 979 | $ NODE_ENV=dev gulp build 980 | ``` 981 | 982 | #### 自订执行时期环境模式 983 | 984 | Gulp-chef 允许你自订执行时期环境模式。如果你崇尚极简主义,你甚至可以分别使用 `d`, `p` 及 `s` 代表 `development`, `production` 及 `staging` 模式。只是要记得,组态配置必须与执行时期环境模式配套才行。 985 | 986 | ``` javascript 987 | var ingredients = { 988 |     scripts: { 989 |         src: 'src/', 990 |         lint: { 991 |         }, 992 |         d: { 993 |             debug: true 994 |         }, 995 |         p: { 996 |             debug: false, 997 |             sourcemap: 'external', 998 |             uglify: { 999 |             }, 1000 |             concat: { 1001 |             } 1002 |         } 1003 |     } 1004 | }; 1005 | var settings = { 1006 |     modes: { 1007 |         production: ['p'], 1008 |         development: ['d'], 1009 |         staging: ['s'], 1010 |         default: 'production' 1011 |     } 1012 | }; 1013 | var meals = chef(ingredients, settings); 1014 | ``` 1015 | 1016 | 注意到在 `settings.modes` 之下的 `default` 属性。这个属性不会定义新的模式​​,它是用来指定缺省的模式。如果没有指定 `settings.modes.default` ,那么,缺省模式会成为列在 `settings.modes` 之下的第一个模式。建议最好不要省略。 1017 | 1018 | 除了改变模式的代号,你甚至可以设计自己的模式,并且还能一次提供多个代号。 1019 | 1020 | ``` javascript 1021 | var settings = { 1022 |     modes = { 1023 |         build: ['b', 'build'], 1024 |         compile: ['c', 'compile'], 1025 |         deploy: ['d', 'deploy', 'deployment'], 1026 |         review: ['r', 'review'] 1027 |         default: 'build' 1028 |     } 1029 | }; 1030 | ``` 1031 | 1032 | 但是要注意的是,不要使用到保留给任务使用的[关键字](#keywords)。 1033 | 1034 | ## 内建的 Recipe 1035 | 1036 | #### [clean](https://github.com/gulp-cookery/gulp-ccr-clean) 1037 | 1038 | 清除 `dest` 属性指定的目录。 1039 | 1040 | #### [copy](https://github.com/gulp-cookery/gulp-ccr-copy) 1041 | 1042 | 复制由 `src` 属性指定的档案,到由 `dest` 属性指定的目录,可以选择是否移除或改变档案的相对路径。 1043 | 1044 | #### [merge](https://github.com/gulp-cookery/gulp-ccr-merge) 1045 | 1046 | 这是一个串流处理器。回传一个新的串流,该串流只有在所有的子任务的串流都停止时才会停止。 1047 | 1048 | 更多资讯请参考 [merge-stream](https://www.npmjs.com/package/merge-stream) 。 1049 | 1050 | #### [queue](https://github.com/gulp-cookery/gulp-ccr-queue) 1051 | 1052 | 这是一个串流处理器。可以汇集子任务所回传的串流,并回传一个新的串流,该串流会将子任务回传的串流,依照子任务的顺序排列在一起。 1053 | 1054 | 更多资讯请参考 [streamqueue](https://www.npmjs.com/package/streamqueue) 。 1055 | 1056 | #### [pipe](https://github.com/gulp-cookery/gulp-ccr-pipe) 1057 | 1058 | 这是一个串流处理器。提供与 [`stream.Readable.pipe()`](https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options) 相同的功能。方便在子任务之间递送 (pipe) 串流。 1059 | 1060 | #### [parallel](https://github.com/gulp-cookery/gulp-ccr-parallel) 1061 | 1062 | 这是一个流程控制器。会以并行 (parallel) 的方式执行子任务,子任务之间不会互相等待。 1063 | 1064 | #### [series](https://github.com/gulp-cookery/gulp-ccr-series) 1065 | 1066 | 这是一个流程控制器。会以序列 (series) 的方式执行子任务,前一个子任务结束之后才会执行下一个子任务。 1067 | 1068 | #### [watch](https://github.com/gulp-cookery/gulp-ccr-watch) 1069 | 1070 | 这是一个流程控制器。负责监看指定的子任务、以及其所有子任务的来源档案,当有任何档案异动时,执行对应的指定任务。 1071 | 1072 | ## 使用 Plugin 1073 | 1074 | 在你撰写自己的 recipe 之前,先看一下别人已经做了哪些东西,也许有现成的可以拿来用。你可以使用" `gulp recipe`",或者,更建议使用"`gulp-ccr`",在[github.com](https://github.com/search?utf8=%E2%9C%93&q=gulp -ccr) 和[npmjs.com](https://www.npmjs.com/search?q=gulp-ccr) 上搜寻。这个 "`gulp-ccr`" 是 "Cascading Configurable Recipe for Gulp" 的简写,意思是『可层叠组态配置、可重复使用的 Gulp 任务』。 1075 | 1076 | 一旦你找到了,譬如,[`gulp-ccr-browserify`](https://github.com/gulp-cookery/gulp-ccr-browserify) ,将它安装为专案的 devDependencies: 1077 | 1078 | ``` bash 1079 | $ npm install --save-dev gulp-ccr-browserify 1080 | ``` 1081 | 1082 | Gulp-chef 会为你移除附加在前面的 "`gulp-ccr-`" 名称,所以你在使用 plugin 的时候,请移除 "`gulp-ccr-`" 部份。 1083 | 1084 | ``` javascript 1085 | { 1086 |     browserify: { 1087 |         description: 'Using the gulp-ccr-browserify plugin' 1088 |     } 1089 | } 1090 | ``` 1091 | 1092 | ## 撰写 Recipe 1093 | 1094 | 有三种 recipe: __任务型 (task)__、__串流处理器 (stream processor)__ 以及 __流程控制器 (flow controller)__。 1095 | 1096 | 大多数时候,你想要写的是任务型 recipe。任务型 recipe 负责做苦工,而串流处理器及流程控制器则负责操弄其它 recipe。 1097 | 1098 | 更多关于串流处理器及流程控制器的说明,或者你乐于分享你的 recipe,你可以写成 plugin,请参考 [撰写 Plugin](#writing-plugins) 的说明。 1099 | 1100 | 如果你撰写的 recipe 只打算给特定专案使用,你可以将它们放在专案根目录下的特定子目录下: 1101 | 1102 | 类型 |目录 1103 | ---------|------------------ 1104 | 任务型 |gulp, gulp/tasks 1105 | 串流处理器 |gulp/streams 1106 | 流程控制器 |gulp/flows 1107 | 1108 | 如果你的 recipe 不需要组态配置,你可以像平常写 gulp task 一样的方式撰写 recipe。知道这代表什么意思吗?这代表你以前写的 gulp task 都可以直接拿来当作 recipe 用。你只需要将它们个别存放到专属的模组档案,然后放到专案根目录下的 "gulp" 目录下即可。 1109 | 1110 | 使用 recipe 的时候,在组态配置中,使用一个属性名称与 recipe 模组名称一模一样的项目来引用该 recipe。 1111 | 1112 | 譬如,假设你有一个 "`my-recipe.js`" recipe 放在 `/gulp` 目录下。可以这样撰写组态配置来引用它: 1113 | 1114 | ``` javascript 1115 | var gulp = require('gulp'); 1116 | var chef = require('gulp-chef'); 1117 | var meals = chef({ 1118 |     "my-recipe": {} 1119 | }); 1120 | gulp.registry(meals); 1121 | ``` 1122 | 1123 | 就是这么简单。之后你就可以在命令行下,以 `gulp my-recipe` 指令执行它。 1124 | 1125 | 然而,提供组态配置的能力,才能最大化 recipe 的重复使用价值。 1126 | 1127 | 要让 recipe 可以处理组态内容,可以在 recipe 函数中,透过执行环境,也就是 `this` 变数,取得组态。 1128 | 1129 | ``` javascript 1130 | function scripts(done) { 1131 |     var gulp = this.gulp; 1132 |     var config = this.config; 1133 | 1134 |     return gulp.src(config.src.globs) 1135 |         .pipe(eslint()) 1136 |         .pipe(concat(config.file)) 1137 |         .pipe(uglify()) 1138 |         .pipe(gulp.dest(config.dest.path)); 1139 | } 1140 | 1141 | module.exports = scripts; 1142 | ``` 1143 | 1144 | 上面的 "`scripts`" recipe,在使用的时候可以像这样配置: 1145 | 1146 | ``` javascript 1147 | var meals = chef({ 1148 |     src: 'src/', 1149 |     dest: 'dist/', 1150 |     scripts: { 1151 |         src: '**/*.js', 1152 |         file: 'bundle.js' 1153 |     } 1154 | }); 1155 | ``` 1156 | 1157 | ### Development / Production 模式 1158 | 1159 | Gulp-chef 的 recipe 不需要自行处理条件式组态配置。组态配置在传递给 recipe 之前,已经先根据执行环境模式处理完毕。 1160 | 1161 | ## 撰写 Plugin 1162 | 1163 | Gulp-chef 的 plugin,只是普通的 Node.js 模组,再加上一些必要的资讯。 1164 | 1165 | ### Plugin 的类型 1166 | 1167 | 在前面[撰写Recipe](#writing-recipes) 的部份提到过,recipe 有三种:__任务型(task)__、__串流处理器(stream processor)__ 以及__流程控制器( flow controller)__。 Gulp-chef 需要知道 plugin 的类型,才能安插必要的辅助功​​能。由于 plugin 必须使用 `npm install` 安装,gulp-chef 无法像本地的 recipe 一样,由目录决定 recipe 的类型,因此 plugin 必须自行提供类型资讯。 1168 | 1169 | ``` javascript 1170 | function myPlugin(done) { 1171 |     done(); 1172 | } 1173 | 1174 | module.exports = myPlugin; 1175 | module.exports.type = 'flow'; 1176 | ``` 1177 | 1178 | 有效的类型为: "`flow`"、"`stream`" 以及 "`task`"。 1179 | 1180 | ### 组态架构 (Configuration Schema) 1181 | 1182 | 为了简化组态配置的处理过程,gulp-chef 鼓励使用 [JSON Schema](http://json-schema.org/) 来验证和转换组态配置。 Gulp-chef 使用[json-normalizer](https://github.com/amobiz/json-normalizer?utm_referer="gulp-chef") 来为JSON Schema 提供扩充功能,并且协助将组态内容一般化(或称正规化),以提供最大的组态配置弹性。你可以为你的 plugin 定义组态架构,以提供属性别名、类型转换、缺省值等功能。同时,组态架构的定义内容还可以显示在命令行中,使用者可以使用指令 `gulp --recipe ` 查询,不必另外查阅文件,就可以了解如何撰写组态配置。请参考 [json-normalizer](https://github.com/amobiz/json-normalizer?utm_referer="gulp-chef") 的说明,了解如何定义组态架构,甚至加以扩充。 1183 | 1184 | 以下是一个简单的 plugin,示范如何定义组态架构: 1185 | 1186 | ``` javascript 1187 | var gulpif = require('gulp-if'); 1188 | var concat = require('gulp-concat'); 1189 | var sourcemaps = require('gulp-sourcemaps'); 1190 | var uglify = require('gulp-uglify'); 1191 | 1192 | function myPlugin() { 1193 |     var gulp = this.gulp; 1194 |     var config = this.config; 1195 |     var options = this.config.options || {}; 1196 |     var maps = (options.sourcemaps === 'external') ? './' : null; 1197 | 1198 |     return gulp.src(config.src.globs) 1199 |         .pipe(gulpif(config.sourcemaps, sourcemaps.init()) 1200 |         .pipe(concat(config.file)) 1201 |         .pipe(gulpif(options.uglify, uglify())) 1202 |         .pipe(gulpif(options.sourcemaps, sourcemaps.write(maps))) 1203 |         .pipe(gulp.dest(config.dest.path)); 1204 | } 1205 | 1206 | module.exports = myPlugin; 1207 | module.exports.type = 'task'; 1208 | module.exports.schema = { 1209 |     title: 'My Plugin', 1210 |     description: 'My first plugin', 1211 |     type: 'object', 1212 |     properties: { 1213 |         src: { 1214 |             type: 'glob' 1215 |         }, 1216 |         dest: { 1217 |             type: 'path' 1218 |         }, 1219 |         file: { 1220 |             description: 'Output file name', 1221 |             type: 'string' 1222 |         }, 1223 |         options: { 1224 |             type: 'object', 1225 |             properties: { 1226 |                 sourcemaps: { 1227 |                     description: 'Sourcemap support', 1228 |                     alias: ['sourcemap'], 1229 |                     enum: [false, 'inline', 'external'], 1230 |                     default: false 1231 |                 }, 1232 |                 uglify: { 1233 |                     description: 'Uglify bundle file', 1234 |                     type: 'boolean', 1235 |                     default: false 1236 |                 } 1237 |             } 1238 |         } 1239 |     }, 1240 |     required: ['file'] 1241 | }; 1242 | ``` 1243 | 1244 | 首先,注意到 "`file`" 被标示为『必须』,plugin 可以利用组态验证工具自动进行检查,因此在程式中就不须要再自行判断。 1245 | 1246 | 另外注意到"`sourcemaps`" 选项允许"`sourcemap`" 别名,因此使用者可以在组态配置中随意使用"`sourcemaps`" 或"`sourcemap`",但是同时在plugin 中,却只需要处理"`sourcemaps`" 即可。 1247 | 1248 | #### 扩充资料型别 1249 | 1250 | Gulp-chef 提供两个扩充的 JSON Schema 资料型别: "`glob`" 及 "`path`"。 1251 | 1252 | ##### glob 1253 | 1254 | 一个属性如果是 "`glob`" 型别,它可以接受一个路径、一个路径匹配表达式 (glob),或者是一个由路径或路径匹配表达式组成的数组。另外还可以额外附带选项资料。 1255 | 1256 | 以下都是正确的 "`glob`" 数值: 1257 | 1258 | ``` javascript 1259 | // 一个路径字串 1260 | 'src' 1261 | // 一个由路径字串组成的数组 1262 | ['src', 'lib'] 1263 | // 一个路径匹配表达式 1264 | '**/*.js' 1265 | // 一个由路径或路径匹配表达式组成的数组 1266 | ['**/*.{js,ts}', '!test*'] 1267 | // 非正规化的『物件表达形式』(注意 "glob" 属性) 1268 | { glob: '**/*.js' } 1269 | ``` 1270 | 1271 | 上面所有的数值,都会被正规化为所谓的『物件表达形式』: 1272 | 1273 | ``` javascript 1274 | // 一个路径字串 1275 | { globs: ['src'] } 1276 | // 一个由路径字串组成的数组 1277 | { globs: ['src', 'lib'] } 1278 | // 一个路径匹配表达式 1279 | { globs: ['**/*.js'] } 1280 | // 一个由路径或路径匹配表达式组成的数组 1281 | { globs: ['**/*.{js,ts}', '!test*'] } 1282 | // 正规化之后的『物件表达形式』(注意 "glob" 属性已经正规化为 "globs") 1283 | { globs: ['**/*.js'] } 1284 | ``` 1285 | 1286 | 注意到 "`glob`" 是 "`globs`" 属性的别名,在正规化之后,被更正为 "`globs`"。同时,"`glob`" 型别的 "`globs`" 属性的型态为数组,因此,所有的值都将自动被转换为数组。 1287 | 1288 | 当以『物件表达形式』呈现时,还可以使用 "`options`" 属性额外附带选项资料。 1289 | 1290 | ``` javascript 1291 | { 1292 |     globs: ['**/*.{js,ts}', '!test*'], 1293 |     options: { 1294 |         base: 'src', 1295 |         buffer: true, 1296 |         dot: true 1297 |     } 1298 | } 1299 | ``` 1300 | 1301 | 更多选项资料,请参考 [node-glob](https://github.com/isaacs/node-glob#options) 的说明。 1302 | 1303 | 在任务中,任何具有 "`glob`" 型别的组态属性,都会继承其父任务的 "`src`" 属性。这意谓着,当父任务定义了"`src`" 属性时,gulp-chef 会为子任务的"`glob`" 型别的组态属性,自动连接好父任务的"`src`" 属性的路径。 1304 | 1305 | ``` javascript 1306 | { 1307 |     src: 'src', 1308 |     browserify: { 1309 |         bundles: { 1310 |             entries: 'main.js' 1311 |         } 1312 |     } 1313 | } 1314 | ``` 1315 | 1316 | 在这个例子中,"[browserify](https://github.com/gulp-cookery/gulp-ccr-browserify)" plugin 具有一个"`bundles`" 属性,"`bundles`" 属性下又有一个" `entries`" 属性,而该属性为"`glob`" 型别。这个 "`entries`" 属性将继承外部的 "`src`" 属性,因而变成: `{ globs: "src/main.js" }` 。 1317 | 1318 | 如果这不是你要的,你可以指定 "`join`" 选项来覆盖这个行为。 1319 | 1320 | ``` javascript 1321 | { 1322 |     src: 'src', 1323 |     browserify: { 1324 |         bundles: { 1325 |             entry: { 1326 |                 glob: 'main.js', 1327 |                 options: { 1328 |                     join: false 1329 |                 } 1330 |             } 1331 |         } 1332 |     } 1333 | } 1334 | ``` 1335 | 1336 | 现在 "`entries`" 属性的值将成为: `{ globs: "main.js" }` 。 1337 | 1338 | 选项​​ "`join`" 也可以接受字串,用来指定要从哪一个属性继承路径,该属性必须是 "`glob`" 或 "`path`" 型别。 1339 | 1340 | 在 plugin 中,也可以透过[组态架构](#configuration-schema)来定义要继承的缺省属性。请记住,除非有好的理由,请永远记得同时将 "`options`" 传递给呼叫的 API,以便允许使用者指定选项。像这样: 1341 | 1342 | ``` javascript 1343 | module.exports = function () { 1344 |     var gulp = this.gulp; 1345 |     var config = this.config; 1346 | 1347 |     return gulp.src(config.src.globs, config.src.options) 1348 |         .pipe(...); 1349 | } 1350 | ``` 1351 | 1352 | ##### path 1353 | 1354 | 一个属性如果是 "`path`" 型别,它可以接受一个路径字串。另外还可以额外附带选项资料。 1355 | 1356 | 以下都是正确的 "`path`" 数值: 1357 | 1358 | ``` javascript 1359 | // 一个路径字串 1360 | 'dist' 1361 | // 一个路径字串 1362 | 'src/lib/' 1363 | // 『物件表达形式』 1364 | { path: 'maps/' } 1365 | ``` 1366 | 1367 | 上面所有的数值,都会被正规化为所谓的『物件表达形式』: 1368 | 1369 | ``` javascript 1370 | // 一个路径字串 1371 | { path: 'dist' } 1372 | // 一个路径字串 1373 | { path: 'src/lib/' } 1374 | // 『物件表达形式』 1375 | { path: 'maps/' } 1376 | ``` 1377 | 1378 | 当以『物件表达形式』呈现时,还可以使用 "`options`" 属性额外附带选项资料。 1379 | 1380 | ``` javascript 1381 | { 1382 |     path: 'dist/', 1383 |     options: { 1384 |         cwd: './', 1385 |         overwrite: true 1386 |     } 1387 | } 1388 | ``` 1389 | 1390 | 更多选项资料,请参考 [gulp.dest()](https://github.com/gulpjs/gulp/blob/4.0/docs/API.md#options-1) 的说明。 1391 | 1392 | 在任务中,任何具有 "`path`" 型别的组态属性,都会继承其父任务的 "`dest`" 属性。这意谓着,当父任务定义了"`dest`" 属性时,gulp-chef 会为子任务的"`path`" 型别的组态属性,自动连接好父任务的"`dest`" 属性的路径。 1393 | 1394 | ``` javascript 1395 | { 1396 |     dest: 'dist/', 1397 |     scripts: { 1398 |         file: 'bundle.js' 1399 |     } 1400 | } 1401 | ``` 1402 | 1403 | 假设这里的 "`file`" 属性是 "`path`" 型别,它将会继承外部的 "`dest`" 属性,而成为: "`{ path: 'dist/bundle.js' }`"。 1404 | 1405 | 如果这不是你要的,你可以指定 "`join`" 选项来覆盖这个行为。 1406 | 1407 | ``` javascript 1408 | { 1409 |     dest: 'dist/', 1410 |     scripts: { 1411 |         file: { 1412 |             path: 'bundle.js', 1413 |             options: { 1414 |                 join: false 1415 |             } 1416 |         } 1417 |     } 1418 | } 1419 | ``` 1420 | 1421 | 现在 "`file`" 属性将成为: "`{ path: 'bundle.js' }`"。 1422 | 1423 | 选项​​ "`join`" 也可以接受字串,用来指定要从哪一个属性继承路径,该属性必须是 "`path`" 型别。 1424 | 1425 | 在 plugin 中,也可以透过[组态架构](#configuration-schema)来定义要继承的缺省属性。请记住,除非有好的理由,请永远记得同时将 "`options`" 传递给呼叫的 API,以便允许使用者指定选项。像这样: 1426 | 1427 | ``` javascript 1428 | module.exports = function () { 1429 |     var gulp = this.gulp; 1430 |     var config = this.config; 1431 | 1432 |     return gulp.src(config.src.globs, config.src.options) 1433 |         .pipe(...) 1434 |         .pipe(gulp.dest(config.dest.path, config.dest.options)); 1435 | } 1436 | ``` 1437 | ### 撰写串流处理器 1438 | 1439 | 串流处理器负责操作它的子任务输入或输出串流。 1440 | 1441 | 串流处理器可以自己输出串流,或者由其中的子任务输出。串流处理器可以在子任务之间递送串流;或合并;或串接子任务的串流。任何你能想像得到的处理方式。唯一必要的要求就是:串流处理器必须回传一个串流。 1442 | 1443 | 串流处理器由执行环境中取得 "`tasks`" 属性,子任务即是经由此属性,以数组的方式传入。 1444 | 1445 | 当呼叫子任务时,串流处理器必须为子任务建立适当的执行环境。 1446 | 1447 | ``` javascript 1448 | module.exports = function () { 1449 |     var gulp = this.gulp; 1450 |     var config = this.config; 1451 |     var tasks = this.tasks; 1452 |     var context, stream; 1453 | 1454 |     context = { 1455 |         gulp: gulp, 1456 |         // 传入获得的组态配置,以便将上层父任务动态插入的组态属性传递给子任务 1457 |         config: config 1458 |     }; 1459 |     // 如果需要的话,可以额外插入新的组态属性 1460 |     context.config.injectedValue = 'hello!'; 1461 |     stream = tasks[0].call(context); 1462 |     // ... 1463 |     return stream; 1464 | }; 1465 | ``` 1466 | 1467 | 注意父任务可以动态给子任务插入新的组态属性。只有新的值可以成功插入,若子任务原本就配置了同名的属性,则新插入的属性不会覆盖原本的属性。 1468 | 1469 | 如果要传递串流给子任务,串流处理器必须透过 "`upstream`" 属性传递。 1470 | 1471 | ``` javascript 1472 | module.exports = function () { 1473 |     var gulp = this.gulp; 1474 |     var config = this.config; 1475 |     var tasks = this.tasks; 1476 |     var context, stream, i; 1477 | 1478 |     context = { 1479 |         gulp: gulp, 1480 |         config: config 1481 |     }; 1482 |     stream = gulp.src(config.src.globs, config.src.options); 1483 |     for (i = 0; i < tasks.length; ++i) { 1484 |         context.upstream = stream; 1485 |         stream = tasks[i].call(context); 1486 |     } 1487 |     return stream; 1488 | }; 1489 | ``` 1490 | 1491 | 如果串流处理器期望子任务回传一个串流,然而子任务却没有,那么此时串流处理器必须抛出一个错误。 1492 | 1493 | 注意:官方关于撰写gulp plugin 的[指导方针](https://github.com/gulpjs/gulp/blob/4.0/docs/writing-a-plugin/guidelines.md) 中提到: "__不要在串流中抛出错误(do not throw errors inside a stream)__"。没错,你不应该在串流中抛出错误。但是在串流处理器中,如果不是位于处理串流的程式流程中,而是在处理流程之外,那么,抛出错误是没有问题的。 1494 | 1495 | 你可以使用 [gulp-ccr-stream-helper](https://github.com/gulp-cookery/gulp-ccr-stream-helper) 来协助呼叫子任务,并且检查其是否正确回传一个串流。 1496 | 1497 | 你可以从[gulp-ccr-merge](https://github.com/gulp-cookery/gulp-ccr-merge) 以及[gulp-ccr-queue](https://github.com/gulp-cookery/ gulp-ccr-queue) 专案,参考串流处理器的实作。 1498 | 1499 | ### 撰写流程控制器 1500 | 1501 | 流程控制器负责控制子任务的执行时机,顺序等,而且并不关心子任务的输出、入串流。 1502 | 1503 | 流程控制器没有什么特别的限制,唯一的规则是,流程控制器必须正确处理子任务的结束事件。譬如,子任务可以呼叫 "`done()`" 回呼函数;回传一个串流或 Promise,等等。 1504 | 1505 | 你可以从[gulp-ccr-parallel](https://github.com/gulp-cookery/gulp-ccr-parallel) 、 [gulp-ccr-series](https://github.com/gulp-cookery/ gulp-ccr-series) 以及[gulp-ccr-watch](https://github.com/gulp-cookery/gulp-ccr-watch) 专案,参考流程控制器的实作。 1506 | 1507 | ### 测试 Plugin 1508 | 1509 | 建议你可以先写供专案使用的本地 recipe,完成之后,再转换为 plugin。大多数的 recipe 测试都是资料导向的,如果你的 recipe 也是这样,也许你可以考虑使用我的另一个专案: [mocha-cases](https://github.com/amobiz/mocha-cases) 。 1510 | 1511 | ## 任务专用属性列表 (关键字) 1512 | 1513 | 以下的关键字保留给任务属性使用,你不能使用这些关键字做为你的任务或属性名称。 1514 | 1515 | #### config 1516 | 1517 | 要传递给任务的组态配置。 1518 | 1519 | #### description 1520 | 1521 | 描述任务的工作内容。 1522 | 1523 | #### dest 1524 | 1525 | 要写出档案的路径。定义在子任务中的路径,缺省情形下会继承父任务定义的 dest 路径。属性值可以是字串,或者是如下的物件形式: `{ path: '', options: {} }` 。实际传递给任务的是后者的形式。 1526 | 1527 | #### name 1528 | 1529 | 任务名称。通常会自动由组态项目名称获得。除非任务是定义在数组中,而你仍然希望能够在命令行中执行。 1530 | 1531 | #### order 1532 | 1533 | 任务的执行顺序。只有在你以物件属性的方式定义子任务时,又希望子任务能够依序执行时才需要。数值仅用来排序,因此不需要是连续值。需要配合 "`series`" 属性才能发挥作用。 1534 | 1535 | #### parallel 1536 | 1537 | 要求子任务以并行的方式同时执行。缺省情形下,以物件属性的方式定义的子任务才会并行执行。使用此关键字时,子任务不论是以数组项目或物件属性的方式定义,都将并行执行。 1538 | 1539 | #### plugin 1540 | 1541 | 要使用的原生 gulp plugin,可以是模组名称或函数。 1542 | 1543 | #### recipe 1544 | 1545 | 任务所要对应的 recipe 模组名称。缺省情形下与任务名称 "`name`" 属性相同。 1546 | 1547 | #### series 1548 | 1549 | 要求子任务以序列的方式逐一执行。缺省情形下,以数组项目的方式定义的子任务才会序列执行。使用此关键字时,子任务不论是以数组项目或物件属性的方式定义,都将序列执行。 1550 | 1551 | #### spit 1552 | 1553 | 要求任务写出档案。任务允许使用者决定要不要写出档案时才有作用。 1554 | 1555 | #### src 1556 | 1557 | 要读入的档案来源的路径或档案匹配表达式。由于缺省情形下会继承父任务的 "`src`" 属性,通常你会在父任务中定义路径,在终端任务中才定义档案匹配表达式。属性值可以是任意合格的档案匹配表达式,或由档案匹配表达式组成的数组,或者如下的物件形式: `{ globs: [], options: {} }` 呈现。实际传递给任务的是后者的形式。 1558 | 1559 | #### task 1560 | 1561 | 定义实际执行任务的方式。可以是普通函数的引用、内联函数或对其它任务的参照。子任务如果以数组的形式提供,子任务将以序列的顺序执行,否则子任务将以并行的方式同时执行。 1562 | 1563 | #### visibility 1564 | 1565 | 任务的可见性。有效值为 `normal` 、 `hidden` 以及 `disabled` 。 1566 | 1567 | 1568 | ## 设定选项 1569 | 1570 | 设定选项可以改变 gulp-chef 的 缺省行为,以及用来定义自订条件式组态配置的执行时期环境模式。 1571 | 1572 | 设定选项是经由 `chef()` 方法的第二个参数传递: 1573 | 1574 | ``` javascript 1575 | var config = { 1576 | }; 1577 | var settings = { 1578 | }; 1579 | var meals = chef(config, settings); 1580 | ``` 1581 | 1582 | ### settings.exposeWithPrefix 1583 | 1584 | 开关自动附加任务名称功能。 缺省值为 `"auto"`,当发生名称冲突时,gulp-chef 会自动为发生冲突的的任务,在前方附加父任务的名称,像这样:"`make:scripts:concat`"。你可以设定为 `true` 强制开启。设定为 `false` 强制关闭,此时若遇到名称冲突时,会抛出错误。 1585 | 1586 | ### settings.lookups 1587 | 1588 | 设定本地通用任务模组 (recipe) 的查找目录。 缺省值为: 1589 | 1590 | ``` javascript 1591 | { 1592 | lookups: { 1593 | flows: 'flows', 1594 | streams: 'streams', 1595 | tasks: 'tasks' 1596 | } 1597 | } 1598 | ``` 1599 | 1600 | #### settings.lookups.flows 1601 | 1602 | 设定本地流程控制器的查找目录。 缺省值为 `"flows"` 。 1603 | 1604 | #### settings.lookups.streams 1605 | 1606 | 设定本地串流处理器的查找目录。 缺省值为 `"streams"` 。 1607 | 1608 | #### settings.lookups.tasks 1609 | 1610 | 设定本地通用任务的查找目录。 缺省值为 `"tasks"` 。 1611 | 1612 | ### settings.plugins 1613 | 1614 | 传递给 "[gulp-load-plugins](https://github.com/jackfranklin/gulp-load-plugins)" 的选项。 1615 | Gulp-chef 使用 "gulp-load-plugins" 来载入共享任务模组,或称为 "gulp-ccr" 模组。 1616 | 缺省情形下,不是以 `"gulp-ccr"` 名称开头的共享任务模组将不会被载入。你可以透过更改 "`plugins`" 选项来载入这些模组。 1617 | 1618 | 缺省选项为: 1619 | 1620 | ``` javascript 1621 | { 1622 | plugins: { 1623 | camelize: false, 1624 | config: process.cwd() + '/package.json', 1625 | pattern: ['gulp-ccr-*'], 1626 | replaceString: /^gulp[-.]ccr[-.]/g 1627 | } 1628 | } 1629 | ``` 1630 | 1631 | #### settings.plugins.DEBUG 1632 | 1633 | 当设定为 `true` 时,"gulp-load-plugins" 将输出 log 讯息到 console。 1634 | 1635 | #### settings.plugins.camelize 1636 | 1637 | 若设定为 `true`,使用 `"-"` 连接的名称将被改为驼峰形式。 1638 | 1639 | #### settings.plugins.config 1640 | 1641 | 由何处查找共享任务模组的资讯。 缺省为专案的 package.json。 1642 | 1643 | #### settings.plugins.pattern 1644 | 1645 | 共享任务模组的路径匹配表达式 (glob)。 缺省为 `"gulp-ccr-*"`。 1646 | 1647 | #### settings.plugins.scope 1648 | 1649 | 要查找哪些相依范围。 缺省为: 1650 | 1651 | ``` javascript 1652 | ['dependencies', 'devDependencies', 'peerDependencies']. 1653 | ``` 1654 | 1655 | #### settings.plugins.replaceString 1656 | 1657 | 要移除的模组附加名称。 缺省为: `/^gulp[-.]ccr[-.]/g` 。 1658 | 1659 | #### settings.plugins.lazy 1660 | 1661 | 是否延迟载入模组。 缺省为 `true`。 1662 | 1663 | #### settings.plugins.rename 1664 | 1665 | 指定改名。必须为 hash 物件。键为原始名称,值为改名名称。 1666 | 1667 | #### settings.plugins.renameFn 1668 | 1669 | 改名函数。 1670 | 1671 | ### settings.modes 1672 | 1673 | 定义自订条件式组态配置的执行时期环境模式。 1674 | 1675 | 除了 `default` 属性是用来指定缺省模式之外,其余的属性名称定义新的模式​​,而值必须是数组,数组的项目是可用于组态配置及命令行的识别字代号。注意不要使用到保留给任务使用的[关键字](#keywords)。 缺省为: 1676 | 1677 | ``` javascript 1678 | { 1679 | modes: { 1680 | production: ['production', 'prod'], 1681 | development: ['development', 'dev'], 1682 | staging: ['staging'], 1683 | default: 'production' 1684 | } 1685 | } 1686 | ``` 1687 | 1688 | 1689 | ## 命令行选项列表 1690 | 1691 | ### --task 1692 | 1693 | 查询任务并显示其工作内容说明以及组态配置内容。 1694 | 1695 | ``` bash 1696 | $ gulp --task 1697 | ``` 1698 | 1699 | ### --recipe 1700 | 1701 | 列举可用的 recipe,包含内建的 recipe、本地的 recipe 以及已安装的 plugin 。 1702 | 1703 | 你可以任意使用 "`--recipes`" 、 "`--recipe`" 以及 "`--r`" 。 1704 | 1705 | ``` bash 1706 | $ gulp --recipes 1707 | ``` 1708 | 1709 | 查询指定 recipe,显示其用途说明,以及,如果有定义的话,显示其[组态架构](#configuration-schema)。 1710 | 1711 | ``` bash 1712 | $ gulp --recipe 1713 | ``` 1714 | 1715 | ## 专案建制与贡献 1716 | 1717 | ``` bash 1718 | $ git clone https://github.com/gulp-cookery/gulp-chef.git 1719 | $ cd gulp-chef 1720 | $ npm install 1721 | ``` 1722 | 1723 | ## 问题提报 1724 | 1725 | [Issues](https://github.com/gulp-cookery/gulp-chef/issues) 1726 | 1727 | ## 测试 1728 | 1729 | 测试是以 [mocha](https://mochajs.org/) 撰写,请在命令行下执行下列指令: 1730 | 1731 | ``` bash 1732 | $ npm test 1733 | ``` 1734 | 1735 | ## 授权 1736 | 1737 | [MIT](http://opensource.org/licenses/MIT) 1738 | 1739 | ## 作者 1740 | 1741 | [Amobiz](https://github.com/amobiz) 1742 | -------------------------------------------------------------------------------- /README.zh_TW.md: -------------------------------------------------------------------------------- 1 | # gulp-chef 2 | 3 | 支援 Gulp 4.0,允許巢狀配置任務及組態。以優雅、直覺的方式,重複使用 gulp 任務。 4 | 5 | 寫程式的時候你謹守 DRY 原則,那編寫 gulpfile.js 的時候,為什麼不呢? 6 | 7 | 注意:此專案目前仍處於早期開發階段,因此可能還存有錯誤。請協助回報問題並分享您的使用經驗,謝謝! 8 | 9 | [![加入在 https://gitter.im/gulp-cookery/gulp-chef 上的討論](https://badges.gitter.im/gulp-cookery/gulp-chef.svg)](https://gitter.im/gulp-cookery/gulp-chef?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 10 | 11 | ## 功能 12 | 13 | * 支援 Gulp 4.0, 14 | * 自動載入本地通用任務 (recipe), 15 | * 支援透過 npm 安裝 plugin, 16 | * 支援巢狀任務並且允許子任務繼承組態配置, 17 | * 支援向前、向後參照任務, 18 | * 透過組態配置即可處理串流:譬如 merge, queue, 或者 concat, 19 | * 透過組態配置即可控制子任務的執行: parallel 或者 series, 20 | * 支援條件式組態配置, 21 | * 支援命令列指令,查詢可用的 recpies 及使用方式,以及 22 | * 支援命令列指令,查詢可用的任務說明及其組態配置。 23 | 24 | ## 問與答 25 | 26 | ### 問: gulp-chef 違反了 gulp 的『編碼優於組態配置 (preferring code over configuration)』哲學嗎? 27 | 28 | __答__: 沒有, 你還是像平常一樣寫程式, 並且將可變動部份以組態配置的形式萃取出來。 29 | 30 | Gulp-chef 透過簡化以下的工作來提昇使用彈性: 31 | 32 | * [分割任務到不同的檔案](https://github.com/gulpjs/gulp/blob/master/docs/recipes/split-tasks-across-multiple-files.md),以及 33 | * [讓任務可分享並立即可用](https://github.com/gulpjs/gulp/tree/master/docs/recipes)。 34 | 35 | ### 問: 有其它類似的替代方案嗎? 36 | 37 | __答__: 有,像 [gulp-cozy](https://github.com/lmammino/gulp-cozy), [gulp-load-subtasks](https://github.com/skorlir/gulp-load-subtasks), [gulp-starter](https://github.com/vigetlabs/gulp-starter), [elixir](https://github.com/laravel/elixir), 還有[更多其他方案](https://github.com/search?utf8=%E2%9C%93&q=gulp+recipes&type=Repositories&ref=searchresults)。 38 | 39 | ### 問: 那麼,跟其它方案比起來,gulp-chef 的優勢何在? 40 | 41 | __答__: 42 | 43 | * Gulp-chef 不是侵入式的。它不強迫也不限定你使用它的 API 來撰寫通用任務 (recipe)。 44 | * Gulp-chef 強大且易用。它提供了最佳實務作法,如:合併串流、序列串流等。這表示,你可以讓任務『[只做一件事並做好 (do one thing and do it well)](https://en.wikipedia.org/wiki/Unix_philosophy)』,然後使用組態配置來組合任務。 45 | * Gulp-chef 本身以及共享任務 (plugin) 都是標準的 node 模組。你可以透過 npm 安裝並管理依賴關係,不再需要手動複製工具程式庫或任務程式碼,不再需要擔心忘記更新某個專案的任務,或者擔心專案之間的任務版本因各自修改而導致不一致的狀況。 46 | * Gulp-chef 提供極大的彈性,讓你依喜好方式決定如何使用它: 『[最精簡 (minimal)](https://github.com/gulp-cookery/example-minimal-configuration)』 或 『[最全面 (maximal)](https://github.com/gulp-cookery/example-recipes-demo)』,隨你選擇。 47 | 48 | ## 入門 49 | 50 | ### 將 gulp cli 4.0 安裝為公用程式 (全域安裝) 51 | 52 | Gulp-chef 目前僅支援 gulp 4.0。如果你還沒開始使用 gulp 4.0,你需要先將全域安裝的舊 gulp 版本替換為新的 gulp-cli。 53 | 54 | ``` bash 55 | npm uninstall -g gulp 56 | ``` 57 | 58 | ``` bash 59 | npm install -g "gulpjs/gulp-cli#4.0" 60 | ``` 61 | 62 | 不用擔心,新的 gulp-cli 同時支援 gulp 4.0 與 gulp 3.x。所以你可以在既有的專案中繼續使用 gulp 3.x。 63 | 64 | ### 將 gulp 4.0 安裝為專案的 devDependencies 65 | 66 | ``` bash 67 | npm install --save-dev "gulpjs/gulp#4.0" 68 | ``` 69 | 70 | 更詳細的安裝及 Gulp 4.0 入門請參閱 <<[Gulp 4.0 前瞻](http://segmentfault.com/a/1190000002528547)>> 這篇文章。 71 | 72 | ### 將 gulp-chef 安裝為專案的 devDependencies 73 | 74 | ``` bash 75 | $ npm install --save-dev gulp-chef 76 | ``` 77 | 78 | ### 根據你的專案的需要,安裝相關的 plugin 為專案的 devDependencies 79 | 80 | ``` bash 81 | npm install --save-dev gulp-ccr-browserify gulp-ccr-postcss browserify-shim stringify stylelint postcss-import postcss-cssnext lost cssnano 82 | ``` 83 | 84 | ### 在專案根目錄建立 gulpfile.js 檔案 85 | 86 | ``` javascript 87 | var gulp = require('gulp'); 88 | var chef = require('gulp-chef'); 89 | 90 | var ingredients = { 91 | src: 'src/', 92 | dest: 'dist/', 93 | clean: {}, 94 | make: { 95 | postcss: { 96 | src: 'styles.css', 97 | processors: { 98 | stylelint: {}, 99 | import: {}, 100 | cssnext: { 101 | features: { 102 | autoprefixer: { 103 | browser: 'last 2 versions' 104 | } 105 | } 106 | }, 107 | lost: {}, 108 | production: { 109 | cssnano: {} 110 | } 111 | } 112 | }, 113 | browserify: { 114 | bundle: { 115 | entry: 'main.js', 116 | file: 'scripts.js', 117 | transform: ['stringify', 'browserify-shim'], 118 | production: { 119 | uglify: true 120 | } 121 | } 122 | }, 123 | assets: { 124 | src: [ 125 | 'index.html', 126 | 'favicon.ico', 127 | 'opensearch.xml' 128 | ], 129 | recipe: 'copy' 130 | } 131 | }, 132 | build: ['clean', 'make'], 133 | default: 'build' 134 | }; 135 | 136 | var meals = chef(ingredients); 137 | 138 | gulp.registry(meals); 139 | ``` 140 | 141 | ### 執行 Gulp 142 | ``` bash 143 | $ gulp 144 | ``` 145 | 146 | ## 參考範例 147 | 148 | * [example-minimal-configuration](https://github.com/gulp-cookery/example-minimal-configuration) 149 | 150 | 示範只將 gulp-chef 作為任務的黏合工具,所有的任務都是沒有組態配置、單純的 JavaScript 函數。 151 | 152 | * [example-recipes-demo](https://github.com/gulp-cookery/example-recipes-demo) 153 | 154 | 根據 [gulp-cheatsheet](https://github.com/osscafe/gulp-cheatsheet) 的範例,展示 gulp-chef 的能耐。通常不建議以這裡採用的方式配置任務及組態。 155 | 156 | * [example-todomvc-angularjs-browserify](https://github.com/gulp-cookery/example-todomvc-angularjs-browserify) 157 | 158 | 根據現成完整可運作的範例程式 [angularjs-gulp-example](https://github.com/jhades/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/)"。 159 | 160 | * [example-webapp-seed](https://github.com/gulp-cookery/example-webapp-seed) 161 | 162 | 一個簡單的 web app 種子專案。同時也可以當做是一個示範使用本地 recipe 的專案。 163 | 164 | 165 | ## 用語說明 166 | 167 | ### Gulp Task 168 | 169 | 一個 gulp task 只是普通的 JavaScript 函數,函數可以回傳 Promise, Observable, Stream, 子行程,或者是在完成任務時呼叫 `done()` 回呼函數。從 Gulp 4.0 開始,函數被呼叫時,其執行環境 (context),也就是 `this` 值,是 `undefined`。 170 | 171 | ``` javascript 172 | function gulpTask(done) { 173 | assert(this === null); 174 | // do things ... 175 | done(); 176 | } 177 | ``` 178 | 179 | 必須使用 `gulp.task()` 函數註冊之後,函數才會成為 gulp task。 180 | 181 | ``` javascript 182 | gulp.task(gulpTask); 183 | ``` 184 | 185 | 然後才能在命令列下執行。 186 | 187 | ``` bash 188 | $ gulp gulpTask 189 | ``` 190 | 191 | ### Configurable Task 192 | 193 | 一個可組態配置的 gulp 任務在 gulp-chef 中稱為 configurable task,其函數參數配置與普通 gulp task 相同。但是被 gulp-chef 呼叫時,gulp-chef 將傳遞一個 `{ gulp, config, upstream }` 物件做為其執行環境 (context)。 194 | 195 | ``` javascript 196 | // 注意: configurable task 不能直接撰寫,必須透過配置組態的方式來產生。 197 | function configurableTask(done) { 198 | done(); 199 | } 200 | ``` 201 | 202 | 你不能直接撰寫 configurable task,而是必須透過定義組態配置,並呼叫 `chef()` 函數來產生。 203 | 204 | ``` javascript 205 | var gulp = require('gulp'); 206 | var chef = require('gulp-chef'); 207 | var meals = chef({ 208 | scripts: { 209 | src: 'src/**/*.js', 210 | dest: 'dist/' 211 | } 212 | }); 213 | 214 | gulp.registry(meals); 215 | ``` 216 | 217 | 在這個範例中,gulp-chef 為你建立了一個名為 "`scripts`" 的 configurable task。注意 `chef()` 函數回傳一個 gulp registry 物件,你可以透過回傳的 gulp registry 物件,以 `meals.get('scripts')` 的方式取得該 configurable task。但是通常你會呼叫 `gulp.registry()` 來註冊所有包含在 registry 之中的任務。 218 | 219 | ``` javascript 220 | gulp.registry(meals); 221 | ``` 222 | 223 | 一旦你呼叫了 `gulp.registry()` 之後,你就可以在命令列下執行那些已註冊的任務。 224 | 225 | ``` bash 226 | $ gulp scripts 227 | ``` 228 | 229 | 當 configurable task 被呼叫時,將連同其一起配置的組態,經由其執行環境傳入,大致上是以如下的方式呼叫: 230 | 231 | ``` javascript 232 | scripts.call({ 233 | gulp: gulp, 234 | config: { 235 | src: 'src/**/*.js', 236 | dest: 'dist/' 237 | } 238 | }, done); 239 | ``` 240 | 241 | 另外注意到在這個例子中,在組態中的 "`scripts`" 項目,實際上對應到一個 recipe 或 plugin 的模組名稱。如果是 recipe 的話,該 recipe 模組檔案必須位於專案的 "`gulp`" 目錄下。如果對應的是 plugin 的話,該 plugin 必須先安裝到專案中。更多細節請參考『[撰寫 recipe](#writing-recipes)』及『[使用 plugin](#using-plugins)』的說明。 242 | 243 | ### Configurable Recipe 244 | 245 | 一個支援組態配置,可供 gulp 重複使用的任務,在 gulp-chef 中稱為 configurable recipe [註],其函數參數配置也與普通 gulp task 相同,在被 gulp-chef 呼叫時,gulp-chef 也將傳遞一個 `{ gulp, config, upstream }` 物件做為其執行環境 (context)。這是你真正撰寫,並且重複使用的函數。事實上,前面提到的 "[configurable task](#configurable-task)",就是透過名稱對應的方式,在組態配置中對應到真正的 configurable recipe,然後加以包裝、註冊為 gulp 任務。 246 | 247 | ``` javascript 248 | function scripts(done) { 249 | // 注意:在 configurable recipe 中,你可以直接由 context 取得 gulp 實體。 250 | var gulp = this.gulp; 251 | // 注意:在 configurable recipe 中,你可以直接由 context 取得 config 組態。 252 | var config = this.config; 253 | 254 | // 用力 ... 255 | 256 | done(); 257 | } 258 | ``` 259 | 260 | 註:在 gulp-chef 中,recipe 意指可重複使用的任務。就像一份『食譜』可以用來做出無數的菜餚一樣。 261 | 262 | ## 撰寫組態配置 263 | 264 | 組態配置只是普通的 JSON 物件。組態中的每個項目、項目的子項目,要嘛是屬性 (property),要不然就是子任務。 265 | 266 | ### 巢狀任務 267 | 268 | 任務可以巢狀配置。子任務依照組態的語法結構 (lexically),或稱靜態語彙結構 (statically),以層疊結構 (cascading) 的形式繼承 (inherit) 其父任務的組態。更棒的是,一些預先定義的屬性,譬如 "`src`", "`dest`" 等路徑性質的屬性,gulp-chef 會自動幫你連接好路徑。 269 | 270 | ``` javascript 271 | var meals = chef({ 272 | src: 'src/', 273 | dest: 'dist/', 274 | build: { 275 | scripts: { 276 | src: '**/*.js' 277 | }, 278 | styles: { 279 | src: '**/*.css' 280 | } 281 | } 282 | }); 283 | ``` 284 | 285 | 這個例子建立了__三個__ configurable tasks 任務:`build`, `scripts` 以及 `styles`。 286 | 287 | ### 並行任務 288 | 289 | 在上面的例子中,當你執行 `build` 任務時,它的子任務 `scripts` 和 `styles` 會以並行 (parallel) 的方式同時執行。並且由於繼承的關係,它們將獲得如下的組態配置: 290 | 291 | ``` javascript 292 | scripts: { 293 | src: 'src/**/*.js', 294 | dest: 'dist/' 295 | }, 296 | styles: { 297 | src: 'src/**/*.css', 298 | dest: 'dist/' 299 | } 300 | ``` 301 | 302 | ### 序列任務 303 | 304 | 如果你希望任務以序列 (series) 的順序執行,你可以使用 "`series`" __流程控制器 (flow controller)__,並且在子任務的組態配置中,加上 "`order`" 屬性: 305 | 306 | ``` javascript 307 | var meals = chef({ 308 | src: 'src/', 309 | dest: 'dist/', 310 | build: { 311 | series: { 312 | scripts: { 313 | src: '**/*.js', 314 | order: 0 315 | }, 316 | styles: { 317 | src: '**/*.css', 318 | order: 1 319 | } 320 | } 321 | } 322 | }); 323 | ``` 324 | 325 | 記住,你必須使用 "`series`" 流程控制器,子任務才會以序列的順序執行,僅僅只是加上 "`order`" 屬性並不會達到預期的效果。 326 | 327 | ``` javascript 328 | var meals = chef({ 329 | src: 'src/', 330 | dest: 'dist/', 331 | build: { 332 | scripts: { 333 | src: '**/*.js', 334 | order: 0 335 | }, 336 | styles: { 337 | src: '**/*.css', 338 | order: 1 339 | } 340 | } 341 | }); 342 | ``` 343 | 344 | 在這個例子中,`scripts` 和 `styles` 會以並行的方式同時執行。 345 | 346 | 其實有更簡單的方式,可以使子任務以序列的順序執行:使用陣列。 347 | 348 | ``` javascript 349 | var meals = chef({ 350 | src: 'src/', 351 | dest: 'dist/', 352 | build: [{ 353 | name: 'scripts', 354 | src: '**/*.js' 355 | }, { 356 | name: 'styles', 357 | src: '**/*.css' 358 | }] 359 | }; 360 | ``` 361 | 362 | 不過,看起來似乎有點可笑?別急,請繼續往下看。 363 | 364 | ### 參照任務 365 | 366 | 你可以使用名稱來參照其他任務。向前、向後參照皆可。 367 | 368 | ``` javascript 369 | var meals = chef({ 370 | src: 'src/', 371 | dest: 'dist/', 372 | clean: {}, 373 | scripts: { 374 | src: '**/*.js' 375 | }, 376 | styles: { 377 | src: '**/*.css' 378 | }, 379 | build: ['clean', 'scripts', 'styles'] 380 | }; 381 | ``` 382 | 383 | 在這個例子中,`build` 任務有三個子任務,分別參照到 `clean`, `scripts` 以及 `styles` 任務。參照任務並不會產生並註冊新的任務,所以,在這個例子中,你無法直接執行 `build` 任務的子任務,但是你可以透過執行 `build` 任務執行它們。 384 | 385 | 前面提到過,子任務依照組態的語法結構 (lexically),或稱靜態語彙結構 (statically),以層疊結構 (cascading) 的形式繼承 (inherit) 其父任務的組態。既然『被參照的任務』不是定義在『參照任務』之下,『被參照的任務』自然不會繼承『參照任務』及其父任務的靜態組態配置。不過,有另一種組態是執行時期動態產生的,動態組態會在執行時期注入到『被參照的任務』。更多細節請參考『[動態組態](#dynamic-configuration)』的說明。 386 | 387 | 在這個例子中,由於使用陣列來指定參照 `clean`, `scripts` 及 `styles` 的任務,所以是以序列的順序執行。你可以使用 "`parallel`" 流程控制器改變這個預設行為。 388 | 389 | ``` javascript 390 | var meals = chef({ 391 | src: 'src/', 392 | dest: 'dist/', 393 | clean: {}, 394 | scripts: { 395 | src: '**/*.js' 396 | }, 397 | styles: { 398 | src: '**/*.css' 399 | }, 400 | build: ['clean', { parallel: ['scripts', 'styles'] }] 401 | }; 402 | ``` 403 | 404 | 或者,其實你可以將子任務以物件屬性的方式,放在一個共同父任務之下,這樣它們就會預設以並行的方式執行。 405 | 406 | ``` javascript 407 | var meals = chef({ 408 | src: 'src/', 409 | dest: 'dist/', 410 | clean: {}, 411 | make: { 412 | scripts: { 413 | src: '**/*.js' 414 | }, 415 | styles: { 416 | src: '**/*.css' 417 | } 418 | }, 419 | build: ['clean', 'make'] 420 | }); 421 | ``` 422 | 423 | 你可以另外使用 "`task`" 關鍵字來引用『被參照的任務』,這樣『參照任務』本身就可以同時擁有其他屬性。 424 | 425 | ``` javascript 426 | var meals = chef({ 427 | src: 'src/', 428 | dest: 'dist/', 429 | clean: {}, 430 | make: { 431 | scripts: { 432 | src: '**/*.js' 433 | }, 434 | styles: { 435 | src: '**/*.css' 436 | } 437 | }, 438 | build: { 439 | description: 'Clean and make', 440 | task: ['clean', 'make'] 441 | }, 442 | watch: { 443 | description: 'Watch and run related task', 444 | options: { 445 | usePolling: true 446 | }, 447 | task: ['scripts', 'styles'] 448 | } 449 | }; 450 | ``` 451 | 452 | ### 純函數 / 內聯函數 453 | 454 | 任務也可以以普通函數的方式定義並且直接引用,或以內聯匿名函數的形式引用。 455 | 456 | ``` javascript 457 | function clean() { 458 | return del(this.config.dest.path); 459 | } 460 | 461 | var meals = chef({ 462 | src: 'src/', 463 | dest: 'dist/', 464 | scripts: function (done) { 465 | }, 466 | styles: function (done) { 467 | }, 468 | build: [clean, { parallel: ['scripts', 'styles'] }] 469 | }; 470 | ``` 471 | 472 | 注意在這個例子中,在組態配置中並未定義 `clean` 項目,所以`clean` 並不會被註冊為 gulp task。 473 | 474 | 另外一個需要注意的地方是,即使只是純函數,gulp-chef 呼叫時,總是會以 `{ gulp, config, upstream }` 做為執行環境來呼叫。 475 | 476 | 你一樣可以使用 "`task`" 關鍵字來引用函數,這樣任務本身就可以同時擁有其他屬性。 477 | 478 | ``` javascript 479 | function clean() { 480 | return del(this.config.dest.path); 481 | } 482 | 483 | var meals = chef({ 484 | src: 'src/', 485 | dest: 'dist/', 486 | clean: { 487 | options: { 488 | dryRun: true 489 | }, 490 | task: clean 491 | }, 492 | make: { 493 | scripts: { 494 | src: '**/*.js', 495 | task: function (done) { 496 | } 497 | }, 498 | styles: { 499 | src: '**/*.css', 500 | task: function (done) { 501 | } 502 | } 503 | }, 504 | build: ['clean', 'make'], 505 | watch: { 506 | options: { 507 | usePolling: true 508 | }, 509 | task: ['scripts', 'styles'] 510 | } 511 | }; 512 | ``` 513 | 514 | 注意到與上個例子相反地,在這裡組態配置中定義了 `clean` 項目,因此 gulp-chef 會產生並註冊 `clean` 任務,所以可以由命令列執行`clean` 任務。 515 | 516 | ### 隱藏任務 517 | 518 | 有時候,某些任務永遠不需要單獨在命令列下執行。隱藏任務可以讓任務不要註冊,同時不可被其它任務引用。隱藏一個任務不會影響到它的子任務,子任務仍然會繼承它的組態配置並且註冊為 gulp 任務。隱藏任務仍然是具有功能的,但是只能透過它的父任務執行。 519 | 520 | 要隱藏一個任務,可以在項目的組態中加入具有 "`hidden`" 值的 "`visibility`" 屬性。 521 | 522 | ``` javascript 523 | var meals = chef({ 524 | src: 'src/', 525 | dest: 'dist/', 526 | scripts: { 527 | concat: { 528 | visibility: 'hidden', 529 | file: 'bundle.js', 530 | src: 'lib/', 531 | coffee: { 532 | src: '**/*.coffee' 533 | }, 534 | js: { 535 | src: '**/*.js' 536 | } 537 | } 538 | } 539 | }; 540 | ``` 541 | 542 | 在這個例子中,`concat` 任務已經被隱藏了,然而它的子任務 `coffee` 和 `js` 依然可見。 543 | 544 | 為了簡化組態配置,你也可以使用在任務名稱前面附加上一個 "`.`" 字元的方式來隱藏任務,就像 UNIX 系統的 [dot-files](https://en.wikipedia.org/wiki/Dot-file) 一樣。 545 | 546 | ``` javascript 547 | var meals = chef({ 548 | src: 'src/', 549 | dest: 'dist/', 550 | scripts: { 551 | '.concat': { 552 | file: 'bundle.js', 553 | src: 'lib', 554 | coffee: { 555 | src: '**/*.coffee' 556 | }, 557 | js: { 558 | src: '**/*.js' 559 | } 560 | } 561 | } 562 | }; 563 | ``` 564 | 565 | 這將產出與上一個例子完全相同的結果。 566 | 567 | ### 停用任務 568 | 569 | 有時候,當你在調整 gulpfile.js 時,你可能需要暫時移除某些任務,找出發生問題的根源。這時候你可以停用任務。停用任務時,連同其全部的子任務都將被停用,就如同未曾定義過一樣。 570 | 571 | 要停用一個任務,可以在項目的組態中加入具有 "`disabled`" 值的 "`visibility`" 屬性。 572 | 573 | ``` javascript 574 | var meals = chef({ 575 | src: 'src/', 576 | dest: 'dist/', 577 | scripts: { 578 | concat: { 579 | file: 'bundle.js', 580 | src: 'lib/', 581 | coffee: { 582 | visibility: 'disabled', 583 | src: '**/*.coffee' 584 | }, 585 | js: { 586 | src: '**/*.js' 587 | } 588 | } 589 | } 590 | }; 591 | ``` 592 | 593 | 在這個例子中,`coffee` 任務已經被停用了。 594 | 595 | 為了簡化組態配置,你也可以使用在任務名稱前面附加上一個 "`#`" 字元的方式來停用任務,就像 UNIX 系統的 bash 指令檔的註解一樣。 596 | 597 | ``` javascript 598 | var meals = chef({ 599 | src: 'src/', 600 | dest: 'dist/', 601 | scripts: { 602 | concat: { 603 | file: 'bundle.js', 604 | src: 'lib', 605 | '#coffee': { 606 | src: '**/*.coffee' 607 | }, 608 | js: { 609 | src: '**/*.js' 610 | } 611 | } 612 | } 613 | }; 614 | ``` 615 | 616 | 這將產出與上一個例子完全相同的結果。 617 | 618 | ### 處理命名衝突問題 619 | 620 | 在使用 gulp-chef 時,建議你為所有的任務,分別取用唯一、容易區別的名稱。 621 | 622 | 然而,如果你有非常多的任務,那麼將有很高的機率,有一個以上的任務必須使用相同的 recipe 或 plugin。 623 | 624 | 在預設情況下,任務名稱必須與 recipe 名稱相同,這樣 gulp-chef 才有辦法找到對應的 recipe。那麼,當發生名稱衝突時,gulp-chef 是怎麼處理的呢?gulp-chef 會自動為發生衝突的的任務,在前方附加父任務的名稱,像這樣:"`make:scripts:concat`"。 625 | 626 | 事實上,你也可以將這個附加名稱的行為變成預設行為:在呼叫 `chef()` 函數時,在 `settings` 參數傳入值為 `true` 的 "`exposeWithPrefix`" 屬性即可。 "`exposeWithPrefix`" 屬性的預設值為 `"auto"`。 627 | 628 | ``` javascript 629 | var ingredients = { ... }; 630 | var settings = { exposeWithPrefix: true }; 631 | var meals = chef(ingredients, settings); 632 | ``` 633 | 634 | 不是你的菜?沒關係,你也可以使用其他辦法。 635 | 636 | #### 引入新的父任務並隱藏名稱衝突的任務 637 | 638 | ``` javascript 639 | { 640 | scripts: { 641 | concatScripts: { 642 | '.concat': { 643 | file: 'bundle.js' 644 | } 645 | } 646 | }, 647 | styles: { 648 | concatStyles: { 649 | '.concat': { 650 | file: 'main.css' 651 | } 652 | } 653 | } 654 | } 655 | ``` 656 | 657 | #### 使用 `recipe` 關鍵字 658 | 659 | ``` javascript 660 | { 661 | scripts: { 662 | concatScripts: { 663 | recipe: 'concat', 664 | file: 'bundle.js' 665 | } 666 | }, 667 | styles: { 668 | concatStyles: { 669 | recipe: 'concat', 670 | file: 'main.css' 671 | } 672 | } 673 | } 674 | ``` 675 | 676 | 注意:為了盡量避免發生名稱衝突的可能性,並且簡化任務樹,某些特定種類的任務預設是隱藏的。主要是『__串流處理器 (stream processor)__』及『__流程控制器 (flow controller)__』。請參考 [撰寫串流處理器](#writing-stream-processor) and [撰寫流程控制器](#writing-flow-controller) 的說明。 677 | 678 | ### 使用 Gulp Plugins 679 | 680 | 有時候,你所撰寫的任務所做的,只不過是轉呼叫一個 plugin。如果只是這樣的話,事實上你完全可以不用費心寫一個 recipe,你可以直接在組態配置中使用 "`plugin`" 關鍵字做為屬性來引用 plugin。 681 | 682 | ``` javascript 683 | { 684 | concat: { 685 | plugin: 'gulp-concat', 686 | options: 'bundle.js' 687 | } 688 | } 689 | ``` 690 | 691 | 這個 "`plugin`" 屬性可以接受 `string` 和 `function` 類型的值。當指定的值不是 `function` 而是 `string` 類型時,gulp-chef 將以此字串做為模組名稱,嘗試去 "`require()`" 該模組。使用 "`plugin`" 屬性時,另外還可以指定 "`options`" 屬性,該屬性的值將直接做為唯一參數,用來呼叫 plugin 函數。 692 | 693 | 任何 gulp plugin,只要它只接受 0 或 1 個參數,並且回傳一個 Stream 或 Promise 物件,就可以使用 `plugin`" 關鍵字來加以引用。前提當然是 plugin 已經使用 `npm install` 指令先安裝好了。 694 | 695 | 千萬不要將 gulp plugin 與 [gulp-chef 專用的 plugin](#using-plugins) 搞混了。gulp-chef 專用的 plugin 稱為 "Cascading Configurable Recipe for Gulp" 或簡稱 "gulp-ccr",意思是『可層疊組態配置、可重複使用的 Gulp 任務』。 696 | 697 | ### 傳遞組態值 698 | 699 | 如同你到目前為止所看到的,在組態配置中的項目,要嘛是任務的屬性,要不然就是子任務。你要如何區別兩者?基本的規則是,除了 "`config`", "`description`", "`dest`", "`name`", "`order`", "`parallel`", "`plugin`", "`recipe`", "`series`", "`spit`", "`src`", "`task`" 以及 "`visibility`" 這些[關鍵字](#keywords)之外,其餘的項目都將被視為子任務。 700 | 701 | 那麼,你要如何傳遞組態值給你的 recipe 函數呢?其實,"`config`" 關鍵字就是特地為了這個目的而保留的。 702 | 703 | ``` javascript 704 | { 705 | myPlugin: { 706 | config: { 707 | file: 'bundle.js' 708 | } 709 | } 710 | } 711 | ``` 712 | 713 | 這裡 "`config`" 屬性連同其 "`file`" 屬性,將一起被傳遞給 recipe 函數,而 recipe 函數則透過執行環境依序取得 "`config`" 屬性及 "`file`" 屬性 (在『[撰寫 recipe](#writing-recipes)』中詳細說明)。 714 | 715 | ``` javascript 716 | function myPlugin(done) { 717 | var file = this.config.file; 718 | done(); 719 | } 720 | 721 | module.exports = myPlugin; 722 | ``` 723 | 724 | 只為了傳遞一個屬性,就必須特地寫一個 "`config`" 項目來傳遞它,如果你覺得這樣做太超過了,你也可以直接在任意屬性名稱前面附加一個 "`$`" 字元,這樣它們就會被視為是組態屬性,而不再會被當作是子任務。 725 | 726 | ``` javascript 727 | { 728 | myPlugin: { 729 | $file: 'bundle.js' 730 | } 731 | } 732 | ``` 733 | 734 | 這樣 "`$file`" 項目就會被當作是組態屬性,而你在組態配置及 recipe 中,可以透過 "`file`" 名稱來存取它。 (注意,名稱不是 "`$file`",這是為了允許使用者可以交換使用 "`$`" 字元和 "`config`" 項目來傳遞組態屬性。) 735 | 736 | #### Recipe / Plugin 專屬組態屬性 737 | 738 | Recipe 以及 plugin 可以使用 [JSON Schema](http://json-schema.org/) 來定義它們的組態屬性及架構。如果它們確實定義了組態架構,那麼你就可以在組態配置項目中,直接列舉專屬的屬性,而不需要透過 "`$`" 字元和 "`config`" 關鍵字。 739 | 740 | 舉例,在 "[gulp-ccr-browserify](https://github.com/gulp-cookery/gulp-ccr-browserify)" plugin 中,它定義了 "`bundles`" 及 "`options`" 屬性,因此你可以在組態項目中直接使用這兩個屬性。 741 | 742 | 原本需要這樣寫: 743 | 744 | ``` javascript 745 | { 746 | src: 'src/', 747 | dest: 'dest/', 748 | browserify: { 749 | config: { 750 | bundles: { 751 | entry: 'main.ts' 752 | }, 753 | options: { 754 | plugins: 'tsify', 755 | sourcemaps: 'external' 756 | } 757 | } 758 | } 759 | } 760 | ``` 761 | 762 | 現在可以省略寫成這樣: 763 | 764 | ``` javascript 765 | { 766 | src: 'src/', 767 | dest: 'dest/', 768 | browserify: { 769 | bundles: { 770 | entry: 'main.ts' 771 | }, 772 | options: { 773 | plugins: 'tsify', 774 | sourcemaps: 'external' 775 | } 776 | } 777 | } 778 | ``` 779 | 780 | #### 自動識別屬性 781 | 782 | 為了方便起見,當組態項目中包含有 "`task`", "`series`", "`parallel`" 或 "`plugin`" 關鍵字的時候,這時候除了保留屬性之外,其餘的屬性都將自動認定為組態屬性,而不是子任務。 783 | 784 | ### 動態組態屬性 / 模板引值 785 | 786 | 有些『串流處理器』 (譬如 "[gulp-ccr-each-dir](https://github.com/gulp-cookery/gulp-ccr-each-dir)"),會以程序化或動態的方式產生新的組態屬性。這些新產生的屬性,將在執行時期,插入到子任務的的組態中。除了 recipe 及 plugin 可以透過 "`config`" 屬性取得這些值之外,子任務也可以透過使用模板的方式,以 "`{{var}}`" 這樣的語法,直接在組態中引用這些值。 787 | 788 | ``` javascript 789 | { 790 | src: 'src/', 791 | dest: 'dist/', 792 | 'each-dir': { 793 | dir: 'modules/', 794 | concat: { 795 | file: '{{dir}}', 796 | spit: true 797 | } 798 | } 799 | } 800 | ``` 801 | 802 | 這個例子裡,"[each-dir](https://github.com/gulp-cookery/gulp-ccr-each-dir)" plugin 會根據 "`dir`" 屬性指定的內容,也就是 "`modules`" 目錄,找出其下的所有子目錄,然後產生新的 "`dir`" 屬性,透過這個屬性將子目錄資訊傳遞給每個子任務 (這裡只有 "concat" 任務)。子任務可以透過 "`config`" 屬性讀取這個值。使用者也可以使用 "`{{dir}}`" 這樣的語法,在組態配置中引用這個值。 803 | 804 | ### 條件式組態配置 805 | 806 | Gulp-chef 支援條件式組態配置。可以透過設定執行時期環境的模式來啟用不同的條件式組態配置。這個功能的實作是基於 [json-regulator](https://github.com/amobiz/json-regulator?utm_referer="gulp-chef") 這個模組,可以參考該模組的說明以便獲得更多的相關資訊。 807 | 808 | 預設提供了 `development`, `production` 及 `staging` 三個模式。你可以在組態配置中,將相關的組態內容,分別寫在對應的 `development` 或 `dev`, `production` 或 `prod` ,或 `staging` 項目之下。 809 | 810 | 譬如,如果將組態配置寫成這樣: 811 | 812 | ``` javascript 813 | { 814 | scripts: { 815 | // common configs 816 | src: 'src/', 817 | 818 | development: { 819 | // development configs 820 | description: 'development mode', 821 | dest: 'build/', 822 | 823 | options: { 824 | // development options 825 | debug: true 826 | }, 827 | 828 | // sub tasks for development mode 829 | lint: { 830 | } 831 | }, 832 | 833 | production: { 834 | // production configs 835 | description: 'production mode', 836 | dest: 'dist/', 837 | 838 | options: { 839 | // production options 840 | debug: false 841 | } 842 | }, 843 | 844 | options: { 845 | // common options 846 | 847 | dev: { 848 | // development options 849 | description: 'development mode', 850 | sourcemap: false 851 | }, 852 | 853 | prod: { 854 | // production options 855 | description: 'production mode', 856 | sourcemap: 'external' 857 | } 858 | }, 859 | 860 | // sub tasks 861 | pipe: [{ 862 | typescript: { 863 | src: '**/*.ts' 864 | }, 865 | 866 | js: { 867 | src: '**/*.js' 868 | } 869 | }, { 870 | production: { 871 | // production configs 872 | description: 'production mode', 873 | 874 | // sub tasks for production mode 875 | uglify: { 876 | } 877 | } 878 | }, { 879 | production: { 880 | // production configs 881 | description: 'production mode', 882 | 883 | // sub tasks for production mode 884 | concat: { 885 | } 886 | } 887 | }] 888 | } 889 | } 890 | ``` 891 | 892 | 當啟用 `development` 模式時,組態配置將被轉換為: 893 | 894 | ``` javascript 895 | { 896 | scripts: { 897 | src: 'src/', 898 | description: 'development mode', 899 | dest: 'build/', 900 | options: { 901 | description: 'development mode', 902 | sourcemap: false, 903 | debug: true 904 | }, 905 | lint: { 906 | }, 907 | pipe: [{ 908 | typescript: { 909 | src: '**/*.ts' 910 | }, 911 | js: { 912 | src: '**/*.js' 913 | } 914 | }] 915 | } 916 | } 917 | ``` 918 | 919 | 而啟用 `production` 模式時,組態配置將被轉換為: 920 | 921 | ``` javascript 922 | { 923 | scripts: { 924 | src: 'src/', 925 | description: 'production mode', 926 | dest: 'dist/', 927 | options: { 928 | description: 'production mode', 929 | sourcemap: 'external', 930 | debug: false 931 | }, 932 | pipe: [{ 933 | typescript: { 934 | src: '**/*.ts' 935 | }, 936 | js: { 937 | src: '**/*.js' 938 | } 939 | }, { 940 | description: 'production mode', 941 | uglify: { 942 | } 943 | }, { 944 | description: 'production mode', 945 | concat: { 946 | } 947 | }] 948 | } 949 | } 950 | ``` 951 | 952 | 超強的! 953 | 954 | #### 以特定的執行時期環境模式啟動 Gulp 955 | 956 | ##### 經由命令列參數 957 | 958 | ``` bash 959 | $ gulp --development build 960 | ``` 961 | 962 | 也可以使用簡寫: 963 | 964 | ``` bash 965 | $ gulp --dev build 966 | ``` 967 | 968 | ##### 經由環境變數 969 | 970 | 在 Linux/Unix 下: 971 | 972 | ``` bash 973 | $ NODE_ENV=development gulp build 974 | ``` 975 | 976 | 同樣地,若使用簡寫: 977 | 978 | ``` bash 979 | $ NODE_ENV=dev gulp build 980 | ``` 981 | 982 | #### 自訂執行時期環境模式 983 | 984 | Gulp-chef 允許你自訂執行時期環境模式。如果你崇尚極簡主義,你甚至可以分別使用 `d`, `p` 及 `s` 代表 `development`, `production` 及 `staging` 模式。只是要記得,組態配置必須與執行時期環境模式配套才行。 985 | 986 | ``` javascript 987 | var ingredients = { 988 | scripts: { 989 | src: 'src/', 990 | lint: { 991 | }, 992 | d: { 993 | debug: true 994 | }, 995 | p: { 996 | debug: false, 997 | sourcemap: 'external', 998 | uglify: { 999 | }, 1000 | concat: { 1001 | } 1002 | } 1003 | } 1004 | }; 1005 | var settings = { 1006 | modes: { 1007 | production: ['p'], 1008 | development: ['d'], 1009 | staging: ['s'], 1010 | default: 'production' 1011 | } 1012 | }; 1013 | var meals = chef(ingredients, settings); 1014 | ``` 1015 | 1016 | 注意到在 `settings.modes` 之下的 `default` 屬性。這個屬性不會定義新的模式,它是用來指定預設的模式。如果沒有指定 `settings.modes.default` ,那麼,預設模式會成為列在 `settings.modes` 之下的第一個模式。建議最好不要省略。 1017 | 1018 | 除了改變模式的代號,你甚至可以設計自己的模式,並且還能一次提供多個代號。 1019 | 1020 | ``` javascript 1021 | var settings = { 1022 | modes = { 1023 | build: ['b', 'build'], 1024 | compile: ['c', 'compile'], 1025 | deploy: ['d', 'deploy', 'deployment'], 1026 | review: ['r', 'review'] 1027 | default: 'build' 1028 | } 1029 | }; 1030 | ``` 1031 | 1032 | 但是要注意的是,不要使用到保留給任務使用的[關鍵字](#keywords)。 1033 | 1034 | ## 內建的 Recipe 1035 | 1036 | #### [clean](https://github.com/gulp-cookery/gulp-ccr-clean) 1037 | 1038 | 清除 `dest` 屬性指定的目錄。 1039 | 1040 | #### [copy](https://github.com/gulp-cookery/gulp-ccr-copy) 1041 | 1042 | 複製由 `src` 屬性指定的檔案,到由 `dest` 屬性指定的目錄,可以選擇是否移除或改變檔案的相對路徑。 1043 | 1044 | #### [merge](https://github.com/gulp-cookery/gulp-ccr-merge) 1045 | 1046 | 這是一個串流處理器。回傳一個新的串流,該串流只有在所有的子任務的串流都停止時才會停止。 1047 | 1048 | 更多資訊請參考 [merge-stream](https://www.npmjs.com/package/merge-stream) 。 1049 | 1050 | #### [queue](https://github.com/gulp-cookery/gulp-ccr-queue) 1051 | 1052 | 這是一個串流處理器。可以匯集子任務所回傳的串流,並回傳一個新的串流,該串流會將子任務回傳的串流,依照子任務的順序排列在一起。 1053 | 1054 | 更多資訊請參考 [streamqueue](https://www.npmjs.com/package/streamqueue) 。 1055 | 1056 | #### [pipe](https://github.com/gulp-cookery/gulp-ccr-pipe) 1057 | 1058 | 這是一個串流處理器。提供與 [`stream.Readable.pipe()`](https://nodejs.org/api/stream.html#stream_readable_pipe_destination_options) 相同的功能。方便在子任務之間遞送 (pipe) 串流。 1059 | 1060 | #### [parallel](https://github.com/gulp-cookery/gulp-ccr-parallel) 1061 | 1062 | 這是一個流程控制器。會以並行 (parallel) 的方式執行子任務,子任務之間不會互相等待。 1063 | 1064 | #### [series](https://github.com/gulp-cookery/gulp-ccr-series) 1065 | 1066 | 這是一個流程控制器。會以序列 (series) 的方式執行子任務,前一個子任務結束之後才會執行下一個子任務。 1067 | 1068 | #### [watch](https://github.com/gulp-cookery/gulp-ccr-watch) 1069 | 1070 | 這是一個流程控制器。負責監看指定的子任務、以及其所有子任務的來源檔案,當有任何檔案異動時,執行對應的指定任務。 1071 | 1072 | ## 使用 Plugin 1073 | 1074 | 在你撰寫自己的 recipe 之前,先看一下別人已經做了哪些東西,也許有現成的可以拿來用。你可以使用" `gulp recipe`",或者,更建議使用 "`gulp-ccr`",在 [github.com](https://github.com/search?utf8=%E2%9C%93&q=gulp-ccr) 和 [npmjs.com](https://www.npmjs.com/search?q=gulp-ccr) 上搜尋。這個 "`gulp-ccr`" 是 "Cascading Configurable Recipe for Gulp" 的簡寫,意思是『可層疊組態配置、可重複使用的 Gulp 任務』。 1075 | 1076 | 一旦你找到了,譬如,[`gulp-ccr-browserify`](https://github.com/gulp-cookery/gulp-ccr-browserify) ,將它安裝為專案的 devDependencies: 1077 | 1078 | ``` bash 1079 | $ npm install --save-dev gulp-ccr-browserify 1080 | ``` 1081 | 1082 | Gulp-chef 會為你移除附加在前面的 "`gulp-ccr-`" 名稱,所以你在使用 plugin 的時候,請移除 "`gulp-ccr-`" 部份。 1083 | 1084 | ``` javascript 1085 | { 1086 | browserify: { 1087 | description: 'Using the gulp-ccr-browserify plugin' 1088 | } 1089 | } 1090 | ``` 1091 | 1092 | ## 撰寫 Recipe 1093 | 1094 | 斯斯,不是,recipe 有三種: __任務型 (task)__、__串流處理器 (stream processor)__ 以及 __流程控制器 (flow controller)__。 1095 | 1096 | 大多數時候,你想要寫的是任務型 recipe。任務型 recipe 負責做苦工,而串流處理器及流程控制器則負責操弄其它 recipe。 1097 | 1098 | 更多關於串流處理器及流程控制器的說明,或者你樂於分享你的 recipe,你可以寫成 plugin,請參考 [撰寫 Plugin](#writing-plugins) 的說明。 1099 | 1100 | 如果你撰寫的 recipe 只打算給特定專案使用,你可以將它們放在專案根目錄下的特定子目錄下: 1101 | 1102 | 類型 |目錄 1103 | ---------|------------------ 1104 | 任務型 |gulp, gulp/tasks 1105 | 串流處理器 |gulp/streams 1106 | 流程控制器 |gulp/flows 1107 | 1108 | 如果你的 recipe 不需要組態配置,你可以像平常寫 gulp task 一樣的方式撰寫 recipe。知道這代表什麼意思嗎?這代表你以前寫的 gulp task 都可以直接拿來當作 recipe 用。你只需要將它們個別存放到專屬的模組檔案,然後放到專案根目錄下的 "gulp" 目錄下即可。 1109 | 1110 | 使用 recipe 的時候,在組態配置中,使用一個屬性名稱與 recipe 模組名稱一模一樣的項目來引用該 recipe。 1111 | 1112 | 譬如,假設你有一個 "`my-recipe.js`" recipe 放在 `/gulp` 目錄下。可以這樣撰寫組態配置來引用它: 1113 | 1114 | ``` javascript 1115 | var gulp = require('gulp'); 1116 | var chef = require('gulp-chef'); 1117 | var meals = chef({ 1118 | "my-recipe": {} 1119 | }); 1120 | gulp.registry(meals); 1121 | ``` 1122 | 1123 | 就是這麼簡單。之後你就可以在命令列下,以 `gulp my-recipe` 指令執行它。 1124 | 1125 | 然而,提供組態配置的能力,才能最大化 recipe 的重複使用價值。 1126 | 1127 | 要讓 recipe 可以處理組態內容,可以在 recipe 函數中,透過執行環境,也就是 `this` 變數,取得組態。 1128 | 1129 | ``` javascript 1130 | function scripts(done) { 1131 | var gulp = this.gulp; 1132 | var config = this.config; 1133 | 1134 | return gulp.src(config.src.globs) 1135 | .pipe(eslint()) 1136 | .pipe(concat(config.file)) 1137 | .pipe(uglify()) 1138 | .pipe(gulp.dest(config.dest.path)); 1139 | } 1140 | 1141 | module.exports = scripts; 1142 | ``` 1143 | 1144 | 上面的 "`scripts`" recipe,在使用的時候可以像這樣配置: 1145 | 1146 | ``` javascript 1147 | var meals = chef({ 1148 | src: 'src/', 1149 | dest: 'dist/', 1150 | scripts: { 1151 | src: '**/*.js', 1152 | file: 'bundle.js' 1153 | } 1154 | }); 1155 | ``` 1156 | 1157 | ### Development / Production 模式 1158 | 1159 | Gulp-chef 的 recipe 不需要自行處理條件式組態配置。組態配置在傳遞給 recipe 之前,已經先根據執行環境模式處理完畢。 1160 | 1161 | ## 撰寫 Plugin 1162 | 1163 | Gulp-chef 的 plugin,只是普通的 Node.js 模組,再加上一些必要的資訊。 1164 | 1165 | ### Plugin 的類型 1166 | 1167 | 在前面 [撰寫 Recipe](#writing-recipes) 的部份提到過,recipe 有三種:__任務型 (task)__、__串流處理器 (stream processor)__ 以及 __流程控制器 (flow controller)__。Gulp-chef 需要知道 plugin 的類型,才能安插必要的輔助功能。由於 plugin 必須使用 `npm install` 安裝,gulp-chef 無法像本地的 recipe 一樣,由目錄決定 recipe 的類型,因此 plugin 必須自行提供類型資訊。 1168 | 1169 | ``` javascript 1170 | function myPlugin(done) { 1171 | done(); 1172 | } 1173 | 1174 | module.exports = myPlugin; 1175 | module.exports.type = 'flow'; 1176 | ``` 1177 | 1178 | 有效的類型為: "`flow`"、"`stream`" 以及 "`task`"。 1179 | 1180 | ### 組態架構 (Configuration Schema) 1181 | 1182 | 為了簡化組態配置的處理過程,gulp-chef 鼓勵使用 [JSON Schema](http://json-schema.org/) 來驗證和轉換組態配置。Gulp-chef 使用 [json-normalizer](https://github.com/amobiz/json-normalizer?utm_referer="gulp-chef") 來為 JSON Schema 提供擴充功能,並且協助將組態內容一般化 (或稱正規化),以提供最大的組態配置彈性。你可以為你的 plugin 定義組態架構,以提供屬性別名、類型轉換、預設值等功能。同時,組態架構的定義內容還可以顯示在命令列中,使用者可以使用指令 `gulp --recipe ` 查詢,不必另外查閱文件,就可以了解如何撰寫組態配置。請參考 [json-normalizer](https://github.com/amobiz/json-normalizer?utm_referer="gulp-chef") 的說明,了解如何定義組態架構,甚至加以擴充。 1183 | 1184 | 以下是一個簡單的 plugin,示範如何定義組態架構: 1185 | 1186 | ``` javascript 1187 | var gulpif = require('gulp-if'); 1188 | var concat = require('gulp-concat'); 1189 | var sourcemaps = require('gulp-sourcemaps'); 1190 | var uglify = require('gulp-uglify'); 1191 | 1192 | function myPlugin() { 1193 | var gulp = this.gulp; 1194 | var config = this.config; 1195 | var options = this.config.options || {}; 1196 | var maps = (options.sourcemaps === 'external') ? './' : null; 1197 | 1198 | return gulp.src(config.src.globs) 1199 | .pipe(gulpif(config.sourcemaps, sourcemaps.init()) 1200 | .pipe(concat(config.file)) 1201 | .pipe(gulpif(options.uglify, uglify())) 1202 | .pipe(gulpif(options.sourcemaps, sourcemaps.write(maps))) 1203 | .pipe(gulp.dest(config.dest.path)); 1204 | } 1205 | 1206 | module.exports = myPlugin; 1207 | module.exports.type = 'task'; 1208 | module.exports.schema = { 1209 | title: 'My Plugin', 1210 | description: 'My first plugin', 1211 | type: 'object', 1212 | properties: { 1213 | src: { 1214 | type: 'glob' 1215 | }, 1216 | dest: { 1217 | type: 'path' 1218 | }, 1219 | file: { 1220 | description: 'Output file name', 1221 | type: 'string' 1222 | }, 1223 | options: { 1224 | type: 'object', 1225 | properties: { 1226 | sourcemaps: { 1227 | description: 'Sourcemap support', 1228 | alias: ['sourcemap'], 1229 | enum: [false, 'inline', 'external'], 1230 | default: false 1231 | }, 1232 | uglify: { 1233 | description: 'Uglify bundle file', 1234 | type: 'boolean', 1235 | default: false 1236 | } 1237 | } 1238 | } 1239 | }, 1240 | required: ['file'] 1241 | }; 1242 | ``` 1243 | 1244 | 首先,注意到 "`file`" 被標示為『必須』,plugin 可以利用組態驗證工具自動進行檢查,因此在程式中就不須要再自行判斷。 1245 | 1246 | 另外注意到 "`sourcemaps`" 選項允許 "`sourcemap`" 別名,因此使用者可以在組態配置中隨意使用 "`sourcemaps`" 或 "`sourcemap`",但是同時在 plugin 中,卻只需要處理 "`sourcemaps`" 即可。 1247 | 1248 | #### 擴充資料型別 1249 | 1250 | Gulp-chef 提供兩個擴充的 JSON Schema 資料型別: "`glob`" 及 "`path`"。 1251 | 1252 | ##### glob 1253 | 1254 | 一個屬性如果是 "`glob`" 型別,它可以接受一個路徑、一個路徑匹配表達式 (glob),或者是一個由路徑或路徑匹配表達式組成的陣列。另外還可以額外附帶選項資料。 1255 | 1256 | 以下都是正確的 "`glob`" 數值: 1257 | 1258 | ``` javascript 1259 | // 一個路徑字串 1260 | 'src' 1261 | // 一個由路徑字串組成的陣列 1262 | ['src', 'lib'] 1263 | // 一個路徑匹配表達式 1264 | '**/*.js' 1265 | // 一個由路徑或路徑匹配表達式組成的陣列 1266 | ['**/*.{js,ts}', '!test*'] 1267 | // 非正規化的『物件表達形式』(注意 "glob" 屬性) 1268 | { glob: '**/*.js' } 1269 | ``` 1270 | 1271 | 上面所有的數值,都會被正規化為所謂的『物件表達形式』: 1272 | 1273 | ``` javascript 1274 | // 一個路徑字串 1275 | { globs: ['src'] } 1276 | // 一個由路徑字串組成的陣列 1277 | { globs: ['src', 'lib'] } 1278 | // 一個路徑匹配表達式 1279 | { globs: ['**/*.js'] } 1280 | // 一個由路徑或路徑匹配表達式組成的陣列 1281 | { globs: ['**/*.{js,ts}', '!test*'] } 1282 | // 正規化之後的『物件表達形式』(注意 "glob" 屬性已經正規化為 "globs") 1283 | { globs: ['**/*.js'] } 1284 | ``` 1285 | 1286 | 注意到 "`glob`" 是 "`globs`" 屬性的別名,在正規化之後,被更正為 "`globs`"。同時,"`glob`" 型別的 "`globs`" 屬性的型態為陣列,因此,所有的值都將自動被轉換為陣列。 1287 | 1288 | 當以『物件表達形式』呈現時,還可以使用 "`options`" 屬性額外附帶選項資料。 1289 | 1290 | ``` javascript 1291 | { 1292 | globs: ['**/*.{js,ts}', '!test*'], 1293 | options: { 1294 | base: 'src', 1295 | buffer: true, 1296 | dot: true 1297 | } 1298 | } 1299 | ``` 1300 | 1301 | 更多選項資料,請參考 [node-glob](https://github.com/isaacs/node-glob#options) 的說明。 1302 | 1303 | 在任務中,任何具有 "`glob`" 型別的組態屬性,都會繼承其父任務的 "`src`" 屬性。這意謂著,當父任務定義了 "`src`" 屬性時,gulp-chef 會為子任務的 "`glob`" 型別的組態屬性,自動連接好父任務的 "`src`" 屬性的路徑。 1304 | 1305 | ``` javascript 1306 | { 1307 | src: 'src', 1308 | browserify: { 1309 | bundles: { 1310 | entries: 'main.js' 1311 | } 1312 | } 1313 | } 1314 | ``` 1315 | 1316 | 在這個例子中,"[browserify](https://github.com/gulp-cookery/gulp-ccr-browserify)" plugin 具有一個 "`bundles`" 屬性,"`bundles`" 屬性下又有一個 "`entries`" 屬性,而該屬性為 "`glob`" 型別。這個 "`entries`" 屬性將繼承外部的 "`src`" 屬性,因而變成: `{ globs: "src/main.js" }` 。 1317 | 1318 | 如果這不是你要的,你可以指定 "`join`" 選項來覆蓋這個行為。 1319 | 1320 | ``` javascript 1321 | { 1322 | src: 'src', 1323 | browserify: { 1324 | bundles: { 1325 | entry: { 1326 | glob: 'main.js', 1327 | options: { 1328 | join: false 1329 | } 1330 | } 1331 | } 1332 | } 1333 | } 1334 | ``` 1335 | 1336 | 現在 "`entries`" 屬性的值將成為: `{ globs: "main.js" }` 。 1337 | 1338 | 選項 "`join`" 也可以接受字串,用來指定要從哪一個屬性繼承路徑,該屬性必須是 "`glob`" 或 "`path`" 型別。 1339 | 1340 | 在 plugin 中,也可以透過[組態架構](#configuration-schema)來定義預設要繼承的屬性。請記住,除非有好的理由,請永遠記得同時將 "`options`" 傳遞給呼叫的 API,以便允許使用者指定選項。像這樣: 1341 | 1342 | ``` javascript 1343 | module.exports = function () { 1344 | var gulp = this.gulp; 1345 | var config = this.config; 1346 | 1347 | return gulp.src(config.src.globs, config.src.options) 1348 | .pipe(...); 1349 | } 1350 | ``` 1351 | 1352 | ##### path 1353 | 1354 | 一個屬性如果是 "`path`" 型別,它可以接受一個路徑字串。另外還可以額外附帶選項資料。 1355 | 1356 | 以下都是正確的 "`path`" 數值: 1357 | 1358 | ``` javascript 1359 | // 一個路徑字串 1360 | 'dist' 1361 | // 一個路徑字串 1362 | 'src/lib/' 1363 | // 『物件表達形式』 1364 | { path: 'maps/' } 1365 | ``` 1366 | 1367 | 上面所有的數值,都會被正規化為所謂的『物件表達形式』: 1368 | 1369 | ``` javascript 1370 | // 一個路徑字串 1371 | { path: 'dist' } 1372 | // 一個路徑字串 1373 | { path: 'src/lib/' } 1374 | // 『物件表達形式』 1375 | { path: 'maps/' } 1376 | ``` 1377 | 1378 | 當以『物件表達形式』呈現時,還可以使用 "`options`" 屬性額外附帶選項資料。 1379 | 1380 | ``` javascript 1381 | { 1382 | path: 'dist/', 1383 | options: { 1384 | cwd: './', 1385 | overwrite: true 1386 | } 1387 | } 1388 | ``` 1389 | 1390 | 更多選項資料,請參考 [gulp.dest()](https://github.com/gulpjs/gulp/blob/4.0/docs/API.md#options-1) 的說明。 1391 | 1392 | 在任務中,任何具有 "`path`" 型別的組態屬性,都會繼承其父任務的 "`dest`" 屬性。這意謂著,當父任務定義了 "`dest`" 屬性時,gulp-chef 會為子任務的 "`path`" 型別的組態屬性,自動連接好父任務的 "`dest`" 屬性的路徑。 1393 | 1394 | ``` javascript 1395 | { 1396 | dest: 'dist/', 1397 | scripts: { 1398 | file: 'bundle.js' 1399 | } 1400 | } 1401 | ``` 1402 | 1403 | 假設這裡的 "`file`" 屬性是 "`path`" 型別,它將會繼承外部的 "`dest`" 屬性,而成為: "`{ path: 'dist/bundle.js' }`"。 1404 | 1405 | 如果這不是你要的,你可以指定 "`join`" 選項來覆蓋這個行為。 1406 | 1407 | ``` javascript 1408 | { 1409 | dest: 'dist/', 1410 | scripts: { 1411 | file: { 1412 | path: 'bundle.js', 1413 | options: { 1414 | join: false 1415 | } 1416 | } 1417 | } 1418 | } 1419 | ``` 1420 | 1421 | 現在 "`file`" 屬性將成為: "`{ path: 'bundle.js' }`"。 1422 | 1423 | 選項 "`join`" 也可以接受字串,用來指定要從哪一個屬性繼承路徑,該屬性必須是 "`path`" 型別。 1424 | 1425 | 在 plugin 中,也可以透過[組態架構](#configuration-schema)來定義預設要繼承的屬性。請記住,除非有好的理由,請永遠記得同時將 "`options`" 傳遞給呼叫的 API,以便允許使用者指定選項。像這樣: 1426 | 1427 | ``` javascript 1428 | module.exports = function () { 1429 | var gulp = this.gulp; 1430 | var config = this.config; 1431 | 1432 | return gulp.src(config.src.globs, config.src.options) 1433 | .pipe(...) 1434 | .pipe(gulp.dest(config.dest.path, config.dest.options)); 1435 | } 1436 | ``` 1437 | ### 撰寫串流處理器 1438 | 1439 | 串流處理器負責操作它的子任務輸入或輸出串流。 1440 | 1441 | 串流處理器可以自己輸出串流,或者由其中的子任務輸出。串流處理器可以在子任務之間遞送串流;或合併;或串接子任務的串流。任何你能想像得到的處理方式。唯一必要的要求就是:串流處理器必須回傳一個串流。 1442 | 1443 | 串流處理器由執行環境中取得 "`tasks`" 屬性,子任務即是經由此屬性,以陣列的方式傳入。 1444 | 1445 | 當呼叫子任務時,串流處理器必須為子任務建立適當的執行環境。 1446 | 1447 | ``` javascript 1448 | module.exports = function () { 1449 | var gulp = this.gulp; 1450 | var config = this.config; 1451 | var tasks = this.tasks; 1452 | var context, stream; 1453 | 1454 | context = { 1455 | gulp: gulp, 1456 | // 傳入獲得的組態配置,以便將上層父任務動態插入的組態屬性傳遞給子任務 1457 | config: config 1458 | }; 1459 | // 如果需要的話,可以額外插入新的組態屬性 1460 | context.config.injectedValue = 'hello!'; 1461 | stream = tasks[0].call(context); 1462 | // ... 1463 | return stream; 1464 | }; 1465 | ``` 1466 | 1467 | 注意父任務可以動態給子任務插入新的組態屬性。只有新的值可以成功插入,若子任務原本就配置了同名的屬性,則新插入的屬性不會覆蓋原本的屬性。 1468 | 1469 | 如果要傳遞串流給子任務,串流處理器必須透過 "`upstream`" 屬性傳遞。 1470 | 1471 | ``` javascript 1472 | module.exports = function () { 1473 | var gulp = this.gulp; 1474 | var config = this.config; 1475 | var tasks = this.tasks; 1476 | var context, stream, i; 1477 | 1478 | context = { 1479 | gulp: gulp, 1480 | config: config 1481 | }; 1482 | stream = gulp.src(config.src.globs, config.src.options); 1483 | for (i = 0; i < tasks.length; ++i) { 1484 | context.upstream = stream; 1485 | stream = tasks[i].call(context); 1486 | } 1487 | return stream; 1488 | }; 1489 | ``` 1490 | 1491 | 如果串流處理器期望子任務回傳一個串流,然而子任務卻沒有,那麼此時串流處理器必須拋出一個錯誤。 1492 | 1493 | 注意:官方關於撰寫 gulp plugin 的 [指導方針](https://github.com/gulpjs/gulp/blob/4.0/docs/writing-a-plugin/guidelines.md) 中提到: "__不要在串流中拋出錯誤 (do not throw errors inside a stream)__"。 沒錯,你不應該在串流中拋出錯誤。但是在串流處理器中,如果不是位於處理串流的程式流程中,而是在處理流程之外,那麼,拋出錯誤是沒有問題的。 1494 | 1495 | 你可以使用 [gulp-ccr-stream-helper](https://github.com/gulp-cookery/gulp-ccr-stream-helper) 來協助呼叫子任務,並且檢查其是否正確回傳一個串流。 1496 | 1497 | 你可以從 [gulp-ccr-merge](https://github.com/gulp-cookery/gulp-ccr-merge) 以及 [gulp-ccr-queue](https://github.com/gulp-cookery/gulp-ccr-queue) 專案,參考串流處理器的實作。 1498 | 1499 | ### 撰寫流程控制器 1500 | 1501 | 流程控制器負責控制子任務的執行時機,順序等,而且並不關心子任務的輸出、入串流。 1502 | 1503 | 流程控制器沒有什麼特別的限制,唯一的規則是,流程控制器必須正確處理子任務的結束事件。譬如,子任務可以呼叫 "`done()`" 回呼函數;回傳一個串流或 Promise,等等。 1504 | 1505 | 你可以從 [gulp-ccr-parallel](https://github.com/gulp-cookery/gulp-ccr-parallel) 、 [gulp-ccr-series](https://github.com/gulp-cookery/gulp-ccr-series) 以及 [gulp-ccr-watch](https://github.com/gulp-cookery/gulp-ccr-watch) 專案,參考流程控制器的實作。 1506 | 1507 | ### 測試 Plugin 1508 | 1509 | 建議你可以先寫供專案使用的本地 recipe,完成之後,再轉換為 plugin。大多數的 recipe 測試都是資料導向的,如果你的 recipe 也是這樣,也許你可以考慮使用我的另一個專案: [mocha-cases](https://github.com/amobiz/mocha-cases) 。 1510 | 1511 | ## 任務專用屬性列表 (關鍵字) 1512 | 1513 | 以下的關鍵字保留給任務屬性使用,你不能使用這些關鍵字做為你的任務或屬性名稱。 1514 | 1515 | #### config 1516 | 1517 | 要傳遞給任務的組態配置。 1518 | 1519 | #### description 1520 | 1521 | 描述任務的工作內容。 1522 | 1523 | #### dest 1524 | 1525 | 要寫出檔案的路徑。定義在子任務中的路徑,預設會繼承父任務的定義的 dest 路徑。屬性值可以是字串,或者是如下的物件形式: `{ path: '', options: {} }` 。實際傳遞給任務的是後者的形式。 1526 | 1527 | #### name 1528 | 1529 | 任務名稱。通常會自動由組態項目名稱獲得。除非任務是定義在陣列中,而你仍然希望能夠在命令列中執行。 1530 | 1531 | #### order 1532 | 1533 | 任務的執行順序。只有在你以物件屬性的方式定義子任務時,又希望子任務能夠依序執行時才需要。數值僅用來排序,因此不需要是連續值。需要配合 "`series`" 屬性才能發揮作用。 1534 | 1535 | #### parallel 1536 | 1537 | 要求子任務以並行的方式同時執行。預設情形下,以物件屬性的方式定義的子任務才會並行執行。使用此關鍵字時,子任務不論是以陣列項目或物件屬性的方式定義,都將並行執行。 1538 | 1539 | #### plugin 1540 | 1541 | 要使用的原生 gulp plugin,可以是模組名稱或函數。 1542 | 1543 | #### recipe 1544 | 1545 | 任務所要對應的 recipe 模組名稱。預設與任務名稱 "`name`" 屬性相同。 1546 | 1547 | #### series 1548 | 1549 | 要求子任務以序列的方式逐一執行。預設情形下,以陣列項目的方式定義的子任務才會序列執行。使用此關鍵字時,子任務不論是以陣列項目或物件屬性的方式定義,都將序列執行。 1550 | 1551 | #### spit 1552 | 1553 | 要求任務寫出檔案。任務允許使用者決定要不要寫出檔案時才有作用。 1554 | 1555 | #### src 1556 | 1557 | 要讀入的檔案來源的路徑或檔案匹配表達式。由於預設會繼承父任務的 "`src`" 屬性,通常你會在父任務中定義路徑,在終端任務中才定義檔案匹配表達式。屬性值可以是任意合格的檔案匹配表達式,或由檔案匹配表達式組成的陣列,或者如下的物件形式: `{ globs: [], options: {} }` 呈現。實際傳遞給任務的是後者的形式。 1558 | 1559 | #### task 1560 | 1561 | 定義實際執行任務的方式。可以是普通函數的引用、內聯函數或對其它任務的參照。子任務如果以陣列的形式提供,子任務將以序列的順序執行,否則子任務將以並行的方式同時執行。 1562 | 1563 | #### visibility 1564 | 1565 | 任務的可見性。有效值為 `normal` 、 `hidden` 以及 `disabled` 。 1566 | 1567 | 1568 | ## 設定選項 1569 | 1570 | 設定選項可以改變 gulp-chef 的預設行為,以及用來定義自訂條件式組態配置的執行時期環境模式。 1571 | 1572 | 設定選項是經由 `chef()` 方法的第二個參數傳遞: 1573 | 1574 | ``` javascript 1575 | var config = { 1576 | }; 1577 | var settings = { 1578 | }; 1579 | var meals = chef(config, settings); 1580 | ``` 1581 | 1582 | ### settings.exposeWithPrefix 1583 | 1584 | 開關自動附加任務名稱功能。預設值為 `"auto"`,當發生名稱衝突時,gulp-chef 會自動為發生衝突的的任務,在前方附加父任務的名稱,像這樣:"`make:scripts:concat`"。你可以設定為 `true` 強制開啟。設定為 `false` 強制關閉,此時若遇到名稱衝突時,會拋出錯誤。 1585 | 1586 | ### settings.lookups 1587 | 1588 | 設定本地通用任務模組 (recipe) 的查找目錄。預設值為: 1589 | 1590 | ``` javascript 1591 | { 1592 | lookups: { 1593 | flows: 'flows', 1594 | streams: 'streams', 1595 | tasks: 'tasks' 1596 | } 1597 | } 1598 | ``` 1599 | 1600 | #### settings.lookups.flows 1601 | 1602 | 設定本地流程控制器的查找目錄。預設值為 `"flows"` 。 1603 | 1604 | #### settings.lookups.streams 1605 | 1606 | 設定本地串流處理器的查找目錄。預設值為 `"streams"` 。 1607 | 1608 | #### settings.lookups.tasks 1609 | 1610 | 設定本地通用任務的查找目錄。預設值為 `"tasks"` 。 1611 | 1612 | ### settings.plugins 1613 | 1614 | 傳遞給 "[gulp-load-plugins](https://github.com/jackfranklin/gulp-load-plugins)" 的選項。 1615 | Gulp-chef 使用 "gulp-load-plugins" 來載入共享任務模組,或稱為 "gulp-ccr" 模組。 1616 | 預設情形下,不是以 `"gulp-ccr"` 名稱開頭的共享任務模組將不會被載入。你可以透過更改 "`plugins`" 選項來載入這些模組。 1617 | 1618 | 預設選項為: 1619 | 1620 | ``` javascript 1621 | { 1622 | plugins: { 1623 | camelize: false, 1624 | config: process.cwd() + '/package.json', 1625 | pattern: ['gulp-ccr-*'], 1626 | replaceString: /^gulp[-.]ccr[-.]/g 1627 | } 1628 | } 1629 | ``` 1630 | 1631 | #### settings.plugins.DEBUG 1632 | 1633 | 當設定為 `true` 時,"gulp-load-plugins" 將輸出 log 訊息到 console。 1634 | 1635 | #### settings.plugins.camelize 1636 | 1637 | 若設定為 `true`,使用 `"-"` 連接的名稱將被改為駝峰形式。 1638 | 1639 | #### settings.plugins.config 1640 | 1641 | 由何處查找共享任務模組的資訊。預設為專案的 package.json。 1642 | 1643 | #### settings.plugins.pattern 1644 | 1645 | 共享任務模組的路徑匹配表達式 (glob)。預設為 `"gulp-ccr-*"`。 1646 | 1647 | #### settings.plugins.scope 1648 | 1649 | 要查找哪些相依範圍。預設為: 1650 | 1651 | ``` javascript 1652 | ['dependencies', 'devDependencies', 'peerDependencies']. 1653 | ``` 1654 | 1655 | #### settings.plugins.replaceString 1656 | 1657 | 要移除的模組附加名稱。預設為: `/^gulp[-.]ccr[-.]/g` 。 1658 | 1659 | #### settings.plugins.lazy 1660 | 1661 | 是否延遲載入模組。預設為 `true`。 1662 | 1663 | #### settings.plugins.rename 1664 | 1665 | 指定改名。必須為 hash 物件。鍵為原始名稱,值為改名名稱。 1666 | 1667 | #### settings.plugins.renameFn 1668 | 1669 | 改名函數。 1670 | 1671 | ### settings.modes 1672 | 1673 | 定義自訂條件式組態配置的執行時期環境模式。 1674 | 1675 | 除了 `default` 屬性是用來指定預設模式之外,其餘的屬性名稱定義新的模式,而值必須是陣列,陣列的項目是可用於組態配置及命令列的識別字代號。注意不要使用到保留給任務使用的[關鍵字](#keywords)。預設為: 1676 | 1677 | ``` javascript 1678 | { 1679 | modes: { 1680 | production: ['production', 'prod'], 1681 | development: ['development', 'dev'], 1682 | staging: ['staging'], 1683 | default: 'production' 1684 | } 1685 | } 1686 | ``` 1687 | 1688 | 1689 | ## 命令列選項列表 1690 | 1691 | ### --task 1692 | 1693 | 查詢任務並顯示其工作內容說明以及組態配置內容。 1694 | 1695 | ``` bash 1696 | $ gulp --task 1697 | ``` 1698 | 1699 | ### --recipe 1700 | 1701 | 列舉可用的 recipe,包含內建的 recipe、本地的 recipe 以及已安裝的 plugin 。 1702 | 1703 | 你可以任意使用 "`--recipes`" 、 "`--recipe`" 以及 "`--r`" 。 1704 | 1705 | ``` bash 1706 | $ gulp --recipes 1707 | ``` 1708 | 1709 | 查詢指定 recipe,顯示其用途說明,以及,如果有定義的話,顯示其[組態架構](#configuration-schema)。 1710 | 1711 | ``` bash 1712 | $ gulp --recipe 1713 | ``` 1714 | 1715 | ## 專案建制與貢獻 1716 | 1717 | ``` bash 1718 | $ git clone https://github.com/gulp-cookery/gulp-chef.git 1719 | $ cd gulp-chef 1720 | $ npm install 1721 | ``` 1722 | 1723 | ## 問題提報 1724 | 1725 | [Issues](https://github.com/gulp-cookery/gulp-chef/issues) 1726 | 1727 | ## 測試 1728 | 1729 | 測試是以 [mocha](https://mochajs.org/) 撰寫,請在命令列下執行下列指令: 1730 | 1731 | ``` bash 1732 | $ npm test 1733 | ``` 1734 | 1735 | ## 授權 1736 | 1737 | [MIT](http://opensource.org/licenses/MIT) 1738 | 1739 | ## 作者 1740 | 1741 | [Amobiz](https://github.com/amobiz) 1742 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var mocha = require('gulp-mocha'); 5 | 6 | function test() { 7 | return gulp.src(['test/**/*_test.js'], { 8 | read: false 9 | }) 10 | .pipe(mocha({ 11 | reporter: 'spec', 12 | timeout: Infinity 13 | })); 14 | } 15 | 16 | gulp.task(test); 17 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/configure'); 4 | -------------------------------------------------------------------------------- /lib/cli/exit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Fix stdout truncation on windows 4 | function exit(code) { 5 | if (process.platform === 'win32' && process.stdout.bufferSize) { 6 | process.stdout.once('drain', function () { 7 | process.exit(code); 8 | }); 9 | return; 10 | } 11 | process.exit(code); 12 | } 13 | 14 | module.exports = exit; 15 | -------------------------------------------------------------------------------- /lib/cli/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var yargs = require('yargs'); 4 | 5 | var recipes = require('./recipes'); 6 | var tasks = require('./tasks'); 7 | var exit = require('./exit'); 8 | 9 | var actions = { 10 | recipes: { 11 | desc: 'Print the available recipes or information of a recipe for the project.', 12 | alias: ['recipe', 'r'], 13 | fn: recipes, 14 | type: 'string', 15 | phase: 'preConfigure', 16 | exit: 0 17 | }, 18 | task: { 19 | desc: 'Print the configuration of a task for the project.', 20 | fn: tasks, 21 | type: 'string', 22 | phase: 'postRegister' 23 | } 24 | }; 25 | 26 | module.exports = function (argv, phase) { 27 | var alias = _alias(); 28 | var acts = yargs(argv) 29 | .alias(alias) 30 | .argv; 31 | 32 | Object.keys(acts).forEach(function (key) { 33 | var action = actions[key]; 34 | 35 | if (action) { 36 | phase[action.phase](function (event) { 37 | action.fn(event, acts[key]); 38 | if ('exit' in action) { 39 | exit(action.exit); 40 | } 41 | }); 42 | } 43 | }); 44 | 45 | function _alias() { 46 | return Object.keys(actions).reduce(function (ret, key) { 47 | if (actions[key].alias) { 48 | ret[key] = actions[key].alias; 49 | } 50 | return ret; 51 | }, {}); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /lib/cli/recipes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var chalk = require('chalk'); 5 | var regulate = require('json-regulator'); 6 | var deref = require('json-normalizer/lib/deref').sync; 7 | 8 | function detail(stuff, name) { 9 | var recipe; 10 | 11 | recipe = stuff.tasks.lookup(name) || stuff.streams.lookup(name) || stuff.flows.lookup(name); 12 | if (recipe) { 13 | return _schema() || _simple(); 14 | } 15 | return 'The recipe "' + name + '" not found.'; 16 | 17 | function _schema() { 18 | var schema; 19 | 20 | if (recipe.schema) { 21 | schema = deref(recipe.schema); 22 | // NOTE: requires json-regulator options.overwrite: false. 23 | // Because json-schema can override extended schema. 24 | // so schemas from 'extends' should not overwrite referer schema. 25 | // NOTE: it is possible there is circular references, and can cause stack overflow problems. 26 | // If this is the situation, remove this call, and just display raw schema. 27 | schema = regulate(schema, ['extends'], ['definitions'], { overwrite: false }); 28 | return JSON.stringify(schema, null, ' '); 29 | } 30 | } 31 | 32 | function _simple() { 33 | return _name() + '\n' + chalk.gray(_desc()); 34 | } 35 | 36 | function _name() { 37 | return recipe.schema && recipe.schema.title || recipe.displayName || recipe.name; 38 | } 39 | 40 | function _desc() { 41 | return recipe.schema && recipe.schema.description || recipe.description; 42 | } 43 | } 44 | 45 | function list(stuff) { 46 | var recipes; 47 | 48 | recipes = _.defaults({}, stuff.tasks.recipes, stuff.streams.recipes, stuff.flows.recipes); 49 | recipes = Object.keys(recipes).sort().join(', '); 50 | return 'Available recipes:\n' + recipes; 51 | } 52 | 53 | module.exports = function (stuff, name) { 54 | var message; 55 | 56 | if (typeof name === 'string') { 57 | message = detail(stuff, name); 58 | } else { 59 | message = list(stuff); 60 | } 61 | console.log(message); 62 | }; 63 | -------------------------------------------------------------------------------- /lib/cli/tasks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chalk = require('chalk'); 4 | var exit = require('./exit'); 5 | 6 | var MSG_NO_DESCRIPTION = '(no description)'; 7 | 8 | module.exports = function (registry, name) { 9 | var gulp = registry.gulp; 10 | 11 | if (typeof name === 'string') { 12 | console.log(detail()); 13 | exit(0); 14 | } 15 | 16 | function detail() { 17 | var task; 18 | 19 | task = gulp.task(name); 20 | if (task) { 21 | return name + _recipe() + ':' + chalk.gray(_desc(task)) + _config(); 22 | } 23 | return 'The task "' + name + '" not found.'; 24 | 25 | function _recipe() { 26 | if (task.recipe) { 27 | return '(' + task.recipe + ')'; 28 | } 29 | return ''; 30 | } 31 | 32 | function _config() { 33 | if (task.config) { 34 | return '\n' + JSON.stringify(task.config, null, ' '); 35 | } 36 | return ''; 37 | } 38 | } 39 | 40 | function list() { 41 | // NOTE: 42 | // in gulp 3.X, tasks can be accessed through gulp.tasks 43 | // in gulp 4.X, must through gulp.registry().tasks() 44 | var tasks = gulp.tasks || gulp.registry().tasks(); 45 | 46 | return Object.keys(tasks).sort().reduce(function (message, key) { 47 | var task; 48 | 49 | task = tasks[key]; 50 | message.push(key); 51 | // NOTE: in gulp 3.X task are stored in task.fn, and we store description in fn. 52 | message.push(_desc(task)); 53 | }, []).join('\n'); 54 | } 55 | 56 | function _desc(task) { 57 | return ' ' + (task.description || (task.fn && task.fn.description) || MSG_NO_DESCRIPTION); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /lib/configuration/defaults.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | // hack for _.defaultsDeep() that: 6 | // defaultsDeep() try to mix array items into object 7 | // defaultsDeep() try to mix string characters into array 8 | // https://github.com/lodash/lodash/issues/1560 9 | _.defaultsDeep = defaultsDeep; 10 | 11 | function defaultsDeep(object) { 12 | var sources = Array.prototype.slice.call(arguments, 1); 13 | 14 | sources.forEach(function (source) { 15 | _defaults(object, source); 16 | }); 17 | return object; 18 | 19 | function _defaults(target, source) { 20 | _.forIn(source, function (value, key) { 21 | if (_.isPlainObject(target[key]) && _.isPlainObject(value)) { 22 | _defaults(target[key], value); 23 | } else if (!(key in target)) { 24 | target[key] = typeof value === 'function' ? value : _.cloneDeep(value); 25 | } 26 | }); 27 | } 28 | } 29 | 30 | module.exports = defaultsDeep; 31 | -------------------------------------------------------------------------------- /lib/configuration/glob.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var normalize = require('json-normalizer').sync; 4 | 5 | /* 6 | * NOTE: 7 | * According to [gulp API](https://github.com/gulpjs/gulp/blob/master/docs/API.md) 8 | * and [gulp 4.0 API](https://github.com/gulpjs/gulp/blob/4.0/docs/API.md): 9 | * gulp supports all options supported by node-glob and glob-stream except ignore and adds the following options. 10 | * 11 | * @see 12 | * [node-glob](https://github.com/isaacs/node-glob) 13 | * [glob-stream](https://github.com/gulpjs/glob-stream) 14 | * 15 | */ 16 | var SCHEMA_GLOB = require('../schema/glob.json'); 17 | 18 | function glob(values) { 19 | return normalize(SCHEMA_GLOB, values); 20 | } 21 | 22 | module.exports = glob; 23 | module.exports.SCHEMA = SCHEMA_GLOB; 24 | -------------------------------------------------------------------------------- /lib/configuration/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | realize: require('./realize'), 5 | sort: require('./sort') 6 | }; 7 | -------------------------------------------------------------------------------- /lib/configuration/path.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var normalize = require('json-normalizer').sync; 4 | 5 | var SCHEMA_PATH = require('../schema/path.json'); 6 | 7 | 8 | function path(values) { 9 | return normalize(SCHEMA_PATH, values); 10 | } 11 | 12 | module.exports = path; 13 | module.exports.SCHEMA = SCHEMA_PATH; 14 | -------------------------------------------------------------------------------- /lib/configuration/realize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var defaults = require('./defaults'); 5 | 6 | var INTERPOLATE = /{{([\s\S]+?)}}/g; 7 | 8 | function realize(original, additional) { 9 | var values; 10 | 11 | values = defaults({}, original, additional); 12 | return realizeAll({}, values); 13 | 14 | function realizeAll(target, source) { 15 | _.each(source, function (value, name) { 16 | target[name] = _realize(value); 17 | }); 18 | return target; 19 | } 20 | 21 | function _realize(source) { 22 | if (typeof source === 'string') { 23 | return source.replace(INTERPOLATE, function (match, path) { 24 | var value; 25 | 26 | value = _.get(values, path) || path; 27 | if (typeof value === 'function') { 28 | value = value(values); 29 | } 30 | return value; 31 | }); 32 | } 33 | if (typeof source === 'function') { 34 | return source(values); 35 | } 36 | if (_.isArray(source)) { 37 | return realizeAll([], source); 38 | } 39 | if (_.isPlainObject(source)) { 40 | return realizeAll({}, source); 41 | } 42 | return source; 43 | } 44 | } 45 | 46 | module.exports = realize; 47 | -------------------------------------------------------------------------------- /lib/configuration/sort.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Path = require('path'); 4 | var _ = require('lodash'); 5 | var normalize = require('json-normalizer').sync; 6 | var regulate = require('json-regulator'); 7 | var globsJoin = require('../helpers/globs').join; 8 | var from = require('../helpers/dataflow'); 9 | 10 | var glob = require('./glob'); 11 | var path = require('./path'); 12 | var defaults = require('./defaults'); 13 | 14 | var PATTERN_PROPERTY_REGEX = /^\$(.*)$/; 15 | 16 | var SCHEMA_DEFAULTS = { 17 | type: 'object', 18 | properties: { 19 | src: glob.SCHEMA, 20 | dest: path.SCHEMA, 21 | config: { 22 | description: 'Any property insides "config" property considered a configuration property.', 23 | type: 'object', 24 | additionalProperties: true 25 | }, 26 | options: { 27 | type: 'object', 28 | additionalProperties: true 29 | } 30 | }, 31 | patternProperties: { 32 | '^\\$.*$': { 33 | description: 'Any property prefixed with $ considered a configuration property and can be accessed both with or without $ prefix.' 34 | } 35 | }, 36 | additionalProperties: false 37 | }; 38 | 39 | var SCHEMA_TASK = require('../schema/task.json'); 40 | 41 | var TASK_SCHEMA_MAPPINGS = { 42 | title: 'name', 43 | description: 'description' 44 | }; 45 | 46 | function resolveSrc(parent, value, property) { 47 | var join; 48 | 49 | if (value) { 50 | if (value.options && 'join' in value.options) { 51 | if (value.options.join) { 52 | join = parent[value.options.join]; 53 | } 54 | delete value.options.join; 55 | if (_.size(value.options) === 0) { 56 | delete value.options; 57 | } 58 | } else { 59 | join = property && parent[property]; 60 | } 61 | if (join) { 62 | value.globs = globsJoin(join.globs || join.path || join, value.globs); 63 | } 64 | return value; 65 | } 66 | return parent.src; 67 | } 68 | 69 | function resolveDest(parent, value, property) { 70 | var join; 71 | 72 | if (value) { 73 | if (value.options && 'join' in value.options) { 74 | if (value.options.join) { 75 | join = parent[value.options.join]; 76 | } 77 | delete value.options.join; 78 | if (_.size(value.options) === 0) { 79 | delete value.options; 80 | } 81 | } else { 82 | join = property && parent[property]; 83 | } 84 | if (join) { 85 | value.path = Path.join(join.path || (join.globs && join.globs[0]) || join, value.path); 86 | } 87 | return value; 88 | } 89 | return parent.dest; 90 | } 91 | 92 | function renamePatternProperties(target) { 93 | Object.keys(target).forEach(function (key) { 94 | var match; 95 | 96 | match = PATTERN_PROPERTY_REGEX.exec(key); 97 | if (match && match[1].length) { 98 | target[match[1]] = target[key]; 99 | delete target[key]; 100 | } 101 | }); 102 | return target; 103 | } 104 | 105 | /** 106 | * If both parentConfig and taskConfig specified src property 107 | * then try to join paths. 108 | */ 109 | function sort(taskInfo, rawConfig, parentConfig, optionalSchema) { 110 | var schema, subTaskConfigs, taskConfig, value; 111 | 112 | schema = optionalSchema || {}; 113 | from(schema).to(taskInfo).imply(TASK_SCHEMA_MAPPINGS); 114 | schema = defaults({}, schema, SCHEMA_DEFAULTS); 115 | 116 | taskConfig = rawConfig; 117 | // NOTE: If schema provided, try to normalize properties inside 'config' property. 118 | if (optionalSchema) { 119 | taskConfig = regulate(taskConfig, ['config']); 120 | } 121 | taskConfig = normalize(schema, taskConfig, { 122 | before: before, 123 | after: after 124 | }); 125 | taskConfig = renamePatternProperties(taskConfig); 126 | // NOTE: A thought about that `config` should be "normalized". 127 | // But remember that the `config` and `$` property prefix are designed for tasks that have no schemas. 128 | // It just won't do anything try to normalize it without schema. 129 | taskConfig = regulate(taskConfig, ['config']); 130 | 131 | // NOTE: When there is `plugin`, `task`, `series` or `parallel` property, 132 | // then all other properties will be treated as properties, not sub-task configs. 133 | // So user don't have to use `config` keyword or `$` prefix. 134 | value = _.omit(rawConfig, Object.keys(taskConfig).concat('config')); 135 | if (!optionalSchema && (taskInfo.plugin || taskInfo.task || taskInfo.series || taskInfo.parallel)) { 136 | taskConfig = defaults(taskConfig, value); 137 | } else { 138 | subTaskConfigs = value; 139 | } 140 | 141 | // inherit parent's config 142 | taskConfig = defaults(taskConfig, parentConfig); 143 | 144 | return { 145 | taskInfo: taskInfo, 146 | taskConfig: taskConfig, 147 | subTaskConfigs: subTaskConfigs 148 | }; 149 | 150 | function before(propSchema, values) { 151 | if (propSchema.type === 'glob') { 152 | _.defaults(propSchema, glob.SCHEMA); 153 | } else if (propSchema.type === 'path') { 154 | _.defaults(propSchema, path.SCHEMA); 155 | } 156 | } 157 | 158 | function after(propSchema, resolved) { 159 | var value, join; 160 | 161 | if (resolved) { 162 | value = resolved(); 163 | 164 | if (propSchema.type === 'glob') { 165 | join = propSchema.properties.options.properties.join.default || 'src'; 166 | return resolve(resolveSrc(parentConfig, value, join)); 167 | } else if (propSchema.type === 'path') { 168 | join = propSchema.properties.options.properties.join.default || 'dest'; 169 | return resolve(resolveDest(parentConfig, value, join)); 170 | } 171 | } 172 | 173 | return resolved; 174 | 175 | function resolve(value) { 176 | return function () { 177 | return value; 178 | }; 179 | } 180 | } 181 | } 182 | 183 | module.exports = sort; 184 | -------------------------------------------------------------------------------- /lib/configure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ConfigurableRecipeFactory = require('./recipe/factory'); 4 | 5 | var ConfigurableTaskFactory = require('./task/factory'); 6 | var ConfigurableTaskRegistry = require('./task/registry'); 7 | var expose = require('./task/expose'); 8 | 9 | var ConfigurationRegulator = require('./regulator'); 10 | var Configuration = require('./configuration'); 11 | 12 | var loadStuff = require('./stuff'); 13 | 14 | var Settings = require('./helpers/settings'); 15 | var observable = require('./helpers/observable'); 16 | 17 | var cli = require('./cli'); 18 | 19 | var log = require('gulplog'); 20 | 21 | var phase = { 22 | preConfigure: observable(), 23 | postConfigure: observable(), 24 | postRegister: observable() 25 | }; 26 | 27 | function configure(settings, stuff, registry, rawConfigs) { 28 | var regulator = new ConfigurationRegulator(settings.get('modes')); 29 | var configs = regulator.regulate(rawConfigs); 30 | 31 | var recipes = new ConfigurableRecipeFactory(stuff, registry); 32 | var tasks = new ConfigurableTaskFactory(recipes, registry, expose(registry, settings)); 33 | 34 | configs = Configuration.sort({}, configs, {}); 35 | tasks.multiple('', configs.subTaskConfigs, configs.taskConfig); 36 | 37 | return registry; 38 | } 39 | 40 | function run(rawConfigs, optionalSettings) { 41 | var settings, stuff, registry; 42 | 43 | settings = new Settings(optionalSettings); 44 | stuff = loadStuff(settings); 45 | 46 | cli(process.argv.slice(2), phase); 47 | 48 | phase.postRegister(report); 49 | phase.preConfigure.notify(stuff); 50 | registry = new ConfigurableTaskRegistry(phase.postRegister.notify); 51 | configure(settings, stuff, registry, rawConfigs); 52 | phase.postConfigure.notify(registry); 53 | return registry; 54 | 55 | function report() { 56 | var missing; 57 | 58 | missing = registry.missing(); 59 | if (missing.length) { 60 | log.error('Error:', 'missing task reference: ' + missing.join(', ')); 61 | } 62 | } 63 | } 64 | 65 | module.exports = run; 66 | -------------------------------------------------------------------------------- /lib/helpers/dataflow.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function from(source) { 4 | return { 5 | to: function (target) { 6 | return { 7 | imply: function (mappings, overwrite) { 8 | Object.keys(mappings).forEach(function (sourceProperty) { 9 | var targetProperty; 10 | 11 | if (source.hasOwnProperty(sourceProperty)) { 12 | targetProperty = mappings[sourceProperty]; 13 | if (overwrite || !target.hasOwnProperty(targetProperty)) { 14 | target[targetProperty] = source[sourceProperty]; 15 | } 16 | } 17 | }); 18 | return target; 19 | }, 20 | move: function (properties, overwrite) { 21 | properties.forEach(function (name) { 22 | if (source.hasOwnProperty(name)) { 23 | if (overwrite || !target.hasOwnProperty(name)) { 24 | target[name] = source[name]; 25 | } 26 | delete source[name]; 27 | } 28 | }); 29 | return target; 30 | } 31 | }; 32 | } 33 | }; 34 | } 35 | 36 | module.exports = from; 37 | -------------------------------------------------------------------------------- /lib/helpers/globs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var FileSystem = require('fs'); 4 | var glob = require('glob'); 5 | var globjoin = require('globjoin'); 6 | var globby = require('globby'); 7 | 8 | // glob support in src: 9 | function folders(globs, options) { 10 | return globby.sync(globs, options || {}).filter(isDirectory); 11 | } 12 | 13 | function isDirectory(path) { 14 | try { 15 | return FileSystem.statSync(path).isDirectory(); 16 | } catch (ex) { 17 | return false; 18 | } 19 | } 20 | 21 | exports.folders = folders; 22 | exports.isDirectory = isDirectory; 23 | exports.join = globjoin; 24 | exports.isGlob = glob.hasMagic; 25 | -------------------------------------------------------------------------------- /lib/helpers/observable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function observable() { 4 | var listeners = []; 5 | 6 | listen.notify = notify; 7 | return listen; 8 | 9 | function listen(listener) { 10 | listeners.push(listener); 11 | } 12 | 13 | function notify(event) { 14 | listeners.forEach(function (listener) { 15 | listener(event); 16 | }); 17 | } 18 | } 19 | 20 | module.exports = observable; 21 | -------------------------------------------------------------------------------- /lib/helpers/safe_require_dir.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var requireDir = require('require-dir'); 5 | var _ = require('lodash'); 6 | 7 | function safeRequireDir() { 8 | var dirs, modules; 9 | 10 | var parent = module.parent; 11 | var parentFile = parent.filename; 12 | var parentDir = path.dirname(parentFile); 13 | 14 | dirs = Array.prototype.slice.call(arguments, 0); 15 | modules = dirs.map(function (dir) { 16 | try { 17 | return requireDir(path.resolve(parentDir, dir || '.')); 18 | } catch (ex) { 19 | if (ex.code !== 'ENOENT') { 20 | throw ex; 21 | } 22 | } 23 | return {}; 24 | }); 25 | return _.defaults.apply(null, modules); 26 | } 27 | 28 | module.exports = safeRequireDir; 29 | -------------------------------------------------------------------------------- /lib/helpers/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | function Settings(settings) { 6 | this.set(settings); 7 | } 8 | 9 | Settings.prototype.defaults = function (defaults) { 10 | this.settings = _.defaultsDeep({}, this.settings, defaults); 11 | return this; 12 | }; 13 | 14 | Settings.prototype.set = function (settings) { 15 | this.settings = _.defaultsDeep({}, settings, this.settings); 16 | return this; 17 | }; 18 | 19 | Settings.prototype.get = function (name) { 20 | if (name) { 21 | return this.settings[name]; 22 | } 23 | return this.settings; 24 | }; 25 | 26 | module.exports = Settings; 27 | -------------------------------------------------------------------------------- /lib/recipe/factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var PluginError = require('gulp-util').PluginError; 4 | 5 | /** 6 | * A ConfigurableRecipeFactory lookups or creates recipe function of the signature: `function (done)`. 7 | * 8 | * @param stuff 9 | * @constructor 10 | */ 11 | function ConfigurableRecipeFactory(stuff, registry) { 12 | this.stuff = stuff; 13 | this.registry = registry; 14 | } 15 | 16 | ConfigurableRecipeFactory.prototype.flow = function (taskInfo) { 17 | return lookup(this.stuff.flows, taskInfo); 18 | }; 19 | 20 | ConfigurableRecipeFactory.prototype.inline = function (taskInfo) { 21 | var task; 22 | 23 | task = taskInfo.parallel || taskInfo.series || taskInfo.task; 24 | if (typeof task === 'function') { 25 | return task; 26 | } 27 | }; 28 | 29 | ConfigurableRecipeFactory.prototype.noop = function () { 30 | return function (done) { 31 | done(); 32 | }; 33 | }; 34 | 35 | ConfigurableRecipeFactory.prototype.plugin = function (taskInfo) { 36 | var plugin, type; 37 | 38 | type = typeof taskInfo.plugin; 39 | if (type === 'string' || type === 'function') { 40 | plugin = taskInfo.plugin; 41 | return runner; 42 | } 43 | 44 | function runner() { 45 | var gulp = this.gulp; 46 | var config = this.config; 47 | var stream = this.upstream || gulp.src(config.src.globs, config.src.options); 48 | 49 | _load(); 50 | 51 | return stream.pipe(plugin(config.options)); 52 | } 53 | 54 | function _load() { 55 | if (typeof plugin === 'string') { 56 | try { 57 | plugin = require(plugin); 58 | } catch (ex) { 59 | throw new PluginError('can not load plugin'); 60 | } 61 | } 62 | } 63 | }; 64 | 65 | ConfigurableRecipeFactory.prototype.reference = function (taskInfo, resolved) { 66 | var registry, name, result; 67 | 68 | registry = this.registry; 69 | name = taskInfo.parallel || taskInfo.series || taskInfo.task; 70 | if (typeof name === 'string') { 71 | result = resolve() || pending(); 72 | result.displayName = name; 73 | result.description = taskInfo.description; 74 | return result; 75 | } 76 | 77 | function resolve() { 78 | var task, wrapper; 79 | 80 | task = registry.refer(name, resolved); 81 | if (task) { 82 | wrapper = function (done) { 83 | return task.call(this, done); 84 | }; 85 | wrapper.config = task.config; 86 | return wrapper; 87 | } 88 | } 89 | 90 | function pending() { 91 | return function (done) { 92 | var task; 93 | 94 | task = this.gulp.task(name); 95 | if (typeof task !== 'function') { 96 | throw new PluginError(__filename, 'referring task not found: ' + name); 97 | } 98 | return task.call(this, done); 99 | }; 100 | } 101 | }; 102 | 103 | /** 104 | * if there is configurations not being consumed, then treat them as sub-tasks. 105 | */ 106 | ConfigurableRecipeFactory.prototype.stream = function (taskInfo) { 107 | return lookup(this.stuff.streams, taskInfo); 108 | }; 109 | 110 | /** 111 | * if there is a matching recipe, use it and ignore any sub-configs. 112 | */ 113 | ConfigurableRecipeFactory.prototype.task = function (taskInfo) { 114 | return lookup(this.stuff.tasks, taskInfo); 115 | }; 116 | 117 | function lookup(registry, taskInfo) { 118 | var name, recipe; 119 | 120 | name = taskInfo.recipe || taskInfo.name; 121 | recipe = registry.lookup(name); 122 | if (recipe) { 123 | taskInfo.recipe = name; 124 | taskInfo.recipeInstance = recipe; 125 | } 126 | return recipe; 127 | } 128 | 129 | module.exports = ConfigurableRecipeFactory; 130 | -------------------------------------------------------------------------------- /lib/recipe/registry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Path = require('path'); 4 | var _ = require('lodash'); 5 | var loadPlugins = require('gulp-load-plugins'); 6 | var safeRequireDir = require('../helpers/safe_require_dir'); 7 | var log = require('gulplog'); 8 | 9 | var _replaceString = /^gulp[-.]ccr[-.]/g; 10 | 11 | function ConfigurableRecipeRegistry(recipes) { 12 | this.recipes = recipes; 13 | } 14 | 15 | ConfigurableRecipeRegistry.prototype.size = function () { 16 | return _.size(this.recipes); 17 | }; 18 | 19 | ConfigurableRecipeRegistry.prototype.lookup = function (name) { 20 | return this.recipes[name]; 21 | }; 22 | 23 | ConfigurableRecipeRegistry.replaceString = _replaceString; 24 | 25 | ConfigurableRecipeRegistry.builder = function (type) { 26 | var recipes = {}; 27 | 28 | return { 29 | dir: function (base, path) { 30 | var sources; 31 | 32 | sources = safeRequireDir(Path.join(base, path)); 33 | _.defaults(recipes, sources); 34 | return this; 35 | }, 36 | npm: function (options) { 37 | var sources; 38 | 39 | sources = loadPlugins(options); 40 | lazyDefaults(recipes, sources, type); 41 | return this; 42 | }, 43 | require: function (moduleName) { 44 | var name; 45 | 46 | name = moduleName.replace(_replaceString, ''); 47 | if (!(name in recipes)) { 48 | try { 49 | recipes[name] = require(moduleName); 50 | } catch (ex) { 51 | log.error('gulp-chef: recipe-registry:', 'error loading module: ' + moduleName); 52 | } 53 | } 54 | return this; 55 | }, 56 | build: function () { 57 | return new ConfigurableRecipeRegistry(recipes); 58 | } 59 | }; 60 | }; 61 | 62 | function lazyDefaults(target, source, type) { 63 | var properties; 64 | 65 | properties = Object.getOwnPropertyNames(source); 66 | properties.forEach(function (property) { 67 | if (!target.hasOwnProperty(property)) { 68 | assign(property); 69 | } 70 | }); 71 | 72 | function assign(property) { 73 | var descriptor; 74 | 75 | descriptor = Object.getOwnPropertyDescriptor(source, property); 76 | if (descriptor.get) { 77 | Object.defineProperty(target, property, { 78 | enumerable: true, 79 | get: function () { 80 | var inst; 81 | 82 | inst = descriptor.get(); 83 | if (inst.type === type) { 84 | return inst; 85 | } 86 | } 87 | }); 88 | } else { 89 | target[property] = source[property]; 90 | } 91 | } 92 | } 93 | 94 | module.exports = ConfigurableRecipeRegistry; 95 | -------------------------------------------------------------------------------- /lib/regulator.js: -------------------------------------------------------------------------------- 1 | /* eslint no-process-env: 0 */ 2 | 'use strict'; 3 | 4 | var regulate = require('json-regulator'); 5 | var cli = require('gulp-util').env; 6 | var env = process.env.NODE_ENV; 7 | 8 | var MODES = { 9 | production: ['production', 'prod'], 10 | development: ['development', 'dev'], 11 | staging: ['staging'], 12 | default: 'production' 13 | }; 14 | 15 | var SOURCES = [ 16 | function _cli(key) { 17 | if (key in cli) { 18 | return true; 19 | } 20 | }, 21 | function _env(key) { 22 | return env === key; 23 | } 24 | ]; 25 | 26 | function ConfigurationRegulator(optionalModes, optionalProvider) { 27 | var keys, mode, modes, provider; 28 | 29 | this.modes = modes = optionalModes || MODES; 30 | provider = optionalProvider || this.mode.bind(this); 31 | 32 | keys = Object.keys(modes); 33 | mode = provider(modes); 34 | this.promotions = modes[mode]; 35 | this.eliminations = keys.reduce(function (result, key) { 36 | if (key === mode || key === 'default') { 37 | return result; 38 | } 39 | return result.concat(modes[key]); 40 | }, []); 41 | } 42 | 43 | ConfigurationRegulator.prototype.regulate = function (configurations) { 44 | return regulate(configurations, this.promotions, this.eliminations); 45 | }; 46 | 47 | ConfigurationRegulator.prototype.mode = function () { 48 | return ConfigurationRegulator.mode(SOURCES, this.modes) || this.modes.default || Object.keys(this.modes)[0]; 49 | }; 50 | 51 | ConfigurationRegulator.mode = function (sources, modes) { 52 | var names; 53 | 54 | names = Object.keys(modes); 55 | return any(sources, match); 56 | 57 | function match(source) { 58 | return any(names, function (name) { 59 | var keys; 60 | 61 | keys = modes[name]; 62 | return any(keys, function (key) { 63 | if (source(key)) { 64 | return name; 65 | } 66 | }); 67 | }); 68 | } 69 | }; 70 | 71 | function any(values, fn) { 72 | var value, i, n; 73 | 74 | for (i = 0, n = values.length; i < n; ++i) { 75 | value = fn(values[i]); 76 | if (value) { 77 | return value; 78 | } 79 | } 80 | } 81 | 82 | module.exports = ConfigurationRegulator; 83 | -------------------------------------------------------------------------------- /lib/schema/glob.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "glob", 3 | "properties": { 4 | "globs": { 5 | "description": "Glob or array of globs to read.", 6 | "type": "array", 7 | "items": { 8 | "type": "string" 9 | }, 10 | "alias": ["glob"] 11 | }, 12 | "options": { 13 | "description": "Options to pass to node-glob through glob-stream.", 14 | "properties": { 15 | "allowEmpty": { 16 | "description": "If true, won't emit an error when a glob pointing at a single file fails to match.", 17 | "type": "boolean", 18 | "default": false 19 | }, 20 | "base": { 21 | "description": "Used for relative pathing. Typically where a glob starts.", 22 | "type": "string" 23 | }, 24 | "buffer": { 25 | "description": "Setting this to false will return file.contents as a stream and not buffer files. This is useful when working with large files.", 26 | "type": "boolean", 27 | "default": true 28 | }, 29 | "cache": { 30 | "description": "Pass in a previously generated cache object to save some fs calls.", 31 | "type": "object" 32 | }, 33 | "cwd": { 34 | "description": "cwd for the input folder, only has an effect if provided input folder is relative. Default is process.cwd().", 35 | "type": "string" 36 | }, 37 | "cwdbase": { 38 | "description": "Setting this to true is the same as saying opt.base = opt.cwd.", 39 | "type": "boolean", 40 | "default": false 41 | }, 42 | "debug": { 43 | "description": "Set to enable debug logging in minimatch and glob.", 44 | "type": "boolean", 45 | "default": false 46 | }, 47 | "dot": { 48 | "description": "Setting this to true to include .dot files in normal matches and globstar matches.", 49 | "type": "boolean", 50 | "default": false 51 | }, 52 | "follow": { 53 | "description": "Follow symlinked directories when expanding ** patterns. Note that this can result in a lot of duplicate references in the presence of cyclic links.", 54 | "type": "boolean", 55 | "default": false 56 | }, 57 | "ignore": { 58 | "description": "Add a pattern or an array of glob patterns to exclude matches. Note: ignore patterns are always in dot:true mode, regardless of any other settings.", 59 | "type": ["string", "array"] 60 | }, 61 | "mark": { 62 | "description": "Add a / character to directory matches. Note that this requires additional stat calls.", 63 | "type": "boolean", 64 | "default": false 65 | }, 66 | "matchBase": { 67 | "description": "Perform a basename-only match if the pattern does not contain any slash characters. That is, *.js would be treated as equivalent to **/*.js, matching all js files in all directories.", 68 | "type": "boolean", 69 | "default": false 70 | }, 71 | "nobrace": { 72 | "description": "Do not expand {a,b} and {1..3} brace sets.", 73 | "type": "boolean", 74 | "default": false 75 | }, 76 | "nocase": { 77 | "description": "Perform a case-insensitive match. Note: on case-insensitive filesystems, non-magic patterns will match by default, since stat and readdir will not raise errors.", 78 | "type": "boolean", 79 | "default": false 80 | }, 81 | "nodir": { 82 | "description": "Do not match directories, only files. (Note: to match only directories, simply put a / at the end of the pattern.)", 83 | "type": "boolean", 84 | "default": false 85 | }, 86 | "noext": { 87 | "description": "Do not match +(a|b) \"extglob\" patterns.", 88 | "type": "boolean", 89 | "default": false 90 | }, 91 | "noglobstar": { 92 | "description": "Do not match ** against multiple filenames. (Ie, treat it as a normal * instead.)", 93 | "type": "boolean", 94 | "default": false 95 | }, 96 | "nomount": { 97 | "description": "By default, a pattern starting with a forward-slash will be \"mounted\" onto the root setting, so that a valid filesystem path is returned. Set this flag to disable that behavior.", 98 | "type": "boolean", 99 | "default": false 100 | }, 101 | "nonull": { 102 | "description": "Set to never return an empty set, instead returning a set containing the pattern itself. This is the default in glob(3).", 103 | "type": "boolean", 104 | "default": false 105 | }, 106 | "nosort": { 107 | "description": "Don't sort the results.", 108 | "type": "boolean", 109 | "default": false 110 | }, 111 | "nounique": { 112 | "description": "In some cases, brace-expanded patterns can result in the same file showing up multiple times in the result set. By default, this implementation prevents duplicates in the result set. Set this flag to disable that behavior.", 113 | "type": "boolean", 114 | "default": false 115 | }, 116 | "passthrough": { 117 | "description": "If true, it will create a duplex stream which passes items through and emits globbed files. Since Gulp 4.0.", 118 | "type": "boolean", 119 | "default": false 120 | }, 121 | "read": { 122 | "description": "Setting this to false will return file.contents as null and not read the file at all.", 123 | "type": "boolean", 124 | "default": true 125 | }, 126 | "realpath": { 127 | "description": "Set to true to call fs.realpath on all of the results. In the case of a symlink that cannot be resolved, the full absolute path to the matched entry is returned (though it will usually be a broken symlink)", 128 | "type": "boolean", 129 | "default": false 130 | }, 131 | "root": { 132 | "description": "The place where patterns starting with / will be mounted onto. Defaults to path.resolve(options.cwd, \"/\") (/ on Unix systems, and C:\\ or some such on Windows.)", 133 | "type": "string" 134 | }, 135 | "silent": { 136 | "description": "When an unusual error is encountered when attempting to read a directory, a warning will be printed to stderr. Set the silent option to true to suppress these warnings.", 137 | "type": "boolean", 138 | "default": false 139 | }, 140 | "since": { 141 | "description": "Setting this to a Date or a time stamp will discard any file that have not been modified since the time specified. Since Gulp 4.0.", 142 | "type": ["object", "integer"] 143 | }, 144 | "stat": { 145 | "description": "Set to true to stat all results. This reduces performance somewhat, and is completely unnecessary, unless readdir is presumed to be an untrustworthy indicator of file existence.", 146 | "type": "boolean", 147 | "default": false 148 | }, 149 | "statCache": { 150 | "description": "A cache of results of filesystem information, to prevent unnecessary stat calls. While it should not normally be necessary to set this, you may pass the statCache from one glob() call to the options object of another, if you know that the filesystem will not change between calls.", 151 | "type": "object" 152 | }, 153 | "strict": { 154 | "description": "When an unusual error is encountered when attempting to read a directory, the process will just continue on in search of other matches. Set the strict option to raise an error in these cases.", 155 | "type": "boolean", 156 | "default": false 157 | }, 158 | "symlinks": { 159 | "description": "A cache of known symbolic links. You may pass in a previously generated symlinks object to save lstat calls when resolving ** matches.", 160 | "type": "object" 161 | }, 162 | "join": { 163 | "description": "Join parent's folder. Since Gulp-Chef 0.1.0.", 164 | "type": ["string", "boolean"], 165 | "default": "src" 166 | } 167 | } 168 | }, 169 | "required": ["globs"] 170 | }, 171 | "primary": "globs", 172 | "gathering": "options" 173 | } 174 | -------------------------------------------------------------------------------- /lib/schema/path.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "path", 3 | "properties": { 4 | "path": { 5 | "description": "The path (output folder) to write files to.", 6 | "type": "string" 7 | }, 8 | "options": { 9 | "description": "", 10 | "properties": { 11 | "cwd": { 12 | "description": "cwd for the output folder, only has an effect if provided output folder is relative.", 13 | "type": "string" 14 | }, 15 | "mode": { 16 | "description": "Octal permission specifying the mode the files should be created with: e.g. \"0744\", 0744 or 484 (0744 in base 10). Default: the mode of the input file (file.stat.mode) or the process mode if the input file has no mode property.", 17 | "type": ["string", "integer"] 18 | }, 19 | "dirMode": { 20 | "description": "Octal permission specifying the mode the directory should be created with: e.g. \"0755\", 0755 or 493 (0755 in base 10). Default is the process mode. Since Gulp 4.0.", 21 | "type": ["string", "integer"] 22 | }, 23 | "overwrite": { 24 | "description": "Specify if existing files with the same path should be overwritten or not. Since Gulp 4.0.", 25 | "type": "boolean", 26 | "default": true 27 | }, 28 | "flatten": { 29 | "description": "Remove or replace relative path for files. Since Gulp-Chef 0.1.0.", 30 | "type": "boolean", 31 | "default": false 32 | }, 33 | "join": { 34 | "description": "Join parent's folder. Since Gulp-Chef 0.1.0.", 35 | "type": ["string", "boolean"], 36 | "default": "dest" 37 | } 38 | } 39 | }, 40 | "required": ["path"] 41 | }, 42 | "primary": "path", 43 | "gathering": "options" 44 | } 45 | -------------------------------------------------------------------------------- /lib/schema/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "description": { 5 | "description": "Description of this task.", 6 | "type": "string" 7 | }, 8 | "name": { 9 | "description": "Name of this task.", 10 | "type": "string" 11 | }, 12 | "order": { 13 | "description": "Series execution order of this task. Just used to sort, don't have to be unique or continuous.", 14 | "type": "integer" 15 | }, 16 | "plugin": { 17 | "description": "The name of plugin module to use.", 18 | "type": ["string", "any"] 19 | }, 20 | "parallel": { 21 | "description": "Same as task, but child tasks are forced to run in parallel.", 22 | "type": "array" 23 | }, 24 | "recipe": { 25 | "description": "The name of recipe to use.", 26 | "type": "string" 27 | }, 28 | "series": { 29 | "description": "Same as task, but child tasks are forced to run in series.", 30 | "type": "array" 31 | }, 32 | "spit": { 33 | "description": "Instruct recipe or task to write file(s) out if was optional.", 34 | "type": "boolean" 35 | }, 36 | "task": { 37 | "description": "Inline function(s) or reference(s) to other task(s). If provided as an array, child tasks are forced to run in series. If an object, child tasks are forced to run in parallel.", 38 | "type": "array" 39 | }, 40 | "visibility": { 41 | "description": "Visibility of this task. Valid values are \"visible\" and \"hidden\".", 42 | "type": "string" 43 | } 44 | }, 45 | "additionalProperties": false 46 | } 47 | -------------------------------------------------------------------------------- /lib/stuff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ConfigurableRecipeRegistry = require('./recipe/registry'); 4 | 5 | var cwd = process.cwd(); 6 | 7 | var _defaults = { 8 | lookups: { 9 | flows: 'gulp/flows', 10 | streams: 'gulp/streams', 11 | tasks: 'gulp/tasks' 12 | }, 13 | plugins: { 14 | camelize: false, 15 | config: process.cwd() + '/package.json', 16 | pattern: ['gulp-ccr-*'], 17 | replaceString: ConfigurableRecipeRegistry.replaceString 18 | } 19 | }; 20 | 21 | module.exports = function (settings) { 22 | var lookups, plugins; 23 | 24 | settings = settings.defaults(_defaults); 25 | lookups = settings.get('lookups'); 26 | plugins = settings.get('plugins'); 27 | return { 28 | flows: ConfigurableRecipeRegistry.builder('flow') 29 | .dir(cwd, lookups.flows) 30 | .npm(plugins) 31 | .require('gulp-ccr-parallel') 32 | .require('gulp-ccr-series') 33 | .require('gulp-ccr-watch') 34 | .build(), 35 | streams: ConfigurableRecipeRegistry.builder('stream') 36 | .dir(cwd, lookups.streams) 37 | .npm(plugins) 38 | .require('gulp-ccr-merge') 39 | .require('gulp-ccr-pipe') 40 | .require('gulp-ccr-queue') 41 | .build(), 42 | tasks: ConfigurableRecipeRegistry.builder('task') 43 | .dir(cwd, lookups.tasks) 44 | .dir(cwd, 'gulp') 45 | .npm(plugins) 46 | .require('gulp-ccr-clean') 47 | .require('gulp-ccr-copy') 48 | .build() 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /lib/task/expose.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var log = require('gulplog'); 4 | var PluginError = require('gulp-util').PluginError; 5 | 6 | var _defaults = { 7 | exposeWithPrefix: 'auto' 8 | }; 9 | 10 | module.exports = function (registry, settings) { 11 | var strategies = { 12 | always: function (prefix, name) { 13 | return prefix + name; 14 | }, 15 | auto: function (prefix, name) { 16 | var msg; 17 | 18 | msg = check(name); 19 | if (msg) { 20 | log.info('configure:', msg + ', using "' + prefix + name + '" instead.'); 21 | return prefix + name; 22 | } 23 | return name; 24 | }, 25 | never: function (prefix, name) { 26 | var msg; 27 | 28 | msg = check(name); 29 | if (msg) { 30 | throw new PluginError('configure:', msg); 31 | } 32 | return name; 33 | } 34 | }; 35 | 36 | function check(name) { 37 | var task; 38 | 39 | task = registry.get(name); 40 | if (task) { 41 | return 'the name "' + name + '" already taken by "' + task.fullName + '"'; 42 | } 43 | } 44 | 45 | return strategies[settings.defaults(_defaults).get().exposeWithPrefix] || strategies.auto; 46 | }; 47 | -------------------------------------------------------------------------------- /lib/task/factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var log = require('gulplog'); 4 | var _ = require('lodash'); 5 | var from = require('../helpers/dataflow'); 6 | 7 | var PluginError = require('gulp-util').PluginError; 8 | var Configuration = require('../configuration'); 9 | var metadata = require('./metadata'); 10 | var profile = require('./profile'); 11 | 12 | var SCHEMA_TASK = require('../schema/task.json'); 13 | var TASK_METADATAS = Object.keys(SCHEMA_TASK.properties); 14 | 15 | var REGEX_RUNTIME_OPTIONS = /^([.#]?)([_\w][-_:\s\w]*)$/; 16 | 17 | var CONSTANT = { 18 | VISIBILITY: { 19 | /** hidden configurable task can't be run from cli, but still functional */ 20 | HIDDEN: 'hidden', 21 | /** disabled configurable task is not processed and not functional, including all it's descendants */ 22 | DISABLED: 'disabled', 23 | /** normal configurable task can be run from cli */ 24 | NORMAL: 'normal' 25 | } 26 | }; 27 | 28 | /** 29 | * NOTE: Config is inherited at config time and injected, realized at runtime. 30 | */ 31 | function ConfigurableTaskFactory(recipes, registry, expose) { 32 | this.recipes = recipes; 33 | this.registry = registry; 34 | this.expose = expose; 35 | } 36 | 37 | function getTaskRuntimeInfo(rawConfig) { 38 | var match, name, taskInfo; 39 | 40 | taskInfo = from(rawConfig).to({}).move(TASK_METADATAS); 41 | 42 | if (taskInfo.name) { 43 | name = _.trim(taskInfo.name); 44 | match = REGEX_RUNTIME_OPTIONS.exec(name); 45 | if (!match) { 46 | throw new PluginError(__filename, 'invalid task name: ' + name); 47 | } 48 | 49 | taskInfo.name = match[2] || name; 50 | 51 | if (match[1]) { 52 | switch (match[1]) { 53 | case '#': 54 | taskInfo.visibility = CONSTANT.VISIBILITY.DISABLED; 55 | break; 56 | case '.': 57 | taskInfo.visibility = CONSTANT.VISIBILITY.HIDDEN; 58 | break; 59 | } 60 | } 61 | } 62 | 63 | return taskInfo; 64 | } 65 | 66 | function isVisible(task) { 67 | return task.name && task.visibility === CONSTANT.VISIBILITY.NORMAL || !('visibility' in task); 68 | } 69 | 70 | function isDisabled(task) { 71 | return task.visibility === CONSTANT.VISIBILITY.DISABLED; 72 | } 73 | 74 | ConfigurableTaskFactory.CONSTANT = CONSTANT; 75 | ConfigurableTaskFactory.getTaskRuntimeInfo = getTaskRuntimeInfo; 76 | ConfigurableTaskFactory.isVisible = isVisible; 77 | ConfigurableTaskFactory.isDisabled = isDisabled; 78 | 79 | ConfigurableTaskFactory.prototype.one = function (prefix, rawConfig, parentConfig) { 80 | var self, recipes, taskInfo; 81 | 82 | self = this; 83 | recipes = this.recipes; 84 | 85 | taskInfo = getTaskRuntimeInfo(rawConfig); 86 | if (isDisabled(taskInfo)) { 87 | return null; 88 | } 89 | 90 | return simple() || auxiliary() || reference() || composite() || noop(); 91 | 92 | // Simple task should show up in task tree and cli, unless anonymous. 93 | function simple() { 94 | var recipe, configs; 95 | 96 | recipe = recipes.inline(taskInfo) || recipes.plugin(taskInfo) || recipes.task(taskInfo); 97 | if (recipe) { 98 | configs = Configuration.sort(taskInfo, rawConfig, parentConfig, recipe.schema); 99 | if (!configs.taskInfo.name) { 100 | configs.taskInfo.visibility = CONSTANT.VISIBILITY.HIDDEN; 101 | } 102 | return self.create(prefix, configs.taskInfo, configs.taskConfig, recipe); 103 | } 104 | } 105 | 106 | // Reference task should show up in task tree and cli, unless anonymous. 107 | // Skip metadata for reference task on purpose. 108 | // (The task tree was already too complicated, no reason to duplicate information.) 109 | function reference() { 110 | var recipe, configs, wrapper, result, displayName, missingName; 111 | 112 | recipe = recipes.reference(taskInfo, resolved); 113 | if (recipe) { 114 | configs = Configuration.sort(taskInfo, rawConfig, parentConfig); 115 | displayName = recipe.displayName + ' (reference)'; 116 | missingName = recipe.displayName + ' (missing reference)'; 117 | wrapper = self.create(prefix, { 118 | name: displayName, 119 | type: 'reference', 120 | visibility: CONSTANT.VISIBILITY.HIDDEN 121 | }, configs.taskConfig, recipe); 122 | // setup reference missing warning. 123 | wrapper.displayName = missingName; 124 | metadata.set(wrapper, missingName); 125 | if (configs.taskInfo.name) { 126 | // 127 | // parent: { 128 | // target: 'build' 129 | // } 130 | // or 131 | // parent: [{ 132 | // name: 'target', 133 | // task: 'build' 134 | // }] 135 | // normalized to: 136 | // parent: [{ 137 | // name: 'target', 138 | // task: 'build' 139 | // }] 140 | // realized to: 141 | // [{ 142 | // name: 'parent', 143 | // tasks: [{ 144 | // name: 'target', 145 | // tasks: [{ 146 | // name: ' build', 147 | // task: function () {} 148 | // }] 149 | // }] 150 | // }] 151 | // tree: 152 | // └─┬ parent 153 | // └─┬ target 154 | // └── build 155 | result = self.create(prefix, configs.taskInfo, configs.taskConfig, wrapper, [wrapper]); 156 | } else { 157 | // parent: [{ 158 | // task: 'build' 159 | // }] 160 | // or 161 | // parent: ['build'] 162 | // normalized to: 163 | // parent: [{ 164 | // task: 'build' 165 | // }] 166 | // realized to: 167 | // [{ 168 | // name: 'parent', 169 | // tasks: [{ 170 | // name: '', 171 | // tasks: [{ 172 | // name: ' build', 173 | // task: function () {} 174 | // }] 175 | // }] 176 | // }] 177 | // tree: 178 | // └─┬ parent 179 | // └── build 180 | result = wrapper; 181 | } 182 | return result; 183 | } 184 | 185 | function resolved(task) { 186 | // Watch task need refering target's config and tasks. 187 | result.config = task.config; 188 | result.tasks = task.tasks; 189 | // remove reference missing warning message. 190 | wrapper.displayName = displayName; 191 | metadata.set(wrapper, displayName); 192 | } 193 | } 194 | 195 | // Auxiliary task should show up in task tree, but not in cli by default for simplicity. 196 | function auxiliary() { 197 | var recipe, configs, tasks; 198 | 199 | recipe = recipes.stream(taskInfo) || recipes.flow(taskInfo); 200 | if (recipe) { 201 | configs = Configuration.sort(taskInfo, rawConfig, parentConfig, recipe.schema); 202 | if (configs.taskInfo.name !== 'watch' && (!configs.taskInfo.name || !('visibility' in configs.taskInfo))) { 203 | configs.taskInfo.visibility = CONSTANT.VISIBILITY.HIDDEN; 204 | configs.taskInfo.name = '<' + configs.taskInfo.name + '>'; 205 | } 206 | tasks = createTasks(configs); 207 | return self.create(prefix, configs.taskInfo, configs.taskConfig, recipe, tasks); 208 | } 209 | } 210 | 211 | // Composite task should show up in task treem, and show up in cli if holding task is a named task. 212 | function composite() { 213 | var configs, type, recipe, tasks, wrapper; 214 | 215 | configs = Configuration.sort(taskInfo, rawConfig, parentConfig); 216 | tasks = configs.taskInfo.parallel || configs.taskInfo.series || configs.taskInfo.task || configs.subTaskConfigs; 217 | type = _type(); 218 | if (type) { 219 | tasks = createTasks(configs); 220 | recipe = recipes.flow({ recipe: type }); 221 | wrapper = self.create(prefix, { 222 | name: '<' + type + '>', 223 | visibility: CONSTANT.VISIBILITY.HIDDEN 224 | }, configs.taskConfig, recipe, tasks); 225 | if (configs.taskInfo.name) { 226 | return self.create(prefix, configs.taskInfo, configs.taskConfig, wrapper, [wrapper]); 227 | } 228 | configs.taskInfo.visibility = CONSTANT.VISIBILITY.HIDDEN; 229 | return wrapper; 230 | } 231 | 232 | if (_forward()) { 233 | tasks = createTasks(configs); 234 | recipe = function (done) { 235 | return tasks[0].call(this, done); 236 | }; 237 | return self.create(prefix, configs.taskInfo, configs.taskConfig, recipe, tasks); 238 | } 239 | 240 | function _type() { 241 | if (configs.taskInfo.series) { 242 | return 'series'; 243 | } else if (configs.taskInfo.parallel) { 244 | return 'parallel'; 245 | } else if (_.size(tasks) > 1) { 246 | if (Array.isArray(tasks)) { 247 | return 'series'; 248 | } else if (_.isPlainObject(tasks)) { 249 | return 'parallel'; 250 | } 251 | } 252 | } 253 | 254 | function _forward() { 255 | return _.size(tasks) === 1; 256 | } 257 | } 258 | 259 | function noop() { 260 | if (!taskInfo.name) { 261 | taskInfo.name = ''; 262 | } 263 | log.warn('configure:', 'the task "' + taskInfo.name + "\" won't do anything, misspelled a recipe name?"); 264 | return self.create(prefix, taskInfo, {}, self.recipes.noop()); 265 | } 266 | 267 | function createTasks(configs) { 268 | var tasksPrefix, tasks; 269 | 270 | tasks = configs.taskInfo.parallel || configs.taskInfo.series || configs.taskInfo.task || configs.subTaskConfigs; 271 | if (tasks && _.size(tasks)) { 272 | tasksPrefix = _tasksPrefix(); 273 | return self.multiple(tasksPrefix, tasks, configs.taskConfig); 274 | } 275 | 276 | function _tasksPrefix() { 277 | return (isVisible(configs.taskInfo) && configs.taskInfo.name) ? prefix + configs.taskInfo.name + ':' : prefix; 278 | } 279 | } 280 | }; 281 | 282 | ConfigurableTaskFactory.prototype.multiple = function (prefix, subTaskConfigs, parentConfig) { 283 | var self; 284 | 285 | self = this; 286 | return self.arrayify(subTaskConfigs).reduce(create, []); 287 | 288 | function create(result, config) { 289 | var task; 290 | 291 | task = self.one(prefix, config, parentConfig); 292 | if (task) { 293 | result.push(task); 294 | } 295 | return result; 296 | } 297 | }; 298 | 299 | ConfigurableTaskFactory.prototype.arrayify = function (taskConfigs) { 300 | return _array() || _arrayify(); 301 | 302 | function _array() { 303 | if (Array.isArray(taskConfigs)) { 304 | return _.flatten(taskConfigs).map(objectify); 305 | } 306 | 307 | function objectify(value) { 308 | if (typeof value === 'string') { 309 | return { 310 | task: value 311 | }; 312 | } 313 | if (typeof value === 'function') { 314 | return { 315 | task: value 316 | }; 317 | } 318 | if (_.isPlainObject(value)) { 319 | return value; 320 | } 321 | // array etc. 322 | return { 323 | task: value 324 | }; 325 | } 326 | } 327 | 328 | function _arrayify() { 329 | if (_.isPlainObject(taskConfigs)) { 330 | return Object.keys(taskConfigs).map(objectify).sort(comparator); 331 | } 332 | return [{ 333 | task: taskConfigs 334 | }]; 335 | 336 | function objectify(name) { 337 | var value; 338 | 339 | value = taskConfigs[name]; 340 | if (_.isPlainObject(value)) { 341 | value.name = name; 342 | return value; 343 | } 344 | return { 345 | name: name, 346 | task: value 347 | }; 348 | } 349 | 350 | // tasks not defined "order" property defaults to 0 351 | function comparator(a, b) { 352 | return (a.order || 0) - (b.order || 0); 353 | } 354 | } 355 | }; 356 | 357 | ConfigurableTaskFactory.prototype.create = function (prefix, taskInfo, taskConfig, recipe, tasks) { 358 | var registry = this.registry; 359 | var branch = tasks && tasks.length > 0; 360 | var name, runner; 361 | 362 | if (taskInfo.spit) { 363 | runner = function (done) { 364 | var gulp, config; 365 | 366 | gulp = this.gulp; 367 | config = this.config; 368 | return recipe.call(this, done) 369 | .pipe(gulp.dest(config.dest.path, config.dest.options)); 370 | }; 371 | } else { 372 | runner = recipe; 373 | } 374 | 375 | function configurableTask(done) { 376 | var gulp, context, hrtime; 377 | 378 | // NOTE: gulp 4.0 task are called on undefined context. 379 | // So, 380 | // 1.We need gulp reference from registry here. 381 | // see: [Gulp 4: invoke task on gulp context](https://github.com/gulpjs/gulp/issues/1377) 382 | // 2.If `this` is undefined, then this task was invoked from gulp. 383 | // 3.If `this` is defined, then this task was invoked from another configurable task. 384 | gulp = registry.gulp; 385 | context = { 386 | gulp: gulp, 387 | helper: Configuration 388 | }; 389 | 390 | if (tasks) { 391 | context.tasks = tasks; 392 | } 393 | 394 | // this task was invoked by another task 395 | if (this) { 396 | // inject and realize runtime configuration 397 | context.config = Configuration.realize(taskConfig, this.config); 398 | if (this.upstream) { 399 | context.upstream = this.upstream; 400 | } 401 | if (taskInfo.type === 'reference') { 402 | return runner.call(context, done); 403 | } 404 | return profile(runner, context, done, start, stop); 405 | } 406 | 407 | // this task was invoked by gulp 408 | // (by gulp.parallel() actually, and that will show profile messages, so we don't do profile here.) 409 | context.config = Configuration.realize(taskConfig); 410 | return runner.call(context, done); 411 | 412 | function start() { 413 | if (name) { 414 | hrtime = process.hrtime(); 415 | gulp.emit('start', { 416 | name: name, 417 | branch: branch, 418 | time: Date.now() 419 | }); 420 | } 421 | } 422 | 423 | function stop(err) { 424 | var event; 425 | 426 | if (name) { 427 | event = { 428 | name: name, 429 | branch: branch, 430 | time: Date.now(), 431 | duration: process.hrtime(hrtime) 432 | }; 433 | if (err) { 434 | event.error = err; 435 | gulp.emit('error', event); 436 | } else { 437 | gulp.emit('stop', event); 438 | } 439 | } 440 | } 441 | } 442 | 443 | name = taskInfo.name || recipe.displayName || recipe.name || ''; 444 | set(configurableTask, 'description', taskInfo.description || recipe.description); 445 | set(configurableTask, 'visibility', taskInfo.visibility); 446 | set(configurableTask, 'tasks', tasks); 447 | set(configurableTask, 'recipe', taskInfo.recipe); 448 | set(configurableTask, 'recipeInstance', taskInfo.recipeInstance || recipe); 449 | configurableTask.fullName = prefix + name; 450 | configurableTask.config = taskConfig; 451 | if (isVisible(configurableTask)) { 452 | name = this.expose(prefix, name); 453 | this.registry.set(name, configurableTask); 454 | configurableTask.displayName = name; 455 | metadata.set(configurableTask, name); 456 | } else { 457 | configurableTask.displayName = name; 458 | metadata.set(configurableTask, name); 459 | } 460 | if (tasks) { 461 | metadata.tree(configurableTask, tasks); 462 | } 463 | return configurableTask; 464 | 465 | function set(target, property, value) { 466 | if (typeof value !== 'undefined') { 467 | target[property] = value; 468 | } 469 | } 470 | }; 471 | 472 | module.exports = ConfigurableTaskFactory; 473 | -------------------------------------------------------------------------------- /lib/task/metadata.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * WeakMap for storing metadata. 5 | * 6 | * Add metadata to make gulp.tree() happy. 7 | * 8 | * Note: we need the very metadata instance that gulp uses. 9 | * So, we have 2 options: 10 | * 11 | * 1. A hack: require that very module instance, as here we do. 12 | * 2. A override: override gulp.tree() method. 13 | * 14 | * Reference: 15 | * 16 | * https://github.com/gulpjs/undertaker/blob/master/lib/set-task.js 17 | * https://github.com/gulpjs/undertaker/blob/master/lib/parallel.js 18 | * https://github.com/gulpjs/undertaker/blob/master/lib/helpers/buildTree.js 19 | * 20 | */ 21 | var _metadata; 22 | 23 | try { 24 | _metadata = require('gulp/node_modules/undertaker/lib/helpers/metadata'); 25 | } catch (ex) { 26 | _metadata = require('undertaker/lib/helpers/metadata'); 27 | } 28 | 29 | function get(target) { 30 | return _metadata.get(target); 31 | } 32 | 33 | function set(target, label, optionalNodes) { 34 | var meta, name, nodes; 35 | 36 | nodes = optionalNodes || []; 37 | meta = _metadata.get(target); 38 | if (meta) { 39 | if (label) { 40 | meta.name = label; 41 | if (meta.tree) { 42 | meta.tree.label = label; 43 | } 44 | } 45 | if (nodes.length) { 46 | nodes = meta.tree.nodes = meta.tree.nodes.concat(nodes); 47 | } 48 | } else { 49 | name = target.displayName || target.name || ''; 50 | meta = { 51 | name: name, 52 | // Note: undertaker use taskWrapper function in set-task to allow for aliases. 53 | // Since we already wrap recipe function in configurable task, we don't need another wrapper. 54 | // So just add "orig" field here. 55 | // see https://github.com/gulpjs/undertaker/commit/9d0ee9cad5cffb64ffe8cdeee5a3ff69286c41eb for detail. 56 | orig: target, 57 | tree: { 58 | label: label, 59 | // non-top lavel gulp tasks are treated as normal function. 60 | type: 'function', 61 | nodes: nodes 62 | } 63 | }; 64 | _metadata.set(target, meta); 65 | } 66 | if (nodes.length) { 67 | meta.branch = true; 68 | } 69 | return meta.tree; 70 | } 71 | 72 | function tree(target, optionalNodes) { 73 | var name, nodes; 74 | 75 | name = target.displayName || target.name || ''; 76 | nodes = (optionalNodes || []).map(function (node) { 77 | var meta; 78 | 79 | meta = get(node); 80 | return meta.tree; 81 | }); 82 | set(target, name, nodes); 83 | } 84 | 85 | module.exports = { 86 | get: get, 87 | set: set, 88 | tree: tree 89 | }; 90 | -------------------------------------------------------------------------------- /lib/task/profile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var eos = require('end-of-stream'); 4 | var exhaust = require('stream-exhaust'); 5 | 6 | function profile(fn, context, done, start, stop) { 7 | var result; 8 | 9 | try { 10 | start(); 11 | result = fn.call(context, callbackDone); 12 | if (result) { 13 | // was implemented using `async-done`, 14 | // but it will emit stream events immediately, 15 | // and show profile message in incorrect order. 16 | if (typeof result.on === 'function') { 17 | eos(exhaust(result), { error: false }, asyncDone); 18 | } else if (typeof result.subscribe === 'function') { 19 | result.subscribe(function (next) { 20 | }, function (error) { 21 | asyncDone(error); 22 | }, function (result) { 23 | asyncDone(null, result); 24 | }); 25 | } else if (typeof result.then === 'function') { 26 | result.then(function (result) { 27 | asyncDone(null, result); 28 | }, function (error) { 29 | asyncDone(error); 30 | }); 31 | } 32 | } 33 | return result; 34 | } catch (ex) { 35 | stop(ex); 36 | } 37 | 38 | function callbackDone(err, ret) { 39 | asyncDone(err, ret); 40 | done(err, ret); 41 | } 42 | 43 | function asyncDone(err, ret) { 44 | if (err) { 45 | stop(err); 46 | } else { 47 | stop(null, ret); 48 | } 49 | } 50 | } 51 | 52 | module.exports = profile; 53 | -------------------------------------------------------------------------------- /lib/task/registry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var observable = require('../helpers/observable'); 5 | 6 | /** 7 | * @constructor 8 | * 9 | * Reference: 10 | * https://github.com/gulpjs/undertaker-registry/blob/master/index.js 11 | * 12 | */ 13 | function Registry(listener) { 14 | this._tasks = {}; 15 | this._refers = {}; 16 | this._listener = listener || function () {}; 17 | } 18 | 19 | Registry.prototype.init = function (gulp) { 20 | this.gulp = gulp; 21 | this._listener(this); 22 | }; 23 | 24 | Registry.prototype.get = function (name) { 25 | return this._tasks[name]; 26 | }; 27 | 28 | Registry.prototype.set = function (name, task) { 29 | this._tasks[name] = task; 30 | return task; 31 | }; 32 | 33 | Registry.prototype.tasks = function () { 34 | return _.clone(this._tasks); 35 | }; 36 | 37 | Registry.prototype.refer = function (name, listener) { 38 | var task, listen; 39 | 40 | task = this._tasks[name]; 41 | if (task) { 42 | if (listener) { 43 | process.nextTick(function () { 44 | listener(task); 45 | }); 46 | } 47 | } else { 48 | listen = this._refers[name] || (this._refers[name] = observable()); 49 | if (listener) { 50 | listen(listener); 51 | } 52 | } 53 | return task; 54 | }; 55 | 56 | Registry.prototype.missing = function () { 57 | var self, gulp; 58 | 59 | self = this; 60 | gulp = this.gulp; 61 | return Object.keys(self._refers).filter(function (ref) { 62 | var task = gulp.task(ref); 63 | 64 | if (task) { 65 | self._refers[ref].notify(task); 66 | return false; 67 | } 68 | return true; 69 | }); 70 | }; 71 | 72 | module.exports = Registry; 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-chef", 3 | "version": "0.1.4", 4 | "description": "Cascading configurable recipes for gulp 4.0. An elegant, intuitive way to reuse gulp tasks.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha ./test/specs/**/*_test.js", 8 | "debug": "node ./node_modules/gulp/bin/gulp.js build" 9 | }, 10 | "keywords": [ 11 | "async", 12 | "automatic", 13 | "automation", 14 | "build", 15 | "cascade", 16 | "cascading", 17 | "cookbook", 18 | "conditional", 19 | "conditional configuration", 20 | "config", 21 | "configure", 22 | "configurable", 23 | "configuration", 24 | "dry", 25 | "env", 26 | "environment", 27 | "flow", 28 | "glob", 29 | "gulp", 30 | "gulp 4.0", 31 | "gulpfriendly", 32 | "gulp-cookery", 33 | "gulp-chef", 34 | "inherit", 35 | "inheritance", 36 | "join", 37 | "json", 38 | "json-schema", 39 | "merge", 40 | "make", 41 | "nesting", 42 | "normalize", 43 | "path", 44 | "parallel", 45 | "pipe", 46 | "plugin", 47 | "queue", 48 | "recipe", 49 | "registry", 50 | "reuse", 51 | "runner", 52 | "schema", 53 | "series", 54 | "settings", 55 | "stream", 56 | "task" 57 | ], 58 | "repository": { 59 | "type": "git", 60 | "url": "git+https://github.com/gulp-cookery/gulp-chef.git" 61 | }, 62 | "author": "Amobiz", 63 | "license": "MIT", 64 | "peerDependencies": { 65 | "gulp": ">=4.0.0 || >=4.0.0-alpha.2 || >=4.0.0-beta.0" 66 | }, 67 | "devDependencies": { 68 | "chai": "^3.5.0", 69 | "chai-as-promised": "^5.2.0", 70 | "eslint": "^1.10.3", 71 | "gulp": "github:gulpjs/gulp#4.0", 72 | "gulp-mocha": "^2.2.0", 73 | "mocha": "^2.4.5", 74 | "mocha-cases": "^0.1.10", 75 | "sinon": "^1.17.3", 76 | "streamifier": "^0.1.1" 77 | }, 78 | "dependencies": { 79 | "async": "^1.5.2", 80 | "chalk": "^1.1.1", 81 | "end-of-stream": "^1.1.0", 82 | "glob": "^6.0.4", 83 | "globby": "^2.1.0", 84 | "globjoin": "^0.1.4", 85 | "gulp-ccr-clean": "^0.1.1", 86 | "gulp-ccr-copy": "^0.1.0", 87 | "gulp-ccr-merge": "^0.1.0", 88 | "gulp-ccr-parallel": "^0.1.0", 89 | "gulp-ccr-pipe": "^0.1.0", 90 | "gulp-ccr-queue": "^0.1.0", 91 | "gulp-ccr-series": "^0.1.0", 92 | "gulp-ccr-watch": "^0.1.1", 93 | "gulp-load-plugins": "^1.2.0", 94 | "gulp-util": "^3.0.7", 95 | "gulplog": "^1.0.0", 96 | "json-normalizer": "^0.3.5", 97 | "json-regulator": "^0.1.16", 98 | "lodash": "^4.6.1", 99 | "require-dir": "^0.3.0", 100 | "stream-exhaust": "^1.0.1", 101 | "yargs": "^3.32.0" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "rules": { 6 | "no-unused-expressions": 0, 7 | "max-nested-callbacks": [1, 10] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fake/factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Sinon = require('sinon'); 4 | var _ = require('lodash'); 5 | 6 | var FakeGulp = require('./gulp'); 7 | 8 | function createSpyGulpTask(name, gulpTask) { 9 | var task; 10 | 11 | task = Sinon.spy(gulpTask); 12 | task.displayName = name; 13 | return task; 14 | } 15 | 16 | function createSpyConfigurableTask(name, optionalRecipe, optionalTaskConfig) { 17 | var task, recipe, taskConfig; 18 | 19 | recipe = optionalRecipe || Sinon.spy(); 20 | taskConfig = optionalTaskConfig || {}; 21 | task = createSpyGulpTask(name, function (done) { 22 | var context; 23 | 24 | context = { 25 | config: this ? _.defaultsDeep({}, taskConfig, this.config) : taskConfig 26 | }; 27 | recipe.call(context, done); 28 | }); 29 | task.displayName = name; 30 | return task; 31 | } 32 | 33 | function createFakeGulp() { 34 | var gulp, gulpTask, configurableTask, configurableTaskConfig, configurableTaskRefConfig; 35 | 36 | gulp = new FakeGulp(); 37 | 38 | // task: gulp-task 39 | gulpTask = createSpyGulpTask('gulp-task'); 40 | gulp.task(gulpTask); 41 | 42 | // task: configurable-task 43 | configurableTaskConfig = { keyword: 'configurable-task' }; 44 | configurableTask = createSpyConfigurableTask('configurable-task', Sinon.spy(), configurableTaskConfig); 45 | gulp.task(configurableTask); 46 | 47 | // task: gulp-task-by-ref 48 | gulp.task(createSpyGulpTask('gulp-task-by-ref')); 49 | 50 | // task: configurable-task-by-ref 51 | configurableTaskRefConfig = { keyword: 'configurable-task-by-ref' }; 52 | gulp.task(createSpyConfigurableTask('configurable-task-by-ref', Sinon.spy(), configurableTaskRefConfig)); 53 | 54 | return gulp; 55 | } 56 | 57 | module.exports = { 58 | createSpyGulpTask: createSpyGulpTask, 59 | createSpyConfigurableTask: createSpyConfigurableTask, 60 | createFakeGulp: createFakeGulp 61 | }; 62 | -------------------------------------------------------------------------------- /test/fake/gulp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | var base = process.cwd(); 6 | var Registry = require(base + '/lib/task/registry'); 7 | 8 | function FakeGulp() { 9 | this._registry = new Registry(); 10 | } 11 | 12 | FakeGulp.prototype.task = function (optionalName, taskFn) { 13 | var name, task; 14 | 15 | if (typeof optionalName === 'function') { 16 | task = optionalName; 17 | name = task.displayName || task.name; 18 | } else { 19 | task = taskFn; 20 | name = optionalName; 21 | } 22 | if (typeof name === 'string' && typeof task === 'function') { 23 | this._registry.set(name, task); 24 | } 25 | return this._registry.get(name); 26 | }; 27 | 28 | FakeGulp.prototype.registry = function (registry) { 29 | var tasks; 30 | 31 | if (!registry) { 32 | return this._registry; 33 | } 34 | 35 | tasks = this._registry.tasks(); 36 | this._registry = _.reduce(tasks, setTasks, registry); 37 | this._registry.init(this); 38 | 39 | function setTasks(result, task, name) { 40 | result.set(name, task); 41 | return result; 42 | } 43 | }; 44 | 45 | FakeGulp.prototype.emit = function () { 46 | }; 47 | 48 | module.exports = FakeGulp; 49 | -------------------------------------------------------------------------------- /test/fake/stuff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Sinon = require('sinon'); 4 | var base = process.cwd(); 5 | 6 | var ConfigurableRecipeRegistry = require(base + '/lib/recipe/registry'); 7 | 8 | function create(name, fn) { 9 | var recipe; 10 | 11 | recipe = Sinon.spy(fn); 12 | recipe.schema = { 13 | title: name 14 | }; 15 | return recipe; 16 | } 17 | 18 | module.exports = function () { 19 | 20 | return { 21 | flows: new ConfigurableRecipeRegistry({ 22 | parallel: create('parallel'), 23 | series: create('series'), 24 | 'flow-task': create('flow-task') 25 | }), 26 | streams: new ConfigurableRecipeRegistry({ 27 | merge: create('merge'), 28 | 'stream-task': create('stream-task') 29 | }), 30 | tasks: new ConfigurableRecipeRegistry({ 31 | copy: create('copy'), 32 | 'task-task': create('task-task') 33 | }) 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /test/specs/configuration/glob_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Sinon = require('sinon'); 4 | var expect = require('chai').expect; 5 | var test = require('mocha-cases'); 6 | 7 | var base = process.cwd(); 8 | 9 | var glob = require(base + '/lib/configuration/glob'); 10 | 11 | describe('Core', function () { 12 | describe('Configuration', function () { 13 | describe('.glob()', function () { 14 | it('should accept path string', function () { 15 | var actual; 16 | 17 | actual = glob('src'); 18 | expect(actual).to.deep.equal({ 19 | globs: ['src'] 20 | }); 21 | }); 22 | it('should accept globs', function () { 23 | var actual; 24 | 25 | actual = glob('src/**/*.js'); 26 | expect(actual).to.deep.equal({ 27 | globs: ['src/**/*.js'] 28 | }); 29 | }); 30 | it('should accept globs array', function () { 31 | var actual; 32 | 33 | actual = glob(['src/**/*.js', 'lib/**/*.js']); 34 | expect(actual).to.deep.equal({ 35 | globs: ['src/**/*.js', 'lib/**/*.js'] 36 | }); 37 | }); 38 | it('should accept globs object with options', function () { 39 | var actual; 40 | 41 | actual = glob({ 42 | globs: '**/*.js', 43 | options: { 44 | base: 'src' 45 | } 46 | }); 47 | expect(actual).to.deep.equal({ 48 | globs: ['**/*.js'], 49 | options: { 50 | base: 'src' 51 | } 52 | }); 53 | }); 54 | it('should accept globs object with flat options', function () { 55 | var actual; 56 | 57 | actual = glob({ 58 | globs: '**/*.js', 59 | base: 'src' 60 | }); 61 | expect(actual).to.deep.equal({ 62 | globs: ['**/*.js'], 63 | options: { 64 | base: 'src' 65 | } 66 | }); 67 | }); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/specs/configuration/path_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Sinon = require('sinon'); 4 | var expect = require('chai').expect; 5 | var test = require('mocha-cases'); 6 | 7 | var base = process.cwd(); 8 | 9 | var path = require(base + '/lib/configuration/path'); 10 | 11 | describe('Core', function () { 12 | describe('Configuration', function () { 13 | describe('.path()', function () { 14 | it('should accept path string', function () { 15 | var actual; 16 | 17 | actual = path('dist'); 18 | expect(actual).to.deep.equal({ 19 | path: 'dist' 20 | }); 21 | }); 22 | it('should accept path object', function () { 23 | var actual; 24 | 25 | actual = path({ 26 | path: 'dist' 27 | }); 28 | expect(actual).to.deep.equal({ 29 | path: 'dist' 30 | }); 31 | }); 32 | it('should accept path object with options', function () { 33 | var actual; 34 | 35 | actual = path({ 36 | path: 'dist', 37 | options: { 38 | cwd: '.' 39 | } 40 | }); 41 | expect(actual).to.deep.equal({ 42 | path: 'dist', 43 | options: { 44 | cwd: '.' 45 | } 46 | }); 47 | }); 48 | it('should accept path object with flat options', function () { 49 | var actual; 50 | 51 | actual = path({ 52 | path: 'dist', 53 | cwd: '.' 54 | }); 55 | expect(actual).to.deep.equal({ 56 | path: 'dist', 57 | options: { 58 | cwd: '.' 59 | } 60 | }); 61 | }); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/specs/configuration/realize_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Sinon = require('sinon'); 4 | var expect = require('chai').expect; 5 | var test = require('mocha-cases'); 6 | 7 | var _ = require('lodash'); 8 | 9 | var base = process.cwd(); 10 | 11 | var realize = require(base + '/lib/configuration/realize'); 12 | 13 | describe('Core', function () { 14 | describe('Configuration', function () { 15 | describe('.realize()', function () { 16 | it('should call resolver function', function () { 17 | var resolved, values, expected; 18 | 19 | resolved = 'resolver called'; 20 | values = { 21 | runtime: Sinon.spy(function () { 22 | return resolved; 23 | }) 24 | }; 25 | expected = { 26 | runtime: resolved 27 | }; 28 | 29 | expect(realize(values)).to.deep.equal(expected); 30 | expect(values.runtime.calledWith(values)).to.be.true; 31 | }); 32 | it('should render template using given values', function () { 33 | var rootResolver = function () { 34 | return 'value from rootResolver()'; 35 | }; 36 | var nestResolver = function () { 37 | return 'value from nestedResolver()'; 38 | }; 39 | var template = 'Hello {{plainValue}}! {{nested.plainValue}}, {{resolver}} and {{nested.resolver}}.'; 40 | var realized = 'Hello World! Inner World, value from rootResolver() and value from nestedResolver().'; 41 | var values = { 42 | message: template, 43 | nested: { 44 | message: template, 45 | resolver: nestResolver, 46 | plainValue: 'Inner World' 47 | }, 48 | resolver: rootResolver, 49 | plainValue: 'World' 50 | }; 51 | var expected = { 52 | message: realized, 53 | nested: { 54 | message: realized, 55 | resolver: nestResolver(), 56 | plainValue: 'Inner World' 57 | }, 58 | resolver: rootResolver(), 59 | plainValue: 'World' 60 | }; 61 | var actual; 62 | 63 | actual = realize(values); 64 | expect(actual).to.deep.equal(expected); 65 | }); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/specs/configuration/sort_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Sinon = require('sinon'); 4 | var expect = require('chai').expect; 5 | var test = require('mocha-cases'); 6 | 7 | var _ = require('lodash'); 8 | 9 | var base = process.cwd(); 10 | 11 | var sort = require(base + '/lib/configuration/sort'); 12 | 13 | describe('Core', function () { 14 | describe('Configuration', function () { 15 | describe('.sort()', function () { 16 | it('should accept empty config', function () { 17 | var actual; 18 | 19 | actual = sort({}, {}, {}, {}); 20 | expect(actual).to.deep.equal({ 21 | taskInfo: {}, 22 | taskConfig: {}, 23 | subTaskConfigs: {} 24 | }); 25 | }); 26 | it('should always accept src and dest property even schema not defined', function () { 27 | var config = { 28 | src: 'src', 29 | dest: 'dist' 30 | }; 31 | var actual, original; 32 | 33 | original = _.cloneDeep(config); 34 | actual = sort({}, config, {}, {}); 35 | expect(actual).to.deep.equal({ 36 | taskInfo: {}, 37 | taskConfig: { 38 | src: { 39 | globs: ['src'] 40 | }, 41 | dest: { 42 | path: 'dist' 43 | } 44 | }, 45 | subTaskConfigs: {} 46 | }); 47 | expect(config).to.deep.equal(original); 48 | }); 49 | it('should only include reservied properties if schema not defined', function () { 50 | var config = { 51 | src: 'src', 52 | dest: 'dist', 53 | blabla: ['bla', 'bla'], 54 | foo: false, 55 | bar: { name: 'bar' } 56 | }; 57 | var actual, original; 58 | 59 | original = _.cloneDeep(config); 60 | actual = sort({}, config, {}, null); 61 | expect(actual).to.deep.equal({ 62 | taskInfo: {}, 63 | taskConfig: { 64 | src: { 65 | globs: ['src'] 66 | }, 67 | dest: { 68 | path: 'dist' 69 | } 70 | }, 71 | subTaskConfigs: { 72 | blabla: ['bla', 'bla'], 73 | foo: false, 74 | bar: { 75 | name: 'bar' 76 | } 77 | } 78 | }); 79 | expect(config).to.deep.equal(original); 80 | }); 81 | it('should inherit parent config', function () { 82 | var config = { 83 | src: { 84 | globs: ['src'] 85 | }, 86 | dest: { 87 | path: 'dist' 88 | } 89 | }; 90 | var actual, original; 91 | 92 | original = _.cloneDeep(config); 93 | actual = sort({}, {}, config, {}); 94 | expect(actual).to.deep.equal({ 95 | taskInfo: {}, 96 | taskConfig: { 97 | src: { 98 | globs: ['src'] 99 | }, 100 | dest: { 101 | path: 'dist' 102 | } 103 | }, 104 | subTaskConfigs: {} 105 | }); 106 | expect(config).to.deep.equal(original); 107 | }); 108 | it('should join parent path config', function () { 109 | var config = { 110 | src: ['services/**/*.js', 'views/**/*.js'], 111 | dest: 'lib' 112 | }; 113 | var parent = { 114 | src: { 115 | globs: ['src'] 116 | }, 117 | dest: { 118 | path: 'dist' 119 | } 120 | }; 121 | var actual, original, originalParent; 122 | 123 | original = _.cloneDeep(config); 124 | originalParent = _.cloneDeep(parent); 125 | actual = sort({}, config, parent, {}); 126 | expect(actual).to.deep.equal({ 127 | taskInfo: {}, 128 | taskConfig: { 129 | src: { 130 | globs: ['src/services/**/*.js', 'src/views/**/*.js'] 131 | }, 132 | dest: { 133 | path: 'dist/lib' 134 | } 135 | }, 136 | subTaskConfigs: {} 137 | }); 138 | expect(config).to.deep.equal(original); 139 | expect(parent).to.deep.equal(originalParent); 140 | }); 141 | it('should not join parent path config if said so', function () { 142 | var config = { 143 | src: { 144 | globs: ['services/**/*.js', 'views/**/*.js'], 145 | options: { 146 | join: false 147 | } 148 | }, 149 | dest: { 150 | path: 'lib', 151 | options: { 152 | join: false 153 | } 154 | } 155 | }; 156 | var parent = { 157 | src: { 158 | globs: ['src'] 159 | }, 160 | dest: { 161 | path: 'dist' 162 | } 163 | }; 164 | var actual, original, originalParent; 165 | 166 | original = _.cloneDeep(config); 167 | originalParent = _.cloneDeep(parent); 168 | actual = sort({}, config, parent, {}); 169 | expect(actual).to.deep.equal({ 170 | taskInfo: {}, 171 | taskConfig: { 172 | src: { 173 | globs: ['services/**/*.js', 'views/**/*.js'] 174 | }, 175 | dest: { 176 | path: 'lib' 177 | } 178 | }, 179 | subTaskConfigs: {} 180 | }); 181 | expect(config).to.deep.equal(original); 182 | expect(parent).to.deep.equal(originalParent); 183 | }); 184 | it('should put unknown properties to subTaskConfigs', function () { 185 | var config = { 186 | src: ['services/**/*.js', 'views/**/*.js'], 187 | dest: 'lib', 188 | bundles: { 189 | entries: ['a', 'b', 'c'] 190 | }, 191 | options: { 192 | extensions: ['.js', '.ts', '.coffee'] 193 | }, 194 | unknownProperty: 'what?' 195 | }; 196 | var parent = { 197 | src: { 198 | globs: ['src'] 199 | }, 200 | dest: { 201 | path: 'dist' 202 | } 203 | }; 204 | var schema = { 205 | properties: { 206 | bundles: { 207 | properties: { 208 | entries: {} 209 | } 210 | }, 211 | options: { 212 | } 213 | } 214 | }; 215 | var actual, original, originalParent; 216 | 217 | original = _.cloneDeep(config); 218 | originalParent = _.cloneDeep(parent); 219 | actual = sort({}, config, parent, schema); 220 | 221 | original = _.cloneDeep(config); 222 | expect(actual).to.deep.equal({ 223 | taskInfo: {}, 224 | taskConfig: { 225 | src: { 226 | globs: ['src/services/**/*.js', 'src/views/**/*.js'] 227 | }, 228 | dest: { 229 | path: 'dist/lib' 230 | }, 231 | bundles: { 232 | entries: ['a', 'b', 'c'] 233 | }, 234 | options: { 235 | extensions: ['.js', '.ts', '.coffee'] 236 | } 237 | }, 238 | subTaskConfigs: { 239 | unknownProperty: 'what?' 240 | } 241 | }); 242 | expect(config).to.deep.equal(original); 243 | expect(parent).to.deep.equal(originalParent); 244 | }); 245 | it('should extract title and description from schema if available', function () { 246 | var schema = { 247 | title: 'schema-extractor', 248 | description: 'extract title and description from schema if available' 249 | }; 250 | 251 | expect(sort({}, {}, {}, schema)).to.deep.equal({ 252 | taskInfo: { 253 | name: 'schema-extractor', 254 | description: 'extract title and description from schema if available' 255 | }, 256 | taskConfig: {}, 257 | subTaskConfigs: {} 258 | }); 259 | }); 260 | it('should normalize config using the given schema', function () { 261 | var schema = { 262 | definitions: { 263 | options: { 264 | properties: { 265 | extensions: { 266 | description: '', 267 | alias: ['extension'], 268 | type: 'array', 269 | items: { 270 | type: 'string' 271 | } 272 | }, 273 | require: { 274 | description: '', 275 | alias: ['requires'], 276 | type: 'array', 277 | items: { 278 | type: 'string' 279 | } 280 | }, 281 | external: { 282 | description: '', 283 | alias: ['externals'], 284 | type: 'array', 285 | items: { 286 | type: 'string' 287 | } 288 | }, 289 | plugin: { 290 | description: '', 291 | alias: ['plugins'], 292 | type: 'array', 293 | items: { 294 | type: 'string' 295 | } 296 | }, 297 | transform: { 298 | description: '', 299 | alias: ['transforms'], 300 | type: 'array', 301 | items: { 302 | type: 'string' 303 | } 304 | }, 305 | exclude: { 306 | description: '', 307 | alias: ['excludes'], 308 | type: 'array', 309 | items: { 310 | type: 'string' 311 | } 312 | }, 313 | ignore: { 314 | description: '', 315 | alias: ['ignores'], 316 | type: 'array', 317 | items: { 318 | type: 'string' 319 | } 320 | }, 321 | shim: { 322 | description: 'which library to shim?', 323 | alias: ['shims', 'browserify-shim', 'browserify-shims'], 324 | type: 'array', 325 | items: { 326 | type: 'string' 327 | } 328 | }, 329 | sourcemap: { 330 | description: 'generate sourcemap file or not?', 331 | alias: ['sourcemaps'], 332 | enum: [ 333 | 'inline', 'external', false 334 | ], 335 | default: false 336 | } 337 | } 338 | } 339 | }, 340 | properties: { 341 | options: { 342 | description: 'common options for all bundles', 343 | type: 'object', 344 | extends: { $ref: '#/definitions/options' } 345 | }, 346 | bundles: { 347 | description: '', 348 | alias: ['bundle'], 349 | type: 'array', 350 | items: { 351 | type: 'object', 352 | extends: { $ref: '#/definitions/options' }, 353 | properties: { 354 | file: { 355 | description: '', 356 | type: 'string' 357 | }, 358 | entries: { 359 | description: '', 360 | alias: ['entry'], 361 | type: 'array', 362 | items: { 363 | type: 'string' 364 | } 365 | }, 366 | options: { 367 | description: 'options for this bundle', 368 | type: 'object', 369 | extends: { $ref: '#/definitions/options' } 370 | } 371 | }, 372 | required: ['file', 'entries'] 373 | } 374 | } 375 | }, 376 | required: ['bundles'] 377 | }; 378 | var options = { 379 | extensions: ['.js', '.json', '.jsx', '.es6', '.ts'], 380 | plugin: ['tsify'], 381 | transform: ['brfs'] 382 | }; 383 | var config = { 384 | bundles: [{ 385 | file: 'deps.js', 386 | entries: [{ 387 | file: 'traceur/bin/traceur-runtime' 388 | }, { 389 | file: 'rtts_assert/rtts_assert' 390 | }, { 391 | file: 'reflect-propertydata' 392 | }, { 393 | file: 'zone.js' 394 | }], 395 | require: ['angular2/angular2', 'angular2/router'] 396 | }, { 397 | file: 'services.js', 398 | entry: 'services/*/index.js', 399 | external: ['angular2/angular2', 'angular2/router'], 400 | options: options 401 | }, { 402 | file: 'index.js', 403 | entry: 'index.js', 404 | external: './services', 405 | options: options 406 | }, { 407 | file: 'auth.js', 408 | entry: 'auth/index.js', 409 | external: './services', 410 | options: options 411 | }, { 412 | file: 'dashboard.js', 413 | entry: 'dashboard/index.js', 414 | external: './services', 415 | options: options 416 | }] 417 | }; 418 | var expected = { 419 | bundles: [{ 420 | file: 'deps.js', 421 | entries: [{ 422 | file: 'traceur/bin/traceur-runtime' 423 | }, { 424 | file: 'rtts_assert/rtts_assert' 425 | }, { 426 | file: 'reflect-propertydata' 427 | }, { 428 | file: 'zone.js' 429 | }], 430 | require: ['angular2/angular2', 'angular2/router'] 431 | }, { 432 | file: 'services.js', 433 | entries: ['services/*/index.js'], 434 | external: ['angular2/angular2', 'angular2/router'], 435 | options: options 436 | }, { 437 | file: 'index.js', 438 | entries: ['index.js'], 439 | external: ['./services'], 440 | options: options 441 | }, { 442 | file: 'auth.js', 443 | entries: ['auth/index.js'], 444 | external: ['./services'], 445 | options: options 446 | }, { 447 | file: 'dashboard.js', 448 | entries: ['dashboard/index.js'], 449 | external: ['./services'], 450 | options: options 451 | }] 452 | }; 453 | var actual, original; 454 | 455 | original = _.cloneDeep(config); 456 | actual = sort({}, config, {}, schema); 457 | expect(actual).to.deep.equal({ 458 | taskInfo: {}, 459 | taskConfig: expected, 460 | subTaskConfigs: { 461 | } 462 | }); 463 | expect(config).to.deep.equal(original); 464 | }); 465 | }); 466 | }); 467 | }); 468 | -------------------------------------------------------------------------------- /test/specs/prerequisite_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Chai = require('chai'); 4 | var expect = Chai.expect; 5 | 6 | var Stream = require('stream'); 7 | 8 | var gulp = require('gulp'); 9 | var _ = require('lodash'); 10 | 11 | function isGulp3() { 12 | return !!gulp.run; 13 | } 14 | 15 | function isGulp4() { 16 | return !!gulp.registry; 17 | } 18 | 19 | describe('Prerequisite', function () { 20 | if (isGulp3()) { 21 | describe('Gulp 3.X', function () { 22 | describe('.src()', function () { 23 | it('should always return a stream', function () { 24 | var stream; 25 | 26 | stream = gulp.src('non-existent'); 27 | expect(stream).to.be.an.instanceof(Stream); 28 | expect(stream).to.have.property('on'); 29 | }); 30 | }); 31 | }); 32 | } 33 | 34 | if (isGulp4()) { 35 | describe('Gulp 4.X', function () { 36 | describe('.src()', function () { 37 | it('should throw if target not exist', function () { 38 | if (isGulp4()) { 39 | expect(function () { 40 | gulp.src('non-existent'); 41 | }).to.throw; 42 | } 43 | }); 44 | }); 45 | }); 46 | } 47 | 48 | describe('Lodash', function () { 49 | describe('.defaultsDeep()', function () { 50 | it('should not merge string characters into array', function () { 51 | expect(_.defaultsDeep({ src: ['src'] }, { src: 'src' })).to.deep.equal({ 52 | src: ['src'] 53 | }); 54 | }); 55 | it('should not merge array items into object', function () { 56 | var target = { 57 | src: { 58 | globs: ['src'] 59 | } 60 | }; 61 | var source = { 62 | src: ['src', 'app'] 63 | }; 64 | var expected = { 65 | src: { 66 | globs: ['src'] 67 | } 68 | }; 69 | var actual = { 70 | src: { 71 | '0': 'src', 72 | '1': 'app', 73 | globs: ['src'] 74 | } 75 | }; 76 | expect(_.defaultsDeep(target, source)).to.deep.equal(expected); 77 | }); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/specs/recipe/factory_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Chai = require('chai'); 4 | var expect = Chai.expect; 5 | 6 | var base = process.cwd(); 7 | 8 | var ConfigurableRecipeFactory = require(base + '/lib/recipe/factory'); 9 | 10 | var FakeFactory = require(base + '/test/fake/factory'); 11 | var createFakeStuff = require(base + '/test/fake/stuff'); 12 | 13 | describe('Core', function () { 14 | describe('ConfigurableRecipeFactory', function () { 15 | var gulp, stuff, factory; 16 | 17 | gulp = FakeFactory.createFakeGulp(); 18 | stuff = createFakeStuff(); 19 | factory = new ConfigurableRecipeFactory(stuff, gulp.registry()); 20 | 21 | function done() { 22 | } 23 | 24 | function test(name, method) { 25 | var taskInfo = { 26 | name: name 27 | }; 28 | var rawConfig = { 29 | id: 'recipe-config' 30 | }; 31 | 32 | it('should return a ' + method + ' recipe', function () { 33 | var actual; 34 | 35 | actual = factory[method](taskInfo, rawConfig); 36 | expect(actual).to.be.a('function'); 37 | }); 38 | it('should refer to correct recipe', function () { 39 | var actual, context, lookup; 40 | 41 | lookup = stuff[method + 's'].lookup(name); 42 | context = { 43 | gulp: gulp, 44 | config: rawConfig, 45 | upstream: null 46 | }; 47 | 48 | actual = factory[method](taskInfo); 49 | expect(actual.schema.title).to.equal(name); 50 | 51 | actual.call(context, done); 52 | expect(lookup.called).to.be.true; 53 | expect(lookup.calledOn(context)).to.be.true; 54 | expect(lookup.calledWithExactly(done)).to.be.true; 55 | }); 56 | } 57 | 58 | describe('#flow()', function () { 59 | test('flow-task', 'flow'); 60 | }); 61 | describe('#reference()', function () { 62 | it('should always return a recipe even if the referring task not found', function () { 63 | var taskInfo = { 64 | name: 'reference-task', 65 | task: 'non-existent' 66 | }; 67 | var actual; 68 | 69 | actual = factory.reference(taskInfo); 70 | expect(actual).to.be.a('function'); 71 | }); 72 | it('should throw at runtime if the referring task not found', function () { 73 | var taskInfo = { 74 | name: 'reference-task', 75 | task: 'non-existent' 76 | }; 77 | var context = { 78 | gulp: gulp, 79 | config: {} 80 | }; 81 | var actual, expr; 82 | 83 | actual = factory.reference(taskInfo); 84 | expr = function () { 85 | actual.call(context, done); 86 | }; 87 | expect(expr).to.throw(Error); 88 | }); 89 | it('should wrap a normal gulp task', function () { 90 | var gulpTask = gulp.task('gulp-task'); 91 | var taskInfo = { 92 | name: 'reference-task', 93 | task: gulpTask.displayName 94 | }; 95 | var context = { 96 | gulp: gulp, 97 | config: {} 98 | }; 99 | var actual; 100 | 101 | actual = factory.reference(taskInfo); 102 | expect(actual).to.be.a('function'); 103 | actual.call(context, done); 104 | expect(gulpTask.calledOn(context)).to.be.true; 105 | expect(gulpTask.calledWithExactly(done)).to.be.true; 106 | }); 107 | it("should wrap a configurable task and call it at runtime", function () { 108 | var configurableTask = gulp.task('configurable-task'); 109 | var taskInfo = { 110 | name: 'reference-task', 111 | task: configurableTask.displayName 112 | }; 113 | var context = { 114 | gulp: gulp, 115 | config: {} 116 | }; 117 | var actual; 118 | 119 | actual = factory.reference(taskInfo); 120 | expect(actual).to.be.a('function'); 121 | actual.call(context, done); 122 | expect(configurableTask.calledOn(context)).to.be.true; 123 | expect(configurableTask.calledWithExactly(done)).to.be.true; 124 | }); 125 | }); 126 | describe('#stream()', function () { 127 | test('stream-task', 'stream'); 128 | }); 129 | describe('#task()', function () { 130 | test('task-task', 'task'); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/specs/recipe/registry_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Chai = require('chai'); 4 | var expect = Chai.expect; 5 | 6 | var base = process.cwd(); 7 | 8 | var ConfigurableRecipeRegistry = require(base + '/lib/recipe/registry'); 9 | 10 | describe('Core', function () { 11 | describe('ConfigurableRecipeRegistry', function () { 12 | describe('constructor()', function () { 13 | it('should take a hash object of tasks', function () { 14 | var registry; 15 | 16 | registry = new ConfigurableRecipeRegistry({ 17 | task: function () {} 18 | }); 19 | expect(registry).to.be.instanceof(ConfigurableRecipeRegistry); 20 | expect(registry.size()).to.equal(1); 21 | }); 22 | }); 23 | describe('#lookup()', function () { 24 | it('should return a function if found, otherwise, undefined', function () { 25 | var registry; 26 | 27 | registry = new ConfigurableRecipeRegistry({ 28 | task: function () {} 29 | }); 30 | expect(registry.lookup('task')).to.be.a('function'); 31 | expect(registry.lookup('none')).to.be.an('undefined'); 32 | }); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /test/specs/task/factory_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Sinon = require('sinon'); 4 | var Chai = require('chai'); 5 | var expect = Chai.expect; 6 | var test = require('mocha-cases'); 7 | 8 | var base = process.cwd(); 9 | 10 | var ConfigurableRecipeFactory = require(base + '/lib/recipe/factory'); 11 | var ConfigurableTaskFactory = require(base + '/lib/task/factory'); 12 | var Configuration = require(base + '/lib/configuration'); 13 | var Registry = require(base + '/lib/task/registry'); 14 | var Settings = require(base + '/lib/helpers/settings'); 15 | var expose = require(base + '/lib/task/expose'); 16 | 17 | var createFakeStuff = require(base + '/test/fake/stuff'); 18 | var FakeFactory = require(base + '/test/fake/factory'); 19 | 20 | function assertConfigurableTask(task, name) { 21 | expect(task).to.be.a('function'); 22 | expect(task.displayName).to.equal(name); 23 | } 24 | 25 | describe('Core', function () { 26 | describe('ConfigurableTaskFactory', function () { 27 | var gulp, registry, settings, factory, gulpTask, configurableTask; 28 | 29 | beforeEach(function () { 30 | var stuff; 31 | 32 | stuff = createFakeStuff(); 33 | registry = new Registry(); 34 | settings = new Settings(); 35 | gulp = FakeFactory.createFakeGulp(); 36 | gulp.registry(registry); 37 | factory = new ConfigurableTaskFactory(new ConfigurableRecipeFactory(stuff, registry), registry, expose(registry, settings)); 38 | gulpTask = gulp.task('gulp-task'); 39 | configurableTask = gulp.task('configurable-task'); 40 | }); 41 | 42 | describe('.getTaskRuntimeInfo()', function () { 43 | var testCases = [{ 44 | name: 'should accept normal task name', 45 | value: { 46 | name: 'build' 47 | }, 48 | expected: { 49 | name: 'build' 50 | } 51 | }, { 52 | name: 'should accept task name with space, underscore, dash', 53 | value: { 54 | name: '_build the-project' 55 | }, 56 | expected: { 57 | name: '_build the-project' 58 | } 59 | }, { 60 | name: 'should accept . prefix and mark task hidden', 61 | value: { 62 | name: '.build' 63 | }, 64 | expected: { 65 | name: 'build', 66 | visibility: 'hidden' 67 | } 68 | }, { 69 | name: 'should accept # prefix and mark task undefined', 70 | value: { 71 | name: '#build' 72 | }, 73 | expected: { 74 | name: 'build', 75 | visibility: 'disabled' 76 | } 77 | }, { 78 | name: 'should throw if invalid name', 79 | value: { 80 | name: 'build?!' 81 | }, 82 | error: Error 83 | }, { 84 | name: 'should throw if invalid name', 85 | value: { 86 | name: '?build' 87 | }, 88 | error: Error 89 | }, { 90 | name: 'should also accept properties from config', 91 | value: { 92 | name: 'build', 93 | description: 'description', 94 | order: 999, 95 | task: 'task', 96 | visibility: 'hidden' 97 | }, 98 | expected: { 99 | name: 'build', 100 | description: 'description', 101 | order: 999, 102 | task: 'task', 103 | visibility: 'hidden' 104 | } 105 | }, { 106 | name: 'should properties from raw-name override properties from config', 107 | value: { 108 | name: '#build', 109 | description: 'description', 110 | order: 999, 111 | task: 'task', 112 | visibility: 'hidden' 113 | }, 114 | expected: { 115 | name: 'build', 116 | description: 'description', 117 | order: 999, 118 | task: 'task', 119 | visibility: 'disabled' 120 | } 121 | }]; 122 | 123 | test(testCases, ConfigurableTaskFactory.getTaskRuntimeInfo); 124 | }); 125 | 126 | describe('#create()', function () { 127 | var taskInfo, taskConfig, recipe; 128 | 129 | function done() {} 130 | 131 | beforeEach(function () { 132 | taskInfo = { 133 | name: 'configurable-task', 134 | visibility: ConfigurableTaskFactory.CONSTANT.VISIBILITY.NORMAL 135 | }; 136 | 137 | taskConfig = { 138 | id: 'taskConfig' 139 | }; 140 | 141 | recipe = Sinon.spy(); 142 | }); 143 | 144 | it('should return a ConfigurableTask', function () { 145 | var actual; 146 | 147 | actual = factory.create('', taskInfo, taskConfig, recipe); 148 | assertConfigurableTask(actual, taskInfo.name); 149 | expect(actual.visibility).to.equal(taskInfo.visibility); 150 | }); 151 | it('should return a ConfigurableTask with the given name with prefix', function () { 152 | var actual, prefix; 153 | 154 | prefix = 'dev:'; 155 | actual = factory.create(prefix, taskInfo, taskConfig, recipe); 156 | assertConfigurableTask(actual, prefix + taskInfo.name); 157 | expect(actual.displayName).to.equal(prefix + taskInfo.name); 158 | expect(registry.get(prefix + taskInfo.name)).to.be.a('function'); 159 | }); 160 | it('should accept sub-tasks', function () { 161 | var actual, tasks; 162 | 163 | tasks = []; 164 | actual = factory.create('', taskInfo, taskConfig, recipe, tasks); 165 | expect(actual.tasks).to.be.an('array'); 166 | }); 167 | it('should invoke configurableRunner() method when act as a configurable task', function () { 168 | var context = { 169 | gulp: gulp, 170 | config: taskConfig 171 | }; 172 | var actual, call; 173 | 174 | actual = factory.create('', taskInfo, taskConfig, recipe); 175 | assertConfigurableTask(actual, taskInfo.name); 176 | 177 | actual.call(context, done); 178 | call = recipe.getCall(0); 179 | expect(call.thisValue.config).to.deep.equal(taskConfig); 180 | }); 181 | it('should invoke configurableRunner() when act as a gulp task: invoked directly', function () { 182 | var actual, call; 183 | 184 | actual = factory.create('', taskInfo, taskConfig, recipe); 185 | assertConfigurableTask(actual, taskInfo.name); 186 | 187 | actual(done); 188 | call = recipe.getCall(0); 189 | expect(call.thisValue.config).to.deep.equal(taskConfig); 190 | }); 191 | it('should be able to inject value and resolve config at runtime when act as a configurable task', function () { 192 | var templateConfig = { 193 | template: 'inject-value: {{inject-value}}' 194 | }; 195 | var injectConfig = { 196 | 'inject-value': 'resolved-value' 197 | }; 198 | var expectedConfig = { 199 | template: 'inject-value: resolved-value', 200 | 'inject-value': 'resolved-value' 201 | }; 202 | var context = { 203 | gulp: gulp, 204 | config: injectConfig, 205 | helper: Configuration 206 | }; 207 | var actual; 208 | 209 | actual = factory.create('', taskInfo, templateConfig, recipe); 210 | assertConfigurableTask(actual, taskInfo.name); 211 | 212 | actual.call(context, done); 213 | expect(recipe.thisValues[0].config).to.deep.equal(expectedConfig); 214 | }); 215 | }); 216 | describe('#one()', function () { 217 | it('should be able to resolve to a recipe task', function () { 218 | var name = 'task-task'; 219 | var config = { 220 | name: name 221 | }; 222 | var actual; 223 | 224 | actual = factory.one('', config, {}); 225 | assertConfigurableTask(actual, name); 226 | }); 227 | it('should be able to resolve to a flow task', function () { 228 | var name = 'flow-task'; 229 | var config = { 230 | name: name 231 | }; 232 | var expected = '<' + name + '>'; 233 | var actual; 234 | 235 | actual = factory.one('', config, {}); 236 | assertConfigurableTask(actual, expected); 237 | }); 238 | it('should be able to resolve to a stream task', function () { 239 | var name = 'stream-task'; 240 | var config = { 241 | name: name 242 | }; 243 | var expected = '<' + name + '>'; 244 | var actual; 245 | 246 | actual = factory.one('', config, {}); 247 | assertConfigurableTask(actual, expected); 248 | }); 249 | it('should resolve a non-existent-recipe-with-sub-task-configs to a parallel flow task', function () { 250 | var name = 'non-existent-recipe-with-sub-task-configs'; 251 | var config = { 252 | name: name, 253 | 'recipe-task': {}, 254 | 'stream-task': {}, 255 | 'non-existent-but-with-src-and-dest-defined': { 256 | src: 'src', 257 | dest: 'dist' 258 | } 259 | }; 260 | var actual; 261 | 262 | actual = factory.one('', config, {}); 263 | assertConfigurableTask(actual, name); 264 | expect(actual.tasks[0].displayName).to.equal(''); 265 | }); 266 | it('should not throw even can not resolve to a task', function () { 267 | var name = 'non-existent'; 268 | var config = { 269 | name: name 270 | }; 271 | var actual; 272 | 273 | actual = factory.one('', config, {}); 274 | assertConfigurableTask(actual, name); 275 | }); 276 | }); 277 | describe('#multiple()', function () { 278 | describe('when take subTaskConfigs as an array', function () { 279 | it('should returns an array', function () { 280 | var subTaskConfigs = [{ 281 | name: 'task-1' 282 | }, { 283 | name: 'task-2' 284 | }]; 285 | var actual = factory.multiple('', subTaskConfigs, {}); 286 | 287 | expect(actual).to.be.an('array'); 288 | expect(actual.length).to.equal(2); 289 | }); 290 | it('should process each config defined in subTaskConfigs', function () { 291 | var subTaskConfigs = [{ 292 | name: 'task-1' 293 | }, { 294 | name: 'task-2' 295 | }]; 296 | 297 | Sinon.spy(factory, 'one'); 298 | factory.multiple('', subTaskConfigs, {}); 299 | expect(factory.one.calledTwice).to.be.true; 300 | factory.one.restore(); 301 | }); 302 | it('should give tasks names if not provided', function () { 303 | var subTaskConfigs = [{ 304 | name: 'task-1' 305 | }, { 306 | options: {} 307 | }]; 308 | var actual = factory.multiple('', subTaskConfigs, {}); 309 | 310 | expect(actual[0].displayName).to.be.a('string'); 311 | expect(actual[1].displayName).to.be.a('string'); 312 | }); 313 | it('should dereference task references', function () { 314 | var subTaskConfigs = [ 315 | { name: 'task1' }, 316 | 'gulp-task-by-ref', // reference to registered gulp task 317 | 'configurable-task-by-ref', // reference to registered configurable task 318 | gulpTask, // registered gulp task 319 | configurableTask, // registered configurable task 320 | Sinon.spy() // stand-alone gulp task (not registered to gulp) 321 | ]; 322 | var actual = factory.multiple('', subTaskConfigs, {}); 323 | 324 | expect(actual.length).to.equal(6); 325 | expect(actual[0].displayName).to.equal('task1'); 326 | expect(actual[1].recipeInstance.displayName).to.equal('gulp-task-by-ref'); 327 | expect(actual[2].recipeInstance.displayName).to.equal('configurable-task-by-ref'); 328 | expect(actual[3].displayName).to.equal('gulp-task'); 329 | expect(actual[4].displayName).to.equal('configurable-task'); 330 | expect(actual[5].displayName).to.be.a('string'); 331 | }); 332 | }); 333 | describe('when take subTaskConfigs as an object', function () { 334 | it('should returns an array', function () { 335 | var subTaskConfigs = { 336 | 'task-1': {}, 337 | 'task-2': {} 338 | }; 339 | var actual = factory.multiple('', subTaskConfigs, {}); 340 | 341 | expect(actual).to.be.an('array'); 342 | expect(actual.length).to.equal(2); 343 | }); 344 | it('should process each config defined in subTaskConfigs', function () { 345 | var subTaskConfigs = { 346 | 'task-1': {}, 347 | 'task-2': {} 348 | }; 349 | 350 | Sinon.spy(factory, 'one'); 351 | factory.multiple('', subTaskConfigs, {}); 352 | expect(factory.one.calledTwice).to.be.true; 353 | factory.one.restore(); 354 | }); 355 | it('should sort tasks by "order" if provided', function () { 356 | var subTaskConfigs = { 357 | 'task-1': { 358 | order: 2 359 | }, 360 | 'task-2': { 361 | order: 1 362 | } 363 | }; 364 | var actual = factory.multiple('', subTaskConfigs, {}); 365 | 366 | expect(actual[0].displayName).to.equal('task-2'); 367 | expect(actual[1].displayName).to.equal('task-1'); 368 | }); 369 | it('should be order-stable: tasks not defined "order" property defaults to 0', function () { 370 | var subTaskConfigs = { 371 | 'task-1': {}, 372 | 'task-2': { 373 | order: 1 374 | }, 375 | 'task-3': { 376 | order: 2 377 | }, 378 | 'task-4': { 379 | order: 1 380 | }, 381 | 'task-5': 'gulp-task', 382 | 'task-6': { 383 | order: 0, 384 | task: 'configurable-task' 385 | } 386 | }; 387 | 388 | var actual = factory.multiple('', subTaskConfigs, {}); 389 | 390 | expect(actual[0].displayName).to.equal('task-1'); 391 | expect(actual[1].displayName).to.equal('task-5'); 392 | expect(actual[2].displayName).to.equal('task-6'); 393 | expect(actual[3].displayName).to.equal('task-2'); 394 | expect(actual[4].displayName).to.equal('task-4'); 395 | expect(actual[5].displayName).to.equal('task-3'); 396 | }); 397 | it('should dereference task references', function () { 398 | var subTaskConfigs = { 399 | task1: {}, // sub-config task 400 | task2: 'gulp-task-by-ref', // reference to registered gulp task 401 | task3: 'configurable-task-by-ref', // reference to registered configurable task 402 | task4: gulpTask, // registered gulp task 403 | task5: configurableTask, // registered configurable task 404 | task6: Sinon.spy() // stand-alone gulp task (not registered to gulp) 405 | }; 406 | var actual = factory.multiple('', subTaskConfigs, {}); 407 | 408 | expect(actual.length).to.equal(6); 409 | expect(actual[0].displayName).to.equal('task1'); 410 | expect(actual[1].displayName).to.equal('task2'); 411 | expect(actual[2].displayName).to.equal('task3'); 412 | expect(actual[3].displayName).to.equal('task4'); 413 | expect(actual[4].displayName).to.equal('task5'); 414 | expect(actual[5].displayName).to.equal('task6'); 415 | }); 416 | }); 417 | }); 418 | }); 419 | }); 420 | --------------------------------------------------------------------------------