Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: yeoman/yo
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 411b299fd18ff1790185c40df8c6661156aafd40
Choose a base ref
...
head repository: yeoman/yo
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 4aafcab97799172f3a6c8dcf196619d713ee40e8
Choose a head ref

Commits on Dec 25, 2015

  1. Verified

    This commit was signed with the committer’s verified signature.
    xrmx Riccardo Magliocchetti
    Copy the full SHA
    f2a8ccf View commit details
  2. Fix linting error

    SBoudrias committed Dec 25, 2015

    Verified

    This commit was signed with the committer’s verified signature.
    xrmx Riccardo Magliocchetti
    Copy the full SHA
    61a84dd View commit details

Commits on Dec 27, 2015

  1. Bump dependencies

    SBoudrias committed Dec 27, 2015

    Verified

    This commit was signed with the committer’s verified signature.
    xrmx Riccardo Magliocchetti
    Copy the full SHA
    5bc7042 View commit details
  2. Verified

    This commit was signed with the committer’s verified signature.
    xrmx Riccardo Magliocchetti
    Copy the full SHA
    8f9ac7a View commit details

Commits on Dec 31, 2015

  1. Partially verified

    This commit is signed with the committer’s verified signature.
    xrmx’s contribution has been verified via SSH key.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    31f1e02 View commit details

Commits on Jan 8, 2016

  1. 1.6.0

    SBoudrias committed Jan 8, 2016

    Verified

    This commit was signed with the committer’s verified signature.
    xrmx Riccardo Magliocchetti
    Copy the full SHA
    23cd3dc View commit details

Commits on Jan 16, 2016

  1. add gitter badge

    sindresorhus committed Jan 16, 2016

    Verified

    This commit was signed with the committer’s verified signature.
    xrmx Riccardo Magliocchetti
    Copy the full SHA
    979a498 View commit details

Commits on Feb 14, 2016

  1. Add sponsors to Readme

    SBoudrias committed Feb 14, 2016

    Verified

    This commit was signed with the committer’s verified signature.
    xrmx Riccardo Magliocchetti
    Copy the full SHA
    eee5507 View commit details

Commits on Feb 24, 2016

  1. Added the backers badge

    And renamed "sponsors" to "backers"
    xdamman committed Feb 24, 2016

    Verified

    This commit was signed with the committer’s verified signature.
    xrmx Riccardo Magliocchetti
    Copy the full SHA
    f001821 View commit details
  2. Merge pull request #412 from xdamman/patch-1

    Added the backers badge
    SBoudrias committed Feb 24, 2016

    Partially verified

    This commit is signed with the committer’s verified signature.
    xrmx’s contribution has been verified via SSH key.
    We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
    Copy the full SHA
    2986731 View commit details

Commits on Feb 26, 2016

  1. Add multiple generator intialization in the CLI

    Add multiple generator intialization in the CLI
    
    Readd un-camelizing options
    
    Improve readability
    
    Use cli-list
    
    Fix cli-list setup
    
    Rename variables
    
    Fix cli-list setup
    
    Rename variables
    jamen committed Feb 26, 2016
    Copy the full SHA
    c778a07 View commit details
  2. Merge pull request #414 from jamen/master

    Initialize multiple generators with one command.
    SBoudrias committed Feb 26, 2016
    Copy the full SHA
    89ae30a View commit details
  3. 1.7.0

    SBoudrias committed Feb 26, 2016
    Copy the full SHA
    ce739d3 View commit details

Commits on Apr 24, 2016

  1. package.json tweaks

    sindresorhus committed Apr 24, 2016
    Copy the full SHA
    f9bf583 View commit details

Commits on May 5, 2016

  1. Bump yeoman-environment

    SBoudrias committed May 5, 2016
    Copy the full SHA
    069fb07 View commit details
  2. 1.7.1

    SBoudrias committed May 5, 2016
    Copy the full SHA
    e9c2440 View commit details

Commits on May 8, 2016

  1. Copy the full SHA
    7b50265 View commit details
  2. Add Node v6 to travis

    SBoudrias committed May 8, 2016
    Copy the full SHA
    c0a5c21 View commit details
  3. Bump dev dependencies

    SBoudrias committed May 8, 2016
    Copy the full SHA
    1240864 View commit details
  4. 1.8.0

    SBoudrias committed May 8, 2016
    14
    Copy the full SHA
    6525a66 View commit details

Commits on May 9, 2016

  1. 1.8.1

    SBoudrias committed May 9, 2016
    4
    Copy the full SHA
    b7a2c96 View commit details

Commits on May 18, 2016

  1. Yo completion (#436)

    * completion: Invoke tabtab install on yo completion command
    
    * completion: Add `yo completion` to --help output
    
    * completion: remove postinstall tabtab part
    
    * completion: comply with eslint
    mklabs authored and SBoudrias committed May 18, 2016
    Copy the full SHA
    861aacd View commit details
  2. 1.8.2

    SBoudrias committed May 18, 2016
    Copy the full SHA
    2492a28 View commit details
  3. 1.8.3

    SBoudrias committed May 18, 2016
    Copy the full SHA
    0adca9b View commit details

Commits on May 23, 2016

  1. Copy the full SHA
    cc4493a View commit details

Commits on May 31, 2016

  1. Fix main generator detection pattern (#447)

    * add helper to create fake read-pkg-up module
    
    * cover Router#updateAvailableGenerators with tests
    
    * ignore sub-generators that contain 'app'
    
    * inline fake read-pkg-up module
    
    * Revert "add helper to create fake read-pkg-up module"
    
    This reverts commit a6f7ff7.
    mlenkeit authored and SBoudrias committed May 31, 2016
    Copy the full SHA
    82a37a4 View commit details
  2. 1.8.4

    SBoudrias committed May 31, 2016
    Copy the full SHA
    8a54b6e View commit details

Commits on Sep 1, 2016

  1. Copy the full SHA
    a1b0532 View commit details
  2. 1.8.5

    SBoudrias committed Sep 1, 2016
    Copy the full SHA
    3be725b View commit details

Commits on Nov 14, 2016

  1. Bump inquirer (#429)

    SBoudrias authored Nov 14, 2016
    Copy the full SHA
    846fd3c View commit details

Commits on Nov 23, 2016

  1. Remove Node 0.10 from travis config (#489)

    * Remove Node 0.10 from travis config
    
    * Add Node 7 to travis config
    mischah authored and SBoudrias committed Nov 23, 2016
    Copy the full SHA
    1d22d1c View commit details
  2. Drop pinkie-promise. (#488)

    * Drop `pinkie-promise`.
    
    * Set `env.es6` to `true`.
    wtgtybhertgeghgtwtg authored and mischah committed Nov 23, 2016
    Copy the full SHA
    da8a3cf View commit details

Commits on Dec 2, 2016

  1. Add issue and pull request template (#492)

    * Add issue and pull request template
    
    … and move contributing.md to .github directory as well.
    Related to yeoman/yeoman#1652
    
    * Add info about getting versions
    
    * Improvements suggested by Sindre
    
    * Github → GitHub
    
    * Move instructions about how to get versions
    
    * Remove comment about how to handle GFM checkboxes
    mischah authored Dec 2, 2016
    Copy the full SHA
    e4a635c View commit details

Commits on Dec 21, 2016

  1. Fix pull request template (#497)

    evenstensberg authored and mischah committed Dec 21, 2016
    Copy the full SHA
    6de9fbb View commit details

Commits on Dec 30, 2016

  1. Drop v.12 from Travis (#496)

    `node v.12` is being left for EOL now and I don't see any reason to keep it supported in Travis. Merge is optional!
    evenstensberg authored and hemanth committed Dec 30, 2016
    Copy the full SHA
    e5e4bf0 View commit details

Commits on Jan 4, 2017

  1. Update fullname package (#498)

    pd4d10 authored and SBoudrias committed Jan 4, 2017
    Copy the full SHA
    1992fc1 View commit details
  2. Copy the full SHA
    50683a0 View commit details

Commits on Feb 5, 2017

  1. Bump got. (#501)

    * Bump `got`.
    
    * Rename `gotCallback`.
    wtgtybhertgeghgtwtg authored and SBoudrias committed Feb 5, 2017
    Copy the full SHA
    ca4b84c View commit details
  2. Bump read-pkg-up. (#502)

    wtgtybhertgeghgtwtg authored and SBoudrias committed Feb 5, 2017
    Copy the full SHA
    5149db0 View commit details

Commits on Feb 6, 2017

  1. Bump inquirer. (#503)

    wtgtybhertgeghgtwtg authored and SBoudrias committed Feb 6, 2017
    Copy the full SHA
    71ae911 View commit details

Commits on Feb 10, 2017

  1. Copy the full SHA
    9631848 View commit details
  2. Bump lodash. (#505)

    wtgtybhertgeghgtwtg authored and SBoudrias committed Feb 10, 2017
    Copy the full SHA
    de5ca0b View commit details
  3. Bump async. (#506)

    wtgtybhertgeghgtwtg authored and SBoudrias committed Feb 10, 2017
    Copy the full SHA
    b41958c View commit details

Commits on Feb 11, 2017

  1. Bump dependencies

    sindresorhus committed Feb 11, 2017
    Copy the full SHA
    018cd95 View commit details
  2. ES2015ify

    sindresorhus committed Feb 11, 2017
    Copy the full SHA
    ecd7ffa View commit details

Commits on Apr 1, 2017

  1. Bump dependencies

    sindresorhus committed Apr 1, 2017
    Copy the full SHA
    6e1a870 View commit details

Commits on May 3, 2017

  1. Copy the full SHA
    a75d174 View commit details

Commits on Jun 7, 2017

  1. Copy the full SHA
    11af8d6 View commit details
  2. 2.0.0

    SBoudrias committed Jun 7, 2017
    Copy the full SHA
    ab77908 View commit details

Commits on Nov 1, 2017

  1. Copy the full SHA
    77bdb9b View commit details
4 changes: 1 addition & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -3,9 +3,7 @@ root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* text=auto
* text=auto eol=lf
File renamed without changes.
44 changes: 44 additions & 0 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!--
Allo' allo'!
Thanks for taking the time to submit an issue.
Please keep in mind, that GitHub issues are meant to be used for reporting bugs and to
request new features.
Use Stack Overflow for support: http://stackoverflow.com/questions/tagged/yeoman
Head over to one of our Gitter rooms (https://gitter.im/yeoman/home) and ask for help if you’re unsure if you ran into a bug or if you have any other question.
You would like to report a bug?
Use the search feature to ensure that the bug hasn't been reported before.
Please ensure to provide the following information to make sure we have all we need to address your issue.
-->

## Type of issue

<!-- Feature request or bug -->

<!-- Please delete the rest of the template in case of a feature request -->

----

## My environment

* *OS version/details*: `eg. Windows 10 64-bit`
* *Node version:* `x.x.x` (run `node --version` in your terminal)
* *npm version:* `x.x.x` (run `npm --version` in your terminal)
* *Version of yo :* `x.x.x` (run `yo --version` in your terminal)

## Expected behavior

<!-- Description over here -->

## Current behavior

<!-- Description over here -->

## Steps to reproduce the behavior

## Command line output

```
Paste your error output over here
```
25 changes: 25 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!--
Allo' allo'!
Thanks for taking the time to submit a pull request ❤
Please make sure you read and fulfill our pull request guidelines:
https://github.com/yeoman/yeoman/blob/master/contributing.md
Additional useful information is placed on the Yeoman Website:
http://yeoman.io/contributing/pull-request.html
-->

## Purpose of this pull request?

- [ ] Documentation update
- [ ] Bug fix
- [ ] Enhancement
- [ ] Other, please explain:

## What changes did you make?

<!-- Give an overview -->

## Is there anything you'd like reviewers to focus on?

<!-- Just in case -->
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
node_modules
yarn.lock
coverage
9 changes: 5 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
sudo: false
language: node_js
node_js:
- 'stable'
- '0.12'
- '0.10'
- '10'
- '8'
- '6'
before_install:
- npm install --global npm@latest
25 changes: 25 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
BSD 2-Clause License

Copyright (c) Google
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42 changes: 42 additions & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';
const path = require('path');
const gulp = require('gulp');
const mocha = require('gulp-mocha');
const istanbul = require('gulp-istanbul');
const plumber = require('gulp-plumber');
const coveralls = require('gulp-coveralls');

gulp.task('pre-test', () =>
gulp.src('lib/**/*.js')
.pipe(istanbul({includeUntested: true}))
.pipe(istanbul.hookRequire())
);

gulp.task('test', ['pre-test'], cb => {
let mochaErr;

// Will disable cache for completion tests
process.env.YO_TEST = 1;

gulp.src('test/**/*.js')
.pipe(plumber())
.pipe(mocha({reporter: 'spec'}))
.on('error', err => {
mochaErr = err;
})
.pipe(istanbul.writeReports())
.on('end', () => {
cb(mochaErr);
});
});

gulp.task('coveralls', ['test'], () => {
if (!process.env.CI) {
return;
}

return gulp.src(path.join(__dirname, 'coverage/lcov.info'))
.pipe(coveralls());
});

gulp.task('default', ['test', 'coveralls']);
165 changes: 100 additions & 65 deletions lib/cli.js
Original file line number Diff line number Diff line change
@@ -1,40 +1,56 @@
#!/usr/bin/env node
'use strict';
var fs = require('fs');
var path = require('path');
var chalk = require('chalk');
var updateNotifier = require('update-notifier');
var Insight = require('insight');
var yosay = require('yosay');
var stringLength = require('string-length');
var rootCheck = require('root-check');
var meow = require('meow');
var pkg = require('../package.json');
var Router = require('./router');

var cli = meow({
help: false,
pkg: pkg
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const updateNotifier = require('update-notifier');
const Insight = require('insight');
const yosay = require('yosay');
const stringLength = require('string-length');
const rootCheck = require('root-check');
const meow = require('meow');
const list = require('cli-list');
const Tabtab = require('tabtab');
const pkg = require('../package.json');
const Router = require('./router');

const gens = list(process.argv.slice(2));

// Override http networking to go through a proxy ifone is configured
require('global-tunnel-ng').initialize();

/* eslint new-cap: 0, no-extra-parens: 0 */
const tabtab = new Tabtab.Commands.default({
name: 'yo',
completer: 'yo-complete'
});

var opts = cli.flags;
var args = cli.input;
var cmd = args[0];
var insight;
const cli = gens.map(gen => {
const minicli = meow({help: false, pkg, argv: gen});
const opts = minicli.flags;
const args = minicli.input;

// add un-camelized options too, for legacy
// TODO: remove some time in the future when generators have upgraded
Object.keys(opts).forEach(function (key) {
var legacyKey = key.replace(/[A-Z]/g, function (m) {
return '-' + m.toLowerCase();
});
// Add un-camelized options too, for legacy
// TODO: Remove some time in the future when generators have upgraded
for (const key of Object.keys(opts)) {
const legacyKey = key.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);
opts[legacyKey] = opts[key];
}

return {opts, args};
});

const firstCmd = cli[0] || {opts: {}, args: {}};
const cmd = firstCmd.args[0];

opts[legacyKey] = opts[key];
const insight = new Insight({
trackingCode: 'UA-31537568-1',
pkg
});

function updateCheck() {
var notifier = updateNotifier({pkg: pkg});
var message = [];
const notifier = updateNotifier({pkg});
const message = [];

if (notifier.update) {
message.push('Update available: ' + chalk.green.bold(notifier.update.latest) + chalk.gray(' (current: ' + notifier.update.current + ')'));
@@ -44,13 +60,17 @@ function updateCheck() {
}

function pre() {
// debugging helper
// Debugging helper
if (cmd === 'doctor') {
require('yeoman-doctor')();
return;
}

// easteregg
if (cmd === 'completion') {
return tabtab.install();
}

// Easteregg
if (cmd === 'yeoman' || cmd === 'yo') {
console.log(require('yeoman-character'));
return;
@@ -60,13 +80,15 @@ function pre() {
}

function createGeneratorList(env) {
var generators = Object.keys(env.getGeneratorsMeta()).reduce(function (namesByGenerator, generator) {
var parts = generator.split(':');
var generatorName = parts.shift();
const generators = Object.keys(env.getGeneratorsMeta()).reduce((namesByGenerator, generator) => {
const parts = generator.split(':');
const generatorName = parts.shift();

// If first time we found this generator, prepare to save all its sub-generators
if (!namesByGenerator[generatorName]) {
namesByGenerator[generatorName] = [];
}

// If sub-generator (!== app), save it
if (parts[0] !== 'app') {
namesByGenerator[generatorName].push(parts.join(':'));
@@ -75,54 +97,72 @@ function createGeneratorList(env) {
return namesByGenerator;
}, {});

return Object.keys(generators).map(function (generator) {
var subGenerators = generators[generator].map(function (subGenerator) {
return ' ' + subGenerator;
}).join('\n');
if (Object.keys(generators).length === 0) {
return ' Couldn\'t find any generators, did you install any? Troubleshoot issues by running\n\n $ yo doctor';
}

return ' ' + generator + '\n' + subGenerators;
return Object.keys(generators).map(generator => {
const subGenerators = generators[generator].map(subGenerator => ` ${subGenerator}`).join('\n');
return ` ${generator}\n${subGenerators}`;
}).join('\n');
}

function init() {
var env = require('yeoman-environment').createEnv();
const env = require('yeoman-environment').createEnv();

env.on('error', function (err) {
env.on('error', err => {
console.error('Error', process.argv.slice(2).join(' '), '\n');
console.error(opts.debug ? err.stack : err.message);
console.error(firstCmd.opts.debug ? err.stack : err.message);
process.exit(err.code || 1);
});

// lookup for every namespaces, within the environments.paths and lookups
env.lookup(function () {
// list generators
if (opts.generators) {
console.log(env.getGeneratorNames().join('\n'));
// Lookup for every namespaces, within the environments.paths and lookups
env.lookup(() => {
const generatorList = createGeneratorList(env);

// List generators
if (firstCmd.opts.generators) {
console.log('Available Generators:\n\n' + generatorList);
return;
}

// start the interactive UI if no generator is passed
// Start the interactive UI if no generator is passed
if (!cmd) {
if (opts.help) {
var usageText = fs.readFileSync(path.join(__dirname, 'usage.txt'), 'utf8');
var generatorList = createGeneratorList(env);
console.log(usageText + '\nAvailable Generators:\n' + generatorList);
if (firstCmd.opts.help) {
const usageText = fs.readFileSync(path.join(__dirname, 'usage.txt'), 'utf8');
console.log(`${usageText}\nAvailable Generators:\n\n${generatorList}`);
return;
}

runYo(env);
return;
}

// More detailed error message
// If users type in generator name with prefix 'generator-'
if (cmd.startsWith('generator-')) {
const generatorName = cmd.replace('generator-', '');
const generatorCommand = chalk.yellow('yo ' + generatorName);

console.log(chalk.red('Installed generators don\'t need the "generator-" prefix.'));
console.log(`In the future, run ${generatorCommand} instead!\n`);

env.run(generatorName, firstCmd.opts);

return;
}

// Note: at some point, nopt needs to know about the generator options, the
// one that will be triggered by the below args. Maybe the nopt parsing
// should be done internally, from the args.
env.run(args, opts);
for (const gen of cli) {
env.run(gen.args, gen.opts);
}
});
}

function runYo(env) {
var router = new Router(env, insight);
const router = new Router(env, insight);
router.insight.track('yoyo', 'init');
router.registerRoute('help', require('./routes/help'));
router.registerRoute('update', require('./routes/update'));
@@ -140,33 +180,28 @@ function runYo(env) {

rootCheck('\n' + chalk.red('Easy with the `sudo`. Yeoman is the master around here.') + '\n\nSince yo is a user command, there is no need to execute it with root\npermissions. If you\'re having permission errors when using yo without sudo,\nplease spend a few minutes learning more about how your system should work\nand make any necessary repairs.\n\nA quick solution would be to change where npm stores global packages by\nputting ~/npm/bin in your PATH and running:\n' + chalk.blue('npm config set prefix ~/npm') + '\n\nSee: https://github.com/sindresorhus/guides/blob/master/npm-global-without-sudo.md');

var insightMsg = chalk.gray('==========================================================================') +
const insightMsg = chalk.gray('==========================================================================') +
chalk.yellow('\nWe\'re constantly looking for ways to make ') + chalk.bold.red(pkg.name) +
chalk.yellow(
' better! \nMay we anonymously report usage statistics to improve the tool over time? \n' +
'More info: https://github.com/yeoman/insight & http://yeoman.io'
) +
chalk.gray('\n==========================================================================');

insight = new Insight({
trackingCode: 'UA-31537568-1',
pkg: pkg
});

if (opts.insight === false) {
if (firstCmd.opts.insight === false) {
insight.config.set('optOut', true);
} else if (opts.insight) {
} else if (firstCmd.opts.insight) {
insight.config.set('optOut', false);
}

if (opts.insight !== false && insight.optOut === undefined) {
if (firstCmd.opts.insight !== false && insight.optOut === undefined) {
insight.optOut = insight.config.get('optOut');
insight.track('downloaded');
insight.askPermission(insightMsg, pre);
} else {
if (opts.insight !== false) {
// only track the two first subcommands
insight.track.apply(insight, args.slice(0, 2));
if (firstCmd.opts.insight !== false) {
// Only track the two first subcommands
insight.track(...firstCmd.args.slice(0, 2));
}

updateCheck();
112 changes: 112 additions & 0 deletions lib/completion/completer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use strict';
const path = require('path');
const {execFile} = require('child_process');
const parseHelp = require('parse-help');

/**
* The Completer is in charge of handling `yo-complete` behavior.
* @constructor
* @param {Environment} env A yeoman environment instance
*/
class Completer {
constructor(env) {
this.env = env;
}

/**
* Completion event done
*
* @param {String} data Environment object as parsed by tabtab
* @param {Function} done Callback to invoke with completion results
*/
complete(data, done) {
if (data.last !== 'yo' && !data.last.startsWith('-')) {
return this.generator(data, done);
}

this.env.lookup(err => {
if (err) {
done(err);
return;
}

const meta = this.env.getGeneratorsMeta();
const results = Object.keys(meta).map(this.item('yo'), this);
done(null, results);
});
}

/**
* Generator completion event done
*
* @param {String} data Environment object as parsed by tabtab
* @param {Function} done Callback to invoke with completion results
*/
generator(data, done) {
const {last} = data;
const bin = path.resolve(__dirname, '../cli.js');

execFile('node', [bin, last, '--help'], (err, out) => {
if (err) {
done(err);
return;
}

const results = this.parseHelp(last, out);
done(null, results);
});
}

/**
* Helper to format completion results into { name, description } objects
*
* @param {String} data Environment object as parsed by tabtab
* @param {Function} done Callback to invoke with completion results
*/
item(desc, prefix) {
prefix = prefix || '';
return item => {
const name = typeof item === 'string' ? item : item.name;
desc = typeof item !== 'string' && item.description ? item.description : desc;
desc = desc.replace(/^#?\s*/g, '');
desc = desc.replace(/:/g, '->');
desc = desc.replace(/'/g, ' ');

return {
name: prefix + name,
description: desc
};
};
}

/**
* Parse-help wrapper. Invokes parse-help with stdout result, returning the
* list of completion items for flags / alias.
*
* @param {String} last Last word in COMP_LINE (completed line in command line)
* @param {String} out Help output
*/
parseHelp(last, out) {
const help = parseHelp(out);
const alias = [];

let results = Object.keys(help.flags).map(key => {
const flag = help.flags[key];

if (flag.alias) {
alias.push(Object.assign({}, flag, {name: flag.alias}));
}

flag.name = key;

return flag;
}).map(this.item(last, '--'), this);

results = results.concat(alias.map(this.item(last.replace(':', '_'), '-'), this));
results = results.filter(r => r.name !== '--help' && r.name !== '-h');

return results;
}
}

module.exports = Completer;
21 changes: 21 additions & 0 deletions lib/completion/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#! /usr/bin/env node
'use strict';
const env = require('yeoman-environment').createEnv();
const tabtab = require('tabtab')({
name: 'yo',
cache: !process.env.YO_TEST
});
const Completer = require('./completer');

const completer = new Completer(env);

tabtab.completer = completer;

// Lookup installed generator in yeoman environment,
// respond completion results with each generator
tabtab.on('yo', completer.complete.bind(completer));

// Register complete command
tabtab.start();

module.exports = tabtab;
131 changes: 67 additions & 64 deletions lib/router.js
Original file line number Diff line number Diff line change
@@ -1,84 +1,87 @@
'use strict';
var path = require('path');
var _ = require('lodash');
var titleize = require('titleize');
var humanizeString = require('humanize-string');
var readPkgUp = require('read-pkg-up');
var updateNotifier = require('update-notifier');
var Configstore = require('configstore');
var namespaceToName = require('yeoman-environment').namespaceToName;
var pkg = require('../package.json');
const path = require('path');
const _ = require('lodash');
const titleize = require('titleize');
const humanizeString = require('humanize-string');
const readPkgUp = require('read-pkg-up');
const updateNotifier = require('update-notifier');
const Configstore = require('configstore');
const {namespaceToName} = require('yeoman-environment');

/**
* The router is in charge of handling `yo` different screens.
* @constructor
* @param {Environment} env A yeoman environment instance
* @param {Insight} insight An insight instance
* @param {Configstore} [conf] An optionnal config store instance
* @param {Configstore} [conf] An optional config store instance
*/
var Router = module.exports = function (env, insight, conf) {
this.routes = {};
this.env = env;
this.insight = insight;
this.conf = conf || new Configstore(pkg.name, {
generatorRunCount: {}
});
};

/**
* Navigate to a route
* @param {String} name Route name
* @param {*} arg A single argument to pass to the route handler
*/
Router.prototype.navigate = function (name, arg) {
if (typeof this.routes[name] === 'function') {
return this.routes[name].call(null, this, arg);
class Router {
constructor(env, insight, conf) {
const pkg = require('../package.json');
this.routes = {};
this.env = env;
this.insight = insight;
this.conf = conf || new Configstore(pkg.name, {
generatorRunCount: {}
});
}

throw new Error('no routes called: ' + name);
};
/**
* Navigate to a route
* @param {String} name Route name
* @param {*} arg A single argument to pass to the route handler
*/
navigate(name, arg) {
if (typeof this.routes[name] === 'function') {
return this.routes[name].call(null, this, arg);
}

/**
* Register a route handler
* @param {String} name Name of the route
* @param {Function} handler Route handler
*/
Router.prototype.registerRoute = function (name, handler) {
this.routes[name] = handler;
return this;
};
throw new Error(`No routes called: ${name}`);
}

/**
* Update the available generators in the app
* TODO: Move this function elsewhere, try to make it stateless.
*/
Router.prototype.updateAvailableGenerators = function () {
this.generators = {};
/**
* Register a route handler
* @param {String} name Name of the route
* @param {Function} handler Route handler
*/
registerRoute(name, handler) {
this.routes[name] = handler;
return this;
}

var resolveGenerators = function (generator) {
// Skip sub generators
if (!/(app|all)$/.test(generator.namespace)) {
return;
}
/**
* Update the available generators in the app
* TODO: Move this function elsewhere, try to make it stateless.
*/
updateAvailableGenerators() {
this.generators = {};

var pkg = readPkgUp.sync({cwd: path.dirname(generator.resolved)}).pkg;
const resolveGenerators = generator => {
// Skip sub generators
if (!/:(app|all)$/.test(generator.namespace)) {
return;
}

if (!pkg) {
return;
}
const {pkg} = readPkgUp.sync({cwd: path.dirname(generator.resolved)});

pkg.namespace = generator.namespace;
pkg.appGenerator = true;
pkg.prettyName = titleize(humanizeString(namespaceToName(generator.namespace)));
if (!pkg) {
return;
}

pkg.update = updateNotifier({pkg: pkg}).update;
pkg.namespace = generator.namespace;
pkg.appGenerator = true;
pkg.prettyName = titleize(humanizeString(namespaceToName(generator.namespace)));
pkg.update = updateNotifier({pkg}).update;

if (pkg.update && pkg.version !== pkg.update.latest) {
pkg.updateAvailable = true;
}
if (pkg.update && pkg.version !== pkg.update.latest) {
pkg.updateAvailable = true;
}

this.generators[pkg.name] = pkg;
};

this.generators[pkg.name] = pkg;
};
_.forEach(this.env.getGeneratorsMeta(), resolveGenerators);
}
}

_.each(this.env.getGeneratorsMeta(), resolveGenerators, this);
};
module.exports = Router;
36 changes: 17 additions & 19 deletions lib/routes/clear-config.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
'use strict';
var _ = require('lodash');
var chalk = require('chalk');
var inquirer = require('inquirer');
var namespaceToName = require('yeoman-environment').namespaceToName;
var globalConfig = require('../utils/global-config');
const _ = require('lodash');
const chalk = require('chalk');
const inquirer = require('inquirer');
const {namespaceToName} = require('yeoman-environment');
const globalConfig = require('../utils/global-config');

module.exports = function (app) {
module.exports = app => {
app.insight.track('yoyo', 'clearGlobalConfig');

var defaultChoices = [
const defaultChoices = [
{
name: 'Take me back home, Yo!',
value: 'home'
}
];

var generatorList = _.chain(globalConfig.getAll()).map(function (val, key) {
var prettyName = '';
var sort = 0;
const generatorList = _.chain(globalConfig.getAll()).map((val, key) => {
let prettyName = '';
let sort = 0;

// Remove version from generator name
var name = key.split(':')[0];
var generator = app.generators[name];
const name = key.split(':')[0];
const generator = app.generators[name];

if (generator) {
prettyName = generator.prettyName;
({prettyName} = generator);
sort = -app.conf.get('generatorRunCount')[namespaceToName(generator.namespace)] || 0;
} else {
prettyName = name.replace(/^generator-/, '') + chalk.red(' (not installed anymore)');
@@ -33,12 +33,10 @@ module.exports = function (app) {

return {
name: prettyName,
sort: sort,
sort,
value: key
};
}).compact().sortBy(function (generatorName) {
return generatorName.sort;
}).value();
}).compact().sortBy(generatorName => generatorName.sort).value();

if (generatorList.length > 0) {
generatorList.push(new inquirer.Separator());
@@ -48,15 +46,15 @@ module.exports = function (app) {
});
}

inquirer.prompt([{
return inquirer.prompt([{
name: 'whatNext',
type: 'list',
message: 'Which store would you like to clear?',
choices: _.flatten([
generatorList,
defaultChoices
])
}], function (answer) {
}]).then(answer => {
app.insight.track('yoyo', 'clearGlobalConfig', answer);

if (answer.whatNext === 'home') {
15 changes: 7 additions & 8 deletions lib/routes/exit.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
'use strict';
var yosay = require('yosay');
var repeating = require('repeating');
const yosay = require('yosay');

module.exports = function (app) {
module.exports = app => {
app.insight.track('yoyo', 'exit');

var PADDING = 5;
var url = 'http://yeoman.io';
var maxLength = url.length + PADDING;
var newLine = repeating(' ', maxLength);
const PADDING = 5;
const url = 'http://yeoman.io';
const maxLength = url.length + PADDING;
const newLine = ' '.repeat(maxLength);

console.log(yosay(
'Bye from us!' +
newLine +
'Chat soon.' +
newLine +
'Yeoman team ' + url,
{maxLength: maxLength}
{maxLength}
));
};
15 changes: 7 additions & 8 deletions lib/routes/help.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
'use strict';
var inquirer = require('inquirer');
var opn = require('opn');
const inquirer = require('inquirer');
const opn = require('opn');

module.exports = function (app) {
module.exports = app => {
app.insight.track('yoyo', 'help');

inquirer.prompt([{
return inquirer.prompt([{
name: 'whereTo',
type: 'list',
message: 'Here are a few helpful resources.\n' +
'\nI will open the link you select in your browser for you',
message: 'Here are a few helpful resources.\n\nI will open the link you select in your browser for you',
choices: [{
name: 'Take me to the documentation',
value: 'http://yeoman.io/learning/index.html'
value: 'http://yeoman.io/learning/'
}, {
name: 'View Frequently Asked Questions',
value: 'http://yeoman.io/learning/faq.html'
@@ -23,7 +22,7 @@ module.exports = function (app) {
name: 'Take me back home, Yo!',
value: 'home'
}]
}], function (answer) {
}]).then(answer => {
app.insight.track('yoyo', 'help', answer);

if (answer.whereTo === 'home') {
37 changes: 19 additions & 18 deletions lib/routes/home.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'use strict';
var _ = require('lodash');
var chalk = require('chalk');
var fullname = require('fullname');
var inquirer = require('inquirer');
var namespaceToName = require('yeoman-environment').namespaceToName;
var globalConfigHasContent = require('../utils/global-config').hasContent;
const _ = require('lodash');
const chalk = require('chalk');
const fullname = require('fullname');
const inquirer = require('inquirer');
const {isString} = require('lodash');
const {namespaceToName} = require('yeoman-environment');
const globalConfigHasContent = require('../utils/global-config').hasContent;

module.exports = function (app) {
var defaultChoices = [{
module.exports = app => {
const defaultChoices = [{
name: 'Install a generator',
value: 'install'
}, {
@@ -25,12 +26,12 @@ module.exports = function (app) {
});
}

var generatorList = _.chain(app.generators).map(function (generator) {
const generatorList = _.chain(app.generators).map(generator => {
if (!generator.appGenerator) {
return null;
}

var updateInfo = generator.updateAvailable ? chalk.dim.yellow(' ♥ Update Available!') : '';
const updateInfo = generator.updateAvailable ? chalk.dim.yellow(' ♥ Update Available!') : '';

return {
name: generator.prettyName + updateInfo,
@@ -39,12 +40,12 @@ module.exports = function (app) {
generator: generator.namespace
}
};
}).compact().sortBy(function (el) {
var generatorName = namespaceToName(el.value.generator);
}).compact().sortBy(el => {
const generatorName = namespaceToName(el.value.generator);
return -app.conf.get('generatorRunCount')[generatorName] || 0;
}).value();

if (generatorList.length) {
if (generatorList.length > 0) {
defaultChoices.unshift({
name: 'Update your generators',
value: 'update'
@@ -53,21 +54,21 @@ module.exports = function (app) {

app.insight.track('yoyo', 'home');

fullname().then(function (name) {
var allo = name ? ('\'Allo ' + name.split(' ')[0] + '! ') : '\'Allo! ';
return fullname().then(name => {
const allo = (name && isString(name)) ? `'Allo ${name.split(' ')[0]}! ` : '\'Allo! ';

inquirer.prompt([{
return inquirer.prompt([{
name: 'whatNext',
type: 'list',
message: allo + 'What would you like to do?',
message: `${allo}What would you like to do?`,
choices: _.flatten([
new inquirer.Separator('Run a generator'),
generatorList,
new inquirer.Separator(),
defaultChoices,
new inquirer.Separator()
])
}], function (answer) {
}]).then(answer => {
if (answer.whatNext.method === 'run') {
app.navigate('run', answer.whatNext.generator);
return;
133 changes: 55 additions & 78 deletions lib/routes/install.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
'use strict';
var async = require('async');
var chalk = require('chalk');
var inquirer = require('inquirer');
var spawn = require('cross-spawn-async');
var sortOn = require('sort-on');
var figures = require('figures');
var npmKeyword = require('npm-keyword');
var packageJson = require('package-json');
var got = require('got');

var OFFICIAL_GENERATORS = [
/* eslint-disable promise/no-callback-in-promise */
const _ = require('lodash');
const async = require('async');
const chalk = require('chalk');
const inquirer = require('inquirer');
const spawn = require('cross-spawn');
const sortOn = require('sort-on');
const figures = require('figures');
const npmKeyword = require('npm-keyword');
const packageJson = require('package-json');
const got = require('got');

const OFFICIAL_GENERATORS = [
'generator-angular',
'generator-backbone',
'generator-bootstrap',
@@ -29,109 +31,84 @@ var OFFICIAL_GENERATORS = [
'generator-webapp'
];

module.exports = function (app) {
module.exports = app => {
app.insight.track('yoyo', 'install');

inquirer.prompt([{
return inquirer.prompt([{
name: 'searchTerm',
message: 'Search npm for generators:'
}], function (answers) {
searchNpm(app, answers.searchTerm, function (err) {
if (err) {
throw err;
}
});
});
}]).then(answers => searchNpm(app, answers.searchTerm));
};

var getAllGenerators = (function () {
var allGenerators;

return function (cb) {
if (allGenerators) {
return cb(null, allGenerators);
}

npmKeyword('yeoman-generator').then(function (packages) {
cb(null, packages.map(function (el) {
el.match = function (term) {
return (el.name + ' ' + el.description).indexOf(term) >= 0;
};

return el;
}));
}).catch(cb);
};
})();
const generatorMatchTerm = (generator, term) => `${generator.name} ${generator.description}`.includes(term);
const getAllGenerators = _.memoize(() => npmKeyword('yeoman-generator'));

function searchMatchingGenerators(app, term, cb) {
got('yeoman.io/blacklist.json', {json: true}, function (err, data) {
var blacklist = err ? [] : data;
var installedGenerators = app.env.getGeneratorNames();

getAllGenerators(function (err, allGenerators) {
if (err) {
cb(err);
return;
}
function handleBlacklist(blacklist) {
const installedGenerators = app.env.getGeneratorNames();

cb(null, allGenerators.filter(function (generator) {
if (blacklist.indexOf(generator.name) !== -1) {
getAllGenerators().then(allGenerators => {
cb(null, allGenerators.filter(generator => {
if (blacklist.includes(generator.name)) {
return false;
}

if (installedGenerators.indexOf(generator.name) !== -1) {
if (installedGenerators.includes(generator.name)) {
return false;
}

return generator.match(term);
return generatorMatchTerm(generator, term);
}));
});
});
}, cb);
}
got('http://yeoman.io/blacklist.json', {json: true})
.then(response => handleBlacklist(response.body))
.catch(() => handleBlacklist([]));
}

function fetchGeneratorInfo(generator, cb) {
packageJson(generator.name).then(function (pkg) {
var official = OFFICIAL_GENERATORS.indexOf(pkg.name) !== -1;
var mustache = official ? chalk.green(' ' + figures.mustache + ' ') : '';
packageJson(generator.name, {fullMetadata: true}).then(pkg => {
const official = OFFICIAL_GENERATORS.includes(pkg.name);
const mustache = official ? chalk.green(` ${figures.mustache} `) : '';

cb(null, {
name: generator.name.replace(/^generator-/, '') +
// `gray` → `dim` when iTerm 2.9 is out
mustache + ' ' + chalk.gray(pkg.description),
name: generator.name.replace(/^generator-/, '') + mustache + ' ' + chalk.dim(pkg.description),
value: generator.name,
official: -official
});
}).catch(cb);
}

function searchNpm(app, term, cb) {
searchMatchingGenerators(app, term, function (err, matches) {
if (err) {
cb(err);
return;
}

async.map(matches, fetchGeneratorInfo, function (err, choices) {
function searchNpm(app, term) {
const promise = new Promise((resolve, reject) => {
searchMatchingGenerators(app, term, (err, matches) => {
if (err) {
cb(err);
reject(err);
return;
}

promptInstallOptions(app, sortOn(choices, ['official', 'name']));
cb();
async.map(matches, fetchGeneratorInfo, (err2, choices) => {
if (err2) {
reject(err2);
return;
}

resolve(choices);
});
});
});

return promise.then(choices => promptInstallOptions(app, sortOn(choices, ['official', 'name'])));
}

function promptInstallOptions(app, choices) {
var introMessage = 'Sorry, no results matches your search term';
let introMessage = 'Sorry, no results matches your search term';

if (choices.length > 0) {
introMessage = 'Here\'s what I found. ' + chalk.gray('Official generator → ' + chalk.green(figures.mustache)) + '\n Install one?';
}

var resultsPrompt = [{
const resultsPrompt = [{
name: 'toInstall',
type: 'list',
message: introMessage,
@@ -144,7 +121,7 @@ function promptInstallOptions(app, choices) {
}])
}];

inquirer.prompt(resultsPrompt, function (answer) {
return inquirer.prompt(resultsPrompt).then(answer => {
if (answer.toInstall === 'home' || answer.toInstall === 'install') {
return app.navigate(answer.toInstall);
}
@@ -156,20 +133,20 @@ function promptInstallOptions(app, choices) {
function installGenerator(app, pkgName) {
app.insight.track('yoyo', 'install', pkgName);

return spawn('npm', ['install', '-g', pkgName], {stdio: 'inherit'})
.on('error', function (err) {
return spawn('npm', ['install', '--global', pkgName], {stdio: 'inherit'})
.on('error', err => {
app.insight.track('yoyo:err', 'install', pkgName);
throw err;
})
.on('close', function () {
.on('close', () => {
app.insight.track('yoyo', 'installed', pkgName);

console.log(
'\nI just installed a generator by running:\n' +
chalk.blue.bold('\n npm install -g ' + pkgName + '\n')
);

app.env.lookup(function () {
app.env.lookup(() => {
app.updateAvailableGenerators();
app.navigate('home');
});
14 changes: 7 additions & 7 deletions lib/routes/run.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
'use strict';
var chalk = require('chalk');
var namespaceToName = require('yeoman-environment').namespaceToName;
const chalk = require('chalk');
const {namespaceToName} = require('yeoman-environment');

module.exports = function (app, name) {
var baseName = namespaceToName(name);
module.exports = (app, name) => {
const baseName = namespaceToName(name);
app.insight.track('yoyo', 'run', baseName);

console.log(
chalk.yellow('\nMake sure you are in the directory you want to scaffold into.') +
chalk.dim('\nThis generator can also be run with: ') +
chalk.blue('yo ' + baseName + '\n')
chalk.blue(`yo ${baseName}\n`)
);

// save the generator run count
var generatorRunCount = app.conf.get('generatorRunCount');
// Save the generator run count
const generatorRunCount = app.conf.get('generatorRunCount');
generatorRunCount[baseName] = generatorRunCount[baseName] + 1 || 1;
app.conf.set('generatorRunCount', generatorRunCount);
app.env.run(name);
27 changes: 15 additions & 12 deletions lib/routes/update.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,42 @@
'use strict';
var chalk = require('chalk');
var inquirer = require('inquirer');
var spawn = require('cross-spawn-async');
const chalk = require('chalk');
const inquirer = require('inquirer');
const spawn = require('cross-spawn');

var successMsg = 'I\'ve just updated your generators. Remember, you can update' +
'\na specific generator with npm by running:\n' +
const successMsg = 'I\'ve just updated your generators. Remember, you can update\na specific generator with npm by running:\n' +
chalk.magenta('\n npm install -g generator-_______');

function updateSuccess(app) {
app.insight.track('yoyo', 'updated');
console.log('\n' + chalk.cyan(successMsg) + '\n');
app.env.lookup(function () {
console.log(`\n${chalk.cyan(successMsg)}\n`);
app.env.lookup(() => {
app.updateAvailableGenerators();
app.navigate('home');
});
}

function updateGenerators(app, pkgs) {
spawn('npm', ['install', '-g'].concat(pkgs), {stdio: 'inherit'})
spawn('npm', ['install', '--global'].concat(pkgs), {stdio: 'inherit'})
.on('close', updateSuccess.bind(null, app));
}

module.exports = function (app) {
module.exports = app => {
app.insight.track('yoyo', 'update');
inquirer.prompt([{

return inquirer.prompt([{
name: 'generators',
message: 'Generators to update',
type: 'checkbox',
choices: Object.keys(app.generators || {}).map(function (key) {
validate(input) {
return input.length > 0 ? true : 'Please select at least one generator to update.';
},
choices: Object.keys(app.generators || {}).map(key => {
return {
name: app.generators[key].name,
checked: true
};
})
}], function (answer) {
}]).then(answer => {
updateGenerators(app, answer.generators);
});
};
27 changes: 27 additions & 0 deletions lib/usage.txt
Original file line number Diff line number Diff line change
@@ -7,3 +7,30 @@ General options:
--no-color # Disable colors
--[no-]insight # Toggle anonymous tracking
--generators # Print available generators

Install a generator:

Generators can be installed through npm.

$ npm install generator-angular
$ yo angular --help

Run local generators:

Additionally, you can also run local generators without installing via npm.

$ yo ./path/to/some/generator

Completion:

To enable shell completion for the yo command, try running

$ yo completion

Troubleshooting:

For any issues, try running

$ yo doctor

Full Documentation: http://yeoman.io
18 changes: 9 additions & 9 deletions lib/utils/global-config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use strict';
var fs = require('fs');
var path = require('path');
var userHome = require('user-home');
const fs = require('fs');
const path = require('path');
const os = require('os');

// Path to the config file
var globalConfigPath = path.join(userHome, '.yo-rc-global.json');
const globalConfigPath = path.join(os.homedir(), '.yo-rc-global.json');

function write(content) {
content = JSON.stringify(content, null, ' ');
@@ -32,7 +32,7 @@ function remove(name) {
return;
}

var content = getAll();
const content = getAll();
delete content[name];
write(content);
}
@@ -53,9 +53,9 @@ function hasContent() {
}

module.exports = {
getAll: getAll,
remove: remove,
removeAll: removeAll,
hasContent: hasContent,
getAll,
remove,
removeAll,
hasContent,
path: globalConfigPath
};
8,526 changes: 8,526 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

101 changes: 64 additions & 37 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
{
"name": "yo",
"version": "1.5.1",
"version": "2.0.3",
"description": "CLI tool for running Yeoman generators",
"license": "BSD-2-Clause",
"repository": "yeoman/yo",
"homepage": "http://yeoman.io",
"author": "Yeoman",
"bin": "lib/cli.js",
"main": "lib",
"bin": {
"yo": "lib/cli.js",
"yo-complete": "lib/completion/index.js"
},
"engines": {
"node": ">=0.12.0"
"node": ">=6"
},
"scripts": {
"test": "xo && mocha test/**/*.js",
"test": "xo && gulp",
"postinstall": "yodoctor",
"postupdate": "yodoctor",
"prepublish": "nsp check"
"postupdate": "yodoctor"
},
"files": [
"lib"
@@ -36,49 +40,72 @@
"boilerplate"
],
"dependencies": {
"async": "^1.0.0",
"chalk": "^1.0.0",
"configstore": "^1.0.0",
"cross-spawn-async": "^2.0.0",
"figures": "^1.3.5",
"fullname": "^2.0.0",
"got": "^5.0.0",
"async": "^2.1.4",
"chalk": "^2.4.1",
"cli-list": "^0.2.0",
"configstore": "^3.0.0",
"cross-spawn": "^6.0.5",
"figures": "^2.0.0",
"fullname": "^3.2.0",
"global-tunnel-ng": "^2.1.1",
"got": "^8.0.3",
"humanize-string": "^1.0.0",
"inquirer": "^0.11.0",
"insight": "^0.7.0",
"lodash": "^3.2.0",
"inquirer": "^6.0.0",
"insight": "^0.10.1",
"lodash": "^4.17.10",
"meow": "^3.0.0",
"npm-keyword": "^4.1.0",
"opn": "^3.0.2",
"package-json": "^2.1.0",
"read-pkg-up": "^1.0.1",
"repeating": "^2.0.0",
"npm-keyword": "^5.0.0",
"opn": "^5.3.0",
"package-json": "^5.0.0",
"parse-help": "^1.0.0",
"read-pkg-up": "^4.0.0",
"root-check": "^1.0.0",
"sort-on": "^1.0.0",
"string-length": "^1.0.0",
"sort-on": "^3.0.0",
"string-length": "^2.0.0",
"tabtab": "^1.3.2",
"titleize": "^1.0.0",
"update-notifier": "^0.5.0",
"update-notifier": "^2.1.0",
"user-home": "^2.0.0",
"yeoman-character": "^1.0.0",
"yeoman-doctor": "^2.0.0",
"yeoman-environment": "^1.2.7",
"yosay": "^1.0.0"
"yeoman-doctor": "^3.0.1",
"yeoman-environment": "^2.3.0",
"yosay": "^2.0.0"
},
"devDependencies": {
"mocha": "^2.1.0",
"mockery": "^1.4.0",
"nock": "^2.0.1",
"nsp": "^2.2.0",
"proxyquire": "^1.0.1",
"registry-url": "^3.0.0",
"gulp": "^3.6.0",
"gulp-coveralls": "^0.1.0",
"gulp-istanbul": "^1.0.0",
"gulp-mocha": "^3.0.1",
"gulp-plumber": "^1.0.0",
"mocha": "^5.2.0",
"mockery": "^2.0.0",
"nock": "^9.0.5",
"proxyquire": "^2.0.1",
"registry-url": "^4.0.0",
"sinon": "^1.12.1",
"xo": "*"
"xo": "^0.21.1"
},
"tabtab": {
"yo": [
"-f",
"--force",
"--version",
"--no-color",
"--no-insight",
"--insight",
"--generators"
]
},
"xo": {
"space": true,
"envs": [
"node",
"mocha"
"overrides": [
{
"files": "test/**",
"envs": [
"node",
"mocha"
]
}
]
}
}
104 changes: 103 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# yo [![Build Status](https://travis-ci.org/yeoman/yo.svg?branch=master)](https://travis-ci.org/yeoman/yo) [![](http://img.shields.io/badge/unicorn-approved-ff69b4.svg)](https://www.youtube.com/watch?v=9auOCbH5Ns4)
# yo [![npm](https://badge.fury.io/js/yo.svg)](http://badge.fury.io/js/yo) [![Build Status](https://travis-ci.org/yeoman/yo.svg?branch=master)](https://travis-ci.org/yeoman/yo) [![Coverage Status](https://coveralls.io/repos/yeoman/yo/badge.svg)](https://coveralls.io/r/yeoman/yo) [![](https://img.shields.io/badge/unicorn-approved-ff69b4.svg)](https://www.youtube.com/watch?v=9auOCbH5Ns4) [![Gitter](https://img.shields.io/badge/Gitter-Join_the_Yeoman_chat_%E2%86%92-00d06f.svg)](https://gitter.im/yeoman/yeoman) [![OpenCollective](https://opencollective.com/yeoman/backers/badge.svg)](https://opencollective.com/yeoman#support)

[![](https://raw.githubusercontent.com/yeoman/media/master/optimized/yeoman-masthead.png)](http://yeoman.io)

@@ -27,6 +27,12 @@ yo webapp

*To create and distribute your own generator, refer to [our official documentation](http://yeoman.io/authoring/)*

You can also run a local generator on your computer as such:

```sh
# Running a local generator
yo ./path/to/local/generator
```

## Options

@@ -56,6 +62,102 @@ See the [contributing docs](http://yeoman.io/contributing/).
See the [release page](https://github.com/yeoman/yo/releases).


## Backers
Love Yeoman work and community? Help us keep it alive by donating funds to cover project expenses! <br />
[[Become a backer](https://opencollective.com/yeoman#support)]

<a href="https://opencollective.com/yeoman/backers/0/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/0/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/1/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/1/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/2/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/2/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/3/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/3/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/4/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/4/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/5/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/5/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/6/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/6/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/7/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/7/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/8/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/8/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/9/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/9/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/10/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/10/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/11/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/11/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/12/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/12/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/13/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/13/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/14/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/14/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/15/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/15/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/16/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/16/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/17/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/17/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/18/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/18/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/19/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/19/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/20/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/20/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/21/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/21/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/22/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/22/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/23/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/23/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/24/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/24/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/25/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/25/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/26/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/26/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/27/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/27/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/28/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/28/avatar">
</a>
<a href="https://opencollective.com/yeoman/backers/29/website" target="_blank">
<img src="https://opencollective.com/yeoman/backers/29/avatar">
</a>


## License

BSD-2-Clause © Google
55 changes: 29 additions & 26 deletions test/cli.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
'use strict';
var path = require('path');
var assert = require('assert');
var execFile = require('child_process').execFile;
var mockery = require('mockery');
var pkg = require('../package.json');
const path = require('path');
const assert = require('assert');
const {execFile} = require('child_process');
const mockery = require('mockery');
const pkg = require('../package.json');

describe('bin', function () {
describe('mocked', function () {
describe('bin', () => {
describe('mocked', () => {
beforeEach(function () {
this.origArgv = process.argv;
this.origExit = process.exit;
@@ -17,9 +17,7 @@ describe('bin', function () {
});

mockery.registerMock('yeoman-environment', {
createEnv: function () {
return this.env;
}.bind(this)
createEnv: () => this.env
});
});

@@ -29,43 +27,48 @@ describe('bin', function () {
process.exit = this.origExit;
});

it('should exit with status 1 if there were errors', function (cb) {
var called = false;
process.exit = function (arg) {
it('should exit with status 1 if there were errors', function (done) {
let called = false;
process.exit = arg => {
if (called) {
// Exit can be called more than once.
// Exit can be called more than once
return;
}

called = true;
assert(arg, 1, 'exit code should be 1');
cb();
done();
};

process.argv = ['node', path.join(__dirname, '../', pkg.bin), 'notexisting'];
process.argv = ['node', path.resolve(__dirname, '..', pkg.bin.yo), 'non-existent'];

this.env.lookup = function (cb) {
this.env.lookup = cb => {
cb();
};

require('../lib/cli');
require('../lib/cli'); // eslint-disable-line import/no-unassigned-import
});
});

it('should return the version', function (cb) {
var cp = execFile('node', [path.join(__dirname, '../', pkg.bin), '--version', '--no-insight', '--no-update-notifier']);
var expected = pkg.version;
it('should return the version', cb => {
const cp = execFile('node', [
path.resolve(__dirname, '..', pkg.bin.yo),
'--version',
'--no-insight',
'--no-update-notifier'
]);
const expected = pkg.version;

cp.stdout.on('data', function (data) {
assert.equal(data.replace(/\r\n|\n/g, ''), expected);
cp.stdout.on('data', data => {
assert.equal(data.toString().replace(/\r\n|\n/g, ''), expected);
cb();
});
});

it('should output available generators when `--generators` flag is supplied', function (cb) {
var cp = execFile('node', [path.join(__dirname, '../', pkg.bin), '--generators', '--no-insight', '--no-update-notifier']);
it('should output available generators when `--generators` flag is supplied', cb => {
const cp = execFile('node', [path.resolve(__dirname, '..', pkg.bin.yo), '--generators', '--no-insight', '--no-update-notifier']);

cp.stdout.once('data', function (data) {
cp.stdout.once('data', data => {
assert(data.length > 0);
assert(!/\[/.test(data));
cb();
164 changes: 164 additions & 0 deletions test/completion.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
'use strict';
const path = require('path');
const assert = require('assert');
const events = require('events');
const {execFile} = require('child_process');
const {find} = require('lodash');
const Completer = require('../lib/completion/completer');
const completion = require('../lib/completion');

const help = `
Usage:
yo backbone:app [options] [<app_name>]
Options:
-h, --help # Print the generator's options and usage
--skip-cache # Do not remember prompt answers Default: false
--skip-install # Do not automatically install dependencies Default: false
--appPath # Name of application directory Default: app
--requirejs # Support requirejs Default: false
--template-framework # Choose template framework. lodash/handlebars/mustache Default: lodash
--test-framework # Choose test framework. mocha/jasmine Default: mocha
Arguments:
app_name Type: String Required: false`;

describe('Completion', () => {
before(function () {
this.env = require('yeoman-environment').createEnv();
});

describe('Test completion STDOUT output', () => {
it('Returns the completion candidates for both options and installed generators', done => {
const yocomplete = path.join(__dirname, '../lib/completion/index.js');
const yo = path.join(__dirname, '../lib/cli');

let cmd = 'export cmd="yo" && DEBUG="tabtab*" COMP_POINT="4" COMP_LINE="$cmd" COMP_CWORD="$cmd"';
cmd += `node ${yocomplete} completion -- ${yo} $cmd`;

execFile('bash', ['-c', cmd], (err, out) => {
if (err) {
done(err);
return;
}

assert.ok(/-f/.test(out));
assert.ok(/--force/.test(out));
assert.ok(/--version/.test(out));
assert.ok(/--no-color/.test(out));
assert.ok(/--no-insight/.test(out));
assert.ok(/--insight/.test(out));
assert.ok(/--generators/.test(out));

done();
});
});
});

describe('Completion', () => {
it('Creates tabtab instance', () => {
assert.ok(completion instanceof events);
});
});

describe('Completer', () => {
beforeEach(function () {
// Mock / Monkey patch env.getGeneratorsMeta() here, since we pass the
// instance directly to completer.
this.getGeneratorsMeta = this.env.getGeneratorsMeta;

this.env.getGeneratorsMeta = () => ({
'dummy:app': {
resolved: '/home/user/.nvm/versions/node/v6.1.0/lib/node_modules/generator-dummy/app/index.js',
namespace: 'dummy:app'
},
'dummy:yo': {
resolved: '/home/user/.nvm/versions/node/v6.1.0/lib/node_modules/generator-dummy/yo/index.js',
namespace: 'dummy:yo'
}
});

this.completer = new Completer(this.env);
});

afterEach(function () {
this.env.getGeneratorsMeta = this.getGeneratorsMeta;
});

describe('#parseHelp', () => {
it('Returns completion items based on help output', function () {
const results = this.completer.parseHelp('backbone:app', help);
const first = results[0];

assert.equal(results.length, 6);
assert.deepEqual(first, {
name: '--skip-cache',
description: 'Do not remember prompt answers Default-> false'
});
});
});

describe('#item', () => {
it('Format results into { name, description }', function () {
const list = ['foo', 'bar'];
const results = list.map(this.completer.item('yo!', '--'));
assert.deepEqual(results, [{
name: '--foo',
description: 'yo!'
}, {
name: '--bar',
description: 'yo!'
}]);
});

it('Escapes certain characters before consumption by shell scripts', function () {
const list = ['foo'];

const desc = '# yo I\'m a very subtle description, with chars that likely will break your Shell: yeah I\'m mean';
const expected = 'yo I m a very subtle description, with chars that likely will break your Shell-> yeah I m mean';
const results = list.map(this.completer.item(desc, '-'));

assert.equal(results[0].description, expected);
});
});

describe('#generator', () => {
it('Returns completion candidates from generator help output', function (done) {
// Here we test against yo --help (could use dummy:yo --help)
this.completer.complete({last: ''}, (err, results) => {
if (err) {
done(err);
return;
}

/* eslint no-multi-spaces: 0 */
assert.deepEqual(results, [
{name: '--force', description: 'Overwrite files that already exist'},
{name: '--version', description: 'Print version'},
{name: '--no-color', description: 'Disable colors'},
{name: '-f', description: 'Overwrite files that already exist'}
]);

done();
});
});
});

describe('#complete', () => {
it('Returns the list of user installed generators as completion candidates', function (done) {
this.completer.complete({last: 'yo'}, (err, results) => {
if (err) {
done(err);
return;
}

const dummy = find(results, result => result.name === 'dummy:yo');
assert.equal(dummy.name, 'dummy:yo');
assert.equal(dummy.description, 'yo');

done();
});
});
});
});
});
14 changes: 7 additions & 7 deletions test/global-config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use strict';
var assert = require('assert');
var fs = require('fs');
var sinon = require('sinon');
var globalConfig = require('../lib/utils/global-config');
const assert = require('assert');
const fs = require('fs');
const sinon = require('sinon');
const globalConfig = require('../lib/utils/global-config');

describe('global config', function () {
describe('global config', () => {
beforeEach(function () {
this.sandbox = sinon.sandbox.create();
});
@@ -13,7 +13,7 @@ describe('global config', function () {
this.sandbox.restore();
});

describe('.getAll()', function () {
describe('.getAll()', () => {
it('when config file exists', function () {
this.sandbox.stub(fs, 'existsSync').returns(true);
this.sandbox.stub(fs, 'readFileSync').returns('{"foo": "bar"}');
@@ -26,7 +26,7 @@ describe('global config', function () {
});
});

describe('.hasContent()', function () {
describe('.hasContent()', () => {
it('when config is present', function () {
this.sandbox.stub(fs, 'existsSync').returns(true);
this.sandbox.stub(fs, 'readFileSync').returns('{"foo": "bar"}');
22 changes: 10 additions & 12 deletions test/helpers.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
'use strict';
var sinon = require('sinon');
var yeoman = require('yeoman-environment');
const sinon = require('sinon');
const yeoman = require('yeoman-environment');

exports.fakeInsight = function () {
return {
track: sinon.stub()
};
};
exports.fakeInsight = () => ({
track: sinon.stub()
});

exports.fakeCrossSpawn = function (event) {
exports.fakeCrossSpawn = event => {
return sinon.stub().returns({
on: function (name, cb) {
on(name, cb) {
if (name === event) {
cb();
}
@@ -20,9 +18,9 @@ exports.fakeCrossSpawn = function (event) {
});
};

exports.fakeEnv = function () {
var env = yeoman.createEnv();
env.lookup = sinon.spy(function (cb) {
exports.fakeEnv = () => {
const env = yeoman.createEnv();
env.lookup = sinon.spy(cb => {
cb();
});
env.run = sinon.stub();
91 changes: 47 additions & 44 deletions test/route-clear-config.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
'use strict';
var assert = require('assert');
var proxyquire = require('proxyquire');
var sinon = require('sinon');
var _ = require('lodash');
var inquirer = require('inquirer');
var Router = require('../lib/router');
var helpers = require('./helpers');
const assert = require('assert');
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const _ = require('lodash');
const inquirer = require('inquirer');
const Router = require('../lib/router');
const helpers = require('./helpers');

describe('clear config route', function () {
describe('clear config route', () => {
beforeEach(function () {
this.sandbox = sinon.sandbox.create();
this.insight = helpers.fakeInsight();
this.globalConfig = {
remove: sinon.stub(),
removeAll: sinon.stub(),
getAll: function () {
getAll() {
return {
'generator-phoenix': {},
'generator-unicorn': {}
};
}
};
var conf = {
get: function () {
const conf = {
get() {
return {
unicorn: 20,
phoenix: 10
@@ -32,7 +32,7 @@ describe('clear config route', function () {
this.homeRoute = sinon.spy();
this.router = new Router(sinon.stub(), this.insight, conf);
this.router.registerRoute('home', this.homeRoute);
var clearConfig = proxyquire('../lib/routes/clear-config', {
const clearConfig = proxyquire('../lib/routes/clear-config', {
'../utils/global-config': this.globalConfig
});

@@ -56,56 +56,59 @@ describe('clear config route', function () {
});

it('allow returning home', function () {
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
cb({whereTo: 'home'});
this.sandbox.stub(inquirer, 'prompt').returns(Promise.resolve({whatNext: 'home'}));
return this.router.navigate('clearConfig').then(() => {
sinon.assert.calledOnce(this.homeRoute);
});
this.router.navigate('clearConfig');
sinon.assert.calledOnce(this.homeRoute);
});

it('track page and answer', function () {
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
cb({whatNext: 'generator-angular:0.0.0'});
this.sandbox.stub(inquirer, 'prompt').returns(
Promise.resolve({whatNext: 'generator-angular:0.0.0'})
);
return this.router.navigate('clearConfig').then(() => {
sinon.assert.calledWith(this.insight.track, 'yoyo', 'clearGlobalConfig');
sinon.assert.calledWith(
this.insight.track,
'yoyo',
'clearGlobalConfig',
{whatNext: 'generator-angular:0.0.0'}
);
});
this.router.navigate('clearConfig');
sinon.assert.calledWith(this.insight.track, 'yoyo', 'clearGlobalConfig');
sinon.assert.calledWith(this.insight.track, 'yoyo', 'clearGlobalConfig', {whatNext: 'generator-angular:0.0.0'});
});

it('allows clearing a generator and return user to home screen', function () {
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
cb({whatNext: 'foo'});
this.sandbox.stub(inquirer, 'prompt').returns(Promise.resolve({whatNext: 'foo'}));
this.router.navigate('clearConfig').then(() => {
sinon.assert.calledOnce(this.globalConfig.remove);
sinon.assert.calledWith(this.globalConfig.remove, 'foo');
sinon.assert.calledOnce(this.homeRoute);
});
this.router.navigate('clearConfig');
sinon.assert.calledOnce(this.globalConfig.remove);
sinon.assert.calledWith(this.globalConfig.remove, 'foo');
sinon.assert.calledOnce(this.homeRoute);
});

it('allows clearing all generators and return user to home screen', function () {
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
cb({whatNext: '*'});
this.sandbox.stub(inquirer, 'prompt').returns(Promise.resolve({whatNext: '*'}));
return this.router.navigate('clearConfig').then(() => {
sinon.assert.calledOnce(this.globalConfig.removeAll);
sinon.assert.calledOnce(this.homeRoute);
});
this.router.navigate('clearConfig');
sinon.assert.calledOnce(this.globalConfig.removeAll);
sinon.assert.calledOnce(this.homeRoute);
});

it('shows generator with global config entry', function () {
var choices = [];
let choices = [];

this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
choices = arg[0].choices;
cb({whatNext: 'foo'});
this.sandbox.stub(inquirer, 'prompt', arg => {
({choices} = arg[0]);
return Promise.resolve({whatNext: 'foo'});
});
this.router.navigate('clearConfig');
return this.router.navigate('clearConfig').then(() => {
// Clear all generators entry is present
assert.ok(_.find(choices, {value: '*'}));

// Clear all generators entry is present
assert.ok(_.find(choices, {value: '*'}));

assert.ok(_.find(choices, {value: 'generator-unicorn'}));
assert.ok(_.find(choices, {value: 'generator-phoenix'}));
assert.ok(_.find(choices, {name: 'Unicorn'}));
assert.ok(_.find(choices, {name: 'phoenix\u001b[31m (not installed anymore)\u001b[39m'}));
assert.ok(_.find(choices, {value: 'generator-unicorn'}));
assert.ok(_.find(choices, {value: 'generator-phoenix'}));
assert.ok(_.find(choices, {name: 'Unicorn'}));
assert.ok(_.find(choices, {name: 'phoenix\u001B[31m (not installed anymore)\u001B[39m'}));
});
});
});
8 changes: 4 additions & 4 deletions test/route-exit.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use strict';
var sinon = require('sinon');
var Router = require('../lib/router');
var helpers = require('./helpers');
const sinon = require('sinon');
const Router = require('../lib/router');
const helpers = require('./helpers');

describe('exit route', function () {
describe('exit route', () => {
beforeEach(function () {
this.insight = helpers.fakeInsight();
this.router = new Router(sinon.stub(), this.insight);
42 changes: 19 additions & 23 deletions test/route-help.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
'use strict';
var proxyquire = require('proxyquire');
var sinon = require('sinon');
var inquirer = require('inquirer');
var Router = require('../lib/router');
var helpers = require('./helpers');
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const inquirer = require('inquirer');
const Router = require('../lib/router');
const helpers = require('./helpers');

describe('help route', function () {
describe('help route', () => {
beforeEach(function () {
this.sandbox = sinon.sandbox.create();
this.insight = helpers.fakeInsight();
this.homeRoute = sinon.spy();
this.router = new Router(sinon.stub(), this.insight);
this.router.registerRoute('home', this.homeRoute);

this.opn = sinon.stub();
var helpRoute = proxyquire('../lib/routes/help', {
const helpRoute = proxyquire('../lib/routes/help', {
opn: this.opn
});
this.router.registerRoute('help', helpRoute);
@@ -25,29 +24,26 @@ describe('help route', function () {
});

it('allow returning home', function () {
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
cb({whereTo: 'home'});
this.sandbox.stub(inquirer, 'prompt').returns(Promise.resolve({whereTo: 'home'}));
return this.router.navigate('help').then(() => {
sinon.assert.calledOnce(this.homeRoute);
});
this.router.navigate('help');
sinon.assert.calledOnce(this.homeRoute);
});

it('track page and answer', function () {
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
cb({whereTo: 'home'});
this.sandbox.stub(inquirer, 'prompt').returns(Promise.resolve({whereTo: 'home'}));
return this.router.navigate('help').then(() => {
sinon.assert.calledWith(this.insight.track, 'yoyo', 'help');
sinon.assert.calledWith(this.insight.track, 'yoyo', 'help', {whereTo: 'home'});
});
this.router.navigate('help');
sinon.assert.calledWith(this.insight.track, 'yoyo', 'help');
sinon.assert.calledWith(this.insight.track, 'yoyo', 'help', {whereTo: 'home'});
});

it('open urls in browsers', function () {
var url = 'http://yeoman.io';
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
cb({whereTo: url});
const url = 'http://yeoman.io';
this.sandbox.stub(inquirer, 'prompt').returns(Promise.resolve({whereTo: url}));
return this.router.navigate('help').then(() => {
sinon.assert.calledWith(this.opn, url);
sinon.assert.calledOnce(this.opn);
});
this.router.navigate('help');
sinon.assert.calledWith(this.opn, url);
sinon.assert.calledOnce(this.opn);
});
});
98 changes: 47 additions & 51 deletions test/route-home.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
'use strict';
var _ = require('lodash');
var assert = require('assert');
var sinon = require('sinon');
var inquirer = require('inquirer');
var Router = require('../lib/router');
var helpers = require('./helpers');

describe('home route', function () {
const assert = require('assert');
const _ = require('lodash');
const sinon = require('sinon');
const inquirer = require('inquirer');
const Router = require('../lib/router');
const helpers = require('./helpers');

describe('home route', () => {
beforeEach(function () {
this.sandbox = sinon.sandbox.create();
this.insight = helpers.fakeInsight();
this.env = helpers.fakeEnv();
this.router = new Router(this.env, this.insight);
this.router.registerRoute('home', require('../lib/routes/home'));

this.runRoute = sinon.spy();
this.router.registerRoute('run', this.runRoute);

this.helpRoute = sinon.spy();
this.router.registerRoute('help', this.helpRoute);
this.installRoute = sinon.spy();
@@ -29,93 +27,91 @@ describe('home route', function () {
this.sandbox.restore();
});

it('track usage', function (done) {
this.sandbox.stub(inquirer, 'prompt', function (prompts, cb) {
it('track usage', function () {
this.sandbox.stub(inquirer, 'prompt').returns(Promise.resolve({whatNext: 'exit'}));
return this.router.navigate('home').then(() => {
sinon.assert.calledWith(this.insight.track, 'yoyo', 'home');
cb({whatNext: 'exit'});
done();
}.bind(this));
this.router.navigate('home');
});
});

it('allow going to help', function (done) {
this.sandbox.stub(inquirer, 'prompt', function (prompts, cb) {
cb({whatNext: 'help'});
it('allow going to help', function () {
this.sandbox.stub(inquirer, 'prompt').returns(Promise.resolve({whatNext: 'help'}));
return this.router.navigate('home').then(() => {
sinon.assert.calledOnce(this.helpRoute);
done();
}.bind(this));
this.router.navigate('home');
});
});

it('allow going to install', function (done) {
this.sandbox.stub(inquirer, 'prompt', function (prompts, cb) {
cb({whatNext: 'install'});
it('allow going to install', function () {
this.sandbox.stub(inquirer, 'prompt').returns(Promise.resolve({whatNext: 'install'}));
return this.router.navigate('home').then(() => {
sinon.assert.calledOnce(this.installRoute);
done();
}.bind(this));
this.router.navigate('home');
});
});

it('does not display update options if no generators is installed', function (done) {
it('does not display update options if no generators is installed', function () {
this.router.generator = [];
this.sandbox.stub(inquirer, 'prompt', function (prompts) {
assert.equal(_.pluck(prompts[0].choices, 'value').indexOf('update'), -1);
done();
this.sandbox.stub(inquirer, 'prompt', prompts => {
assert.equal(_.map(prompts[0].choices, 'value').includes('update'), false);
return Promise.resolve({whatNext: 'exit'});
});
this.router.navigate('home');

return this.router.navigate('home');
});

it('show update menu option if there is installed generators', function (done) {
it('show update menu option if there is installed generators', function () {
this.router.generators = [{
namespace: 'unicorn:app',
appGenerator: true,
prettyName: 'unicorn',
updateAvailable: false
}];

this.sandbox.stub(inquirer, 'prompt', function (prompts, cb) {
assert(_.pluck(prompts[0].choices, 'value').indexOf('update') >= 0);
cb({whatNext: 'update'});
this.sandbox.stub(inquirer, 'prompt', prompts => {
assert(_.map(prompts[0].choices, 'value').includes('update'));
return Promise.resolve({whatNext: 'update'});
});

return this.router.navigate('home').then(() => {
sinon.assert.calledOnce(this.updateRoute);
done();
}.bind(this));
this.router.navigate('home');
});
});

it('list runnable generators', function (done) {
it('list runnable generators', function () {
this.router.generators = [{
namespace: 'unicorn:app',
appGenerator: true,
prettyName: 'unicorn',
updateAvailable: false
}];

this.sandbox.stub(inquirer, 'prompt', function (prompts, cb) {
this.sandbox.stub(inquirer, 'prompt', prompts => {
assert.equal(prompts[0].choices[1].value.generator, 'unicorn:app');
cb({
return Promise.resolve({
whatNext: {
method: 'run',
generator: 'unicorn:app'
}
});
});

return this.router.navigate('home').then(() => {
sinon.assert.calledWith(this.runRoute, this.router, 'unicorn:app');
done();
}.bind(this));
this.router.navigate('home');
});
});

it('show update available message behind generator name', function (done) {
it('show update available message behind generator name', function () {
this.router.generators = [{
namespace: 'unicorn:app',
appGenerator: true,
prettyName: 'unicorn',
updateAvailable: true
}];

this.sandbox.stub(inquirer, 'prompt', function (prompts) {
assert(prompts[0].choices[1].name.indexOf('♥ Update Available!') >= 0);
done();
this.sandbox.stub(inquirer, 'prompt', prompts => {
assert(prompts[0].choices[1].name.includes('♥ Update Available!'));
return Promise.resolve({whatNext: 'exit'});
});
this.router.navigate('home');

return this.router.navigate('home');
});
});
230 changes: 138 additions & 92 deletions test/route-install.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,64 @@
'use strict';
var _ = require('lodash');
var assert = require('assert');
var inquirer = require('inquirer');
var nock = require('nock');
var proxyquire = require('proxyquire');
var sinon = require('sinon');
var registryUrl = require('registry-url')();
var Router = require('../lib/router');
var helpers = require('./helpers');

describe('install route', function () {
const assert = require('assert');
const _ = require('lodash');
const inquirer = require('inquirer');
const nock = require('nock');
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const registryUrl = require('registry-url')();
const Router = require('../lib/router');
const helpers = require('./helpers');

describe('install route', () => {
beforeEach(function () {
this.sandbox = sinon.sandbox.create();
this.insight = helpers.fakeInsight();

this.env = helpers.fakeEnv();

this.homeRoute = sinon.spy();
this.router = new Router(this.env, this.insight);
this.router.registerRoute('home', this.homeRoute);

this.spawn = helpers.fakeCrossSpawn('close');
var installRoute = proxyquire('../lib/routes/install', {
'cross-spawn-async': this.spawn
});
this.router.registerRoute('install', installRoute);
this.router.registerRoute('install', proxyquire('../lib/routes/install', {
'cross-spawn': this.spawn
}));
this.env.registerStub(_.noop, 'generator-unicorn');
});

afterEach(function () {
this.sandbox.restore();
});

describe('npm success with results', function () {
describe('npm success with results', () => {
beforeEach(function () {
this.rows = [
{key: ['yeoman-generator', 'generator-unicorn', 'some unicorn']},
{key: ['yeoman-generator', 'generator-unrelated', 'some description']},
{key: ['yeoman-generator', 'generator-unicorn-1', 'foo description']},
{key: ['yeoman-generator', 'generator-foo', 'description with unicorn word']},
{key: ['yeoman-generator', 'generator-blacklist-1', 'foo description']},
{key: ['yeoman-generator', 'generator-blacklist-2', 'foo description']},
{key: ['yeoman-generator', 'generator-blacklist-3', 'foo description']}
this.packages = [
{
name: 'generator-unicorn',
description: 'some unicorn'
},
{
name: 'generator-unrelated',
description: 'some description'
},
{
name: 'generator-unicorn-1',
description: 'foo description'
},
{
name: 'generator-foo',
description: 'description with unicorn word'
},
{
name: 'generator-blacklist-1',
description: 'foo description'
},
{
name: 'generator-blacklist-2',
description: 'foo description'
},
{
name: 'generator-blacklist-3',
description: 'foo description'
}
];

this.blacklist = [
@@ -50,149 +67,178 @@ describe('install route', function () {
];

this.pkgData = {
author: {
name: 'Simon'
'dist-tags': {
latest: '1.0.0'
},
versions: {
'1.0.0': {
name: 'test'
}
}
};

nock(registryUrl)
.get('/-/_view/byKeyword')
.query(true)
.reply(200, {rows: this.rows})
.filteringPath(/\/[^\?]+$/g, '/pkg')
.get('/pkg')
.times(2)
.reply(200, this.pkgData);
.get('/-/v1/search')
.query(true)
.reply(200, {objects: this.packages.map(data => ({package: data}))})
.filteringPath(/\/[^?]+$/g, '/pkg')
.get('/pkg')
.times(4)
.reply(200, this.pkgData);

nock('http://yeoman.io')
.get('/blacklist.json')
.times(2)
.reply(200, this.blacklist);
});

afterEach(() => {
nock.cleanAll();
});

it('filters already installed generators and match search term', function (done) {
var call = 0;
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
let call = 0;
this.sandbox.stub(inquirer, 'prompt', arg => {
call++;
if (call === 1) {
return cb({searchTerm: 'unicorn'});
return Promise.resolve({searchTerm: 'unicorn'});
}
if (call === 2) {
var choices = arg[0].choices;
assert.equal(_.where(choices, {value: 'generator-foo'}).length, 1);
assert.equal(_.where(choices, {value: 'generator-unicorn-1'}).length, 1);
assert.equal(_.where(choices, {value: 'generator-unicorn'}).length, 0);
assert.equal(_.where(choices, {value: 'generator-unrelated'}).length, 0);
const {choices} = arg[0];
assert.equal(_.filter(choices, {value: 'generator-foo'}).length, 1);
assert.equal(_.filter(choices, {value: 'generator-unicorn-1'}).length, 1);
assert.equal(_.filter(choices, {value: 'generator-unicorn'}).length, 0);
assert.equal(_.filter(choices, {value: 'generator-unrelated'}).length, 0);
done();
}

return Promise.resolve({toInstall: 'home'});
});

this.router.navigate('install');
});

it('filters blacklisted generators and match search term', function (done) {
var call = 0;
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
let call = 0;
this.sandbox.stub(inquirer, 'prompt', arg => {
call++;
if (call === 1) {
return cb({searchTerm: 'blacklist'});
return Promise.resolve({searchTerm: 'blacklist'});
}
if (call === 2) {
var choices = arg[0].choices;
assert.equal(_.where(choices, {value: 'generator-blacklist-1'}).length, 0);
assert.equal(_.where(choices, {value: 'generator-blacklist-2'}).length, 0);
assert.equal(_.where(choices, {value: 'generator-blacklist-3'}).length, 1);
const {choices} = arg[0];
assert.equal(_.filter(choices, {value: 'generator-blacklist-1'}).length, 0);
assert.equal(_.filter(choices, {value: 'generator-blacklist-2'}).length, 0);
assert.equal(_.filter(choices, {value: 'generator-blacklist-3'}).length, 1);
done();
}

return Promise.resolve({toInstall: 'home'});
});

this.router.navigate('install');
});

it('allow redo the search', function (done) {
var call = 0;
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
let call = 0;
this.sandbox.stub(inquirer, 'prompt', arg => {
call++;
if (call === 1) {
return cb({searchTerm: 'unicorn'});
return Promise.resolve({searchTerm: 'unicorn'});
}
if (call === 2) {
return cb({toInstall: 'install'});
return Promise.resolve({toInstall: 'install'});
}
if (call === 3) {
assert.equal(arg[0].name, 'searchTerm');
done();
return Promise.resolve({searchTerm: 'unicorn'});
}

done();
return Promise.resolve({toInstall: 'home'});
});

this.router.navigate('install');
});

it('allow going back home', function (done) {
var call = 0;
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
it('allow going back home', function () {
let call = 0;
this.sandbox.stub(inquirer, 'prompt', () => {
call++;
if (call === 1) {
return cb({searchTerm: 'unicorn'});
}
if (call === 2) {
cb({toInstall: 'home'});
sinon.assert.calledOnce(this.homeRoute);
done();
return Promise.resolve({searchTerm: 'unicorn'});
}
}.bind(this));

this.router.navigate('install');
return Promise.resolve({toInstall: 'home'});
});

return this.router.navigate('install').then(() => {
sinon.assert.calledOnce(this.homeRoute);
});
});

it('install a generator', function (done) {
var call = 0;
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
it('install a generator', function () {
let call = 0;
this.sandbox.stub(inquirer, 'prompt', () => {
call++;
if (call === 1) {
return cb({searchTerm: 'unicorn'});
return Promise.resolve({searchTerm: 'unicorn'});
}
if (call === 2) {
cb({toInstall: 'generator-unicorn'});
sinon.assert.calledWith(this.spawn, 'npm', ['install', '-g', 'generator-unicorn'], {stdio: 'inherit'});
sinon.assert.calledOnce(this.spawn);
sinon.assert.calledOnce(this.homeRoute);
done();
return Promise.resolve({toInstall: 'generator-unicorn'});
}
}.bind(this));

this.router.navigate('install');
return Promise.resolve({toInstall: 'home'});
});

return this.router.navigate('install').then(() => {
sinon.assert.calledWith(this.spawn, 'npm', ['install', '--global', 'generator-unicorn'], {stdio: 'inherit'});
sinon.assert.calledOnce(this.spawn);
sinon.assert.calledOnce(this.homeRoute);
});
});
});

describe('npm success without results', function () {
beforeEach(function () {
this.rows = [
{key: ['yeoman-generator', 'generator-unrelated', 'some description']},
{key: ['yeoman-generator', 'generator-unrelevant', 'some description']}
];

describe('npm success without results', () => {
beforeEach(() => {
nock(registryUrl)
.get('/-/_view/byKeyword')
.get('/-/v1/search')
.query(true)
.reply(200, {rows: this.rows});
.reply(200, {
objects: [
{
package: {
name: 'generator-unrelated',
description: 'some description'
}
},
{
package: {
name: 'generator-unrelevant',
description: 'some description'
}
}
]
});
});

it('list options if search have no results', function (done) {
var call = 0;
let call = 0;

this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
this.sandbox.stub(inquirer, 'prompt', arg => {
call++;

if (call === 1) {
return cb({searchTerm: 'unicorn'});
return Promise.resolve({searchTerm: 'foo'});
}

if (call === 2) {
var choices = arg[0].choices;
assert.deepEqual(_.pluck(choices, 'value'), ['install', 'home']);
const {choices} = arg[0];
assert.deepEqual(_.map(choices, 'value'), ['install', 'home']);
done();
}

return Promise.resolve({toInstall: 'home'});
});

this.router.navigate('install');
21 changes: 11 additions & 10 deletions test/route-run.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
'use strict';
var assert = require('assert');
var fs = require('fs');
var sinon = require('sinon');
var Configstore = require('configstore');
var conf = new Configstore('yoyo-test-purposes', {
const assert = require('assert');
const fs = require('fs');
const sinon = require('sinon');
const Configstore = require('configstore');
const Router = require('../lib/router');
const runRoute = require('../lib/routes/run');
const helpers = require('./helpers');

const conf = new Configstore('yoyo-test-purposes', {
generatorRunCount: {}
});
var Router = require('../lib/router');
var runRoute = require('../lib/routes/run');
var helpers = require('./helpers');

describe('run route', function () {
describe('run route', () => {
beforeEach(function () {
this.insight = helpers.fakeInsight();
this.env = helpers.fakeEnv();
this.router = new Router(this.env, this.insight, conf);
this.router.registerRoute('run', runRoute);
});

afterEach(function () {
afterEach(() => {
fs.unlinkSync(conf.path);
});

40 changes: 22 additions & 18 deletions test/route-update.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use strict';
var proxyquire = require('proxyquire');
var sinon = require('sinon');
var inquirer = require('inquirer');
var Router = require('../lib/router');
var helpers = require('./helpers');
const proxyquire = require('proxyquire');
const sinon = require('sinon');
const inquirer = require('inquirer');
const Router = require('../lib/router');
const helpers = require('./helpers');

describe('update route', function () {
describe('update route', () => {
beforeEach(function () {
this.sandbox = sinon.sandbox.create();
this.insight = helpers.fakeInsight();
@@ -17,8 +17,8 @@ describe('update route', function () {
this.router.registerRoute('home', this.homeRoute);

this.crossSpawn = helpers.fakeCrossSpawn('close');
var updateRoute = proxyquire('../lib/routes/update', {
'cross-spawn-async': this.crossSpawn
const updateRoute = proxyquire('../lib/routes/update', {
'cross-spawn': this.crossSpawn
});
this.router.registerRoute('update', updateRoute);
});
@@ -28,16 +28,20 @@ describe('update route', function () {
});

it('allows updating generators and return user to home screen', function () {
var generators = ['generator-cat', 'generator-unicorn'];
this.sandbox.stub(inquirer, 'prompt', function (arg, cb) {
cb({generators: generators});
const generators = ['generator-cat', 'generator-unicorn'];
this.sandbox.stub(inquirer, 'prompt').returns(
Promise.resolve({generators})
);
return this.router.navigate('update').then(() => {
sinon.assert.calledWith(
this.crossSpawn,
'npm',
['install', '--global'].concat(generators)
);
sinon.assert.calledWith(this.insight.track, 'yoyo', 'update');
sinon.assert.calledWith(this.insight.track, 'yoyo', 'updated');
sinon.assert.calledOnce(this.homeRoute);
sinon.assert.calledOnce(this.env.lookup);
});
this.router.navigate('update');

sinon.assert.calledWith(this.crossSpawn, 'npm', ['install', '-g'].concat(generators));
sinon.assert.calledWith(this.insight.track, 'yoyo', 'update');
sinon.assert.calledWith(this.insight.track, 'yoyo', 'updated');
sinon.assert.calledOnce(this.homeRoute);
sinon.assert.calledOnce(this.env.lookup);
});
});
74 changes: 66 additions & 8 deletions test/router.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
'use strict';
var assert = require('assert');
var _ = require('lodash');
var sinon = require('sinon');
var Router = require('../lib/router');
const path = require('path');
const assert = require('assert');
const _ = require('lodash');
const sinon = require('sinon');
const proxyquire = require('proxyquire');
const helpers = require('./helpers');

describe('Router', function () {
const Router = proxyquire('../lib/router', {
'read-pkg-up': {
sync(options) {
// Turn `/phoenix/app` into `phoenix-app`
const name = options.cwd.split(path.sep).filter(chunk => Boolean(chunk)).join('-');
return {
pkg: {
name,
version: '0.1.0'
}
};
}
}
});

describe('Router', () => {
beforeEach(function () {
this.router = new Router(sinon.stub());
this.env = helpers.fakeEnv();
this.env.getGeneratorsMeta = sinon.stub();
this.router = new Router(this.env);
});

describe('#registerRoute()', function () {
describe('#registerRoute()', () => {
it('is chainable', function () {
assert.equal(this.router.registerRoute('foo', _.noop), this.router);
});
});

describe('#navigate()', function () {
describe('#navigate()', () => {
beforeEach(function () {
this.route = sinon.spy();
this.router.registerRoute('foo', this.route);
@@ -37,4 +56,43 @@ describe('Router', function () {
assert.throws(this.router.navigate.bind(this.route, 'invalid route name'));
});
});

describe('#updateAvailableGenerators()', () => {
beforeEach(function () {
this.env.getGeneratorsMeta.returns([
{
namespace: 'xanadu:all',
resolved: '/xanadu/all/index.js'
},
{
namespace: 'phoenix:app',
resolved: '/phoenix/app/index.js'
},
{
namespace: 'phoenix:misc',
resolved: '/phoenix/misc/index.js'
},
{
namespace: 'phoenix:sub-app',
resolved: '/phoenix/sub-app/index.js'
}
]);
});

it('finds generators where an `all` generator is implemented', function () {
this.router.updateAvailableGenerators();
assert.ok(this.router.generators['xanadu-all'], 'xanadu:all found');
});

it('finds generators where an `app` generator is implemented', function () {
this.router.updateAvailableGenerators();
assert.ok(this.router.generators['phoenix-app'], 'phoenix:app found');
});

it('ignores sub-generators', function () {
this.router.updateAvailableGenerators();
assert.ok(!this.router.generators['phoenix-misc'], 'phoenix:misc ignored');
assert.ok(!this.router.generators['phoenix-sub-app'], 'phoenix:sub-app ignored');
});
});
});