├── .gitignore ├── .npmrc ├── .travis.yml ├── README.md ├── bin └── grunty.js ├── package.json ├── src └── fake-gruntfile-code.js └── test ├── .gitignore ├── a.js ├── b.js ├── concat.js ├── concat.json └── foo.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/out.js 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '4' 10 | before_install: 11 | - npm i -g npm@^2.0.0 12 | before_script: 13 | - npm prune 14 | after_success: 15 | - npm run semantic-release 16 | branches: 17 | except: 18 | - "/^v\\d+\\.\\d+\\.\\d+$/" 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # grunty 2 | 3 | > Run any grunt plugin as NPM script without Gruntfile.js 4 | 5 | [![NPM][grunty-icon] ][grunty-url] 6 | 7 | [![Build status][grunty-ci-image] ][grunty-ci-url] 8 | [![dependencies][grunty-dependencies-image] ][grunty-dependencies-url] 9 | [![devdependencies][grunty-devdependencies-image] ][grunty-devdependencies-url] 10 | [![semantic-release][semantic-image] ][semantic-url] 11 | 12 | ## Zero task configuration (defaults) 13 | 14 | Often I want to run a simple task as NPM script, but hate creating verbose Gruntfile, installing 15 | grunt dependencies, etc. Enter **grunty** - just specify the plugin's module, name and any options 16 | in the NPM script command. No global grunt installation is necessary. 17 | 18 | `npm install --save-dev grunty` 19 | 20 | then use in the `package.json` 21 | 22 | ```json 23 | "scripts": { 24 | "concat": "grunty grunt-contrib-concat concat --src=a.js,b.js --dest=dist/out.js", 25 | "nice": "grunty grunt-nice-package nice-package" 26 | } 27 | ``` 28 | 29 | ## Using configuration file 30 | 31 | You can even pass configuration using a JSON file or JS file (just export an object) 32 | 33 | For example, put grunt plugin configurations into `config.json` 34 | 35 | ```json 36 | { 37 | "concat": { 38 | "default": { 39 | "src": ["a.js", "b.js"], 40 | "dest": "dist/out.js" 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | The add `config.json` to the NPM script line 47 | 48 | ```json 49 | "scripts": { 50 | "concat": "grunty grunt-contrib-concat concat config.json", 51 | } 52 | ``` 53 | 54 | You can see an example JSON config file in [test/concat.json](test/concat.json) which can be 55 | triggered using `npm run concat-config` command defined in the [package scripts](package.json). 56 | 57 | Similarly you can export the JavaScript object from a JS file for configuration, 58 | see [test/concat.js](test/concat.js) 59 | 60 | ## Examples 61 | 62 | ### Deploying to GitHub pages 63 | 64 | ```sh 65 | npm i -D grunty grunt-gh-pages 66 | ``` 67 | 68 | Create `deploy.json` 69 | 70 | ```json 71 | { 72 | "gh-pages": { 73 | "options": { 74 | "base": "." 75 | }, 76 | "src": [ 77 | "index.html", 78 | "dist/app-bundle.min.js" 79 | ] 80 | } 81 | } 82 | ``` 83 | 84 | ```json 85 | { 86 | "scripts": { 87 | "deploy": "grunty grunt-gh-pages gh-pages deploy.json" 88 | } 89 | } 90 | ``` 91 | 92 | The run `npm run deploy` to deploy to GitHub pages. 93 | 94 | ## Details 95 | 96 | Separate multiple values like filenames using `,` as in `--src=path/to/foo,path/to/bar` 97 | 98 | The plugin runs with the your local `package.json` attached to the config, thus you can use 99 | `pkg.name`, `pkg.description` and other properties. 100 | If the plugin requires more elaborate options, write `Gruntfile.js`. 101 | 102 | You do need to install the actual plugin and save reference in the `dev` dependencies. 103 | 104 | Read [Put mock data into Node require cache](http://glebbahmutov.com/blog/put-mock-data-into-node-require-cache/) 105 | to learn how this project fakes `gruntfile.js` without actually even saving mock one to disk. 106 | 107 | If you need to run an example and see diagnostic messages, you can using npm scripts and enabling 108 | the debug messages using [debug](https://www.npmjs.com/package/debug). 109 | For example to debug parsing config json 110 | 111 | $ DEBUG=cli npm run concat-config 112 | > grunty grunt-contrib-concat concat test/concat.json 113 | grunty@0.1.2 - Run any grunt plugin as NPM script without Gruntfile.js 114 | "Gleb Bahmutov " https://github.com/bahmutov/grunty 115 | cwd /Users/gleb/git/grunty 116 | cli grunty options +0ms { tasks: [], npm: [] } 117 | cli grunty plugin +2ms grunt-contrib-concat 118 | cli grunty target +0ms concat 119 | cli grunty config filename +1ms /grunty/test/concat.json 120 | 121 | ### Small print 122 | 123 | Author: Gleb Bahmutov © 2015 124 | 125 | * [@bahmutov](https://twitter.com/bahmutov) 126 | * [glebbahmutov.com](http://glebbahmutov.com) 127 | * [blog](http://glebbahmutov.com/blog/) 128 | 129 | License: MIT - do anything with the code, but don't blame me if it does not work. 130 | 131 | Spread the word: tweet, star on github, etc. 132 | 133 | Support: if you find any problems with this module, email / tweet / 134 | [open issue](https://github.com/bahmutov/grunty/issues) on Github 135 | 136 | ## MIT License 137 | 138 | Copyright (c) 2015 Gleb Bahmutov 139 | 140 | Permission is hereby granted, free of charge, to any person 141 | obtaining a copy of this software and associated documentation 142 | files (the "Software"), to deal in the Software without 143 | restriction, including without limitation the rights to use, 144 | copy, modify, merge, publish, distribute, sublicense, and/or sell 145 | copies of the Software, and to permit persons to whom the 146 | Software is furnished to do so, subject to the following 147 | conditions: 148 | 149 | The above copyright notice and this permission notice shall be 150 | included in all copies or substantial portions of the Software. 151 | 152 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 153 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 154 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 155 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 156 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 157 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 158 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 159 | OTHER DEALINGS IN THE SOFTWARE. 160 | 161 | [grunty-icon]: https://nodei.co/npm/grunty.png?downloads=true 162 | [grunty-url]: https://npmjs.org/package/grunty 163 | [grunty-ci-image]: https://travis-ci.org/bahmutov/grunty.png?branch=master 164 | [grunty-ci-url]: https://travis-ci.org/bahmutov/grunty 165 | [grunty-dependencies-image]: https://david-dm.org/bahmutov/grunty.png 166 | [grunty-dependencies-url]: https://david-dm.org/bahmutov/grunty 167 | [grunty-devdependencies-image]: https://david-dm.org/bahmutov/grunty/dev-status.png 168 | [grunty-devdependencies-url]: https://david-dm.org/bahmutov/grunty#info=devDependencies 169 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 170 | [semantic-url]: https://github.com/semantic-release/semantic-release 171 | -------------------------------------------------------------------------------- /bin/grunty.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('lazy-ass'); 4 | var check = require('check-more-types'); 5 | 6 | var log = require('debug')('cli'); 7 | 8 | var join = require('path').join; 9 | var fileExists = require('fs').existsSync; 10 | var fromThis = join.bind(null, __dirname); 11 | var pkg = require(fromThis('../package.json')); 12 | 13 | log('%s@%s - %s\n %s %s', 14 | pkg.name, pkg.version, 15 | pkg.description, 16 | JSON.stringify(pkg.author), pkg.homepage); 17 | log('cwd', process.cwd()) 18 | 19 | var resolve = require('path').resolve; 20 | 21 | /* 22 | var pkg = require('../package'); 23 | var packageResolved = resolve(fromThis('../package.json')); 24 | console.log('require cache has for package', packageResolved); 25 | console.log(require.cache[packageResolved]); 26 | */ 27 | 28 | var fakeGruntfile = 'fake-gruntfile.js'; 29 | var resolved = resolve(fakeGruntfile); 30 | 31 | // put fake stuff into the Require cache 32 | var Module = require('module'); 33 | var _resolveFilename = Module._resolveFilename; 34 | console.assert(typeof _resolveFilename === 'function'); 35 | Module._resolveFilename = function fakeResolveFilename(request, parent) { 36 | if (request === resolved) { 37 | return resolved; 38 | } else { 39 | return _resolveFilename(request, parent); 40 | } 41 | }; 42 | 43 | var grunt = require('grunt'); 44 | // console.log(grunt.task.registerTask.toString()); 45 | Object.keys(grunt.cli.options).forEach(function (key) { 46 | var str = grunt.cli.options[key]; 47 | if (check.unemptyString(str)) { 48 | var split = str.split(','); 49 | if (split.length > 1) { 50 | grunt.cli.options[key] = split; 51 | } 52 | } 53 | }); 54 | 55 | log('grunty options', grunt.cli.options); 56 | 57 | var plugin = process.argv[2]; 58 | la(check.unemptyString(plugin), 59 | 'missing grunt plugin name to run, for example grunt-contrib-concat', 60 | process.argv); 61 | log('grunty plugin', plugin); 62 | 63 | var target = process.argv[3]; 64 | function isOption(x) { 65 | return check.unemptyString(x) && /^-/.test(x); 66 | } 67 | if (check.unemptyString(target)) { 68 | if (isOption(target)) { 69 | target = null; 70 | } 71 | } 72 | log('grunty target', target); 73 | 74 | function isJson(name) { 75 | return /\.json$/.test(name); 76 | } 77 | 78 | function isJS(name) { 79 | return /\.js$/.test(name); 80 | } 81 | 82 | function isConfigFilename(name) { 83 | return check.unemptyString(name) && 84 | name !== __filename && 85 | fileExists(name) && 86 | (isJson(name) || isJS(name)); 87 | } 88 | 89 | var configFile; 90 | process.argv.some(function (arg) { 91 | if (isConfigFilename(arg)) { 92 | configFile = require('path').resolve(arg); 93 | return true; 94 | } 95 | }); 96 | // console.log('program arguments', process.argv, 'config filename', configFile); 97 | log('grunty config filename', configFile); 98 | 99 | var fakeGruntfileFunc = require('../src/fake-gruntfile-code')({ 100 | plugin: plugin, 101 | target: target, 102 | config: configFile ? require(configFile) : null, 103 | src: grunt.cli.options.src, 104 | dest: grunt.cli.options.dest 105 | }); 106 | la(check.fn(fakeGruntfileFunc), 'expected fake gruntfile function', fakeGruntfileFunc); 107 | la(fakeGruntfileFunc.length === 1, 108 | 'fake gruntfile function should expect 1 argument', fakeGruntfileFunc.toString()); 109 | require.cache[resolved] = { 110 | id: resolved, 111 | exports: fakeGruntfileFunc, 112 | parent: null, 113 | filename: resolved, 114 | loaded: true 115 | }; 116 | 117 | // process.exit(0); 118 | var _exists = grunt.file.exists; 119 | grunt.file.exists = function mockExists(filename) { 120 | if (filename === fakeGruntfile) { 121 | return true; 122 | } else { 123 | return _exists(filename) 124 | } 125 | }; 126 | 127 | // console.log('grunt.tasks', grunt.tasks); 128 | 129 | function done() { 130 | console.log('grunt is done'); 131 | } 132 | 133 | var options = { 134 | verbose: Boolean(grunt.cli.options.verbose), 135 | gruntfile: fakeGruntfile 136 | }; 137 | 138 | if (target) { 139 | grunt.tasks([target], options, done); 140 | } 141 | // console.log(grunt.file); 142 | // grunt.cli(); 143 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunty", 3 | "description": "Run any grunt plugin as NPM script without Gruntfile.js", 4 | "version": "0.0.0-semantic-release", 5 | "author": "Gleb Bahmutov ", 6 | "bin": { 7 | "grunty": "bin/grunty.js" 8 | }, 9 | "bugs": { 10 | "url": "https://github.com/bahmutov/grunty/issues" 11 | }, 12 | "contributors": [], 13 | "dependencies": { 14 | "check-more-types": "1.7.2", 15 | "debug": "2.2.0", 16 | "grunt": "0.4.5", 17 | "lazy-ass": "0.5.8" 18 | }, 19 | "devDependencies": { 20 | "git-issues": "1.2.0", 21 | "grunt-contrib-concat": "0.5.1", 22 | "grunt-nice-package": "0.9.2", 23 | "pre-git": "0.2.1", 24 | "semantic-release": "4.3.5" 25 | }, 26 | "engines": { 27 | "node": "> 0.10.0" 28 | }, 29 | "homepage": "https://github.com/bahmutov/grunty", 30 | "keywords": [ 31 | "grunt", 32 | "cli", 33 | "helper", 34 | "npm", 35 | "script", 36 | "runner", 37 | "plugin" 38 | ], 39 | "license": "MIT", 40 | "main": "index.js", 41 | "pre-commit": [ 42 | "npm test", 43 | "npm version" 44 | ], 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/bahmutov/grunty.git" 48 | }, 49 | "scripts": { 50 | "issues": "git-issues", 51 | "test": "npm run concat; npm run concat2; npm run nice; npm run concat-config; npm run concat-js-config", 52 | "concat": "bin/grunty.js grunt-contrib-concat concat --src='test/[a|b].js' --dest=test/out.js --verbose", 53 | "concat2": "bin/grunty.js grunt-contrib-concat concat --src=test/a.js,test/b.js --dest=test/out.js", 54 | "concat-config": "bin/grunty.js grunt-contrib-concat concat test/concat.json", 55 | "concat-js-config": "bin/grunty.js grunt-contrib-concat concat test/concat.js", 56 | "nice": "bin/grunty.js grunt-nice-package nice-package", 57 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/fake-gruntfile-code.js: -------------------------------------------------------------------------------- 1 | require('lazy-ass'); 2 | var check = require('check-more-types'); 3 | 4 | function isValidSource(x) { 5 | if (check.not.defined(x)) { 6 | return true; 7 | } 8 | return check.unemptyString(x) || 9 | check.array(x); 10 | } 11 | 12 | var optionsSchema = { 13 | plugin: check.unemptyString, 14 | target: check.maybe.unemptyString, 15 | src: isValidSource, 16 | dest: check.maybe.unemptyString 17 | }; 18 | var isValidOptions = check.schema.bind(null, optionsSchema); 19 | 20 | function isMultiTask(options) { 21 | return check.object(options) && 22 | check.unemptyString(options.target) && 23 | check.has(options, 'src') && 24 | check.has(options, 'dest') && 25 | check.maybe.object(options.config); 26 | } 27 | 28 | module.exports = function fakeGruntfileInit(options) { 29 | la(check.object(options), 'missing grunty options', options); 30 | la(isValidOptions(options), 'invalid options', options); 31 | 32 | function fakeGruntfile(grunt) { 33 | console.log('inside fake gruntfile'); 34 | 35 | var pkg = grunt.file.readJSON('package.json'); 36 | 37 | var config = check.object(options.config) ? options.config : {}; 38 | config.pkg = pkg; 39 | 40 | if (isMultiTask(options)) { 41 | config[options.target] = { 42 | default: options 43 | }; 44 | } 45 | 46 | grunt.initConfig(config); 47 | grunt.task.loadNpmTasks(options.plugin); 48 | grunt.registerTask('default', []); 49 | } 50 | 51 | return fakeGruntfile; 52 | }; 53 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | out-js-config.js 2 | out-json-config.js 3 | -------------------------------------------------------------------------------- /test/a.js: -------------------------------------------------------------------------------- 1 | // first line 2 | -------------------------------------------------------------------------------- /test/b.js: -------------------------------------------------------------------------------- 1 | // second line 2 | -------------------------------------------------------------------------------- /test/concat.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | concat: { 3 | default: { 4 | src: ['test/a.js', 'test/b.js'], 5 | dest: 'test/out-js-config.js' 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /test/concat.json: -------------------------------------------------------------------------------- 1 | { 2 | "concat": { 3 | "default": { 4 | "src": ["test/a.js", "test/b.js"], 5 | "dest": "test/out-json-config.js" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/foo.js: -------------------------------------------------------------------------------- 1 | module.exports = 'this is foo'; 2 | --------------------------------------------------------------------------------