├── .eslintrc ├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── SECURITY.md ├── bin └── grunt ├── docs └── README.md ├── internal-tasks └── subgrunt.js ├── lib ├── grunt.js ├── grunt │ ├── cli.js │ ├── config.js │ ├── event.js │ ├── fail.js │ ├── file.js │ ├── help.js │ ├── option.js │ ├── task.js │ └── template.js └── util │ └── task.js ├── package.json └── test ├── .eslintrc ├── fixtures ├── BOM.txt ├── Gruntfile-cli.js ├── a.js ├── b.js ├── banner.js ├── banner2.js ├── banner3.js ├── error.yaml ├── expand-mapping-ext │ ├── dir.ectory │ │ ├── file-no-extension │ │ └── sub.dir.ectory │ │ │ └── file.ext.ension │ └── file.ext.ension ├── expand │ ├── README.md │ ├── css │ │ ├── baz.css │ │ └── qux.css │ ├── deep │ │ ├── deep.txt │ │ └── deeper │ │ │ ├── deeper.txt │ │ │ └── deepest │ │ │ └── deepest.txt │ └── js │ │ ├── bar.js │ │ └── foo.js ├── files │ ├── dist │ │ ├── built-123-a.js │ │ ├── built-123-b.js │ │ ├── built-a.js │ │ ├── built-b.js │ │ └── built.js │ └── src │ │ ├── file1-123.js │ │ ├── file1.js │ │ ├── file2-123.js │ │ └── file2.js ├── iso-8859-1.json ├── iso-8859-1.txt ├── iso-8859-1.yaml ├── lint.txt ├── load-npm-tasks │ ├── node_modules │ │ └── grunt-foo-plugin │ │ │ ├── package.json │ │ │ └── tasks │ │ │ └── foo.js │ ├── package.json │ └── test-package │ │ └── package.json ├── no_BOM.txt ├── octocat.png ├── spawn-multibyte.js ├── spawn.js ├── template.txt ├── test.json ├── utf8.json ├── utf8.txt └── utf8.yaml ├── grunt ├── cli_test.js ├── config_test.js ├── event_test.js ├── file_test.js ├── option_test.js ├── task_test.js └── template_test.js ├── gruntfile ├── load-npm-tasks.js └── multi-task-files.js └── util └── task_test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "grunt", 3 | "root": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | /test/fixtures/*.txt text eol=lf 3 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | FORCE_COLOR: 2 7 | 8 | jobs: 9 | run: 10 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node: [16, 18, 20, 22] 17 | os: [ubuntu-latest, windows-latest] 18 | 19 | steps: 20 | - name: Clone repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node }} 27 | 28 | - name: Install npm dependencies 29 | run: npm i 30 | 31 | - name: Run tests 32 | run: npm test 33 | 34 | # We test multiple Windows shells because of prior stdout buffering issues 35 | # filed against Grunt. https://github.com/joyent/node/issues/3584 36 | - name: Run PowerShell tests 37 | run: "npm test # PowerShell" # Pass comment to PS for easier debugging 38 | shell: powershell 39 | if: startsWith(matrix.os, 'windows') 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .npm-debug.log 3 | package-lock.json 4 | tmp 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | "Cowboy" Ben Alman (http://benalman.com/) 2 | Kyle Robinson Young (http://dontkry.com/) 3 | Tyler Kellen (http://goingslowly.com) 4 | Sindre Sorhus (http://sindresorhus.com) 5 | Vlad Filippov (http://vladfilippov.com/) 6 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v1.6.1 2 | date: 2023-01-31 3 | changes: 4 | - Downgrades to glob 7 for Windows compatability 5 | - Removes mkdirp and rimraf in favour of node.js APIs. 6 | v1.6.0 7 | date: 2023-01-28 8 | changes: 9 | - Requires node.js 16+. 10 | - template.date now uses dateformat ~4.6.2. 11 | - other dependency updates such as glob, rimraf, etc. 12 | v1.5.3 13 | date: 2022-04-23 14 | changes: 15 | - Patch up race condition in symlink copying. 16 | v1.5.2 17 | date: 2022-04-12 18 | changes: 19 | - Unlink symlinks when copy destination is a symlink. 20 | v1.5.1 21 | date: 2022-04-11 22 | changes: 23 | - Fixed symlink destination handling. 24 | v1.5.0 25 | date: 2022-04-10 26 | changes: 27 | - Updated dependencies. 28 | - Add symlink handling for copying files. 29 | v1.4.1 30 | date: 2021-05-24 31 | changes: 32 | - Fix --preload option to be a known option 33 | - Switch to GitHub Actions 34 | v1.4.0 35 | date: 2021-04-21 36 | changes: 37 | - Security fixes in production and dev dependencies 38 | - Liftup/Liftoff upgrade breaking change. Update your scripts to use --preload instead of --require. Ref: https://github.com/js-cli/js-liftoff/commit/e7a969d6706e730d90abb4e24d3cb4d3bce06ddb. 39 | v1.3.0 40 | date: 2020-08-18 41 | changes: 42 | - Switch to use `safeLoad` for loading YML files via `file.readYAML`. 43 | - Upgrade legacy-log to ~3.0.0. 44 | - Upgrade legacy-util to ~2.0.0. 45 | v1.2.1 46 | date: 2020-07-07 47 | changes: 48 | - Remove path-is-absolute dependency. 49 | (PR: https://github.com/gruntjs/grunt/pull/1715) 50 | v1.2.0 51 | date: 2020-07-03 52 | changes: 53 | - Allow usage of grunt plugins that are located in any location that 54 | is visible to Node.js and NPM, instead of node_modules directly 55 | inside package that have a dev dependency to these plugins. 56 | (PR: https://github.com/gruntjs/grunt/pull/1677) 57 | - Removed coffeescript from dependencies. To ease transition, if 58 | coffeescript is still around, Grunt will attempt to load it. 59 | If it is not, and the user loads a CoffeeScript file, 60 | Grunt will print a useful error indicating that the 61 | coffeescript package should be installed as a dev dependency. 62 | This is considerably more user-friendly than dropping the require entirely, 63 | but doing so is feasible with the latest grunt-cli as users 64 | may simply use grunt --require coffeescript/register. 65 | (PR: https://github.com/gruntjs/grunt/pull/1675) 66 | - Exposes Grunt Option keys for ease of use. 67 | (PR: https://github.com/gruntjs/grunt/pull/1570) 68 | - Avoiding infinite loop on very long command names. 69 | (PR: https://github.com/gruntjs/grunt/pull/1697) 70 | v1.1.0 71 | date: 2020-03-16 72 | changes: 73 | - Update to mkdirp ~1.0.3 74 | - Only support versions of Node >= 8 75 | v1.0.4 76 | date: 2019-04-22 77 | changes: 78 | - Update js-yaml to address https://npmjs.com/advisories/788 79 | - Use SOURCE_DATE_EPOCH to render dates in template. 80 | v1.0.3 81 | date: 2018-06-03 82 | changes: 83 | - Drop support for Node 0.10 and 0.12. 84 | - Dependency updates: rimraf, grunt-legacy-log, grunt-legacy-util. 85 | - Fix race condition with file.mkdir. 86 | v1.0.2 87 | date: 2018-02-07 88 | changes: 89 | - Fix for readYAML error messages. 90 | - Remove deprecation warning for coffeescript. Pull #1621. 91 | v1.0.1 92 | date: 2016-04-05 93 | changes: 94 | - minor fix for npm issues when installing grunt and grunt-cli at the same time. Pull #1500. 95 | v1.0.0 96 | date: 2016-04-04 97 | changes: 98 | - full list of changes is on http://gruntjs.com, please also see changes from 1.0.0-rc1. 99 | - if you have a Grunt plugin that includes `grunt` in the `peerDependencies`, 100 | we recommend tagging with `"grunt": "">= 0.4.0"` and publishing a new version on npm. 101 | - Prevent async callback from being called multiple times. Pull #1464. 102 | - Update copyright to jQuery Foundation and remove redundant headers. Fixes #1478. 103 | - Update glob to 7.0.x. Fixes #1467. 104 | - Removing duplicate BOM strip code. Pull #1482. 105 | - Update legacy log and util to 1.0.0. 106 | - Update to latest cli ~1.2.0. 107 | - Use grunt-known-options for shared options between Grunt and grunt-cli. 108 | v1.0.0-rc1 109 | date: 2016-02-11 110 | changes: 111 | - full list of changes is on http://gruntjs.com 112 | - if you have a Grunt plugin that includes `grunt` in the `peerDependencies`, 113 | we recommend tagging with `"grunt": "">= 0.4.0"` 114 | - `coffee-script` is upgraded to `~1.10.0` which could incur breaking changes 115 | when using the language with plugins and Gruntfiles. 116 | - `nopt` is upgraded to `~3.0.6` which has fixed many issues, including passing 117 | multiple arguments and dealing with numbers as options. Be aware previously 118 | `--foo bar` used to pass the value `'bar'` to the option `foo`. It will now 119 | set the option `foo` to `true` and run the task `bar`. 120 | -`glob` is upgraded to `~6.0.4` and `minimatch` is upgraded to `~3.0.0`. Results 121 | are now sorted by default with `grunt.file.expandMapping()`. Pass the 122 | `nosort: true` option if you don't want the results to be sorted. 123 | - `lodash` was upgraded to `~4.3.0`. Many changes have occurred. Some of which 124 | that directly effect Grunt are `grunt.util._.template()` returns a compile 125 | function and `grunt.util._.flatten` no longer flattens deeply. 126 | `grunt.util._` is deprecated and we highly encourage you to 127 | `npm install lodash` and `var _ = require('lodash')` to use `lodash`. 128 | Please see the lodash changelog for a full list of changes: https://github.com/lodash/lodash/wiki/Changelog 129 | - `iconv-lite` is upgraded to `~0.4.13` and strips the BOM by default. 130 | - `js-yaml` is upgraded to `~3.5.2` and may affect `grunt.file.readYAML`. 131 | We encourage you to please `npm install js-yaml` and use 132 | `var YAML = require('js-yaml')` directly in case of future deprecations. 133 | - A file `mode` option can be passed into 134 | [grunt.file.write()](http://gruntjs.com/api/grunt.file#grunt.file.write). 135 | - `Done, without errors.` was changed to `Done.` to avoid failing by mistake 136 | on the word `errors`. 137 | v0.4.5: 138 | date: 2014-05-12 139 | changes: 140 | - Updated rimraf to 2.2.8. Closes gh-1134. 141 | - Moved grunt.log into separate grunt-legacy-log module. 142 | - Updated grunt-legacy-util to 0.2.0. Closes gh-971, gh-1129, gh-1118. 143 | - Added grunt.task.exists method to check if a task exists. Closes gh-1131. 144 | - Added grunt.config.merge method to deep merge config data. See gh-1039. 145 | - Fixed symlink issues with 'file.isPathCwd' and 'file.doesPathContain'. Closes gh-1112. 146 | - Config and util.recurse no longer mangle Buffer instances. See gh-971. 147 | - Config and util.recurse now enumerate inherited object properties. See gh-1129. 148 | - Config and util.recurse now throw useful circular reference error. See gh-1118. 149 | - Warn instead of error when no new tasks found via '.loadTasks' method. Closes gh-1059. 150 | - Added Windows CI testing. Closes gh-1110. 151 | - Removed "CONTRIBUTING.md" from .npmignore. Closes gh-1093. 152 | v0.4.4: 153 | date: 2014-03-12 154 | changes: 155 | - Only signal completion of tasks async if grunt.task.start is invoked with `{asyncDone:true}`. 156 | v0.4.3: 157 | date: 2014-03-07 158 | changes: 159 | - When devving Grunt, do "npm install && npm uninstall grunt" (isaacs/npm#3958) 160 | - Grunt is now tested on Node.js 0.11 161 | - Extracted internal "util" lib to "grunt-legacy-util" lib 162 | - task.normalizeMultiTaskFiles now flattens nested "files" arrays. Closes gh-1034. 163 | - Better error in renameTask if task doesn't exist. Closes gh-1058. 164 | - Update rimraf to latest version. Closes gh-1043. 165 | - Empty string "ext" should strip extension. Closes gh-1087. 166 | - Add expandMapping .extDot option. Can be 'first' or 'last' but defaults to 'first'. Closes gh-979. 167 | - Add default array for util.spawn optional args. Closes gh-1064. 168 | - util.spawn "grunt" option now uses proper Node, passes Node exec options. Closes gh-980, gh-981, gh-877. 169 | - Make all tasks asynchronous to reduce call stack. Closes gh-1026. 170 | - Fix <%= grunt.task.current.target %> in Multitask files. Closes gh-994. 171 | - Generalize cli tests, see gh-983, gh-991. 172 | - --debug option can optionally be Boolean. Closes Gh-983, gh-991. 173 | v0.4.2: 174 | date: 2013-11-21 175 | changes: 176 | - Extract internal "namespace" lib to external "getobject" lib. 177 | - '"Grunt collections" are now deprecated, use peerDependencies. See "grunt-contrib" 0.8.0 for details.' 178 | - Fix stdout / stderr issues on Windows. Closes gh-940, gh-921, gh-744, gh-792, gh-644, gh-708. 179 | - Fix pipe-redirecting on Windows. Closes gh-510. 180 | - Fixed this.options() in renamed basic tasks. Closes gh-855. 181 | - Update underscore.string dependency to follow semver. Closes gh-886. 182 | - Output task options in verbose mode. Closes gh-749. 183 | - Add file.preserveBOM property. Closes gh-806, gh-937. 184 | - Test that file methods warn. Closes gh-909. 185 | - Fixed a few spelling errors in code comments. Closes gh-849. 186 | - Updated watch, jshint and nodeunit deps. Closes gh-914. 187 | v0.4.1: 188 | date: 2013-03-13 189 | changes: 190 | - Fix path.join thrown errors with expandMapping rename. Closes gh-725. 191 | - Update copyright date to 2013. Closes gh-660. 192 | - Remove some side effects from manually requiring Grunt. Closes gh-605. 193 | - "grunt.log: add formatting support and implicitly cast msg to a string. Closes gh-703." 194 | - Update js-yaml to version 2. Closes gh-683. 195 | - The grunt.util.spawn method now falls back to stdout when the `grunt` option is set. Closes gh-691. 196 | - Making --verbose "Files:" warnings less scary. Closes gh-657. 197 | - "Fixing typo: the grunt.fatal method now defaults to FATAL_ERROR. Closes gh-656, gh-707." 198 | - Removed a duplicate line. Closes gh-702. 199 | - Gruntfile name should no longer be case sensitive. Closes gh-685. 200 | - The grunt.file.delete method warns and returns false if file doesn't exist. Closes gh-635, gh-714. 201 | - The grunt.package property is now resolved via require(). Closes gh-704. 202 | - The grunt.util.spawn method no longer breaks on multibyte stdio. Closes gh-710. 203 | - Fix "path.join arguments must be strings" error in file.expand/recurse when options.cwd is not set. Closes gh-722. 204 | - Adding a fairly relevant keyword to package.json (task). 205 | v0.4.0: 206 | date: 2013-02-18 207 | changes: 208 | - Initial release of 0.4.0. 209 | - See http://gruntjs.com/upgrading-from-0.3-to-0.4 for a list of changes / migration guide. 210 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Grunt, The OpenJS Foundation and its member projects use [Contributor Covenant v2.0](https://contributor-covenant.org/version/2/0/code_of_conduct) as their code of conduct. The full text is included [below](#contributor-covenant-code-of-conduct) in English, and translations are available from the Contributor Covenant organisation: 4 | 5 | - [contributor-covenant.org/translations](https://www.contributor-covenant.org/translations) 6 | - [github.com/ContributorCovenant](https://github.com/ContributorCovenant/contributor_covenant/tree/release/content/version/2/0) 7 | 8 | Refer to the sections on reporting and escalation in this document for the specific emails that can be used to report and escalate issues. 9 | 10 | ## Reporting 11 | 12 | ### Project Spaces 13 | 14 | For reporting issues in spaces related to a member project please use the email provided by the project for reporting. Projects handle CoC issues related to the spaces that they maintain. Projects maintainers commit to: 15 | 16 | - maintain the confidentiality with regard to the reporter of an incident 17 | - to participate in the path for escalation as outlined in 18 | the section on Escalation when required. 19 | 20 | ### Foundation Spaces 21 | 22 | For reporting issues in spaces managed by the OpenJS Foundation, for example, repositories within the OpenJS organization, use the email `report@lists.openjsf.org`. The Cross Project Council (CPC) is responsible for managing these reports and commits to: 23 | 24 | - maintain the confidentiality with regard to the reporter of an incident 25 | - to participate in the path for escalation as outlined in 26 | the section on Escalation when required. 27 | 28 | ## Escalation 29 | 30 | The OpenJS Foundation maintains a Code of Conduct Panel (CoCP). This is a foundation-wide team established to manage escalation when a reporter believes that a report to a member project or the CPC has not been properly handled. In order to escalate to the CoCP send an email to `coc-escalation@lists.openjsf.org`. 31 | 32 | For more information, refer to the full 33 | [Code of Conduct governance document](https://github.com/openjs-foundation/bootstrap/blob/master/proposals/stage-2/CODE_OF_CONDUCT/FOUNDATION_CODE_OF_CONDUCT_REQUIREMENTS.md). 34 | 35 | --- 36 | 37 | ## Contributor Covenant Code of Conduct v2.0 38 | 39 | ### Our Pledge 40 | 41 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 42 | 43 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 44 | 45 | ### Our Standards 46 | 47 | Examples of behavior that contributes to a positive environment for our community include: 48 | 49 | * Demonstrating empathy and kindness toward other people 50 | * Being respectful of differing opinions, viewpoints, and experiences 51 | * Giving and gracefully accepting constructive feedback 52 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 53 | * Focusing on what is best not just for us as individuals, but for the overall community 54 | 55 | Examples of unacceptable behavior include: 56 | 57 | * The use of sexualized language or imagery, and sexual attention or advances of any kind 58 | * Trolling, insulting or derogatory comments, and personal or political attacks 59 | * Public or private harassment 60 | * Publishing others' private information, such as a physical or email address, without their explicit permission 61 | * Other conduct which could reasonably be considered inappropriate in a professional setting 62 | 63 | ### Enforcement Responsibilities 64 | 65 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 66 | 67 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 68 | 69 | ### Scope 70 | 71 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at the email addresses listed above in the [Reporting](#reporting) and [Escalation](#escalation) sections. All complaints will be reviewed and investigated promptly and fairly. 76 | 77 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 78 | 79 | ### Enforcement Guidelines 80 | 81 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 82 | 83 | #### 1. Correction 84 | 85 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 86 | 87 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 88 | 89 | #### 2. Warning 90 | 91 | **Community Impact**: A violation through a single incident or series of actions. 92 | 93 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 94 | 95 | #### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 100 | 101 | #### 4. Permanent Ban 102 | 103 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 104 | 105 | **Consequence**: A permanent ban from any sort of public interaction within the project community. 106 | 107 | ### Attribution 108 | 109 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at [contributor-covenant.org/version/2/0/code_of_conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct). 110 | 111 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 112 | 113 | For answers to common questions about this code of conduct, see the FAQ at 114 | [contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are available at [contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). 115 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please see the [Contributing to grunt](http://gruntjs.com/contributing) guide for information on contributing to this project. 2 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | nodeunit: { 8 | all: ['test/{grunt,tasks,util}/**/*.js'], 9 | tap: { 10 | src: '<%= nodeunit.all %>', 11 | options: { 12 | reporter: 'tap', 13 | reporterOutput: 'tests.tap' 14 | } 15 | } 16 | }, 17 | eslint: { 18 | gruntfileTasks: ['Gruntfile.js', 'internal-tasks/*.js'], 19 | libsAndTests: ['lib/**/*.js', '<%= nodeunit.all %>'], 20 | subgrunt: ['<%= subgrunt.all %>'] 21 | }, 22 | watch: { 23 | gruntfileTasks: { 24 | files: ['<%= eslint.gruntfileTasks %>'], 25 | tasks: ['eslint:gruntfileTasks'] 26 | }, 27 | libsAndTests: { 28 | files: ['<%= eslint.libsAndTests %>'], 29 | tasks: ['eslint:libsAndTests', 'nodeunit'] 30 | }, 31 | subgrunt: { 32 | files: ['<%= subgrunt.all %>'], 33 | tasks: ['eslint:subgrunt', 'subgrunt'] 34 | } 35 | }, 36 | subgrunt: { 37 | all: ['test/gruntfile/*.js'] 38 | }, 39 | }); 40 | 41 | // These plugins provide necessary tasks. 42 | grunt.loadNpmTasks('grunt-eslint'); 43 | grunt.loadNpmTasks('grunt-contrib-nodeunit'); 44 | grunt.loadNpmTasks('grunt-contrib-watch'); 45 | 46 | // Some internal tasks. Maybe someday these will be released. 47 | grunt.loadTasks('internal-tasks'); 48 | 49 | // "npm test" runs these tasks 50 | grunt.registerTask('test', '', function(reporter) { 51 | grunt.task.run(['eslint', 'nodeunit:' + (reporter || 'all'), 'subgrunt']); 52 | }); 53 | 54 | // Default task. 55 | grunt.registerTask('default', ['test']); 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright jQuery Foundation and other contributors, https://jquery.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/gruntjs/grunt . 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules directory are externally maintained 34 | libraries used by this software which have their own licenses; we recommend 35 | you read them, as their terms may differ from the terms above. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Grunt: The JavaScript Task Runner 2 | 3 | [![Built with Grunt](https://cdn.gruntjs.com/builtwith.svg)](http://gruntjs.com/) 4 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fgruntjs%2Fgrunt.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fgruntjs%2Fgrunt?ref=badge_shield) 5 | 6 | 7 | 8 | ### Documentation 9 | 10 | Visit the [gruntjs.com](https://gruntjs.com/) website for all the things. 11 | 12 | ### Support 13 | 14 | We support the latest version with security and bug fixes. The previous versions are all end-of-life and will not receive any security or bug fixes. 15 | 16 | Our OpenJS Ecosystem Sustainability Program partner [HeroDevs](https://www.herodevs.com/support#request-technologies) provides drop-in replacements for older versions of Grunt that are kept up-to-date for security and compliance issues. Learn More. 17 | 18 | ### Version Support 19 | 20 | | Version | Supported? | Commercial Support | 21 | | ------- | ---------- | ----------------------------------------------------------------------- | 22 | | 1.6 | YES | | 23 | | 1.5 | NO | [Available Here](https://www.herodevs.com/support#request-technologies) | 24 | | 1.4 | NO | [Available Here](https://www.herodevs.com/support#request-technologies) | 25 | | 1.3 | NO | [Available Here](https://www.herodevs.com/support#request-technologies) | 26 | | 1.2 | NO | [Available Here](https://www.herodevs.com/support#request-technologies) | 27 | | 1.1 | NO | [Available Here](https://www.herodevs.com/support#request-technologies) | 28 | | 1.0 | NO | [Available Here](https://www.herodevs.com/support#request-technologies) | 29 | | 0.4 | NO | [Available Here](https://www.herodevs.com/support#request-technologies) | 30 | 31 | ### Contributing 32 | 33 | Before you make an issue, please read our [Contributing](https://gruntjs.com/contributing) guide. 34 | 35 | ### Release History 36 | 37 | See the [CHANGELOG](CHANGELOG). 38 | 39 | ### License 40 | 41 | [MIT](LICENSE) 42 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Reporting a Vulnerability 2 | 3 | If you discover a security vulnerability within grunt, please submit a report via the Github's Private Vulnerability Reporting feature. 4 | 5 | All security vulnerabilities will be promptly addressed. 6 | -------------------------------------------------------------------------------- /bin/grunt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('grunt-cli/bin/grunt'); 4 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Visit the [gruntjs.com](https://gruntjs.com/) website for all the things. 2 | -------------------------------------------------------------------------------- /internal-tasks/subgrunt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | // Run sub-grunt files, because right now, testing tasks is a pain. 6 | grunt.registerMultiTask('subgrunt', 'Run a sub-gruntfile.', function() { 7 | var path = require('path'); 8 | grunt.util.async.forEachSeries(this.filesSrc, function(gruntfile, next) { 9 | grunt.log.write('Loading ' + gruntfile + '...'); 10 | grunt.util.spawn({ 11 | grunt: true, 12 | args: ['--gruntfile', path.resolve(gruntfile)], 13 | }, function(error, result) { 14 | if (error) { 15 | grunt.log.error().error(result.stdout).writeln(); 16 | next(new Error('Error running sub-gruntfile "' + gruntfile + '".')); 17 | } else { 18 | grunt.log.ok().verbose.ok(result.stdout); 19 | next(); 20 | } 21 | }); 22 | }, this.async()); 23 | }); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /lib/grunt.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Nodejs libs. 4 | var path = require('path'); 5 | 6 | // This allows grunt to require() .coffee files. 7 | try { 8 | // Note: grunt no longer depends on CoffeeScript, it will only use it if it is intentionally 9 | // installed in the project. 10 | require('coffeescript/register'); 11 | } catch (e) { 12 | // This is fine, and will cause no problems so long as the user doesn't load .coffee files. 13 | // Print a useful error if we attempt to load a .coffee file. 14 | if (require.extensions) { 15 | var FILE_EXTENSIONS = ['.coffee', '.litcoffee', '.coffee.md']; 16 | for (var i = 0; i < FILE_EXTENSIONS.length; i++) { 17 | require.extensions[FILE_EXTENSIONS[i]] = function() { 18 | throw new Error( 19 | 'Grunt attempted to load a .coffee file but CoffeeScript was not installed.\n' + 20 | 'Please run `npm install --dev coffeescript` to enable loading CoffeeScript.' 21 | ); 22 | }; 23 | } 24 | } 25 | } 26 | 27 | // The module to be exported. 28 | var grunt = module.exports = {}; 29 | 30 | // Expose internal grunt libs. 31 | function gRequire(name) { 32 | return grunt[name] = require('./grunt/' + name); 33 | } 34 | 35 | var util = require('grunt-legacy-util'); 36 | grunt.util = util; 37 | grunt.util.task = require('./util/task'); 38 | 39 | var Log = require('grunt-legacy-log').Log; 40 | var log = new Log({grunt: grunt}); 41 | grunt.log = log; 42 | 43 | gRequire('template'); 44 | gRequire('event'); 45 | var fail = gRequire('fail'); 46 | gRequire('file'); 47 | var option = gRequire('option'); 48 | var config = gRequire('config'); 49 | var task = gRequire('task'); 50 | var help = gRequire('help'); 51 | gRequire('cli'); 52 | var verbose = grunt.verbose = log.verbose; 53 | 54 | // Expose some grunt metadata. 55 | grunt.package = require('../package.json'); 56 | grunt.version = grunt.package.version; 57 | 58 | // Expose specific grunt lib methods on grunt. 59 | function gExpose(obj, methodName, newMethodName) { 60 | grunt[newMethodName || methodName] = obj[methodName].bind(obj); 61 | } 62 | gExpose(task, 'registerTask'); 63 | gExpose(task, 'registerMultiTask'); 64 | gExpose(task, 'registerInitTask'); 65 | gExpose(task, 'renameTask'); 66 | gExpose(task, 'loadTasks'); 67 | gExpose(task, 'loadNpmTasks'); 68 | gExpose(config, 'init', 'initConfig'); 69 | gExpose(fail, 'warn'); 70 | gExpose(fail, 'fatal'); 71 | 72 | // Expose the task interface. I've never called this manually, and have no idea 73 | // how it will work. But it might. 74 | grunt.tasks = function(tasks, options, done) { 75 | // Update options with passed-in options. 76 | option.init(options); 77 | 78 | // Display the grunt version and quit if the user did --version. 79 | var _tasks, _options; 80 | if (option('version')) { 81 | // Not --verbose. 82 | log.writeln('grunt v' + grunt.version); 83 | 84 | if (option('verbose')) { 85 | // --verbose 86 | verbose.writeln('Install path: ' + path.resolve(__dirname, '..')); 87 | // Yes, this is a total hack, but we don't want to log all that verbose 88 | // task initialization stuff here. 89 | grunt.log.muted = true; 90 | // Initialize task system so that available tasks can be listed. 91 | grunt.task.init([], {help: true}); 92 | // Re-enable logging. 93 | grunt.log.muted = false; 94 | 95 | // Display available tasks (for shell completion, etc). 96 | _tasks = Object.keys(grunt.task._tasks).sort(); 97 | verbose.writeln('Available tasks: ' + _tasks.join(' ')); 98 | 99 | // Display available options (for shell completion, etc). 100 | _options = []; 101 | Object.keys(grunt.cli.optlist).forEach(function(long) { 102 | var o = grunt.cli.optlist[long]; 103 | _options.push('--' + (o.negate ? 'no-' : '') + long); 104 | if (o.short) { _options.push('-' + o.short); } 105 | }); 106 | verbose.writeln('Available options: ' + _options.join(' ')); 107 | } 108 | 109 | return; 110 | } 111 | 112 | // Init colors. 113 | log.initColors(); 114 | 115 | // Display help and quit if the user did --help. 116 | if (option('help')) { 117 | help.display(); 118 | return; 119 | } 120 | 121 | // A little header stuff. 122 | verbose.header('Initializing').writeflags(option.flags(), 'Command-line options'); 123 | 124 | // Determine and output which tasks will be run. 125 | var tasksSpecified = tasks && tasks.length > 0; 126 | tasks = task.parseArgs([tasksSpecified ? tasks : 'default']); 127 | 128 | // Initialize tasks. 129 | task.init(tasks, options); 130 | 131 | verbose.writeln(); 132 | if (!tasksSpecified) { 133 | verbose.writeln('No tasks specified, running default tasks.'); 134 | } 135 | verbose.writeflags(tasks, 'Running tasks'); 136 | 137 | // Handle otherwise unhandleable (probably asynchronous) exceptions. 138 | var uncaughtHandler = function(e) { 139 | fail.fatal(e, fail.code.TASK_FAILURE); 140 | }; 141 | process.on('uncaughtException', uncaughtHandler); 142 | 143 | // Report, etc when all tasks have completed. 144 | task.options({ 145 | error: function(e) { 146 | fail.warn(e, fail.code.TASK_FAILURE); 147 | }, 148 | done: function() { 149 | // Stop handling uncaught exceptions so that we don't leave any 150 | // unwanted process-level side effects behind. There is no need to do 151 | // this in the error callback, because fail.warn() will either kill 152 | // the process, or with --force keep on going all the way here. 153 | process.removeListener('uncaughtException', uncaughtHandler); 154 | 155 | // Output a final fail / success report. 156 | fail.report(); 157 | 158 | if (done) { 159 | // Execute "done" function when done (only if passed, of course). 160 | done(); 161 | } else { 162 | // Otherwise, explicitly exit. 163 | util.exit(0); 164 | } 165 | } 166 | }); 167 | 168 | // Execute all tasks, in order. Passing each task individually in a forEach 169 | // allows the error callback to execute multiple times. 170 | tasks.forEach(function(name) { task.run(name); }); 171 | // Run tasks async internally to reduce call-stack, per: 172 | // https://github.com/gruntjs/grunt/pull/1026 173 | task.start({asyncDone: true}); 174 | }; 175 | -------------------------------------------------------------------------------- /lib/grunt/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../grunt'); 4 | 5 | // External libs. 6 | var nopt = require('nopt'); 7 | var gruntOptions = require('grunt-known-options'); 8 | 9 | // This is only executed when run via command line. 10 | var cli = module.exports = function(options, done) { 11 | // CLI-parsed options override any passed-in "default" options. 12 | if (options) { 13 | // For each default option... 14 | Object.keys(options).forEach(function(key) { 15 | if (!(key in cli.options)) { 16 | // If this option doesn't exist in the parsed cli.options, add it in. 17 | cli.options[key] = options[key]; 18 | } else if (cli.optlist[key].type === Array) { 19 | // If this option's type is Array, append it to any existing array 20 | // (or create a new array). 21 | [].push.apply(cli.options[key], options[key]); 22 | } 23 | }); 24 | } 25 | 26 | // Run tasks. 27 | grunt.tasks(cli.tasks, cli.options, done); 28 | }; 29 | 30 | // Default options. 31 | var optlist = cli.optlist = gruntOptions; 32 | 33 | // Parse `optlist` into a form that nopt can handle. 34 | var aliases = {}; 35 | var known = {}; 36 | 37 | Object.keys(optlist).forEach(function(key) { 38 | var short = optlist[key].short; 39 | if (short) { 40 | aliases[short] = '--' + key; 41 | } 42 | known[key] = optlist[key].type; 43 | }); 44 | 45 | var parsed = nopt(known, aliases, process.argv, 2); 46 | cli.tasks = parsed.argv.remain; 47 | cli.options = parsed; 48 | delete parsed.argv; 49 | 50 | // Initialize any Array options that weren't initialized. 51 | Object.keys(optlist).forEach(function(key) { 52 | if (optlist[key].type === Array && !(key in cli.options)) { 53 | cli.options[key] = []; 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /lib/grunt/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../grunt'); 4 | 5 | // Get/set config data. If value was passed, set. Otherwise, get. 6 | var config = module.exports = function(prop, value) { 7 | if (arguments.length === 2) { 8 | // Two arguments were passed, set the property's value. 9 | return config.set(prop, value); 10 | } else { 11 | // Get the property's value (or the entire data object). 12 | return config.get(prop); 13 | } 14 | }; 15 | 16 | // The actual config data. 17 | config.data = {}; 18 | 19 | // Escape any . in name with \. so dot-based namespacing works properly. 20 | config.escape = function(str) { 21 | return str.replace(/\./g, '\\.'); 22 | }; 23 | 24 | // Return prop as a string. 25 | config.getPropString = function(prop) { 26 | return Array.isArray(prop) ? prop.map(config.escape).join('.') : prop; 27 | }; 28 | 29 | // Get raw, unprocessed config data. 30 | config.getRaw = function(prop) { 31 | if (prop) { 32 | // Prop was passed, get that specific property's value. 33 | return grunt.util.namespace.get(config.data, config.getPropString(prop)); 34 | } else { 35 | // No prop was passed, return the entire config.data object. 36 | return config.data; 37 | } 38 | }; 39 | 40 | // Match '<%= FOO %>' where FOO is a propString, eg. foo or foo.bar but not 41 | // a method call like foo() or foo.bar(). 42 | var propStringTmplRe = /^<%=\s*([a-z0-9_$]+(?:\.[a-z0-9_$]+)*)\s*%>$/i; 43 | 44 | // Get config data, recursively processing templates. 45 | config.get = function(prop) { 46 | return config.process(config.getRaw(prop)); 47 | }; 48 | 49 | // Expand a config value recursively. Used for post-processing raw values 50 | // already retrieved from the config. 51 | config.process = function(raw) { 52 | return grunt.util.recurse(raw, function(value) { 53 | // If the value is not a string, return it. 54 | if (typeof value !== 'string') { return value; } 55 | // If possible, access the specified property via config.get, in case it 56 | // doesn't refer to a string, but instead refers to an object or array. 57 | var matches = value.match(propStringTmplRe); 58 | var result; 59 | if (matches) { 60 | result = config.get(matches[1]); 61 | // If the result retrieved from the config data wasn't null or undefined, 62 | // return it. 63 | if (result != null) { return result; } 64 | } 65 | // Process the string as a template. 66 | return grunt.template.process(value, {data: config.data}); 67 | }); 68 | }; 69 | 70 | // Set config data. 71 | config.set = function(prop, value) { 72 | return grunt.util.namespace.set(config.data, config.getPropString(prop), value); 73 | }; 74 | 75 | // Deep merge config data. 76 | config.merge = function(obj) { 77 | grunt.util._.merge(config.data, obj); 78 | return config.data; 79 | }; 80 | 81 | // Initialize config data. 82 | config.init = function(obj) { 83 | grunt.verbose.write('Initializing config...').ok(); 84 | // Initialize and return data. 85 | return (config.data = obj || {}); 86 | }; 87 | 88 | // Test to see if required config params have been defined. If not, throw an 89 | // exception (use this inside of a task). 90 | config.requires = function() { 91 | var p = grunt.util.pluralize; 92 | var props = grunt.util.toArray(arguments).map(config.getPropString); 93 | var msg = 'Verifying propert' + p(props.length, 'y/ies') + 94 | ' ' + grunt.log.wordlist(props) + ' exist' + p(props.length, 's') + 95 | ' in config...'; 96 | grunt.verbose.write(msg); 97 | var failProps = config.data && props.filter(function(prop) { 98 | return config.get(prop) == null; 99 | }).map(function(prop) { 100 | return '"' + prop + '"'; 101 | }); 102 | if (config.data && failProps.length === 0) { 103 | grunt.verbose.ok(); 104 | return true; 105 | } else { 106 | grunt.verbose.or.write(msg); 107 | grunt.log.error().error('Unable to process task.'); 108 | if (!config.data) { 109 | throw grunt.util.error('Unable to load config.'); 110 | } else { 111 | throw grunt.util.error('Required config propert' + 112 | p(failProps.length, 'y/ies') + ' ' + failProps.join(', ') + ' missing.'); 113 | } 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /lib/grunt/event.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // External lib. 4 | var EventEmitter2 = require('eventemitter2').EventEmitter2; 5 | 6 | // Awesome. 7 | module.exports = new EventEmitter2({wildcard: true}); 8 | -------------------------------------------------------------------------------- /lib/grunt/fail.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../grunt'); 4 | 5 | // The module to be exported. 6 | var fail = module.exports = {}; 7 | 8 | // Error codes. 9 | fail.code = { 10 | FATAL_ERROR: 1, 11 | MISSING_GRUNTFILE: 2, 12 | TASK_FAILURE: 3, 13 | TEMPLATE_ERROR: 4, 14 | INVALID_AUTOCOMPLETE: 5, 15 | WARNING: 6, 16 | }; 17 | 18 | // DRY it up! 19 | function writeln(e, mode) { 20 | grunt.log.muted = false; 21 | var msg = String(e.message || e); 22 | if (!grunt.option('no-color')) { msg += '\x07'; } // Beep! 23 | if (mode === 'warn') { 24 | msg = 'Warning: ' + msg + ' '; 25 | msg += (grunt.option('force') ? 'Used --force, continuing.'.underline : 'Use --force to continue.'); 26 | msg = msg.yellow; 27 | } else { 28 | msg = ('Fatal error: ' + msg).red; 29 | } 30 | grunt.log.writeln(msg); 31 | } 32 | 33 | // If --stack is enabled, log the appropriate error stack (if it exists). 34 | function dumpStack(e) { 35 | if (grunt.option('stack')) { 36 | if (e.origError && e.origError.stack) { 37 | console.log(e.origError.stack); 38 | } else if (e.stack) { 39 | console.log(e.stack); 40 | } 41 | } 42 | } 43 | 44 | // A fatal error occurred. Abort immediately. 45 | fail.fatal = function(e, errcode) { 46 | writeln(e, 'fatal'); 47 | dumpStack(e); 48 | grunt.util.exit(typeof errcode === 'number' ? errcode : fail.code.FATAL_ERROR); 49 | }; 50 | 51 | // Keep track of error and warning counts. 52 | fail.errorcount = 0; 53 | fail.warncount = 0; 54 | 55 | // A warning occurred. Abort immediately unless -f or --force was used. 56 | fail.warn = function(e, errcode) { 57 | var message = typeof e === 'string' ? e : e.message; 58 | fail.warncount++; 59 | writeln(message, 'warn'); 60 | // If -f or --force aren't used, stop script processing. 61 | if (!grunt.option('force')) { 62 | dumpStack(e); 63 | grunt.log.writeln().fail('Aborted due to warnings.'); 64 | grunt.util.exit(typeof errcode === 'number' ? errcode : fail.code.WARNING); 65 | } 66 | }; 67 | 68 | // This gets called at the very end. 69 | fail.report = function() { 70 | if (fail.warncount > 0) { 71 | grunt.log.writeln().fail('Done, but with warnings.'); 72 | } else { 73 | grunt.log.writeln().success('Done.'); 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /lib/grunt/file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../grunt'); 4 | 5 | // Nodejs libs. 6 | var fs = require('fs'); 7 | var path = require('path'); 8 | 9 | // The module to be exported. 10 | var file = module.exports = {}; 11 | 12 | // External libs. 13 | file.glob = require('glob'); 14 | file.minimatch = require('minimatch'); 15 | file.findup = require('findup-sync'); 16 | var YAML = require('js-yaml'); 17 | var iconv = require('iconv-lite'); 18 | 19 | // Windows? 20 | var win32 = process.platform === 'win32'; 21 | 22 | // Normalize \\ paths to / paths. 23 | var unixifyPath = function(filepath) { 24 | if (win32) { 25 | return filepath.replace(/\\/g, '/'); 26 | } else { 27 | return filepath; 28 | } 29 | }; 30 | 31 | // Change the current base path (ie, CWD) to the specified path. 32 | file.setBase = function() { 33 | var dirpath = path.join.apply(path, arguments); 34 | process.chdir(dirpath); 35 | }; 36 | 37 | // Process specified wildcard glob patterns or filenames against a 38 | // callback, excluding and uniquing files in the result set. 39 | var processPatterns = function(patterns, fn) { 40 | // Filepaths to return. 41 | var result = []; 42 | // Iterate over flattened patterns array. 43 | grunt.util._.flattenDeep(patterns).forEach(function(pattern) { 44 | // If the first character is ! it should be omitted 45 | var exclusion = pattern.indexOf('!') === 0; 46 | // If the pattern is an exclusion, remove the ! 47 | if (exclusion) { pattern = pattern.slice(1); } 48 | // Find all matching files for this pattern. 49 | var matches = fn(pattern); 50 | if (exclusion) { 51 | // If an exclusion, remove matching files. 52 | result = grunt.util._.difference(result, matches); 53 | } else { 54 | // Otherwise add matching files. 55 | result = grunt.util._.union(result, matches); 56 | } 57 | }); 58 | return result; 59 | }; 60 | 61 | // Match a filepath or filepaths against one or more wildcard patterns. Returns 62 | // all matching filepaths. 63 | file.match = function(options, patterns, filepaths) { 64 | if (grunt.util.kindOf(options) !== 'object') { 65 | filepaths = patterns; 66 | patterns = options; 67 | options = {}; 68 | } 69 | // Return empty set if either patterns or filepaths was omitted. 70 | if (patterns == null || filepaths == null) { return []; } 71 | // Normalize patterns and filepaths to arrays. 72 | if (!Array.isArray(patterns)) { patterns = [patterns]; } 73 | if (!Array.isArray(filepaths)) { filepaths = [filepaths]; } 74 | // Return empty set if there are no patterns or filepaths. 75 | if (patterns.length === 0 || filepaths.length === 0) { return []; } 76 | // Return all matching filepaths. 77 | return processPatterns(patterns, function(pattern) { 78 | return file.minimatch.match(filepaths, pattern, options); 79 | }); 80 | }; 81 | 82 | // Match a filepath or filepaths against one or more wildcard patterns. Returns 83 | // true if any of the patterns match. 84 | file.isMatch = function() { 85 | return file.match.apply(file, arguments).length > 0; 86 | }; 87 | 88 | // Return an array of all file paths that match the given wildcard patterns. 89 | file.expand = function() { 90 | var args = grunt.util.toArray(arguments); 91 | // If the first argument is an options object, save those options to pass 92 | // into the file.glob.sync method. 93 | var options = grunt.util.kindOf(args[0]) === 'object' ? args.shift() : {}; 94 | // Use the first argument if it's an Array, otherwise convert the arguments 95 | // object to an array and use that. 96 | var patterns = Array.isArray(args[0]) ? args[0] : args; 97 | // Return empty set if there are no patterns or filepaths. 98 | if (patterns.length === 0) { return []; } 99 | // Return all matching filepaths. 100 | var matches = processPatterns(patterns, function(pattern) { 101 | // Find all matching files for this pattern. 102 | return file.glob.sync(pattern, options); 103 | }); 104 | // Filter result set? 105 | if (options.filter) { 106 | matches = matches.filter(function(filepath) { 107 | filepath = path.join(options.cwd || '', filepath); 108 | try { 109 | if (typeof options.filter === 'function') { 110 | return options.filter(filepath); 111 | } else { 112 | // If the file is of the right type and exists, this should work. 113 | return fs.statSync(filepath)[options.filter](); 114 | } 115 | } catch (e) { 116 | // Otherwise, it's probably not the right type. 117 | return false; 118 | } 119 | }); 120 | } 121 | return matches; 122 | }; 123 | 124 | var pathSeparatorRe = /[\/\\]/g; 125 | 126 | // The "ext" option refers to either everything after the first dot (default) 127 | // or everything after the last dot. 128 | var extDotRe = { 129 | first: /(\.[^\/]*)?$/, 130 | last: /(\.[^\/\.]*)?$/, 131 | }; 132 | 133 | // Build a multi task "files" object dynamically. 134 | file.expandMapping = function(patterns, destBase, options) { 135 | options = grunt.util._.defaults({}, options, { 136 | extDot: 'first', 137 | rename: function(destBase, destPath) { 138 | return path.join(destBase || '', destPath); 139 | } 140 | }); 141 | var files = []; 142 | var fileByDest = {}; 143 | // Find all files matching pattern, using passed-in options. 144 | file.expand(options, patterns).forEach(function(src) { 145 | var destPath = src; 146 | // Flatten? 147 | if (options.flatten) { 148 | destPath = path.basename(destPath); 149 | } 150 | // Change the extension? 151 | if ('ext' in options) { 152 | destPath = destPath.replace(extDotRe[options.extDot], options.ext); 153 | } 154 | // Generate destination filename. 155 | var dest = options.rename(destBase, destPath, options); 156 | // Prepend cwd to src path if necessary. 157 | if (options.cwd) { src = path.join(options.cwd, src); } 158 | // Normalize filepaths to be unix-style. 159 | dest = dest.replace(pathSeparatorRe, '/'); 160 | src = src.replace(pathSeparatorRe, '/'); 161 | // Map correct src path to dest path. 162 | if (fileByDest[dest]) { 163 | // If dest already exists, push this src onto that dest's src array. 164 | fileByDest[dest].src.push(src); 165 | } else { 166 | // Otherwise create a new src-dest file mapping object. 167 | files.push({ 168 | src: [src], 169 | dest: dest, 170 | }); 171 | // And store a reference for later use. 172 | fileByDest[dest] = files[files.length - 1]; 173 | } 174 | }); 175 | return files; 176 | }; 177 | 178 | // Like mkdir -p. Create a directory and any intermediary directories. 179 | file.mkdir = function(dirpath, mode) { 180 | if (grunt.option('no-write')) { return; } 181 | try { 182 | fs.mkdirSync(dirpath, { recursive: true, mode: mode }); 183 | } catch (e) { 184 | throw grunt.util.error('Unable to create directory "' + dirpath + '" (Error code: ' + e.code + ').', e); 185 | } 186 | }; 187 | 188 | // Recurse into a directory, executing callback for each file. 189 | file.recurse = function recurse(rootdir, callback, subdir) { 190 | var abspath = subdir ? path.join(rootdir, subdir) : rootdir; 191 | fs.readdirSync(abspath).forEach(function(filename) { 192 | var filepath = path.join(abspath, filename); 193 | if (fs.statSync(filepath).isDirectory()) { 194 | recurse(rootdir, callback, unixifyPath(path.join(subdir || '', filename || ''))); 195 | } else { 196 | callback(unixifyPath(filepath), rootdir, subdir, filename); 197 | } 198 | }); 199 | }; 200 | 201 | // The default file encoding to use. 202 | file.defaultEncoding = 'utf8'; 203 | // Whether to preserve the BOM on file.read rather than strip it. 204 | file.preserveBOM = false; 205 | 206 | // Read a file, return its contents. 207 | file.read = function(filepath, options) { 208 | if (!options) { options = {}; } 209 | var contents; 210 | grunt.verbose.write('Reading ' + filepath + '...'); 211 | try { 212 | contents = fs.readFileSync(String(filepath)); 213 | // If encoding is not explicitly null, convert from encoded buffer to a 214 | // string. If no encoding was specified, use the default. 215 | if (options.encoding !== null) { 216 | contents = iconv.decode(contents, options.encoding || file.defaultEncoding, {stripBOM: !file.preserveBOM}); 217 | } 218 | grunt.verbose.ok(); 219 | return contents; 220 | } catch (e) { 221 | grunt.verbose.error(); 222 | throw grunt.util.error('Unable to read "' + filepath + '" file (Error code: ' + e.code + ').', e); 223 | } 224 | }; 225 | 226 | // Read a file, parse its contents, return an object. 227 | file.readJSON = function(filepath, options) { 228 | var src = file.read(filepath, options); 229 | var result; 230 | grunt.verbose.write('Parsing ' + filepath + '...'); 231 | try { 232 | result = JSON.parse(src); 233 | grunt.verbose.ok(); 234 | return result; 235 | } catch (e) { 236 | grunt.verbose.error(); 237 | throw grunt.util.error('Unable to parse "' + filepath + '" file (' + e.message + ').', e); 238 | } 239 | }; 240 | 241 | // Read a YAML file, parse its contents, return an object. 242 | file.readYAML = function(filepath, options, yamlOptions) { 243 | if (!options) { options = {}; } 244 | if (!yamlOptions) { yamlOptions = {}; } 245 | 246 | var src = file.read(filepath, options); 247 | var result; 248 | grunt.verbose.write('Parsing ' + filepath + '...'); 249 | try { 250 | // use the recommended way of reading YAML files 251 | // https://github.com/nodeca/js-yaml#safeload-string---options- 252 | if (yamlOptions.unsafeLoad) { 253 | result = YAML.load(src); 254 | } else { 255 | result = YAML.safeLoad(src); 256 | } 257 | grunt.verbose.ok(); 258 | return result; 259 | } catch (e) { 260 | grunt.verbose.error(); 261 | throw grunt.util.error('Unable to parse "' + filepath + '" file (' + e.message + ').', e); 262 | } 263 | }; 264 | 265 | // Write a file. 266 | file.write = function(filepath, contents, options) { 267 | if (!options) { options = {}; } 268 | var nowrite = grunt.option('no-write'); 269 | grunt.verbose.write((nowrite ? 'Not actually writing ' : 'Writing ') + filepath + '...'); 270 | // Create path, if necessary. 271 | file.mkdir(path.dirname(filepath)); 272 | try { 273 | // If contents is already a Buffer, don't try to encode it. If no encoding 274 | // was specified, use the default. 275 | if (!Buffer.isBuffer(contents)) { 276 | contents = iconv.encode(contents, options.encoding || file.defaultEncoding); 277 | } 278 | // Actually write file. 279 | if (!nowrite) { 280 | fs.writeFileSync(filepath, contents, 'mode' in options ? {mode: options.mode} : {}); 281 | } 282 | grunt.verbose.ok(); 283 | return true; 284 | } catch (e) { 285 | grunt.verbose.error(); 286 | throw grunt.util.error('Unable to write "' + filepath + '" file (Error code: ' + e.code + ').', e); 287 | } 288 | }; 289 | 290 | // Read a file, optionally processing its content, then write the output. 291 | // Or read a directory, recursively creating directories, reading files, 292 | // processing content, writing output. 293 | // Handles symlinks by coping them as files or directories. 294 | file.copy = function copy(srcpath, destpath, options) { 295 | if (file.isLink(srcpath)) { 296 | file._copySymbolicLink(srcpath, destpath); 297 | } else if (file.isDir(srcpath)) { 298 | // Copy a directory, recursively. 299 | // Explicitly create new dest directory. 300 | file.mkdir(destpath); 301 | // Iterate over all sub-files/dirs, recursing. 302 | fs.readdirSync(srcpath).forEach(function(filepath) { 303 | copy(path.join(srcpath, filepath), path.join(destpath, filepath), options); 304 | }); 305 | } else { 306 | // Copy a single file. 307 | file._copy(srcpath, destpath, options); 308 | } 309 | }; 310 | 311 | // Read a file, optionally processing its content, then write the output. 312 | file._copy = function(srcpath, destpath, options) { 313 | if (!options) { options = {}; } 314 | // If a process function was specified, and noProcess isn't true or doesn't 315 | // match the srcpath, process the file's source. 316 | var process = options.process && options.noProcess !== true && 317 | !(options.noProcess && file.isMatch(options.noProcess, srcpath)); 318 | // If the file will be processed, use the encoding as-specified. Otherwise, 319 | // use an encoding of null to force the file to be read/written as a Buffer. 320 | var readWriteOptions = process ? options : {encoding: null}; 321 | // Actually read the file. 322 | var contents = file.read(srcpath, readWriteOptions); 323 | if (process) { 324 | grunt.verbose.write('Processing source...'); 325 | try { 326 | contents = options.process(contents, srcpath, destpath); 327 | grunt.verbose.ok(); 328 | } catch (e) { 329 | grunt.verbose.error(); 330 | throw grunt.util.error('Error while processing "' + srcpath + '" file.', e); 331 | } 332 | } 333 | // Abort copy if the process function returns false. 334 | if (contents === false || file.isLink(destpath)) { 335 | grunt.verbose.writeln('Write aborted. Either the process function returned false or the destination is a symlink'); 336 | } else { 337 | file.write(destpath, contents, readWriteOptions); 338 | } 339 | }; 340 | 341 | // Delete folders and files recursively 342 | file.delete = function(filepath, options) { 343 | filepath = String(filepath); 344 | 345 | var nowrite = grunt.option('no-write'); 346 | if (!options) { 347 | options = {force: grunt.option('force') || false}; 348 | } 349 | 350 | grunt.verbose.write((nowrite ? 'Not actually deleting ' : 'Deleting ') + filepath + '...'); 351 | 352 | if (!file.exists(filepath)) { 353 | grunt.verbose.error(); 354 | grunt.log.warn('Cannot delete nonexistent file.'); 355 | return false; 356 | } 357 | 358 | // Only delete cwd or outside cwd if --force enabled. Be careful, people! 359 | if (!options.force) { 360 | if (file.isPathCwd(filepath)) { 361 | grunt.verbose.error(); 362 | grunt.fail.warn('Cannot delete the current working directory.'); 363 | return false; 364 | } else if (!file.isPathInCwd(filepath)) { 365 | grunt.verbose.error(); 366 | grunt.fail.warn('Cannot delete files outside the current working directory.'); 367 | return false; 368 | } 369 | } 370 | 371 | try { 372 | // Actually delete. Or not. 373 | if (!nowrite) { 374 | fs.rmSync(filepath, { recursive: true, force: true }); 375 | } 376 | grunt.verbose.ok(); 377 | return true; 378 | } catch (e) { 379 | grunt.verbose.error(); 380 | throw grunt.util.error('Unable to delete "' + filepath + '" file (' + e.message + ').', e); 381 | } 382 | }; 383 | 384 | // True if the file path exists. 385 | file.exists = function() { 386 | var filepath = path.join.apply(path, arguments); 387 | return fs.existsSync(filepath); 388 | }; 389 | 390 | // True if the file is a symbolic link. 391 | file.isLink = function() { 392 | var filepath = path.join.apply(path, arguments); 393 | try { 394 | return fs.lstatSync(filepath).isSymbolicLink(); 395 | } catch (e) { 396 | if (e.code === 'ENOENT') { 397 | // The file doesn't exist, so it's not a symbolic link. 398 | return false; 399 | } 400 | throw grunt.util.error('Unable to read "' + filepath + '" file (Error code: ' + e.code + ').', e); 401 | } 402 | }; 403 | 404 | // True if the path is a directory. 405 | file.isDir = function() { 406 | var filepath = path.join.apply(path, arguments); 407 | return file.exists(filepath) && fs.statSync(filepath).isDirectory(); 408 | }; 409 | 410 | // True if the path is a file. 411 | file.isFile = function() { 412 | var filepath = path.join.apply(path, arguments); 413 | return file.exists(filepath) && fs.statSync(filepath).isFile(); 414 | }; 415 | 416 | // Is a given file path absolute? 417 | file.isPathAbsolute = function() { 418 | var filepath = path.join.apply(path, arguments); 419 | return path.isAbsolute(filepath); 420 | }; 421 | 422 | // Do all the specified paths refer to the same path? 423 | file.arePathsEquivalent = function(first) { 424 | first = path.resolve(first); 425 | for (var i = 1; i < arguments.length; i++) { 426 | if (first !== path.resolve(arguments[i])) { return false; } 427 | } 428 | return true; 429 | }; 430 | 431 | // Are descendant path(s) contained within ancestor path? Note: does not test 432 | // if paths actually exist. 433 | file.doesPathContain = function(ancestor) { 434 | ancestor = path.resolve(ancestor); 435 | var relative; 436 | for (var i = 1; i < arguments.length; i++) { 437 | relative = path.relative(path.resolve(arguments[i]), ancestor); 438 | if (relative === '' || /\w+/.test(relative)) { return false; } 439 | } 440 | return true; 441 | }; 442 | 443 | // Test to see if a filepath is the CWD. 444 | file.isPathCwd = function() { 445 | var filepath = path.join.apply(path, arguments); 446 | try { 447 | return file.arePathsEquivalent(fs.realpathSync(process.cwd()), fs.realpathSync(filepath)); 448 | } catch (e) { 449 | return false; 450 | } 451 | }; 452 | 453 | file._copySymbolicLink = function(srcpath, destpath) { 454 | var destdir = path.join(destpath, '..'); 455 | // Use the correct relative path for the symlink 456 | if (!grunt.file.isPathAbsolute(srcpath)) { 457 | srcpath = path.relative(destdir, srcpath) || '.'; 458 | } 459 | file.mkdir(destdir); 460 | var mode = grunt.file.isDir(srcpath) ? 'dir' : 'file'; 461 | if (fs.existsSync(destpath)) { 462 | // skip symlink if file already exists 463 | return; 464 | } 465 | return fs.symlinkSync(srcpath, destpath, mode); 466 | }; 467 | 468 | // Test to see if a filepath is contained within the CWD. 469 | file.isPathInCwd = function() { 470 | var filepath = path.join.apply(path, arguments); 471 | try { 472 | return file.doesPathContain(fs.realpathSync(process.cwd()), fs.realpathSync(filepath)); 473 | } catch (e) { 474 | return false; 475 | } 476 | }; 477 | -------------------------------------------------------------------------------- /lib/grunt/help.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../grunt'); 4 | 5 | // Nodejs libs. 6 | var path = require('path'); 7 | 8 | // Set column widths. 9 | var col1len = 0; 10 | exports.initCol1 = function(str) { 11 | col1len = Math.max(col1len, str.length); 12 | }; 13 | exports.initWidths = function() { 14 | // Widths for options/tasks table output. 15 | var commandWidth = Math.max(col1len + 20, 76); 16 | exports.widths = [1, col1len, 2, commandWidth - col1len]; 17 | }; 18 | 19 | // Render an array in table form. 20 | exports.table = function(arr) { 21 | arr.forEach(function(item) { 22 | grunt.log.writetableln(exports.widths, ['', grunt.util._.pad(item[0], col1len), '', item[1]]); 23 | }); 24 | }; 25 | 26 | // Methods to run, in-order. 27 | exports.queue = [ 28 | 'initOptions', 29 | 'initTasks', 30 | 'initWidths', 31 | 'header', 32 | 'usage', 33 | 'options', 34 | 'optionsFooter', 35 | 'tasks', 36 | 'footer', 37 | ]; 38 | 39 | // Actually display stuff. 40 | exports.display = function() { 41 | exports.queue.forEach(function(name) { exports[name](); }); 42 | }; 43 | 44 | // Header. 45 | exports.header = function() { 46 | grunt.log.writeln('Grunt: The JavaScript Task Runner (v' + grunt.version + ')'); 47 | }; 48 | 49 | // Usage info. 50 | exports.usage = function() { 51 | grunt.log.header('Usage'); 52 | grunt.log.writeln(' ' + path.basename(process.argv[1]) + ' [options] [task [task ...]]'); 53 | }; 54 | 55 | // Options. 56 | exports.initOptions = function() { 57 | // Build 2-column array for table view. 58 | exports._options = Object.keys(grunt.cli.optlist).map(function(long) { 59 | var o = grunt.cli.optlist[long]; 60 | var col1 = '--' + (o.negate ? 'no-' : '') + long + (o.short ? ', -' + o.short : ''); 61 | exports.initCol1(col1); 62 | return [col1, o.info]; 63 | }); 64 | }; 65 | 66 | exports.options = function() { 67 | grunt.log.header('Options'); 68 | exports.table(exports._options); 69 | }; 70 | 71 | exports.optionsFooter = function() { 72 | grunt.log.writeln().writelns( 73 | 'Options marked with * have methods exposed via the grunt API and should ' + 74 | 'instead be specified inside the Gruntfile wherever possible.' 75 | ); 76 | }; 77 | 78 | // Tasks. 79 | exports.initTasks = function() { 80 | // Initialize task system so that the tasks can be listed. 81 | grunt.task.init([], {help: true}); 82 | 83 | // Build object of tasks by info (where they were loaded from). 84 | exports._tasks = []; 85 | Object.keys(grunt.task._tasks).forEach(function(name) { 86 | exports.initCol1(name); 87 | var task = grunt.task._tasks[name]; 88 | exports._tasks.push(task); 89 | }); 90 | }; 91 | 92 | exports.tasks = function() { 93 | grunt.log.header('Available tasks'); 94 | if (exports._tasks.length === 0) { 95 | grunt.log.writeln('(no tasks found)'); 96 | } else { 97 | exports.table(exports._tasks.map(function(task) { 98 | var info = task.info; 99 | if (task.multi) { info += ' *'; } 100 | return [task.name, info]; 101 | })); 102 | 103 | grunt.log.writeln().writelns( 104 | 'Tasks run in the order specified. Arguments may be passed to tasks that ' + 105 | 'accept them by using colons, like "lint:files". Tasks marked with * are ' + 106 | '"multi tasks" and will iterate over all sub-targets if no argument is ' + 107 | 'specified.' 108 | ); 109 | } 110 | 111 | grunt.log.writeln().writelns( 112 | 'The list of available tasks may change based on tasks directories or ' + 113 | 'grunt plugins specified in the Gruntfile or via command-line options.' 114 | ); 115 | }; 116 | 117 | // Footer. 118 | exports.footer = function() { 119 | grunt.log.writeln().writeln('For more information, see http://gruntjs.com/'); 120 | }; 121 | -------------------------------------------------------------------------------- /lib/grunt/option.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // The actual option data. 4 | var data = {}; 5 | 6 | // Get or set an option value. 7 | var option = module.exports = function(key, value) { 8 | var no = key.match(/^no-(.+)$/); 9 | if (arguments.length === 2) { 10 | return (data[key] = value); 11 | } else if (no) { 12 | return data[no[1]] === false; 13 | } else { 14 | return data[key]; 15 | } 16 | }; 17 | 18 | // Initialize option data. 19 | option.init = function(obj) { 20 | return (data = obj || {}); 21 | }; 22 | 23 | // List of options as flags. 24 | option.flags = function() { 25 | return Object.keys(data).filter(function(key) { 26 | // Don't display empty arrays. 27 | return !(Array.isArray(data[key]) && data[key].length === 0); 28 | }).map(function(key) { 29 | var val = data[key]; 30 | return '--' + (val === false ? 'no-' : '') + key + 31 | (typeof val === 'boolean' ? '' : '=' + val); 32 | }); 33 | }; 34 | 35 | // Get all option keys 36 | option.keys = function() { 37 | return Object.keys(data); 38 | }; 39 | -------------------------------------------------------------------------------- /lib/grunt/task.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Keep track of the number of log.error() calls and the last specified tasks message. 4 | var errorcount, lastInfo; 5 | 6 | var grunt = require('../grunt'); 7 | 8 | // Nodejs libs. 9 | var path = require('path'); 10 | 11 | // Extend generic "task" util lib. 12 | var parent = grunt.util.task.create(); 13 | 14 | // The module to be exported. 15 | var task = module.exports = Object.create(parent); 16 | 17 | // A temporary registry of tasks and metadata. 18 | var registry = {tasks: [], untasks: [], meta: {}}; 19 | 20 | // Number of levels of recursion when loading tasks in collections. 21 | var loadTaskDepth = 0; 22 | 23 | // Override built-in registerTask. 24 | task.registerTask = function(name) { 25 | // Add task to registry. 26 | registry.tasks.push(name); 27 | // Register task. 28 | parent.registerTask.apply(task, arguments); 29 | // This task, now that it's been registered. 30 | var thisTask = task._tasks[name]; 31 | // Metadata about the current task. 32 | thisTask.meta = grunt.util._.clone(registry.meta); 33 | // Override task function. 34 | var _fn = thisTask.fn; 35 | thisTask.fn = function(arg) { 36 | // Guaranteed to always be the actual task name. 37 | var name = thisTask.name; 38 | // Initialize the errorcount for this task. 39 | errorcount = grunt.fail.errorcount; 40 | // Return the number of errors logged during this task. 41 | Object.defineProperty(this, 'errorCount', { 42 | enumerable: true, 43 | get: function() { 44 | return grunt.fail.errorcount - errorcount; 45 | } 46 | }); 47 | // Expose task.requires on `this`. 48 | this.requires = task.requires.bind(task); 49 | // Expose config.requires on `this`. 50 | this.requiresConfig = grunt.config.requires; 51 | // Return an options object with the specified defaults overwritten by task- 52 | // specific overrides, via the "options" property. 53 | this.options = function() { 54 | var args = [{}].concat(grunt.util.toArray(arguments)).concat([ 55 | grunt.config([name, 'options']) 56 | ]); 57 | var options = grunt.util._.extend.apply(null, args); 58 | grunt.verbose.writeflags(options, 'Options'); 59 | return options; 60 | }; 61 | // If this task was an alias or a multi task called without a target, 62 | // only log if in verbose mode. 63 | var logger = _fn.alias || (thisTask.multi && (!arg || arg === '*')) ? 'verbose' : 'log'; 64 | // Actually log. 65 | grunt[logger].header('Running "' + this.nameArgs + '"' + 66 | (this.name !== this.nameArgs ? ' (' + this.name + ')' : '') + ' task'); 67 | // If --debug was specified, log the path to this task's source file. 68 | grunt[logger].debug('Task source: ' + thisTask.meta.filepath); 69 | // Actually run the task. 70 | return _fn.apply(this, arguments); 71 | }; 72 | return task; 73 | }; 74 | 75 | // Multi task targets can't start with _ or be a reserved property (options). 76 | function isValidMultiTaskTarget(target) { 77 | return !/^_|^options$/.test(target); 78 | } 79 | 80 | // Normalize multi task files. 81 | task.normalizeMultiTaskFiles = function(data, target) { 82 | var prop, obj; 83 | var files = []; 84 | if (grunt.util.kindOf(data) === 'object') { 85 | if ('src' in data || 'dest' in data) { 86 | obj = {}; 87 | for (prop in data) { 88 | if (prop !== 'options') { 89 | obj[prop] = data[prop]; 90 | } 91 | } 92 | files.push(obj); 93 | } else if (grunt.util.kindOf(data.files) === 'object') { 94 | for (prop in data.files) { 95 | files.push({src: data.files[prop], dest: grunt.config.process(prop)}); 96 | } 97 | } else if (Array.isArray(data.files)) { 98 | grunt.util._.flattenDeep(data.files).forEach(function(obj) { 99 | var prop; 100 | if ('src' in obj || 'dest' in obj) { 101 | files.push(obj); 102 | } else { 103 | for (prop in obj) { 104 | files.push({src: obj[prop], dest: grunt.config.process(prop)}); 105 | } 106 | } 107 | }); 108 | } 109 | } else { 110 | files.push({src: data, dest: grunt.config.process(target)}); 111 | } 112 | 113 | // If no src/dest or files were specified, return an empty files array. 114 | if (files.length === 0) { 115 | grunt.verbose.writeln('File: ' + '[no files]'.yellow); 116 | return []; 117 | } 118 | 119 | // Process all normalized file objects. 120 | files = grunt.util._(files).chain().forEach(function(obj) { 121 | if (!('src' in obj) || !obj.src) { return; } 122 | // Normalize .src properties to flattened array. 123 | if (Array.isArray(obj.src)) { 124 | obj.src = grunt.util._.flatten(obj.src); 125 | } else { 126 | obj.src = [obj.src]; 127 | } 128 | }).map(function(obj) { 129 | // Build options object, removing unwanted properties. 130 | var expandOptions = grunt.util._.extend({}, obj); 131 | delete expandOptions.src; 132 | delete expandOptions.dest; 133 | 134 | // Expand file mappings. 135 | if (obj.expand) { 136 | return grunt.file.expandMapping(obj.src, obj.dest, expandOptions).map(function(mapObj) { 137 | // Copy obj properties to result. 138 | var result = grunt.util._.extend({}, obj); 139 | // Make a clone of the orig obj available. 140 | result.orig = grunt.util._.extend({}, obj); 141 | // Set .src and .dest, processing both as templates. 142 | result.src = grunt.config.process(mapObj.src); 143 | result.dest = grunt.config.process(mapObj.dest); 144 | // Remove unwanted properties. 145 | ['expand', 'cwd', 'flatten', 'rename', 'ext'].forEach(function(prop) { 146 | delete result[prop]; 147 | }); 148 | return result; 149 | }); 150 | } 151 | 152 | // Copy obj properties to result, adding an .orig property. 153 | var result = grunt.util._.extend({}, obj); 154 | // Make a clone of the orig obj available. 155 | result.orig = grunt.util._.extend({}, obj); 156 | 157 | if ('src' in result) { 158 | // Expose an expand-on-demand getter method as .src. 159 | Object.defineProperty(result, 'src', { 160 | enumerable: true, 161 | get: function fn() { 162 | var src; 163 | if (!('result' in fn)) { 164 | src = obj.src; 165 | // If src is an array, flatten it. Otherwise, make it into an array. 166 | src = Array.isArray(src) ? grunt.util._.flatten(src) : [src]; 167 | // Expand src files, memoizing result. 168 | fn.result = grunt.file.expand(expandOptions, src); 169 | } 170 | return fn.result; 171 | } 172 | }); 173 | } 174 | 175 | if ('dest' in result) { 176 | result.dest = obj.dest; 177 | } 178 | 179 | return result; 180 | }).flatten().value(); 181 | 182 | // Log this.file src and dest properties when --verbose is specified. 183 | if (grunt.option('verbose')) { 184 | files.forEach(function(obj) { 185 | var output = []; 186 | if ('src' in obj) { 187 | output.push(obj.src.length > 0 ? grunt.log.wordlist(obj.src) : '[no src]'.yellow); 188 | } 189 | if ('dest' in obj) { 190 | output.push('-> ' + (obj.dest ? String(obj.dest).cyan : '[no dest]'.yellow)); 191 | } 192 | if (output.length > 0) { 193 | grunt.verbose.writeln('Files: ' + output.join(' ')); 194 | } 195 | }); 196 | } 197 | 198 | return files; 199 | }; 200 | 201 | // This is the most common "multi task" pattern. 202 | task.registerMultiTask = function(name, info, fn) { 203 | // If optional "info" string is omitted, shuffle arguments a bit. 204 | if (fn == null) { 205 | fn = info; 206 | info = 'Custom multi task.'; 207 | } 208 | // Store a reference to the task object, in case the task gets renamed. 209 | var thisTask; 210 | task.registerTask(name, info, function(target) { 211 | // Guaranteed to always be the actual task name. 212 | var name = thisTask.name; 213 | // Arguments (sans target) as an array. 214 | this.args = grunt.util.toArray(arguments).slice(1); 215 | // If a target wasn't specified, run this task once for each target. 216 | if (!target || target === '*') { 217 | return task.runAllTargets(name, this.args); 218 | } else if (!isValidMultiTaskTarget(target)) { 219 | throw new Error('Invalid target "' + target + '" specified.'); 220 | } 221 | // Fail if any required config properties have been omitted. 222 | this.requiresConfig([name, target]); 223 | // Return an options object with the specified defaults overwritten by task- 224 | // and/or target-specific overrides, via the "options" property. 225 | this.options = function() { 226 | var targetObj = grunt.config([name, target]); 227 | var args = [{}].concat(grunt.util.toArray(arguments)).concat([ 228 | grunt.config([name, 'options']), 229 | grunt.util.kindOf(targetObj) === 'object' ? targetObj.options : {} 230 | ]); 231 | var options = grunt.util._.extend.apply(null, args); 232 | grunt.verbose.writeflags(options, 'Options'); 233 | return options; 234 | }; 235 | // Expose the current target. 236 | this.target = target; 237 | // Recreate flags object so that the target isn't set as a flag. 238 | this.flags = {}; 239 | this.args.forEach(function(arg) { this.flags[arg] = true; }, this); 240 | // Expose data on `this` (as well as task.current). 241 | this.data = grunt.config([name, target]); 242 | // Expose normalized files object. 243 | this.files = task.normalizeMultiTaskFiles(this.data, target); 244 | // Expose normalized, flattened, uniqued array of src files. 245 | Object.defineProperty(this, 'filesSrc', { 246 | enumerable: true, 247 | get: function() { 248 | return grunt.util._(this.files).chain().map('src').flatten().uniq().value(); 249 | }.bind(this) 250 | }); 251 | // Call original task function, passing in the target and any other args. 252 | return fn.apply(this, this.args); 253 | }); 254 | 255 | thisTask = task._tasks[name]; 256 | thisTask.multi = true; 257 | }; 258 | 259 | // Init tasks don't require properties in config, and as such will preempt 260 | // config loading errors. 261 | task.registerInitTask = function(name, info, fn) { 262 | task.registerTask(name, info, fn); 263 | task._tasks[name].init = true; 264 | }; 265 | 266 | // Override built-in renameTask to use the registry. 267 | task.renameTask = function(oldname, newname) { 268 | var result; 269 | try { 270 | // Actually rename task. 271 | result = parent.renameTask.apply(task, arguments); 272 | // Add and remove task. 273 | registry.untasks.push(oldname); 274 | registry.tasks.push(newname); 275 | // Return result. 276 | return result; 277 | } catch (e) { 278 | grunt.log.error(e.message); 279 | } 280 | }; 281 | 282 | // If a property wasn't passed, run all task targets in turn. 283 | task.runAllTargets = function(taskname, args) { 284 | // Get an array of sub-property keys under the given config object. 285 | var targets = Object.keys(grunt.config.getRaw(taskname) || {}); 286 | // Remove invalid target properties. 287 | targets = targets.filter(isValidMultiTaskTarget); 288 | // Fail if there are no actual properties to iterate over. 289 | if (targets.length === 0) { 290 | grunt.log.error('No "' + taskname + '" targets found.'); 291 | return false; 292 | } 293 | // Iterate over all targets, running a task for each. 294 | targets.forEach(function(target) { 295 | // Be sure to pass in any additionally specified args. 296 | task.run([taskname, target].concat(args || []).join(':')); 297 | }); 298 | }; 299 | 300 | // Load tasks and handlers from a given tasks file. 301 | var loadTaskStack = []; 302 | function loadTask(filepath) { 303 | // In case this was called recursively, save registry for later. 304 | loadTaskStack.push(registry); 305 | // Reset registry. 306 | registry = {tasks: [], untasks: [], meta: {info: lastInfo, filepath: filepath}}; 307 | var filename = path.basename(filepath); 308 | var msg = 'Loading "' + filename + '" tasks...'; 309 | var regCount = 0; 310 | var fn; 311 | try { 312 | // Load taskfile. 313 | fn = require(path.resolve(filepath)); 314 | if (typeof fn === 'function') { 315 | fn.call(grunt, grunt); 316 | } 317 | grunt.verbose.write(msg).ok(); 318 | // Log registered/renamed/unregistered tasks. 319 | ['un', ''].forEach(function(prefix) { 320 | var list = grunt.util._.chain(registry[prefix + 'tasks']).uniq().sort().value(); 321 | if (list.length > 0) { 322 | regCount++; 323 | grunt.verbose.writeln((prefix ? '- ' : '+ ') + grunt.log.wordlist(list)); 324 | } 325 | }); 326 | if (regCount === 0) { 327 | grunt.verbose.warn('No tasks were registered or unregistered.'); 328 | } 329 | } catch (e) { 330 | // Something went wrong. 331 | grunt.log.write(msg).error().verbose.error(e.stack).or.error(e); 332 | } 333 | // Restore registry. 334 | registry = loadTaskStack.pop() || {}; 335 | } 336 | 337 | // Log a message when loading tasks. 338 | function loadTasksMessage(info) { 339 | // Only keep track of names of top-level loaded tasks and collections, 340 | // not sub-tasks. 341 | if (loadTaskDepth === 0) { lastInfo = info; } 342 | grunt.verbose.subhead('Registering ' + info + ' tasks.'); 343 | } 344 | 345 | // Load tasks and handlers from a given directory. 346 | function loadTasks(tasksdir) { 347 | try { 348 | var files = grunt.file.glob.sync('*.{js,cjs,coffee}', {cwd: tasksdir, maxDepth: 1}); 349 | // Load tasks from files. 350 | files.forEach(function(filename) { 351 | loadTask(path.join(tasksdir, filename)); 352 | }); 353 | } catch (e) { 354 | grunt.log.verbose.error(e.stack).or.error(e); 355 | } 356 | } 357 | 358 | // Load tasks and handlers from a given directory. 359 | task.loadTasks = function(tasksdir) { 360 | loadTasksMessage('"' + tasksdir + '"'); 361 | if (grunt.file.exists(tasksdir)) { 362 | loadTasks(tasksdir); 363 | } else { 364 | grunt.log.error('Tasks directory "' + tasksdir + '" not found.'); 365 | } 366 | }; 367 | 368 | // Load tasks and handlers from a given locally-installed Npm module (installed 369 | // relative to the base dir). 370 | task.loadNpmTasks = function(name) { 371 | loadTasksMessage('"' + name + '" local Npm module'); 372 | var root = path.resolve('node_modules'); 373 | var pkgpath = path.join(root, name); 374 | var pkgfile = path.join(pkgpath, 'package.json'); 375 | // If package does not exist where grunt expects it to be, 376 | // try to find it using Node's package path resolution mechanism 377 | if (!grunt.file.exists(pkgpath)) { 378 | var nameParts = name.split('/'); 379 | // In case name points to directory inside module, 380 | // get real name of the module with respect to scope (if any) 381 | var normailzedName = (name[0] === '@' ? nameParts.slice(0,2).join('/') : nameParts[0]); 382 | try { 383 | pkgfile = require.resolve(normailzedName + '/package.json'); 384 | root = pkgfile.substr(0, pkgfile.length - normailzedName.length - '/package.json'.length); 385 | } catch (err) { 386 | grunt.log.error('Local Npm module "' + normailzedName + '" not found. Is it installed?'); 387 | return; 388 | } 389 | } 390 | var pkg = grunt.file.exists(pkgfile) ? grunt.file.readJSON(pkgfile) : {keywords: []}; 391 | 392 | // Process collection plugins. 393 | if (pkg.keywords && pkg.keywords.indexOf('gruntcollection') !== -1) { 394 | loadTaskDepth++; 395 | Object.keys(pkg.dependencies).forEach(function(depName) { 396 | // Npm sometimes pulls dependencies out if they're shared, so find 397 | // upwards if not found locally. 398 | var filepath = grunt.file.findup('node_modules/' + depName, { 399 | cwd: path.resolve('node_modules', name), 400 | nocase: true 401 | }); 402 | if (filepath) { 403 | // Load this task plugin recursively. 404 | task.loadNpmTasks(path.relative(root, filepath)); 405 | } 406 | }); 407 | loadTaskDepth--; 408 | return; 409 | } 410 | 411 | // Process task plugins. 412 | var tasksdir = path.join(root, name, 'tasks'); 413 | if (grunt.file.exists(tasksdir)) { 414 | loadTasks(tasksdir); 415 | } else { 416 | grunt.log.error('Local Npm module "' + name + '" not found. Is it installed?'); 417 | } 418 | }; 419 | 420 | // Initialize tasks. 421 | task.init = function(tasks, options) { 422 | if (!options) { options = {}; } 423 | 424 | // Were only init tasks specified? 425 | var allInit = tasks.length > 0 && tasks.every(function(name) { 426 | var obj = task._taskPlusArgs(name).task; 427 | return obj && obj.init; 428 | }); 429 | 430 | // Get any local Gruntfile or tasks that might exist. Use --gruntfile override 431 | // if specified, otherwise search the current directory or any parent. 432 | var gruntfile, msg; 433 | if (allInit || options.gruntfile === false) { 434 | gruntfile = null; 435 | } else { 436 | gruntfile = grunt.option('gruntfile') || 437 | grunt.file.findup('Gruntfile.{js,cjs,coffee}', {nocase: true}); 438 | msg = 'Reading "' + (gruntfile ? path.basename(gruntfile) : '???') + '" Gruntfile...'; 439 | } 440 | 441 | if (options.gruntfile === false) { 442 | // Grunt was run as a lib with {gruntfile: false}. 443 | } else if (gruntfile && grunt.file.exists(gruntfile)) { 444 | grunt.verbose.writeln().write(msg).ok(); 445 | // Change working directory so that all paths are relative to the 446 | // Gruntfile's location (or the --base option, if specified). 447 | process.chdir(grunt.option('base') || path.dirname(gruntfile)); 448 | // Load local tasks, if the file exists. 449 | loadTasksMessage('Gruntfile'); 450 | loadTask(gruntfile); 451 | } else if (options.help || allInit) { 452 | // Don't complain about missing Gruntfile. 453 | } else if (grunt.option('gruntfile')) { 454 | // If --config override was specified and it doesn't exist, complain. 455 | grunt.log.writeln().write(msg).error(); 456 | grunt.fatal('Unable to find "' + gruntfile + '" Gruntfile.', grunt.fail.code.MISSING_GRUNTFILE); 457 | } else if (!grunt.option('help')) { 458 | grunt.verbose.writeln().write(msg).error(); 459 | grunt.log.writelns( 460 | 'A valid Gruntfile could not be found. Please see the getting ' + 461 | 'started guide for more information on how to configure grunt: ' + 462 | 'http://gruntjs.com/getting-started' 463 | ); 464 | grunt.fatal('Unable to find Gruntfile.', grunt.fail.code.MISSING_GRUNTFILE); 465 | } 466 | 467 | // Load all user-specified --npm tasks. 468 | (grunt.option('npm') || []).map(String).forEach(task.loadNpmTasks); 469 | // Load all user-specified --tasks. 470 | (grunt.option('tasks') || []).map(String).forEach(task.loadTasks); 471 | }; 472 | -------------------------------------------------------------------------------- /lib/grunt/template.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../grunt'); 4 | 5 | // The module to be exported. 6 | var template = module.exports = {}; 7 | 8 | // External libs. 9 | template.date = require('dateformat'); 10 | 11 | // Format today's date. 12 | template.today = function(format) { 13 | var now = new Date(); 14 | if (process.env.SOURCE_DATE_EPOCH) { 15 | now = new Date((process.env.SOURCE_DATE_EPOCH * 1000) + (now.getTimezoneOffset() * 60000)); 16 | } 17 | return template.date(now, format); 18 | }; 19 | 20 | // Template delimiters. 21 | var allDelimiters = {}; 22 | 23 | // Initialize template delimiters. 24 | template.addDelimiters = function(name, opener, closer) { 25 | var delimiters = allDelimiters[name] = {}; 26 | // Used by grunt. 27 | delimiters.opener = opener; 28 | delimiters.closer = closer; 29 | // Generate RegExp patterns dynamically. 30 | var a = delimiters.opener.replace(/(.)/g, '\\$1'); 31 | var b = '([\\s\\S]+?)' + delimiters.closer.replace(/(.)/g, '\\$1'); 32 | // Used by Lo-Dash. 33 | delimiters.lodash = { 34 | evaluate: new RegExp(a + b, 'g'), 35 | interpolate: new RegExp(a + '=' + b, 'g'), 36 | escape: new RegExp(a + '-' + b, 'g') 37 | }; 38 | }; 39 | 40 | // The underscore default template syntax should be a pretty sane default for 41 | // the config system. 42 | template.addDelimiters('config', '<%', '%>'); 43 | 44 | // Set Lo-Dash template delimiters. 45 | template.setDelimiters = function(name) { 46 | // Get the appropriate delimiters. 47 | var delimiters = allDelimiters[name in allDelimiters ? name : 'config']; 48 | // Tell Lo-Dash which delimiters to use. 49 | grunt.util._.extend(grunt.util._.templateSettings, delimiters.lodash); 50 | // Return the delimiters. 51 | return delimiters; 52 | }; 53 | 54 | // Process template + data with Lo-Dash. 55 | template.process = function(tmpl, options) { 56 | if (!options) { options = {}; } 57 | // Set delimiters, and get a opening match character. 58 | var delimiters = template.setDelimiters(options.delimiters); 59 | // Clone data, initializing to config data or empty object if omitted. 60 | var data = Object.create(options.data || grunt.config.data || {}); 61 | // Expose grunt so that grunt utilities can be accessed, but only if it 62 | // doesn't conflict with an existing .grunt property. 63 | if (!('grunt' in data)) { data.grunt = grunt; } 64 | // Keep track of last change. 65 | var last = tmpl; 66 | try { 67 | // As long as tmpl contains template tags, render it and get the result, 68 | // otherwise just use the template string. 69 | while (tmpl.indexOf(delimiters.opener) >= 0) { 70 | tmpl = grunt.util._.template(tmpl, options)(data); 71 | // Abort if template didn't change - nothing left to process! 72 | if (tmpl === last) { break; } 73 | last = tmpl; 74 | } 75 | } catch (e) { 76 | // In upgrading to Lo-Dash (or Underscore.js 1.3.3), \n and \r in template 77 | // tags now causes an exception to be thrown. Warn the user why this is 78 | // happening. https://github.com/documentcloud/underscore/issues/553 79 | if (String(e) === 'SyntaxError: Unexpected token ILLEGAL' && /\n|\r/.test(tmpl)) { 80 | grunt.log.errorlns('A special character was detected in this template. ' + 81 | 'Inside template tags, the \\n and \\r special characters must be ' + 82 | 'escaped as \\\\n and \\\\r. (grunt 0.4.0+)'); 83 | } 84 | // Slightly better error message. 85 | e.message = 'An error occurred while processing a template (' + e.message + ').'; 86 | grunt.warn(e, grunt.fail.code.TEMPLATE_ERROR); 87 | } 88 | // Normalize linefeeds and return. 89 | return grunt.util.normalizelf(tmpl); 90 | }; 91 | -------------------------------------------------------------------------------- /lib/util/task.js: -------------------------------------------------------------------------------- 1 | (function(exports) { 2 | 3 | 'use strict'; 4 | 5 | var grunt = require('../grunt'); 6 | 7 | // Construct-o-rama. 8 | function Task() { 9 | // Information about the currently-running task. 10 | this.current = {}; 11 | // Tasks. 12 | this._tasks = {}; 13 | // Task queue. 14 | this._queue = []; 15 | // Queue placeholder (for dealing with nested tasks). 16 | this._placeholder = {placeholder: true}; 17 | // Queue marker (for clearing the queue programmatically). 18 | this._marker = {marker: true}; 19 | // Options. 20 | this._options = {}; 21 | // Is the queue running? 22 | this._running = false; 23 | // Success status of completed tasks. 24 | this._success = {}; 25 | } 26 | 27 | // Expose the constructor function. 28 | exports.Task = Task; 29 | 30 | // Create a new Task instance. 31 | exports.create = function() { 32 | return new Task(); 33 | }; 34 | 35 | // If the task runner is running or an error handler is not defined, throw 36 | // an exception. Otherwise, call the error handler directly. 37 | Task.prototype._throwIfRunning = function(obj) { 38 | if (this._running || !this._options.error) { 39 | // Throw an exception that the task runner will catch. 40 | throw obj; 41 | } else { 42 | // Not inside the task runner. Call the error handler and abort. 43 | this._options.error.call({name: null}, obj); 44 | } 45 | }; 46 | 47 | // Register a new task. 48 | Task.prototype.registerTask = function(name, info, fn) { 49 | // If optional "info" string is omitted, shuffle arguments a bit. 50 | if (fn == null) { 51 | fn = info; 52 | info = null; 53 | } 54 | // String or array of strings was passed instead of fn. 55 | var tasks; 56 | if (typeof fn !== 'function') { 57 | // Array of task names. 58 | tasks = this.parseArgs([fn]); 59 | // This task function just runs the specified tasks. 60 | fn = this.run.bind(this, fn); 61 | fn.alias = true; 62 | // Generate an info string if one wasn't explicitly passed. 63 | if (!info) { 64 | info = 'Alias for "' + tasks.join('", "') + '" task' + 65 | (tasks.length === 1 ? '' : 's') + '.'; 66 | } 67 | } else if (!info) { 68 | info = 'Custom task.'; 69 | } 70 | // Add task into cache. 71 | this._tasks[name] = {name: name, info: info, fn: fn}; 72 | // Make chainable! 73 | return this; 74 | }; 75 | 76 | // Is the specified task an alias? 77 | Task.prototype.isTaskAlias = function(name) { 78 | return !!this._tasks[name].fn.alias; 79 | }; 80 | 81 | // Has the specified task been registered? 82 | Task.prototype.exists = function(name) { 83 | return name in this._tasks; 84 | }; 85 | 86 | // Rename a task. This might be useful if you want to override the default 87 | // behavior of a task, while retaining the old name. This is a billion times 88 | // easier to implement than some kind of in-task "super" functionality. 89 | Task.prototype.renameTask = function(oldname, newname) { 90 | if (!this._tasks[oldname]) { 91 | throw new Error('Cannot rename missing "' + oldname + '" task.'); 92 | } 93 | // Rename task. 94 | this._tasks[newname] = this._tasks[oldname]; 95 | // Update name property of task. 96 | this._tasks[newname].name = newname; 97 | // Remove old name. 98 | delete this._tasks[oldname]; 99 | // Make chainable! 100 | return this; 101 | }; 102 | 103 | // Argument parsing helper. Supports these signatures: 104 | // fn('foo') // ['foo'] 105 | // fn('foo', 'bar', 'baz') // ['foo', 'bar', 'baz'] 106 | // fn(['foo', 'bar', 'baz']) // ['foo', 'bar', 'baz'] 107 | Task.prototype.parseArgs = function(args) { 108 | // Return the first argument if it's an array, otherwise return an array 109 | // of all arguments. 110 | return Array.isArray(args[0]) ? args[0] : [].slice.call(args); 111 | }; 112 | 113 | // Split a colon-delimited string into an array, unescaping (but not 114 | // splitting on) any \: escaped colons. 115 | Task.prototype.splitArgs = function(str) { 116 | if (!str) { return []; } 117 | // Store placeholder for \\ followed by \: 118 | str = str.replace(/\\\\/g, '\uFFFF').replace(/\\:/g, '\uFFFE'); 119 | // Split on : 120 | return str.split(':').map(function(s) { 121 | // Restore place-held : followed by \\ 122 | return s.replace(/\uFFFE/g, ':').replace(/\uFFFF/g, '\\'); 123 | }); 124 | }; 125 | 126 | // Given a task name, determine which actual task will be called, and what 127 | // arguments will be passed into the task callback. "foo" -> task "foo", no 128 | // args. "foo:bar:baz" -> task "foo:bar:baz" with no args (if "foo:bar:baz" 129 | // task exists), otherwise task "foo:bar" with arg "baz" (if "foo:bar" task 130 | // exists), otherwise task "foo" with args "bar" and "baz". 131 | Task.prototype._taskPlusArgs = function(name) { 132 | // Get task name / argument parts. 133 | var parts = this.splitArgs(name); 134 | // Start from the end, not the beginning! 135 | var i = parts.length; 136 | var task; 137 | do { 138 | // Get a task. 139 | task = this._tasks[parts.slice(0, i).join(':')]; 140 | // If the task doesn't exist, decrement `i`, and if `i` is greater than 141 | // 0, repeat. 142 | } while (!task && --i > 0); 143 | // Just the args. 144 | var args = parts.slice(i); 145 | // Maybe you want to use them as flags instead of as positional args? 146 | var flags = {}; 147 | args.forEach(function(arg) { flags[arg] = true; }); 148 | // The task to run and the args to run it with. 149 | return {task: task, nameArgs: name, args: args, flags: flags}; 150 | }; 151 | 152 | // Append things to queue in the correct spot. 153 | Task.prototype._push = function(things) { 154 | // Get current placeholder index. 155 | var index = this._queue.indexOf(this._placeholder); 156 | if (index === -1) { 157 | // No placeholder, add task+args objects to end of queue. 158 | this._queue = this._queue.concat(things); 159 | } else { 160 | // Placeholder exists, add task+args objects just before placeholder. 161 | [].splice.apply(this._queue, [index, 0].concat(things)); 162 | } 163 | }; 164 | 165 | // Enqueue a task. 166 | Task.prototype.run = function() { 167 | // Parse arguments into an array, returning an array of task+args objects. 168 | var things = this.parseArgs(arguments).map(this._taskPlusArgs, this); 169 | // Throw an exception if any tasks weren't found. 170 | var fails = things.filter(function(thing) { return !thing.task; }); 171 | if (fails.length > 0) { 172 | this._throwIfRunning(new Error('Task "' + fails[0].nameArgs + '" not found.')); 173 | return this; 174 | } 175 | // Append things to queue in the correct spot. 176 | this._push(things); 177 | // Make chainable! 178 | return this; 179 | }; 180 | 181 | // Add a marker to the queue to facilitate clearing it programmatically. 182 | Task.prototype.mark = function() { 183 | this._push(this._marker); 184 | // Make chainable! 185 | return this; 186 | }; 187 | 188 | // Run a task function, handling this.async / return value. 189 | Task.prototype.runTaskFn = function(context, fn, done, asyncDone) { 190 | // Async flag. 191 | var async = false; 192 | 193 | // Update the internal status object and run the next task. 194 | var complete = function(success) { 195 | var err = null; 196 | if (success === false) { 197 | // Since false was passed, the task failed generically. 198 | err = new Error('Task "' + context.nameArgs + '" failed.'); 199 | } else if (success instanceof Error || {}.toString.call(success) === '[object Error]') { 200 | // An error object was passed, so the task failed specifically. 201 | err = success; 202 | success = false; 203 | } else { 204 | // The task succeeded. 205 | success = true; 206 | } 207 | // The task has ended, reset the current task object. 208 | this.current = {}; 209 | // A task has "failed" only if it returns false (async) or if the 210 | // function returned by .async is passed false. 211 | this._success[context.nameArgs] = success; 212 | // If task failed, call error handler. 213 | if (!success && this._options.error) { 214 | this._options.error.call({name: context.name, nameArgs: context.nameArgs}, err); 215 | } 216 | // only call done async if explicitly requested to 217 | // see: https://github.com/gruntjs/grunt/pull/1026 218 | if (asyncDone) { 219 | process.nextTick(function() { 220 | done(err, success); 221 | }); 222 | } else { 223 | done(err, success); 224 | } 225 | }.bind(this); 226 | 227 | // When called, sets the async flag and returns a function that can 228 | // be used to continue processing the queue. 229 | context.async = function() { 230 | async = true; 231 | // The returned function should execute asynchronously in case 232 | // someone tries to do this.async()(); inside a task (WTF). 233 | return grunt.util._.once(function(success) { 234 | setTimeout(function() { complete(success); }, 1); 235 | }); 236 | }; 237 | 238 | // Expose some information about the currently-running task. 239 | this.current = context; 240 | 241 | try { 242 | // Get the current task and run it, setting `this` inside the task 243 | // function to be something useful. 244 | var success = fn.call(context); 245 | // If the async flag wasn't set, process the next task in the queue. 246 | if (!async) { 247 | complete(success); 248 | } 249 | } catch (err) { 250 | complete(err); 251 | } 252 | }; 253 | 254 | // Begin task queue processing. Ie. run all tasks. 255 | Task.prototype.start = function(opts) { 256 | if (!opts) { 257 | opts = {}; 258 | } 259 | // Abort if already running. 260 | if (this._running) { return false; } 261 | // Actually process the next task. 262 | var nextTask = function() { 263 | // Get next task+args object from queue. 264 | var thing; 265 | // Skip any placeholders or markers. 266 | do { 267 | thing = this._queue.shift(); 268 | } while (thing === this._placeholder || thing === this._marker); 269 | // If queue was empty, we're all done. 270 | if (!thing) { 271 | this._running = false; 272 | if (this._options.done) { 273 | this._options.done(); 274 | } 275 | return; 276 | } 277 | // Add a placeholder to the front of the queue. 278 | this._queue.unshift(this._placeholder); 279 | 280 | // Expose some information about the currently-running task. 281 | var context = { 282 | // The current task name plus args, as-passed. 283 | nameArgs: thing.nameArgs, 284 | // The current task name. 285 | name: thing.task.name, 286 | // The current task arguments. 287 | args: thing.args, 288 | // The current arguments, available as named flags. 289 | flags: thing.flags 290 | }; 291 | 292 | // Actually run the task function (handling this.async, etc) 293 | this.runTaskFn(context, function() { 294 | return thing.task.fn.apply(this, this.args); 295 | }, nextTask, !!opts.asyncDone); 296 | 297 | }.bind(this); 298 | 299 | // Update flag. 300 | this._running = true; 301 | // Process the next task. 302 | nextTask(); 303 | }; 304 | 305 | // Clear remaining tasks from the queue. 306 | Task.prototype.clearQueue = function(options) { 307 | if (!options) { options = {}; } 308 | if (options.untilMarker) { 309 | this._queue.splice(0, this._queue.indexOf(this._marker) + 1); 310 | } else { 311 | this._queue = []; 312 | } 313 | // Make chainable! 314 | return this; 315 | }; 316 | 317 | // Test to see if all of the given tasks have succeeded. 318 | Task.prototype.requires = function() { 319 | this.parseArgs(arguments).forEach(function(name) { 320 | var success = this._success[name]; 321 | if (!success) { 322 | throw new Error('Required task "' + name + 323 | '" ' + (success === false ? 'failed' : 'must be run first') + '.'); 324 | } 325 | }.bind(this)); 326 | }; 327 | 328 | // Override default options. 329 | Task.prototype.options = function(options) { 330 | Object.keys(options).forEach(function(name) { 331 | this._options[name] = options[name]; 332 | }.bind(this)); 333 | }; 334 | 335 | }(typeof exports === 'object' && exports || this)); 336 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt", 3 | "description": "The JavaScript Task Runner", 4 | "version": "1.6.1", 5 | "author": "Grunt Development Team (https://gruntjs.com/development-team)", 6 | "homepage": "https://gruntjs.com/", 7 | "repository": "https://github.com/gruntjs/grunt.git", 8 | "license": "MIT", 9 | "engines": { 10 | "node": ">=16" 11 | }, 12 | "scripts": { 13 | "test": "node bin/grunt test", 14 | "test-tap": "node bin/grunt test:tap" 15 | }, 16 | "main": "lib/grunt", 17 | "bin": { 18 | "grunt": "bin/grunt" 19 | }, 20 | "keywords": [ 21 | "task", 22 | "async", 23 | "cli", 24 | "minify", 25 | "uglify", 26 | "build", 27 | "lodash", 28 | "unit", 29 | "test", 30 | "qunit", 31 | "nodeunit", 32 | "server", 33 | "init", 34 | "scaffold", 35 | "make", 36 | "jake", 37 | "tool" 38 | ], 39 | "dependencies": { 40 | "dateformat": "~4.6.2", 41 | "eventemitter2": "~0.4.13", 42 | "exit": "~0.1.2", 43 | "findup-sync": "~5.0.0", 44 | "glob": "~7.1.6", 45 | "grunt-cli": "^1.4.3", 46 | "grunt-known-options": "~2.0.0", 47 | "grunt-legacy-log": "~3.0.0", 48 | "grunt-legacy-util": "~2.0.1", 49 | "iconv-lite": "~0.6.3", 50 | "js-yaml": "~3.14.0", 51 | "minimatch": "~3.0.4", 52 | "nopt": "^5.0.0" 53 | }, 54 | "devDependencies": { 55 | "difflet": "~1.0.1", 56 | "eslint-config-grunt": "~2.0.1", 57 | "grunt-contrib-nodeunit": "~5.0.0", 58 | "grunt-contrib-watch": "~1.1.0", 59 | "grunt-eslint": "~24.1.0", 60 | "temporary": "~1.1.0", 61 | "through2": "~4.0.2" 62 | }, 63 | "files": [ 64 | "lib", 65 | "bin" 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // Nullified until the files in test/ can be cleaned 4 | "max-len": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/BOM.txt: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /test/fixtures/Gruntfile-cli.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | var obj = {}; 4 | grunt.registerTask('finalize', 'Print all option values.', function() { 5 | console.log('###' + JSON.stringify(obj) + '###'); 6 | }); 7 | 8 | // Create a per-CLI-option task that stores the value of that option 9 | // to be output via the "finalize" task. 10 | Object.keys(grunt.cli.optlist).forEach(function(name) { 11 | grunt.registerTask(name, 'Store the current "' + name + '" option value.', function() { 12 | obj[this.name] = grunt.option(this.name); 13 | }); 14 | }); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /test/fixtures/a.js: -------------------------------------------------------------------------------- 1 | var a = 1; 2 | -------------------------------------------------------------------------------- /test/fixtures/b.js: -------------------------------------------------------------------------------- 1 | var b = 2; 2 | -------------------------------------------------------------------------------- /test/fixtures/banner.js: -------------------------------------------------------------------------------- 1 | 2 | /* THIS 3 | * IS 4 | * A 5 | * SAMPLE 6 | * BANNER! 7 | */ 8 | 9 | // Comment 10 | 11 | /* Comment */ 12 | -------------------------------------------------------------------------------- /test/fixtures/banner2.js: -------------------------------------------------------------------------------- 1 | 2 | /*! SAMPLE 3 | * BANNER */ 4 | 5 | // Comment 6 | 7 | /* Comment */ 8 | -------------------------------------------------------------------------------- /test/fixtures/banner3.js: -------------------------------------------------------------------------------- 1 | 2 | // This is 3 | // A sample 4 | // Banner 5 | 6 | // But this is not 7 | 8 | /* And neither 9 | * is this 10 | */ 11 | -------------------------------------------------------------------------------- /test/fixtures/error.yaml: -------------------------------------------------------------------------------- 1 | v1.0.0: 2 | - Duplicate key 3 | v1.0.0: 4 | - Duplicate key 5 | -------------------------------------------------------------------------------- /test/fixtures/expand-mapping-ext/dir.ectory/file-no-extension: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/expand-mapping-ext/dir.ectory/file-no-extension -------------------------------------------------------------------------------- /test/fixtures/expand-mapping-ext/dir.ectory/sub.dir.ectory/file.ext.ension: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/expand-mapping-ext/dir.ectory/sub.dir.ectory/file.ext.ension -------------------------------------------------------------------------------- /test/fixtures/expand-mapping-ext/file.ext.ension: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/expand-mapping-ext/file.ext.ension -------------------------------------------------------------------------------- /test/fixtures/expand/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/expand/README.md -------------------------------------------------------------------------------- /test/fixtures/expand/css/baz.css: -------------------------------------------------------------------------------- 1 | baz -------------------------------------------------------------------------------- /test/fixtures/expand/css/qux.css: -------------------------------------------------------------------------------- 1 | qux -------------------------------------------------------------------------------- /test/fixtures/expand/deep/deep.txt: -------------------------------------------------------------------------------- 1 | deep -------------------------------------------------------------------------------- /test/fixtures/expand/deep/deeper/deeper.txt: -------------------------------------------------------------------------------- 1 | deeper -------------------------------------------------------------------------------- /test/fixtures/expand/deep/deeper/deepest/deepest.txt: -------------------------------------------------------------------------------- 1 | deepest -------------------------------------------------------------------------------- /test/fixtures/expand/js/bar.js: -------------------------------------------------------------------------------- 1 | bar -------------------------------------------------------------------------------- /test/fixtures/expand/js/foo.js: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /test/fixtures/files/dist/built-123-a.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/files/dist/built-123-a.js -------------------------------------------------------------------------------- /test/fixtures/files/dist/built-123-b.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/files/dist/built-123-b.js -------------------------------------------------------------------------------- /test/fixtures/files/dist/built-a.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/files/dist/built-a.js -------------------------------------------------------------------------------- /test/fixtures/files/dist/built-b.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/files/dist/built-b.js -------------------------------------------------------------------------------- /test/fixtures/files/dist/built.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/files/dist/built.js -------------------------------------------------------------------------------- /test/fixtures/files/src/file1-123.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/files/src/file1-123.js -------------------------------------------------------------------------------- /test/fixtures/files/src/file1.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/files/src/file1.js -------------------------------------------------------------------------------- /test/fixtures/files/src/file2-123.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/files/src/file2-123.js -------------------------------------------------------------------------------- /test/fixtures/files/src/file2.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/files/src/file2.js -------------------------------------------------------------------------------- /test/fixtures/iso-8859-1.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/iso-8859-1.json -------------------------------------------------------------------------------- /test/fixtures/iso-8859-1.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/iso-8859-1.txt -------------------------------------------------------------------------------- /test/fixtures/iso-8859-1.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/iso-8859-1.yaml -------------------------------------------------------------------------------- /test/fixtures/lint.txt: -------------------------------------------------------------------------------- 1 | // This file is encoded with UTF-16 BE, which produces an error on character 0 with JSHint -------------------------------------------------------------------------------- /test/fixtures/load-npm-tasks/node_modules/grunt-foo-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "grunt-foo-plugin", 4 | "description": "", 5 | "version": "1.0.0" 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/load-npm-tasks/node_modules/grunt-foo-plugin/tasks/foo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | grunt.registerTask('foo', function() { 5 | grunt.log.writeln(this.name + ' has ran.'); 6 | }); 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/load-npm-tasks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "load-npm-tasks", 4 | "devDependencies": { 5 | "grunt-foo-plugin": "1.0.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/load-npm-tasks/test-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "test-package", 4 | "devDependencies": { 5 | "grunt-foo-plugin": "1.0.0" 6 | } 7 | } -------------------------------------------------------------------------------- /test/fixtures/no_BOM.txt: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /test/fixtures/octocat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gruntjs/grunt/aa15bdc5b435e2938744658dec31ec29c3109afc/test/fixtures/octocat.png -------------------------------------------------------------------------------- /test/fixtures/spawn-multibyte.js: -------------------------------------------------------------------------------- 1 | // This is a test fixture for a case where spawn receives incomplete 2 | // multibyte strings in separate data events. 3 | 4 | // A multibyte buffer containing all our output. We will slice it later. 5 | // In this case we are using a Japanese word for hello / good day, where each 6 | // character takes three bytes. 7 | var fullOutput = Buffer.from('こんにちは'); 8 | 9 | // Output one full character and one third of a character 10 | process.stdout.write(fullOutput.slice(0, 4)); 11 | 12 | // Output the rest of the string 13 | process.stdout.write(fullOutput.slice(4)); 14 | 15 | // Do the same for stderr 16 | process.stderr.write(fullOutput.slice(0, 4)); 17 | process.stderr.write(fullOutput.slice(4)); 18 | -------------------------------------------------------------------------------- /test/fixtures/spawn.js: -------------------------------------------------------------------------------- 1 | 2 | var code = Number(process.argv[2]); 3 | 4 | process.stdout.write('stdout\n'); 5 | process.stderr.write('stderr\n'); 6 | 7 | // Instead of process.exit. See https://github.com/cowboy/node-exit 8 | require('exit')(code); 9 | -------------------------------------------------------------------------------- /test/fixtures/template.txt: -------------------------------------------------------------------------------- 1 | Version: <%= grunt.version %>, today: <%= grunt.template.today("yyyy-mm-dd") %>. -------------------------------------------------------------------------------- /test/fixtures/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "baz": [1, 2, 3] 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/utf8.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "Ação é isso aí", 3 | "bar": ["ømg", "pønies"] 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/utf8.txt: -------------------------------------------------------------------------------- 1 | Ação é isso aí 2 | -------------------------------------------------------------------------------- /test/fixtures/utf8.yaml: -------------------------------------------------------------------------------- 1 | foo: Ação é isso aí 2 | bar: 3 | - ømg 4 | - pønies 5 | -------------------------------------------------------------------------------- /test/grunt/cli_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../../lib/grunt'); 4 | 5 | // Parse options printed by fixtures/Gruntfile-cli into an object. 6 | var optionValueRe = /###(.*?)###/; 7 | function getOptionValues(str) { 8 | var matches = str.match(optionValueRe); 9 | return matches ? JSON.parse(matches[1]) : {}; 10 | } 11 | 12 | exports.cli = { 13 | '--debug taskname': function(test) { 14 | test.expect(1); 15 | grunt.util.spawn({ 16 | grunt: true, 17 | args: ['--gruntfile', 'test/fixtures/Gruntfile-cli.js', '--debug', 'debug', 'finalize'], 18 | }, function(err, result) { 19 | test.deepEqual(getOptionValues(result.stdout), {debug: 1}, 'Options should parse correctly.'); 20 | test.done(); 21 | }); 22 | }, 23 | 'taskname --debug': function(test) { 24 | test.expect(1); 25 | grunt.util.spawn({ 26 | grunt: true, 27 | args: ['--gruntfile', 'test/fixtures/Gruntfile-cli.js', 'debug', '--debug', 'finalize'], 28 | }, function(err, result) { 29 | test.deepEqual(getOptionValues(result.stdout), {debug: 1}, 'Options should parse correctly.'); 30 | test.done(); 31 | }); 32 | }, 33 | '--debug --verbose': function(test) { 34 | test.expect(1); 35 | grunt.util.spawn({ 36 | grunt: true, 37 | args: ['--gruntfile', 'test/fixtures/Gruntfile-cli.js', '--debug', '--verbose', 'debug', 'verbose', 'finalize'], 38 | }, function(err, result) { 39 | test.deepEqual(getOptionValues(result.stdout), {debug: 1, verbose: true}, 'Options should parse correctly.'); 40 | test.done(); 41 | }); 42 | }, 43 | '--verbose --debug': function(test) { 44 | test.expect(1); 45 | grunt.util.spawn({ 46 | grunt: true, 47 | args: ['--gruntfile', 'test/fixtures/Gruntfile-cli.js', '--verbose', '--debug', 'debug', 'verbose', 'finalize'], 48 | }, function(err, result) { 49 | test.deepEqual(getOptionValues(result.stdout), {debug: 1, verbose: true}, 'Options should parse correctly.'); 50 | test.done(); 51 | }); 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /test/grunt/config_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../../lib/grunt'); 4 | 5 | exports.config = { 6 | setUp: function(done) { 7 | this.origData = grunt.config.data; 8 | grunt.config.init({ 9 | meta: grunt.file.readJSON('test/fixtures/test.json'), 10 | foo: '<%= meta.foo %>', 11 | foo2: '<%= foo %>', 12 | obj: { 13 | foo: '<%= meta.foo %>', 14 | foo2: '<%= obj.foo %>', 15 | Arr: ['foo', '<%= obj.foo2 %>'], 16 | arr2: ['<%= arr %>', '<%= obj.Arr %>'], 17 | }, 18 | bar: 'bar', 19 | arr: ['foo', '<%= obj.foo2 %>'], 20 | arr2: ['<%= arr %>', '<%= obj.Arr %>'], 21 | buffer: Buffer.from('test'), 22 | }); 23 | done(); 24 | }, 25 | tearDown: function(done) { 26 | grunt.config.data = this.origData; 27 | done(); 28 | }, 29 | 'config.escape': function(test) { 30 | test.expect(2); 31 | test.equal(grunt.config.escape('foo'), 'foo', 'Should do nothing if no . chars.'); 32 | test.equal(grunt.config.escape('foo.bar.baz'), 'foo\\.bar\\.baz', 'Should escape all . chars.'); 33 | test.done(); 34 | }, 35 | 'config.getPropString': function(test) { 36 | test.expect(4); 37 | test.equal(grunt.config.getPropString('foo'), 'foo', 'Should do nothing if already a string.'); 38 | test.equal(grunt.config.getPropString('foo.bar.baz'), 'foo.bar.baz', 'Should do nothing if already a string.'); 39 | test.equal(grunt.config.getPropString(['foo', 'bar']), 'foo.bar', 'Should join parts into a dot-delimited string.'); 40 | test.equal(grunt.config.getPropString(['foo.bar', 'baz.qux.zip']), 'foo\\.bar.baz\\.qux\\.zip', 'Should join parts into a dot-delimited string, escaping . chars in parts.'); 41 | test.done(); 42 | }, 43 | 'config.getRaw': function(test) { 44 | test.expect(4); 45 | test.equal(grunt.config.getRaw('foo'), '<%= meta.foo %>', 'Should not process templates.'); 46 | test.equal(grunt.config.getRaw('obj.foo2'), '<%= obj.foo %>', 'Should not process templates.'); 47 | test.equal(grunt.config.getRaw(['obj', 'foo2']), '<%= obj.foo %>', 'Should not process templates.'); 48 | test.deepEqual(grunt.config.getRaw('arr'), ['foo', '<%= obj.foo2 %>'], 'Should not process templates.'); 49 | test.done(); 50 | }, 51 | 'config.process': function(test) { 52 | test.expect(7); 53 | test.equal(grunt.config.process('<%= meta.foo %>'), 'bar', 'Should process templates.'); 54 | test.equal(grunt.config.process('<%= foo %>'), 'bar', 'Should process templates recursively.'); 55 | test.equal(grunt.config.process('<%= obj.foo %>'), 'bar', 'Should process deeply nested templates recursively.'); 56 | test.deepEqual(grunt.config.process(['foo', '<%= obj.foo2 %>']), ['foo', 'bar'], 'Should process templates in arrays.'); 57 | test.deepEqual(grunt.config.process(['<%= arr %>', '<%= obj.Arr %>']), [['foo', 'bar'], ['foo', 'bar']], 'Should expand <%= arr %> and <%= obj.Arr %> values as objects if possible.'); 58 | var buf = grunt.config.process('<%= buffer %>'); 59 | test.ok(Buffer.isBuffer(buf), 'Should retrieve Buffer instances as Buffer.'); 60 | test.deepEqual(buf, Buffer.from('test'), 'Should return buffers as-is.'); 61 | test.done(); 62 | }, 63 | 'config.get': function(test) { 64 | test.expect(10); 65 | test.equal(grunt.config.get('foo'), 'bar', 'Should process templates.'); 66 | test.equal(grunt.config.get('foo2'), 'bar', 'Should process templates recursively.'); 67 | test.equal(grunt.config.get('obj.foo2'), 'bar', 'Should process deeply nested templates recursively.'); 68 | test.equal(grunt.config.get(['obj', 'foo2']), 'bar', 'Should process deeply nested templates recursively.'); 69 | test.deepEqual(grunt.config.get('arr'), ['foo', 'bar'], 'Should process templates in arrays.'); 70 | test.deepEqual(grunt.config.get('obj.Arr'), ['foo', 'bar'], 'Should process templates in arrays.'); 71 | test.deepEqual(grunt.config.get('arr2'), [['foo', 'bar'], ['foo', 'bar']], 'Should expand <%= arr %> and <%= obj.Arr %> values as objects if possible.'); 72 | test.deepEqual(grunt.config.get(['obj', 'arr2']), [['foo', 'bar'], ['foo', 'bar']], 'Should expand <%= arr %> and <%= obj.Arr %> values as objects if possible.'); 73 | var buf = grunt.config.get('buffer'); 74 | test.ok(Buffer.isBuffer(buf), 'Should retrieve Buffer instances as Buffer.'); 75 | test.deepEqual(buf, Buffer.from('test'), 'Should return buffers as-is.'); 76 | test.done(); 77 | }, 78 | 'config.set': function(test) { 79 | test.expect(6); 80 | test.equal(grunt.config.set('foo3', '<%= foo2 %>'), '<%= foo2 %>', 'Should set values.'); 81 | test.equal(grunt.config.getRaw('foo3'), '<%= foo2 %>', 'Should have set the value.'); 82 | test.equal(grunt.config.data.foo3, '<%= foo2 %>', 'Should have set the value.'); 83 | test.equal(grunt.config.set('a.b.c', '<%= foo2 %>'), '<%= foo2 %>', 'Should create interim objects.'); 84 | test.equal(grunt.config.getRaw('a.b.c'), '<%= foo2 %>', 'Should have set the value.'); 85 | test.equal(grunt.config.data.a.b.c, '<%= foo2 %>', 'Should have set the value.'); 86 | test.done(); 87 | }, 88 | 'config.merge': function(test) { 89 | test.expect(4); 90 | test.deepEqual(grunt.config.merge({}), grunt.config.getRaw(), 'Should return internal data object.'); 91 | grunt.config.set('obj', {a: 12}); 92 | grunt.config.merge({ 93 | foo: 'test', 94 | baz: '123', 95 | obj: {a: 34, b: 56}, 96 | }); 97 | test.deepEqual(grunt.config.getRaw('foo'), 'test', 'Should overwrite existing properties.'); 98 | test.deepEqual(grunt.config.getRaw('baz'), '123', 'Should add new properties.'); 99 | test.deepEqual(grunt.config.getRaw('obj'), {a: 34, b: 56}, 'Should deep merge.'); 100 | test.done(); 101 | }, 102 | 'config': function(test) { 103 | test.expect(10); 104 | test.equal(grunt.config('foo'), 'bar', 'Should retrieve processed data.'); 105 | test.equal(grunt.config('obj.foo2'), 'bar', 'Should retrieve processed data.'); 106 | test.equal(grunt.config(['obj', 'foo2']), 'bar', 'Should retrieve processed data.'); 107 | test.deepEqual(grunt.config('arr'), ['foo', 'bar'], 'Should process templates in arrays.'); 108 | 109 | test.equal(grunt.config('foo3', '<%= foo2 %>'), '<%= foo2 %>', 'Should set values.'); 110 | test.equal(grunt.config.getRaw('foo3'), '<%= foo2 %>', 'Should have set the value.'); 111 | test.equal(grunt.config.data.foo3, '<%= foo2 %>', 'Should have set the value.'); 112 | test.equal(grunt.config('a.b.c', '<%= foo2 %>'), '<%= foo2 %>', 'Should create interim objects.'); 113 | test.equal(grunt.config.getRaw('a.b.c'), '<%= foo2 %>', 'Should have set the value.'); 114 | test.equal(grunt.config.data.a.b.c, '<%= foo2 %>', 'Should have set the value.'); 115 | test.done(); 116 | }, 117 | 'config.requires': function(test) { 118 | test.expect(8); 119 | grunt.log.muted = true; 120 | test.doesNotThrow(function() { grunt.config.requires('foo'); }, 'This property exists.'); 121 | test.doesNotThrow(function() { grunt.config.requires('obj.foo'); }, 'This property exists.'); 122 | test.doesNotThrow(function() { grunt.config.requires('foo', 'obj.foo', 'obj.foo2'); }, 'These properties exist.'); 123 | test.doesNotThrow(function() { grunt.config.requires('foo', ['obj', 'foo'], ['obj', 'foo2']); }, 'These properties exist.'); 124 | test.throws(function() { grunt.config.requires('xyz'); }, 'This property does not exist.'); 125 | test.throws(function() { grunt.config.requires('obj.xyz'); }, 'This property does not exist.'); 126 | test.throws(function() { grunt.config.requires('foo', 'obj.foo', 'obj.xyz'); }, 'One property does not exist.'); 127 | test.throws(function() { grunt.config.requires('foo', ['obj', 'foo'], ['obj', 'xyz']); }, 'One property does not exist.'); 128 | grunt.log.muted = false; 129 | test.done(); 130 | }, 131 | }; 132 | -------------------------------------------------------------------------------- /test/grunt/event_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../../lib/grunt'); 4 | 5 | exports.event = function(test) { 6 | test.expect(3); 7 | grunt.event.on('test.foo', function(a, b, c) { 8 | // This should get executed once (emit test.foo). 9 | test.equals(a + b + c, '123', 'Should have received the correct arguments.'); 10 | }); 11 | grunt.event.on('test.*', function(a, b, c) { 12 | // This should get executed twice (emit test.foo and test.bar). 13 | test.equals(a + b + c, '123', 'Should have received the correct arguments.'); 14 | }); 15 | grunt.event.emit('test.foo', '1', '2', '3'); 16 | grunt.event.emit('test.bar', '1', '2', '3'); 17 | test.done(); 18 | }; 19 | -------------------------------------------------------------------------------- /test/grunt/file_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../../lib/grunt'); 4 | 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | 8 | var Tempfile = require('temporary/lib/file'); 9 | var Tempdir = require('temporary/lib/dir'); 10 | 11 | var win32 = process.platform === 'win32'; 12 | 13 | var tmpdir = new Tempdir(); 14 | try { 15 | fs.symlinkSync(path.resolve('test/fixtures/octocat.png'), path.join(tmpdir.path, 'octocat.png'), 'file'); 16 | fs.symlinkSync(path.resolve('test/fixtures/expand'), path.join(tmpdir.path, 'expand'), 'dir'); 17 | } catch (err) { 18 | console.error('** ERROR: Cannot create symbolic links; link-related tests will fail.'); 19 | if (win32) { 20 | console.error('** Tests must be run with Administrator privileges on Windows.'); 21 | } 22 | } 23 | 24 | exports['file.match'] = { 25 | 'empty set': function(test) { 26 | test.expect(12); 27 | // Should return empty set if a required argument is missing or an empty set. 28 | test.deepEqual(grunt.file.match(null, null), [], 'should return empty set.'); 29 | test.deepEqual(grunt.file.match({}, null, null), [], 'should return empty set.'); 30 | test.deepEqual(grunt.file.match(null, 'foo.js'), [], 'should return empty set.'); 31 | test.deepEqual(grunt.file.match('*.js', null), [], 'should return empty set.'); 32 | test.deepEqual(grunt.file.match({}, null, 'foo.js'), [], 'should return empty set.'); 33 | test.deepEqual(grunt.file.match({}, '*.js', null), [], 'should return empty set.'); 34 | test.deepEqual(grunt.file.match({}, [], 'foo.js'), [], 'should return empty set.'); 35 | test.deepEqual(grunt.file.match({}, '*.js', []), [], 'should return empty set.'); 36 | test.deepEqual(grunt.file.match(null, ['foo.js']), [], 'should return empty set.'); 37 | test.deepEqual(grunt.file.match(['*.js'], null), [], 'should return empty set.'); 38 | test.deepEqual(grunt.file.match({}, null, ['foo.js']), [], 'should return empty set.'); 39 | test.deepEqual(grunt.file.match({}, ['*.js'], null), [], 'should return empty set.'); 40 | test.done(); 41 | }, 42 | 'basic matching': function(test) { 43 | test.expect(6); 44 | test.deepEqual(grunt.file.match('*.js', 'foo.js'), ['foo.js'], 'should match correctly.'); 45 | test.deepEqual(grunt.file.match('*.js', ['foo.js']), ['foo.js'], 'should match correctly.'); 46 | test.deepEqual(grunt.file.match('*.js', ['foo.js', 'bar.css']), ['foo.js'], 'should match correctly.'); 47 | test.deepEqual(grunt.file.match(['*.js', '*.css'], 'foo.js'), ['foo.js'], 'should match correctly.'); 48 | test.deepEqual(grunt.file.match(['*.js', '*.css'], ['foo.js']), ['foo.js'], 'should match correctly.'); 49 | test.deepEqual(grunt.file.match(['*.js', '*.css'], ['foo.js', 'bar.css']), ['foo.js', 'bar.css'], 'should match correctly.'); 50 | test.done(); 51 | }, 52 | 'no matches': function(test) { 53 | test.expect(2); 54 | test.deepEqual(grunt.file.match('*.js', 'foo.css'), [], 'should fail to match.'); 55 | test.deepEqual(grunt.file.match('*.js', ['foo.css', 'bar.css']), [], 'should fail to match.'); 56 | test.done(); 57 | }, 58 | 'unique': function(test) { 59 | test.expect(2); 60 | test.deepEqual(grunt.file.match('*.js', ['foo.js', 'foo.js']), ['foo.js'], 'should return a uniqued set.'); 61 | test.deepEqual(grunt.file.match(['*.js', '*.*'], ['foo.js', 'foo.js']), ['foo.js'], 'should return a uniqued set.'); 62 | test.done(); 63 | }, 64 | 'flatten': function(test) { 65 | test.expect(1); 66 | test.deepEqual(grunt.file.match([['*.js', '*.css'], ['*.*', '*.js']], ['foo.js', 'bar.css']), ['foo.js', 'bar.css'], 'should process nested pattern arrays correctly.'); 67 | test.done(); 68 | }, 69 | 'exclusion': function(test) { 70 | test.expect(5); 71 | test.deepEqual(grunt.file.match(['!*.js'], ['foo.js', 'bar.js']), [], 'solitary exclusion should match nothing'); 72 | test.deepEqual(grunt.file.match(['*.js', '!*.js'], ['foo.js', 'bar.js']), [], 'exclusion should cancel match'); 73 | test.deepEqual(grunt.file.match(['*.js', '!f*.js'], ['foo.js', 'bar.js', 'baz.js']), ['bar.js', 'baz.js'], 'partial exclusion should partially cancel match'); 74 | test.deepEqual(grunt.file.match(['*.js', '!*.js', 'b*.js'], ['foo.js', 'bar.js', 'baz.js']), ['bar.js', 'baz.js'], 'inclusion / exclusion order matters'); 75 | test.deepEqual(grunt.file.match(['*.js', '!f*.js', '*.js'], ['foo.js', 'bar.js', 'baz.js']), ['bar.js', 'baz.js', 'foo.js'], 'inclusion / exclusion order matters'); 76 | test.done(); 77 | }, 78 | 'options.matchBase': function(test) { 79 | test.expect(2); 80 | test.deepEqual(grunt.file.match({matchBase: true}, '*.js', ['foo.js', 'bar', 'baz/xyz.js']), ['foo.js', 'baz/xyz.js'], 'should matchBase (minimatch) when specified.'); 81 | test.deepEqual(grunt.file.match('*.js', ['foo.js', 'bar', 'baz/xyz.js']), ['foo.js'], 'should not matchBase (minimatch) by default.'); 82 | test.done(); 83 | } 84 | }; 85 | 86 | exports['file.isMatch'] = { 87 | 'basic matching': function(test) { 88 | test.expect(6); 89 | test.ok(grunt.file.isMatch('*.js', 'foo.js'), 'should match correctly.'); 90 | test.ok(grunt.file.isMatch('*.js', ['foo.js']), 'should match correctly.'); 91 | test.ok(grunt.file.isMatch('*.js', ['foo.js', 'bar.css']), 'should match correctly.'); 92 | test.ok(grunt.file.isMatch(['*.js', '*.css'], 'foo.js'), 'should match correctly.'); 93 | test.ok(grunt.file.isMatch(['*.js', '*.css'], ['foo.js']), 'should match correctly.'); 94 | test.ok(grunt.file.isMatch(['*.js', '*.css'], ['foo.js', 'bar.css']), 'should match correctly.'); 95 | test.done(); 96 | }, 97 | 'no matches': function(test) { 98 | test.expect(6); 99 | test.equal(grunt.file.isMatch('*.js', 'foo.css'), false, 'should fail to match.'); 100 | test.equal(grunt.file.isMatch('*.js', ['foo.css', 'bar.css']), false, 'should fail to match.'); 101 | test.equal(grunt.file.isMatch(null, 'foo.css'), false, 'should fail to match.'); 102 | test.equal(grunt.file.isMatch('*.js', null), false, 'should fail to match.'); 103 | test.equal(grunt.file.isMatch([], 'foo.css'), false, 'should fail to match.'); 104 | test.equal(grunt.file.isMatch('*.js', []), false, 'should fail to match.'); 105 | test.done(); 106 | }, 107 | 'options.matchBase': function(test) { 108 | test.expect(2); 109 | test.ok(grunt.file.isMatch({matchBase: true}, '*.js', ['baz/xyz.js']), 'should matchBase (minimatch) when specified.'); 110 | test.equal(grunt.file.isMatch('*.js', ['baz/xyz.js']), false, 'should not matchBase (minimatch) by default.'); 111 | test.done(); 112 | } 113 | }; 114 | 115 | exports['file.expand*'] = { 116 | setUp: function(done) { 117 | this.cwd = process.cwd(); 118 | process.chdir('test/fixtures/expand'); 119 | done(); 120 | }, 121 | tearDown: function(done) { 122 | process.chdir(this.cwd); 123 | done(); 124 | }, 125 | 'basic matching': function(test) { 126 | test.expect(8); 127 | test.deepEqual(grunt.file.expand('**/*.js'), ['js/bar.js', 'js/foo.js'], 'should match.'); 128 | test.deepEqual(grunt.file.expand('**/*.js', '**/*.css'), ['js/bar.js', 'js/foo.js', 'css/baz.css', 'css/qux.css'], 'should match.'); 129 | test.deepEqual(grunt.file.expand(['**/*.js', '**/*.css']), ['js/bar.js', 'js/foo.js', 'css/baz.css', 'css/qux.css'], 'should match.'); 130 | test.deepEqual(grunt.file.expand('**d*/**'), [ 131 | 'deep', 132 | 'deep/deep.txt', 133 | 'deep/deeper', 134 | 'deep/deeper/deeper.txt', 135 | 'deep/deeper/deepest', 136 | 'deep/deeper/deepest/deepest.txt'], 'should match files and directories.'); 137 | test.deepEqual(grunt.file.expand({mark: true}, '**d*/**'), [ 138 | 'deep/', 139 | 'deep/deep.txt', 140 | 'deep/deeper/', 141 | 'deep/deeper/deeper.txt', 142 | 'deep/deeper/deepest/', 143 | 'deep/deeper/deepest/deepest.txt'], 'the minimatch "mark" option ensures directories end in /.'); 144 | test.deepEqual(grunt.file.expand('**d*/**/'), [ 145 | 'deep/', 146 | 'deep/deeper/', 147 | 'deep/deeper/deepest/'], 'should match directories, arbitrary / at the end appears in matches.'); 148 | test.deepEqual(grunt.file.expand({mark: true}, '**d*/**/'), [ 149 | 'deep/', 150 | 'deep/deeper/', 151 | 'deep/deeper/deepest/'], 'should match directories, arbitrary / at the end appears in matches.'); 152 | test.deepEqual(grunt.file.expand('*.xyz'), [], 'should fail to match.'); 153 | test.done(); 154 | }, 155 | 'filter': function(test) { 156 | test.expect(5); 157 | test.deepEqual(grunt.file.expand({filter: 'isFile'}, '**d*/**'), [ 158 | 'deep/deep.txt', 159 | 'deep/deeper/deeper.txt', 160 | 'deep/deeper/deepest/deepest.txt' 161 | ], 'should match files only.'); 162 | test.deepEqual(grunt.file.expand({filter: 'isDirectory'}, '**d*/**'), [ 163 | 'deep', 164 | 'deep/deeper', 165 | 'deep/deeper/deepest' 166 | ], 'should match directories only.'); 167 | test.deepEqual(grunt.file.expand({filter: function(filepath) { return (/deepest/).test(filepath); }}, '**'), [ 168 | 'deep/deeper/deepest', 169 | 'deep/deeper/deepest/deepest.txt', 170 | ], 'should filter arbitrarily.'); 171 | test.deepEqual(grunt.file.expand({filter: 'isFile'}, 'js', 'css'), [], 'should fail to match.'); 172 | test.deepEqual(grunt.file.expand({filter: 'isDirectory'}, '**/*.js'), [], 'should fail to match.'); 173 | test.done(); 174 | }, 175 | 'unique': function(test) { 176 | test.expect(4); 177 | test.deepEqual(grunt.file.expand('**/*.js', 'js/*.js'), ['js/bar.js', 'js/foo.js'], 'file list should be uniqed.'); 178 | test.deepEqual(grunt.file.expand('**/*.js', '**/*.css', 'js/*.js'), ['js/bar.js', 'js/foo.js', 'css/baz.css', 'css/qux.css'], 'file list should be uniqed.'); 179 | test.deepEqual(grunt.file.expand('js', 'js/'), ['js', 'js/'], 'mixed non-ending-/ and ending-/ dirs will not be uniqed by default.'); 180 | test.deepEqual(grunt.file.expand({mark: true}, 'js', 'js/'), ['js/'], 'mixed non-ending-/ and ending-/ dirs will be uniqed when "mark" is specified.'); 181 | test.done(); 182 | }, 183 | 'file order': function(test) { 184 | test.expect(4); 185 | var actual = grunt.file.expand('**/*.{js,css}'); 186 | var expected = ['css/baz.css', 'css/qux.css', 'js/bar.js', 'js/foo.js']; 187 | test.deepEqual(actual, expected, 'should select 4 files in this order, by default.'); 188 | 189 | actual = grunt.file.expand('js/foo.js', 'js/bar.js', '**/*.{js,css}'); 190 | expected = ['js/foo.js', 'js/bar.js', 'css/baz.css', 'css/qux.css']; 191 | test.deepEqual(actual, expected, 'specifically-specified-up-front file order should be maintained.'); 192 | 193 | actual = grunt.file.expand('js/bar.js', 'js/foo.js', '**/*.{js,css}'); 194 | expected = ['js/bar.js', 'js/foo.js', 'css/baz.css', 'css/qux.css']; 195 | test.deepEqual(actual, expected, 'specifically-specified-up-front file order should be maintained.'); 196 | 197 | actual = grunt.file.expand('js/foo.js', '**/*.{js,css}', '!js/bar.js', 'js/bar.js'); 198 | expected = ['js/foo.js', 'css/baz.css', 'css/qux.css', 'js/bar.js']; 199 | test.deepEqual(actual, expected, 'if a file is excluded and then re-added, it should be added at the end.'); 200 | test.done(); 201 | }, 202 | 'flatten': function(test) { 203 | test.expect(1); 204 | test.deepEqual(grunt.file.expand([['**/*.js'], ['**/*.css', 'js/*.js']]), ['js/bar.js', 'js/foo.js', 'css/baz.css', 'css/qux.css'], 'should match.'); 205 | test.done(); 206 | }, 207 | 'exclusion': function(test) { 208 | test.expect(8); 209 | test.deepEqual(grunt.file.expand(['!js/*.js']), [], 'solitary exclusion should match nothing'); 210 | test.deepEqual(grunt.file.expand(['js/bar.js', '!js/bar.js']), [], 'exclusion should cancel match'); 211 | test.deepEqual(grunt.file.expand(['**/*.js', '!js/foo.js']), ['js/bar.js'], 'should omit single file from matched set'); 212 | test.deepEqual(grunt.file.expand(['!js/foo.js', '**/*.js']), ['js/bar.js', 'js/foo.js'], 'inclusion / exclusion order matters'); 213 | test.deepEqual(grunt.file.expand(['**/*.js', '**/*.css', '!js/bar.js', '!css/baz.css']), ['js/foo.js', 'css/qux.css'], 'multiple exclusions should be removed from the set'); 214 | test.deepEqual(grunt.file.expand(['**/*.js', '**/*.css', '!**/*.css']), ['js/bar.js', 'js/foo.js'], 'excluded wildcards should be removed from the matched set'); 215 | test.deepEqual(grunt.file.expand(['js/bar.js', 'js/foo.js', 'css/baz.css', 'css/qux.css', '!**/b*.*']), ['js/foo.js', 'css/qux.css'], 'different pattern for exclusion should still work'); 216 | test.deepEqual(grunt.file.expand(['js/bar.js', '!**/b*.*', 'js/foo.js', 'css/baz.css', 'css/qux.css']), ['js/foo.js', 'css/baz.css', 'css/qux.css'], 'inclusion / exclusion order matters'); 217 | test.done(); 218 | }, 219 | 'options.matchBase': function(test) { 220 | test.expect(4); 221 | var opts = {matchBase: true}; 222 | test.deepEqual(grunt.file.expand('*.js'), [], 'should not matchBase (minimatch) by default.'); 223 | test.deepEqual(grunt.file.expand(opts, '*.js'), ['js/bar.js', 'js/foo.js'], 'options should be passed through to minimatch.'); 224 | test.deepEqual(grunt.file.expand(opts, '*.js', '*.css'), ['js/bar.js', 'js/foo.js', 'css/baz.css', 'css/qux.css'], 'should match.'); 225 | test.deepEqual(grunt.file.expand(opts, ['*.js', '*.css']), ['js/bar.js', 'js/foo.js', 'css/baz.css', 'css/qux.css'], 'should match.'); 226 | test.done(); 227 | }, 228 | 'options.cwd': function(test) { 229 | test.expect(4); 230 | var cwd = path.resolve(process.cwd(), '..'); 231 | test.deepEqual(grunt.file.expand({cwd: cwd}, ['expand/js', 'expand/js/*']), ['expand/js', 'expand/js/bar.js', 'expand/js/foo.js'], 'should match.'); 232 | test.deepEqual(grunt.file.expand({cwd: cwd, filter: 'isFile'}, ['expand/js', 'expand/js/*']), ['expand/js/bar.js', 'expand/js/foo.js'], 'should match.'); 233 | test.deepEqual(grunt.file.expand({cwd: cwd, filter: 'isDirectory'}, ['expand/js', 'expand/js/*']), ['expand/js'], 'should match.'); 234 | test.deepEqual(grunt.file.expand({cwd: cwd, filter: 'isFile'}, ['expand/js', 'expand/js/*', '!**/b*.js']), ['expand/js/foo.js'], 'should negate properly.'); 235 | test.done(); 236 | }, 237 | 'options.nonull': function(test) { 238 | test.expect(2); 239 | var opts = {nonull: true}; 240 | test.deepEqual(grunt.file.expand(opts, ['js/a*', 'js/b*', 'js/c*']), ['js/a*', 'js/bar.js', 'js/c*'], 'non-matching patterns should be returned in result set.'); 241 | test.deepEqual(grunt.file.expand(opts, ['js/foo.js', 'js/bar.js', 'js/baz.js']), ['js/foo.js', 'js/bar.js', 'js/baz.js'], 'non-matching filenames should be returned in result set.'); 242 | test.done(); 243 | }, 244 | }; 245 | 246 | exports['file.expandMapping'] = { 247 | setUp: function(done) { 248 | this.cwd = process.cwd(); 249 | process.chdir('test/fixtures'); 250 | done(); 251 | }, 252 | tearDown: function(done) { 253 | process.chdir(this.cwd); 254 | done(); 255 | }, 256 | 'basic matching': function(test) { 257 | test.expect(2); 258 | 259 | var actual = grunt.file.expandMapping(['expand/**/*.txt'], 'dest'); 260 | var expected = [ 261 | {dest: 'dest/expand/deep/deep.txt', src: ['expand/deep/deep.txt']}, 262 | {dest: 'dest/expand/deep/deeper/deeper.txt', src: ['expand/deep/deeper/deeper.txt']}, 263 | {dest: 'dest/expand/deep/deeper/deepest/deepest.txt', src: ['expand/deep/deeper/deepest/deepest.txt']}, 264 | ]; 265 | test.deepEqual(actual, expected, 'basic src-dest options'); 266 | 267 | actual = grunt.file.expandMapping(['expand/**/*.txt'], 'dest/'); 268 | test.deepEqual(actual, expected, 'destBase should behave the same both with or without trailing slash'); 269 | 270 | test.done(); 271 | }, 272 | 'flatten': function(test) { 273 | test.expect(1); 274 | var actual = grunt.file.expandMapping(['expand/**/*.txt'], 'dest', {flatten: true}); 275 | var expected = [ 276 | {dest: 'dest/deep.txt', src: ['expand/deep/deep.txt']}, 277 | {dest: 'dest/deeper.txt', src: ['expand/deep/deeper/deeper.txt']}, 278 | {dest: 'dest/deepest.txt', src: ['expand/deep/deeper/deepest/deepest.txt']}, 279 | ]; 280 | test.deepEqual(actual, expected, 'dest paths should be flattened pre-destBase+destPath join'); 281 | test.done(); 282 | }, 283 | 'ext': function(test) { 284 | test.expect(3); 285 | var actual, expected; 286 | actual = grunt.file.expandMapping(['expand/**/*.txt'], 'dest', {ext: '.foo'}); 287 | expected = [ 288 | {dest: 'dest/expand/deep/deep.foo', src: ['expand/deep/deep.txt']}, 289 | {dest: 'dest/expand/deep/deeper/deeper.foo', src: ['expand/deep/deeper/deeper.txt']}, 290 | {dest: 'dest/expand/deep/deeper/deepest/deepest.foo', src: ['expand/deep/deeper/deepest/deepest.txt']}, 291 | ]; 292 | test.deepEqual(actual, expected, 'specified extension should be added'); 293 | actual = grunt.file.expandMapping(['expand-mapping-ext/**/file*'], 'dest', {ext: '.foo'}); 294 | expected = [ 295 | {dest: 'dest/expand-mapping-ext/dir.ectory/file-no-extension.foo', src: ['expand-mapping-ext/dir.ectory/file-no-extension']}, 296 | {dest: 'dest/expand-mapping-ext/dir.ectory/sub.dir.ectory/file.foo', src: ['expand-mapping-ext/dir.ectory/sub.dir.ectory/file.ext.ension']}, 297 | {dest: 'dest/expand-mapping-ext/file.foo', src: ['expand-mapping-ext/file.ext.ension']}, 298 | ]; 299 | test.deepEqual(actual, expected, 'specified extension should be added'); 300 | actual = grunt.file.expandMapping(['expand/**/*.txt'], 'dest', {ext: ''}); 301 | expected = [ 302 | {dest: 'dest/expand/deep/deep', src: ['expand/deep/deep.txt']}, 303 | {dest: 'dest/expand/deep/deeper/deeper', src: ['expand/deep/deeper/deeper.txt']}, 304 | {dest: 'dest/expand/deep/deeper/deepest/deepest', src: ['expand/deep/deeper/deepest/deepest.txt']}, 305 | ]; 306 | test.deepEqual(actual, expected, 'empty string extension should be added'); 307 | test.done(); 308 | }, 309 | 'extDot': function(test) { 310 | test.expect(2); 311 | var actual, expected; 312 | 313 | actual = grunt.file.expandMapping(['expand-mapping-ext/**/file*'], 'dest', {ext: '.foo', extDot: 'first'}); 314 | expected = [ 315 | {dest: 'dest/expand-mapping-ext/dir.ectory/file-no-extension.foo', src: ['expand-mapping-ext/dir.ectory/file-no-extension']}, 316 | {dest: 'dest/expand-mapping-ext/dir.ectory/sub.dir.ectory/file.foo', src: ['expand-mapping-ext/dir.ectory/sub.dir.ectory/file.ext.ension']}, 317 | {dest: 'dest/expand-mapping-ext/file.foo', src: ['expand-mapping-ext/file.ext.ension']}, 318 | ]; 319 | test.deepEqual(actual, expected, 'extDot of "first" should replace everything after the first dot in the filename.'); 320 | 321 | actual = grunt.file.expandMapping(['expand-mapping-ext/**/file*'], 'dest', {ext: '.foo', extDot: 'last'}); 322 | expected = [ 323 | {dest: 'dest/expand-mapping-ext/dir.ectory/file-no-extension.foo', src: ['expand-mapping-ext/dir.ectory/file-no-extension']}, 324 | {dest: 'dest/expand-mapping-ext/dir.ectory/sub.dir.ectory/file.ext.foo', src: ['expand-mapping-ext/dir.ectory/sub.dir.ectory/file.ext.ension']}, 325 | {dest: 'dest/expand-mapping-ext/file.ext.foo', src: ['expand-mapping-ext/file.ext.ension']}, 326 | ]; 327 | test.deepEqual(actual, expected, 'extDot of "last" should replace everything after the last dot in the filename.'); 328 | 329 | test.done(); 330 | }, 331 | 'cwd': function(test) { 332 | test.expect(1); 333 | var actual = grunt.file.expandMapping(['**/*.txt'], 'dest', {cwd: 'expand'}); 334 | var expected = [ 335 | {dest: 'dest/deep/deep.txt', src: ['expand/deep/deep.txt']}, 336 | {dest: 'dest/deep/deeper/deeper.txt', src: ['expand/deep/deeper/deeper.txt']}, 337 | {dest: 'dest/deep/deeper/deepest/deepest.txt', src: ['expand/deep/deeper/deepest/deepest.txt']}, 338 | ]; 339 | test.deepEqual(actual, expected, 'cwd should be stripped from front of destPath, pre-destBase+destPath join'); 340 | test.done(); 341 | }, 342 | 'rename': function(test) { 343 | test.expect(1); 344 | var actual = grunt.file.expandMapping(['**/*.txt'], 'dest', { 345 | cwd: 'expand', 346 | flatten: true, 347 | rename: function(destBase, destPath, options) { 348 | return path.join(destBase, options.cwd, 'o-m-g', destPath); 349 | } 350 | }); 351 | var expected = [ 352 | {dest: 'dest/expand/o-m-g/deep.txt', src: ['expand/deep/deep.txt']}, 353 | {dest: 'dest/expand/o-m-g/deeper.txt', src: ['expand/deep/deeper/deeper.txt']}, 354 | {dest: 'dest/expand/o-m-g/deepest.txt', src: ['expand/deep/deeper/deepest/deepest.txt']}, 355 | ]; 356 | test.deepEqual(actual, expected, 'custom rename function should be used to build dest, post-flatten'); 357 | test.done(); 358 | }, 359 | 'rename to same dest': function(test) { 360 | test.expect(1); 361 | var actual = grunt.file.expandMapping(['**/*'], 'dest', { 362 | filter: 'isFile', 363 | cwd: 'expand', 364 | flatten: true, 365 | nosort: true, 366 | rename: function(destBase, destPath) { 367 | return path.join(destBase, 'all' + path.extname(destPath)); 368 | } 369 | }); 370 | var expected = [ 371 | {dest: 'dest/all.md', src: ['expand/README.md']}, 372 | {dest: 'dest/all.css', src: ['expand/css/baz.css', 'expand/css/qux.css']}, 373 | {dest: 'dest/all.txt', src: ['expand/deep/deep.txt', 'expand/deep/deeper/deeper.txt', 'expand/deep/deeper/deepest/deepest.txt']}, 374 | {dest: 'dest/all.js', src: ['expand/js/bar.js', 'expand/js/foo.js']}, 375 | ]; 376 | test.deepEqual(actual, expected, 'if dest is same for multiple src, create an array of src'); 377 | test.done(); 378 | }, 379 | }; 380 | 381 | // Compare two buffers. Returns true if they are equivalent. 382 | var compareBuffers = function(buf1, buf2) { 383 | if (!Buffer.isBuffer(buf1) || !Buffer.isBuffer(buf2)) { return false; } 384 | if (buf1.length !== buf2.length) { return false; } 385 | for (var i = 0; i < buf2.length; i++) { 386 | if (buf1[i] !== buf2[i]) { return false; } 387 | } 388 | return true; 389 | }; 390 | 391 | // Compare two files. Returns true if they are equivalent. 392 | var compareFiles = function(filepath1, filepath2) { 393 | return compareBuffers(fs.readFileSync(filepath1), fs.readFileSync(filepath2)); 394 | }; 395 | 396 | exports.file = { 397 | setUp: function(done) { 398 | this.defaultEncoding = grunt.file.defaultEncoding; 399 | grunt.file.defaultEncoding = 'utf8'; 400 | this.string = 'Ação é isso aí\n'; 401 | this.object = {foo: 'Ação é isso aí', bar: ['ømg', 'pønies']}; 402 | this.writeOption = grunt.option('write'); 403 | 404 | // Testing that warnings were displayed. 405 | this.oldFailWarnFn = grunt.fail.warn; 406 | this.oldLogWarnFn = grunt.log.warn; 407 | this.resetWarnCount = function() { 408 | this.warnCount = 0; 409 | }.bind(this); 410 | grunt.fail.warn = grunt.log.warn = function() { 411 | this.warnCount += 1; 412 | }.bind(this); 413 | 414 | done(); 415 | }, 416 | tearDown: function(done) { 417 | grunt.file.defaultEncoding = this.defaultEncoding; 418 | grunt.option('write', this.writeOption); 419 | 420 | grunt.fail.warn = this.oldFailWarnFn; 421 | grunt.log.warn = this.oldLogWarnFn; 422 | 423 | done(); 424 | }, 425 | 'read': function(test) { 426 | test.expect(6); 427 | test.strictEqual(grunt.file.read('test/fixtures/utf8.txt'), this.string, 'file should be read as utf8 by default.'); 428 | test.strictEqual(grunt.file.read('test/fixtures/iso-8859-1.txt', {encoding: 'iso-8859-1'}), this.string, 'file should be read using the specified encoding.'); 429 | test.ok(compareBuffers(grunt.file.read('test/fixtures/octocat.png', {encoding: null}), fs.readFileSync('test/fixtures/octocat.png')), 'file should be read as a buffer if encoding is specified as null.'); 430 | 431 | test.strictEqual(grunt.file.read('test/fixtures/BOM.txt'), 'foo', 'file should have BOM stripped.'); 432 | grunt.file.preserveBOM = true; 433 | test.strictEqual(grunt.file.read('test/fixtures/BOM.txt'), '\ufeff' + 'foo', 'file should have BOM preserved.'); 434 | grunt.file.preserveBOM = false; 435 | 436 | grunt.file.defaultEncoding = 'iso-8859-1'; 437 | test.strictEqual(grunt.file.read('test/fixtures/iso-8859-1.txt'), this.string, 'changing the default encoding should work.'); 438 | test.done(); 439 | }, 440 | 'readJSON': function(test) { 441 | test.expect(3); 442 | var obj; 443 | obj = grunt.file.readJSON('test/fixtures/utf8.json'); 444 | test.deepEqual(obj, this.object, 'file should be read as utf8 by default and parsed correctly.'); 445 | 446 | obj = grunt.file.readJSON('test/fixtures/iso-8859-1.json', {encoding: 'iso-8859-1'}); 447 | test.deepEqual(obj, this.object, 'file should be read using the specified encoding.'); 448 | 449 | grunt.file.defaultEncoding = 'iso-8859-1'; 450 | obj = grunt.file.readJSON('test/fixtures/iso-8859-1.json'); 451 | test.deepEqual(obj, this.object, 'changing the default encoding should work.'); 452 | test.done(); 453 | }, 454 | 'readYAML': function(test) { 455 | test.expect(5); 456 | var obj; 457 | obj = grunt.file.readYAML('test/fixtures/utf8.yaml'); 458 | test.deepEqual(obj, this.object, 'file should be safely read as utf8 by default and parsed correctly.'); 459 | 460 | obj = grunt.file.readYAML('test/fixtures/utf8.yaml', null, {unsafeLoad: true}); 461 | test.deepEqual(obj, this.object, 'file should be unsafely read as utf8 by default and parsed correctly.'); 462 | 463 | obj = grunt.file.readYAML('test/fixtures/iso-8859-1.yaml', {encoding: 'iso-8859-1'}); 464 | test.deepEqual(obj, this.object, 'file should be read using the specified encoding.'); 465 | 466 | test.throws(function() { 467 | obj = grunt.file.readYAML('test/fixtures/error.yaml'); 468 | }, function(err) { 469 | return err.message.indexOf('undefined') === -1; 470 | }, 'error thrown should not contain undefined.'); 471 | 472 | grunt.file.defaultEncoding = 'iso-8859-1'; 473 | obj = grunt.file.readYAML('test/fixtures/iso-8859-1.yaml'); 474 | test.deepEqual(obj, this.object, 'changing the default encoding should work.'); 475 | test.done(); 476 | }, 477 | 'write': function(test) { 478 | test.expect(6); 479 | var tmpfile; 480 | tmpfile = new Tempfile(); 481 | grunt.file.write(tmpfile.path, this.string); 482 | test.strictEqual(fs.readFileSync(tmpfile.path, 'utf8'), this.string, 'file should be written as utf8 by default.'); 483 | tmpfile.unlinkSync(); 484 | 485 | tmpfile = new Tempfile(); 486 | grunt.file.write(tmpfile.path, this.string, {encoding: 'iso-8859-1'}); 487 | test.strictEqual(grunt.file.read(tmpfile.path, {encoding: 'iso-8859-1'}), this.string, 'file should be written using the specified encoding.'); 488 | tmpfile.unlinkSync(); 489 | 490 | tmpfile = new Tempfile(); 491 | tmpfile.unlinkSync(); 492 | grunt.file.write(tmpfile.path, this.string, {mode: parseInt('0444', 8)}); 493 | test.strictEqual(fs.statSync(tmpfile.path).mode & parseInt('0222', 8), 0, 'file should be read only.'); 494 | fs.chmodSync(tmpfile.path, parseInt('0666', 8)); 495 | tmpfile.unlinkSync(); 496 | 497 | grunt.file.defaultEncoding = 'iso-8859-1'; 498 | tmpfile = new Tempfile(); 499 | grunt.file.write(tmpfile.path, this.string); 500 | grunt.file.defaultEncoding = 'utf8'; 501 | test.strictEqual(grunt.file.read(tmpfile.path, {encoding: 'iso-8859-1'}), this.string, 'changing the default encoding should work.'); 502 | tmpfile.unlinkSync(); 503 | 504 | tmpfile = new Tempfile(); 505 | var octocat = fs.readFileSync('test/fixtures/octocat.png'); 506 | grunt.file.write(tmpfile.path, octocat); 507 | test.ok(compareBuffers(fs.readFileSync(tmpfile.path), octocat), 'buffers should always be written as-specified, with no attempt at re-encoding.'); 508 | tmpfile.unlinkSync(); 509 | 510 | grunt.option('write', false); 511 | var filepath = path.join(tmpdir.path, 'should-not-exist.txt'); 512 | grunt.file.write(filepath, 'test'); 513 | test.equal(grunt.file.exists(filepath), false, 'file should NOT be created if --no-write was specified.'); 514 | test.done(); 515 | }, 516 | 'copy': function(test) { 517 | test.expect(4); 518 | var tmpfile; 519 | tmpfile = new Tempfile(); 520 | grunt.file.copy('test/fixtures/utf8.txt', tmpfile.path); 521 | test.ok(compareFiles(tmpfile.path, 'test/fixtures/utf8.txt'), 'files should just be copied as encoding-agnostic by default.'); 522 | tmpfile.unlinkSync(); 523 | 524 | tmpfile = new Tempfile(); 525 | grunt.file.copy('test/fixtures/iso-8859-1.txt', tmpfile.path); 526 | test.ok(compareFiles(tmpfile.path, 'test/fixtures/iso-8859-1.txt'), 'files should just be copied as encoding-agnostic by default.'); 527 | tmpfile.unlinkSync(); 528 | 529 | tmpfile = new Tempfile(); 530 | grunt.file.copy('test/fixtures/octocat.png', tmpfile.path); 531 | test.ok(compareFiles(tmpfile.path, 'test/fixtures/octocat.png'), 'files should just be copied as encoding-agnostic by default.'); 532 | tmpfile.unlinkSync(); 533 | 534 | grunt.option('write', false); 535 | var filepath = path.join(tmpdir.path, 'should-not-exist.txt'); 536 | grunt.file.copy('test/fixtures/utf8.txt', filepath); 537 | test.equal(grunt.file.exists(filepath), false, 'file should NOT be created if --no-write was specified.'); 538 | test.done(); 539 | }, 540 | 'copy and process': function(test) { 541 | test.expect(14); 542 | var tmpfile; 543 | tmpfile = new Tempfile(); 544 | grunt.file.copy('test/fixtures/utf8.txt', tmpfile.path, { 545 | process: function(src, srcpath, destpath) { 546 | test.equal(srcpath, 'test/fixtures/utf8.txt', 'srcpath should be passed in, as-specified.'); 547 | test.equal(destpath, tmpfile.path, 'destpath should be passed in, as-specified.'); 548 | test.equal(Buffer.isBuffer(src), false, 'when no encoding is specified, use default encoding and process src as a string'); 549 | test.equal(typeof src, 'string', 'when no encoding is specified, use default encoding and process src as a string'); 550 | return 'føø' + src + 'bår'; 551 | } 552 | }); 553 | test.equal(grunt.file.read(tmpfile.path), 'føø' + this.string + 'bår', 'file should be saved as properly encoded processed string.'); 554 | tmpfile.unlinkSync(); 555 | 556 | tmpfile = new Tempfile(); 557 | grunt.file.copy('test/fixtures/iso-8859-1.txt', tmpfile.path, { 558 | encoding: 'iso-8859-1', 559 | process: function(src) { 560 | test.equal(Buffer.isBuffer(src), false, 'use specified encoding and process src as a string'); 561 | test.equal(typeof src, 'string', 'use specified encoding and process src as a string'); 562 | return 'føø' + src + 'bår'; 563 | } 564 | }); 565 | test.equal(grunt.file.read(tmpfile.path, {encoding: 'iso-8859-1'}), 'føø' + this.string + 'bår', 'file should be saved as properly encoded processed string.'); 566 | tmpfile.unlinkSync(); 567 | 568 | tmpfile = new Tempfile(); 569 | grunt.file.copy('test/fixtures/utf8.txt', tmpfile.path, { 570 | encoding: null, 571 | process: function(src) { 572 | test.ok(Buffer.isBuffer(src), 'when encoding is specified as null, process src as a buffer'); 573 | return Buffer.from('føø' + src.toString() + 'bår'); 574 | } 575 | }); 576 | test.equal(grunt.file.read(tmpfile.path), 'føø' + this.string + 'bår', 'file should be saved as the buffer returned by process.'); 577 | tmpfile.unlinkSync(); 578 | 579 | grunt.file.defaultEncoding = 'iso-8859-1'; 580 | tmpfile = new Tempfile(); 581 | grunt.file.copy('test/fixtures/iso-8859-1.txt', tmpfile.path, { 582 | process: function(src) { 583 | test.equal(Buffer.isBuffer(src), false, 'use non-utf8 default encoding and process src as a string'); 584 | test.equal(typeof src, 'string', 'use non-utf8 default encoding and process src as a string'); 585 | return 'føø' + src + 'bår'; 586 | } 587 | }); 588 | test.equal(grunt.file.read(tmpfile.path), 'føø' + this.string + 'bår', 'file should be saved as properly encoded processed string.'); 589 | tmpfile.unlinkSync(); 590 | 591 | var filepath = path.join(tmpdir.path, 'should-not-exist.txt'); 592 | grunt.file.copy('test/fixtures/iso-8859-1.txt', filepath, { 593 | process: function() { 594 | return false; 595 | } 596 | }); 597 | test.equal(grunt.file.exists(filepath), false, 'file should NOT be created if process returns false.'); 598 | test.done(); 599 | }, 600 | 'copy and process, noprocess': function(test) { 601 | test.expect(4); 602 | var tmpfile; 603 | tmpfile = new Tempfile(); 604 | grunt.file.copy('test/fixtures/utf8.txt', tmpfile.path, { 605 | noProcess: true, 606 | process: function(src) { 607 | return 'føø' + src + 'bår'; 608 | } 609 | }); 610 | test.equal(grunt.file.read(tmpfile.path), this.string, 'file should not have been processed.'); 611 | tmpfile.unlinkSync(); 612 | 613 | ['process', 'noprocess', 'othernoprocess'].forEach(function(filename) { 614 | var filepath = path.join(tmpdir.path, filename); 615 | grunt.file.copy('test/fixtures/utf8.txt', filepath); 616 | var tmpfile = new Tempfile(); 617 | grunt.file.copy(filepath, tmpfile.path, { 618 | noProcess: ['**/*no*'], 619 | process: function(src) { 620 | return 'føø' + src + 'bår'; 621 | } 622 | }); 623 | if (filename === 'process') { 624 | test.equal(grunt.file.read(tmpfile.path), 'føø' + this.string + 'bår', 'file should have been processed.'); 625 | } else { 626 | test.equal(grunt.file.read(tmpfile.path), this.string, 'file should not have been processed.'); 627 | } 628 | tmpfile.unlinkSync(); 629 | }, this); 630 | 631 | test.done(); 632 | }, 633 | 'copy directory recursively': function(test) { 634 | test.expect(34); 635 | var copyroot1 = path.join(tmpdir.path, 'copy-dir-1'); 636 | var copyroot2 = path.join(tmpdir.path, 'copy-dir-2'); 637 | grunt.file.copy('test/fixtures/expand/', copyroot1); 638 | grunt.file.recurse('test/fixtures/expand/', function(srcpath, rootdir, subdir, filename) { 639 | var destpath = path.join(copyroot1, subdir || '', filename); 640 | test.ok(grunt.file.isFile(srcpath), 'file should have been copied.'); 641 | test.equal(grunt.file.read(srcpath), grunt.file.read(destpath), 'file contents should be the same.'); 642 | }); 643 | grunt.file.mkdir(path.join(copyroot1, 'empty')); 644 | grunt.file.mkdir(path.join(copyroot1, 'deep/deeper/empty')); 645 | grunt.file.copy(copyroot1, copyroot2, { 646 | process: function(contents) { 647 | return '<' + contents + '>'; 648 | }, 649 | }); 650 | test.ok(grunt.file.isDir(path.join(copyroot2, 'empty')), 'empty directory should have been created.'); 651 | test.ok(grunt.file.isDir(path.join(copyroot2, 'deep/deeper/empty')), 'empty directory should have been created.'); 652 | grunt.file.recurse('test/fixtures/expand/', function(srcpath, rootdir, subdir, filename) { 653 | var destpath = path.join(copyroot2, subdir || '', filename); 654 | test.ok(grunt.file.isFile(srcpath), 'file should have been copied.'); 655 | test.equal('<' + grunt.file.read(srcpath) + '>', grunt.file.read(destpath), 'file contents should be processed correctly.'); 656 | }); 657 | test.done(); 658 | }, 659 | 'delete': function(test) { 660 | test.expect(2); 661 | var oldBase = process.cwd(); 662 | var cwd = path.resolve(tmpdir.path, 'delete', 'folder'); 663 | grunt.file.mkdir(cwd); 664 | grunt.file.setBase(tmpdir.path); 665 | 666 | grunt.file.write(path.join(cwd, 'test.js'), 'var test;'); 667 | test.ok(grunt.file.delete(cwd), 'should return true after deleting file.'); 668 | test.equal(grunt.file.exists(cwd), false, 'file should have been deleted.'); 669 | grunt.file.setBase(oldBase); 670 | test.done(); 671 | }, 672 | 'delete nonexistent file': function(test) { 673 | test.expect(2); 674 | this.resetWarnCount(); 675 | test.ok(!grunt.file.delete('nonexistent'), 'should return false if file does not exist.'); 676 | test.ok(this.warnCount, 'should issue a warning when deleting non-existent file'); 677 | test.done(); 678 | }, 679 | 'delete outside working directory': function(test) { 680 | test.expect(4); 681 | var oldBase = process.cwd(); 682 | var cwd = path.resolve(tmpdir.path, 'delete', 'folder'); 683 | var outsidecwd = path.resolve(tmpdir.path, 'delete', 'outsidecwd'); 684 | grunt.file.mkdir(cwd); 685 | grunt.file.mkdir(outsidecwd); 686 | grunt.file.setBase(cwd); 687 | 688 | grunt.file.write(path.join(outsidecwd, 'test.js'), 'var test;'); 689 | 690 | this.resetWarnCount(); 691 | test.equal(grunt.file.delete(path.join(outsidecwd, 'test.js')), false, 'should not delete anything outside the cwd.'); 692 | test.ok(this.warnCount, 'should issue a warning when deleting outside working directory'); 693 | 694 | test.ok(grunt.file.delete(path.join(outsidecwd), {force: true}), 'should delete outside cwd when using the --force.'); 695 | test.equal(grunt.file.exists(outsidecwd), false, 'file outside cwd should have been deleted when using the --force.'); 696 | 697 | grunt.file.setBase(oldBase); 698 | test.done(); 699 | }, 700 | 'dont delete current working directory': function(test) { 701 | test.expect(3); 702 | var oldBase = process.cwd(); 703 | var cwd = path.resolve(tmpdir.path, 'dontdelete', 'folder'); 704 | grunt.file.mkdir(cwd); 705 | grunt.file.setBase(cwd); 706 | 707 | this.resetWarnCount(); 708 | test.equal(grunt.file.delete(cwd), false, 'should not delete the cwd.'); 709 | test.ok(this.warnCount, 'should issue a warning when trying to delete cwd'); 710 | 711 | test.ok(grunt.file.exists(cwd), 'the cwd should exist.'); 712 | 713 | grunt.file.setBase(oldBase); 714 | test.done(); 715 | }, 716 | 'dont actually delete with no-write option on': function(test) { 717 | test.expect(2); 718 | grunt.option('write', false); 719 | 720 | var oldBase = process.cwd(); 721 | var cwd = path.resolve(tmpdir.path, 'dontdelete', 'folder'); 722 | grunt.file.mkdir(cwd); 723 | grunt.file.setBase(tmpdir.path); 724 | 725 | grunt.file.write(path.join(cwd, 'test.js'), 'var test;'); 726 | test.ok(grunt.file.delete(cwd), 'should return true after not actually deleting file.'); 727 | test.equal(grunt.file.exists(cwd), true, 'file should NOT be deleted if --no-write was specified.'); 728 | grunt.file.setBase(oldBase); 729 | 730 | test.done(); 731 | }, 732 | 'mkdir': function(test) { 733 | test.expect(5); 734 | test.doesNotThrow(function() { 735 | grunt.file.mkdir(tmpdir.path); 736 | }, 'Should not explode if the directory already exists.'); 737 | test.ok(fs.existsSync(tmpdir.path), 'path should still exist.'); 738 | 739 | test.doesNotThrow(function() { 740 | grunt.file.mkdir(path.join(tmpdir.path, 'aa/bb/cc')); 741 | }, 'Should also not explode, otherwise.'); 742 | test.ok(path.join(tmpdir.path, 'aa/bb/cc'), 'path should have been created.'); 743 | 744 | fs.writeFileSync(path.join(tmpdir.path, 'aa/bb/xx'), 'test'); 745 | test.throws(function() { 746 | grunt.file.mkdir(path.join(tmpdir.path, 'aa/bb/xx/yy')); 747 | }, 'Should throw if a path cannot be created (ENOTDIR).'); 748 | 749 | test.done(); 750 | }, 751 | 'recurse': function(test) { 752 | test.expect(1); 753 | var rootdir = 'test/fixtures/expand'; 754 | var expected = {}; 755 | expected[rootdir + '/css/baz.css'] = [rootdir, 'css', 'baz.css']; 756 | expected[rootdir + '/css/qux.css'] = [rootdir, 'css', 'qux.css']; 757 | expected[rootdir + '/deep/deep.txt'] = [rootdir, 'deep', 'deep.txt']; 758 | expected[rootdir + '/deep/deeper/deeper.txt'] = [rootdir, 'deep/deeper', 'deeper.txt']; 759 | expected[rootdir + '/deep/deeper/deepest/deepest.txt'] = [rootdir, 'deep/deeper/deepest', 'deepest.txt']; 760 | expected[rootdir + '/js/bar.js'] = [rootdir, 'js', 'bar.js']; 761 | expected[rootdir + '/js/foo.js'] = [rootdir, 'js', 'foo.js']; 762 | expected[rootdir + '/README.md'] = [rootdir, undefined, 'README.md']; 763 | 764 | var actual = {}; 765 | grunt.file.recurse(rootdir, function(abspath, rootdir, subdir, filename) { 766 | actual[abspath] = [rootdir, subdir, filename]; 767 | }); 768 | 769 | test.deepEqual(actual, expected, 'paths and arguments should match.'); 770 | test.done(); 771 | }, 772 | 'exists': function(test) { 773 | test.expect(6); 774 | test.ok(grunt.file.exists('test/fixtures/octocat.png'), 'files exist.'); 775 | test.ok(grunt.file.exists('test', 'fixtures', 'octocat.png'), 'should work for paths in parts.'); 776 | test.ok(grunt.file.exists('test/fixtures'), 'directories exist.'); 777 | test.ok(grunt.file.exists(path.join(tmpdir.path, 'octocat.png')), 'file links exist.'); 778 | test.ok(grunt.file.exists(path.join(tmpdir.path, 'expand')), 'directory links exist.'); 779 | test.equal(grunt.file.exists('test/fixtures/does/not/exist'), false, 'nonexistent files do not exist.'); 780 | test.done(); 781 | }, 782 | 'isLink': function(test) { 783 | test.expect(8); 784 | test.equals(grunt.file.isLink('test/fixtures/octocat.png'), false, 'files are not links.'); 785 | test.equals(grunt.file.isLink('test/fixtures'), false, 'directories are not links.'); 786 | test.ok(grunt.file.isLink(path.join(tmpdir.path, 'octocat.png')), 'file links are links.'); 787 | test.ok(grunt.file.isLink(path.join(tmpdir.path, 'expand')), 'directory links are links.'); 788 | grunt.file.mkdir(path.join(tmpdir.path, 'relative-links')); 789 | fs.symlinkSync('test/fixtures/octocat.png', path.join(tmpdir.path, 'relative-links/octocat.png'), 'file'); 790 | fs.symlinkSync('test/fixtures/expand', path.join(tmpdir.path, 'relative-links/expand'), 'file'); 791 | test.ok(grunt.file.isLink(path.join(tmpdir.path, 'relative-links/octocat.png')), 'relative file links are links.'); 792 | test.ok(grunt.file.isLink(path.join(tmpdir.path, 'relative-links/expand')), 'relative directory links are links.'); 793 | test.ok(grunt.file.isLink(tmpdir.path, 'octocat.png'), 'should work for paths in parts.'); 794 | test.equals(grunt.file.isLink('test/fixtures/does/not/exist'), false, 'nonexistent files are not links.'); 795 | test.done(); 796 | }, 797 | 'isDir': function(test) { 798 | test.expect(6); 799 | test.equals(grunt.file.isDir('test/fixtures/octocat.png'), false, 'files are not directories.'); 800 | test.ok(grunt.file.isDir('test/fixtures'), 'directories are directories.'); 801 | test.ok(grunt.file.isDir('test', 'fixtures'), 'should work for paths in parts.'); 802 | test.equals(grunt.file.isDir(path.join(tmpdir.path, 'octocat.png')), false, 'file links are not directories.'); 803 | test.ok(grunt.file.isDir(path.join(tmpdir.path, 'expand')), 'directory links are directories.'); 804 | test.equals(grunt.file.isDir('test/fixtures/does/not/exist'), false, 'nonexistent files are not directories.'); 805 | test.done(); 806 | }, 807 | 'isFile': function(test) { 808 | test.expect(6); 809 | test.ok(grunt.file.isFile('test/fixtures/octocat.png'), 'files are files.'); 810 | test.ok(grunt.file.isFile('test', 'fixtures', 'octocat.png'), 'should work for paths in parts.'); 811 | test.equals(grunt.file.isFile('test/fixtures'), false, 'directories are not files.'); 812 | test.ok(grunt.file.isFile(path.join(tmpdir.path, 'octocat.png')), 'file links are files.'); 813 | test.equals(grunt.file.isFile(path.join(tmpdir.path, 'expand')), false, 'directory links are not files.'); 814 | test.equals(grunt.file.isFile('test/fixtures/does/not/exist'), false, 'nonexistent files are not files.'); 815 | test.done(); 816 | }, 817 | 'isPathAbsolute': function(test) { 818 | test.expect(6); 819 | test.ok(grunt.file.isPathAbsolute(path.resolve('/foo')), 'should return true'); 820 | test.ok(grunt.file.isPathAbsolute(path.resolve('/foo') + path.sep), 'should return true'); 821 | test.equal(grunt.file.isPathAbsolute('foo'), false, 'should return false'); 822 | test.ok(grunt.file.isPathAbsolute(path.resolve('test/fixtures/a.js')), 'should return true'); 823 | test.equal(grunt.file.isPathAbsolute('test/fixtures/a.js'), false, 'should return false'); 824 | if (win32) { 825 | test.equal(grunt.file.isPathAbsolute('C:/Users/'), true, 'should return true'); 826 | } else { 827 | test.equal(grunt.file.isPathAbsolute('/'), true, 'should return true'); 828 | } 829 | test.done(); 830 | }, 831 | 'arePathsEquivalent': function(test) { 832 | test.expect(5); 833 | test.ok(grunt.file.arePathsEquivalent('/foo'), 'should return true'); 834 | test.ok(grunt.file.arePathsEquivalent('/foo', '/foo/', '/foo/../foo/'), 'should return true'); 835 | test.ok(grunt.file.arePathsEquivalent(process.cwd(), '.', './', 'test/..'), 'should return true'); 836 | test.equal(grunt.file.arePathsEquivalent(process.cwd(), '..'), false, 'should return false'); 837 | test.equal(grunt.file.arePathsEquivalent('.', '..'), false, 'should return false'); 838 | test.done(); 839 | }, 840 | 'doesPathContain': function(test) { 841 | test.expect(6); 842 | test.ok(grunt.file.doesPathContain('/foo', '/foo/bar'), 'should return true'); 843 | test.ok(grunt.file.doesPathContain('/foo/', '/foo/bar/baz', '/foo/bar', '/foo/whatever'), 'should return true'); 844 | test.equal(grunt.file.doesPathContain('/foo', '/foo'), false, 'should return false'); 845 | test.equal(grunt.file.doesPathContain('/foo/xyz', '/foo/xyz/123', '/foo/bar/baz'), false, 'should return false'); 846 | test.equal(grunt.file.doesPathContain('/foo/xyz', '/foo'), false, 'should return false'); 847 | test.ok(grunt.file.doesPathContain(process.cwd(), 'test', 'test/fixtures', 'lib'), 'should return true'); 848 | test.done(); 849 | }, 850 | 'isPathCwd': function(test) { 851 | test.expect(8); 852 | test.ok(grunt.file.isPathCwd(process.cwd()), 'cwd is cwd'); 853 | test.ok(grunt.file.isPathCwd('.'), 'cwd is cwd'); 854 | test.equal(grunt.file.isPathCwd('test'), false, 'subdirectory is not cwd'); 855 | test.equal(grunt.file.isPathCwd(path.resolve('test')), false, 'subdirectory is not cwd'); 856 | test.equal(grunt.file.isPathCwd('..'), false, 'parent is not cwd'); 857 | test.equal(grunt.file.isPathCwd(path.resolve('..')), false, 'parent is not cwd'); 858 | test.equal(grunt.file.isPathCwd('/'), false, 'root is not cwd (I hope)'); 859 | test.equal(grunt.file.isPathCwd('nonexistent'), false, 'nonexistent path is not cwd'); 860 | test.done(); 861 | }, 862 | 'isPathInCwd': function(test) { 863 | test.expect(8); 864 | test.equal(grunt.file.isPathInCwd(process.cwd()), false, 'cwd is not IN cwd'); 865 | test.equal(grunt.file.isPathInCwd('.'), false, 'cwd is not IN cwd'); 866 | test.ok(grunt.file.isPathInCwd('test'), 'subdirectory is in cwd'); 867 | test.ok(grunt.file.isPathInCwd(path.resolve('test')), 'subdirectory is in cwd'); 868 | test.equal(grunt.file.isPathInCwd('..'), false, 'parent is not in cwd'); 869 | test.equal(grunt.file.isPathInCwd(path.resolve('..')), false, 'parent is not in cwd'); 870 | test.equal(grunt.file.isPathInCwd('/'), false, 'root is not in cwd (I hope)'); 871 | test.equal(grunt.file.isPathInCwd('nonexistent'), false, 'nonexistent path is not in cwd'); 872 | test.done(); 873 | }, 874 | 'cwdUnderSymlink': { 875 | setUp: function(done) { 876 | this.cwd = process.cwd(); 877 | process.chdir(path.join(tmpdir.path, 'expand')); 878 | done(); 879 | }, 880 | tearDown: function(done) { 881 | process.chdir(this.cwd); 882 | done(); 883 | }, 884 | 'isPathCwd': function(test) { 885 | test.expect(2); 886 | test.ok(grunt.file.isPathCwd(process.cwd()), 'cwd is cwd'); 887 | test.ok(grunt.file.isPathCwd('.'), 'cwd is cwd'); 888 | test.done(); 889 | }, 890 | 'isPathInCwd': function(test) { 891 | test.expect(2); 892 | test.ok(grunt.file.isPathInCwd('deep'), 'subdirectory is in cwd'); 893 | test.ok(grunt.file.isPathInCwd(path.resolve('deep')), 'subdirectory is in cwd'); 894 | test.done(); 895 | }, 896 | 'symbolicLinkCopy': function(test) { 897 | test.expect(4); 898 | var srcfile = new Tempdir(); 899 | fs.symlinkSync(path.resolve('test/fixtures/octocat.png'), path.join(srcfile.path, 'octocat.png'), 'file'); 900 | // test symlink copy for files 901 | var destdir = new Tempdir(); 902 | grunt.file.copy(path.join(srcfile.path, 'octocat.png'), path.join(destdir.path, 'octocat.png')); 903 | test.ok(fs.lstatSync(path.join(srcfile.path, 'octocat.png')).isSymbolicLink()); 904 | test.ok(fs.lstatSync(path.join(destdir.path, 'octocat.png')).isSymbolicLink()); 905 | 906 | // test symlink copy for directories 907 | var srcdir = new Tempdir(); 908 | var destdir = new Tempdir(); 909 | var fixtures = path.resolve('test/fixtures'); 910 | var symlinkSource = path.join(srcdir.path, path.basename(fixtures)); 911 | var destSource = path.join(destdir.path, path.basename(fixtures)); 912 | fs.symlinkSync(fixtures, symlinkSource, 'dir'); 913 | 914 | grunt.file.copy(symlinkSource, destSource); 915 | test.ok(fs.lstatSync(symlinkSource).isSymbolicLink()); 916 | test.ok(fs.lstatSync(path.join(destdir.path, path.basename(fixtures))).isSymbolicLink()); 917 | test.done(); 918 | }, 919 | }, 920 | 'symbolicLinkDestError': function(test) { 921 | test.expect(1); 922 | var tmpfile = new Tempdir(); 923 | fs.symlinkSync(path.resolve('test/fixtures/octocat.png'), path.join(tmpfile.path, 'octocat.png'), 'file'); 924 | grunt.file.copy(path.resolve('test/fixtures/octocat.png'), path.join(tmpfile.path, 'octocat.png')); 925 | test.ok(fs.lstatSync(path.join(tmpfile.path, 'octocat.png')).isSymbolicLink()); 926 | test.done(); 927 | }, 928 | }; 929 | -------------------------------------------------------------------------------- /test/grunt/option_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../../lib/grunt'); 4 | 5 | exports.option = { 6 | setUp: function(done) { 7 | grunt.option.init(); 8 | done(); 9 | }, 10 | tearDown: function(done) { 11 | grunt.option.init(); 12 | done(); 13 | }, 14 | 'option.init': function(test) { 15 | test.expect(1); 16 | var expected = {foo: 'bar', bool: true, 'bar': {foo: 'bar'}}; 17 | test.deepEqual(grunt.option.init(expected), expected); 18 | test.done(); 19 | }, 20 | 'option': function(test) { 21 | test.expect(4); 22 | test.equal(grunt.option('foo', 'bar'), grunt.option('foo')); 23 | grunt.option('foo', {foo: 'bar'}); 24 | test.deepEqual(grunt.option('foo'), {foo: 'bar'}); 25 | test.equal(grunt.option('no-there'), false); 26 | grunt.option('there', false); 27 | test.equal(grunt.option('no-there'), true); 28 | test.done(); 29 | }, 30 | 'option.flags': function(test) { 31 | test.expect(1); 32 | grunt.option.init({ 33 | foo: 'bar', 34 | there: true, 35 | obj: {foo: 'bar'}, 36 | arr: [] 37 | }); 38 | test.deepEqual(grunt.option.flags(), ['--foo=bar', '--there', '--obj=[object Object]']); 39 | test.done(); 40 | }, 41 | 'option.keys': function(test) { 42 | test.expect(1); 43 | grunt.option.init({ 44 | foo: 'bar', 45 | there: true, 46 | obj: {foo: 'bar'}, 47 | arr: [] 48 | }); 49 | test.deepEqual(grunt.option.keys(), ['foo', 'there', 'obj', 'arr']); 50 | test.done(); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /test/grunt/task_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../../lib/grunt'); 4 | 5 | exports['task.normalizeMultiTaskFiles'] = { 6 | setUp: function(done) { 7 | this.cwd = process.cwd(); 8 | process.chdir('test/fixtures/files'); 9 | done(); 10 | }, 11 | tearDown: function(done) { 12 | process.chdir(this.cwd); 13 | done(); 14 | }, 15 | 'normalize': function(test) { 16 | test.expect(7); 17 | var actual, expected, key, value; 18 | var flatten = grunt.util._.flatten; 19 | 20 | key = 'dist/built.js'; 21 | value = 'src/*1.js'; 22 | actual = grunt.task.normalizeMultiTaskFiles(value, key); 23 | expected = [ 24 | { 25 | dest: 'dist/built.js', 26 | src: ['src/file1.js'], 27 | orig: {dest: key, src: [value]}, 28 | }, 29 | ]; 30 | test.deepEqual(actual, expected, 'should normalize destTarget: srcString.'); 31 | 32 | key = 'dist/built.js'; 33 | value = [['src/*1.js'], ['src/*2.js']]; 34 | actual = grunt.task.normalizeMultiTaskFiles(value, key); 35 | expected = [ 36 | { 37 | dest: 'dist/built.js', 38 | src: ['src/file1.js', 'src/file2.js'], 39 | orig: {dest: key, src: flatten(value)}, 40 | }, 41 | ]; 42 | test.deepEqual(actual, expected, 'should normalize destTarget: srcArray.'); 43 | 44 | value = { 45 | src: ['src/*1.js', 'src/*2.js'], 46 | dest: 'dist/built.js' 47 | }; 48 | actual = grunt.task.normalizeMultiTaskFiles(value, 'ignored'); 49 | expected = [ 50 | { 51 | dest: 'dist/built.js', 52 | src: ['src/file1.js', 'src/file2.js'], 53 | orig: value, 54 | }, 55 | ]; 56 | test.deepEqual(actual, expected, 'should normalize target: {src: srcStuff, dest: destStuff}.'); 57 | 58 | value = { 59 | files: { 60 | 'dist/built-a.js': 'src/*1.js', 61 | 'dist/built-b.js': ['src/*1.js', [['src/*2.js']]] 62 | } 63 | }; 64 | actual = grunt.task.normalizeMultiTaskFiles(value, 'ignored'); 65 | expected = [ 66 | { 67 | dest: 'dist/built-a.js', 68 | src: ['src/file1.js'], 69 | orig: {dest: 'dist/built-a.js', src: [value.files['dist/built-a.js']]}, 70 | }, 71 | { 72 | dest: 'dist/built-b.js', 73 | src: ['src/file1.js', 'src/file2.js'], 74 | orig: {dest: 'dist/built-b.js', src: flatten(value.files['dist/built-b.js'])}, 75 | }, 76 | ]; 77 | test.deepEqual(actual, expected, 'should normalize target: {files: {destTarget: srcStuff, ...}}.'); 78 | 79 | value = { 80 | files: [ 81 | {'dist/built-a.js': 'src/*.whoops'}, 82 | {'dist/built-b.js': [[['src/*1.js'], 'src/*2.js']]} 83 | ] 84 | }; 85 | actual = grunt.task.normalizeMultiTaskFiles(value, 'ignored'); 86 | expected = [ 87 | { 88 | dest: 'dist/built-a.js', 89 | src: [], 90 | orig: {dest: Object.keys(value.files[0])[0], src: [value.files[0]['dist/built-a.js']]}, 91 | }, 92 | { 93 | dest: 'dist/built-b.js', 94 | src: ['src/file1.js', 'src/file2.js'], 95 | orig: {dest: Object.keys(value.files[1])[0], src: flatten(value.files[1]['dist/built-b.js'])}, 96 | }, 97 | ]; 98 | test.deepEqual(actual, expected, 'should normalize target: {files: [{destTarget: srcStuff}, ...]}.'); 99 | 100 | value = { 101 | files: [ 102 | {dest: 'dist/built-a.js', src: ['src/*2.js']}, 103 | {dest: 'dist/built-b.js', src: ['src/*1.js', 'src/*2.js']} 104 | ] 105 | }; 106 | actual = grunt.task.normalizeMultiTaskFiles(value, 'ignored'); 107 | expected = [ 108 | { 109 | dest: 'dist/built-a.js', 110 | src: ['src/file2.js'], 111 | orig: value.files[0], 112 | }, 113 | { 114 | dest: 'dist/built-b.js', 115 | src: ['src/file1.js', 'src/file2.js'], 116 | orig: value.files[1], 117 | }, 118 | ]; 119 | test.deepEqual(actual, expected, 'should normalize target: {files: [{src: srcStuff, dest: destStuff}, ...]}.'); 120 | 121 | value = { 122 | files: [ 123 | {dest: 'dist/built-a.js', src: ['src/*2.js'], foo: 123, bar: true}, 124 | {dest: 'dist/built-b.js', src: ['src/*1.js', 'src/*2.js'], foo: 456, bar: null} 125 | ] 126 | }; 127 | actual = grunt.task.normalizeMultiTaskFiles(value, 'ignored'); 128 | expected = [ 129 | { 130 | dest: 'dist/built-a.js', 131 | src: ['src/file2.js'], 132 | foo: 123, 133 | bar: true, 134 | orig: value.files[0], 135 | }, 136 | { 137 | dest: 'dist/built-b.js', 138 | src: ['src/file1.js', 'src/file2.js'], 139 | foo: 456, 140 | bar: null, 141 | orig: value.files[1], 142 | }, 143 | ]; 144 | test.deepEqual(actual, expected, 'should propagate extra properties.'); 145 | 146 | test.done(); 147 | }, 148 | 'nonull': function(test) { 149 | test.expect(2); 150 | var actual, expected, value; 151 | 152 | value = { 153 | src: ['src/xxx*.js', 'src/yyy*.js'], 154 | dest: 'dist/built.js', 155 | }; 156 | actual = grunt.task.normalizeMultiTaskFiles(value, 'ignored'); 157 | expected = [ 158 | { 159 | dest: value.dest, 160 | src: [], 161 | orig: value, 162 | }, 163 | ]; 164 | test.deepEqual(actual, expected, 'if nonull is not set, should not include non-matching patterns.'); 165 | 166 | value = { 167 | src: ['src/xxx*.js', 'src/yyy*.js'], 168 | dest: 'dist/built.js', 169 | nonull: true, 170 | }; 171 | actual = grunt.task.normalizeMultiTaskFiles(value, 'ignored'); 172 | expected = [ 173 | { 174 | dest: value.dest, 175 | src: value.src, 176 | nonull: true, 177 | orig: value, 178 | }, 179 | ]; 180 | test.deepEqual(actual, expected, 'if nonull is set, should include non-matching patterns.'); 181 | test.done(); 182 | }, 183 | 'expandMapping': function(test) { 184 | test.expect(3); 185 | var actual, expected, value; 186 | 187 | value = { 188 | files: [ 189 | {dest: 'dist/', src: ['src/file?.js'], expand: true}, 190 | {dest: 'dist/', src: ['file?.js'], expand: true, cwd: 'src'}, 191 | ] 192 | }; 193 | actual = grunt.task.normalizeMultiTaskFiles(value, 'ignored'); 194 | expected = [ 195 | { 196 | dest: 'dist/src/file1.js', src: ['src/file1.js'], 197 | orig: value.files[0], 198 | }, 199 | { 200 | dest: 'dist/src/file2.js', src: ['src/file2.js'], 201 | orig: value.files[0], 202 | }, 203 | { 204 | dest: 'dist/file1.js', src: ['src/file1.js'], 205 | orig: value.files[1], 206 | }, 207 | { 208 | dest: 'dist/file2.js', src: ['src/file2.js'], 209 | orig: value.files[1], 210 | }, 211 | ]; 212 | test.deepEqual(actual, expected, 'expand to file mapping, removing cwd from destination paths.'); 213 | 214 | value = { 215 | files: [ 216 | {dest: 'dist/', src: ['src/file?.js'], expand: true, flatten: true}, 217 | ] 218 | }; 219 | actual = grunt.task.normalizeMultiTaskFiles(value, 'ignored'); 220 | expected = [ 221 | { 222 | dest: 'dist/file1.js', src: ['src/file1.js'], 223 | orig: value.files[0], 224 | }, 225 | { 226 | dest: 'dist/file2.js', src: ['src/file2.js'], 227 | orig: value.files[0], 228 | }, 229 | ]; 230 | test.deepEqual(actual, expected, 'expand to file mapping, flattening destination paths.'); 231 | 232 | value = { 233 | files: [ 234 | { 235 | dest: 'dist/', 236 | src: ['src/file?.js'], 237 | expand: true, 238 | rename: function(destBase, destPath) { 239 | return destBase + 'min/' + destPath.replace(/(\.js)$/, '.min$1'); 240 | }, 241 | }, 242 | ] 243 | }; 244 | actual = grunt.task.normalizeMultiTaskFiles(value, 'ignored'); 245 | expected = [ 246 | { 247 | dest: 'dist/min/src/file1.min.js', src: ['src/file1.js'], 248 | orig: value.files[0], 249 | }, 250 | { 251 | dest: 'dist/min/src/file2.min.js', src: ['src/file2.js'], 252 | orig: value.files[0], 253 | }, 254 | ]; 255 | test.deepEqual(actual, expected, 'expand to file mapping, renaming files.'); 256 | 257 | test.done(); 258 | }, 259 | }; 260 | -------------------------------------------------------------------------------- /test/grunt/template_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var grunt = require('../../lib/grunt'); 4 | 5 | exports.template = { 6 | 'process': function(test) { 7 | test.expect(4); 8 | var obj = { 9 | foo: 'c', 10 | bar: 'b<%= foo %>d', 11 | baz: 'a<%= bar %>e' 12 | }; 13 | 14 | test.equal(grunt.template.process('<%= foo %>', {data: obj}), 'c', 'should retrieve value.'); 15 | test.equal(grunt.template.process('<%= bar %>', {data: obj}), 'bcd', 'should recurse.'); 16 | test.equal(grunt.template.process('<%= baz %>', {data: obj}), 'abcde', 'should recurse.'); 17 | 18 | obj.foo = '<% oops %'; 19 | test.equal(grunt.template.process('<%= baz %>', {data: obj}), 'ab<% oops %de', 'should not explode.'); 20 | test.done(); 21 | }, 22 | 23 | 'custom delimiters': function(test) { 24 | test.expect(6); 25 | var obj = { 26 | foo: 'c', 27 | bar: 'b{%= foo %}d', 28 | baz: 'a{%= bar %}e' 29 | }; 30 | 31 | test.equal(grunt.template.process('{%= foo %}', {data: obj, delimiters: 'custom'}), '{%= foo %}', 'custom delimiters have yet to be defined.'); 32 | 33 | // Define custom delimiters. 34 | grunt.template.addDelimiters('custom', '{%', '%}'); 35 | 36 | test.equal(grunt.template.process('{%= foo %}', {data: obj, delimiters: 'custom'}), 'c', 'should retrieve value.'); 37 | test.equal(grunt.template.process('{%= bar %}', {data: obj, delimiters: 'custom'}), 'bcd', 'should recurse.'); 38 | test.equal(grunt.template.process('{%= baz %}', {data: obj, delimiters: 'custom'}), 'abcde', 'should recurse.'); 39 | 40 | test.equal(grunt.template.process('{%= foo %}<%= foo %>', {data: obj, delimiters: 'custom'}), 'c<%= foo %>', 'should ignore default delimiters'); 41 | 42 | obj.foo = '{% oops %'; 43 | test.equal(grunt.template.process('{%= baz %}', {data: obj, delimiters: 'custom'}), 'ab{% oops %de', 'should not explode.'); 44 | 45 | test.done(); 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /test/gruntfile/load-npm-tasks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Log = require('grunt-legacy-log').Log; 4 | var assert = require('assert'); 5 | var through = require('through2'); 6 | 7 | function test(grunt, fixture) { 8 | grunt.file.setBase('../fixtures/' + fixture); 9 | 10 | // Create a custom log to assert output 11 | var stdout = []; 12 | var oldlog = grunt.log; 13 | var stream = through(function(data, enc, next) { 14 | stdout.push(data.toString()); 15 | next(null, data); 16 | }); 17 | stream.pipe(process.stdout); 18 | var log = new Log({ 19 | grunt: grunt, 20 | outStream: stream, 21 | }); 22 | grunt.log = log; 23 | 24 | // Load a npm task 25 | grunt.loadNpmTasks('grunt-foo-plugin'); 26 | 27 | // Run them 28 | grunt.registerTask('default', ['foo', 'done']); 29 | 30 | // Assert they loaded and ran correctly 31 | grunt.registerTask('done', function() { 32 | grunt.log = oldlog; 33 | stdout = stdout.join('\n'); 34 | try { 35 | assert.ok(stdout.indexOf('foo has ran.') !== -1, 'oh-four task should have ran.'); 36 | } catch (err) { 37 | grunt.log.subhead(err.message); 38 | grunt.log.error('Expected ' + err.expected + ' but actually: ' + err.actual); 39 | throw err; 40 | } 41 | }); 42 | } 43 | 44 | module.exports = function(grunt) { 45 | // NPM task package is inside $CWD/node_modules 46 | test(grunt, 'load-npm-tasks'); 47 | // NPM task package hoisted to $CWD/../node_modules 48 | test(grunt, 'load-npm-tasks/test-package'); 49 | }; 50 | -------------------------------------------------------------------------------- /test/gruntfile/multi-task-files.js: -------------------------------------------------------------------------------- 1 | // For now, run this "test suite" with: 2 | // grunt --gruntfile ./test/gruntfile/multi-task-files.js 3 | 4 | 'use strict'; 5 | 6 | module.exports = function(grunt) { 7 | grunt.file.setBase('../fixtures/files'); 8 | 9 | grunt.initConfig({ 10 | build: '123', 11 | mappings: { 12 | cwd: 'src/', 13 | dest: 'foo/', 14 | ext: '.bar', 15 | rename: function(destBase, destPath) { 16 | return destBase + 'baz/' + destPath.replace(/\.js$/, '<%= mappings.ext %>'); 17 | }, 18 | }, 19 | run: { 20 | options: {a: 1, b: 11}, 21 | // This is the "compact" format, where the target name is actually the 22 | // dest filename. Doesn't support per-target options, templated dest, or 23 | // >1 srcs-dest grouping. 24 | 'dist/built.js': 'src/*1.js', 25 | 'dist/built1.js': ['src/*1.js', 'src/*2.js'], 26 | // This is the "medium" format. The target name is arbitrary and can be 27 | // used like "grunt run:built". Supports per-target options, templated 28 | // dest, and arbitrary "extra" parameters. Doesn't support >1 srcs-dest 29 | // grouping. 30 | built: { 31 | options: {a: 2, c: 22}, 32 | src: ['src/*1.js', 'src/*2.js'], 33 | dest: 'dist/built-<%= build %>.js', 34 | extra: 123, 35 | }, 36 | // This is the "full" format. The target name is arbitrary and can be 37 | // used like "grunt run:long1". Supports per-target options, templated 38 | // dest and >1 srcs-dest grouping. 39 | long1: { 40 | options: {a: 3, c: 33}, 41 | files: { 42 | 'dist/built-<%= build %>-a.js': ['src/*1.js'], 43 | 'dist/built-<%= build %>-b.js': ['src/*1.js', 'src/*2.js'], 44 | } 45 | }, 46 | long2: { 47 | options: {a: 4, c: 44}, 48 | files: [ 49 | {'dist/built-<%= build %>-a.js': ['src/*.whoops']}, 50 | {'dist/built-<%= build %>-b.js': ['src/*1.js', 'src/*2.js']}, 51 | ] 52 | }, 53 | // This "full" variant supports per srcs-dest arbitrary "extra" parameters. 54 | long3: { 55 | options: {a: 5, c: 55}, 56 | files: [ 57 | {dest: 'dist/built-<%= build %>-a.js', src: ['src/*2.js'], extra: 456}, 58 | {dest: 'dist/built-<%= build %>-b.js', src: ['src/*1.js', 'src/*2.js'], extra: 789}, 59 | ] 60 | }, 61 | // File mapping options can be specified in these 2 formats. 62 | builtMapping: { 63 | options: {a: 6, c: 66}, 64 | expand: true, 65 | cwd: '<%= mappings.cwd %>', 66 | src: ['*1.js', '*2.js'], 67 | dest: '<%= mappings.dest %>', 68 | rename: '<%= mappings.rename %>', 69 | extra: 123 70 | }, 71 | long3Mapping: { 72 | options: {a: 7, c: 77}, 73 | files: [ 74 | { 75 | expand: true, 76 | cwd: '<%= mappings.cwd %>', 77 | src: ['*1.js', '*2.js'], 78 | dest: '<%= mappings.dest %>', 79 | rename: '<%= mappings.rename %>', 80 | extra: 123 81 | } 82 | ] 83 | }, 84 | long4Mapping: { 85 | options: {a: 8, c: 88}, 86 | files: [ 87 | '<%= run.long3Mapping.files %>' 88 | ] 89 | }, 90 | long5Mapping: { 91 | options: {a: 9, c: 99}, 92 | files: [ 93 | '<%= run.long3Mapping.files %>', 94 | '<%= run.long4Mapping.files %>' 95 | ] 96 | }, 97 | // Need to ensure the task function is run if no files or options were 98 | // specified! 99 | noFilesOrOptions: {}, 100 | }, 101 | }); 102 | 103 | var results = {}; 104 | 105 | var counters = []; 106 | var counter = -1; 107 | grunt.registerMultiTask('run', 'Store stuff for later testing.', function() { 108 | var key = this.nameArgs; 109 | results[key] = { 110 | options: this.options({d: 9}), 111 | files: this.files, 112 | }; 113 | // Test asynchronous-ness. 114 | var done; 115 | if (counter++ % 2 === 0) { 116 | done = this.async(); 117 | setTimeout(function() { 118 | counters.push(counter); 119 | done(); 120 | }, 10); 121 | } else { 122 | counters.push(counter); 123 | } 124 | }); 125 | 126 | var expecteds = { 127 | 'run:noFilesOrOptions': { 128 | options: {a: 1, b: 11, d: 9}, 129 | files: [], 130 | }, 131 | 'run:dist/built.js': { 132 | options: {a: 1, b: 11, d: 9}, 133 | files: [ 134 | { 135 | dest: 'dist/built.js', 136 | src: ['src/file1.js'], 137 | orig: { 138 | dest: 'dist/built.js', 139 | src: ['src/*1.js'], 140 | }, 141 | }, 142 | ] 143 | }, 144 | 'run:dist/built1.js': { 145 | options: {a: 1, b: 11, d: 9}, 146 | files: [ 147 | { 148 | dest: 'dist/built1.js', 149 | src: ['src/file1.js', 'src/file2.js'], 150 | orig: { 151 | dest: 'dist/built1.js', 152 | src: ['src/*1.js', 'src/*2.js'], 153 | }, 154 | }, 155 | ] 156 | }, 157 | 'run:built': { 158 | options: {a: 2, b: 11, c: 22, d: 9}, 159 | files: [ 160 | { 161 | dest: 'dist/built-123.js', 162 | src: ['src/file1.js', 'src/file2.js'], 163 | extra: 123, 164 | orig: { 165 | dest: 'dist/built-123.js', 166 | src: ['src/*1.js', 'src/*2.js'], 167 | extra: 123, 168 | }, 169 | }, 170 | ], 171 | }, 172 | 'run:long1': { 173 | options: {a: 3, b: 11, c: 33, d: 9}, 174 | files: [ 175 | { 176 | dest: 'dist/built-123-a.js', 177 | src: ['src/file1.js'], 178 | orig: { 179 | dest: 'dist/built-123-a.js', 180 | src: ['src/*1.js'], 181 | }, 182 | }, 183 | { 184 | dest: 'dist/built-123-b.js', 185 | src: ['src/file1.js', 'src/file2.js'], 186 | orig: { 187 | dest: 'dist/built-123-b.js', 188 | src: ['src/*1.js', 'src/*2.js'], 189 | }, 190 | }, 191 | ], 192 | }, 193 | 'run:long2': { 194 | options: {a: 4, b: 11, c: 44, d: 9}, 195 | files: [ 196 | { 197 | dest: 'dist/built-123-a.js', 198 | src: [], 199 | orig: { 200 | dest: 'dist/built-123-a.js', 201 | src: ['src/*.whoops'], 202 | }, 203 | }, 204 | { 205 | dest: 'dist/built-123-b.js', 206 | src: ['src/file1.js', 'src/file2.js'], 207 | orig: { 208 | dest: 'dist/built-123-b.js', 209 | src: ['src/*1.js', 'src/*2.js'], 210 | }, 211 | }, 212 | ], 213 | }, 214 | 'run:long3': { 215 | options: {a: 5, b: 11, c: 55, d: 9}, 216 | files: [ 217 | { 218 | dest: 'dist/built-123-a.js', 219 | src: ['src/file2.js'], 220 | extra: 456, 221 | orig: { 222 | dest: 'dist/built-123-a.js', 223 | src: ['src/*2.js'], 224 | extra: 456, 225 | }, 226 | }, 227 | { 228 | dest: 'dist/built-123-b.js', 229 | src: ['src/file1.js', 'src/file2.js'], 230 | extra: 789, 231 | orig: { 232 | src: ['src/*1.js', 'src/*2.js'], 233 | dest: 'dist/built-123-b.js', 234 | extra: 789, 235 | }, 236 | }, 237 | ], 238 | }, 239 | 'run:builtMapping': { 240 | options: {a: 6, b: 11, c: 66, d: 9}, 241 | files: [ 242 | { 243 | dest: 'foo/baz/file1.bar', 244 | src: ['src/file1.js'], 245 | extra: 123, 246 | orig: { 247 | expand: true, 248 | cwd: grunt.config.get('mappings.cwd'), 249 | src: ['*1.js', '*2.js'], 250 | dest: grunt.config.get('mappings.dest'), 251 | rename: grunt.config.get('run.builtMapping.rename'), 252 | extra: 123, 253 | }, 254 | }, 255 | { 256 | dest: 'foo/baz/file2.bar', 257 | src: ['src/file2.js'], 258 | extra: 123, 259 | orig: { 260 | expand: true, 261 | cwd: grunt.config.get('run.builtMapping.cwd'), 262 | src: ['*1.js', '*2.js'], 263 | dest: grunt.config.get('run.builtMapping.dest'), 264 | rename: grunt.config.get('run.builtMapping.rename'), 265 | extra: 123, 266 | }, 267 | }, 268 | ], 269 | }, 270 | 'run:long3Mapping': { 271 | options: {a: 7, b: 11, c: 77, d: 9}, 272 | files: [ 273 | { 274 | dest: 'foo/baz/file1.bar', 275 | src: ['src/file1.js'], 276 | extra: 123, 277 | orig: { 278 | expand: true, 279 | cwd: grunt.config.get('mappings.cwd'), 280 | src: ['*1.js', '*2.js'], 281 | dest: grunt.config.get('mappings.dest'), 282 | rename: grunt.config.get('mappings.rename'), 283 | extra: 123, 284 | }, 285 | }, 286 | { 287 | dest: 'foo/baz/file2.bar', 288 | src: ['src/file2.js'], 289 | extra: 123, 290 | orig: { 291 | expand: true, 292 | cwd: grunt.config.get('mappings.cwd'), 293 | src: ['*1.js', '*2.js'], 294 | dest: grunt.config.get('mappings.dest'), 295 | rename: grunt.config.get('run.builtMapping.rename'), 296 | extra: 123, 297 | }, 298 | }, 299 | ], 300 | }, 301 | 'run:long4Mapping': { 302 | options: {a: 8, b: 11, c: 88, d: 9}, 303 | files: [ 304 | { 305 | dest: 'foo/baz/file1.bar', 306 | src: ['src/file1.js'], 307 | extra: 123, 308 | orig: { 309 | expand: true, 310 | cwd: grunt.config.get('mappings.cwd'), 311 | src: ['*1.js', '*2.js'], 312 | dest: grunt.config.get('mappings.dest'), 313 | rename: grunt.config.get('mappings.rename'), 314 | extra: 123, 315 | }, 316 | }, 317 | { 318 | dest: 'foo/baz/file2.bar', 319 | src: ['src/file2.js'], 320 | extra: 123, 321 | orig: { 322 | expand: true, 323 | cwd: grunt.config.get('mappings.cwd'), 324 | src: ['*1.js', '*2.js'], 325 | dest: grunt.config.get('mappings.dest'), 326 | rename: grunt.config.get('run.builtMapping.rename'), 327 | extra: 123, 328 | }, 329 | }, 330 | ], 331 | }, 332 | 'run:long5Mapping': { 333 | options: {a: 9, b: 11, c: 99, d: 9}, 334 | files: [ 335 | { 336 | dest: 'foo/baz/file1.bar', 337 | src: ['src/file1.js'], 338 | extra: 123, 339 | orig: { 340 | expand: true, 341 | cwd: grunt.config.get('mappings.cwd'), 342 | src: ['*1.js', '*2.js'], 343 | dest: grunt.config.get('mappings.dest'), 344 | rename: grunt.config.get('mappings.rename'), 345 | extra: 123, 346 | }, 347 | }, 348 | { 349 | dest: 'foo/baz/file2.bar', 350 | src: ['src/file2.js'], 351 | extra: 123, 352 | orig: { 353 | expand: true, 354 | cwd: grunt.config.get('mappings.cwd'), 355 | src: ['*1.js', '*2.js'], 356 | dest: grunt.config.get('mappings.dest'), 357 | rename: grunt.config.get('run.builtMapping.rename'), 358 | extra: 123, 359 | }, 360 | }, 361 | { 362 | dest: 'foo/baz/file1.bar', 363 | src: ['src/file1.js'], 364 | extra: 123, 365 | orig: { 366 | expand: true, 367 | cwd: grunt.config.get('mappings.cwd'), 368 | src: ['*1.js', '*2.js'], 369 | dest: grunt.config.get('mappings.dest'), 370 | rename: grunt.config.get('mappings.rename'), 371 | extra: 123, 372 | }, 373 | }, 374 | { 375 | dest: 'foo/baz/file2.bar', 376 | src: ['src/file2.js'], 377 | extra: 123, 378 | orig: { 379 | expand: true, 380 | cwd: grunt.config.get('mappings.cwd'), 381 | src: ['*1.js', '*2.js'], 382 | dest: grunt.config.get('mappings.dest'), 383 | rename: grunt.config.get('run.builtMapping.rename'), 384 | extra: 123, 385 | }, 386 | }, 387 | ], 388 | }, 389 | }; 390 | 391 | var assert = require('assert'); 392 | var difflet = require('difflet')({indent: 2, comment: true}); 393 | var test = function(name, fn) { 394 | try { 395 | fn(); 396 | } catch (err) { 397 | grunt.log.subhead('Assertion Failure in ' + name); 398 | console.log(difflet.compare(err.expected, err.actual)); 399 | throw new Error(err.message); 400 | } 401 | }; 402 | 403 | grunt.registerTask('test', 'Test file and option objects.', function() { 404 | var key = 'run:' + this.nameArgs.replace(/^.*?:/, ''); 405 | var all = key === 'run:all'; 406 | var actual = all ? results : results[key]; 407 | var expected = all ? expecteds : expecteds[key]; 408 | 409 | test(this.name, function() { 410 | assert.deepEqual(actual, expected, 'Actual should match expected.'); 411 | }); 412 | 413 | if (all) { 414 | results = {}; 415 | } else { 416 | delete results[key]; 417 | } 418 | }); 419 | 420 | grunt.registerTask('test:counters', 'Test function execution order.', function() { 421 | test(this.name, function() { 422 | assert.equal(counters.length, counter + 1, 'Task functions should have run the correct number of times.'); 423 | var expected = []; 424 | for (var i = 0; i < counters.length; i++) { expected.push(i); } 425 | assert.deepEqual(counters, expected, 'Task functions should have actually executed in-order.'); 426 | }); 427 | }); 428 | 429 | grunt.registerTask('default', [ 430 | 'run:noFilesOrOptions', 431 | 'test:noFilesOrOptions', 432 | 'run:dist/built.js', 433 | 'test:dist/built.js', 434 | 'run:dist/built1.js', 435 | 'test:dist/built1.js', 436 | 'run:built', 437 | 'test:built', 438 | 'run:long1', 439 | 'test:long1', 440 | 'run:long2', 441 | 'test:long2', 442 | 'run:long3', 443 | 'test:long3', 444 | 'run:builtMapping', 445 | 'test:builtMapping', 446 | 'run:long3Mapping', 447 | 'test:long3Mapping', 448 | 'run:long4Mapping', 449 | 'test:long4Mapping', 450 | 'run:long5Mapping', 451 | 'test:long5Mapping', 452 | 'run', 453 | 'test:all', 454 | 'test:counters', 455 | ]); 456 | 457 | }; 458 | -------------------------------------------------------------------------------- /test/util/task_test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Test helpers. 4 | function delay(fn) { setTimeout(fn, 10); } 5 | 6 | var result = (function() { 7 | var arr; 8 | var push = function() { [].push.apply(arr, arguments); }; 9 | return { 10 | reset: function() { arr = []; }, 11 | push: push, 12 | pushTaskname: function() { push(this.name); }, 13 | get: function() { return arr; }, 14 | getJoined: function() { return arr.join(''); } 15 | }; 16 | }()); 17 | 18 | var requireTask = require.bind(exports, '../../lib/util/task.js'); 19 | 20 | exports['new Task'] = { 21 | 'create': function(test) { 22 | test.expect(1); 23 | var tasklib = requireTask(); 24 | test.ok(tasklib.create() instanceof tasklib.Task, 'It should return a Task instance.'); 25 | test.done(); 26 | } 27 | }; 28 | 29 | exports.Tasks = { 30 | setUp: function(done) { 31 | result.reset(); 32 | this.task = requireTask().create(); 33 | var task = this.task; 34 | task.registerTask('nothing', 'Do nothing.', function() {}); 35 | done(); 36 | }, 37 | 'Task#registerTask': function(test) { 38 | test.expect(1); 39 | var task = this.task; 40 | test.ok('nothing' in task._tasks, 'It should register the passed task.'); 41 | test.done(); 42 | }, 43 | 'Task#registerTask (alias)': function(test) { 44 | test.expect(1); 45 | var task = this.task; 46 | task.registerTask('a', 'Push task name onto result.', result.pushTaskname); 47 | task.registerTask('b', 'Push task name onto result.', result.pushTaskname); 48 | task.registerTask('c d', 'Push task name onto result.', result.pushTaskname); 49 | task.registerTask('y', ['a', 'b', 'c d']); 50 | task.registerTask('z', ['a', 'b', 'nonexistent', 'c d']); 51 | task.options({ 52 | error: function() { 53 | result.push('!' + this.name); 54 | }, 55 | done: function() { 56 | test.strictEqual(result.getJoined(), 'abc d!z', 'The specified tasks should have run, in-order.'); 57 | test.done(); 58 | } 59 | }); 60 | task.run('y', 'z').start(); 61 | }, 62 | 'Task#isTaskAlias': function(test) { 63 | test.expect(2); 64 | var task = this.task; 65 | task.registerTask('a', 'nothing', function() {}); 66 | task.registerTask('b', ['a']); 67 | test.strictEqual(task.isTaskAlias('a'), false, 'It should not be an alias.'); 68 | test.strictEqual(task.isTaskAlias('b'), true, 'It should be an alias.'); 69 | test.done(); 70 | }, 71 | 'Task#renameTask': function(test) { 72 | test.expect(4); 73 | var task = this.task; 74 | task.renameTask('nothing', 'newnothing'); 75 | test.ok('newnothing' in task._tasks, 'It should rename the specified task.'); 76 | test.equal('nothing' in task._tasks, false, 'It should remove the previous task.'); 77 | test.doesNotThrow(function() { task.run('newnothing'); }, 'It should be accessible by its new name.'); 78 | test.throws(function() { task.run('nothing'); }, 'It should not be accessible by its previous name and throw an exception.'); 79 | test.done(); 80 | }, 81 | 'Task#run (exception handling)': function(test) { 82 | test.expect(4); 83 | var task = this.task; 84 | test.doesNotThrow(function() { task.run('nothing'); }, 'Registered tasks should be runnable.'); 85 | test.throws(function() { task.run('nonexistent'); }, 'Attempting to run unregistered tasks should throw an exception.'); 86 | task.options({ 87 | error: result.pushTaskname 88 | }); 89 | test.doesNotThrow(function() { task.run('nonexistent'); }, 'It should not throw an exception because an error handler is defined.'); 90 | test.deepEqual(result.get(), [null], 'Non-nested tasks have a null name.'); 91 | test.done(); 92 | }, 93 | 'Task#run (async failing)': function(test) { 94 | test.expect(1); 95 | var task = this.task; 96 | var results = []; 97 | 98 | task.registerTask('sync1', 'sync, gonna succeed', function() {}); 99 | 100 | task.registerTask('sync2', 'sync, gonna fail', function() { 101 | return false; 102 | }); 103 | 104 | task.registerTask('sync3', 'sync, gonna fail', function() { 105 | return new Error('sync3: Error'); 106 | }); 107 | 108 | task.registerTask('sync4', 'sync, gonna fail', function() { 109 | return new TypeError('sync4: TypeError'); 110 | }); 111 | 112 | task.registerTask('sync5', 'sync, gonna fail', function() { 113 | throw new Error('sync5: Error'); 114 | }); 115 | 116 | task.registerTask('sync6', 'sync, gonna fail', function() { 117 | throw new TypeError('sync6: TypeError'); 118 | }); 119 | 120 | task.registerTask('syncs', ['sync1', 'sync2', 'sync3', 'sync4', 'sync5', 'sync6']); 121 | 122 | task.registerTask('async1', 'async, gonna succeed', function() { 123 | var done = this.async(); 124 | setTimeout(function() { 125 | done(); 126 | }, 1); 127 | }); 128 | 129 | task.registerTask('async2', 'async, gonna fail', function() { 130 | var done = this.async(); 131 | setTimeout(function() { 132 | done(false); 133 | }, 1); 134 | }); 135 | 136 | task.registerTask('async3', 'async, gonna fail', function() { 137 | var done = this.async(); 138 | setTimeout(function() { 139 | done(new Error('async3: Error')); 140 | }, 1); 141 | }); 142 | 143 | task.registerTask('async4', 'async, gonna fail', function() { 144 | var done = this.async(); 145 | setTimeout(function() { 146 | done(new TypeError('async4: TypeError')); 147 | }, 1); 148 | }); 149 | 150 | task.registerTask('asyncs', ['async1', 'async2', 'async3', 'async4']); 151 | 152 | task.options({ 153 | error: function(e) { 154 | results.push({name: e.name, message: e.message}); 155 | }, 156 | done: function() { 157 | test.deepEqual(results, [ 158 | {name: 'Error', message: 'Task "sync2" failed.'}, 159 | {name: 'Error', message: 'sync3: Error'}, 160 | {name: 'TypeError', message: 'sync4: TypeError'}, 161 | {name: 'Error', message: 'sync5: Error'}, 162 | {name: 'TypeError', message: 'sync6: TypeError'}, 163 | {name: 'Error', message: 'Task "async2" failed.'}, 164 | {name: 'Error', message: 'async3: Error'}, 165 | {name: 'TypeError', message: 'async4: TypeError'} 166 | ], 'The specified tasks should have run, in-order.'); 167 | test.done(); 168 | } 169 | }); 170 | task.run('syncs', 'asyncs').start(); 171 | }, 172 | 'Task#exists': function(test) { 173 | test.expect(2); 174 | var task = this.task; 175 | test.equal(task.exists('nothing'), true, 'A task should not be exists (registered).'); 176 | test.equal(task.exists('notexistent'), false, 'A task should not be exists (registered).'); 177 | test.done(); 178 | }, 179 | 'Task#run (nested, exception handling)': function(test) { 180 | test.expect(2); 181 | var task = this.task; 182 | task.registerTask('yay', 'Run a registered task.', function() { 183 | test.doesNotThrow(function() { task.run('nothing'); }, 'Registered tasks should be runnable.'); 184 | }); 185 | task.registerTask('nay', 'Attempt to run an unregistered task.', function() { 186 | test.throws(function() { task.run('nonexistent'); }, 'Attempting to run unregistered tasks should throw an exception.'); 187 | }); 188 | task.options({ 189 | done: test.done 190 | }); 191 | task.run('yay', 'nay').start(); 192 | }, 193 | 'Task#run (signatures, queue order)': function(test) { 194 | test.expect(1); 195 | var task = this.task; 196 | task.registerTask('a', 'Push task name onto result.', result.pushTaskname); 197 | task.registerTask('b', 'Push task name onto result.', result.pushTaskname); 198 | task.registerTask('c', 'Push task name onto result.', result.pushTaskname); 199 | task.registerTask('d', 'Push task name onto result.', result.pushTaskname); 200 | task.registerTask('e', 'Push task name onto result.', result.pushTaskname); 201 | task.registerTask('f g', 'Push task name onto result.', result.pushTaskname); 202 | task.options({ 203 | done: function() { 204 | test.strictEqual(result.getJoined(), 'abcdef g', 'The specified tasks should have run, in-order.'); 205 | test.done(); 206 | } 207 | }); 208 | task.run('a').run('b', 'c').run(['d', 'e']).run('f g').start(); 209 | }, 210 | 'Task#run (colon separated arguments)': function(test) { 211 | test.expect(1); 212 | var task = this.task; 213 | task.registerTask('a', 'Push task name and args onto result.', function(x, y) { result.push([this.nameArgs, 1, this.name, x, y]); }); 214 | task.registerTask('a:b', 'Push task name and args onto result.', function(x, y) { result.push([this.nameArgs, 2, this.name, x, y]); }); 215 | task.registerTask('a:b:c', 'Push task name and args onto result.', function(x, y) { result.push([this.nameArgs, 3, this.name, x, y]); }); 216 | task.options({ 217 | done: function() { 218 | test.deepEqual(result.get(), [ 219 | ['a', 1, 'a', undefined, undefined], 220 | ['a:x', 1, 'a', 'x', undefined], 221 | ['a:x:c', 1, 'a', 'x', 'c'], 222 | ['a:b ', 1, 'a', 'b ', undefined], 223 | ['a: b:c', 1, 'a', ' b', 'c'], 224 | ['a:x\\:y:\\:z\\:', 1, 'a', 'x:y', ':z:'], 225 | 226 | ['a:b', 2, 'a:b', undefined, undefined], 227 | ['a:b:x', 2, 'a:b', 'x', undefined], 228 | ['a:b:x:y', 2, 'a:b', 'x', 'y'], 229 | ['a:b:c ', 2, 'a:b', 'c ', undefined], 230 | ['a:b:x\\:y:\\:z\\:', 2, 'a:b', 'x:y', ':z:'], 231 | 232 | ['a:b:c', 3, 'a:b:c', undefined, undefined], 233 | ['a:b:c: d', 3, 'a:b:c', ' d', undefined], 234 | ], 'Named tasks should be called as-specified if possible, and arguments should be passed properly.'); 235 | test.done(); 236 | } 237 | }); 238 | task.run( 239 | 'a', 'a:x', 'a:x:c', 'a:b ', 'a: b:c', 'a:x\\:y:\\:z\\:', 240 | 'a:b', 'a:b:x', 'a:b:x:y', 'a:b:c ', 'a:b:x\\:y:\\:z\\:', 241 | 'a:b:c', 'a:b:c: d' 242 | ).start(); 243 | }, 244 | 'Task#run (nested tasks, queue order)': function(test) { 245 | test.expect(1); 246 | var task = this.task; 247 | task.registerTask('a', 'Push task name onto result and run other tasks.', function() { result.push(this.name); task.run('b', 'e'); }); 248 | task.registerTask('b', 'Push task name onto result and run other tasks.', function() { result.push(this.name); task.run('c', 'd d'); }); 249 | task.registerTask('c', 'Push task name onto result.', result.pushTaskname); 250 | task.registerTask('d d', 'Push task name onto result.', result.pushTaskname); 251 | task.registerTask('e', 'Push task name onto result and run other tasks.', function() { result.push(this.name); task.run('f f'); }); 252 | task.registerTask('f f', 'Push task name onto result.', result.pushTaskname); 253 | task.registerTask('g', 'Push task name onto result.', result.pushTaskname); 254 | task.options({ 255 | done: function() { 256 | test.strictEqual(result.getJoined(), 'abcd def fg', 'The specified tasks should have run, in-order.'); 257 | test.done(); 258 | } 259 | }); 260 | task.run('a', 'g').start(); 261 | }, 262 | 'Task#run (async, nested tasks, queue order)': function(test) { 263 | test.expect(1); 264 | var task = this.task; 265 | task.registerTask('a', 'Push task name onto result and run other tasks.', function() { result.push(this.name); task.run('b', 'e'); delay(this.async()); }); 266 | task.registerTask('b', 'Push task name onto result and run other tasks.', function() { result.push(this.name); delay(this.async()); task.run('c', 'd d'); }); 267 | task.registerTask('c', 'Push task name onto result.', result.pushTaskname); 268 | task.registerTask('d d', 'Push task name onto result.', result.pushTaskname); 269 | task.registerTask('e', 'Push task name onto result and run other tasks.', function() { delay(this.async()); result.push(this.name); task.run('f f'); }); 270 | task.registerTask('f f', 'Push task name onto result and run other tasks.', function() { this.async()(); result.push(this.name); task.run('g'); }); 271 | task.registerTask('g', 'Push task name onto result.', result.pushTaskname); 272 | task.registerTask('h', 'Push task name onto result.', result.pushTaskname); 273 | task.options({ 274 | done: function() { 275 | test.strictEqual(result.getJoined(), 'abcd def fgh', 'The specified tasks should have run, in-order.'); 276 | test.done(); 277 | } 278 | }); 279 | task.run('a', 'h').start(); 280 | }, 281 | 'Task#run (async task with multiple callbacks)': function(test) { 282 | test.expect(1); 283 | var task = this.task; 284 | task.registerTask('a', 'Call async callback twice.', function() { var done = this.async(); done(); done(); }); 285 | task.registerTask('b', 'Never call async callback.', function() { this.async(); }); 286 | task.run('a', 'b').start(); 287 | delay(function() { 288 | test.deepEqual(task.current.name, 'b', 'Should be stuck on task with no async callback'); 289 | test.done(); 290 | }); 291 | }, 292 | 'Task#current': function(test) { 293 | test.expect(8); 294 | var task = this.task; 295 | test.deepEqual(task.current, {}, 'Should start empty.'); 296 | task.registerTask('a', 'Sample task.', function() { 297 | test.equal(task.current, this, 'This and task.current should be the same object.'); 298 | test.equal(task.current.nameArgs, 'a:b:c', 'Should be task name + args, as-specified.'); 299 | test.equal(task.current.name, 'a', 'Should be just the task name, no args.'); 300 | test.equal(typeof task.current.async, 'function', 'Should be a function.'); 301 | test.deepEqual(task.current.args, ['b', 'c'], 'Should be an array of args.'); 302 | test.deepEqual(task.current.flags, {b: true, c: true}, 'Should be a map of flags.'); 303 | }); 304 | task.options({ 305 | done: function() { 306 | test.deepEqual(task.current, {}, 'Should be empty again once tasks are done.'); 307 | test.done(); 308 | } 309 | }); 310 | task.run('a:b:c').start(); 311 | }, 312 | 'Task#clearQueue': function(test) { 313 | test.expect(1); 314 | var task = this.task; 315 | task.registerTask('a', 'Push task name onto result.', result.pushTaskname); 316 | task.registerTask('b', 'Push task name onto result.', result.pushTaskname); 317 | task.registerTask('c', 'Clear the queue.', function() { 318 | result.push(this.name); 319 | task.clearQueue().run('f'); 320 | }); 321 | task.registerTask('d', 'Push task name onto result.', result.pushTaskname); 322 | task.registerTask('e', 'Push task name onto result.', result.pushTaskname); 323 | task.registerTask('f', 'Push task name onto result.', result.pushTaskname); 324 | task.options({ 325 | done: function() { 326 | test.strictEqual(result.getJoined(), 'abcf', 'The specified tasks should have run, in-order.'); 327 | test.done(); 328 | } 329 | }); 330 | task.run('a', 'b', 'c', 'd', 'e').start(); 331 | }, 332 | 'Task#mark': function(test) { 333 | test.expect(1); 334 | var task = this.task; 335 | task.registerTask('a', 'Explode.', function() { 336 | throw task.taskError('whoops.'); 337 | }); 338 | task.registerTask('b', 'This task should never run.', result.pushTaskname); 339 | task.registerTask('c', 'This task should never run.', result.pushTaskname); 340 | 341 | task.registerTask('d', 'Push task name onto result.', result.pushTaskname); 342 | task.registerTask('e', 'Explode.', function() { 343 | throw task.taskError('whoops.'); 344 | }); 345 | task.registerTask('f', 'This task should never run.', result.pushTaskname); 346 | 347 | task.registerTask('g', 'Push task name onto result.', result.pushTaskname); 348 | task.registerTask('h', 'Push task name onto result.', result.pushTaskname); 349 | task.registerTask('i', 'Explode.', function() { 350 | throw task.taskError('whoops.'); 351 | }); 352 | 353 | task.registerTask('j', 'Run a task and push task name onto result.', function() { 354 | task.run('k'); 355 | result.push(this.name); 356 | }); 357 | task.registerTask('k', 'Explode.', function() { 358 | throw task.taskError('whoops.'); 359 | }); 360 | task.registerTask('l', 'This task should never run.', result.pushTaskname); 361 | 362 | task.registerTask('m', 'Push task name onto result.', result.pushTaskname); 363 | task.registerTask('n', 'Run a task and push task name onto result.', function() { 364 | task.run('o'); 365 | result.push(this.name); 366 | }); 367 | task.registerTask('o', 'Explode.', function() { 368 | throw task.taskError('whoops.'); 369 | }); 370 | 371 | task.registerTask('p', 'Push task name onto result.', result.pushTaskname); 372 | 373 | task.options({ 374 | error: function() { 375 | result.push('!' + this.name); 376 | task.clearQueue({untilMarker: true}); 377 | }, 378 | done: function() { 379 | test.strictEqual(result.getJoined(), '!ad!egh!ij!kmn!op', 'The specified tasks should have run, in-order.'); 380 | test.done(); 381 | } 382 | }); 383 | task.run('a', 'b', 'c').mark().run('d', 'e', 'f').mark().run('g', 'h', 'i').mark().run('j', 'l').mark().run('m', 'n').mark().run('p').mark().start(); 384 | }, 385 | 'Task#requires': function(test) { 386 | test.expect(1); 387 | var task = this.task; 388 | task.registerTask('notrun', 'This task is never run.', function() {}); 389 | task.registerTask('a a', 'Push task name onto result, but fail.', function() { 390 | result.push(this.name); 391 | return false; 392 | }); 393 | task.registerTask('b', 'Push task name onto result, but fail.', function() { 394 | var done = this.async(); 395 | delay(function() { done(false); }); 396 | result.push(this.name); 397 | }); 398 | task.registerTask('c', 'Succeed.', result.pushTaskname); 399 | task.registerTask('d', 'Succeed.', result.pushTaskname); 400 | task.registerTask('e', 'Succeed because all required tasks ran and succeeded.', function() { 401 | task.requires('c', 'd'); 402 | result.push(this.name); 403 | }); 404 | task.registerTask('x', 'Fail because a required task never ran.', function() { 405 | task.requires('c', 'notrun', 'd'); 406 | result.push(this.name); 407 | }); 408 | task.registerTask('y', 'Fail because a synchronous required task has failed.', function() { 409 | task.requires('a a', 'c', 'd'); 410 | result.push(this.name); 411 | }); 412 | task.registerTask('z', 'Fail because an asynchronous required task has failed.', function() { 413 | task.requires('b', 'c', 'd'); 414 | result.push(this.name); 415 | }); 416 | task.options({ 417 | error: function() { 418 | result.push('!' + this.name); 419 | }, 420 | done: function() { 421 | test.strictEqual(result.getJoined(), 'a a!a ab!bcde!x!y!z', 'Tasks whose requirements have failed or are missing should not run.'); 422 | test.done(); 423 | } 424 | }); 425 | task.run('a a', 'b', 'c', 'd', 'e', 'x', 'y', 'z').start(); 426 | } 427 | }; 428 | 429 | exports['Task#parseArgs'] = { 430 | setUp: function(done) { 431 | var task = requireTask().create(); 432 | this.parseTest = function() { 433 | return task.parseArgs(arguments); 434 | }; 435 | done(); 436 | }, 437 | 'arguments': function(test) { 438 | test.expect(4); 439 | test.deepEqual(this.parseTest('foo bar'), ['foo bar'], 'single argument should be converted to array.'); 440 | test.deepEqual(this.parseTest('foo bar: aa : bb '), ['foo bar: aa : bb '], 'single argument should be converted to array.'); 441 | test.deepEqual(this.parseTest('foo bar', 'baz', 'test 1 2 3'), ['foo bar', 'baz', 'test 1 2 3'], 'arguments should be converted to array.'); 442 | test.deepEqual(this.parseTest('foo bar', 'baz:x y z', 'test 1 2 3: 4 : 5'), ['foo bar', 'baz:x y z', 'test 1 2 3: 4 : 5'], 'arguments should be converted to array.'); 443 | test.done(); 444 | }, 445 | 'array': function(test) { 446 | test.expect(1); 447 | test.deepEqual(this.parseTest(['foo bar', 'baz:x y z', 'test 1 2 3: 4 : 5']), ['foo bar', 'baz:x y z', 'test 1 2 3: 4 : 5'], 'passed array should be used.'); 448 | test.done(); 449 | }, 450 | 'object': function(test) { 451 | test.expect(1); 452 | var obj = {}; 453 | test.deepEqual(this.parseTest(obj), [obj], 'single object should be returned as array.'); 454 | test.done(); 455 | }, 456 | 'nothing': function(test) { 457 | test.expect(1); 458 | test.deepEqual(this.parseTest(), [], 'should return an empty array if nothing passed.'); 459 | test.done(); 460 | } 461 | }; 462 | 463 | exports['Task#splitArgs'] = { 464 | setUp: function(done) { 465 | this.task = requireTask().create(); 466 | done(); 467 | }, 468 | 'arguments': function(test) { 469 | test.expect(9); 470 | var task = this.task; 471 | test.deepEqual(task.splitArgs(), [], 'missing items = empty array.'); 472 | test.deepEqual(task.splitArgs(''), [], 'missing items = empty array.'); 473 | test.deepEqual(task.splitArgs('a'), ['a'], 'single item should be parsed.'); 474 | test.deepEqual(task.splitArgs('a:b:c'), ['a', 'b', 'c'], 'mutliple items should be parsed.'); 475 | test.deepEqual(task.splitArgs('a::c'), ['a', '', 'c'], 'missing items should be parsed.'); 476 | test.deepEqual(task.splitArgs('::'), ['', '', ''], 'missing items should be parsed.'); 477 | test.deepEqual(task.splitArgs('\\:a:\\:b\\::c\\:'), [':a', ':b:', 'c:'], 'escaped colons should be unescaped.'); 478 | test.deepEqual(task.splitArgs('a\\\\:b\\\\:c'), ['a\\', 'b\\', 'c'], 'escaped backslashes should not be parsed.'); 479 | test.deepEqual(task.splitArgs('\\:a\\\\:\\\\\\:b\\:\\\\:c\\\\\\:\\\\'), [':a\\', '\\:b:\\', 'c\\:\\'], 'please avoid doing this, ok?'); 480 | test.done(); 481 | } 482 | }; 483 | --------------------------------------------------------------------------------