├── .gitattributes ├── .jshintignore ├── test ├── fixtures │ ├── conflict.js │ ├── dir-css-fixtures │ │ ├── file1.css │ │ └── file2.css │ ├── dummy-project │ │ ├── subdir │ │ │ └── .gitkeep │ │ └── .yo-rc.json │ ├── foo.js │ ├── testFile │ ├── foo-copy.js │ ├── testFile2 │ ├── file-conflict.txt │ ├── foo-process.js │ ├── dir-fixtures │ │ ├── foo.js │ │ ├── foo-process.js │ │ └── foo-template.js │ ├── template-tags.jst │ ├── template.jst │ ├── dummy-package │ │ ├── index.js │ │ └── package.json │ ├── fooooooo-file.js │ ├── foo-template.js │ ├── template-setting.xml │ ├── custom-template-setting.xml │ ├── append-prepend-to-file.html │ ├── config.json │ ├── testFile.tar.gz │ ├── yeoman-logo.png │ ├── testRemoteFile.tar.gz │ ├── lookup-project │ │ ├── subdir │ │ │ └── package.json │ │ └── package.json │ ├── js_block.html │ ├── css_block_dir.html │ ├── css_block.html │ ├── js_block_with_attr.html │ ├── js_block_dir.html │ ├── mocha-generator │ │ ├── package.json │ │ └── main.js │ ├── mocha-generator-base │ │ ├── package.json │ │ └── main.js │ ├── options-generator │ │ ├── package.json │ │ └── main.js │ ├── custom-generator-simple │ │ ├── package.json │ │ └── main.js │ ├── custom-generator-extend │ │ ├── support │ │ │ ├── index.js │ │ │ └── scaffold │ │ │ │ └── main.js │ │ └── package.json │ └── help.txt ├── .jshintrc ├── generators │ ├── test-testacular-app.js │ ├── test-mocha-generator.js │ ├── test-ember-starter.js │ ├── test-angular.js │ ├── test-quickstart.js │ ├── test-bbb.js │ ├── test-ember.js │ └── test-backbone.js ├── fetch.js ├── invoke.js ├── user.js ├── conflicter.js ├── install.js ├── generators.js ├── storage.js ├── helpers.js ├── wiring.js ├── remote.js ├── prompt-suggestion.js ├── run-context.js └── actions.js ├── .gitignore ├── .travis.yml ├── .npmignore ├── benchmark └── module.js ├── .editorconfig ├── jsdoc.json ├── .jshintrc ├── lib ├── actions │ ├── spawn_command.js │ ├── string.js │ ├── invoke.js │ ├── file.js │ ├── user.js │ ├── fetch.js │ ├── help.js │ ├── install.js │ ├── remote.js │ ├── wiring.js │ └── actions.js ├── named-base.js ├── util │ ├── common.js │ ├── engines.js │ ├── storage.js │ ├── prompt-suggestion.js │ └── conflicter.js └── test │ ├── adapter.js │ ├── run-context.js │ └── helpers.js ├── .eslintrc ├── .jscsrc ├── gulpfile.js ├── readme.md ├── index.js └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | test/fixtures/** -------------------------------------------------------------------------------- /test/fixtures/conflict.js: -------------------------------------------------------------------------------- 1 | var a = 1; 2 | -------------------------------------------------------------------------------- /test/fixtures/dir-css-fixtures/file1.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/dir-css-fixtures/file2.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/dummy-project/subdir/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/foo.js: -------------------------------------------------------------------------------- 1 | var foo = 'foo'; 2 | -------------------------------------------------------------------------------- /test/fixtures/testFile: -------------------------------------------------------------------------------- 1 | Roses are red. 2 | -------------------------------------------------------------------------------- /test/fixtures/dummy-project/.yo-rc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/fixtures/foo-copy.js: -------------------------------------------------------------------------------- 1 | var foo = 'foo'; 2 | -------------------------------------------------------------------------------- /test/fixtures/testFile2: -------------------------------------------------------------------------------- 1 | Violets are blue. 2 | -------------------------------------------------------------------------------- /test/fixtures/file-conflict.txt: -------------------------------------------------------------------------------- 1 | initial content 2 | -------------------------------------------------------------------------------- /test/fixtures/foo-process.js: -------------------------------------------------------------------------------- 1 | var foo = 'foo'; 2 | -------------------------------------------------------------------------------- /test/fixtures/dir-fixtures/foo.js: -------------------------------------------------------------------------------- 1 | var foo = 'foo'; 2 | -------------------------------------------------------------------------------- /test/fixtures/template-tags.jst: -------------------------------------------------------------------------------- 1 | ${bar} = <%= foo %> 2 | -------------------------------------------------------------------------------- /test/fixtures/dir-fixtures/foo-process.js: -------------------------------------------------------------------------------- 1 | var foo = 'foo'; 2 | -------------------------------------------------------------------------------- /test/fixtures/template.jst: -------------------------------------------------------------------------------- 1 | var <%= foo %> = '<%= foo %>'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | temp/ 3 | temp.dev/ 4 | coverage/ 5 | -------------------------------------------------------------------------------- /test/fixtures/dummy-package/index.js: -------------------------------------------------------------------------------- 1 | exports.yeoman = "Yo!"; 2 | -------------------------------------------------------------------------------- /test/fixtures/fooooooo-file.js: -------------------------------------------------------------------------------- 1 | var <%= foo %> = '<%= foo %>'; 2 | -------------------------------------------------------------------------------- /test/fixtures/dir-fixtures/foo-template.js: -------------------------------------------------------------------------------- 1 | var <%= foo %> = '<%= foo %>'; 2 | -------------------------------------------------------------------------------- /test/fixtures/dummy-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dummy" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/foo-template.js: -------------------------------------------------------------------------------- 1 | var <%= foo %> = '<%= foo %>'; 2 | <%%= extra %> 3 | -------------------------------------------------------------------------------- /test/fixtures/template-setting.xml: -------------------------------------------------------------------------------- 1 | {{= foo }} <%= foo %>; 2 | -------------------------------------------------------------------------------- /test/fixtures/custom-template-setting.xml: -------------------------------------------------------------------------------- 1 | {{= foo }}{{ spy() }} 2 | -------------------------------------------------------------------------------- /test/fixtures/append-prepend-to-file.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /test/fixtures/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "name": "test", 4 | "testFramework": "mocha" 5 | } 6 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | env: 5 | global: 6 | - DEBUG="generators:*" 7 | -------------------------------------------------------------------------------- /test/fixtures/testFile.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/generator/master/test/fixtures/testFile.tar.gz -------------------------------------------------------------------------------- /test/fixtures/yeoman-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/generator/master/test/fixtures/yeoman-logo.png -------------------------------------------------------------------------------- /test/fixtures/testRemoteFile.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanbuck/generator/master/test/fixtures/testRemoteFile.tar.gz -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | doc/ 3 | .npmignore 4 | .editorconfig 5 | .travis.yml 6 | .jshintrc 7 | .gitattributes 8 | contributing.md 9 | doc.js 10 | -------------------------------------------------------------------------------- /test/fixtures/lookup-project/subdir/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lookup-subdir-dummy-env", 3 | "dependencies": { 4 | "generator-dummy": "~0.1.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/lookup-project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lookup-dummy-env", 3 | "dependencies": { 4 | "generator-commonjs": "*", 5 | "generator-dummy": "~0.1.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.jshintrc", 3 | "globals": [ 4 | "describe", 5 | "it", 6 | "xit", 7 | "before", 8 | "after", 9 | "beforeEach", 10 | "afterEach" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/js_block.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/css_block_dir.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/css_block.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /benchmark/module.js: -------------------------------------------------------------------------------- 1 | /*global suite, bench */ 2 | 'use strict'; 3 | 4 | suite('yeoman-generator module', function () { 5 | bench('require', function () { 6 | require('..'); 7 | delete require.cache[require.resolve('..')]; 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /test/fixtures/js_block_with_attr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/fixtures/js_block_dir.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": ["index.js", "lib"], 4 | "includePattern": ".+\\.js(doc)?$" 5 | }, 6 | "opts": { 7 | "recurse": true, 8 | "destination": "../yeoman-generator-doc/" 9 | }, 10 | "plugins": [ 11 | "plugins/markdown" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/mocha-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "mocha-generator", 4 | "version": "0.0.0", 5 | "main": "main.js", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "optionalDependencies": {}, 9 | "engines": { 10 | "node": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/mocha-generator-base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "mocha-generator", 4 | "version": "0.0.0", 5 | "main": "main.js", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "optionalDependencies": {}, 9 | "engines": { 10 | "node": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/options-generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "options-generator", 4 | "version": "0.0.0", 5 | "main": "main.js", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "optionalDependencies": {}, 9 | "engines": { 10 | "node": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/custom-generator-simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "custom-generator-simple", 4 | "version": "0.0.0", 5 | "main": "main.js", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "optionalDependencies": {}, 9 | "engines": { 10 | "node": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/custom-generator-extend/support/index.js: -------------------------------------------------------------------------------- 1 | var generators = require('../../../..'); 2 | var util = require('util'); 3 | 4 | var Generator = module.exports = function Generator(args, options) { 5 | generators.NamedBase.apply(this, arguments); 6 | }; 7 | 8 | util.inherits(Generator, generators.NamedBase); 9 | -------------------------------------------------------------------------------- /test/fixtures/custom-generator-extend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "", 3 | "name": "custom-generator-extend", 4 | "version": "0.0.0", 5 | "main": "support/scaffold/main.js", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "optionalDependencies": {}, 9 | "engines": { 10 | "node": "*" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/fixtures/help.txt: -------------------------------------------------------------------------------- 1 | Usage: init GENERATOR [args] [options] 2 | 3 | General options: 4 | -h, --help # Print generator's options and usage 5 | -f, --force # Overwrite files that already exist 6 | 7 | Please choose a generator below. 8 | 9 | 10 | Extend 11 | extend:support:scaffold 12 | 13 | Simple 14 | simple 15 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": false, 5 | "curly": false, 6 | "eqeqeq": true, 7 | "eqnull": true, 8 | "immed": true, 9 | "latedef": false, 10 | "newcap": true, 11 | "noarg": true, 12 | "undef": true, 13 | "strict": true, 14 | "quotmark": "single", 15 | "scripturl": true 16 | } 17 | -------------------------------------------------------------------------------- /lib/actions/spawn_command.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | var spawn = require('cross-spawn'); 4 | 5 | /** 6 | * Normalize a command across OS and spawn it. 7 | * 8 | * @param {String} command 9 | * @param {Array} args 10 | * 11 | * @mixin 12 | * @alias actions/spawn_command 13 | */ 14 | 15 | module.exports = function spawnCommand(command, args, opt) { 16 | opt = opt || {}; 17 | return spawn(command, args, _.defaults(opt, { stdio: 'inherit' })); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/actions/string.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | _.str = require('underscore.string'); 4 | _.mixin(_.str.exports()); 5 | 6 | /** 7 | * @mixin 8 | * @alias actions/string 9 | */ 10 | var string = module.exports; 11 | 12 | /** 13 | * Mix in non-conflicting functions to underscore namespace and generators. 14 | * 15 | * @mixes lodash 16 | * @mixes underscore.string 17 | * @example 18 | * 19 | * this._.humanize('stuff-dash'); 20 | * this._.classify('hello-model'); 21 | */ 22 | 23 | string._ = _; 24 | -------------------------------------------------------------------------------- /test/generators/test-testacular-app.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, after, it, afterEach, beforeEach */ 2 | 'use strict'; 3 | var path = require('path'); 4 | var helpers = require('../..').test; 5 | 6 | describe('Testacular:App generator test', function () { 7 | before(helpers.before(path.join(__dirname, './temp'))); 8 | 9 | it('runs sucessfully', function (done) { 10 | helpers.runGenerator('testacular:app', done); 11 | }); 12 | 13 | it('creates expected files', function () { 14 | 15 | helpers.assertFile('testacular.conf.js'); 16 | 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/fixtures/custom-generator-extend/support/scaffold/main.js: -------------------------------------------------------------------------------- 1 | 2 | // in real use case, users need to require `yeoman-generators` 3 | // var generators = require('yeoman-generators'); 4 | var generators = require('../../../../../'); 5 | var util = require('util'); 6 | 7 | var Generator = module.exports = function Generator(args, options) { 8 | generators.NamedBase.apply(this, arguments); 9 | // arguments === this.arguments 10 | // options === this.options 11 | }; 12 | 13 | // namespace 14 | // Generator.namespace = 'custom-generator-extend'; 15 | 16 | util.inherits(Generator, generators.NamedBase); 17 | -------------------------------------------------------------------------------- /test/fixtures/mocha-generator-base/main.js: -------------------------------------------------------------------------------- 1 | 2 | // in real use case, users need to require `yeoman-generators` 3 | // var generators = require('yeoman-generators'); 4 | var generators = require('../../../'); 5 | var util = require('util'); 6 | 7 | var Generator = module.exports = function Generator(args, options) { 8 | generators.NamedBase.apply(this, arguments); 9 | // arguments === this.arguments 10 | // options === this.options 11 | }; 12 | 13 | // namespace 14 | Generator.namespace = 'mocha:generator-base'; 15 | 16 | util.inherits(Generator, generators.NamedBase); 17 | 18 | Generator.prototype.doStuff = function() {}; 19 | -------------------------------------------------------------------------------- /lib/named-base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Base = require('./base'); 3 | 4 | /** 5 | * The `NamedBase` object is only dealing with one argument: `name`. 6 | * 7 | * You can use it whenever you need at least one **required** positional 8 | * argument for your generator (which is a fairly common use case). 9 | * 10 | * @constructor 11 | * @augments Base 12 | * @alias NamedBase 13 | * @param {String|Array} args [description] 14 | * @param {Object} options [description] 15 | */ 16 | 17 | module.exports = Base.extend({ 18 | constructor: function () { 19 | Base.apply(this, arguments); 20 | this.argument('name', { type: String, required: true }); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /test/generators/test-mocha-generator.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, after, it, afterEach, beforeEach */ 2 | 'use strict'; 3 | var path = require('path'); 4 | var helpers = require('../..').test; 5 | 6 | describe('Mocha:Generator generator test', function () { 7 | before(helpers.before(path.join(__dirname, './temp'))); 8 | 9 | it('runs sucessfully', function (done) { 10 | helpers.runGenerator('mocha:generator', done); 11 | }); 12 | 13 | it('creates expected files', function () { 14 | 15 | // Use helpers.assertFile() to help you test the output of your generator 16 | // 17 | // Example: 18 | // 19 | // // check file exists 20 | // helpers.assertFile('app/model/post.js'); 21 | // // Check content 22 | // helpers.assertFile('app/model/post.js', /Backbone\.model/); 23 | it('should create expected files'); 24 | 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /lib/util/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chalk = require('chalk'); 3 | 4 | /** 5 | * Common/General utilities 6 | * 7 | * @mixin util/common 8 | */ 9 | var common = module.exports; 10 | 11 | /** 12 | * 'Welcome to Yeoman' prompt intro 13 | * @type {String} 14 | * @memberof util/common 15 | */ 16 | common.yeoman = 17 | '\n _-----_' + 18 | '\n | |' + 19 | '\n |' + chalk.red('--(o)--') + '| .--------------------------.' + 20 | '\n `---------´ | ' + chalk.yellow.bold('Welcome to Yeoman') + ', |' + 21 | '\n ' + chalk.yellow('(') + ' _' + chalk.yellow('´U`') + '_ ' + chalk.yellow(')') + ' | ' + chalk.yellow.bold('ladies and gentlemen!') + ' |' + 22 | '\n /___A___\\ \'__________________________\'' + 23 | '\n ' + chalk.yellow('| ~ |') + 24 | '\n __' + chalk.yellow('\'.___.\'') + '__' + 25 | '\n ´ ' + chalk.red('` |') + '° ' + chalk.red('´ Y') + ' `\n'; 26 | -------------------------------------------------------------------------------- /test/fixtures/options-generator/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Example of a generator with options. 4 | // 5 | // It takes a list of arguments (usually CLI args) and a Hash of options 6 | // (CLI options), the context of the function is a `new Generator.Base` 7 | // object, which means that you can use the API as if you were extending 8 | // `Base`. 9 | 10 | var yeoman = require('../../../'); 11 | 12 | module.exports = yeoman.generators.Base.extend({ 13 | constructor: function () { 14 | yeoman.generators.Base.apply(this, arguments); 15 | 16 | // this.log('as passed in: ', this.options.testOption); 17 | this.option('testOption', { 18 | type: Boolean, 19 | desc: 'Testing falsey values for option', 20 | defaults: true 21 | }); 22 | }, 23 | 24 | testOption: function () { 25 | // this.log('as rendered: ', this.options.testOption); 26 | } 27 | }); 28 | 29 | module.exports.namespace = 'options:generator'; 30 | -------------------------------------------------------------------------------- /test/generators/test-ember-starter.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, after, it, afterEach, beforeEach */ 2 | 'use strict'; 3 | var path = require('path'); 4 | var helpers = require('../..').test; 5 | 6 | describe('Ember-Starter generator test', function () { 7 | before(helpers.before(path.join(__dirname, './temp'))); 8 | 9 | it('runs sucessfully', function (done) { 10 | helpers.runGenerator('ember-starter', done); 11 | }); 12 | 13 | it('creates expected files', function () { 14 | 15 | helpers.assertFile('app/scripts'); 16 | 17 | helpers.assertFile('app/styles'); 18 | 19 | helpers.assertFile('app/scripts/app.js'); 20 | 21 | helpers.assertFile('app/scripts/libs/ember-1.0.pre.js'); 22 | 23 | helpers.assertFile('app/scripts/libs/handlebars-1.0.0.beta.6.js'); 24 | 25 | helpers.assertFile('app/scripts/libs/jquery-1.7.2.min.js'); 26 | 27 | helpers.assertFile('app/index.html'); 28 | 29 | helpers.assertFile('app/styles/style.css'); 30 | 31 | helpers.assertFile('Gruntfile.js'); 32 | 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "amd": true, 4 | "mocha": true, 5 | "node": true 6 | }, 7 | "rules": { 8 | "camelcase": 0, 9 | "consistent-return": 0, 10 | "curly": 0, 11 | "eol-last":0, 12 | "eqeqeq": [2, "smart"], 13 | "handle-callback-err": 0, 14 | "no-alert": 2, 15 | "no-bitwise": 0, 16 | "new-cap": 2, 17 | "no-caller": 2, 18 | "no-console": 0, 19 | "no-debugger": 1, 20 | "no-dupe-keys": 2, 21 | "no-eq-null": 0, 22 | "no-extra-bind": 0, 23 | "no-extra-parens": 0, 24 | "no-mixed-spaces-and-tabs": [2, true], 25 | "no-new": 0, 26 | "no-path-concat": 0, 27 | "no-process-exit": 0, 28 | "no-trailing-spaces": 2, 29 | "no-undef": 2, 30 | "no-underscore-dangle": 0, 31 | "no-unreachable": 1, 32 | "no-unused-vars": [2, {"vars": "all", "args": "none"}], 33 | "no-use-before-define": 2, 34 | "quotes": [2, "single"], 35 | "strict": 2, 36 | "valid-jsdoc": [2, { "requireReturn": false, "requireParamDescription": false }], 37 | "wrap-iife": [2, "any"], 38 | "yoda": 0 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/fixtures/custom-generator-simple/main.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Example of a simple generator. 4 | // 5 | // A raw function that is executed when this generator is resolved. 6 | // 7 | // It takes a list of arguments (usually CLI args) and a Hash of options 8 | // (CLI options), the context of the function is a `new Generator.Base` 9 | // object, which means that you can use the API as if you were extending 10 | // `Base`. 11 | // 12 | // It works with simple generator. If you need to do a bit more complex 13 | // stuff, extend from Generator.Base and defines your generator steps 14 | // in several methods. 15 | 16 | module.exports = function(args, options) { 17 | console.log('Executing generator with', args, options); 18 | }; 19 | 20 | module.exports.name = 'You can name your generator'; 21 | module.exports.description = 'And add a custom description by adding a `description` property to your function.'; 22 | module.exports.usage = 'Usage can be used to customize the help output'; 23 | 24 | // namespace is resolved depending on the location of this generator, 25 | // unless you specifically define it. 26 | // module.exports.namespace = 'custom-generator'; 27 | -------------------------------------------------------------------------------- /lib/actions/invoke.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Receives a `namespace`, and an Hash of `options` to invoke a given 5 | * generator. The usual list of arguments can be set with `options.args` 6 | * (ex. nopt's argv.remain array) 7 | * 8 | * DEPRECATION notice: As of version 0.17.0, `invoke()` should usually be 9 | * replaced by `composeWith()`. 10 | * 11 | * @param {String} namespace 12 | * @param {Object} options 13 | * @param {Function} cb 14 | * 15 | * @mixin 16 | * @alias actions/invoke 17 | */ 18 | 19 | module.exports = function invoke(namespace, options, cb) { 20 | cb = cb || function () {}; 21 | options = options || {}; 22 | options.args = options.args || []; 23 | 24 | // Hack: create a clone of the environment because we don't want to share 25 | // the runLoop 26 | var env = require('yeoman-environment').util.duplicateEnv(this.env); 27 | var generator = env.create(namespace, options); 28 | 29 | this.log.emit('up'); 30 | this.log.invoke(namespace); 31 | this.log.emit('up'); 32 | 33 | generator.on('end', this.log.emit.bind(this.log, 'down')); 34 | generator.on('end', this.log.emit.bind(this.log, 'down')); 35 | 36 | return generator.run(cb); 37 | }; 38 | -------------------------------------------------------------------------------- /test/fixtures/mocha-generator/main.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Example of a simple generator. 4 | // 5 | // A raw function that is executed when this generator is resolved. 6 | // 7 | // It takes a list of arguments (usually CLI args) and a Hash of options 8 | // (CLI options), the context of the function is a `new Generator.Base` 9 | // object, which means that you can use the API as if you were extending 10 | // `Base`. 11 | // 12 | // It works with simple generator, if you need to do a bit more complex 13 | // stuff, extends from Generator.Base and defines your generator steps 14 | // in several methods. 15 | var util = require('util'); 16 | var generators = require('../../../'); 17 | 18 | module.exports = function(args, options) { 19 | generators.Base.apply(this, arguments); 20 | console.log('Executing generator with', args, options); 21 | }; 22 | util.inherits(module.exports, generators.Base); 23 | 24 | module.exports.name = 'You can name your generator'; 25 | module.exports.description = 'Ana add a custom description by adding a `description` property to your function.'; 26 | module.exports.usage = 'Usage can be used to customize the help output'; 27 | 28 | // namespace is resolved depending on the location of this generator, 29 | // unless you specifically define it. 30 | module.exports.namespace = 'mocha:generator'; 31 | -------------------------------------------------------------------------------- /test/generators/test-angular.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, after, it, afterEach, beforeEach */ 2 | 'use strict'; 3 | var path = require('path'); 4 | var helpers = require('../..').test; 5 | var generators = require('../..'); 6 | 7 | describe('Angular generator test', function () { 8 | // cleanup the temp dir and cd into it 9 | before(helpers.before(path.join(__dirname, './temp'))); 10 | 11 | before(function (done) { 12 | // setup the environment 13 | this.env = generators().lookup('*:*'); 14 | this.env.run('angular:all foo bar', done); 15 | }); 16 | 17 | it('creates expected files', function () { 18 | helpers.assertFile('app/.htaccess'); 19 | helpers.assertFile('app/404.html'); 20 | helpers.assertFile('app/favicon.ico'); 21 | helpers.assertFile('app/robots.txt'); 22 | helpers.assertFile('app/scripts/vendor/angular.js'); 23 | helpers.assertFile('app/scripts/vendor/angular.min.js'); 24 | helpers.assertFile('app/styles/main.css'); 25 | helpers.assertFile('app/views/main.html'); 26 | helpers.assertFile('Gruntfile.js'); 27 | helpers.assertFile('package.json'); 28 | helpers.assertFile('test/vendor/angular-mocks.js'); 29 | helpers.assertFile('app/scripts/app.js'); 30 | helpers.assertFile('app/index.html'); 31 | helpers.assertFile('app/scripts/controllers/main.js'); 32 | helpers.assertFile('test/spec/controllers/main.js'); 33 | helpers.assertFile('testacular.conf.js'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": ["else", "for", "while", "do", "try", "catch"], 3 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch", "function"], 4 | "requireSpacesInFunctionExpression": { 5 | "beforeOpeningCurlyBrace": true 6 | }, 7 | "disallowMultipleVarDecl": true, 8 | "requireSpacesInsideObjectBrackets": "allButNested", 9 | "disallowSpacesInsideArrayBrackets": true, 10 | "disallowSpacesInsideParentheses": true, 11 | "disallowSpaceAfterObjectKeys": true, 12 | "disallowQuotedKeysInObjects": true, 13 | "requireSpaceBeforeBinaryOperators": ["?", "+", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 14 | "disallowSpaceAfterBinaryOperators": ["!"], 15 | "requireSpaceAfterBinaryOperators": ["?", ",", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 16 | "disallowSpaceBeforeBinaryOperators": [","], 17 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 18 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 19 | "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], 20 | "requireSpaceAfterBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], 21 | "disallowImplicitTypeConversion": ["numeric", "binary", "string"], 22 | "disallowKeywords": ["with", "eval"], 23 | "disallowMultipleLineBreaks": true, 24 | "disallowKeywordsOnNewLine": ["else"], 25 | "requireLineFeedAtFileEnd": true, 26 | "excludeFiles": ["node_modules/**", "bower_components/**"], 27 | "validateIndentation": 2 28 | } 29 | -------------------------------------------------------------------------------- /lib/actions/file.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var glob = require('glob'); 5 | var mkdirp = require('mkdirp'); 6 | 7 | /** 8 | * @mixin 9 | * @alias actions/file 10 | */ 11 | var file = module.exports; 12 | 13 | /** 14 | * Performs a glob search with the provided pattern and optional Hash of 15 | * options. Options can be any option supported by 16 | * [glob](https://github.com/isaacs/node-glob#options) 17 | * 18 | * @param {String} pattern 19 | * @param {Object} options 20 | */ 21 | 22 | file.expand = function expand(pattern, options) { 23 | return glob.sync(pattern, options); 24 | }; 25 | 26 | /** 27 | * Performs a glob search with the provided pattern and optional Hash of 28 | * options, filtering results to only return files (not directories). Options 29 | * can be any option supported by 30 | * [glob](https://github.com/isaacs/node-glob#options) 31 | * 32 | * @param {String} pattern 33 | * @param {Object} options 34 | */ 35 | 36 | file.expandFiles = function expandFiles(pattern, options) { 37 | options = options || {}; 38 | var cwd = options.cwd || process.cwd(); 39 | return this.expand(pattern, options).filter(function (filepath) { 40 | return fs.statSync(path.join(cwd, filepath)).isFile(); 41 | }); 42 | }; 43 | 44 | /** 45 | * Checks a given file path being absolute or not. 46 | * Borrowed from grunt's file API. 47 | */ 48 | 49 | file.isPathAbsolute = function () { 50 | var filepath = path.join.apply(path, arguments); 51 | return path.resolve(filepath) === filepath; 52 | }; 53 | 54 | file.mkdir = mkdirp.sync.bind(mkdirp); 55 | -------------------------------------------------------------------------------- /lib/actions/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | var shell = require('shelljs'); 4 | var githubUsername = require('github-username'); 5 | 6 | var nameCache = {}; 7 | var emailCache = {}; 8 | 9 | /** 10 | * @mixin 11 | * @alias actions/user 12 | */ 13 | var user = module.exports; 14 | 15 | user.git = {}; 16 | user.github = {}; 17 | 18 | /** 19 | * Retrieves user's name from Git in the global scope or the project scope 20 | * (it'll take what Git will use in the current context) 21 | */ 22 | 23 | user.git.name = function () { 24 | var name = nameCache[process.cwd()]; 25 | 26 | if (name) { 27 | return name; 28 | } 29 | 30 | if (shell.which('git')) { 31 | name = shell.exec('git config --get user.name', { silent: true }).output.trim(); 32 | nameCache[process.cwd()] = name; 33 | } 34 | 35 | return name; 36 | }; 37 | 38 | /** 39 | * Retrieves user's email from Git in the global scope or the project scope 40 | * (it'll take what Git will use in the current context) 41 | */ 42 | 43 | user.git.email = function () { 44 | var email = emailCache[process.cwd()]; 45 | 46 | if (email) { 47 | return email; 48 | } 49 | 50 | if (shell.which('git')) { 51 | email = shell.exec('git config --get user.email', { silent: true }).output.trim(); 52 | emailCache[process.cwd()] = email; 53 | } 54 | 55 | return email; 56 | }; 57 | 58 | /** 59 | * Retrieves GitHub's username from the GitHub API. 60 | */ 61 | 62 | user.github.username = function () { 63 | var args = _.toArray(arguments); 64 | args.unshift(user.git.email()); 65 | return githubUsername.apply(null, args); 66 | }; 67 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var gulp = require('gulp'); 4 | var mocha = require('gulp-mocha'); 5 | var jshint = require('gulp-jshint'); 6 | var jscs = require('gulp-jscs'); 7 | var eslint = require('gulp-eslint'); 8 | var istanbul = require('gulp-istanbul'); 9 | var coveralls = require('gulp-coveralls'); 10 | var plumber = require('gulp-plumber'); 11 | 12 | var handleErr = function (err) { 13 | console.log(err.message); 14 | process.exit(1); 15 | }; 16 | 17 | gulp.task('static', function () { 18 | return gulp.src([ 19 | 'test/*.js', 20 | 'lib/**/*.js', 21 | 'benchmark/**/*.js', 22 | 'index.js', 23 | 'doc.js', 24 | 'gulpfile.js' 25 | ]) 26 | .pipe(jshint('.jshintrc')) 27 | .pipe(jshint.reporter('jshint-stylish')) 28 | .pipe(jshint.reporter('fail')) 29 | .pipe(jscs()) 30 | .on('error', handleErr) 31 | .pipe(eslint()) 32 | .pipe(eslint.format()) 33 | .pipe(eslint.failOnError()); 34 | }); 35 | 36 | gulp.task('test', function (cb) { 37 | gulp.src([ 38 | 'lib/**/*.js', 39 | 'index.js' 40 | ]) 41 | .pipe(istanbul({ 42 | includeUntested: true 43 | })) 44 | .pipe(istanbul.hookRequire()) 45 | .on('finish', function () { 46 | gulp.src(['test/*.js']) 47 | .pipe(plumber()) 48 | .pipe(mocha({ 49 | reporter: 'spec' 50 | })) 51 | .pipe(istanbul.writeReports()) 52 | .on('end', cb); 53 | }); 54 | }); 55 | 56 | gulp.task('coveralls', ['test'], function () { 57 | if (!process.env.CI) return; 58 | return gulp.src(path.join(__dirname, 'coverage/lcov.info')) 59 | .pipe(coveralls()); 60 | }); 61 | 62 | gulp.task('default', ['static', 'test', 'coveralls']); 63 | -------------------------------------------------------------------------------- /lib/util/engines.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | 4 | // TODO(mklabs): 5 | // - handle cache 6 | // - implement adpaters for others engines (but do not add hard deps on them, 7 | // should require manual install for anything that is not an underscore 8 | // template) 9 | // - put in multiple files, possibly other packages. 10 | 11 | var engines = module.exports; 12 | 13 | // Underscore 14 | // ---------- 15 | 16 | // Underscore templates facade. 17 | // 18 | // Special kind of markers `<%%` for opening tags can be used to escape the 19 | // placeholder opening tag. This is often useful for templates including 20 | // snippet of templates you don't want to be interpolated. 21 | 22 | engines.underscore = function underscore(source, data, options) { 23 | source = source.replace(engines.underscore.options.matcher, function (m, content) { 24 | // let's add some funny markers to replace back when templating is done, 25 | // should be fancy enough to reduce frictions with files using markers like 26 | // this already. 27 | return '(;>%%<;)' + content + '(;>%<;)'; 28 | }); 29 | 30 | //let the user an option to use settings of _.template 31 | source = _.template(source, null, options)(data); 32 | 33 | source = source 34 | .replace(/\(;>%%<;\)/g, engines.underscore.options.start) 35 | .replace(/\(;>%<;\)/g, engines.underscore.options.end); 36 | 37 | return source; 38 | }; 39 | 40 | engines.underscore.options = { 41 | matcher: /<%%([^%]+)%>/g, 42 | detecter: /<%%?[^%]+%>/, 43 | start: '<%', 44 | end: '%>' 45 | }; 46 | 47 | engines.underscore.detect = function detect(body) { 48 | return engines.underscore.options.detecter.test(body); 49 | }; 50 | -------------------------------------------------------------------------------- /test/generators/test-quickstart.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, after, it, afterEach, beforeEach */ 2 | 'use strict'; 3 | var path = require('path'); 4 | var helpers = require('../..').test; 5 | 6 | describe('Quickstart generator test', function () { 7 | before(helpers.before(path.join(__dirname, './temp'))); 8 | 9 | it('runs sucessfully', function (done) { 10 | helpers.runGenerator('quickstart', done); 11 | }); 12 | 13 | it('creates expected files', function () { 14 | 15 | helpers.assertFile('.editorconfig'); 16 | 17 | helpers.assertFile('.gitattributes'); 18 | 19 | helpers.assertFile('.gitignore'); 20 | 21 | helpers.assertFile('.jshintrc'); 22 | 23 | helpers.assertFile('app/.htaccess'); 24 | 25 | helpers.assertFile('app/404.html'); 26 | 27 | helpers.assertFile('app/favicon.ico'); 28 | 29 | helpers.assertFile('app/index.html'); 30 | 31 | helpers.assertFile('app/robots.txt'); 32 | 33 | helpers.assertFile('app/scripts/vendor/jquery.min.js'); 34 | 35 | helpers.assertFile('app/scripts/vendor/modernizr.min.js'); 36 | 37 | helpers.assertFile('app/styles/main.css'); 38 | 39 | helpers.assertFile('Gruntfile.js'); 40 | 41 | helpers.assertFile('package.json'); 42 | 43 | helpers.assertFile('test/index.html'); 44 | 45 | helpers.assertFile('test/lib/chai.js'); 46 | 47 | helpers.assertFile('test/lib/expect.js'); 48 | 49 | helpers.assertFile('test/lib/mocha/mocha.css'); 50 | 51 | helpers.assertFile('test/lib/mocha/mocha.js'); 52 | 53 | helpers.assertFile('test/runner/mocha.js'); 54 | 55 | helpers.assertFile('test/spec/.gitkeep'); 56 | 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/fetch.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, beforeEach, it */ 2 | 'use strict'; 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var tmpdir = require('os').tmpdir(); 6 | var nock = require('nock'); 7 | var yeoman = require('yeoman-environment'); 8 | var generators = require('..'); 9 | var assert = generators.assert; 10 | var fetch = require('../lib/actions/fetch'); 11 | var TestAdapter = require('../lib/test/adapter').TestAdapter; 12 | 13 | var tmp = path.join(tmpdir, 'yeoman-fetch'); 14 | 15 | describe('generators.Base (actions/fetch)', function () { 16 | beforeEach(function () { 17 | this.dummy = new generators.Base({ 18 | env: yeoman.createEnv([], {}, new TestAdapter()), 19 | resolved: 'test:fetch' 20 | }); 21 | }); 22 | 23 | describe('#tarball()', function () { 24 | it('download and untar via the NPM download package', function (done) { 25 | var scope = nock('http://example.com') 26 | .get('/f.tar.gz') 27 | .replyWithFile(200, path.join(__dirname, 'fixtures/testFile.tar.gz')); 28 | 29 | this.dummy.tarball('http://example.com/f.tar.gz', tmp, function (err) { 30 | if (err) return done(err); 31 | assert(scope.isDone()); 32 | done(); 33 | }); 34 | }); 35 | 36 | it('aliases #extract()', function () { 37 | assert.equal(fetch.tarball, fetch.extract); 38 | }); 39 | }); 40 | 41 | describe('#fetch()', function () { 42 | it('allow the fetching of a single file', function (done) { 43 | var scope = nock('http://example.com') 44 | .get('/f.txt') 45 | .replyWithFile(200, path.join(__dirname, 'fixtures/help.txt')); 46 | 47 | this.dummy.fetch('http://example.com/f.txt', tmp, function (err) { 48 | if (err) return done(err); 49 | assert(scope.isDone()); 50 | fs.stat(path.join(tmp, 'f.txt'), done); 51 | }); 52 | }); 53 | }); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /lib/actions/fetch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | var Download = require('download'); 4 | var chalk = require('chalk'); 5 | 6 | /** 7 | * @mixin 8 | * @alias actions/fetch 9 | */ 10 | var fetch = module.exports; 11 | 12 | /** 13 | * Download a file to a given destination. 14 | * 15 | * @param {String} url 16 | * @param {String} destination 17 | * @param {Function} cb 18 | */ 19 | 20 | fetch.fetch = function _fetch(url, destination, cb) { 21 | var log = this.log('... Fetching %s ...', url); 22 | var download = new Download() 23 | .get(url) 24 | .dest(destination) 25 | .use(function (res) { 26 | res.on('data', function () { 27 | log.write('.'); 28 | }); 29 | }); 30 | 31 | download.run(function (err) { 32 | if (err) return cb(err); 33 | 34 | log.ok('Done in ' + destination).write(); 35 | cb(); 36 | }); 37 | }; 38 | 39 | /** 40 | * Fetch an archive and extract it to a given destination. 41 | * 42 | * @param {String} archive 43 | * @param {String} destination 44 | * @param {Object} opts 45 | * @param {Function} cb 46 | */ 47 | 48 | fetch.extract = function _extract(archive, destination, opts, cb) { 49 | if (_.isFunction(opts) && !cb) { 50 | cb = opts; 51 | opts = { extract: true }; 52 | } 53 | 54 | opts = _.assign({ extract: true }, opts); 55 | 56 | var log = this.log.write() 57 | .info('... Fetching %s ...', archive) 58 | .info(chalk.yellow('This might take a few moments')); 59 | 60 | var download = new Download(opts) 61 | .get(archive) 62 | .dest(destination) 63 | .use(function (res) { 64 | res.on('data', function () { 65 | log.write('.'); 66 | }); 67 | }); 68 | 69 | download.run(function (err) { 70 | if (err) return cb(err); 71 | 72 | log.write().ok('Done in ' + destination).write(); 73 | cb(); 74 | }); 75 | }; 76 | 77 | /** @alias fetch.extract */ 78 | fetch.tarball = fetch.extract; 79 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Generator [![Build Status](https://secure.travis-ci.org/yeoman/generator.svg?branch=master)](http://travis-ci.org/yeoman/generator) [![Coverage Status](https://coveralls.io/repos/yeoman/generator/badge.png)](https://coveralls.io/r/yeoman/generator) 2 | 3 | > A Rails-inspired generator system that provides scaffolding for your apps. 4 | 5 | 6 | ### [Getting Started](http://yeoman.io/authoring/getting-started.html)   [API Documentation](http://yeoman.github.io/generator/) 7 | 8 | 9 | ![Generator output](https://img.skitch.com/20120923-jxbn2njgk5dp7ttk94i1tx9ek2.png) 10 | 11 | ![Generator diff](https://img.skitch.com/20120922-kpjs68bgkshtsru4cwnb64fn82.png) 12 | 13 | 14 | ## Getting Started 15 | 16 | If you're interested in writing your own Yeoman generator we recommend reading [the official getting started guide](http://yeoman.io/authoring/). 17 | 18 | There are typically two types of generators - simple boilerplate 'copiers' and more advanced generators which can use custom prompts, remote dependencies, wiring and much more. 19 | 20 | The docs cover how to create generators from scratch as well as recommending command-line generators for making other generators. 21 | 22 | For deeper research, read the code source or visit our [API documentation](http://yeoman.github.io/generator/). 23 | 24 | 25 | ### Debugging 26 | 27 | To debug a generator, you can pass Node.js debug's flags by running it like this: 28 | 29 | ```sh 30 | # OS X / Linux 31 | node --debug `which yo` [arguments] 32 | 33 | # Windows 34 | node --debug [arguments] 35 | ``` 36 | 37 | Yeoman generators also use a debug mode to log relevant informations. You can activate it by setting the `DEBUG` environment variable to the desired scope (for the generator system scope is `generators:*`). 38 | 39 | ```sh 40 | # OS X / Linux 41 | DEBUG=generators/* 42 | 43 | # Windows 44 | set DEBUG=generators/* 45 | ``` 46 | 47 | 48 | ## License 49 | 50 | [BSD license](http://opensource.org/licenses/bsd-license.php) 51 | Copyright (c) Google 52 | -------------------------------------------------------------------------------- /lib/test/adapter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var inquirer = require('inquirer'); 5 | var sinon = require('sinon'); 6 | var events = require('events'); 7 | 8 | function DummyPrompt(answers, q) { 9 | this.answers = answers; 10 | this.question = q; 11 | } 12 | 13 | DummyPrompt.prototype.run = function (cb) { 14 | var answer = this.answers[this.question.name]; 15 | 16 | var isSet; 17 | switch (this.question.type) { 18 | case 'list': 19 | // list prompt accepts any answer value including null 20 | isSet = answer !== undefined; 21 | break; 22 | case 'confirm': 23 | // ensure that we don't replace `false` with default `true` 24 | isSet = answer || answer === false; 25 | break; 26 | default: 27 | // other prompts treat all falsy values to default 28 | isSet = !!answer; 29 | } 30 | 31 | if (!isSet) { 32 | answer = this.question.default; 33 | if (answer === undefined && this.question.type === 'confirm') { 34 | answer = true; 35 | } 36 | } 37 | 38 | setImmediate(function () { 39 | cb(answer); 40 | }); 41 | }; 42 | 43 | function TestAdapter(answers) { 44 | 45 | answers = answers || {}; 46 | 47 | this.prompt = inquirer.createPromptModule(); 48 | 49 | Object.keys(this.prompt.prompts).forEach(function (promptName) { 50 | this.prompt.registerPrompt(promptName, DummyPrompt.bind(DummyPrompt, answers)); 51 | }, this); 52 | 53 | this.diff = sinon.spy(); 54 | 55 | this.log = sinon.spy(); 56 | 57 | _.extend(this.log, events.EventEmitter.prototype); 58 | 59 | // make sure all log methods are defined 60 | [ 61 | 'write', 62 | 'writeln', 63 | 'ok', 64 | 'error', 65 | 'skip', 66 | 'force', 67 | 'create', 68 | 'invoke', 69 | 'conflict', 70 | 'identical', 71 | 'info', 72 | 'table' 73 | ].forEach(function (methodName) { 74 | this.log[methodName] = sinon.stub().returns(this.log); 75 | }, this); 76 | 77 | } 78 | 79 | module.exports = { 80 | DummyPrompt: DummyPrompt, 81 | TestAdapter: TestAdapter 82 | }; 83 | -------------------------------------------------------------------------------- /test/generators/test-bbb.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, after, it, afterEach, beforeEach */ 2 | 'use strict'; 3 | var path = require('path'); 4 | var helpers = require('../..').test; 5 | 6 | describe('Bbb generator test', function () { 7 | before(helpers.before(path.join(__dirname, './temp'))); 8 | 9 | it('runs sucessfully', function (done) { 10 | helpers.runGenerator('bbb', done); 11 | }); 12 | 13 | it('creates expected files', function () { 14 | 15 | helpers.assertFile('.editorconfig'); 16 | 17 | helpers.assertFile('.gitattributes'); 18 | 19 | helpers.assertFile('.gitignore'); 20 | 21 | helpers.assertFile('.jshintrc'); 22 | 23 | helpers.assertFile('app/.htaccess'); 24 | 25 | helpers.assertFile('app/404.html'); 26 | 27 | helpers.assertFile('app/favicon.ico'); 28 | 29 | helpers.assertFile('app/index.html'); 30 | 31 | helpers.assertFile('app/robots.txt'); 32 | 33 | helpers.assertFile('app/scripts/app.js'); 34 | 35 | helpers.assertFile('app/scripts/config.js'); 36 | 37 | helpers.assertFile('app/scripts/libs/almond.js'); 38 | 39 | helpers.assertFile('app/scripts/libs/backbone.js'); 40 | 41 | helpers.assertFile('app/scripts/libs/jquery.js'); 42 | 43 | helpers.assertFile('app/scripts/libs/lodash.js'); 44 | 45 | helpers.assertFile('app/scripts/libs/require.js'); 46 | 47 | helpers.assertFile('app/scripts/main.js'); 48 | 49 | helpers.assertFile('app/scripts/plugins/backbone.layoutmanager.js'); 50 | 51 | helpers.assertFile('app/scripts/router.js'); 52 | 53 | helpers.assertFile('app/styles/h5bp.css'); 54 | 55 | helpers.assertFile('app/styles/index.css'); 56 | 57 | helpers.assertFile('Gruntfile.js'); 58 | 59 | helpers.assertFile('package.json'); 60 | 61 | helpers.assertFile('test/index.html'); 62 | 63 | helpers.assertFile('test/lib/chai.js'); 64 | 65 | helpers.assertFile('test/lib/expect.js'); 66 | 67 | helpers.assertFile('test/lib/mocha/mocha.css'); 68 | 69 | helpers.assertFile('test/lib/mocha/mocha.js'); 70 | 71 | helpers.assertFile('test/runner/mocha.js'); 72 | 73 | helpers.assertFile('test/spec/example.js'); 74 | 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module yeoman-generator 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var Environment = require('yeoman-environment'); 8 | 9 | /** 10 | * The generator system is a framework for node to author reusable and 11 | * composable Generators, for a vast majority of use-case. 12 | * 13 | * Inspired and based off the work done on Thor and Rails 3 Generators, we try 14 | * to provide the same kind of infrastructure. 15 | * 16 | * Generators are registered by namespace, where namespaces are mapping the 17 | * structure of the file system with `:` being simply converted to `/`. 18 | * 19 | * Generators are standard node modules, they are simply required as usual, and 20 | * they can be shipped into reusable npm packages. 21 | * 22 | * The lookup is done depending on the configured load path, which is by 23 | * default `lib/generators` in every generators package installed (ie. 24 | * `node_modules/yeoman-backbone/lib/generators`) 25 | * 26 | * @example 27 | * var yeoman = require('yeoman-generators'); 28 | * 29 | * var env = yeoman('angular:model') 30 | * .run(function(err) { 31 | * console.log('done!'); 32 | * }); 33 | * 34 | * @alias module:yeoman-generator 35 | */ 36 | 37 | var yeoman = module.exports = function createEnv() { 38 | return Environment.createEnv.apply(Environment, arguments); 39 | }; 40 | 41 | /** 42 | * Global file helpers methods 43 | * {@link https://github.com/SBoudrias/file-utils} 44 | */ 45 | yeoman.file = require('file-utils'); 46 | 47 | // hoist up top level class the generator extend 48 | yeoman.Base = require('./lib/base'); 49 | yeoman.NamedBase = require('./lib/named-base'); 50 | 51 | /** 52 | * Test helpers 53 | * {@link module:test/helpers} 54 | */ 55 | yeoman.test = require('./lib/test/helpers'); 56 | 57 | /** 58 | * Test assertions helpers 59 | * {@link https://github.com/yeoman/yeoman-assert} 60 | */ 61 | yeoman.assert = require('yeoman-assert'); 62 | 63 | /** 64 | * Yeoman base's generators 65 | * @enum generators 66 | */ 67 | yeoman.generators = { 68 | 69 | /** 70 | * Base Generator 71 | * {@link Base} 72 | */ 73 | Base: yeoman.Base, 74 | 75 | /** 76 | * Named Base Generator 77 | * {@link NamedBase} 78 | */ 79 | NamedBase: yeoman.NamedBase 80 | }; 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yeoman-generator", 3 | "version": "0.18.5", 4 | "description": "Rails-inspired generator system that provides scaffolding for your apps", 5 | "license": "BSD", 6 | "keywords": [ 7 | "development", 8 | "dev", 9 | "build", 10 | "tool", 11 | "cli", 12 | "scaffold", 13 | "scaffolding", 14 | "generate", 15 | "generator", 16 | "yeoman", 17 | "app" 18 | ], 19 | "homepage": "http://yeoman.io", 20 | "author": "The Yeoman Team", 21 | "repository": "yeoman/generator", 22 | "engines": { 23 | "node": ">=0.10.0" 24 | }, 25 | "scripts": { 26 | "test": "gulp", 27 | "doc": "jsdoc -c ./jsdoc.json ./readme.md", 28 | "test-generator": "mocha test/generators/*.js --reporter spec --timeout 100000", 29 | "prepublish": "nsp audit-package" 30 | }, 31 | "dependencies": { 32 | "async": "^0.9.0", 33 | "chalk": "^0.5.1", 34 | "cheerio": "^0.18.0", 35 | "class-extend": "^0.1.0", 36 | "cross-spawn": "^0.2.3", 37 | "dargs": "^3.0.0", 38 | "debug": "^2.1.0", 39 | "detect-conflict": "^1.0.0", 40 | "diff": "^1.0.4", 41 | "download": "^3.1.2", 42 | "file-utils": "^0.2.0", 43 | "findup-sync": "^0.1.2", 44 | "github-username": "^1.0.0", 45 | "glob": "4.2.2", 46 | "gruntfile-editor": "^0.2.0", 47 | "inquirer": "^0.8.0", 48 | "isbinaryfile": "^2.0.0", 49 | "lodash": "^2.4.1", 50 | "mem-fs-editor": "^1.0.0", 51 | "mime": "^1.2.9", 52 | "mkdirp": "^0.5.0", 53 | "nopt": "^3.0.0", 54 | "rimraf": "^2.2.0", 55 | "run-async": "^0.1.0", 56 | "shelljs": "^0.3.0", 57 | "sinon": "^1.9.1", 58 | "text-table": "^0.2.0", 59 | "through2": "^0.6.3", 60 | "underscore.string": "^2.3.1", 61 | "user-home": "^1.1.0", 62 | "xdg-basedir": "^1.0.0", 63 | "yeoman-assert": "^1.0.0", 64 | "yeoman-environment": "^1.1.0" 65 | }, 66 | "devDependencies": { 67 | "gulp": "^3.6.0", 68 | "gulp-coveralls": "^0.1.0", 69 | "gulp-eslint": "~0.1.8", 70 | "gulp-istanbul": "^0.5.0", 71 | "gulp-jscs": "^1.1.0", 72 | "gulp-jshint": "^1.5.3", 73 | "gulp-mocha": "^2.0.0", 74 | "gulp-plumber": "~0.6.6", 75 | "jshint-stylish": "^1.0.0", 76 | "mockery": "^1.4.0", 77 | "nock": "^0.51.0", 78 | "nsp": "*", 79 | "proxyquire": "^1.0.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/invoke.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before, after, beforeEach, afterEach */ 2 | 'use strict'; 3 | var yeoman = require('yeoman-environment'); 4 | var generators = require('..'); 5 | var TestAdapter = require('../lib/test/adapter').TestAdapter; 6 | var helpers = generators.test; 7 | var assert = generators.assert; 8 | 9 | describe('generators.Base#invoke()', function () { 10 | beforeEach(function () { 11 | this.env = yeoman.createEnv([], {}, new TestAdapter()); 12 | this.Generator = helpers.createDummyGenerator(); 13 | this.gen = new this.Generator({ 14 | namespace: 'foo:lab', 15 | resolved: 'path/', 16 | env: this.env, 17 | 'skip-install': true 18 | }); 19 | this.SubGen = generators.Base.extend({ 20 | exec: function () { this.stubGenRunned = true; } 21 | }); 22 | this.env.registerStub(this.SubGen, 'foo:bar'); 23 | }); 24 | 25 | it('invoke available generators', function (done) { 26 | var invoked = this.gen.invoke('foo:bar', { 27 | options: { 'skip-install': true } 28 | }); 29 | invoked.on('end', function () { 30 | assert(invoked.stubGenRunned); 31 | done(); 32 | }); 33 | }); 34 | 35 | it('accept a callback argument', function (done) { 36 | var invoked = this.gen.invoke('foo:bar', { 37 | options: { 'skip-install': true } 38 | }, function () { 39 | assert(invoked.stubGenRunned); 40 | done(); 41 | }); 42 | }); 43 | 44 | it('works when invoked from runLoop', function (done) { 45 | var stubGenFinished = false; 46 | var running = 0; 47 | 48 | var asyncFunc = function () { 49 | var cb = this.async(); 50 | running++; 51 | setTimeout(function () { 52 | assert.equal(running, 1); 53 | running--; 54 | cb(); 55 | }, 5); 56 | }; 57 | 58 | this.SubGen.prototype.asyncFunc = asyncFunc; 59 | this.gen.constructor.prototype.asyncFunc = asyncFunc; 60 | 61 | this.gen.constructor.prototype.initializing = function () { 62 | var cb = this.async(); 63 | var invoked = this.invoke('foo:bar', { 64 | options: { 'skip-install': true } 65 | }, function () { 66 | stubGenFinished = true; 67 | assert(invoked.stubGenRunned, 'Stub generator should have runned'); 68 | cb(); 69 | }); 70 | }; 71 | 72 | this.gen.run(function () { 73 | assert(stubGenFinished, 'invoke callback should have been called'); 74 | done(); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/generators/test-ember.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, after, it, afterEach, beforeEach */ 2 | 'use strict'; 3 | var path = require('path'); 4 | var helpers = require('../..').test; 5 | 6 | describe('Ember generator test', function () { 7 | before(helpers.before(path.join(__dirname, './temp'))); 8 | 9 | it('runs sucessfully', function (done) { 10 | helpers.runGenerator('ember', done); 11 | }); 12 | 13 | it('creates expected files', function () { 14 | 15 | helpers.assertFile('app/scripts/models'); 16 | 17 | helpers.assertFile('app/scripts/controllers'); 18 | 19 | helpers.assertFile('app/scripts/views'); 20 | 21 | helpers.assertFile('app/scripts/routes'); 22 | 23 | helpers.assertFile('app/scripts/helpers'); 24 | 25 | helpers.assertFile('app/scripts/templates'); 26 | 27 | helpers.assertFile('app/scripts/main.js'); 28 | 29 | helpers.assertFile('app/scripts/routes/app-router.js'); 30 | 31 | helpers.assertFile('app/scripts/store.js'); 32 | 33 | helpers.assertFile('.gitattributes'); 34 | 35 | helpers.assertFile('.gitignore'); 36 | 37 | helpers.assertFile('app/.htaccess'); 38 | 39 | helpers.assertFile('app/404.html'); 40 | 41 | helpers.assertFile('app/favicon.ico'); 42 | 43 | helpers.assertFile('app/index.html'); 44 | 45 | helpers.assertFile('app/robots.txt'); 46 | 47 | helpers.assertFile('app/scripts/vendor/ember-1.0.pre.min.js'); 48 | 49 | helpers.assertFile('app/scripts/vendor/handlebars-1.0.0.beta.6.js'); 50 | 51 | helpers.assertFile('app/scripts/vendor/jquery.min.js'); 52 | 53 | helpers.assertFile('app/styles/main.css'); 54 | 55 | helpers.assertFile('Gruntfile.js'); 56 | 57 | helpers.assertFile('package.json'); 58 | 59 | helpers.assertFile('test/index.html'); 60 | 61 | helpers.assertFile('test/lib/chai.js'); 62 | 63 | helpers.assertFile('test/lib/expect.js'); 64 | 65 | helpers.assertFile('test/lib/mocha-1.2.2/mocha.css'); 66 | 67 | helpers.assertFile('test/lib/mocha-1.2.2/mocha.js'); 68 | 69 | helpers.assertFile('test/runner/mocha.js'); 70 | 71 | helpers.assertFile('app/scripts/views/application-view.js'); 72 | 73 | helpers.assertFile('app/scripts/templates/application.handlebars'); 74 | 75 | helpers.assertFile('app/scripts/models/application-model.js'); 76 | 77 | helpers.assertFile('app/scripts/controllers/application-controller.js'); 78 | 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/user.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, it, after, before, beforeEach, afterEach */ 2 | 'use strict'; 3 | var assert = require('assert'); 4 | var mkdirp = require('mkdirp'); 5 | var nock = require('nock'); 6 | var os = require('os'); 7 | var path = require('path'); 8 | var proxyquire = require('proxyquire'); 9 | var rimraf = require('rimraf'); 10 | var shell = require('shelljs'); 11 | var sinon = require('sinon'); 12 | var tmpdir = path.join(os.tmpdir(), 'yeoman-user'); 13 | var generators = require('..'); 14 | 15 | describe('generators.Base#user', function () { 16 | before(function () { 17 | this.prevCwd = process.cwd(); 18 | this.tmp = tmpdir; 19 | mkdirp.sync(path.join(tmpdir, 'subdir')); 20 | process.chdir(tmpdir); 21 | shell.exec('git init --quiet'); 22 | shell.exec('git config --local user.name Yeoman'); 23 | shell.exec('git config --local user.email yo@yeoman.io'); 24 | }); 25 | 26 | after(function (done) { 27 | process.chdir(this.prevCwd); 28 | rimraf(tmpdir, done); 29 | }); 30 | 31 | beforeEach(function () { 32 | process.chdir(this.tmp); 33 | this.shell = shell; 34 | sinon.spy(this.shell, 'exec'); 35 | 36 | this.user = proxyquire('../lib/actions/user', { 37 | shelljs: this.shell 38 | }); 39 | }); 40 | 41 | afterEach(function () { 42 | this.shell.exec.restore(); 43 | }); 44 | 45 | it('is exposed on the Base generator', function () { 46 | assert.equal(require('../lib/actions/user'), generators.Base.prototype.user); 47 | }); 48 | 49 | describe('.git', function () { 50 | 51 | describe('.name()', function () { 52 | it('is the name used by git', function () { 53 | assert.equal(this.user.git.name(), 'Yeoman'); 54 | }); 55 | 56 | it('cache the value', function () { 57 | this.user.git.name(); 58 | this.user.git.name(); 59 | assert.equal(this.shell.exec.callCount, 1); 60 | }); 61 | 62 | it('cache is linked to the CWD', function () { 63 | this.user.git.name(); 64 | process.chdir('subdir'); 65 | this.user.git.name(); 66 | assert.equal(this.shell.exec.callCount, 2); 67 | }); 68 | }); 69 | 70 | describe('.email()', function () { 71 | it('is the email used by git', function () { 72 | assert.equal(this.user.git.email(), 'yo@yeoman.io'); 73 | }); 74 | 75 | it('handle cache', function () { 76 | this.user.git.email(); 77 | this.user.git.email(); 78 | assert.equal(this.shell.exec.callCount, 1); 79 | }); 80 | 81 | it('cache is linked to the CWD', function () { 82 | this.user.git.email(); 83 | process.chdir('subdir'); 84 | this.user.git.email(); 85 | assert.equal(this.shell.exec.callCount, 2); 86 | }); 87 | }); 88 | }); 89 | 90 | describe('.github', function () { 91 | describe('.username()', function () { 92 | beforeEach(function () { 93 | nock('https://api.github.com') 94 | .filteringPath(/q=[^&]*/g, 'q=XXX') 95 | .get('/search/users?q=XXX') 96 | .times(1) 97 | .reply(200, { 98 | items: [ 99 | { login: 'mockname' } 100 | ] 101 | }); 102 | }); 103 | 104 | afterEach(function () { 105 | nock.restore(); 106 | }); 107 | 108 | it('is the username used by GitHub', function (done) { 109 | this.user.github.username(function (err, res) { 110 | assert.equal(res, 'mockname'); 111 | done(); 112 | }); 113 | }); 114 | }); 115 | 116 | }); 117 | 118 | }); 119 | -------------------------------------------------------------------------------- /test/generators/test-backbone.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, after, it, afterEach, beforeEach */ 2 | 'use strict'; 3 | var path = require('path'); 4 | var helpers = require('../..').test; 5 | 6 | describe('Backbone generator test', function () { 7 | before(helpers.before(path.join(__dirname, './temp'))); 8 | 9 | it('runs sucessfully', function (done) { 10 | helpers.runGenerator('backbone', done); 11 | }); 12 | 13 | it('creates expected files', function () { 14 | 15 | helpers.assertFile('app/scripts/models'); 16 | 17 | helpers.assertFile('app/scripts/collections'); 18 | 19 | helpers.assertFile('app/scripts/views'); 20 | 21 | helpers.assertFile('app/scripts/routes'); 22 | 23 | helpers.assertFile('app/scripts/helpers'); 24 | 25 | helpers.assertFile('app/scripts/templates'); 26 | 27 | helpers.assertFile('app/scripts/main.js'); 28 | 29 | helpers.assertFile('.gitattributes'); 30 | 31 | helpers.assertFile('.gitignore'); 32 | 33 | helpers.assertFile('app/.htaccess'); 34 | 35 | helpers.assertFile('app/404.html'); 36 | 37 | helpers.assertFile('app/favicon.ico'); 38 | 39 | helpers.assertFile('app/index.html'); 40 | 41 | helpers.assertFile('app/robots.txt'); 42 | 43 | helpers.assertFile('app/scripts/vendor/backbone-min.js'); 44 | 45 | helpers.assertFile('app/scripts/vendor/jquery.min.js'); 46 | 47 | helpers.assertFile('app/scripts/vendor/lodash.min.js'); 48 | 49 | helpers.assertFile('app/styles/main.css'); 50 | 51 | helpers.assertFile('Gruntfile.js'); 52 | 53 | helpers.assertFile('package.json'); 54 | 55 | helpers.assertFile('test/index.html'); 56 | 57 | helpers.assertFile('test/lib/chai.js'); 58 | 59 | helpers.assertFile('test/lib/expect.js'); 60 | 61 | helpers.assertFile('test/lib/mocha/mocha.css'); 62 | 63 | helpers.assertFile('test/lib/mocha/mocha.js'); 64 | 65 | helpers.assertFile('test/runner/mocha.js'); 66 | 67 | helpers.assertFile('app/scripts/routes/application-router.js'); 68 | 69 | helpers.assertFile('app/scripts/views/application-view.js'); 70 | 71 | helpers.assertFile('app/scripts/templates/application.ejs'); 72 | 73 | helpers.assertFile('app/scripts/models/application-model.js'); 74 | 75 | helpers.assertFile('app/scripts/collections/application-collection.js'); 76 | 77 | }); 78 | 79 | it('runs sucessfully with --coffee as argument', function (done) { 80 | helpers.runGenerator('backbone', { coffee: true }, done); 81 | }); 82 | 83 | it('creates expected files when run with --coffee as argument', function () { 84 | helpers.assertFile('app/scripts/main.coffee'); 85 | 86 | helpers.assertFile('app/scripts/routes/application-router.coffee'); 87 | 88 | helpers.assertFile('app/scripts/views/application-view.coffee'); 89 | 90 | helpers.assertFile('app/scripts/models/application-model.coffee'); 91 | 92 | helpers.assertFile('app/scripts/collections/application-collection.coffee'); 93 | }); 94 | 95 | it('runs successfully with --test-framework as argument', function (done) { 96 | helpers.runGenerator('backbone', { 'test-framework': 'jasmine' }, done); 97 | }); 98 | 99 | it('creates jasmine files when run with --test-framework', function () { 100 | helpers.assertFile('test/runner/headless.js'); 101 | 102 | helpers.assertFile('test/runner/html.js'); 103 | 104 | helpers.assertFile('test/lib/jasmine-1.2.0/jasmine.css'); 105 | 106 | helpers.assertFile('test/lib/jasmine-1.2.0/jasmine-html.js'); 107 | 108 | helpers.assertFile('test/lib/jasmine-1.2.0/jasmine.js'); 109 | 110 | helpers.assertFile('test/spec/'); 111 | 112 | helpers.assertFile('test/spec/introduction.js'); 113 | }); 114 | 115 | }); 116 | -------------------------------------------------------------------------------- /lib/actions/help.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | var _ = require('lodash'); 5 | var table = require('text-table'); 6 | 7 | /** 8 | * @mixin 9 | * @alias actions/help 10 | */ 11 | var help = module.exports; 12 | 13 | /** 14 | * Tries to get the description from a USAGE file one folder above the 15 | * source root otherwise uses a default description. 16 | */ 17 | 18 | help.help = function help() { 19 | var filepath = path.join(this.sourceRoot(), '../USAGE'); 20 | var exists = fs.existsSync(filepath); 21 | 22 | var out = [ 23 | 'Usage:', 24 | ' ' + this.usage(), 25 | '' 26 | ]; 27 | 28 | // build options 29 | if (Object.keys(this._options).length) { 30 | out = out.concat([ 31 | 'Options:', 32 | this.optionsHelp(), 33 | '' 34 | ]); 35 | } 36 | 37 | // build arguments 38 | if (this._arguments.length) { 39 | out = out.concat([ 40 | 'Arguments:', 41 | this.argumentsHelp(), 42 | '' 43 | ]); 44 | } 45 | 46 | // append USAGE file is any 47 | if (exists) { 48 | out.push(fs.readFileSync(filepath, 'utf8')); 49 | } 50 | 51 | return out.join('\n'); 52 | }; 53 | 54 | function formatArg(argItem) { 55 | var arg = '<' + argItem.name + '>'; 56 | 57 | if (!argItem.config.required) { 58 | arg = '[' + arg + ']'; 59 | } 60 | 61 | return arg; 62 | } 63 | 64 | /** 65 | * Output usage information for this given generator, depending on its arguments, 66 | * options or hooks. 67 | */ 68 | 69 | help.usage = function usage() { 70 | var options = Object.keys(this._options).length ? '[options]' : ''; 71 | var name = ' ' + this.options.namespace; 72 | var args = ''; 73 | 74 | if (this._arguments.length) { 75 | args = this._arguments.map(formatArg).join(' '); 76 | } 77 | 78 | name = name.replace(/^yeoman:/, ''); 79 | 80 | var out = 'yo' + name + ' ' + options + ' ' + args; 81 | 82 | if (this.description) { 83 | out += '\n\n' + this.description; 84 | } 85 | 86 | return out; 87 | }; 88 | 89 | /** 90 | * Simple setter for custom `description` to append on help output. 91 | * 92 | * @param {String} description 93 | */ 94 | 95 | help.desc = function desc(description) { 96 | this.description = description || ''; 97 | return this; 98 | }; 99 | 100 | /** 101 | * Get help text for arguments 102 | * @returns {String} Text of options in formatted table 103 | */ 104 | help.argumentsHelp = function argumentsHelp() { 105 | var rows = this._arguments.map(function (arg) { 106 | var config = arg.config; 107 | return [ 108 | '', 109 | arg.name ? arg.name : '', 110 | config.desc ? '# ' + config.desc : '', 111 | config.type ? 'Type: ' + config.type.name : '', 112 | 'Required: ' + config.required 113 | ]; 114 | }); 115 | 116 | return table(rows); 117 | }; 118 | 119 | /** 120 | * Get help text for options 121 | * @returns {String} Text of options in formatted table 122 | */ 123 | help.optionsHelp = function optionsHelp() { 124 | var options = _.reject(this._options, function (el) { 125 | return el.hide; 126 | }); 127 | 128 | var hookOpts = this._hooks.map(function (hook) { 129 | return hook.generator && hook.generator._options; 130 | }).reduce(function (a, b) { 131 | a = a.concat(b); 132 | return a; 133 | }, []).filter(function (opts) { 134 | return opts && opts.name !== 'help'; 135 | }); 136 | 137 | var rows = options.concat(hookOpts).map(function (opt) { 138 | var defaults = opt.defaults; 139 | return [ 140 | '', 141 | opt.alias ? '-' + opt.alias + ', ' : '', 142 | '--' + opt.name, 143 | opt.desc ? '# ' + opt.desc : '', 144 | defaults == null || defaults === '' ? '' : 'Default: ' + defaults 145 | ]; 146 | }); 147 | 148 | return table(rows); 149 | }; 150 | -------------------------------------------------------------------------------- /lib/util/storage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var assert = require('assert'); 4 | var _ = require('lodash'); 5 | 6 | /** 7 | * Storage instances handle a json file where Generator authors can store data. 8 | * 9 | * `Base` instantiate the storage as `config` by default. 10 | * 11 | * @constructor 12 | * @param {String} name The name of the new storage (this is a namespace) 13 | * @param {mem-fs-editor} fs A mem-fs editor instance 14 | * @param {String} configPath The filepath used as a storage. `.yo-rc.json` is used 15 | * by default 16 | * 17 | * @example 18 | * var MyGenerator = yeoman.generators.base.extend({ 19 | * config: function() { 20 | * this.config.set('coffeescript', false); 21 | * } 22 | * }); 23 | */ 24 | 25 | var Storage = module.exports = function Storage(name, fs, configPath) { 26 | assert(name, 'A name parameter is required to create a storage'); 27 | 28 | this.path = configPath || path.join(process.cwd(), '.yo-rc.json'); 29 | this.name = name; 30 | this.fs = fs; 31 | 32 | this.existed = Object.keys(this._store()).length > 0; 33 | }; 34 | 35 | /** 36 | * Return the current store as JSON object 37 | * @private 38 | * @return {Object} the store content 39 | */ 40 | Storage.prototype._store = function () { 41 | try { 42 | return this.fs.readJSON(this.path)[this.name] || {}; 43 | } catch (e) { 44 | return {}; 45 | } 46 | }; 47 | 48 | /** 49 | * Save a new object of values 50 | * @param {Object} val - Store new state 51 | * @return {null} 52 | */ 53 | 54 | Storage.prototype.save = function (val) { 55 | var fullStore; 56 | try { 57 | fullStore = this.fs.readJSON(this.path); 58 | } catch (e) { 59 | fullStore = {}; 60 | } 61 | fullStore[this.name] = val; 62 | this.fs.write(this.path, JSON.stringify(fullStore, null, ' ')); 63 | }; 64 | 65 | /** 66 | * Alias to save. 67 | * @deprecated don't use save yourself. 68 | * @return {null} 69 | */ 70 | 71 | Storage.prototype.forceSave = function (val) { this.save(val); }; 72 | 73 | /** 74 | * Get a stored value 75 | * @param {String} key The key under which the value is stored. 76 | * @return {*} The stored value. Any JSON valid type could be returned 77 | */ 78 | 79 | Storage.prototype.get = function (key) { 80 | return this._store()[key]; 81 | }; 82 | 83 | /** 84 | * Get all the stored values 85 | * @return {Object} key-value object 86 | */ 87 | 88 | Storage.prototype.getAll = function () { 89 | return _.cloneDeep(this._store()); 90 | }; 91 | 92 | /** 93 | * Assign a key to a value and schedule a save. 94 | * @param {String} key The key under which the value is stored 95 | * @param {*} val Any valid JSON type value (String, Number, Array, Object). 96 | * @return {*} val Whatever was passed in as val. 97 | */ 98 | 99 | Storage.prototype.set = function (key, val) { 100 | assert(!_.isFunction(val), 'Storage value can\'t be a function'); 101 | 102 | var store = this._store(); 103 | 104 | if (_.isObject(key)) { 105 | val = _.extend(store, key); 106 | } else { 107 | store[key] = val; 108 | } 109 | this.save(store); 110 | return val; 111 | }; 112 | 113 | /** 114 | * Delete a key from the store and schedule a save. 115 | * @param {String} key The key under which the value is stored. 116 | * @return {null} 117 | */ 118 | 119 | Storage.prototype.delete = function (key) { 120 | var store = this._store(); 121 | delete store[key]; 122 | this.save(store); 123 | }; 124 | 125 | /** 126 | * Setup the store with defaults value and schedule a save. 127 | * If keys already exist, the initial value is kept. 128 | * @param {Object} defaults Key-value object to store. 129 | * @return {*} val Returns the merged options. 130 | */ 131 | 132 | Storage.prototype.defaults = function (defaults) { 133 | assert(_.isObject(defaults), 'Storage `defaults` method only accept objects'); 134 | var val = _.defaults(this.getAll(), defaults); 135 | this.set(val); 136 | return val; 137 | }; 138 | -------------------------------------------------------------------------------- /test/conflicter.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, after, it, beforeEach, afterEach */ 2 | 'use strict'; 3 | var assert = require('assert'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var _ = require('lodash'); 7 | var sinon = require('sinon'); 8 | var Conflicter = require('../lib/util/conflicter'); 9 | var TestAdapter = require('../lib/test/adapter').TestAdapter; 10 | 11 | describe('Conflicter', function () { 12 | beforeEach(function () { 13 | this.conflicter = new Conflicter(new TestAdapter()); 14 | }); 15 | 16 | it('#checkForCollision()', function () { 17 | var spy = sinon.spy(); 18 | var contents = fs.readFileSync(__filename, 'utf8'); 19 | this.conflicter.checkForCollision(__filename, contents, spy); 20 | 21 | var conflict = this.conflicter.conflicts.pop(); 22 | assert.deepEqual(conflict.file.path, __filename); 23 | assert.deepEqual(conflict.file.contents, fs.readFileSync(__filename, 'utf8')); 24 | assert.deepEqual(conflict.callback, spy); 25 | }); 26 | 27 | describe('#resolve()', function () { 28 | it('wihout conflict', function (done) { 29 | this.conflicter.resolve(done); 30 | }); 31 | 32 | it('with a conflict', function (done) { 33 | var spy = sinon.spy(); 34 | this.conflicter.force = true; 35 | 36 | this.conflicter.checkForCollision(__filename, fs.readFileSync(__filename), spy); 37 | this.conflicter.checkForCollision('foo.js', 'var foo = "foo";\n', spy); 38 | 39 | this.conflicter.resolve(function () { 40 | assert.equal(spy.callCount, 2); 41 | assert.equal(this.conflicter.conflicts.length, 0, 'Expected conflicter to be empty after running'); 42 | done(); 43 | }.bind(this)); 44 | }); 45 | }); 46 | 47 | describe('#collision()', function () { 48 | beforeEach(function () { 49 | this.conflictingFile = { path: __filename, contents: '' }; 50 | }); 51 | 52 | it('identical status', function (done) { 53 | var me = fs.readFileSync(__filename, 'utf8'); 54 | this.conflicter.collision({ 55 | path: __filename, 56 | contents: me 57 | }, function (status) { 58 | assert.equal(status, 'identical'); 59 | done(); 60 | }); 61 | }); 62 | 63 | it('create status', function (done) { 64 | this.conflicter.collision({ 65 | path: 'file-who-does-not-exist.js', 66 | contents: '' 67 | }, function (status) { 68 | assert.equal(status, 'create'); 69 | done(); 70 | }); 71 | }); 72 | 73 | it('user choose "yes"', function (done) { 74 | var conflicter = new Conflicter(new TestAdapter({ action: 'write' })); 75 | conflicter.collision(this.conflictingFile, function (status) { 76 | assert.equal(status, 'force'); 77 | done(); 78 | }); 79 | }); 80 | 81 | it('user choose "skip"', function (done) { 82 | var conflicter = new Conflicter(new TestAdapter({ action: 'skip' })); 83 | conflicter.collision(this.conflictingFile, function (status) { 84 | assert.equal(status, 'skip'); 85 | done(); 86 | }); 87 | }); 88 | 89 | it('user choose "force"', function (done) { 90 | var conflicter = new Conflicter(new TestAdapter({ action: 'force' })); 91 | conflicter.collision(this.conflictingFile, function (status) { 92 | assert.equal(status, 'force'); 93 | done(); 94 | }); 95 | }); 96 | 97 | it('force conflict status', function (done) { 98 | this.conflicter.force = true; 99 | this.conflicter.collision(this.conflictingFile, function (status) { 100 | assert.equal(status, 'force'); 101 | done(); 102 | }); 103 | }); 104 | 105 | it('does not give a conflict on same binary files', function (done) { 106 | this.conflicter.collision({ 107 | path: path.join(__dirname, 'fixtures/yeoman-logo.png'), 108 | contents: fs.readFileSync(path.join(__dirname, 'fixtures/yeoman-logo.png')) 109 | }, function (status) { 110 | assert.equal(status, 'identical'); 111 | done(); 112 | }.bind(this)); 113 | }); 114 | 115 | it('does not provide a diff option for directory', function (done) { 116 | var conflicter = new Conflicter(new TestAdapter({ action: 'write' })); 117 | var spy = sinon.spy(conflicter.adapter, 'prompt'); 118 | conflicter.collision({ 119 | path: __dirname, 120 | contents: null 121 | }, function (status) { 122 | assert.equal( 123 | _.where(spy.firstCall.args[0][0].choices, { value: 'diff' }).length, 124 | 0 125 | ); 126 | done(); 127 | }); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /test/install.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before, after, beforeEach, afterEach */ 2 | 'use strict'; 3 | var yeoman = require('yeoman-environment'); 4 | var generators = require('..'); 5 | var helpers = generators.test; 6 | var TestAdapter = require('../lib/test/adapter').TestAdapter; 7 | var sinon = require('sinon'); 8 | 9 | var asyncStub = { 10 | on: function (key, cb) { 11 | if (key === 'exit') { 12 | cb(); 13 | } 14 | return asyncStub; 15 | } 16 | }; 17 | 18 | describe('generators.Base (actions/install mixin)', function () { 19 | beforeEach(function () { 20 | this.env = yeoman.createEnv([], {}, new TestAdapter()); 21 | this.env.registerStub(helpers.createDummyGenerator(), 'dummy'); 22 | this.dummy = this.env.create('dummy'); 23 | 24 | // Keep track of all commands executed by spawnCommand. 25 | this.spawnCommandStub = sinon.stub(this.dummy, 'spawnCommand'); 26 | this.spawnCommandStub.returns(asyncStub); 27 | }); 28 | 29 | describe('#runInstall()', function () { 30 | it('takes a config object and passes it to the spawned process', function (done) { 31 | var spawnEnv = { 32 | env: { 33 | PATH: '/path/to/bin' 34 | } 35 | }; 36 | var callbackSpy = sinon.spy(); 37 | //args: installer, paths, options, cb 38 | this.dummy.runInstall('nestedScript', ['path1', 'path2'], spawnEnv, callbackSpy); 39 | this.dummy.run(function () { 40 | sinon.assert.calledWithExactly( 41 | this.spawnCommandStub, 42 | 'nestedScript', 43 | ['install', 'path1', 'path2'], 44 | spawnEnv 45 | ); 46 | sinon.assert.calledOnce(callbackSpy); 47 | done(); 48 | }.bind(this)); 49 | }); 50 | }); 51 | 52 | describe('#bowerInstall()', function () { 53 | it('spawn a bower process once per commands', function (done) { 54 | this.dummy.bowerInstall(); 55 | this.dummy.bowerInstall(); 56 | this.dummy.run(function () { 57 | sinon.assert.calledOnce(this.spawnCommandStub); 58 | sinon.assert.calledWithExactly(this.spawnCommandStub, 'bower', ['install'], {}); 59 | done(); 60 | }.bind(this)); 61 | }); 62 | 63 | it('spawn a bower process with formatted options', function (done) { 64 | this.dummy.bowerInstall('jquery', { saveDev: true }, function () { 65 | sinon.assert.calledOnce(this.spawnCommandStub); 66 | sinon.assert.calledWithExactly( 67 | this.spawnCommandStub, 68 | 'bower', 69 | ['install', 'jquery', '--save-dev'], 70 | { saveDev: true } 71 | ); 72 | done(); 73 | }.bind(this)); 74 | this.dummy.run(); 75 | }); 76 | }); 77 | 78 | describe('#npmInstall()', function () { 79 | it('spawn an install process once per commands', function (done) { 80 | this.dummy.npmInstall(); 81 | this.dummy.npmInstall(); 82 | this.dummy.run(function () { 83 | sinon.assert.calledOnce(this.spawnCommandStub); 84 | sinon.assert.calledWithExactly(this.spawnCommandStub, 'npm', ['install'], {}); 85 | done(); 86 | }.bind(this)); 87 | }); 88 | 89 | it('run without callback', function (done) { 90 | this.dummy.npmInstall('yo', { save: true }); 91 | this.dummy.run(function () { 92 | sinon.assert.calledOnce(this.spawnCommandStub); 93 | done(); 94 | }.bind(this)); 95 | }); 96 | 97 | it('run with callback', function (done) { 98 | this.dummy.npmInstall('yo', { save: true }, function () { 99 | sinon.assert.calledOnce(this.spawnCommandStub); 100 | done(); 101 | }.bind(this)); 102 | this.dummy.run(); 103 | }); 104 | }); 105 | 106 | describe('#installDependencies()', function () { 107 | it('spawn npm and bower', function (done) { 108 | this.dummy.installDependencies(function () { 109 | sinon.assert.calledTwice(this.spawnCommandStub); 110 | sinon.assert.calledWithExactly(this.spawnCommandStub, 'bower', ['install'], {}); 111 | sinon.assert.calledWithExactly(this.spawnCommandStub, 'npm', ['install'], {}); 112 | done(); 113 | }.bind(this)); 114 | this.dummy.run(); 115 | }); 116 | 117 | it('does not spawn anything with skipInstall', function (done) { 118 | this.dummy.installDependencies({ skipInstall: true }); 119 | this.dummy.run(function () { 120 | sinon.assert.notCalled(this.spawnCommandStub); 121 | done(); 122 | }.bind(this)); 123 | }); 124 | 125 | it('call callback if skipInstall', function (done) { 126 | this.dummy.installDependencies({ skipInstall: true, callback: done }); 127 | this.dummy.run(); 128 | }); 129 | 130 | it('execute a callback after installs', function (done) { 131 | this.dummy.installDependencies({ callback: done }); 132 | this.dummy.run(); 133 | }); 134 | 135 | it('accept and execute a function as its only argument', function (done) { 136 | this.dummy.installDependencies(done); 137 | this.dummy.run(); 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /lib/actions/install.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var _ = require('lodash'); 3 | var dargs = require('dargs'); 4 | var async = require('async'); 5 | var chalk = require('chalk'); 6 | var assert = require('assert'); 7 | 8 | /** 9 | * @mixin 10 | * @alias actions/install 11 | */ 12 | var install = module.exports; 13 | 14 | /** 15 | * Combine package manager cmd line arguments and run the `install` command. 16 | * 17 | * Every commands will be schedule to run once on the run loop during the `install` step. 18 | * (So don't combine the callback with `this.async()`) 19 | * 20 | * @param {String} installer Which package manager to use 21 | * @param {String|Array} [paths] Packages to install, empty string for `npm install` 22 | * @param {Object} [options] Options to invoke `install` with. These options will be parsed 23 | * by [dargs]{@link https://www.npmjs.org/package/dargs} 24 | * @param {Function} [cb] 25 | */ 26 | 27 | install.runInstall = function (installer, paths, options, cb) { 28 | if (!cb && _.isFunction(options)) { 29 | cb = options; 30 | options = {}; 31 | } 32 | 33 | options = options || {}; 34 | cb = cb || function () {}; 35 | paths = Array.isArray(paths) ? paths : (paths && paths.split(' ') || []); 36 | 37 | var args = ['install'].concat(paths).concat(dargs(options)); 38 | 39 | this.env.runLoop.add('install', function (done) { 40 | this.emit(installer + 'Install', paths); 41 | this.spawnCommand(installer, args, options) 42 | .on('error', cb) 43 | .on('exit', function (err) { 44 | if (err === 127) { 45 | this.log.error('Could not find ' + installer + '. Please install with ' + 46 | '`npm install -g ' + installer + '`.'); 47 | } 48 | this.emit(installer + 'Install:end', paths); 49 | cb(err); 50 | done(); 51 | }.bind(this)); 52 | }.bind(this), { once: installer + ' ' + args.join(' '), run: false }); 53 | 54 | return this; 55 | }; 56 | 57 | /** 58 | * Runs `npm` and `bower` in the generated directory concurrently and prints a 59 | * message to let the user know. 60 | * 61 | * @example 62 | * this.installDependencies({ 63 | * bower: true, 64 | * npm: true, 65 | * skipInstall: false, 66 | * callback: function () { 67 | * console.log('Everything is ready!'); 68 | * } 69 | * }); 70 | * 71 | * @param {Object} [options] 72 | * @param {Boolean} [options.npm=true] - whether to run `npm install` 73 | * @param {Boolean} [options.bower=true] - whether to run `bower install` 74 | * @param {Boolean} [options.skipInstall=false] - whether to skip automatic installation 75 | * @param {Boolean} [options.skipMessage=false] - whether to log the used commands 76 | * @param {Function} [options.callback] - call once every commands are runned 77 | */ 78 | 79 | install.installDependencies = function (options) { 80 | var msg = { 81 | commands: [], 82 | template: _.template('\n\nI\'m all done. ' + 83 | '<%= skipInstall ? "Just run" : "Running" %> <%= commands %> ' + 84 | '<%= skipInstall ? "" : "for you " %>to install the required dependencies.' + 85 | '<% if (!skipInstall) { %> If this fails, try running the command yourself.<% } %>\n\n') 86 | }; 87 | 88 | var commands = []; 89 | 90 | if (_.isFunction(options)) { 91 | options = { 92 | callback: options 93 | }; 94 | } 95 | 96 | options = _.defaults(options || {}, { 97 | bower: true, 98 | npm: true, 99 | skipInstall: false, 100 | skipMessage: false, 101 | callback: function () {} 102 | }); 103 | 104 | if (options.bower) { 105 | msg.commands.push('bower install'); 106 | commands.push(function (cb) { 107 | this.bowerInstall(null, null, cb); 108 | }.bind(this)); 109 | } 110 | 111 | if (options.npm) { 112 | msg.commands.push('npm install'); 113 | commands.push(function (cb) { 114 | this.npmInstall(null, null, cb); 115 | }.bind(this)); 116 | } 117 | 118 | assert(msg.commands.length, 'installDependencies needs at least one of npm or bower to run.'); 119 | 120 | if (!options.skipMessage) { 121 | this.env.adapter.log(msg.template(_.extend(options, { 122 | commands: chalk.yellow.bold(msg.commands.join(' & ')) 123 | }))); 124 | } 125 | 126 | if (options.skipInstall) { 127 | return options.callback(); 128 | } 129 | 130 | async.parallel(commands, options.callback); 131 | }; 132 | 133 | /** 134 | * Receives a list of `components`, and an `options` object to install through bower. 135 | * 136 | * The installation will automatically be runned during the run loop `install` phase. 137 | * 138 | * @param {String|Array} cmpnt Components to install 139 | * @param {Object} [options] Options to invoke `bower install` with, see `bower help install` 140 | * @param {Function} [cb] 141 | */ 142 | 143 | install.bowerInstall = function install(cmpnt, options, cb) { 144 | return this.runInstall('bower', cmpnt, options, cb); 145 | }; 146 | 147 | /** 148 | * Receives a list of `packages`, and an `options` object to install through npm. 149 | * 150 | * The installation will automatically be runned during the run loop `install` phase. 151 | * 152 | * @param {String|Array} pkgs Packages to install 153 | * @param {Object} [options] Options to invoke `npm install` with, see `npm help install` 154 | * @param {Function} [cb] 155 | */ 156 | 157 | install.npmInstall = function install(pkgs, options, cb) { 158 | return this.runInstall('npm', pkgs, options, cb); 159 | }; 160 | -------------------------------------------------------------------------------- /lib/util/prompt-suggestion.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var _ = require('lodash'); 5 | 6 | /** 7 | * @mixin 8 | * @alias util/prompt-suggestion 9 | */ 10 | var promptSuggestion = module.exports; 11 | 12 | /** 13 | * Returns the default value for a checkbox. 14 | * 15 | * @param {Object} question Inquirer prompt item 16 | * @param {*} defaultValue The stored default value 17 | * @return {*} Default value to set 18 | * @private 19 | */ 20 | var getCheckboxDefault = function (question, defaultValue) { 21 | // For simplicity we uncheck all boxes and 22 | // use .default to set the active ones. 23 | _.each(question.choices, function (choice) { 24 | if (typeof choice === 'object') { 25 | choice.checked = false; 26 | } 27 | }); 28 | return defaultValue; 29 | }; 30 | 31 | /** 32 | * Returns the default value for a list. 33 | * 34 | * @param {Object} question Inquirer prompt item 35 | * @param {*} defaultValue The stored default value 36 | * @return {*} Default value to set 37 | * @private 38 | */ 39 | var getListDefault = function (question, defaultValue) { 40 | var choiceValues = _.map(question.choices, function (choice) { 41 | if (typeof choice === 'object') { 42 | return choice.value; 43 | } else { 44 | return choice; 45 | } 46 | }); 47 | return choiceValues.indexOf(defaultValue); 48 | }; 49 | 50 | /** 51 | * Return true if the answer should be store in 52 | * the global store, otherwise false. 53 | * 54 | * @param {Object} question Inquirer prompt item 55 | * @param {String|Array} answer The inquirer answer 56 | * @return {Boolean} Answer to be stored 57 | * @private 58 | */ 59 | var storeListAnswer = function (question, answer) { 60 | var choiceValues = _.pluck(question.choices, 'value'); 61 | var choiceIndex = choiceValues.indexOf(answer); 62 | // Check if answer is not equal to default value 63 | if (question.default !== choiceIndex) { 64 | return true; 65 | } 66 | return false; 67 | }; 68 | 69 | /** 70 | * Return true if the answer should be store in 71 | * the global store, otherwise false. 72 | * 73 | * @param {Object} question Inquirer prompt item 74 | * @param {String|Array} answer The inquirer answer 75 | * @return {Boolean} Answer to be stored 76 | * @private 77 | */ 78 | var storeAnswer = function (question, answer) { 79 | // Check if answer is not equal to default value or is undefined 80 | if (answer && question.default !== answer) { 81 | return true; 82 | } 83 | return false; 84 | }; 85 | 86 | /** 87 | * Prefill the defaults with values from the global store. 88 | * 89 | * @param {Store} store `.yo-rc-global` global config 90 | * @param {Array|Object} questions Original prompt questions 91 | * @return {Array} Prompt questions array with prefilled values. 92 | */ 93 | promptSuggestion.prefillQuestions = function (store, questions) { 94 | assert(store, 'A store parameter is required'); 95 | assert(questions, 'A questions parameter is required'); 96 | 97 | var promptValues = store.get('promptValues') || {}; 98 | 99 | if (!Array.isArray(questions)) { 100 | questions = [questions]; 101 | } 102 | 103 | questions = questions.map(_.clone); 104 | 105 | // Write user defaults back to prompt 106 | return questions.map(function (question) { 107 | if (question.store !== true) return question; 108 | 109 | var storedValue = promptValues[question.name]; 110 | if (storedValue == null) return question; 111 | 112 | // Override prompt default with the user's default 113 | switch (question.type) { 114 | 115 | case 'rawlist': 116 | case 'expand': 117 | question.default = getListDefault(question, storedValue); 118 | break; 119 | case 'checkbox': 120 | question.default = getCheckboxDefault(question, storedValue); 121 | break; 122 | default: 123 | question.default = storedValue; 124 | break; 125 | } 126 | return question; 127 | }.bind(this)); 128 | }; 129 | 130 | /** 131 | * Store the answers in the global store. 132 | * 133 | * @param {Store} store `.yo-rc-global` global config 134 | * @param {Array|Object} questions Original prompt questions 135 | * @param {Object} answers The inquirer answers 136 | */ 137 | promptSuggestion.storeAnswers = function (store, questions, answers) { 138 | assert(store, 'A store parameter is required'); 139 | assert(answers, 'A answers parameter is required'); 140 | assert(questions, 'A questions parameter is required'); 141 | assert.ok(_.isObject(answers), 'answers must be a object'); 142 | 143 | var promptValues = store.get('promptValues') || {}; 144 | 145 | if (!Array.isArray(questions)) { 146 | questions = [questions]; 147 | } 148 | 149 | _.each(questions, function (question) { 150 | if (question.store !== true) return; 151 | 152 | var saveAnswer; 153 | var key = question.name; 154 | var answer = answers[key]; 155 | 156 | switch (question.type) { 157 | 158 | case 'rawlist': 159 | case 'expand': 160 | saveAnswer = storeListAnswer(question, answer); 161 | break; 162 | 163 | default: 164 | saveAnswer = storeAnswer(question, answer); 165 | break; 166 | } 167 | 168 | if (saveAnswer) { 169 | promptValues[key] = answer; 170 | } 171 | 172 | }.bind(this)); 173 | 174 | if (Object.keys(promptValues).length) { 175 | store.set('promptValues', promptValues); 176 | } 177 | }; 178 | -------------------------------------------------------------------------------- /lib/actions/remote.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var _ = require('lodash'); 5 | var fileUtils = require('file-utils'); 6 | var rimraf = require('rimraf'); 7 | 8 | /** 9 | * @mixin 10 | * @alias actions/remote 11 | */ 12 | var remote = module.exports; 13 | 14 | /** 15 | * Remotely fetch a package from github (or an archive), store this into a _cache 16 | * folder, and provide a "remote" object as a facade API to ourself (part of 17 | * generator API, copy, template, directory). It's possible to remove local cache, 18 | * and force a new remote fetch of the package. 19 | * 20 | * ### Examples: 21 | * 22 | * this.remote('user', 'repo', function(err, remote) { 23 | * remote.copy('.', 'vendors/user-repo'); 24 | * }); 25 | * 26 | * this.remote('user', 'repo', 'branch', function(err, remote) { 27 | * remote.copy('.', 'vendors/user-repo'); 28 | * }); 29 | * 30 | * this.remote('http://foo.com/bar.zip', function(err, remote) { 31 | * remote.copy('.', 'vendors/user-repo'); 32 | * }); 33 | * 34 | * When fetching from Github 35 | * @param {String} username 36 | * @param {String} repo 37 | * @param {String} branch 38 | * @param {Function} cb 39 | * @param {Boolean} refresh 40 | * 41 | * @also 42 | * When fetching an archive 43 | * @param {String} url 44 | * @param {Function} cb 45 | * @param {Boolean} refresh 46 | */ 47 | 48 | remote.remote = function () { 49 | var username; 50 | var repo; 51 | var branch; 52 | var cb; 53 | var refresh; 54 | var url; 55 | var cache; 56 | 57 | if (arguments.length <= 3 && typeof arguments[2] !== 'function') { 58 | url = arguments[0]; 59 | cb = arguments[1]; 60 | refresh = arguments[2]; 61 | cache = path.join(this.cacheRoot(), _.slugify(url)); 62 | } else { 63 | username = arguments[0]; 64 | repo = arguments[1]; 65 | branch = arguments[2]; 66 | cb = arguments[3]; 67 | refresh = arguments[4]; 68 | 69 | if (!cb) { 70 | cb = branch; 71 | branch = 'master'; 72 | } 73 | 74 | cache = path.join(this.cacheRoot(), username, repo, branch); 75 | url = 'https://github.com/' + [username, repo, 'archive', branch].join('/') + '.tar.gz'; 76 | } 77 | 78 | var self = this; 79 | 80 | var done = function (err) { 81 | if (err) { 82 | return cb(err); 83 | } 84 | 85 | self.remoteDir(cache, cb); 86 | }; 87 | 88 | fs.stat(cache, function (err) { 89 | // already cached 90 | if (!err) { 91 | // no refresh, so we can use this cache 92 | if (!refresh) { 93 | return done(); 94 | } 95 | 96 | // otherwise, we need to remove it, to fetch it again 97 | rimraf(cache, function (err) { 98 | if (err) { 99 | return cb(err); 100 | } 101 | self.extract(url, cache, { strip: 1 }, done); 102 | }); 103 | 104 | } else { 105 | self.extract(url, cache, { strip: 1 }, done); 106 | } 107 | }); 108 | 109 | return this; 110 | }; 111 | 112 | /** 113 | * Retrieve a stored directory and use as a remote reference. This is handy if 114 | * you have files you want to move that may have been downloaded differently to 115 | * using `this.remote()` (eg such as `node_modules` installed via `package.json`) 116 | * @param {String} cache 117 | * @param {Function} cb 118 | * @example 119 | * ### Examples 120 | * 121 | * this.remoteDir('foo/bar', function(err, remote) { 122 | * remote.copy('.', 'vendors/user-repo'); 123 | * }); 124 | */ 125 | remote.remoteDir = function (cache, cb) { 126 | var self = this; 127 | var files = this.expandFiles('**', { cwd: cache, dot: true }); 128 | 129 | var remoteFs = {}; 130 | remoteFs.cachePath = cache; 131 | 132 | // simple proxy to `.copy(source, destination)` 133 | remoteFs.copy = function copy(source, destination) { 134 | source = path.join(cache, source); 135 | self.copy(source, destination); 136 | return this; 137 | }; 138 | 139 | // simple proxy to `.bulkCopy(source, destination)` 140 | remoteFs.bulkCopy = function copy(source, destination) { 141 | source = path.join(cache, source); 142 | self.bulkCopy(source, destination); 143 | return this; 144 | }; 145 | 146 | // same as `.template(source, destination, data)` 147 | remoteFs.template = function template(source, destination, data) { 148 | data = data || self; 149 | destination = destination || source; 150 | source = path.join(cache, source); 151 | 152 | var body = self.engine(self.read(source), data); 153 | self.write(destination, body); 154 | }; 155 | 156 | // same as `.template(source, destination)` 157 | remoteFs.directory = function directory(source, destination) { 158 | var root = self.sourceRoot(); 159 | self.sourceRoot(cache); 160 | self.directory(source, destination); 161 | self.sourceRoot(root); 162 | }; 163 | 164 | // simple proxy to `.bulkDirectory(source, destination)` 165 | remoteFs.bulkDirectory = function directory(source, destination) { 166 | var root = self.sourceRoot(); 167 | self.sourceRoot(cache); 168 | self.bulkDirectory(source, destination); 169 | self.sourceRoot(root); 170 | }; 171 | 172 | var fileLogger = { write: _.noop, warn: _.noop }; 173 | 174 | // Set the file-utils environments 175 | // Set logger as a noop as logging is handled by the yeoman conflicter 176 | remoteFs.src = fileUtils.createEnv({ 177 | base: cache, 178 | dest: self.destinationRoot(), 179 | logger: fileLogger 180 | }); 181 | remoteFs.dest = fileUtils.createEnv({ 182 | base: self.destinationRoot(), 183 | dest: cache, 184 | logger: fileLogger 185 | }); 186 | 187 | remoteFs.dest.registerValidationFilter('collision', self.getCollisionFilter()); 188 | remoteFs.src.registerValidationFilter('collision', self.getCollisionFilter()); 189 | 190 | cb(null, remoteFs, files); 191 | }; 192 | -------------------------------------------------------------------------------- /lib/util/conflicter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var async = require('async'); 6 | var detectConflict = require('detect-conflict'); 7 | var _ = require('lodash'); 8 | 9 | /** 10 | * The Conflicter is a module that can be used to detect conflict between files. Each 11 | * Generator file system helpers pass files through this module to make sure they don't 12 | * break a user file. 13 | * 14 | * When a potential conflict is detected, we prompt the user and ask them for 15 | * confirmation before proceeding with the actual write. 16 | * 17 | * @constructor 18 | * @property {Boolean} force - same as the constructor argument 19 | * 20 | * @param {TerminalAdapter} adapter - The generator adapter 21 | * @param {Boolean} force - When set to true, we won't check for conflict. (the 22 | * conflicter become a passthrough) 23 | */ 24 | var Conflicter = module.exports = function Conflicter(adapter, force) { 25 | this.force = force === true; 26 | this.adapter = adapter; 27 | this.conflicts = []; 28 | }; 29 | 30 | /** 31 | * Add a file to conflicter queue. 32 | * 33 | * @param {String} filepath - File destination path 34 | * @param {String} contents - File new contents 35 | * @param {Function} callback - callback to be called once we know if the user want to 36 | * proceed or not. 37 | */ 38 | Conflicter.prototype.checkForCollision = function (filepath, contents, callback) { 39 | this.conflicts.push({ 40 | file: { 41 | path: path.resolve(filepath), 42 | contents: contents 43 | }, 44 | callback: callback 45 | }); 46 | }; 47 | 48 | /** 49 | * Process the _potential conflict_ queue and ask the user to resolve conflict when they 50 | * occur. 51 | * 52 | * The user is presented with the following options: 53 | * 54 | * - `Y` Yes, overwrite 55 | * - `n` No, do not overwrite 56 | * - `a` All, overwrite this and all others 57 | * - `q` Quit, abort 58 | * - `d` Diff, show the differences between the old and the new 59 | * - `h` Help, show this help 60 | * 61 | * @param {Function} cb Callback once every conflict are resolved. (note that each 62 | * file can specify it's own callback. See `#checkForCollision()`) 63 | */ 64 | Conflicter.prototype.resolve = function (cb) { 65 | cb = cb || _.noop; 66 | var self = this; 67 | var resolveConflicts = function (conflict) { 68 | return function (next) { 69 | if (!conflict) return next(); 70 | 71 | self.collision(conflict.file, function (status) { 72 | // Remove the resolved conflict from the queue 73 | _.pull(self.conflicts, conflict); 74 | conflict.callback(null, status); 75 | next(); 76 | }); 77 | }; 78 | }; 79 | 80 | async.series(this.conflicts.map(resolveConflicts), cb.bind(this)); 81 | }; 82 | 83 | /** 84 | * Check if a file conflict with the current version on the user disk. 85 | * 86 | * A basic check is done to see if the file exists, if it does: 87 | * 88 | * 1. Read its content from `fs` 89 | * 2. Compare it with the provided content 90 | * 3. If identical, mark it as is and skip the check 91 | * 4. If diverged, prepare and show up the file collision menu 92 | * 93 | * @param {Object} file File object respecting this interface: { path, contents } 94 | * @param {Function} cb Callback receiving a status string ('identical', 'create', 95 | * 'skip', 'force') 96 | * @return {null} nothing 97 | */ 98 | Conflicter.prototype.collision = function (file, cb) { 99 | var rfilepath = path.relative(process.cwd(), file.path); 100 | 101 | if (!fs.existsSync(file.path)) { 102 | this.adapter.log.create(rfilepath); 103 | return cb('create'); 104 | } 105 | 106 | if (this.force) { 107 | this.adapter.log.force(rfilepath); 108 | return cb('force'); 109 | } 110 | 111 | if (detectConflict(file.path, file.contents)) { 112 | this.adapter.log.conflict(rfilepath); 113 | return this._ask(file, cb); 114 | } else { 115 | this.adapter.log.identical(rfilepath); 116 | return cb('identical'); 117 | } 118 | }; 119 | 120 | /** 121 | * Actual prompting logic 122 | * @private 123 | * @param {Object} file 124 | * @param {Function} cb 125 | */ 126 | Conflicter.prototype._ask = function (file, cb) { 127 | var rfilepath = path.relative(process.cwd(), file.path); 128 | 129 | var prompt = { 130 | name: 'action', 131 | type: 'expand', 132 | message: 'Overwrite ' + rfilepath + '?', 133 | choices: [{ 134 | key: 'y', 135 | name: 'overwrite', 136 | value: 'write' 137 | }, { 138 | key: 'n', 139 | name: 'do not overwrite', 140 | value: 'skip' 141 | }, { 142 | key: 'a', 143 | name: 'overwrite this and all others', 144 | value: 'force' 145 | }, { 146 | key: 'x', 147 | name: 'abort', 148 | value: 'abort' 149 | }] 150 | }; 151 | 152 | // Only offer diff option for files 153 | if (fs.statSync(file.path).isFile()) { 154 | prompt.choices.push({ 155 | key: 'd', 156 | name: 'show the differences between the old and the new', 157 | value: 'diff' 158 | }); 159 | } 160 | 161 | this.adapter.prompt([prompt], function (result) { 162 | if (result.action === 'abort') { 163 | this.adapter.log.writeln('Aborting ...'); 164 | return process.exit(0); 165 | } 166 | 167 | if (result.action === 'diff') { 168 | this.adapter.diff(fs.readFileSync(file.path, 'utf8'), file.contents); 169 | return this._ask(file, cb); 170 | } 171 | 172 | if (result.action === 'force') { 173 | this.force = true; 174 | } 175 | 176 | if (result.action === 'write') { 177 | result.action = 'force'; 178 | } 179 | 180 | this.adapter.log[result.action](rfilepath); 181 | return cb(result.action); 182 | }.bind(this)); 183 | }; 184 | -------------------------------------------------------------------------------- /test/generators.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, beforeEach, it */ 2 | 'use strict'; 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var os = require('os'); 6 | var events = require('events'); 7 | var file = require('file-utils'); 8 | var generators = require('..'); 9 | var assert = generators.assert; 10 | var helpers = generators.test; 11 | var sinon = require('sinon'); 12 | var tmpdir = path.join(os.tmpdir(), 'yeoman-generators'); 13 | 14 | var Environment = require('yeoman-environment'); 15 | 16 | describe('Generators module', function () { 17 | before(helpers.setUpTestDirectory(tmpdir)); 18 | 19 | describe('module', function () { 20 | it('initialize new Environments', function () { 21 | assert.ok(generators() instanceof Environment); 22 | assert.notEqual(generators(), generators()); 23 | }); 24 | 25 | it('pass arguments to the Environment constructor', function () { 26 | var args = ['model', 'Post']; 27 | var opts = { help: true }; 28 | var env = generators(args, opts); 29 | assert.deepEqual(env.arguments, args); 30 | assert.deepEqual(env.options, opts); 31 | }); 32 | }); 33 | 34 | describe('.generators', function () { 35 | it('should have a Base object to extend from', function () { 36 | assert.ok(generators.Base); 37 | }); 38 | 39 | it('should have a NamedBase object to extend from', function () { 40 | assert.ok(generators.NamedBase); 41 | }); 42 | }); 43 | 44 | describe('generators.Base', function () { 45 | beforeEach(function () { 46 | this.env = generators(); 47 | this.generator = new generators.Base({ 48 | env: this.env, 49 | resolved: 'test' 50 | }); 51 | }); 52 | 53 | it('is an EventEmitter', function (done) { 54 | assert.ok(this.generator instanceof events.EventEmitter); 55 | assert.ok(typeof this.generator.on === 'function'); 56 | assert.ok(typeof this.generator.emit === 'function'); 57 | this.generator.on('yay-o-man', done); 58 | this.generator.emit('yay-o-man'); 59 | }); 60 | 61 | describe('.src', function () { 62 | it('implement the file-utils interface', function () { 63 | assert.implement(this.generator.src, file.constructor.prototype); 64 | }); 65 | 66 | it('generator.sourcePath() update its source base', function () { 67 | this.generator.sourceRoot('foo/src'); 68 | assert.ok(this.generator.src.fromBase('bar'), 'foo/src/bar'); 69 | }); 70 | 71 | it('generator.destinationPath() update its destination base', function () { 72 | this.generator.destinationRoot('foo/src'); 73 | assert.ok(this.generator.src.fromDestBase('bar'), 'foo/src/bar'); 74 | }); 75 | }); 76 | 77 | describe('.dest', function () { 78 | it('implement the file-utils interface', function () { 79 | assert.implement(this.generator.dest, file.constructor.prototype); 80 | }); 81 | 82 | it('generator.sourcePath() update its destination base', function () { 83 | this.generator.sourceRoot('foo/src'); 84 | assert.ok(this.generator.src.fromDestBase('bar'), 'foo/src/bar'); 85 | }); 86 | 87 | it('generator.destinationPath() update its source base', function () { 88 | this.generator.destinationRoot('foo/src'); 89 | assert.ok(this.generator.src.fromBase('bar'), 'foo/src/bar'); 90 | }); 91 | 92 | describe('conflict handler', function () { 93 | var destRoot = path.join(__dirname, 'fixtures'); 94 | var target = path.join(destRoot, 'file-conflict.txt'); 95 | var initialFileContent = fs.readFileSync(target).toString(); 96 | 97 | beforeEach(function () { 98 | this.generator.destinationRoot(destRoot); 99 | assert.ok(file.exists(target)); 100 | assert.textEqual(initialFileContent, 'initial content\n'); 101 | }); 102 | 103 | it('aborting', function () { 104 | // make sure the file exist 105 | var fileContent = this.generator.dest.read('file-conflict.txt'); 106 | var checkForCollision = sinon.stub(this.generator.conflicter, 'checkForCollision'); 107 | 108 | this.generator.dest.write('file-conflict.txt', 'some conficting content'); 109 | 110 | var cb = checkForCollision.args[0][2]; 111 | cb(null, { 112 | status: 'abort', 113 | callback: function () {} 114 | }); 115 | 116 | assert.ok(checkForCollision.calledOnce); 117 | assert.ok(fileContent, this.generator.dest.read('file-conflict.txt')); 118 | }); 119 | 120 | it('allowing', function () { 121 | // make sure the file exist 122 | var fileContent = this.generator.dest.read('file-conflict.txt'); 123 | var checkForCollision = sinon.stub(this.generator.conflicter, 'checkForCollision'); 124 | 125 | this.generator.dest.write('file-conflict.txt', 'some conficting content'); 126 | 127 | var cb = checkForCollision.args[0][2]; 128 | cb(null, { 129 | status: 'create', 130 | callback: function () {} 131 | }); 132 | 133 | assert.ok(checkForCollision.calledOnce); 134 | assert.ok('some conflicting content', fileContent); 135 | 136 | // reset content 137 | fs.writeFileSync(target, initialFileContent); 138 | }); 139 | }); 140 | }); 141 | }); 142 | 143 | describe('generators.NamedBase', function () { 144 | before(function () { 145 | this.env = generators(); 146 | this.generator = new generators.NamedBase(['namedArg'], { 147 | env: this.env, 148 | resolved: 'namedbase:test' 149 | }); 150 | }); 151 | 152 | it('extend Base generator', function () { 153 | assert.ok(this.generator instanceof generators.Base); 154 | }); 155 | 156 | it('have a name property', function () { 157 | assert.equal(this.generator.name, 'namedArg'); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /lib/test/run-context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var crypto = require('crypto'); 3 | var path = require('path'); 4 | var os = require('os'); 5 | var assert = require('assert'); 6 | var _ = require('lodash'); 7 | var yeoman = require('yeoman-environment'); 8 | var util = require('util'); 9 | var EventEmitter = require('events').EventEmitter; 10 | var helpers = require('./helpers'); 11 | var TestAdapter = require('./adapter').TestAdapter; 12 | 13 | /** 14 | * This class provide a run context object to façade the complexity involved in setting 15 | * up a generator for testing 16 | * @constructor 17 | * @param {String|Function} Generator - Namespace or generator constructor. If the later 18 | * is provided, then namespace is assumed to be 19 | * 'gen:test' in all cases 20 | * @param {Object} [settings] 21 | * @param {Boolean} [settings.tmpdir=true] - Automatically run this generator in a tmp dir 22 | * @return {this} 23 | */ 24 | 25 | var RunContext = module.exports = function RunContext(Generator, settings) { 26 | this._asyncHolds = 0; 27 | this.runned = false; 28 | this.inDirSet = false; 29 | this.args = []; 30 | this.options = {}; 31 | this.answers = {}; 32 | this.dependencies = []; 33 | this.Generator = Generator; 34 | this.settings = _.extend({ tmpdir: true }, settings); 35 | 36 | setTimeout(this._run.bind(this), 10); 37 | }; 38 | 39 | util.inherits(RunContext, EventEmitter); 40 | 41 | /** 42 | * Hold the execution until the returned callback is triggered 43 | * @return {Function} Callback to notify the normal execution can resume 44 | */ 45 | 46 | RunContext.prototype.async = function () { 47 | this._asyncHolds++; 48 | return function () { 49 | this._asyncHolds--; 50 | this._run(); 51 | }.bind(this); 52 | }; 53 | 54 | /** 55 | * Method called when the context is ready to run the generator 56 | * @private 57 | */ 58 | 59 | RunContext.prototype._run = function () { 60 | if (!this.inDirSet && this.settings.tmpdir) { 61 | this.inTmpDir(); 62 | } 63 | 64 | if (this._asyncHolds !== 0 || this.runned) return; 65 | this.runned = true; 66 | 67 | var namespace; 68 | this.env = yeoman.createEnv([], {}, new TestAdapter()); 69 | 70 | helpers.registerDependencies(this.env, this.dependencies); 71 | 72 | if (_.isString(this.Generator)) { 73 | namespace = this.env.namespace(this.Generator); 74 | this.env.register(this.Generator); 75 | } else { 76 | namespace = 'gen:test'; 77 | this.env.registerStub(this.Generator, namespace); 78 | } 79 | 80 | this.generator = this.env.create(namespace, { 81 | arguments: this.args, 82 | options: _.extend({ 83 | 'skip-install': true 84 | }, this.options) 85 | }); 86 | 87 | helpers.mockPrompt(this.generator, this.answers); 88 | 89 | this.generator.on('error', this.emit.bind(this, 'error')); 90 | this.generator.once('end', function () { 91 | helpers.restorePrompt(this.generator); 92 | this.emit('end'); 93 | this.completed = true; 94 | }.bind(this)); 95 | 96 | this.emit('ready', this.generator); 97 | this.generator.run(); 98 | }; 99 | 100 | /** 101 | * Clean the provided directory, then change directory into it 102 | * @param {String} dirPath - Directory path (relative to CWD). Prefer passing an absolute 103 | * file path for predictable results 104 | * @param {Function} [cb] - callback who'll receive the folder path as argument 105 | * @return {this} run context instance 106 | */ 107 | 108 | RunContext.prototype.inDir = function (dirPath, cb) { 109 | this.inDirSet = true; 110 | var release = this.async(); 111 | var callBackThenRelease = _.compose(release, (cb || _.noop).bind(this, path.resolve(dirPath))); 112 | helpers.testDirectory(dirPath, callBackThenRelease); 113 | return this; 114 | }; 115 | 116 | /** 117 | * Cleanup a temporary directy and change the CWD into it 118 | * 119 | * This method is called automatically when creating a RunContext. Only use it if you need 120 | * to use the callback. 121 | * 122 | * @param {Function} [cb] - callback who'll receive the folder path as argument 123 | * @return {this} run context instance 124 | */ 125 | RunContext.prototype.inTmpDir = function (cb) { 126 | var tmpdir = path.join(os.tmpdir(), crypto.randomBytes(20).toString('hex')); 127 | return this.inDir(tmpdir, cb); 128 | }; 129 | 130 | /** 131 | * Provide arguments to the run context 132 | * @param {String|Array} args - command line arguments as Array or space separated string 133 | * @return {this} 134 | */ 135 | 136 | RunContext.prototype.withArguments = function (args) { 137 | var argsArray = _.isString(args) ? args.split(' ') : args; 138 | assert(_.isArray(argsArray), 'args should be either a string separated by spaces or an array'); 139 | this.args = this.args.concat(argsArray); 140 | return this; 141 | }; 142 | 143 | /** 144 | * Provide options to the run context 145 | * @param {Object} options - command line options (e.g. `--opt-one=foo`) 146 | * @return {this} 147 | */ 148 | 149 | RunContext.prototype.withOptions = function (options) { 150 | this.options = _.extend(this.options, options); 151 | return this; 152 | }; 153 | 154 | /** 155 | * Mock the prompt with dummy answers 156 | * @param {Object} answers - Answers to the prompt questions 157 | * @return {this} 158 | */ 159 | 160 | RunContext.prototype.withPrompts = function (answers) { 161 | this.answers = _.extend(this.answers, answers); 162 | return this; 163 | }; 164 | 165 | /** 166 | * @alias RunContext.prototype.withPrompts 167 | * @deprecated 168 | */ 169 | 170 | RunContext.prototype.withPrompt = RunContext.prototype.withPrompts; 171 | 172 | /** 173 | * Provide dependent generators 174 | * @param {Array} dependencies - paths to the generators dependencies 175 | * @return {this} 176 | * @example 177 | * var deps = ['../../common', 178 | * '../../controller', 179 | * '../../main', 180 | * [helpers.createDummyGenerator(), 'testacular:app'] 181 | * ]; 182 | * var angular = new RunContext('../../app'); 183 | * angular.withGenerators(deps); 184 | * angular.withPrompts({ 185 | * compass: true, 186 | * bootstrap: true 187 | * }); 188 | * angular.onEnd(function () { 189 | * // assert something 190 | * }); 191 | */ 192 | 193 | RunContext.prototype.withGenerators = function (dependencies) { 194 | assert(_.isArray(dependencies), 'dependencies should be an array'); 195 | this.dependencies = this.dependencies.concat(dependencies); 196 | return this; 197 | }; 198 | 199 | /** 200 | * Add a callback to be called after the generator has ran 201 | * @deprecated `onEnd` is deprecated, use .on('end', onEndHandler) instead. 202 | * @param {Function} callback 203 | * @return {this} 204 | */ 205 | 206 | RunContext.prototype.onEnd = function (cb) { 207 | console.log('`onEnd` is deprecated, use .on(\'end\', onEndHandler) instead.'); 208 | return this.on('end', cb); 209 | }; 210 | -------------------------------------------------------------------------------- /test/storage.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before, after, beforeEach, afterEach */ 2 | 'use strict'; 3 | var assert = require('assert'); 4 | var FileEditor = require('mem-fs-editor'); 5 | var fs = require('fs'); 6 | var os = require('os'); 7 | var path = require('path'); 8 | var sinon = require('sinon'); 9 | var env = require('yeoman-environment'); 10 | var Storage = require('../lib/util/storage'); 11 | var generators = require('..'); 12 | var helpers = generators.test; 13 | var tmpdir = path.join(os.tmpdir(), 'yeoman-storage'); 14 | 15 | function rm(path) { 16 | if (fs.existsSync(path)) { 17 | fs.unlinkSync(path); 18 | } 19 | } 20 | 21 | describe('Storage', function () { 22 | before(helpers.setUpTestDirectory(tmpdir)); 23 | 24 | beforeEach(function () { 25 | this.beforeDir = process.cwd(); 26 | this.storePath = path.join(tmpdir, 'new-config.json'); 27 | this.memFs = env.createEnv().sharedFs; 28 | this.fs = FileEditor.create(this.memFs); 29 | this.store = new Storage('test', this.fs, this.storePath); 30 | this.store.set('foo', 'bar'); 31 | this.saveSpy = sinon.spy(this.store, 'save'); 32 | }); 33 | 34 | afterEach(function () { 35 | rm(this.storePath); 36 | process.chdir(this.beforeDir); 37 | }); 38 | 39 | describe('.constructor()', function () { 40 | it('require a name parameter', function () { 41 | assert.throws(function () { new Storage(); }); 42 | }); 43 | 44 | it('take a path parameter', function () { 45 | var store = new Storage('test', this.fs, path.join(__dirname, './fixtures/config.json')); 46 | assert.equal(store.get('testFramework'), 'mocha'); 47 | assert.ok(store.existed); 48 | }); 49 | }); 50 | 51 | it('namespace each store sharing the same store file', function () { 52 | var store = new Storage('foobar', this.fs, this.storePath); 53 | store.set('foo', 'something else'); 54 | assert.equal(this.store.get('foo'), 'bar'); 55 | }); 56 | 57 | it('defaults store path to `.yo-rc.json`', function () { 58 | process.chdir(tmpdir); 59 | var store = new Storage('yo', this.fs); 60 | store.set('foo', 'bar'); 61 | 62 | var fileContent = this.fs.readJSON('.yo-rc.json'); 63 | assert.equal(fileContent.yo.foo, 'bar'); 64 | }); 65 | 66 | describe('#get()', function () { 67 | beforeEach(function () { 68 | this.store.set('testFramework', 'mocha'); 69 | this.store.set('name', 'test'); 70 | }); 71 | 72 | it('get values', function () { 73 | assert.equal(this.store.get('testFramework'), 'mocha'); 74 | assert.equal(this.store.get('name'), 'test'); 75 | }); 76 | }); 77 | 78 | describe('#set()', function () { 79 | it('set values', function () { 80 | this.store.set('name', 'Yeoman!'); 81 | assert.equal(this.store.get('name'), 'Yeoman!'); 82 | }); 83 | 84 | it('set multipe values at once', function () { 85 | this.store.set({ foo: 'bar', john: 'doe' }); 86 | assert.equal(this.store.get('foo'), 'bar'); 87 | assert.equal(this.store.get('john'), 'doe'); 88 | }); 89 | 90 | it('throws when invalid JSON values are passed', function () { 91 | assert.throws(this.store.set.bind(this, 'foo', function () {})); 92 | }); 93 | 94 | it('save on each changes', function () { 95 | this.store.set('foo', 'bar'); 96 | assert.equal(this.saveSpy.callCount, 1); 97 | this.store.set('foo', 'oo'); 98 | assert.equal(this.saveSpy.callCount, 2); 99 | }); 100 | 101 | describe('@return', function () { 102 | beforeEach(function () { 103 | this.storePath = path.join(tmpdir, 'setreturn.json'); 104 | this.store = new Storage('test', this.fs, this.storePath); 105 | }); 106 | 107 | afterEach(function () { 108 | rm(this.storePath); 109 | }); 110 | 111 | it('the saved value (with key)', function () { 112 | assert.equal(this.store.set('name', 'Yeoman!'), 'Yeoman!'); 113 | }); 114 | 115 | it('the saved value (without key)', function () { 116 | assert.deepEqual( 117 | this.store.set({ foo: 'bar', john: 'doe' }), 118 | { foo: 'bar', john: 'doe' } 119 | ); 120 | }); 121 | 122 | it('the saved value (update values)', function () { 123 | this.store.set({ foo: 'bar', john: 'doe' }); 124 | assert.deepEqual( 125 | this.store.set({ foo: 'moo' }), 126 | { foo: 'moo', john: 'doe' } 127 | ); 128 | }); 129 | }); 130 | }); 131 | 132 | describe('#getAll()', function () { 133 | beforeEach(function () { 134 | this.store.set({ foo: 'bar', john: 'doe' }); 135 | }); 136 | 137 | it('get all values', function () { 138 | assert.deepEqual(this.store.getAll().foo, 'bar'); 139 | }); 140 | 141 | it('does not return a reference to the inner store', function () { 142 | this.store.getAll().foo = 'uhoh'; 143 | assert.equal(this.store.getAll().foo, 'bar'); 144 | }); 145 | }); 146 | 147 | describe('#delete()', function () { 148 | beforeEach(function () { 149 | this.store.set('name', 'test'); 150 | }); 151 | 152 | it('delete value', function () { 153 | this.store.delete('name'); 154 | assert.equal(this.store.get('name'), undefined); 155 | }); 156 | }); 157 | 158 | describe('#save()', function () { 159 | beforeEach(function () { 160 | this.saveStorePath = path.join(tmpdir, 'save.json'); 161 | rm(this.saveStorePath); 162 | this.store = new Storage('test', this.fs, this.saveStorePath); 163 | this.store.set('foo', 'bar'); 164 | this.saveSpy = sinon.spy(this.store, 'save'); 165 | }); 166 | 167 | describe('when multiples instances share the same file', function () { 168 | beforeEach(function () { 169 | this.store2 = new Storage('test2', this.fs, this.saveStorePath); 170 | }); 171 | 172 | it('only update modified namespace', function () { 173 | this.store2.set('bar', 'foo'); 174 | this.store.set('foo', 'bar'); 175 | 176 | var json = this.fs.readJSON(this.saveStorePath); 177 | assert.equal(json.test.foo, 'bar'); 178 | assert.equal(json.test2.bar, 'foo'); 179 | }); 180 | }); 181 | 182 | describe('when multiples instances share the same namespace', function () { 183 | beforeEach(function () { 184 | this.store2 = new Storage('test', this.fs, this.saveStorePath); 185 | }); 186 | 187 | it('only update modified namespace', function () { 188 | this.store2.set('bar', 'foo'); 189 | this.store.set('foo', 'bar'); 190 | 191 | var json = this.fs.readJSON(this.saveStorePath); 192 | assert.equal(json.test.foo, 'bar'); 193 | assert.equal(json.test.bar, 'foo'); 194 | }); 195 | }); 196 | }); 197 | 198 | describe('#defaults()', function () { 199 | beforeEach(function () { 200 | this.store.set('val1', 1); 201 | }); 202 | 203 | it('set defaults values if not predefined', function () { 204 | this.store.defaults({ val1: 3, val2: 4 }); 205 | 206 | assert.equal(this.store.get('val1'), 1); 207 | assert.equal(this.store.get('val2'), 4); 208 | }); 209 | 210 | it('require an Object as argument', function () { 211 | assert.throws(this.store.defaults.bind(this.store, 'foo')); 212 | }); 213 | 214 | describe('@return', function () { 215 | beforeEach(function () { 216 | this.storePath = path.join(tmpdir, 'defaultreturn.json'); 217 | this.store = new Storage('test', this.fs, this.storePath); 218 | this.store.set('val1', 1); 219 | this.store.set('foo', 'bar'); 220 | }); 221 | 222 | afterEach(function () { 223 | rm(this.storePath); 224 | }); 225 | 226 | it('the saved value when passed an empty object', function () { 227 | assert.deepEqual(this.store.defaults({}), { foo: 'bar', val1: 1 }); 228 | }); 229 | 230 | it('the saved value when passed the same key', function () { 231 | assert.deepEqual( 232 | this.store.defaults({ foo: 'baz' }), 233 | { foo: 'bar', val1: 1 } 234 | ); 235 | }); 236 | 237 | it('the saved value when passed new key', function () { 238 | assert.deepEqual( 239 | this.store.defaults({ food: 'pizza' }), 240 | { foo: 'bar', val1: 1, food: 'pizza' } 241 | ); 242 | }); 243 | }); 244 | }); 245 | 246 | }); 247 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | /*global it, describe, before, beforeEach, afterEach */ 2 | 'use strict'; 3 | var util = require('util'); 4 | var path = require('path'); 5 | var assert = require('assert'); 6 | var sinon = require('sinon'); 7 | var yeoman = require('..'); 8 | var helpers = yeoman.test; 9 | var RunContext = require('../lib/test/run-context'); 10 | var env = yeoman(); 11 | 12 | describe('generators.test', function () { 13 | beforeEach(function () { 14 | process.chdir(path.join(__dirname, './fixtures')); 15 | var self = this; 16 | this.StubGenerator = function (args, options) { 17 | self.args = args; 18 | self.options = options; 19 | }; 20 | util.inherits(this.StubGenerator, yeoman.Base); 21 | }); 22 | 23 | describe('.registerDependencies()', function () { 24 | it('accepts dependency as a path', function () { 25 | helpers.registerDependencies(env, ['./custom-generator-simple']); 26 | assert(env.get('simple:app')); 27 | }); 28 | 29 | it('accepts dependency as array of [, ]', function () { 30 | helpers.registerDependencies(env, [[this.StubGenerator, 'stub:app']]); 31 | assert(env.get('stub:app')); 32 | }); 33 | }); 34 | 35 | describe('.createGenerator()', function () { 36 | it('create a new generator', function () { 37 | var generator = helpers.createGenerator('unicorn:app', [ 38 | [this.StubGenerator, 'unicorn:app'] 39 | ]); 40 | assert.ok(generator instanceof this.StubGenerator); 41 | }); 42 | 43 | it('pass args params to the generator', function () { 44 | helpers.createGenerator('unicorn:app', [ 45 | [this.StubGenerator, 'unicorn:app'] 46 | ], ['temp']); 47 | assert.deepEqual(this.args, ['temp']); 48 | }); 49 | 50 | it('pass options param to the generator', function () { 51 | helpers.createGenerator('unicorn:app', [ 52 | [this.StubGenerator, 'unicorn:app'] 53 | ], ['temp'], { ui: 'tdd' }); 54 | assert.equal(this.options.ui, 'tdd'); 55 | }); 56 | }); 57 | 58 | describe('.decorate()', function () { 59 | beforeEach(function () { 60 | this.spy = sinon.stub().returns(1); 61 | this.spyStub = sinon.stub().returns(2); 62 | this.execSpy = sinon.stub().returns(3); 63 | this.execStubSpy = sinon.stub().returns(4); 64 | this.ctx = { 65 | exec: this.execSpy, 66 | execStub: this.execStubSpy 67 | }; 68 | 69 | helpers.decorate(this.ctx, 'exec', this.spy); 70 | helpers.decorate(this.ctx, 'execStub', this.spyStub, { stub: true }); 71 | this.execResult = this.ctx.exec('foo', 'bar'); 72 | this.execStubResult = this.ctx.execStub(); 73 | }); 74 | 75 | it('wraps a method', function () { 76 | assert(this.spy.calledBefore(this.execSpy)); 77 | }); 78 | 79 | it('passes arguments of the original methods', function () { 80 | assert(this.spy.calledWith('foo', 'bar')); 81 | }); 82 | 83 | it('skip original methods if stub: true', function () { 84 | assert(this.execStubSpy.notCalled); 85 | }); 86 | 87 | it('returns original methods if stub: false', function () { 88 | assert.equal(this.execResult, 3); 89 | }); 90 | 91 | it('returns stub methods if stub: true', function () { 92 | assert.equal(this.execStubResult, 2); 93 | }); 94 | }); 95 | 96 | describe('.stub()', function () { 97 | beforeEach(function () { 98 | this.spy = sinon.spy(); 99 | this.initialExec = sinon.spy(); 100 | this.ctx = { 101 | exec: this.initialExec 102 | }; 103 | helpers.stub(this.ctx, 'exec', this.spy); 104 | }); 105 | 106 | it('replace initial method', function () { 107 | this.ctx.exec(); 108 | assert(this.initialExec.notCalled); 109 | assert(this.spy.calledOnce); 110 | }); 111 | }); 112 | 113 | describe('.restore()', function () { 114 | beforeEach(function () { 115 | this.initialExec = function () {}; 116 | this.ctx = { 117 | exec: this.initialExec 118 | }; 119 | helpers.decorate(this.ctx, 'exec', function () {}); 120 | }); 121 | 122 | it('restore decorated methods', function () { 123 | assert.notEqual(this.ctx.exec, this.initialExec); 124 | helpers.restore(); 125 | assert.equal(this.ctx.exec, this.initialExec); 126 | }); 127 | }); 128 | 129 | describe('.mockPrompt()', function () { 130 | beforeEach(function () { 131 | this.generator = env.instantiate(helpers.createDummyGenerator()); 132 | helpers.mockPrompt(this.generator, { answer: 'foo' }); 133 | }); 134 | 135 | it('uses default values', function (done) { 136 | this.generator.prompt([{ name: 'respuesta', type: 'input', default: 'bar' }], function (answers) { 137 | assert.equal(answers.respuesta, 'bar'); 138 | done(); 139 | }); 140 | }); 141 | 142 | it('uses default values when no answer is passed', function (done) { 143 | var generator = env.instantiate(helpers.createDummyGenerator()); 144 | helpers.mockPrompt(generator); 145 | generator.prompt([{ name: 'respuesta', message: 'foo', type: 'input', default: 'bar' }], function (answers) { 146 | assert.equal(answers.respuesta, 'bar'); 147 | done(); 148 | }); 149 | }); 150 | 151 | it('supports `null` answer for `list` type', function (done) { 152 | var generator = env.instantiate(helpers.createDummyGenerator()); 153 | helpers.mockPrompt(generator, { 154 | respuesta: null 155 | }); 156 | generator.prompt([{ name: 'respuesta', message: 'foo', type: 'list', default: 'bar' }], function (answers) { 157 | assert.equal(answers.respuesta, null); 158 | done(); 159 | }); 160 | }); 161 | 162 | it('treats `null` as no answer for `input` type', function (done) { 163 | var generator = env.instantiate(helpers.createDummyGenerator()); 164 | helpers.mockPrompt(generator, { 165 | respuesta: null 166 | }); 167 | generator.prompt([{ name: 'respuesta', message: 'foo', type: 'input', default: 'bar' }], function (answers) { 168 | assert.equal(answers.respuesta, 'bar'); 169 | done(); 170 | }); 171 | }); 172 | 173 | it('uses `true` as the default value for `confirm` type', function (done) { 174 | var generator = env.instantiate(helpers.createDummyGenerator()); 175 | helpers.mockPrompt(generator, {}); 176 | generator.prompt([{ name: 'respuesta', message: 'foo', type: 'confirm' }], function (answers) { 177 | assert.equal(answers.respuesta, true); 178 | done(); 179 | }); 180 | }); 181 | 182 | it('supports `false` answer for `confirm` type', function (done) { 183 | var generator = env.instantiate(helpers.createDummyGenerator()); 184 | helpers.mockPrompt(generator, { respuesta: false }); 185 | generator.prompt([{ name: 'respuesta', message: 'foo', type: 'confirm' }], function (answers) { 186 | assert.equal(answers.respuesta, false); 187 | done(); 188 | }); 189 | }); 190 | 191 | it('prefers mocked values over defaults', function (done) { 192 | this.generator.prompt([{ name: 'answer', type: 'input', default: 'bar' }], function (answers) { 193 | assert.equal(answers.answer, 'foo'); 194 | done(); 195 | }); 196 | }); 197 | 198 | it('can be call multiple time on the same generator', function (done) { 199 | var generator = env.instantiate(helpers.createDummyGenerator()); 200 | helpers.mockPrompt(generator, { foo: 1 }); 201 | helpers.mockPrompt(generator, { foo: 2 }); 202 | generator.prompt({ message: 'bar', name: 'foo' }, function (answers) { 203 | assert.equal(answers.foo, 2); 204 | done(); 205 | }); 206 | }); 207 | 208 | it('keep prompt method asynchronous', function (done) { 209 | var spy = sinon.spy(); 210 | this.generator.prompt({ name: 'answer', type: 'input' }, function () { 211 | sinon.assert.called(spy); 212 | done(); 213 | }); 214 | spy(); 215 | }); 216 | }); 217 | 218 | describe('.before()', function () { 219 | afterEach(function () { 220 | helpers.restore(); 221 | }); 222 | 223 | it('alias .setUpTestDirectory()', function () { 224 | var spy = sinon.spy(helpers, 'setUpTestDirectory'); 225 | helpers.before('dir'); 226 | sinon.assert.calledWith(spy, 'dir'); 227 | }); 228 | }); 229 | 230 | describe('.run()', function () { 231 | it('return a RunContext object', function () { 232 | assert(helpers.run(helpers.createDummyGenerator()) instanceof RunContext); 233 | }); 234 | 235 | it('pass settings to RunContext', function () { 236 | var runContext = helpers.run(helpers.createDummyGenerator(), { foo: 1 }); 237 | assert.equal(runContext.settings.foo, 1); 238 | }); 239 | }); 240 | }); 241 | -------------------------------------------------------------------------------- /lib/actions/wiring.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var util = require('util'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var cheerio = require('cheerio'); 6 | 7 | /** 8 | * @mixin 9 | * @alias actions/wiring 10 | */ 11 | var wiring = module.exports; 12 | 13 | /** 14 | * Update a file containing HTML markup with new content, either 15 | * appending, prepending or replacing content matching a particular 16 | * selector. 17 | * 18 | * The following modes are available: 19 | * 20 | * - `a` Append 21 | * - `p` Prepend 22 | * - `r` Replace 23 | * - `d` Delete 24 | * 25 | * @param {String} html 26 | * @param {String} tagName 27 | * @param {String} content 28 | * @param {String} mode 29 | */ 30 | 31 | wiring.domUpdate = function domUpdate(html, tagName, content, mode) { 32 | var $ = cheerio.load(html, { decodeEntities: false }); 33 | 34 | if (content !== undefined) { 35 | if (mode === 'a') { 36 | $(tagName).append(content); 37 | } else if (mode === 'p') { 38 | $(tagName).prepend(content); 39 | } else if (mode === 'r') { 40 | $(tagName).html(content); 41 | } else if (mode === 'd') { 42 | $(tagName).remove(); 43 | } 44 | return $.html(); 45 | } else { 46 | console.error('Please supply valid content to be updated.'); 47 | } 48 | }; 49 | 50 | /** 51 | * Insert specific content as the last child of each element matched 52 | * by the `tagName` selector. 53 | * 54 | * @param {String} html 55 | * @param {String} tagName 56 | * @param {String} content 57 | */ 58 | 59 | wiring.append = function append(html, tagName, content) { 60 | return this.domUpdate(html, tagName, content, 'a'); 61 | }; 62 | 63 | /** 64 | * Insert specific content as the first child of each element matched 65 | * by the `tagName` selector. 66 | * 67 | * @param {String} html 68 | * @param {String} tagName 69 | * @param {String} content 70 | */ 71 | 72 | wiring.prepend = function prepend(html, tagName, content) { 73 | return this.domUpdate(html, tagName, content, 'p'); 74 | }; 75 | 76 | /** 77 | * Insert specific content as the last child of each element matched 78 | * by the `tagName` selector. Writes to file. 79 | * 80 | * @param {String} path 81 | * @param {String} tagName 82 | * @param {String} content 83 | */ 84 | 85 | wiring.appendToFile = function appendToFile(path, tagName, content) { 86 | var html = this.readFileAsString(path); 87 | var updatedContent = this.append(html, tagName, content); 88 | this.writeFileFromString(updatedContent, path); 89 | }; 90 | 91 | /** 92 | * Insert specific content as the first child of each element matched 93 | * by the `tagName` selector. Writes to file. 94 | * 95 | * @param {String} path 96 | * @param {String} tagName 97 | * @param {String} content 98 | */ 99 | 100 | wiring.prependToFile = function prependToFile(path, tagName, content) { 101 | var html = this.readFileAsString(path); 102 | var updatedContent = this.prepend(html, tagName, content); 103 | this.writeFileFromString(updatedContent, path); 104 | }; 105 | 106 | /** 107 | * Generate a usemin-handler block. 108 | * 109 | * @param {String} blockType 110 | * @param {String} optimizedPath 111 | * @param {String} filesBlock 112 | * @param {String|Array} searchPath 113 | */ 114 | 115 | wiring.generateBlock = function generateBlock(blockType, optimizedPath, filesBlock, searchPath) { 116 | var blockStart; 117 | var blockEnd; 118 | var blockSearchPath = ''; 119 | 120 | if (searchPath !== undefined) { 121 | if (util.isArray(searchPath)) { 122 | searchPath = '{' + searchPath.join(',') + '}'; 123 | } 124 | blockSearchPath = '(' + searchPath + ')'; 125 | } 126 | 127 | blockStart = '\n \n'; 128 | blockEnd = ' \n'; 129 | return blockStart + filesBlock + blockEnd; 130 | }; 131 | 132 | /** 133 | * Append files, specifying the optimized path and generating the necessary 134 | * usemin blocks to be used for the build process. In the case of scripts and 135 | * styles, boilerplate script/stylesheet tags are written for you. 136 | * 137 | * @param {String|Object} htmlOrOptions 138 | * @param {String} fileType 139 | * @param {String} optimizedPath 140 | * @param {Array} sourceFileList 141 | * @param {Object} attrs 142 | * @param {String} searchPath 143 | */ 144 | 145 | wiring.appendFiles = function appendFiles(htmlOrOptions, fileType, optimizedPath, sourceFileList, attrs, searchPath) { 146 | var blocks; 147 | var updatedContent; 148 | var html = htmlOrOptions; 149 | var files = ''; 150 | 151 | if (typeof htmlOrOptions === 'object') { 152 | html = htmlOrOptions.html; 153 | fileType = htmlOrOptions.fileType; 154 | optimizedPath = htmlOrOptions.optimizedPath; 155 | sourceFileList = htmlOrOptions.sourceFileList; 156 | attrs = htmlOrOptions.attrs; 157 | searchPath = htmlOrOptions.searchPath; 158 | } 159 | 160 | attrs = this.attributes(attrs); 161 | 162 | if (fileType === 'js') { 163 | sourceFileList.forEach(function (el) { 164 | files += ' \n'; 165 | }); 166 | blocks = this.generateBlock('js', optimizedPath, files, searchPath); 167 | updatedContent = this.append(html, 'body', blocks); 168 | } else if (fileType === 'css') { 169 | sourceFileList.forEach(function (el) { 170 | files += ' \n'; 171 | }); 172 | blocks = this.generateBlock('css', optimizedPath, files, searchPath); 173 | updatedContent = this.append(html, 'head', blocks); 174 | } 175 | 176 | // cleanup trailing whitespace 177 | return updatedContent.replace(/[\t ]+$/gm, ''); 178 | }; 179 | 180 | /** 181 | * Computes a given Hash object of attributes into its HTML representation. 182 | * 183 | * @param {Object} attrs 184 | */ 185 | 186 | wiring.attributes = function attributes(attrs) { 187 | return Object.keys(attrs || {}).map(function (key) { 188 | return key + '="' + attrs[key] + '"'; 189 | }).join(' '); 190 | }; 191 | 192 | /** 193 | * Scripts alias to `appendFiles`. 194 | * 195 | * @param {String} html 196 | * @param {String} optimizedPath 197 | * @param {Array} sourceFileList 198 | * @param {Object} attrs 199 | * @param {String} searchPath 200 | */ 201 | 202 | wiring.appendScripts = function appendScripts(html, optimizedPath, sourceFileList, attrs, searchPath) { 203 | return this.appendFiles(html, 'js', optimizedPath, sourceFileList, attrs, searchPath); 204 | }; 205 | 206 | /** 207 | * Simple script removal. 208 | * 209 | * @param {String} html 210 | * @param {String} scriptPath 211 | */ 212 | 213 | wiring.removeScript = function removeScript(html, scriptPath) { 214 | return this.domUpdate(html, 'script[src$="' + scriptPath + '"]', '', 'd'); 215 | }; 216 | 217 | /** 218 | * Style alias to `appendFiles`. 219 | * 220 | * @param {String} html 221 | * @param {String} optimizedPath 222 | * @param {Array} sourceFileList 223 | * @param {Object} attrs 224 | * @param {String} searchPath 225 | */ 226 | 227 | wiring.appendStyles = function appendStyles(html, optimizedPath, sourceFileList, attrs, searchPath) { 228 | return this.appendFiles(html, 'css', optimizedPath, sourceFileList, attrs, searchPath); 229 | }; 230 | 231 | /** 232 | * Simple style removal. 233 | * 234 | * @param {String} html 235 | * @param {String} path 236 | */ 237 | 238 | wiring.removeStyle = function removeStyle(html, path) { 239 | return this.domUpdate(html, 'link[href$="' + path + '"]', '', 'd'); 240 | }; 241 | 242 | /** 243 | * Append a directory of scripts. 244 | * 245 | * @param {String} html 246 | * @param {String} optimizedPath 247 | * @param {String} sourceScriptDir 248 | * @param {Object} attrs 249 | */ 250 | 251 | wiring.appendScriptsDir = function appendScriptsDir(html, optimizedPath, sourceScriptDir, attrs) { 252 | var sourceScriptList = fs.readdirSync(path.resolve(sourceScriptDir)); 253 | return this.appendFiles(html, 'js', optimizedPath, sourceScriptList, attrs); 254 | }; 255 | 256 | /** 257 | * Append a directory of stylesheets. 258 | * 259 | * @param {String} html 260 | * @param {String} optimizedPath 261 | * @param {String} sourceStyleDir 262 | * @param {Object} attrs 263 | */ 264 | 265 | wiring.appendStylesDir = function appendStylesDir(html, optimizedPath, sourceStyleDir, attrs) { 266 | var sourceStyleList = fs.readdirSync(path.resolve(sourceStyleDir)); 267 | return this.appendFiles(html, 'css', optimizedPath, sourceStyleList, attrs); 268 | }; 269 | 270 | /** 271 | * Read in the contents of a resolved file path as a string. 272 | * 273 | * @param {String} filePath 274 | */ 275 | 276 | wiring.readFileAsString = function readFileAsString(filePath) { 277 | return fs.readFileSync(path.resolve(filePath), 'utf8'); 278 | }; 279 | 280 | /** 281 | * Write the content of a string to a resolved file path. 282 | * 283 | * @param {String} html 284 | * @param {String} filePath 285 | */ 286 | 287 | wiring.writeFileFromString = function writeFileFromString(html, filePath) { 288 | fs.writeFileSync(path.resolve(filePath), html, 'utf8'); 289 | }; 290 | -------------------------------------------------------------------------------- /lib/test/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Collection of unit test helpers. (mostly related to Mocha syntax) 3 | * @module test/helpers 4 | */ 5 | 6 | 'use strict'; 7 | var fs = require('fs'); 8 | var path = require('path'); 9 | var _ = require('lodash'); 10 | var rimraf = require('rimraf'); 11 | var mkdirp = require('mkdirp'); 12 | var chalk = require('chalk'); 13 | var yeoman = require('yeoman-environment'); 14 | var assert = require('yeoman-assert'); 15 | var generators = require('../..'); 16 | var RunContext = require('../test/run-context'); 17 | var adapter = require('../test/adapter'); 18 | 19 | exports.decorated = []; 20 | 21 | exports.deprecate = function (message) { 22 | console.log(chalk.yellow('(!) ') + message); 23 | }; 24 | 25 | /** 26 | * Create a function that will clean up the test directory, 27 | * cd into it, and create a dummy gruntfile inside. Intended for use 28 | * as a callback for the mocha `before` hook. 29 | * 30 | * @param {String} dir - path to the test directory 31 | * @returns {Function} mocha callback 32 | */ 33 | 34 | exports.setUpTestDirectory = function before(dir) { 35 | return function (done) { 36 | exports.testDirectory(dir, function () { 37 | exports.gruntfile({ dummy: true }, done); 38 | }); 39 | }; 40 | }; 41 | 42 | /** 43 | * Create a function that will clean up the test directory, 44 | * cd into it, and create a dummy gruntfile inside. Intended for use 45 | * as a callback for the mocha `before` hook. 46 | * 47 | * @deprecated use helpers.setUpDirectory instead 48 | * @param {String} dir - path to the test directory 49 | * @returns {Function} mocha callback 50 | */ 51 | 52 | exports.before = function (dir) { 53 | this.deprecate('before is deprecated. Use setUpTestDirectory instead'); 54 | return exports.setUpTestDirectory(dir); 55 | }; 56 | 57 | /** 58 | * Wrap a method with custom functionality. 59 | * 60 | * @deprecated use sinon.js instead 61 | * @param {Object} context - context to find the original method 62 | * @param {String} method - name of the method to wrap 63 | * @param {Function} replacement - executes before the original method 64 | * @param {Object} options - config settings 65 | */ 66 | 67 | exports.decorate = function decorate(context, method, replacement, options) { 68 | this.deprecate('generators.test.decorate() is deprecated. Prefer using sinon module.'); 69 | options = options || {}; 70 | replacement = replacement || function () {}; 71 | 72 | var naturalMethod = context[method]; 73 | 74 | exports.decorated.push({ 75 | context: context, 76 | method: method, 77 | naturalMethod: naturalMethod 78 | }); 79 | 80 | context[method] = function () { 81 | var rep = replacement.apply(context, arguments); 82 | 83 | if (!options.stub) { 84 | return naturalMethod.apply(context, arguments); 85 | } 86 | 87 | return rep; 88 | }; 89 | }; 90 | 91 | /** 92 | * Override a method with custom functionality. 93 | * 94 | * @deprecated use sinon.js instead 95 | * @param {Object} context - context to find the original method 96 | * @param {String} method - name of the method to wrap 97 | * @param {Function} replacement - executes before the original method 98 | */ 99 | exports.stub = function stub(context, method, replacement) { 100 | this.deprecate('generators.test.stub() is deprecated. Prefer using sinon module.'); 101 | exports.decorate(context, method, replacement, { stub: true }); 102 | }; 103 | 104 | /** 105 | * Restore the original behavior of all decorated and stubbed methods 106 | * @deprecated use sinon.js instead 107 | */ 108 | exports.restore = function restore() { 109 | this.deprecate('generators.test.restore() is deprecated. Prefer using sinon module.'); 110 | exports.decorated.forEach(function (dec) { 111 | dec.context[dec.method] = dec.naturalMethod; 112 | }); 113 | }; 114 | 115 | /** 116 | * 117 | * Generates a new Gruntfile.js in the current working directory based on 118 | * options hash passed in. 119 | * 120 | * @param {Object} options - Grunt configuration 121 | * @param {Function} done - callback to call on completion 122 | * @example 123 | * before(helpers.gruntfile({ 124 | * foo: { 125 | * bar: '' 126 | * } 127 | * })); 128 | * 129 | */ 130 | 131 | exports.gruntfile = function (options, done) { 132 | var config = 'grunt.initConfig(' + JSON.stringify(options, null, 2) + ');'; 133 | config = config.split('\n').map(function (line) { 134 | return ' ' + line; 135 | }).join('\n'); 136 | 137 | var out = [ 138 | 'module.exports = function (grunt) {', 139 | config, 140 | '};' 141 | ]; 142 | 143 | fs.writeFile('Gruntfile.js', out.join('\n'), done); 144 | }; 145 | 146 | // Façade assert module for backward compatibility 147 | exports.assertFile = assert.file; 148 | exports.assertNoFile = assert.noFile; 149 | exports.assertFiles = assert.files; 150 | exports.assertFileContent = assert.fileContent; 151 | exports.assertNoFileContent = assert.noFileContent; 152 | exports.assertTextEqual = assert.textEqual; 153 | exports.assertImplement = assert.implement; 154 | 155 | /** 156 | * Clean-up the test directory and cd into it. 157 | * Call given callback after entering the test directory. 158 | * @param {String} dir - path to the test directory 159 | * @param {Function} cb - callback executed after setting working directory to dir 160 | * @example 161 | * testDirectory(path.join(__dirname, './temp'), function () { 162 | * fs.writeFileSync('testfile', 'Roses are red.'); 163 | * ); 164 | */ 165 | 166 | exports.testDirectory = function (dir, cb) { 167 | if (!dir) { 168 | throw new Error('Missing directory'); 169 | } 170 | 171 | dir = path.resolve(dir); 172 | 173 | // Make sure we're not deleting CWD by moving to top level folder. As we `cd` in the 174 | // test dir after cleaning up, this shouldn't be perceivable. 175 | process.chdir('/'); 176 | 177 | rimraf(dir, function (err) { 178 | if (err) { 179 | return cb(err); 180 | } 181 | mkdirp.sync(dir); 182 | process.chdir(dir); 183 | cb(); 184 | }); 185 | }; 186 | 187 | /** 188 | * Answer prompt questions for the passed-in generator 189 | * @param {Generator} generator - a Yeoman generator 190 | * @param {Object} answers - an object where keys are the 191 | * generators prompt names and values are the answers to 192 | * the prompt questions 193 | * @example 194 | * mockPrompt(angular, {'bootstrap': 'Y', 'compassBoostrap': 'Y'}); 195 | */ 196 | 197 | exports.mockPrompt = function (generator, answers) { 198 | var promptModule = generator.env.adapter.prompt; 199 | answers = answers || {}; 200 | 201 | var DummyPrompt = adapter.DummyPrompt; 202 | Object.keys(promptModule.prompts).forEach(function (name) { 203 | promptModule.registerPrompt(name, DummyPrompt.bind(DummyPrompt, answers)); 204 | }); 205 | }; 206 | 207 | /** 208 | * Restore defaults prompts on a generator. 209 | * @param {Generator} generator 210 | */ 211 | exports.restorePrompt = function (generator) { 212 | generator.env.adapter.prompt.restoreDefaultPrompts(); 213 | }; 214 | 215 | /** 216 | * Create a simple, dummy generator 217 | */ 218 | 219 | exports.createDummyGenerator = function () { 220 | return generators.Base.extend({ 221 | test: function () { 222 | this.shouldRun = true; 223 | } 224 | }); 225 | }; 226 | 227 | /** 228 | * Create a generator, using the given dependencies and controller arguments 229 | * Dependecies can be path (autodiscovery) or an array [, ] 230 | * 231 | * @param {String} name - the name of the generator 232 | * @param {Array} dependencies - paths to the generators dependencies 233 | * @param {Array|String} args - arguments to the generator; 234 | * if String, will be split on spaces to create an Array 235 | * @param {Object} options - configuration for the generator 236 | * @example 237 | * var deps = ['../../app', 238 | * '../../common', 239 | * '../../controller', 240 | * '../../main', 241 | * [createDummyGenerator(), 'testacular:app'] 242 | * ]; 243 | * var angular = createGenerator('angular:app', deps); 244 | */ 245 | 246 | exports.createGenerator = function (name, dependencies, args, options) { 247 | var env = yeoman.createEnv(); 248 | this.registerDependencies(env, dependencies); 249 | 250 | return env.create(name, { arguments: args, options: options }); 251 | }; 252 | 253 | /** 254 | * Register a list of dependent generators into the provided env. 255 | * Dependecies can be path (autodiscovery) or an array [, ] 256 | * 257 | * @param {Array} dependencies - paths to the generators dependencies 258 | */ 259 | 260 | exports.registerDependencies = function (env, dependencies) { 261 | dependencies.forEach(function (dependency) { 262 | if (_.isArray(dependency)) { 263 | env.registerStub.apply(env, dependency); 264 | } else { 265 | env.register(dependency); 266 | } 267 | }); 268 | }; 269 | 270 | /** 271 | * Run the provided Generator 272 | * @param {String|Function} Generator - Generator constructor or namespace 273 | * @return {RunContext} 274 | */ 275 | 276 | exports.run = function (Generator, settings) { 277 | return new RunContext(Generator, settings); 278 | }; 279 | -------------------------------------------------------------------------------- /test/wiring.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, it */ 2 | 'use strict'; 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var wiring = require('../lib/actions/wiring'); 6 | var yeoman = require('..'); 7 | var assert = yeoman.assert; 8 | 9 | describe('generators.Base (actions/wiring)', function () { 10 | before(function () { 11 | this.fixtures = path.join(__dirname, 'fixtures'); 12 | }); 13 | 14 | it('generate a simple block', function () { 15 | var res = wiring.generateBlock('js', 'main.js', [ 16 | 'path/file1.js', 17 | 'path/file2.js' 18 | ]); 19 | 20 | assert.equal(res.trim(), '\npath/file1.js,path/file2.js '); 21 | }); 22 | 23 | it('generate a simple block with search path', function () { 24 | var res = wiring.generateBlock('js', 'main.js', [ 25 | 'path/file1.js', 26 | 'path/file2.js' 27 | ], '.tmp/'); 28 | 29 | assert.equal(res.trim(), '\npath/file1.js,path/file2.js '); 30 | }); 31 | 32 | it('generate block with multiple search paths', function () { 33 | var res = wiring.generateBlock('js', 'main.js', [ 34 | 'path/file1.js', 35 | 'path/file2.js' 36 | ], ['.tmp/', 'dist/']); 37 | 38 | assert.equal(res.trim(), '\npath/file1.js,path/file2.js '); 39 | }); 40 | 41 | it('append js files to an html string', function () { 42 | var html = ''; 43 | var res = wiring.appendFiles(html, 'js', 'out/file.js', ['in/file1.js', 'in/file2.js']); 44 | var fixture = fs.readFileSync(path.join(this.fixtures, 'js_block.html'), 45 | 'utf-8').trim(); 46 | 47 | assert.textEqual(res, fixture); 48 | }); 49 | 50 | it('appendFiles work the same using the object syntax', function () { 51 | var html = ''; 52 | var res = wiring.appendFiles(html, 'js', 'out/file.js', ['in/file1.js', 'in/file2.js']); 53 | var res2 = wiring.appendFiles({ 54 | html: html, 55 | fileType: 'js', 56 | optimizedPath: 'out/file.js', 57 | sourceFileList: ['in/file1.js', 'in/file2.js'] 58 | }); 59 | 60 | assert.equal(res, res2); 61 | }); 62 | 63 | it('append content in the right place', function () { 64 | var html = '
'; 65 | var expected = '
TEST
'; 66 | assert.equal(wiring.append(html, 'section', 'TEST'), expected); 67 | assert.equal(wiring.domUpdate(html, 'section', 'TEST', 'a'), expected); 68 | }); 69 | 70 | it('prepend content in the right place', function () { 71 | var html = '
'; 72 | var expected = '
TEST
'; 73 | assert.equal(wiring.prepend(html, 'section', 'TEST'), expected); 74 | assert.equal(wiring.domUpdate(html, 'section', 'TEST', 'p'), expected); 75 | }); 76 | 77 | it('replace content correctly', function () { 78 | var html = '
'; 79 | var expected = '
TEST
'; 80 | assert.equal(wiring.domUpdate(html, 'section', 'TEST', 'r'), expected); 81 | }); 82 | 83 | it('delete content correctly', function () { 84 | var html = '
'; 85 | var expected = ''; 86 | assert.equal(wiring.domUpdate(html, 'section', 'TEST', 'd'), expected); 87 | }); 88 | 89 | it('append to files in the right place', function () { 90 | var html = '
'; 91 | var expected = '
TEST
'; 92 | var filepath = path.join(this.fixtures, 'append-prepend-to-file.html'); 93 | 94 | fs.writeFileSync(filepath, html, 'utf-8'); 95 | 96 | wiring.appendToFile(filepath, 'section', 'TEST'); 97 | 98 | var actual = fs.readFileSync(filepath, 'utf-8').trim(); 99 | 100 | assert.equal(actual, expected); 101 | 102 | fs.writeFileSync(filepath, html, 'utf-8'); 103 | }); 104 | 105 | it('prepend to files in the right place', function () { 106 | var html = '
'; 107 | var expected = '
TEST
'; 108 | var filepath = path.join(this.fixtures, 'append-prepend-to-file.html'); 109 | 110 | fs.writeFileSync(filepath, html, 'utf-8'); 111 | 112 | wiring.prependToFile(filepath, 'section', 'TEST'); 113 | 114 | var actual = fs.readFileSync(filepath, 'utf-8').trim(); 115 | 116 | assert.equal(actual, expected); 117 | 118 | fs.writeFileSync(filepath, html, 'utf-8'); 119 | }); 120 | 121 | it('handle with non-ascii characters well', function () { 122 | var html = '

'; 123 | var expected = '

Hello World!你好世界!ハローワールド!안녕 하세요 세계!

'; 124 | assert.equal(wiring.domUpdate(html, 'p', 'Hello World!你好世界!ハローワールド!안녕 하세요 세계!', 'p'), expected); 125 | }); 126 | 127 | describe('#appendFiles()', function () { 128 | it('work the same using the object syntax', function () { 129 | var html = ''; 130 | var res = wiring.appendFiles(html, 'js', 'out/file.js', ['in/file1.js', 'in/file2.js']); 131 | var res2 = wiring.appendFiles({ 132 | html: html, 133 | fileType: 'js', 134 | optimizedPath: 'out/file.js', 135 | sourceFileList: ['in/file1.js', 'in/file2.js'] 136 | }); 137 | 138 | assert.equal(res, res2); 139 | }); 140 | 141 | it('work with css file', function () { 142 | var html = ''; 143 | var res = wiring.appendFiles(html, 'css', 'out/file.css', ['in/file1.css', 'in/file2.css']); 144 | var fixture = fs.readFileSync(path.join(this.fixtures, 'css_block.html'), 'utf-8').trim(); 145 | 146 | assert.textEqual(res, fixture); 147 | }); 148 | 149 | it('work with attributes params', function () { 150 | var html = ''; 151 | var res = wiring.appendFiles({ 152 | html: html, 153 | fileType: 'js', 154 | optimizedPath: 'out/file.js', 155 | sourceFileList: ['in/file1.js', 'in/file2.js'], 156 | attrs: { 157 | 'data-test': 'my-attr' 158 | } 159 | }); 160 | var fixture = fs.readFileSync(path.join(this.fixtures, 'js_block_with_attr.html'), 'utf-8').trim(); 161 | 162 | assert.textEqual(res, fixture); 163 | }); 164 | }); 165 | 166 | describe('#appendScripts()', function () { 167 | it('append scripts', function () { 168 | var html = ''; 169 | var res = wiring.appendScripts(html, 'out/file.js', ['in/file1.js', 'in/file2.js']); 170 | var fixture = fs.readFileSync(path.join(this.fixtures, 'js_block.html'), 'utf-8').trim(); 171 | assert.textEqual(res, fixture); 172 | }); 173 | }); 174 | 175 | describe('#removeScript()', function () { 176 | it('remove script', function () { 177 | var withScript = ''; 178 | var html = ''; 179 | 180 | var res = wiring.removeScript(withScript, 'file1.js'); 181 | assert.textEqual(res, html); 182 | }); 183 | }); 184 | 185 | describe('#appendStyles()', function () { 186 | it('append styles', function () { 187 | var html = ''; 188 | var res = wiring.appendStyles(html, 'out/file.css', ['in/file1.css', 'in/file2.css']); 189 | var fixture = fs.readFileSync(path.join(this.fixtures, 'css_block.html'), 'utf-8').trim(); 190 | 191 | assert.textEqual(res, fixture); 192 | }); 193 | }); 194 | 195 | describe('#removeStyle()', function () { 196 | it('remove style', function () { 197 | var withStyle = ''; 198 | var html = ''; 199 | 200 | var res = wiring.removeStyle(withStyle, 'file1.css'); 201 | 202 | assert.textEqual(res, html); 203 | }); 204 | }); 205 | 206 | describe('#appendScriptsDir()', function () { 207 | it('append scripts directory', function () { 208 | var html = ''; 209 | var res = wiring.appendScriptsDir(html, 'out/file.js', path.join(__dirname, 'fixtures/dir-fixtures')); 210 | var fixture = fs.readFileSync(path.join(this.fixtures, 'js_block_dir.html'), 'utf-8').trim(); 211 | 212 | assert.textEqual(res, fixture); 213 | }); 214 | }); 215 | 216 | describe('#appendStylesDir()', function () { 217 | it('append styles directory', function () { 218 | var html = ''; 219 | var res = wiring.appendStylesDir(html, 'out/file.css', path.join(__dirname, 'fixtures/dir-css-fixtures')); 220 | var fixture = fs.readFileSync(path.join(this.fixtures, 'css_block_dir.html'), 'utf-8').trim(); 221 | 222 | assert.textEqual(res, fixture); 223 | }); 224 | }); 225 | }); 226 | -------------------------------------------------------------------------------- /test/remote.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before, beforeEach */ 2 | 'use strict'; 3 | var os = require('os'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var assert = require('assert'); 7 | var yeoman = require('yeoman-environment'); 8 | var nock = require('nock'); 9 | var generators = require('..'); 10 | var TestAdapter = require('../lib/test/adapter').TestAdapter; 11 | var tmpdir = path.join(os.tmpdir(), 'yeoman-remote'); 12 | 13 | describe('generators.Base#remote()', function () { 14 | before(generators.test.setUpTestDirectory(tmpdir)); 15 | 16 | beforeEach(function () { 17 | this.env = yeoman.createEnv([], {}, new TestAdapter()); 18 | this.env.registerStub(generators.test.createDummyGenerator(), 'dummy'); 19 | this.dummy = this.env.create('dummy'); 20 | nock('https://github.com') 21 | .get('/yeoman/generator/archive/master.tar.gz') 22 | .replyWithFile(200, path.join(__dirname, 'fixtures/testRemoteFile.tar.gz')); 23 | }); 24 | 25 | it('remotely fetch a package on github', function (done) { 26 | this.dummy.remote('yeoman', 'generator', done); 27 | }); 28 | 29 | it('cache the result internally into a `_cache` folder', function (done) { 30 | this.dummy.remote('yeoman', 'generator', function () { 31 | fs.stat(path.join(this.dummy.cacheRoot(), 'yeoman/generator/master'), done); 32 | }.bind(this)); 33 | }); 34 | 35 | it('invoke `cb` with a remote object to interract with the downloaded package', function (done) { 36 | this.dummy.remote('yeoman', 'generator', function (err, remote) { 37 | if (err) return done(err); 38 | 39 | assert.implement(remote, ['copy', 'template', 'directory']); 40 | done(); 41 | }); 42 | }); 43 | 44 | describe('github', function () { 45 | describe('callback argument remote#copy', function () { 46 | beforeEach(function (done) { 47 | this.dummy.remote('yeoman', 'generator', 'master', function (err, remote) { 48 | this.dummy.foo = 'foo'; 49 | remote.copy('test/fixtures/template.jst', 'remote-githug/template.js'); 50 | this.dummy._writeFiles(done); 51 | }.bind(this), true); 52 | }); 53 | 54 | it('copy a file from a remote resource', function () { 55 | var data = fs.readFileSync('remote-githug/template.js'); 56 | assert.equal(data, 'var foo = \'foo\';\n'); 57 | }); 58 | }); 59 | 60 | describe('callback argument remote#bulkCopy', function () { 61 | beforeEach(function (done) { 62 | this.dummy.remote('yeoman', 'generator', 'master', function (err, remote) { 63 | remote.bulkCopy('test/fixtures/foo-template.js', 'remote-githug/foo-template.js'); 64 | this.dummy._writeFiles(done); 65 | }.bind(this), true); 66 | }); 67 | 68 | it('copy a file from a remote resource', function (done) { 69 | fs.stat('remote-githug/foo-template.js', done); 70 | }); 71 | 72 | it('doesn\'t process templates on bulkCopy', function () { 73 | var data = fs.readFileSync('remote-githug/foo-template.js'); 74 | assert.equal(data, 'var <%= foo %> = \'<%= foo %>\';\n'); 75 | }); 76 | }); 77 | 78 | describe('callback argument remote#directory', function () { 79 | beforeEach(function (done) { 80 | this.dummy.remote('yeoman', 'generator', 'master', function (err, remote) { 81 | remote.directory('test/generators', 'remote-githug/generators'); 82 | this.dummy._writeFiles(done); 83 | }.bind(this), true); 84 | }); 85 | 86 | it('copy a directory from a remote resource', function (done) { 87 | fs.stat('remote-githug/generators/test-angular.js', done); 88 | }); 89 | }); 90 | 91 | describe('callback argument remote#bulkDirectory', function () { 92 | beforeEach(function (done) { 93 | this.dummy.remote('yeoman', 'generator', 'master', function (err, remote) { 94 | remote.bulkDirectory('test/fixtures', 'remote-githug/fixtures'); 95 | this.dummy.conflicter.force = true; 96 | this.dummy.conflicter.resolve(done); 97 | }.bind(this), true); 98 | }); 99 | 100 | it('copy a directory from a remote resource', function (done) { 101 | fs.stat('remote-githug/fixtures/foo.js', done); 102 | }); 103 | 104 | it('doesn\'t process templates on bulkDirectory', function () { 105 | var data = fs.readFileSync('remote-githug/fixtures/foo-template.js'); 106 | assert.equal(data, 'var <%= foo %> = \'<%= foo %>\';\n'); 107 | }); 108 | }); 109 | 110 | describe('callback argument remote fileUtils Environment instances', function () { 111 | beforeEach(function (done) { 112 | this.cachePath = path.join(this.dummy.cacheRoot(), 'yeoman/generator/master'); 113 | this.dummy.remote('yeoman', 'generator', 'master', function (err, remote) { 114 | this.remoteArg = remote; 115 | done(); 116 | }.bind(this)); 117 | }); 118 | 119 | it('.src is scoped to cachePath', function () { 120 | assert.equal(this.remoteArg.src.fromBase('.'), this.cachePath); 121 | assert.equal(this.remoteArg.src.fromDestBase('.'), this.dummy.destinationRoot()); 122 | }); 123 | 124 | it('.dest is scoped to destinationRoot', function () { 125 | assert.equal(this.remoteArg.dest.fromBase('.'), this.dummy.destinationRoot()); 126 | assert.equal(this.remoteArg.dest.fromDestBase('.'), this.cachePath); 127 | }); 128 | }); 129 | }); 130 | 131 | describe('absolute', function () { 132 | describe('callback argument remote#copy', function () { 133 | beforeEach(function (done) { 134 | this.dummy.foo = 'foo'; 135 | this.dummy.remote('https://github.com/yeoman/generator/archive/master.tar.gz', function (err, remote) { 136 | remote.copy('test/fixtures/template.jst', 'remote-absolute/template.js'); 137 | this.dummy._writeFiles(done); 138 | }.bind(this), true); 139 | }); 140 | 141 | it('copy a file from a remote resource', function () { 142 | var data = fs.readFileSync('remote-absolute/template.js'); 143 | assert.equal(data, 'var foo = \'foo\';\n'); 144 | }); 145 | }); 146 | 147 | describe('callback argument remote#bulkCopy', function () { 148 | beforeEach(function (done) { 149 | this.dummy.remote('https://github.com/yeoman/generator/archive/master.tar.gz', function (err, remote) { 150 | remote.bulkCopy('test/fixtures/foo-template.js', 'remote-absolute/foo-template.js'); 151 | this.dummy.conflicter.force = true; 152 | this.dummy.conflicter.resolve(done); 153 | }.bind(this), true); 154 | }); 155 | 156 | it('copy a file from a remote resource', function (done) { 157 | fs.stat('remote-absolute/foo-template.js', done); 158 | }); 159 | 160 | it('doesn\'t process templates on bulkCopy', function () { 161 | var data = fs.readFileSync('remote-absolute/foo-template.js'); 162 | assert.equal(data, 'var <%= foo %> = \'<%= foo %>\';\n'); 163 | }); 164 | }); 165 | 166 | describe('callback argument remote#directory', function () { 167 | beforeEach(function (done) { 168 | this.dummy.remote('https://github.com/yeoman/generator/archive/master.tar.gz', function (err, remote) { 169 | remote.directory('test/generators', 'remote-absolute/generators'); 170 | this.dummy._writeFiles(done); 171 | }.bind(this), true); 172 | }); 173 | 174 | it('copy a directory from a remote resource', function (done) { 175 | fs.stat('remote-absolute/generators/test-angular.js', done); 176 | }); 177 | }); 178 | 179 | describe('callback argument remote#bulkDirectory', function () { 180 | beforeEach(function (done) { 181 | this.dummy.remote('https://github.com/yeoman/generator/archive/master.tar.gz', function (err, remote) { 182 | remote.bulkDirectory('test/fixtures', 'remote-absolute/fixtures'); 183 | this.dummy.conflicter.force = true; 184 | this.dummy.conflicter.resolve(done); 185 | }.bind(this), true); 186 | }); 187 | 188 | it('copy a directory from a remote resource', function (done) { 189 | fs.stat('remote-absolute/fixtures/foo.js', done); 190 | }); 191 | 192 | it('doesn\'t process templates on bulkDirectory', function () { 193 | var data = fs.readFileSync('remote-absolute/fixtures/foo-template.js'); 194 | assert.equal(data, 'var <%= foo %> = \'<%= foo %>\';\n'); 195 | }); 196 | }); 197 | 198 | describe('callback argument remote fileUtils Environment instances', function () { 199 | beforeEach(function (done) { 200 | this.cachePath = path.join(this.dummy.cacheRoot(), 'httpsgithubcomyeomangeneratorarchivemastertargz'); 201 | this.dummy.remote('https://github.com/yeoman/generator/archive/master.tar.gz', function (err, remote) { 202 | this.remoteArg = remote; 203 | done(); 204 | }.bind(this)); 205 | }); 206 | 207 | it('.src is scoped to cachePath', function () { 208 | assert.equal(this.remoteArg.src.fromBase('.'), this.cachePath); 209 | assert.equal(this.remoteArg.src.fromDestBase('.'), this.dummy.destinationRoot()); 210 | }); 211 | 212 | it('.dest is scoped to destinationRoot', function () { 213 | assert.equal(this.remoteArg.dest.fromBase('.'), this.dummy.destinationRoot()); 214 | assert.equal(this.remoteArg.dest.fromDestBase('.'), this.cachePath); 215 | }); 216 | }); 217 | }); 218 | }); 219 | -------------------------------------------------------------------------------- /test/prompt-suggestion.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, before, after, beforeEach, afterEach */ 2 | 'use strict'; 3 | var path = require('path'); 4 | var assert = require('assert'); 5 | var os = require('os'); 6 | var Storage = require('../lib/util/storage'); 7 | var promptSuggestion = require('../lib/util/prompt-suggestion'); 8 | var rimraf = require('rimraf'); 9 | var inquirer = require('inquirer'); 10 | var env = require('yeoman-environment'); 11 | var FileEditor = require('mem-fs-editor'); 12 | 13 | describe('PromptSuggestion', function () { 14 | beforeEach(function () { 15 | this.memFs = env.createEnv().sharedFs; 16 | this.fs = FileEditor.create(this.memFs); 17 | this.storePath = path.join(os.tmpdir(), 'suggestion-config.json'); 18 | this.store = new Storage('suggestion', this.fs, this.storePath); 19 | this.store.set('promptValues', { respuesta: 'foo' }); 20 | }); 21 | 22 | afterEach(function (done) { 23 | rimraf(this.storePath, done); 24 | }); 25 | 26 | describe('.prefillQuestions()', function () { 27 | it('require a store parameter', function () { 28 | assert.throws(promptSuggestion.prefillQuestions.bind(null)); 29 | }); 30 | 31 | it('require a questions parameter', function () { 32 | assert.throws(promptSuggestion.prefillQuestions.bind(this.store)); 33 | }); 34 | 35 | it('take a questions parameter', function () { 36 | promptSuggestion.prefillQuestions(this.store, []); 37 | }); 38 | 39 | it('take a question object', function () { 40 | var question = { 41 | name: 'respuesta', 42 | default: 'bar', 43 | store: true 44 | }; 45 | var result = promptSuggestion.prefillQuestions(this.store, question)[0]; 46 | 47 | assert.equal(result.default, 'foo'); 48 | }); 49 | 50 | it('take a question array', function () { 51 | var question = [{ 52 | name: 'respuesta', 53 | default: 'bar', 54 | store: true 55 | }]; 56 | var result = promptSuggestion.prefillQuestions(this.store, question)[0]; 57 | 58 | assert.equal(result.default, 'foo'); 59 | }); 60 | 61 | it('don\'t override default when store is set to false', function () { 62 | var question = { 63 | name: 'respuesta', 64 | default: 'bar', 65 | store: false 66 | }; 67 | var result = promptSuggestion.prefillQuestions(this.store, question)[0]; 68 | assert.equal(result.default, 'bar'); 69 | }); 70 | 71 | it('override default when store is set to true', function () { 72 | var question = { 73 | name: 'respuesta', 74 | default: 'bar', 75 | store: true 76 | }; 77 | var result = promptSuggestion.prefillQuestions(this.store, question)[0]; 78 | assert.equal(result.default, 'foo'); 79 | }); 80 | 81 | it('keep inquirer objects', function () { 82 | var question = { 83 | type: 'checkbox', 84 | name: 'respuesta', 85 | default: ['bar'], 86 | store: true, 87 | choices: [new inquirer.Separator('spacer')] 88 | }; 89 | var result = promptSuggestion.prefillQuestions(this.store, question)[0]; 90 | assert.ok(result.choices[0] instanceof inquirer.Separator); 91 | }); 92 | 93 | describe('take a checkbox', function () { 94 | beforeEach(function () { 95 | this.store.set('promptValues', { 96 | respuesta: ['foo'] 97 | }); 98 | }); 99 | 100 | it('override default from an array with objects', function () { 101 | var question = { 102 | type: 'checkbox', 103 | name: 'respuesta', 104 | default: ['bar'], 105 | store: true, 106 | choices: [{ 107 | value: 'foo', 108 | name: 'foo' 109 | }, new inquirer.Separator('spacer'), { 110 | value: 'bar', 111 | name: 'bar' 112 | }, { 113 | value: 'baz', 114 | name: 'baz' 115 | }] 116 | }; 117 | var result = promptSuggestion.prefillQuestions(this.store, question)[0]; 118 | 119 | result.choices.forEach(function (choice) { 120 | assert.equal(choice.checked, false); 121 | }); 122 | assert.deepEqual(result.default, ['foo']); 123 | }); 124 | 125 | it('override default from an array with strings', function () { 126 | var question = { 127 | type: 'checkbox', 128 | name: 'respuesta', 129 | default: ['bar'], 130 | store: true, 131 | choices: ['foo', new inquirer.Separator('spacer'), 'bar', 'baz'] 132 | }; 133 | var result = promptSuggestion.prefillQuestions(this.store, question)[0]; 134 | 135 | assert.deepEqual(result.default, ['foo']); 136 | }); 137 | 138 | describe('with multiple defaults', function () { 139 | beforeEach(function () { 140 | this.store.set('promptValues', { 141 | respuesta: ['foo', 'bar'] 142 | }); 143 | }); 144 | 145 | it('from an array with objects', function () { 146 | var question = { 147 | type: 'checkbox', 148 | name: 'respuesta', 149 | default: ['bar'], 150 | store: true, 151 | choices: [{ 152 | value: 'foo', 153 | name: 'foo' 154 | }, new inquirer.Separator('spacer'), { 155 | value: 'bar', 156 | name: 'bar' 157 | }, { 158 | value: 'baz', 159 | name: 'baz' 160 | }] 161 | }; 162 | var result = promptSuggestion.prefillQuestions(this.store, question)[0]; 163 | 164 | result.choices.forEach(function (choice) { 165 | assert.equal(choice.checked, false); 166 | }); 167 | assert.deepEqual(result.default, ['foo', 'bar']); 168 | }); 169 | 170 | it('from an array with strings', function () { 171 | var question = { 172 | type: 'checkbox', 173 | name: 'respuesta', 174 | default: ['bar'], 175 | store: true, 176 | choices: ['foo', new inquirer.Separator('spacer'), 'bar', 'baz'] 177 | }; 178 | var result = promptSuggestion.prefillQuestions(this.store, question)[0]; 179 | 180 | assert.deepEqual(result.default, ['foo', 'bar']); 181 | }); 182 | }); 183 | }); 184 | 185 | describe('take a rawlist / expand', function () { 186 | beforeEach(function () { 187 | this.store.set('promptValues', { 188 | respuesta: 'bar' 189 | }); 190 | }); 191 | 192 | it('override default arrayWithObjects', function () { 193 | var question = { 194 | type: 'rawlist', 195 | name: 'respuesta', 196 | default: 0, 197 | store: true, 198 | choices: [{ 199 | value: 'foo', 200 | name: 'foo' 201 | }, new inquirer.Separator('spacer'), { 202 | value: 'bar', 203 | name: 'bar' 204 | }, { 205 | value: 'baz', 206 | name: 'baz' 207 | }] 208 | }; 209 | var result = promptSuggestion.prefillQuestions(this.store, question)[0]; 210 | 211 | assert.equal(result.default, 2); 212 | }); 213 | 214 | it('override default arrayWithObjects', function () { 215 | var question = { 216 | type: 'rawlist', 217 | name: 'respuesta', 218 | default: 0, 219 | store: true, 220 | choices: ['foo', new inquirer.Separator('spacer'), 'bar', 'baz'] 221 | }; 222 | var result = promptSuggestion.prefillQuestions(this.store, question)[0]; 223 | 224 | assert.equal(result.default, 2); 225 | }); 226 | }); 227 | }); 228 | 229 | describe('.storeAnswers()', function () { 230 | beforeEach(function () { 231 | this.store.set('promptValues', { respuesta: 'foo' }); 232 | }); 233 | 234 | it('require a store parameter', function () { 235 | assert.throws(promptSuggestion.storeAnswers.bind(null)); 236 | }); 237 | 238 | it('require a question parameter', function () { 239 | assert.throws(promptSuggestion.storeAnswers.bind(this.store)); 240 | }); 241 | 242 | it('require a answer parameter', function () { 243 | assert.throws(promptSuggestion.storeAnswers.bind(this.store, [])); 244 | }); 245 | 246 | it('take a answer parameter', function () { 247 | promptSuggestion.storeAnswers(this.store, [], {}); 248 | }); 249 | 250 | it('store answer in global store', function () { 251 | var question = { 252 | name: 'respuesta', 253 | default: 'bar', 254 | store: true 255 | }; 256 | var mockAnswers = { 257 | respuesta: 'baz' 258 | }; 259 | promptSuggestion.prefillQuestions(this.store, question); 260 | promptSuggestion.storeAnswers(this.store, question, mockAnswers); 261 | 262 | assert.equal(this.store.get('promptValues').respuesta, 'baz'); 263 | }); 264 | 265 | it('don\'t store answer in global store', function () { 266 | var question = { 267 | name: 'respuesta', 268 | default: 'bar', 269 | store: false 270 | }; 271 | var mockAnswers = { 272 | respuesta: 'baz' 273 | }; 274 | promptSuggestion.prefillQuestions(this.store, question); 275 | promptSuggestion.storeAnswers(this.store, question, mockAnswers); 276 | assert.equal(this.store.get('promptValues').respuesta, 'foo'); 277 | }); 278 | 279 | it('store answer from rawlist type', function () { 280 | var question = { 281 | type: 'rawlist', 282 | name: 'respuesta', 283 | default: 0, 284 | store: true, 285 | choices: ['foo', new inquirer.Separator('spacer'), 'bar', 'baz'] 286 | }; 287 | var mockAnswers = { 288 | respuesta: 'baz' 289 | }; 290 | promptSuggestion.prefillQuestions(this.store, question); 291 | promptSuggestion.storeAnswers(this.store, question, mockAnswers); 292 | assert.equal(this.store.get('promptValues').respuesta, 'baz'); 293 | }); 294 | }); 295 | }); 296 | -------------------------------------------------------------------------------- /lib/actions/actions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var mkdirp = require('mkdirp'); 5 | var isBinaryFile = require('isbinaryfile'); 6 | var chalk = require('chalk'); 7 | var xdgBasedir = require('xdg-basedir'); 8 | 9 | /** 10 | * @mixin 11 | * @alias actions/actions 12 | */ 13 | var actions = module.exports; 14 | 15 | /** 16 | * Stores and return the cache root for this class. The cache root is used to 17 | * `git clone` repositories from github by `.remote()` for example. 18 | */ 19 | 20 | actions.cacheRoot = function cacheRoot() { 21 | return path.join(xdgBasedir.cache, 'yeoman'); 22 | }; 23 | 24 | // Copy helper for two versions of copy action 25 | actions._prepCopy = function _prepCopy(source, destination, process) { 26 | var body; 27 | destination = destination || source; 28 | 29 | if (typeof destination === 'function') { 30 | process = destination; 31 | destination = source; 32 | } 33 | 34 | source = this.isPathAbsolute(source) ? source : path.join(this.sourceRoot(), source); 35 | destination = this.isPathAbsolute(destination) ? destination : path.join(this.destinationRoot(), destination); 36 | 37 | var encoding = null; 38 | var binary = isBinaryFile(source); 39 | if (!binary) { 40 | encoding = 'utf8'; 41 | } 42 | 43 | body = fs.readFileSync(source, encoding); 44 | 45 | if (typeof process === 'function' && !binary) { 46 | body = process(body, source, destination, { 47 | encoding: encoding 48 | }); 49 | } 50 | 51 | return { 52 | body: body, 53 | encoding: encoding, 54 | destination: destination, 55 | source: source 56 | }; 57 | }; 58 | 59 | /** 60 | * Make some of the file API aware of our source/destination root paths. 61 | * `copy`, `template` (only when could be applied/required by legacy code), 62 | * `write` and alike consider. 63 | * 64 | * @param {String} source Source file to copy from. Relative to this.sourceRoot() 65 | * @param {String} destination Destination file to write to. Relative to this.destinationRoot() 66 | * @param {Function} process 67 | */ 68 | 69 | actions.copy = function copy(source, destination, process) { 70 | var file = this._prepCopy(source, destination, process); 71 | try { 72 | file.body = this.engine(file.body, this); 73 | } catch (err) { 74 | // this happens in some cases when trying to copy a JS file like lodash/underscore 75 | // (conflicting the templating engine) 76 | } 77 | 78 | this.fs.copy(file.source, file.destination, { 79 | process: function () { 80 | return file.body; 81 | } 82 | }); 83 | 84 | return this; 85 | }; 86 | 87 | /** 88 | * Bulk copy 89 | * https://github.com/yeoman/generator/pull/359 90 | * https://github.com/yeoman/generator/issues/350 91 | * 92 | * A copy method skipping templating and conflict checking. It will allow copying 93 | * a large amount of files without causing too much recursion errors. You should 94 | * never use this method, unless there's no other solution. 95 | * 96 | * @param {String} source Source file to copy from. Relative to this.sourceRoot() 97 | * @param {String} destination Destination file to write to. Relative to this.destinationRoot() 98 | * @param {Function} process 99 | */ 100 | 101 | actions.bulkCopy = function bulkCopy(source, destination, process) { 102 | 103 | var file = this._prepCopy(source, destination, process); 104 | 105 | mkdirp.sync(path.dirname(file.destination)); 106 | fs.writeFileSync(file.destination, file.body); 107 | 108 | // synchronize stats and modification times from the original file. 109 | var stats = fs.statSync(file.source); 110 | try { 111 | fs.chmodSync(file.destination, stats.mode); 112 | fs.utimesSync(file.destination, stats.atime, stats.mtime); 113 | } catch (err) { 114 | this.log.error('Error setting permissions of "' + chalk.bold(file.destination) + '" file: ' + err); 115 | } 116 | 117 | this.log.create(file.destination); 118 | return this; 119 | }; 120 | 121 | /** 122 | * A simple method to read the content of the a file borrowed from Grunt: 123 | * https://github.com/gruntjs/grunt/blob/master/lib/grunt/file.js 124 | * 125 | * Discussion and future plans: 126 | * https://github.com/yeoman/generator/pull/220 127 | * 128 | * The encoding is `utf8` by default, to read binary files, pass the proper 129 | * encoding or null. Non absolute path are prefixed by the source root. 130 | * 131 | * @param {String} filepath 132 | * @param {String} [encoding="utf-8"] Character encoding. 133 | */ 134 | 135 | actions.read = function read(filepath, encoding) { 136 | if (!this.isPathAbsolute(filepath)) { 137 | filepath = path.join(this.sourceRoot(), filepath); 138 | } 139 | 140 | var contents = this.fs.read(filepath, { raw: true }); 141 | return contents.toString(encoding || 'utf8'); 142 | }; 143 | 144 | /** 145 | * Writes a chunk of data to a given `filepath`, checking for collision prior 146 | * to the file write. 147 | * 148 | * @param {String} filepath 149 | * @param {String} content 150 | * @param {Object} writeFile An object containing options for the file writing, as shown here: http://nodejs.org/api/fs.html#fs_fs_writefile_filename_data_options_callback 151 | */ 152 | 153 | actions.write = function write(filepath, content, writeFile) { 154 | this.fs.write(filepath, content); 155 | return this; 156 | }; 157 | 158 | /** 159 | * Gets a template at the relative source, executes it and makes a copy 160 | * at the relative destination. If the destination is not given it's assumed 161 | * to be equal to the source relative to destination. 162 | * 163 | * Use configured engine to render the provided `source` template at the given 164 | * `destination`. The `destination` path its a template itself and supports variable 165 | * interpolation. `data` is an optional hash to pass to the template, if undefined, 166 | * executes the template in the generator instance context. 167 | * 168 | * use options to pass parameters to engine (like _.templateSettings) 169 | * 170 | * @param {String} source Source file to read from. Relative to this.sourceRoot() 171 | * @param {String} destination Destination file to write to. Relative to this.destinationRoot(). 172 | * @param {Object} data Hash to pass to the template. Leave undefined to use the generator instance context. 173 | * @param {Object} options 174 | */ 175 | 176 | actions.template = function template(source, destination, data, options) { 177 | if (!destination || !this.isPathAbsolute(destination)) { 178 | destination = path.join( 179 | this.destinationRoot(), 180 | this.engine(destination || source, data || this, options) 181 | ); 182 | } 183 | 184 | if (!this.isPathAbsolute(source)) { 185 | source = path.join( 186 | this.sourceRoot(), 187 | this.engine(source, data || this, options) 188 | ); 189 | } 190 | 191 | var body = this.engine(this.fs.read(source), data || this, options); 192 | 193 | // Using copy to keep the file mode of the previous file 194 | this.fs.copy(source, destination, { 195 | process: function () { 196 | return body; 197 | } 198 | }); 199 | return this; 200 | }; 201 | 202 | /** 203 | * The engine method is the function used whenever a template needs to be rendered. 204 | * 205 | * It uses the configured engine (default: underscore) to render the `body` 206 | * template with the provided `data`. 207 | * 208 | * use options to pass paramters to engine (like _.templateSettings) 209 | * 210 | * @param {String} body 211 | * @param {Object} data 212 | * @param {Object} options 213 | */ 214 | 215 | actions.engine = function engine(body, data, options) { 216 | if (!this._engine) { 217 | throw new Error('Trying to render template without valid engine.'); 218 | } 219 | 220 | return this._engine.detect && this._engine.detect(body) ? 221 | this._engine(body, data, options) : 222 | body; 223 | }; 224 | 225 | // Shared directory method 226 | actions._directory = function _directory(source, destination, process, bulk) { 227 | // Only add sourceRoot if the path is not absolute 228 | var root = this.isPathAbsolute(source) ? source : path.join(this.sourceRoot(), source); 229 | var files = this.expandFiles('**', { dot: true, cwd: root }); 230 | 231 | destination = destination || source; 232 | 233 | if (typeof destination === 'function') { 234 | process = destination; 235 | destination = source; 236 | } 237 | 238 | var cp = this.copy; 239 | if (bulk) { 240 | cp = this.bulkCopy; 241 | } 242 | 243 | // get the path relative to the template root, and copy to the relative destination 244 | for (var i in files) { 245 | var dest = path.join(destination, files[i]); 246 | cp.call(this, path.join(root, files[i]), dest, process); 247 | } 248 | 249 | return this; 250 | }; 251 | 252 | /** 253 | * Copies recursively the files from source directory to root directory. 254 | * 255 | * @param {String} source Source directory to copy from. Relative to this.sourceRoot() 256 | * @param {String} destination Directory to copy the source files into. Relative to this.destinationRoot(). 257 | * @param {Function} process 258 | */ 259 | 260 | actions.directory = function directory(source, destination, process) { 261 | return this._directory(source, destination, process); 262 | }; 263 | 264 | /** 265 | * Copies recursively the files from source directory to root directory. 266 | * 267 | * A copy method skiping templating and conflict checking. It will allow copying 268 | * a large amount of files without causing too much recursion errors. You should 269 | * never use this method, unless there's no other solution. 270 | * 271 | * @param {String} source Source directory to copy from. Relative to this.sourceRoot() 272 | * @param {String} destination Directory to copy the source files into.Relative to this.destinationRoot(). 273 | * @param {Function} process 274 | */ 275 | 276 | actions.bulkDirectory = function directory(source, destination, process) { 277 | // Join the source here because the conflicter will not run 278 | // until next tick, which resets the source root on remote 279 | // bulkCopy operations 280 | source = path.join(this.sourceRoot(), source); 281 | this.conflicter.checkForCollision(destination, null, function (err, status) { 282 | // create or force means file write, identical or skip prevent the 283 | // actual write. 284 | if (/force|create/.test(status)) { 285 | this._directory(source, destination, process, true); 286 | } 287 | 288 | }.bind(this)); 289 | return this; 290 | }; 291 | -------------------------------------------------------------------------------- /test/run-context.js: -------------------------------------------------------------------------------- 1 | /*global it, describe, before, beforeEach, afterEach */ 2 | 'use strict'; 3 | var os = require('os'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var assert = require('assert'); 7 | var sinon = require('sinon'); 8 | var inquirer = require('inquirer'); 9 | var yo = require('..'); 10 | var helpers = yo.test; 11 | var tmpdir = path.join(os.tmpdir(), 'yeoman-run-context'); 12 | 13 | var RunContext = require('../lib/test/run-context'); 14 | 15 | describe('RunContext', function () { 16 | beforeEach(function () { 17 | process.chdir(__dirname); 18 | this.defaultInput = inquirer.prompts.input; 19 | var Dummy = this.Dummy = helpers.createDummyGenerator(); 20 | this.execSpy = sinon.spy(); 21 | Dummy.prototype.exec = this.execSpy; 22 | this.ctx = new RunContext(Dummy); 23 | }); 24 | 25 | afterEach(function (done) { 26 | process.chdir(__dirname); 27 | if (this.ctx.completed) return done(); 28 | this.ctx.on('end', done); 29 | }); 30 | 31 | describe('constructor', function () { 32 | it('accept path parameter', function (done) { 33 | var ctx = new RunContext(path.join(__dirname, './fixtures/custom-generator-simple')); 34 | ctx 35 | .on('ready', function () { 36 | assert(ctx.env.get('simple:app')); 37 | }) 38 | .on('end', done); 39 | }); 40 | 41 | it('propagate generator error events', function (done) { 42 | var error = new Error(); 43 | var Dummy = helpers.createDummyGenerator(); 44 | var execSpy = sinon.stub().throws(error); 45 | Dummy.prototype.exec = execSpy; 46 | var ctx = new RunContext(Dummy); 47 | ctx.on('error', function (err) { 48 | sinon.assert.calledOnce(execSpy); 49 | assert.equal(err, error); 50 | done(); 51 | }); 52 | }); 53 | 54 | it('accept generator constructor parameter (and assign gen:test as namespace)', function (done) { 55 | this.ctx.on('ready', function () { 56 | assert(this.ctx.env.get('gen:test')); 57 | done(); 58 | }.bind(this)); 59 | }); 60 | 61 | it('run the generator asynchronously', function (done) { 62 | assert(this.execSpy.notCalled); 63 | this.ctx.on('end', function () { 64 | sinon.assert.calledOnce(this.execSpy); 65 | done(); 66 | }.bind(this)); 67 | }); 68 | 69 | it('reset mocked prompt after running', function (done) { 70 | this.ctx.on('end', function () { 71 | assert.equal(this.defaultInput, inquirer.prompts.input); 72 | done(); 73 | }.bind(this)); 74 | }); 75 | 76 | it('automatically run in a random tmpdir', function (done) { 77 | this.ctx.on('end', function () { 78 | assert.notEqual(process.cwd(), __dirname); 79 | assert.equal(fs.realpathSync(os.tmpdir()), path.dirname(process.cwd())); 80 | done(); 81 | }.bind(this)); 82 | }); 83 | 84 | it('allows an option to not automatically run in tmpdir', function (done) { 85 | var cwd = process.cwd(); 86 | this.ctx.settings.tmpdir = false; 87 | this.ctx.on('end', function (err) { 88 | assert.equal(cwd, process.cwd()); 89 | done(); 90 | }); 91 | }); 92 | 93 | it('accepts settings', function () { 94 | var Dummy = helpers.createDummyGenerator(); 95 | var ctx = new RunContext(Dummy, { tmpdir: false }); 96 | assert.equal(ctx.settings.tmpdir, false); 97 | }); 98 | 99 | it('only run a generator once', function (done) { 100 | this.ctx.on('end', function () { 101 | sinon.assert.calledOnce(this.execSpy); 102 | done(); 103 | }.bind(this)); 104 | this.ctx._run(); 105 | this.ctx._run(); 106 | }); 107 | }); 108 | 109 | describe('error handling', function () { 110 | 111 | function removeListeners(host, handlerName) { 112 | if (!host) return; 113 | // store the original handlers for the host 114 | var originalHandlers = host.listeners(handlerName); 115 | // remove the current handlers for the host 116 | host.removeAllListeners(handlerName); 117 | return originalHandlers; 118 | } 119 | 120 | function setListeners(host, handlerName, handlers) { 121 | if (!host) return; 122 | handlers.forEach(host.on.bind(host, handlerName)); 123 | } 124 | 125 | function processError(host, handlerName, cb) { 126 | if (!host) return; 127 | host.once(handlerName, cb); 128 | } 129 | 130 | beforeEach(function () { 131 | this.originalHandlersProcess = removeListeners(process, 'uncaughtException'); 132 | this.originalHandlersProcessDomain = removeListeners(process.domain, 'error'); 133 | }); 134 | 135 | afterEach(function () { 136 | setListeners(process, 'uncaughtException', this.originalHandlersProcess); 137 | setListeners(process.domain, 'error', this.originalHandlersProcessDomain); 138 | }); 139 | 140 | it('throw an error when no listener is present', function (done) { 141 | var error = new Error('dummy exception'); 142 | var execSpy = sinon.stub().throws(error); 143 | 144 | var errorHandler = function (err) { 145 | sinon.assert.calledOnce(execSpy); 146 | assert.equal(err, error); 147 | done(); 148 | }; 149 | 150 | // tests can be run via 2 commands : 'gulp test' or 'mocha' 151 | // in 'mocha' case the error has to be caught using process.on('uncaughtException') 152 | // in 'gulp' case the error has to be caught using process.domain.on('error') 153 | // as we don't know in which case we are, we set the error handler for both 154 | processError(process, 'uncaughtException', errorHandler); 155 | processError(process.domain, 'error', errorHandler); 156 | 157 | var Dummy = helpers.createDummyGenerator(); 158 | Dummy.prototype.exec = execSpy; 159 | 160 | setImmediate(function () { 161 | new RunContext(Dummy); 162 | }); 163 | 164 | }); 165 | 166 | }); 167 | 168 | describe('#inDir()', function () { 169 | beforeEach(function () { 170 | process.chdir(__dirname); 171 | this.tmp = tmpdir; 172 | }); 173 | 174 | it('call helpers.testDirectory()', function () { 175 | sinon.spy(helpers, 'testDirectory'); 176 | this.ctx.inDir(this.tmp); 177 | assert(helpers.testDirectory.withArgs(this.tmp).calledOnce); 178 | helpers.testDirectory.restore(); 179 | }); 180 | 181 | it('is chainable', function () { 182 | assert.equal(this.ctx.inDir(this.tmp), this.ctx); 183 | }); 184 | 185 | it('accepts optional `cb` to be invoked with resolved `dir`', function (done) { 186 | var ctx = new RunContext(this.Dummy); 187 | var cb = sinon.spy(function () { 188 | sinon.assert.calledOnce(cb); 189 | sinon.assert.calledOn(cb, ctx); 190 | sinon.assert.calledWith(cb, path.resolve(this.tmp)); 191 | }.bind(this)); 192 | ctx.inDir(this.tmp, cb).on('end', done); 193 | }); 194 | 195 | it('optional `cb` can use `this.async()` to delay execution', function (done) { 196 | var ctx = new RunContext(this.Dummy); 197 | var delayed = false; 198 | var cb = sinon.spy(function () { 199 | var release = this.async(); 200 | setTimeout(function () { 201 | delayed = true; 202 | release(); 203 | }.bind(this), 1); 204 | }); 205 | ctx.inDir(this.tmp, cb) 206 | .on('ready', function () { 207 | assert(delayed); 208 | }) 209 | .on('end', done); 210 | }); 211 | }); 212 | 213 | describe('#inTmpDir', function () { 214 | it('call helpers.testDirectory()', function () { 215 | sinon.spy(helpers, 'testDirectory'); 216 | this.ctx.inTmpDir(); 217 | sinon.assert.calledOnce(helpers.testDirectory); 218 | helpers.testDirectory.restore(); 219 | }); 220 | 221 | it('is chainable', function () { 222 | assert.equal(this.ctx.inTmpDir(), this.ctx); 223 | }); 224 | 225 | it('accepts optional `cb` to be invoked with resolved `dir`', function (done) { 226 | var ctx = this.ctx; 227 | var cb = sinon.spy(function (dir) { 228 | assert.equal(this, ctx); 229 | assert(dir.indexOf(os.tmpdir()) > -1); 230 | }); 231 | this.ctx.inTmpDir(cb).on('end', done); 232 | }); 233 | }); 234 | 235 | describe('#withArguments()', function () { 236 | it('provide arguments to the generator when passed as Array', function (done) { 237 | this.ctx.withArguments(['one', 'two']); 238 | this.ctx.on('end', function () { 239 | assert.deepEqual(this.execSpy.firstCall.thisValue.arguments, ['one', 'two']); 240 | done(); 241 | }.bind(this)); 242 | }); 243 | 244 | it('provide arguments to the generator when passed as String', function (done) { 245 | this.ctx.withArguments('foo bar'); 246 | this.ctx.on('end', function () { 247 | assert.deepEqual(this.execSpy.firstCall.thisValue.arguments, ['foo', 'bar']); 248 | done(); 249 | }.bind(this)); 250 | }); 251 | 252 | it('throws when arguments passed is neither a String or an Array', function () { 253 | assert.throws(this.ctx.withArguments.bind(this.ctx, { foo: 'bar' })); 254 | }); 255 | 256 | it('is chainable', function (done) { 257 | this.ctx.withArguments('foo').withArguments('bar'); 258 | this.ctx.on('end', function () { 259 | assert.deepEqual(this.execSpy.firstCall.thisValue.arguments, ['foo', 'bar']); 260 | done(); 261 | }.bind(this)); 262 | }); 263 | }); 264 | 265 | describe('#withOptions()', function () { 266 | it('provide options to the generator', function (done) { 267 | this.ctx.withOptions({ foo: 'bar' }); 268 | this.ctx.on('end', function () { 269 | assert.equal(this.execSpy.firstCall.thisValue.options.foo, 'bar'); 270 | done(); 271 | }.bind(this)); 272 | }); 273 | 274 | it('set skip-install by default', function (done) { 275 | this.ctx.on('end', function () { 276 | assert.equal(this.execSpy.firstCall.thisValue.options['skip-install'], true); 277 | done(); 278 | }.bind(this)); 279 | }); 280 | 281 | it('allow skip-install to be overriden', function (done) { 282 | this.ctx.withOptions({ 'skip-install': false }); 283 | this.ctx.on('end', function () { 284 | assert.equal(this.execSpy.firstCall.thisValue.options['skip-install'], false); 285 | done(); 286 | }.bind(this)); 287 | }); 288 | 289 | it('is chainable', function (done) { 290 | this.ctx.withOptions({ foo: 'bar' }).withOptions({ john: 'doe' }); 291 | this.ctx.on('end', function () { 292 | var options = this.execSpy.firstCall.thisValue.options; 293 | assert.equal(options.foo, 'bar'); 294 | assert.equal(options.john, 'doe'); 295 | done(); 296 | }.bind(this)); 297 | }); 298 | }); 299 | 300 | describe('#withPrompts()', function () { 301 | it('is call automatically', function (done) { 302 | this.Dummy.prototype.askFor = function () { 303 | this.prompt({ 304 | name: 'yeoman', 305 | type: 'input', 306 | message: 'Hey!', 307 | default: 'pass' 308 | }, function (answers) { 309 | assert.equal(answers.yeoman, 'pass'); 310 | }); 311 | }; 312 | this.ctx.on('end', done); 313 | }); 314 | 315 | it('mock the prompt', function (done) { 316 | this.Dummy.prototype.askFor = function () { 317 | this.prompt({ 318 | name: 'yeoman', 319 | type: 'input', 320 | message: 'Hey!' 321 | }, function (answers) { 322 | assert.equal(answers.yeoman, 'yes please'); 323 | }); 324 | }; 325 | this.ctx 326 | .withPrompts({ yeoman: 'yes please' }) 327 | .on('end', done); 328 | }); 329 | 330 | it('is chainable', function (done) { 331 | this.Dummy.prototype.askFor = function () { 332 | var cb = this.async(); 333 | this.prompt([{ 334 | name: 'yeoman', 335 | type: 'input', 336 | message: 'Hey!' 337 | }, { 338 | name: 'yo', 339 | type: 'input', 340 | message: 'Yo!' 341 | }], function (answers) { 342 | assert.equal(answers.yeoman, 'yes please'); 343 | assert.equal(answers.yo, 'yo man'); 344 | cb(); 345 | }); 346 | }; 347 | this.ctx 348 | .withPrompts({ yeoman: 'yes please' }) 349 | .withPrompts({ yo: 'yo man' }) 350 | .on('end', done); 351 | }); 352 | }); 353 | 354 | describe('#withGenerators()', function () { 355 | it('register paths', function (done) { 356 | this.ctx.withGenerators([ 357 | path.join(__dirname, './fixtures/custom-generator-simple') 358 | ]).on('ready', function () { 359 | assert(this.ctx.env.get('simple:app')); 360 | done(); 361 | }.bind(this)); 362 | }); 363 | 364 | it('register mocked generator', function (done) { 365 | this.ctx.withGenerators([ 366 | [helpers.createDummyGenerator(), 'dummy:gen'] 367 | ]).on('ready', function () { 368 | assert(this.ctx.env.get('dummy:gen')); 369 | done(); 370 | }.bind(this)); 371 | }); 372 | 373 | it('allow multiple calls', function (done) { 374 | this.ctx.withGenerators([ 375 | path.join(__dirname, './fixtures/custom-generator-simple') 376 | ]).withGenerators([ 377 | [helpers.createDummyGenerator(), 'dummy:gen'] 378 | ]).on('ready', function () { 379 | assert(this.ctx.env.get('dummy:gen')); 380 | assert(this.ctx.env.get('simple:app')); 381 | done(); 382 | }.bind(this)); 383 | }); 384 | }); 385 | }); 386 | -------------------------------------------------------------------------------- /test/actions.js: -------------------------------------------------------------------------------- 1 | /*global describe, before, after, it, afterEach, beforeEach */ 2 | 'use strict'; 3 | var fs = require('fs'); 4 | var os = require('os'); 5 | var path = require('path'); 6 | var sinon = require('sinon'); 7 | var generators = require('..'); 8 | var helpers = generators.test; 9 | var assert = generators.assert; 10 | var TestAdapter = require('../lib/test/adapter').TestAdapter; 11 | var tmpdir = path.join(os.tmpdir(), 'yeoman-actions'); 12 | 13 | describe('generators.Base (actions/actions)', function () { 14 | before(helpers.setUpTestDirectory(tmpdir)); 15 | 16 | beforeEach(function () { 17 | var env = this.env = generators([], {}, new TestAdapter()); 18 | env.registerStub(helpers.createDummyGenerator(), 'dummy'); 19 | this.dummy = env.create('dummy'); 20 | 21 | this.fixtures = path.join(__dirname, 'fixtures'); 22 | this.dummy.sourceRoot(this.fixtures); 23 | this.dummy.foo = 'bar'; 24 | }); 25 | 26 | describe('#sourceRoot()', function () { 27 | it('updates the "_sourceRoot" property when root is given', function () { 28 | this.dummy.sourceRoot(this.fixtures); 29 | assert.equal(this.dummy._sourceRoot, this.fixtures); 30 | }); 31 | 32 | it('returns the updated or current value of "_sourceRoot"', function () { 33 | assert.equal(this.dummy.sourceRoot(), this.fixtures); 34 | }); 35 | }); 36 | 37 | describe('#destinationRoot()', function () { 38 | it('updates the "_destinationRoot" property when root is given', function () { 39 | this.dummy.destinationRoot('.'); 40 | assert.equal(this.dummy._destinationRoot, process.cwd()); 41 | }); 42 | 43 | it('returns the updated or current value of "_destinationRoot"', function () { 44 | assert.equal(this.dummy.destinationRoot(), process.cwd()); 45 | }); 46 | }); 47 | 48 | describe('#cacheRoot()', function () { 49 | it('returns the cache root where yeoman stores all temp files', function () { 50 | assert(/yeoman$/.test(this.dummy.cacheRoot())); 51 | }); 52 | }); 53 | 54 | describe('#copy()', function () { 55 | before(function (done) { 56 | this.dummy.copy(path.join(__dirname, 'fixtures/foo.js'), 'write/to/bar.js'); 57 | this.dummy.copy('foo.js', 'write/to/foo.js'); 58 | this.dummy.copy('foo-copy.js'); 59 | this.dummy.copy('yeoman-logo.png'); 60 | this.dummy.copy(path.join(__dirname, 'fixtures/lodash-copy.js'), 'write/to/lodash.js'); 61 | this.dummy.copy('foo-process.js', 'write/to/foo-process.js', function (contents) { 62 | contents = contents.replace('foo', 'bar'); 63 | contents = contents.replace('\r\n', '\n'); 64 | 65 | return contents; 66 | }); 67 | 68 | var oldDestRoot = this.dummy.destinationRoot(); 69 | this.dummy.destinationRoot('write/to'); 70 | this.dummy.copy('foo.js', 'foo-destRoot.js'); 71 | this.dummy.destinationRoot(oldDestRoot); 72 | this.dummy._writeFiles(done); 73 | }); 74 | 75 | it('copy source files relative to the "sourceRoot" value', function (done) { 76 | fs.stat('write/to/foo.js', done); 77 | }); 78 | 79 | it('copy to destination files relative to the "destinationRoot" value', function (done) { 80 | fs.stat('write/to/foo-destRoot.js', done); 81 | }); 82 | 83 | it('allow absolute path, and prevent the relative paths join', function (done) { 84 | fs.stat('write/to/bar.js', done); 85 | }); 86 | 87 | it('allow to copy without using the templating (conficting with lodash/underscore)', function (done) { 88 | fs.stat('write/to/lodash.js', done); 89 | }); 90 | 91 | it('defaults the destination to the source filepath value', function (done) { 92 | fs.stat('foo-copy.js', done); 93 | }); 94 | 95 | it('retains executable mode on copied files', function (done) { 96 | // Don't run on windows 97 | if (process.platform === 'win32') return done(); 98 | 99 | fs.stat('write/to/bar.js', function (err, stats) { 100 | if (err) throw err; 101 | assert(stats.mode & 1 === 1, 'File be executable.'); 102 | done(); 103 | }); 104 | }); 105 | 106 | it('process source contents via function', function (done) { 107 | fs.readFile('write/to/foo-process.js', function (err, data) { 108 | if (err) throw err; 109 | assert.textEqual(String(data), 'var bar = \'foo\';\n'); 110 | done(); 111 | }); 112 | }); 113 | }); 114 | 115 | describe('#bulkCopy()', function () { 116 | before(function () { 117 | this.dummy.bulkCopy(path.join(__dirname, 'fixtures/foo.js'), 'write/to/foo.js'); 118 | this.dummy.bulkCopy(path.join(__dirname, 'fixtures/foo-template.js'), 'write/to/noProcess.js'); 119 | }); 120 | 121 | it('copy a file', function (done) { 122 | fs.readFile('write/to/foo.js', function (err, data) { 123 | if (err) throw err; 124 | assert.textEqual(String(data), 'var foo = \'foo\';\n'); 125 | done(); 126 | }); 127 | }); 128 | 129 | it('does not run conflicter or template engine', function () { 130 | var data = fs.readFileSync('write/to/noProcess.js'); 131 | assert.textEqual(String(data), 'var <%= foo %> = \'<%= foo %>\';\n<%%= extra %>\n'); 132 | 133 | this.dummy.bulkCopy(path.join(__dirname, 'fixtures/foo.js'), 'write/to/noProcess.js'); 134 | var data2 = fs.readFileSync('write/to/noProcess.js'); 135 | assert.textEqual(String(data2), 'var foo = \'foo\';\n'); 136 | }); 137 | }); 138 | 139 | describe('#read()', function () { 140 | it('read files relative to the "sourceRoot" value', function () { 141 | var body = this.dummy.read('foo.js'); 142 | assert.textEqual(body, 'var foo = \'foo\';' + '\n'); 143 | }); 144 | 145 | it('allow absolute path, and prevent the relative paths join', function () { 146 | var body = this.dummy.read(path.join(__dirname, 'fixtures/foo.js')); 147 | assert.textEqual(body, 'var foo = \'foo\';' + '\n'); 148 | }); 149 | }); 150 | 151 | describe('#write()', function () { 152 | before(function (done) { 153 | this.body = 'var bar = \'bar\';' + '\n'; 154 | this.dummy.write('write/to/foobar.js', this.body); 155 | this.dummy._writeFiles(done); 156 | }); 157 | 158 | it('writes the specified files relative to the "destinationRoot" value', function () { 159 | var body = this.body; 160 | var actual = fs.readFileSync('write/to/foobar.js', 'utf8'); 161 | assert.ok(actual, body); 162 | }); 163 | }); 164 | 165 | describe('#template()', function () { 166 | describe('without options', function () { 167 | before(function (done) { 168 | // Create file with weird permission for testing 169 | var permFileName = this.fixtures + '/perm-test.js'; 170 | fs.writeFileSync(permFileName, 'var foo;', { mode: parseInt(733, 8) }); 171 | 172 | this.dummy.foo = 'fooooooo'; 173 | this.dummy.template('perm-test.js', 'write/to/perm-test.js'); 174 | this.dummy.template('foo-template.js', 'write/to/from-template.js'); 175 | this.dummy.template('foo-template.js'); 176 | this.dummy.template('<%=foo%>-file.js'); 177 | this.dummy.template('foo-template.js', 'write/to/<%=foo%>-directory/from-template.js', { 178 | foo: 'bar' 179 | }); 180 | this.dummy.template('foo-template.js', 'write/to/from-template-bar.js', { 181 | foo: 'bar' 182 | }); 183 | this.dummy.template('template-tags.jst', 'write/to/template-tags.js', { 184 | foo: 'bar', 185 | bar: 'foo' 186 | }); 187 | this.dummy._writeFiles(done); 188 | }); 189 | 190 | after(function () { 191 | fs.unlinkSync(this.fixtures + '/perm-test.js'); 192 | }); 193 | 194 | it('copy and process source file to destination', function (done) { 195 | fs.stat('write/to/from-template.js', done); 196 | }); 197 | 198 | it('defaults the destination to the source filepath value, relative to "destinationRoot" value', function () { 199 | var body = fs.readFileSync('foo-template.js', 'utf8'); 200 | assert.textEqual(body, 'var fooooooo = \'fooooooo\';\n<%= extra %>\n'); 201 | }); 202 | 203 | it('process underscore templates with the passed-in data', function () { 204 | var body = fs.readFileSync('write/to/from-template-bar.js', 'utf8'); 205 | assert.textEqual(body, 'var bar = \'bar\';\n<%= extra %>\n'); 206 | }); 207 | 208 | it('process underscore templates with the actual generator instance, when no data is given', function () { 209 | var body = fs.readFileSync('write/to/from-template.js', 'utf8'); 210 | assert.textEqual(body, 'var fooooooo = \'fooooooo\';\n<%= extra %>\n'); 211 | }); 212 | 213 | it('parses `${}` tags', function () { 214 | var body = fs.readFileSync('write/to/template-tags.js', 'utf8'); 215 | assert.textEqual(body, 'foo = bar\n'); 216 | }); 217 | 218 | it('process underscode templates in destination filename', function () { 219 | var body = fs.readFileSync('fooooooo-file.js', 'utf8'); 220 | assert.textEqual(body, 'var fooooooo = \'fooooooo\';\n'); 221 | }); 222 | 223 | it('process underscore templates in destination path', function () { 224 | var body = fs.readFileSync('write/to/bar-directory/from-template.js', 'utf8'); 225 | assert.textEqual(body, 'var bar = \'bar\';\n<%= extra %>\n'); 226 | }); 227 | 228 | it('keep file mode', function () { 229 | var originFileStat = fs.statSync(this.fixtures + '/perm-test.js'); 230 | var bodyStat = fs.statSync('write/to/perm-test.js'); 231 | assert.equal(originFileStat.mode, bodyStat.mode); 232 | }); 233 | 234 | }); 235 | 236 | describe('with options', function () { 237 | beforeEach(function (done) { 238 | this.src = 'template-setting.xml'; 239 | this.dest = 'write/to/template-setting.xml'; 240 | this.dummy.template(this.src, this.dest, { foo: 'bar' }, { 241 | evaluate: /\{\{([\s\S]+?)\}\}/g, 242 | interpolate: /\{\{=([\s\S]+?)\}\}/g, 243 | escape: /\{\{-([\s\S]+?)\}\}/g 244 | }); 245 | this.dummy._writeFiles(done); 246 | }); 247 | 248 | it('uses tags specified in option', function () { 249 | var body = fs.readFileSync(this.dest, 'utf8'); 250 | assert.textEqual(body, 'bar <%= foo %>;\n'); 251 | }); 252 | }); 253 | 254 | describe('with custom tags', function () { 255 | beforeEach(function (done) { 256 | this.src = 'custom-template-setting.xml'; 257 | this.dest = 'write/to/custom-template-setting.xml'; 258 | this.spy = sinon.spy(); 259 | 260 | var oldEngineOptions = this.dummy.options.engine.options; 261 | 262 | this.dummy.options.engine.options = { 263 | detecter: /\{\{?[^\}]+\}\}/, 264 | matcher: /\{\{\{([^\}]+)\}\}/g, 265 | start: '{{', 266 | end: '}}' 267 | }; 268 | 269 | this.dummy.template(this.src, this.dest, { 270 | foo: 'bar', 271 | spy: this.spy 272 | }, { 273 | evaluate: /\{\{([\s\S]+?)\}\}/g, 274 | interpolate: /\{\{=([\s\S]+?)\}\}/g, 275 | escape: /\{\{-([\s\S]+?)\}\}/g 276 | }); 277 | 278 | this.dummy.options.engine.options = oldEngineOptions; 279 | this.dummy._writeFiles(done); 280 | }); 281 | 282 | it('uses tags specified in option and engine', function () { 283 | var body = fs.readFileSync(this.dest, 'utf8'); 284 | assert.textEqual(body, 'bar\n'); 285 | sinon.assert.calledOnce(this.spy); 286 | }); 287 | }); 288 | }); 289 | 290 | describe('#directory()', function () { 291 | before(function (done) { 292 | this.dummy.directory('./dir-fixtures', 'directory'); 293 | this.dummy.directory('./dir-fixtures'); 294 | this.dummy.directory('./dir-fixtures', 'directory-processed', function (contents, source) { 295 | if (source.indexOf('foo-process.js') !== -1) { 296 | contents = contents.replace('foo', 'bar'); 297 | contents = contents.replace('\r\n', '\n'); 298 | } 299 | 300 | return contents; 301 | }); 302 | this.dummy._writeFiles(done); 303 | }); 304 | 305 | it('copy and process source files to destination', function (done) { 306 | fs.stat('directory/foo-template.js', function (err) { 307 | if (err) { 308 | return done(err); 309 | } 310 | fs.stat('directory/foo.js', done); 311 | }); 312 | }); 313 | 314 | it('defaults the destination to the source filepath value, relative to "destinationRoot" value', function (done) { 315 | fs.stat('dir-fixtures/foo-template.js', function (err) { 316 | if (err) { 317 | return done(err); 318 | } 319 | fs.stat('dir-fixtures/foo.js', done); 320 | }); 321 | }); 322 | 323 | it('process underscore templates with the actual generator instance', function () { 324 | var body = fs.readFileSync('directory/foo-template.js', 'utf8'); 325 | var foo = this.dummy.foo; 326 | assert.textEqual(body, 'var ' + foo + ' = \'' + foo + '\';\n'); 327 | }); 328 | 329 | it('process source contents via function', function () { 330 | var body = fs.readFileSync('directory-processed/foo-process.js', 'utf8'); 331 | assert.textEqual(body, 'var bar = \'foo\';\n'); 332 | }); 333 | 334 | }); 335 | 336 | describe('#bulkDirectory()', function () { 337 | before(function (done) { 338 | this.dummy.sourceRoot(this.fixtures); 339 | this.dummy.destinationRoot('.'); 340 | this.dummy.conflicter.force = true; 341 | // Create temp bulk operation files 342 | // These cannot just be in the repo or the other directory tests fail 343 | require('mkdirp').sync(this.fixtures + '/bulk-operation'); 344 | for (var i = 0; i < 1000; i++) { 345 | fs.writeFileSync(this.fixtures + '/bulk-operation/' + i + '.js', i); 346 | } 347 | 348 | // Copy files without processing 349 | this.dummy.bulkDirectory('bulk-operation', 'bulk-operation'); 350 | this.dummy.conflicter.resolve(done); 351 | }); 352 | 353 | after(function () { 354 | // Now remove them 355 | for (var i = 0; i < 1000; i++) { 356 | fs.unlinkSync(this.fixtures + '/bulk-operation/' + i + '.js'); 357 | } 358 | fs.rmdirSync(this.fixtures + '/bulk-operation'); 359 | }); 360 | 361 | it('bulk copy one thousand files', function (done) { 362 | fs.readFile('bulk-operation/999.js', function (err, data) { 363 | if (err) throw err; 364 | assert.equal(data, '999'); 365 | done(); 366 | }); 367 | }); 368 | 369 | it('check for conflict if directory already exists', function (done) { 370 | this.dummy.conflicter.force = true; 371 | this.dummy.bulkDirectory('bulk-operation', 'bulk-operation'); 372 | this.dummy.conflicter.resolve(done); 373 | }); 374 | }); 375 | 376 | describe('#expandFiles()', function () { 377 | before(function (done) { 378 | this.dummy.copy('foo.js', 'write/abc/abc.js'); 379 | this.dummy._writeFiles(done); 380 | }); 381 | it('returns expand files', function () { 382 | var files = this.dummy.expandFiles('write/abc/**'); 383 | assert.deepEqual(files, ['write/abc/abc.js']); 384 | }); 385 | it('returns expand files', function () { 386 | var files = this.dummy.expandFiles('abc/**', { cwd: './write' }); 387 | assert.deepEqual(files, ['abc/abc.js']); 388 | }); 389 | }); 390 | }); 391 | --------------------------------------------------------------------------------