├── .babelrc ├── .blueprintrc ├── .codeclimate.yml ├── .eslintrc ├── .gitignore ├── .istanbul.yml ├── .mocha.opts ├── .npmignore ├── .nvmrc ├── .travis.yml ├── bin ├── blueprint.js └── bp.js ├── blueprints ├── .eslintrc ├── blueprint │ ├── files │ │ └── blueprints │ │ │ └── __name__ │ │ │ ├── files │ │ │ └── .gitkeep │ │ │ └── index.js.ejs │ └── index.js ├── duck │ ├── files │ │ ├── __root__ │ │ │ └── __duck__ │ │ │ │ └── __name__.js.ejs │ │ └── __test__ │ │ │ └── redux │ │ │ └── modules │ │ │ └── __name__.test.js.ejs │ └── index.js ├── dumb │ ├── files │ │ ├── __root__ │ │ │ └── __dumb__ │ │ │ │ └── __name__.js.ejs │ │ └── __test__ │ │ │ └── __dumb__ │ │ │ └── __name__.test.js.ejs │ └── index.js ├── form │ ├── files │ │ ├── __root__ │ │ │ └── forms │ │ │ │ └── __name__.js.ejs │ │ └── __test__ │ │ │ └── forms │ │ │ └── __name__.test.js.ejs │ └── index.js └── smart │ ├── files │ ├── __root__ │ │ └── __smart__ │ │ │ └── __name__.js.ejs │ └── __test__ │ │ └── __smart__ │ │ └── __name__.test.js.ejs │ └── index.js ├── codecov.yml ├── design.md ├── jest.config.js ├── package.json ├── readme.md ├── redux-cli.gif ├── src ├── cli │ ├── cmds │ │ ├── config.js │ │ ├── generate.js │ │ ├── generate │ │ │ ├── build-blueprint-command.js │ │ │ ├── build-blueprint-commands.js │ │ │ └── handlers.js │ │ ├── init.js │ │ └── new.js │ ├── environment.js │ ├── handler.js │ ├── index.js │ ├── parser.js │ └── yargs.js ├── config.js ├── models │ ├── blueprint-collection.js │ ├── blueprint.js │ ├── file-info.js │ ├── project-settings.js │ ├── sub-command.js │ ├── task.js │ └── ui.js ├── prompts │ ├── initPrompt.js │ └── setup.js ├── sub-commands │ ├── config.js │ ├── generate.js │ ├── init.js │ └── new.js ├── tasks │ ├── create-and-step-into-directory.js │ ├── generate-from-blueprint.js │ └── git-pull.js ├── util │ ├── fs.js │ ├── mixin.js │ └── text-helper.js └── version.js ├── templates ├── .blueprintrc └── .starterrc ├── test ├── .eslintrc ├── cli │ ├── cmds │ │ ├── config.test.js │ │ ├── generate.test.js │ │ ├── generate │ │ │ ├── build-blueprint-command.test.js │ │ │ ├── build-blueprint-commands.test.js │ │ │ └── handlers.test.js │ │ ├── init.test.js │ │ └── new.test.js │ ├── environment.test.js │ ├── handler.test.js │ └── parser.test.js ├── fixtures │ ├── argv.blueprintrc │ ├── basic │ │ ├── files │ │ │ └── expected-file.js │ │ └── index.js │ ├── blueprints │ │ ├── basic │ │ │ ├── files │ │ │ │ └── expected-file.js │ │ │ └── index.js │ │ └── duplicate │ │ │ ├── files │ │ │ └── expected-file.js │ │ │ └── index.js │ ├── env.blueprintrc │ └── file-info-template.txt ├── helpers │ ├── fs-helpers.js │ ├── mock-settings.js │ ├── mock-ui.js │ └── regex-utils.js ├── models │ ├── blueprint-collection.test.js │ ├── blueprint.test.js │ ├── file-info.test.js │ ├── project-settings.test.js │ ├── sub-command.test.js │ ├── task.test.js │ └── ui.test.js ├── prompts │ └── setup.test.js ├── setup.js └── util │ ├── fs.test.js │ ├── mixin.test.js │ └── text-helper.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"], 3 | "plugins": ["transform-object-rest-spread"], 4 | "env": { 5 | "test": { 6 | "presets": ["es2015"], 7 | "plugins": ["transform-object-rest-spread"], 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.blueprintrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceBase": "src", 3 | "testBase": "test", 4 | "smartPath": "container", 5 | "dumbPath": "component", 6 | "fileCasing": "default", 7 | "location": "project", 8 | "blueprints": true 9 | } 10 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: false 5 | config: 6 | languages: 7 | javascript: 8 | mass_threshold: 60 9 | eslint: 10 | enabled: true 11 | fixme: 12 | enabled: true 13 | ratings: 14 | paths: 15 | - "**.inc" 16 | - "**.js" 17 | - "**.jsx" 18 | exclude_paths: 19 | - test/ 20 | - templates/ 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "prettier", 4 | "rules": { 5 | "indent": [2, 2], 6 | "quotes": [2, "single", {"avoidEscape": true}], 7 | "linebreak-style": [2, "unix"], 8 | "semi": [2, "always"], 9 | "no-console": [0] 10 | }, 11 | "env": { 12 | "es6": true, 13 | "node": true, 14 | "browser": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | npm-debug.log 4 | coverage/ 5 | tmp/ 6 | .reduxrc 7 | yarn-error.log 8 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | root: src 3 | -------------------------------------------------------------------------------- /.mocha.opts: -------------------------------------------------------------------------------- 1 | --require ./test/setup.js 2 | --full-trace 3 | --recursive ./test/**/*.js 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 5.1.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5.1.0" 4 | addons: 5 | code_climate: 6 | repo_token: bc3997ee81bddc88a39799cac43d7e6a71e7f0a2334331a7f02ae486433ddc1f 7 | before_install: 8 | - pip install --user codecov 9 | after_success: 10 | - codecov --file coverage/lcov.info --disable search 11 | -------------------------------------------------------------------------------- /bin/blueprint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const cli = require('../lib/cli'); 4 | 5 | cli(); 6 | -------------------------------------------------------------------------------- /bin/bp.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const cli = require('../lib/cli'); 4 | 5 | cli(); 6 | -------------------------------------------------------------------------------- /blueprints/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.eslintrc", 3 | "env": { 4 | "mocha": true 5 | }, 6 | "rules": { 7 | "no-unused-vars": 0 8 | }, 9 | "plugins": ["ejs"], 10 | "globals": { 11 | "expect": false, 12 | "should": false, 13 | "sinon": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /blueprints/blueprint/files/blueprints/__name__/files/.gitkeep: -------------------------------------------------------------------------------- 1 | put your files here 2 | -------------------------------------------------------------------------------- /blueprints/blueprint/files/blueprints/__name__/index.js.ejs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | <% if (description) { %> 3 | description: function() { 4 | return '<%- description %>'; 5 | }, 6 | <% } %> 7 | <% if (command) { %> 8 | command: { 9 | aliases: <%- aliases %>, 10 | options: { 11 | opt: { 12 | alias: 'o', 13 | description: 'blueprint option', 14 | type: 'string' 15 | } 16 | }, 17 | check: (argv, options) => true, 18 | examples: [], 19 | sanitize: argv => argv 20 | }, 21 | <% } %> 22 | <% if (locals) { %> 23 | locals(options) { 24 | // Return custom template variables here 25 | return {}; 26 | }, 27 | <% } %> 28 | <% if (fileMapTokens) { %> 29 | fileMapTokens: function(options) { 30 | // Return custom tokens to be replaced in path and file names 31 | return { 32 | __token__: function(options) { 33 | // logic to determine value goes here 34 | return 'value'; 35 | }, 36 | }; 37 | }, 38 | <% } %> 39 | <% if (beforeInstall) { %> 40 | beforeInstall: function(options, locals) {}, 41 | <% } %> 42 | <% if (afterInstall) { %> 43 | afterInstall: function(options) {}, 44 | <% } %> 45 | }; 46 | -------------------------------------------------------------------------------- /blueprints/blueprint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return 'Generates a blueprint with desired hooks'; 4 | }, 5 | 6 | command: { 7 | aliases: ['bp'], 8 | options: { 9 | description: { 10 | alias: 'D', 11 | describe: 'override default description', 12 | type: 'string' 13 | }, 14 | command: { 15 | alias: 'c', 16 | describe: 'add hook to specify generate command options', 17 | type: 'boolean' 18 | }, 19 | aliases: { 20 | alias: 'A', 21 | describe: 'specify aliases for the blueprint', 22 | type: 'array', 23 | default: [] 24 | }, 25 | locals: { 26 | alias: 'l', 27 | describe: 'add hook to specify locals', 28 | type: 'boolean' 29 | }, 30 | 'file-map-tokens': { 31 | alias: 'm', 32 | describe: 'add hook for fileMapTokens', 33 | type: 'boolean' 34 | }, 35 | 'before-install': { 36 | alias: 'b', 37 | describe: 'add hook for beforeInstall', 38 | type: 'boolean' 39 | }, 40 | 'after-install': { 41 | alias: 'a', 42 | describe: 'add hook for afterInstall', 43 | type: 'boolean' 44 | }, 45 | 'all-hooks': { 46 | alias: 'H', 47 | describe: 'shortcut to add all hooks, equivalent to -clmba', 48 | type: 'boolean' 49 | } 50 | }, 51 | examples: [ 52 | '$0 generate blueprint files_only', 53 | '$0 generate blueprint complex_bp --aliases cpx --all-hooks' 54 | ], 55 | epilogue: 56 | 'Documentation: https://github.com/SpencerCDixon/redux-cli#creating-blueprints', 57 | sanitize: argv => { 58 | // aliases imply command 59 | if (argv.aliases.length) { 60 | argv.command = true; 61 | } 62 | // aliases to be rendered as a string 63 | argv.aliases = JSON.stringify(argv.aliases); 64 | 65 | // NB: if command was specified but aliases is an empty array it will 66 | // still be rendered. This is harmless and serves as a reminder 67 | // to the blueprint author of the supported feature and syntax 68 | 69 | // allHooks? 70 | if (argv.allHooks) { 71 | argv.command = true; 72 | argv.locals = true; 73 | argv.fileMapTokens = true; 74 | argv.beforeInstall = true; 75 | argv.afterInstall = true; 76 | } 77 | 78 | return argv; 79 | } 80 | }, 81 | 82 | locals({ entity: { options } }) { 83 | return options; 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /blueprints/duck/files/__root__/__duck__/__name__.js.ejs: -------------------------------------------------------------------------------- 1 | // Constants 2 | 3 | // export const constants = { }; 4 | 5 | // Action Creators 6 | 7 | // export const actions = { }; 8 | 9 | // Reducer 10 | export const defaultState = {}; 11 | 12 | export default function(state = defaultState, action) { 13 | switch (action.type) { 14 | default: 15 | return state; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /blueprints/duck/files/__test__/redux/modules/__name__.test.js.ejs: -------------------------------------------------------------------------------- 1 | import reducer, { defaultState } from 'redux/modules/<%= camelEntityName %>'; 2 | import deepFreeze from 'deep-freeze'; 3 | 4 | describe('(Redux) <%= camelEntityName %>', () => { 5 | describe('(Reducer)', () => { 6 | it('sets up initial state', () => { 7 | expect(reducer(undefined, {})).to.eql(defaultState); 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /blueprints/duck/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return 'Generates a reducer/actions/constant Duck Module for redux'; 4 | }, 5 | fileMapTokens() { 6 | return { 7 | __duck__: options => { 8 | return 'redux/modules'; 9 | } 10 | }; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /blueprints/dumb/files/__root__/__dumb__/__name__.js.ejs: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | }; 5 | 6 | class <%= pascalEntityName %> extends Component { 7 | render() { 8 | return ( 9 |
10 | ); 11 | } 12 | } 13 | 14 | <%= pascalEntityName %>.propTypes = propTypes; 15 | export default <%= pascalEntityName %>; 16 | -------------------------------------------------------------------------------- /blueprints/dumb/files/__test__/__dumb__/__name__.test.js.ejs: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | describe('(Component) <%= pascalEntityName %>', function() { 5 | it('should exist', function() {}); 6 | }); 7 | -------------------------------------------------------------------------------- /blueprints/dumb/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return 'Generates a dumb (aka Pure) component'; 4 | }, 5 | fileMapTokens() { 6 | return { 7 | __dumb__: options => { 8 | return options.settings.getSetting('dumbPath'); 9 | } 10 | }; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /blueprints/form/files/__root__/forms/__name__.js.ejs: -------------------------------------------------------------------------------- 1 | <% /* eslint-disable */ %> 2 | import React, { Component, PropTypes } from 'react'; 3 | import { reduxForm } from 'redux-form'; 4 | 5 | export const fields = []; 6 | 7 | const validate = (values) => { 8 | const errors = {}; 9 | return errors; 10 | }; 11 | 12 | const propTypes = { 13 | handleSubmit: PropTypes.func.isRequired, 14 | fields: PropTypes.object.isRequired 15 | }; 16 | 17 | export class <%= pascalEntityName %> extends Component { 18 | render() { 19 | const { 20 | fields: {}, 21 | handleSubmit 22 | } = this.props; 23 | 24 | return ( 25 |
26 |
27 | ); 28 | } 29 | }; 30 | 31 | <%= pascalEntityName %>.propTypes = propTypes; 32 | <%= pascalEntityName %> = reduxForm({ 33 | form: '<%= pascalEntityName %>', 34 | fields, 35 | validate 36 | })(<%= pascalEntityName %>); 37 | 38 | export default <%= pascalEntityName %>; 39 | -------------------------------------------------------------------------------- /blueprints/form/files/__test__/forms/__name__.test.js.ejs: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | 3 | describe('(Form) <%= pascalEntityName %>', () => { 4 | it('exists', () => {}); 5 | }); 6 | -------------------------------------------------------------------------------- /blueprints/form/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return 'Generates a connected form component using redux-form'; 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /blueprints/smart/files/__root__/__smart__/__name__.js.ejs: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | 5 | const propTypes = { 6 | }; 7 | 8 | class <%= pascalEntityName %> extends Component { 9 | render() { 10 | return ( 11 |
12 | ); 13 | } 14 | } 15 | 16 | const mapStateToProps = (state) => { 17 | return {}; 18 | }; 19 | const mapDispatchToProps = (dispatch) => { 20 | return {}; 21 | }; 22 | 23 | <%= pascalEntityName %>.propTypes = propTypes; 24 | export default connect( 25 | mapStateToProps, 26 | mapDispatchToProps 27 | )(<%= pascalEntityName %>); 28 | -------------------------------------------------------------------------------- /blueprints/smart/files/__test__/__smart__/__name__.test.js.ejs: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | 3 | describe('(Component) <%= pascalEntityName %>', () => { 4 | it('exists', () => {}); 5 | }); 6 | -------------------------------------------------------------------------------- /blueprints/smart/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return 'Generates a smart (aka container) component'; 4 | }, 5 | fileMapTokens() { 6 | return { 7 | __smart__: options => { 8 | return options.settings.getSetting('smartPath'); 9 | } 10 | }; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | fixes: 2 | - "src/::lib/" -------------------------------------------------------------------------------- /design.md: -------------------------------------------------------------------------------- 1 | ## Design Decisions 1.0 2 | 3 | Would like to emulate the good parts of ember-cli and not use the parts I don't 4 | like/think would work well within the React eco-system. 5 | 6 | **Commands** will be controlled by commander. 7 | **Tasks** will act like rake tasks in rails. 8 | **Blueprints** will be what gets created via generate. These can be overridden 9 | by individual projects so that way projects can manage their own blueprints. 10 | 11 | Commander will be used to spawn off top level commands. The primary purpose of 12 | commander will be to parse options/args and display useful help for all the 13 | different options. Once args have been parsed they can be passed down to 14 | SubCommands. 15 | 16 | ## Design Decisions 2.0 17 | 18 | Rename to blueprint-cli 19 | 20 | Take the foundation laid by 1.0 and extend the capabilities. 21 | 22 | Blueprints are most useful when able to be shared, copied and customized. 23 | The Blueprints directory discovery will be expanded to include home directories, 24 | ENV defined directories, npm packages, config file defined. A single 25 | directory may be defined as the default directory for blueprint generation. 26 | Provide a way to copy blueprints into the default directory in order to 27 | increase the ease of customizing your own version of a default or shared 28 | blueprint. 29 | 30 | Enhance the .blueprintrc experience. Add the ability to have home directory 31 | and ENV var defined locations. Allow merging of multiple .blueprintrc files. 32 | Allow defining blueprint directories in the file. 33 | 34 | Enhance the Generator experience. Look to Ruby on Rails for inspiration. 35 | Look for ways to enable generator composition. Look for ways to insert code 36 | into existing files. Find ways to share and use partials in generators 37 | 38 | Define a generator to create a npm package dir ready to share blueprints with 39 | the community. 40 | 41 | 42 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // jest.config.js 2 | module.exports = { 3 | testEnvironment: "node", 4 | setupTestFrameworkScriptFile: "/test/setup.js", 5 | roots: ["/test", "/src"], 6 | modulePaths: ["/src"], 7 | coverageDirectory: "./coverage/", 8 | collectCoverage: true, 9 | coverageReporters: ["json", "lcov", "text"] 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-cli", 3 | "version": "2.0.0-0.2.1", 4 | "description": "An opinionated CLI to make working on redux apps much faster.", 5 | "main": "bin/bp.js", 6 | "engine": { 7 | "node": ">=5.1" 8 | }, 9 | "scripts": { 10 | "test": "jest --forceExit && codecov --token=75e3e765-443c-4d31-a8f5-b0ca3be8e55f", 11 | "posttest": "npm run lint", 12 | "test:nocov": "jest --coverage=false --forceExit", 13 | "test:cov": "jest --forceExit", 14 | "test:watch": "jest --forceExit --coverageReporters html --watch --notify", 15 | "start": "npm run build:watch", 16 | "pretty": "prettier --write --single-quote \"{src,test,blueprints}/**/*.js\" ", 17 | "build": "babel src -d lib", 18 | "build:watch": "babel src --watch -d lib", 19 | "lint": "eslint ./src ./test ./blueprints", 20 | "clean": "rimraf lib", 21 | "publish:patch": "npm run clean && npm run build && npm version patch && npm publish", 22 | "publish:minor": "npm run clean && npm run build && npm version minor && npm publish" 23 | }, 24 | "keywords": [ 25 | "redux", 26 | "react", 27 | "cli", 28 | "blueprint", 29 | "generator", 30 | "react.js", 31 | "kit", 32 | "app-kit", 33 | "react-starter-kit", 34 | "react-redux-starter-kit" 35 | ], 36 | "author": "Spencer Dixon ", 37 | "license": "MIT", 38 | "bin": { 39 | "bp": "bin/bp.js", 40 | "blueprint": "bin/blueprint.js" 41 | }, 42 | "dependencies": { 43 | "chalk": "^1.1.1", 44 | "denodeify": "^1.2.1", 45 | "ejs": "^2.4.1", 46 | "elegant-spinner": "^1.0.1", 47 | "figlet": "^1.1.1", 48 | "fs-extra": "^0.26.5", 49 | "humps": "^1.0.0", 50 | "jsonfile": "^2.2.3", 51 | "lodash": "^4.5.1", 52 | "log-update": "^1.0.2", 53 | "minimist": "^1.2.0", 54 | "prettyjson": "^1.2.1", 55 | "prompt": "^1.0.0", 56 | "rc": "^1.2.1", 57 | "shelljs": "^0.6.0", 58 | "temp": "^0.8.3", 59 | "through": "^2.3.8", 60 | "walk-sync": "^0.2.6", 61 | "yargs": "^9.0.1" 62 | }, 63 | "devDependencies": { 64 | "babel-cli": "^6.5.1", 65 | "babel-eslint": "^8.0.0", 66 | "babel-jest": "^21.0.2", 67 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 68 | "babel-preset-es2015": "^6.6.0", 69 | "babel-register": "^6.5.1", 70 | "chai": "^3.5.0", 71 | "codecov": "^2.3.0", 72 | "eslint": "^4.7.1", 73 | "eslint-config-prettier": "^2.6.0", 74 | "eslint-plugin-ejs": "0.0.2", 75 | "eslint-plugin-prettier": "^2.3.1", 76 | "eslint-plugin-react": "^7.3.0", 77 | "istanbul": "0.4.5", 78 | "jest-cli": "^21.1.0", 79 | "prettier": "^1.7.3", 80 | "rimraf": "^2.5.2", 81 | "sinon": "^1.17.3" 82 | }, 83 | "repository": { 84 | "type": "git", 85 | "url": "https://github.com/SpencerCDixon/redux-cli" 86 | }, 87 | "bugs": { 88 | "url": "https://github.com/SpencerCDixon/redux-cli/issues" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/SpencerCDixon/redux-cli.svg?branch=master)](https://travis-ci.org/SpencerCDixon/redux-cli) 2 | [![Code Climate](https://codeclimate.com/github/SpencerCDixon/redux-cli/badges/gpa.svg)](https://codeclimate.com/github/SpencerCDixon/redux-cli) 3 | 4 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 5 | [![Gitter Chat Channel](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/redux-cli/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link) 6 | 7 | ``` 8 | ______ _ _____ _ _____ 9 | | ___ \ | | / __ \| | |_ _| 10 | | |_/ /___ __| |_ ___ ________| / \/| | | | 11 | | // _ \/ _` | | | \ \/ /______| | | | | | 12 | | |\ \ __/ (_| | |_| |> < | \__/\| |_____| |_ 13 | \_| \_\___|\__,_|\__,_/_/\_\ \____/\_____/\___/ 14 | ``` 15 | 16 | ## Quick Start 17 | 18 | ```javascript 19 | npm i redux-cli -g // install blueprint-cli globally 20 | blueprint new // create a new blueprint project 21 | blueprint new -S // OR use ssh to pull project down (if ssh setup with github) 22 | blueprint init // OR configure a current project to use the CLI 23 | 24 | // Start generating components/tests and save time \(• ◡ •)/ 25 | //(g is alias for generate) 26 | blueprint g dumb SimpleButton 27 | ``` 28 | 29 | ![Blueprint CLI Usage Gif](redux-cli.gif) 30 | 31 | ## Table Of Contents 32 | 33 | 1. [Getting Started](#getting-started) 34 | 2. [Configuring Existing Project](#config-existing-project) 35 | 3. [Commands](#commands) 36 | 4. [Generators](#generators) 37 | 5. [Roadmap](#roadmap) 38 | 6. [Examples](#examples) 39 | 7. [Creating Custom Blueprints](#creating-blueprints) 40 | 8. [Issues/Contributing](#contributing) 41 | 9. [Changelog](#changelog) 42 | 43 | ### Getting Started 44 | Running `blueprint new ` will pull down the amazing [Redux Starter Kit](https://github.com/davezuko/react-redux-starter-kit) and 45 | initialize a new git repo. Running `new` will automatically set up a `.blueprintrc` 46 | to work with this specific starter kit. If you want to integrate the CLI in an 47 | existing project or store your components in different paths please see [config existing project](#config-existing-project) 48 | 49 | ### Config Existing Project 50 | There is an `init` subcommand for you to specify all paths to where components 51 | live in your project. The `init` command just creates a `.blueprintrc` in your 52 | project root. If you want to you can just create the `.blueprintrc` manually. 53 | 54 | Final `.blueprintrc` might look like this: 55 | 56 | ```javascript 57 | { 58 | "sourceBase":"src", 59 | "testBase":"tests", 60 | "smartPath":"containers", 61 | "dumbPath":"components", 62 | "fileCasing": "default" 63 | } 64 | ``` 65 | 66 | **Note on configuration**: 67 | This project tries to walk on a fine line between convention and configuration. 68 | Since the majority of React applications will separate their smart/dumb 69 | components if you pass in those paths you'll get those generators for free. 70 | However, some of the other generators might not write files to the exact paths 71 | that you use for your project. It's easy to override the CLI generators with 72 | your own so that the generators will write files to the correct location. 73 | [See: creating custom blueprints](#creating-blueprints). 74 | 75 | Alternatively, if you use this CLI as a result of `blueprint new ` the 76 | starter kit will come pre-configured with a bunch of blueprints (generators) 77 | that work out of the gate. Currently, I'm working on a PR for the 78 | `react-redux-starter-kit` with a bunch of blueprints. More starter kits and 79 | blueprints to come! 80 | 81 | #### Default Settings 82 | |Key Name|Required|Description| 83 | |---|---|---| 84 | |**sourceBase**|✓|where you keep your pre-compiled source (relative from root of project)| 85 | |**testBase**|✓|where you keep your tests (relative from root of project)| 86 | |**smartPath**|✓|where you keep your smart (container) components (relative of sourceBase)| 87 | |**dumbPath**|✓|where you keep your dumb (pure) components (relative of sourceBase)| 88 | |**fileCasing**|✓|how do you want generated files to be named (pascal/camel/snake/dashes/default)| 89 | 90 | #### .blueprintrc 91 | It's possible to put `.blueprintrc` files in other locations to better share 92 | configs. It looks for files in the following locations and deep merges the 93 | files it finds together. The defaultSettings will be overwritten by any 94 | following options while the `--config=path/to/file` option will override 95 | everything. 96 | 97 | See the whole list and more ENV tricks 98 | at [rc](https://github.com/dominictarr/rc) 99 | 100 | 1. defaultSettings 101 | 2. /etc/blueprint/config 102 | 3. /etc/blueprintrc 103 | 4. ~/.config/blueprint/config 104 | 5. ~/.config/blueprint 105 | 6. ~/.blueprint/config 106 | 7. ~/.blueprintrc 107 | 9. $BLUEPRINT_CONFIG 108 | 10. --config=path/to/file 109 | 110 | **Note** - All files found at these locations will have their objects deep 111 | merged together. Later file override earlier ones 112 | 113 | ### Commands 114 | 115 | |Command|Description|Alias| 116 | |---|---|---| 117 | |`blueprint new `|creates a new blueprint project|| 118 | |`blueprint init`|configure an existing blueprint app to use the CLI|| 119 | |`blueprint generate `|generates files and tests for you automatically|`blueprint g`| 120 | |`blueprint help g`|show all generators you have available|| 121 | 122 | 123 | ### Generators 124 | 125 | |Name|Description|Options| 126 | |---|---|---| 127 | |`blueprint g dumb `|generates a dumb component and test file|| 128 | |`blueprint g smart `|generates a smart connected component and test file|| 129 | |`blueprint g form
`|generates a form component (assumes redux-form)|| 130 | |`blueprint g duck `|generates a redux duck and test file|| 131 | 132 | You can also see what files would get created with the `--dry-run` option like 133 | so: 134 | 135 | ``` 136 | blueprint g dumb MyNewComponent --dry-run 137 | 138 | // Output: 139 | 140 | info: installing blueprint... 141 | would create: /MyNewComponent.js 142 | would create: /MyNewComponent.test.js 143 | info: finished installing blueprint. 144 | ``` 145 | 146 | ### Roadmap 147 | 148 | #### 2.0 149 | 150 | * rename to blueprint-cli 151 | * replace commander with yargs for cli 152 | * extend .blueprintrc settings. 153 | * Allow .blueprintrc files to set search * path for blueprint directories 154 | * Enable npm blueprint packages 155 | * Enable better options support for blueprint generation 156 | * Add Copy command to cli 157 | * Add config command to cli 158 | * Add blueprintrc and blueprint-package blueprints 159 | * Update existing blueprints and move into own package, to be included by default 160 | 161 | #### 2.X 162 | 163 | * Replace "new" command with alternate ways of project creation 164 | * Enable blueprint partials 165 | * Enable blueprints for assets: icons, images, css, fonts 166 | * Enable ability to insert text into existing files 167 | * Enable generators that can invoke other generators. Inspired by rails scaffold 168 | 169 | ### Examples 170 | Below are some examples of using the generator to speed up development: 171 | 172 | ``` 173 | // generates a dumb component 174 | blueprint g dumb SimpleButton 175 | 176 | // generates a smart component 177 | blueprint g smart CommentContainer 178 | 179 | // generate a redux-form with tags in render statement 180 | blueprint g form ContactForm 181 | 182 | // generate a Redux 'duck' (reducer, constants, action creators) 183 | blueprint g duck todos 184 | ``` 185 | 186 | ### Creating Blueprints 187 | Blueprints are template generators with optional custom install logic. 188 | 189 | `blueprint generate` comes with a default set of blueprints. Every project has 190 | their own configuration & needs, therefore blueprints have been made easy to override 191 | and extend. 192 | 193 | **Preliminary steps**: 194 | 195 | 1. Create a `blueprints` folder in your root directory. Blueprint CLI will search 196 | for blueprints there _first_ before generating blueprints that come by default. 197 | 2. Create a sub directory inside `blueprints` for the new blueprint OR use the 198 | blueprint generator (super meta I know) that comes with Blueprint CLI by typing: 199 | `blueprint g blueprint `. 200 | 3. If you created the directory yourself than make sure to create a `index.js` 201 | file that exports your blueprint and a `files` folder with what you want 202 | generated. 203 | 204 | **Customizing the blueprint**: 205 | 206 | Blueprints follow a simple structure. Let's take the built-in 207 | `smart` blueprint as an example: 208 | 209 | ``` 210 | blueprints/smart 211 | ├── files 212 | │   ├── __root__ 213 | │   │   └── __smart__ 214 | │   │   └── __name__.js 215 | │   └── __test__ 216 | │   └── __smart__ 217 | │   └── __name__.test.js 218 | └── index.js 219 | ``` 220 | 221 | #### File Tokens 222 | 223 | `files` contains templates for all the files to be generated into your project. 224 | 225 | The `__name__` token is subtituted with the 226 | entity name at install time. Entity names can be configued in 227 | either PascalCase, snake_case, camelCase, or dashes-case so teams can customize their file 228 | names accordingly. By default, the `__name__` will return whatever is entered 229 | in the generate CLI command. 230 | 231 | For example, when the user 232 | invokes `blueprint g smart commentContainer` then `__name__` becomes 233 | `commentContainer`. 234 | 235 | The `__root__` token is subsituted with the absolute path to your source. 236 | Whatever path is in your `.blueprintrc`'s `sourceBase` will be used here. 237 | 238 | The `__test__` token is substitued with the absolute path to your tests. 239 | Whatever path is in your `.blueprintrc`'s `testBase` will be used here. 240 | 241 | The `__path__` token is substituted with the blueprint 242 | name at install time. For example, when the user invokes 243 | `blueprint generate smart foo` then `__path__` becomes 244 | `smart`. 245 | 246 | The `__smart__` token is a custom token I added in the `index.js` it pulls from 247 | your `.blueprintrc` configuration file to use whatever you have set as your 248 | `smartPath`. 249 | 250 | #### Template Variables (AKA Locals) 251 | 252 | Variables can be inserted into templates with 253 | `<%= someVariableName %>`. The blueprints use [EJS](http://www.embeddedjs.com/) 254 | for their template rendering so feel free to use any functionality that EJS 255 | supports. 256 | 257 | For example, the built-in `dumb` blueprint 258 | `files/__root__/__name__.js` looks like this: 259 | 260 | ```js 261 | import React, { Component, PropTypes } from 'react'; 262 | 263 | const propTypes = { 264 | }; 265 | 266 | class <%= pascalEntityName %> extends Component { 267 | render() { 268 | return ( 269 | ) 270 | } 271 | } 272 | 273 | <%= pascalEntityName %>.propTypes = propTypes; 274 | export default <%= pascalEntityName %>; 275 | ``` 276 | 277 | `<%= pascalEntityName %>` is replaced with the real 278 | value at install time. If we were to type: `blueprint g dumb simple-button` all 279 | instances of `<%= pascalEntityName %>` would be converted to: `SimpleButton`. 280 | 281 | The following template variables are provided by default: 282 | 283 | - `pascalEntityName` 284 | - `camelEntityName` 285 | - `snakeEntityName` 286 | - `dashesEntityName` 287 | 288 | The mechanism for providing custom template variables is 289 | described below. 290 | 291 | #### Index.js 292 | 293 | Custom installation (and soon uninstallation) behaviour can be added 294 | by overriding the hooks documented below. `index.js` should 295 | export a plain object, which will extend the prototype of the 296 | `Blueprint` class. 297 | 298 | ```js 299 | module.exports = { 300 | locals: function(options) { 301 | // Return custom template variables here. 302 | return {}; 303 | }, 304 | 305 | fileMapTokens: function(options) { 306 | // Return custom tokens to be replaced in your files 307 | return { 308 | __token__: function(options){ 309 | // logic to determine value goes here 310 | return 'value'; 311 | } 312 | } 313 | }, 314 | 315 | filesPath: function() { 316 | // if you want to store generated files in a folder named 317 | // something other than 'files' you can override this 318 | return path.join(this.path, 'files'); 319 | }, 320 | 321 | // before and after install hooks 322 | beforeInstall: function(options) {}, 323 | afterInstall: function(options) {}, 324 | }; 325 | ``` 326 | 327 | #### Blueprint Hooks 328 | 329 | As shown above, the following hooks are available to 330 | blueprint authors: 331 | 332 | - `locals` 333 | - `fileMapTokens` 334 | - `filesPath` 335 | - `beforeInstall` 336 | - `afterInstall` 337 | 338 | #### locals 339 | 340 | Use `locals` to add custom tempate variables. The method 341 | receives one argument: `options`. Options is an object 342 | containing general and entity-specific options. 343 | 344 | When the following is called on the command line: 345 | 346 | ```sh 347 | blueprint g dumb foo --html=button --debug 348 | ``` 349 | 350 | The object passed to `locals` looks like this: 351 | 352 | ```js 353 | { 354 | entity: { 355 | name: 'foo', 356 | options: { 357 | _: ['dumb', 'foo'], 358 | html: 'button' 359 | }, 360 | rawArgs: [ 361 | ... array of rawArgs passed to cli ... 362 | ] 363 | }, 364 | debug: true 365 | } 366 | ``` 367 | 368 | This hook must return an object. It will be merged with the 369 | aforementioned default locals. 370 | 371 | #### fileMapTokens 372 | 373 | Use `fileMapTokens` to add custom fileMap tokens for use 374 | in the `mapFile` method. The hook must return an object in the 375 | following pattern: 376 | 377 | ```js 378 | { 379 | __token__: function(options){ 380 | // logic to determine value goes here 381 | return 'value'; 382 | } 383 | } 384 | ``` 385 | 386 | It will be merged with the default `fileMapTokens`, and can be used 387 | to override any of the default tokens. 388 | 389 | Tokens are used in the files folder (see `files`), and get replaced with 390 | values when the `mapFile` method is called. 391 | 392 | #### filesPath 393 | 394 | Use `filesPath` to define where the blueprint files to install are located. 395 | This can be used to customize which set of files to install based on options 396 | or environmental variables. It defaults to the `files` directory within the 397 | blueprint's folder. 398 | 399 | #### beforeInstall & afterInstall 400 | 401 | Called before any of the template files are processed and receives 402 | the the `options` and `locals` hashes as parameters. Typically used for 403 | validating any additional command line options or for any asynchronous 404 | setup that is needed. 405 | 406 | ### Contributing 407 | This CLI is very much in the beginning phases and I would love to have people 408 | help me to make it more robust. Currently, it's pretty opinonated to use the 409 | tooling/templates I prefer in my projects but I'm open to PR's to make it more 410 | universal and support other platforms (I'm on Mac). 411 | 412 | This repo uses Zenhub for managing issues. If you want to see what's currently 413 | being worked on or in the pipeline make sure to install the [Zenhub Chrome 414 | Extension](https://chrome.google.com/webstore/detail/zenhub-for-github/ogcgkffhplmphkaahpmffcafajaocjbd?hl=en-US) 415 | and check out this projects 'Boards'. 416 | 417 | #### Development Setup/Contributing 418 | Use `npm link` is to install the CLI locally when testing it and adding 419 | features. 420 | 421 | ``` 422 | nvm use 5.1.0 // install node V5.1 if not present (nvm install 5.1.0) 423 | npm install 424 | npm i eslint babel-eslint -g // make sure you have eslint installed globally 425 | npm start // to compile src into lib 426 | npm test // make sure all tests are passing 427 | 428 | // to test the cli in the local directory you can: 429 | npm link // will install the npm package locally so you can run 'blueprint ' 430 | blueprint 431 | ``` 432 | 433 | ### Package Utility Scripts: 434 | ``` 435 | npm start // will watch files in src and compile using babel 436 | npm test // runs test suite with linting. Throws when lint failing 437 | npm run lint // lints all files in src and test 438 | ``` 439 | 440 | ### Changelog 441 | 442 | `1.8.0` - adds `--dry-run` option to generators so you can see files before committing 443 | `1.7.0` - adds option to use a ui kit boilerplate with the `-U` flag. 444 | `1.6.0` - adds option to use a different boilerplate with the `-B` flag. Fixes windows issues 445 | `1.5.1` - fixes windows support, addes ejs eslint plugin, fixes bug with UI in windows 446 | `1.4.1` - default to https instead of ssh for pulling project down 447 | `1.4.0` - better generator help messages 448 | `1.3.5` - properly passes cli options to blueprints so they can use them 449 | `1.3.3` - fixes init command, adds --debug to generators, improves error messages for broken templates 450 | `1.3.0` - major internal refactor, addition of customizable blueprints 451 | `1.1.1` - adds support for html tag in render when generating components 452 | `1.0.1` - adds fileCasing to generators so Linux users can use snake_case_file_names 453 | `1.0` - first public release with stable api (new/generate/init) 454 | -------------------------------------------------------------------------------- /redux-cli.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SpencerCDixon/redux-cli/00684d6c16c43bad36d33877967218d54770270d/redux-cli.gif -------------------------------------------------------------------------------- /src/cli/cmds/config.js: -------------------------------------------------------------------------------- 1 | import getEnvironment from '../environment'; 2 | import Config from '../../sub-commands/config'; 3 | 4 | const subCommand = new Config(getEnvironment()); 5 | 6 | const usage = 'Usage:\n $0 config'; 7 | 8 | module.exports = { 9 | command: 'config', 10 | describe: 'Display current configuration', 11 | builder: yargs => yargs.usage(usage), 12 | handler: () => subCommand.run() 13 | }; 14 | -------------------------------------------------------------------------------- /src/cli/cmds/generate.js: -------------------------------------------------------------------------------- 1 | import getHandler from '../handler'; 2 | import { logYargs } from '../yargs'; 3 | import handlers from './generate/handlers'; 4 | 5 | getHandler().onRun('generate', handlers.handleRun); 6 | getHandler().onHelp('generate', handlers.handleHelp); 7 | 8 | const usage = `Generate: 9 | $0 generate 10 | $0 help generate `; 11 | 12 | module.exports = { 13 | command: 'generate ', 14 | aliases: ['g', 'gen'], 15 | describe: 'Generate project file(s) from a blueprint', 16 | builder: yargs => 17 | yargs 18 | .usage(usage) 19 | .option('dry-run', { 20 | alias: 'd', 21 | describe: "List files but don't generate them", 22 | type: 'boolean' 23 | }) 24 | .option('verbose', { 25 | alias: 'v', 26 | describe: 'Verbose output, including file contents', 27 | type: 'boolean' 28 | }) 29 | .group(['dry-run', 'verbose', 'help'], 'Generate Options:') 30 | .strict(false) // ditch this if we '--' blueprint commands 31 | .exitProcess(false) // allow parse to fall through to cli/handler to emit 32 | .fail((msg = '', err = {} /*, yargs */) => { 33 | // deal with exit conditions 34 | // close over the the original yargs that was passed to builder rather 35 | // than use the 3rd parameter to fail as it doesn't support logYargs 36 | yargs.showHelp(); 37 | 38 | let message = msg || err.message; 39 | 40 | message = message.replace( 41 | 'Not enough non-option arguments: got 0, need at least 2', 42 | 'Missing arguments and ' 43 | ); 44 | message = message.replace( 45 | 'Not enough non-option arguments: got 1, need at least 2', 46 | 'Missing argument blueprint ' 47 | ); 48 | 49 | logYargs(yargs, message); 50 | }) 51 | }; 52 | -------------------------------------------------------------------------------- /src/cli/cmds/generate/build-blueprint-command.js: -------------------------------------------------------------------------------- 1 | /* 2 | Build a yargs command module object from options defined in the blueprint 3 | https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module 4 | 5 | Target object structure: 6 | { 7 | command: 'blueprint ', 8 | aliases: [], 9 | describe: 'Generates a blueprint', 10 | builder: yargs => yargs, 11 | handler: argv => runner.run() 12 | } 13 | */ 14 | const buildBlueprintCommand = (blueprint, runner) => { 15 | // extract custom command pieces 16 | let { 17 | aliases = [], 18 | options, 19 | examples, 20 | epilog, 21 | epilogue, 22 | check, 23 | sanitize 24 | } = blueprint.command; 25 | 26 | // mandate the command name to guarantee name is passed to generate task 27 | let command = `${blueprint.name} `; 28 | 29 | // alert the user to the prescense of options for the command 30 | if (options) { 31 | command = command + ' [options]'; 32 | } 33 | 34 | // rc aliases override blueprint configuration 35 | aliases = [].concat(blueprint.settings.aliases || aliases); 36 | 37 | // default usage 38 | let usage = `Blueprint:\n $0 generate ${command}`; 39 | aliases.forEach( 40 | alias => 41 | (usage += `\n $0 generate ${command.replace(blueprint.name, alias)}`) 42 | ); 43 | 44 | // default options from settings 45 | if (options && blueprint.settings) { 46 | Object.keys(options).forEach(option => { 47 | if (blueprint.settings[option]) { 48 | options[option].default = blueprint.settings[option]; 49 | } 50 | }); 51 | } 52 | 53 | // alterate epilogue keys 54 | epilogue = epilogue || epilog; 55 | 56 | // builder brings together multiple customizations, whilst keeping the 57 | // options easy to parse for prompting in the init command 58 | const builder = yargs => { 59 | yargs.usage(usage).strict(false); 60 | 61 | if (options) yargs.options(options); 62 | if (check) yargs.check(check, false); 63 | if (examples) { 64 | [].concat(examples).forEach(example => yargs.example(example)); 65 | } 66 | if (epilogue) yargs.epilogue(epilogue); 67 | 68 | return yargs; 69 | }; 70 | 71 | // handler runs the generate blueprint task 72 | const handler = argv => { 73 | // merge command line options into rc options 74 | let options = { ...blueprint.settings, ...argv }; 75 | 76 | // tidy up options before passing them on so that all hooks have access 77 | // to the clean version 78 | if (sanitize) options = sanitize(options); 79 | 80 | const cliArgs = { 81 | entity: { 82 | name: argv.name, 83 | options, 84 | rawArgs: argv 85 | }, 86 | debug: argv.verbose || false, 87 | dryRun: argv.dryRun || false 88 | }; 89 | runner.run(blueprint.name, cliArgs); 90 | }; 91 | 92 | return { 93 | command, 94 | aliases, 95 | describe: blueprint.description(), 96 | builder, 97 | handler 98 | }; 99 | }; 100 | 101 | export default buildBlueprintCommand; 102 | -------------------------------------------------------------------------------- /src/cli/cmds/generate/build-blueprint-commands.js: -------------------------------------------------------------------------------- 1 | import _merge from 'lodash/merge'; 2 | import _cloneDeep from 'lodash/cloneDeep'; 3 | 4 | import getEnvironment from '../../environment'; 5 | import Generate from '../../../sub-commands/generate'; 6 | import buildBlueprintCommand from './build-blueprint-command'; 7 | 8 | const loadBlueprintSettings = (blueprint, bp) => 9 | (blueprint.settings = _merge( 10 | _cloneDeep(bp.common), 11 | _cloneDeep(bp[blueprint.name]) 12 | )); 13 | 14 | const buildBlueprintCommands = () => { 15 | const environment = getEnvironment(); 16 | const subCommand = new Generate(environment); 17 | 18 | const { blueprints, settings: { bp = {} } } = environment.settings; 19 | 20 | return blueprints.generators().map(blueprint => { 21 | loadBlueprintSettings(blueprint, bp); 22 | return buildBlueprintCommand(blueprint, subCommand); 23 | }); 24 | }; 25 | 26 | export default buildBlueprintCommands; 27 | -------------------------------------------------------------------------------- /src/cli/cmds/generate/handlers.js: -------------------------------------------------------------------------------- 1 | import buildBlueprintCommands from './build-blueprint-commands'; 2 | import { logYargs } from '../../yargs'; 3 | 4 | const handlers = { 5 | handleRun, 6 | handleHelp 7 | }; 8 | 9 | export default handlers; 10 | 11 | // INFO: helpers object support individual mocks for testing 12 | export const helpers = { 13 | getBlueprintRunner, 14 | getBlueprintHelper, 15 | getBlueprintListHelper, 16 | getBlueprintCommands, 17 | getBlueprintCommand, 18 | logMissingBlueprint 19 | }; 20 | 21 | function handleRun(argv, yargs, rawArgs = process.argv.slice(3)) { 22 | const blueprintRunner = helpers.getBlueprintRunner(yargs, argv.blueprint); 23 | if (blueprintRunner) { 24 | blueprintRunner.parse(rawArgs); 25 | } 26 | } 27 | 28 | function getBlueprintRunner(yargs, blueprintName) { 29 | const blueprintCommand = helpers.getBlueprintCommand(blueprintName); 30 | if (blueprintCommand) { 31 | return yargs 32 | .reset() 33 | .command(blueprintCommand) 34 | .exitProcess(true); 35 | } else { 36 | helpers.logMissingBlueprint(yargs, blueprintName); 37 | } 38 | } 39 | 40 | function handleHelp(argv, yargs) { 41 | const [_, blueprintName] = argv._; 42 | const helper = blueprintName 43 | ? helpers.getBlueprintHelper(yargs, blueprintName) 44 | : helpers.getBlueprintListHelper(yargs); 45 | 46 | if (helper) { 47 | helper.showHelp(); 48 | } 49 | } 50 | 51 | function getBlueprintHelper(yargs, blueprintName) { 52 | const blueprintCommand = helpers.getBlueprintCommand(blueprintName); 53 | if (blueprintCommand) { 54 | blueprintCommand 55 | .builder(yargs) 56 | .updateStrings({ 'Options:': 'Blueprint Options:' }); 57 | return yargs; 58 | } else { 59 | helpers.logMissingBlueprint(yargs, blueprintName); 60 | } 61 | } 62 | 63 | function getBlueprintListHelper(yargs) { 64 | return helpers 65 | .getBlueprintCommands() 66 | .reduce((yargs, command) => yargs.command(command), yargs) 67 | .updateStrings({ 'Commands:': 'Blueprints:' }); 68 | } 69 | 70 | function getBlueprintCommands() { 71 | return buildBlueprintCommands(); 72 | } 73 | 74 | function getBlueprintCommand(blueprintName) { 75 | return buildBlueprintCommands().find( 76 | command => 77 | command.command.match(blueprintName) || 78 | command.aliases.find(alias => alias === blueprintName) 79 | ); 80 | } 81 | 82 | function logMissingBlueprint(yargs, blueprintName) { 83 | logYargs(yargs, `Unknown blueprint '${blueprintName}'`); 84 | } 85 | -------------------------------------------------------------------------------- /src/cli/cmds/init.js: -------------------------------------------------------------------------------- 1 | import getEnvironment from '../environment'; 2 | import Init from '../../sub-commands/init'; 3 | 4 | const subCommand = new Init(getEnvironment()); 5 | 6 | const usage = 'Usage:\n $0 init'; 7 | 8 | module.exports = { 9 | command: 'init', 10 | describe: 'Initialize .blueprintrc for the current project', 11 | builder: yargs => yargs.usage(usage), 12 | handler: () => subCommand.run() 13 | }; 14 | -------------------------------------------------------------------------------- /src/cli/cmds/new.js: -------------------------------------------------------------------------------- 1 | import getEnvironment from '../environment'; 2 | import New from '../../sub-commands/new'; 3 | 4 | const subCommand = new New(getEnvironment()); 5 | 6 | const usage = `Usage: 7 | $0 new 8 | 9 | Create a new project from react-redux-starter-kit`; 10 | 11 | module.exports = { 12 | command: 'new ', 13 | //describe: 'Create a new blueprint-enabled project', 14 | describe: false, // hide in 2.0 15 | builder: yargs => 16 | yargs 17 | .usage(usage) 18 | .option('use-ssh', { 19 | alias: 'S', 20 | describe: 'Fetch starter kit over ssh', 21 | type: 'boolean' 22 | }) 23 | .option('use-boilerplate', { 24 | alias: 'B', 25 | describe: 'Create from redux-cli-boilerplate', 26 | type: 'boolean' 27 | }) 28 | .option('use-uikit', { 29 | alias: 'U', 30 | describe: 'Create from redux-cli-ui-kit-boilerplate', 31 | type: 'boolean' 32 | }) 33 | .strict() 34 | .check(argv => { 35 | if (argv.useBoilerplate && argv.useUikit) { 36 | throw new Error( 37 | 'Only specify one of --use-boilerplate or --use-uikit' 38 | ); 39 | } 40 | return true; 41 | }), 42 | handler: argv => { 43 | subCommand.run({ 44 | dirName: argv.project_name, 45 | useSsh: argv.useSsh, 46 | useBoilerplate: argv.useBoilerplate, 47 | useUIKit: argv.useUikit 48 | }); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/cli/environment.js: -------------------------------------------------------------------------------- 1 | import ProjectSettings from '../models/project-settings'; 2 | import UI from '../models/ui'; 3 | 4 | function makeGetEnvironment() { 5 | let environment; 6 | 7 | return function() { 8 | if (!environment) { 9 | environment = { 10 | ui: new UI(), 11 | settings: new ProjectSettings() 12 | }; 13 | } 14 | return environment; 15 | }; 16 | } 17 | 18 | export default makeGetEnvironment(); 19 | -------------------------------------------------------------------------------- /src/cli/handler.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import { resetYargs } from './yargs'; 3 | 4 | export class Handler { 5 | constructor(emitters = {}) { 6 | const { helpEmitter, runEmitter } = emitters; 7 | this.helpEmitter = helpEmitter || new EventEmitter(); 8 | this.runEmitter = runEmitter || new EventEmitter(); 9 | } 10 | 11 | onHelp(key, fn) { 12 | this.helpEmitter.on(key, fn); 13 | } 14 | 15 | onRun(key, fn) { 16 | this.runEmitter.on(key, fn); 17 | } 18 | 19 | handle(argv, parser) { 20 | let { _: [command], help } = argv; 21 | 22 | if (!command) { 23 | return false; 24 | } 25 | 26 | command = resolveCommandAlias(command, parser); 27 | 28 | if (help) { 29 | // reset the parser ready to build additional help 30 | resetYargs(parser).parse(''); 31 | this.helpEmitter.emit(command, argv, parser); 32 | } else { 33 | // runner controls possible reset 34 | this.runEmitter.emit(command, argv, parser); 35 | } 36 | 37 | return true; 38 | } 39 | } 40 | 41 | export function resolveCommandAlias(command, parser) { 42 | const commands = getCommands(parser); 43 | const resolved = 44 | commands.find( 45 | cmd => cmd.name === command || cmd.aliases.find(a => a === command) 46 | ) || {}; 47 | return resolved.name || command; 48 | } 49 | 50 | export function getCommands(parser) { 51 | return parser 52 | .getUsageInstance() 53 | .getCommands() 54 | .map(command => ({ 55 | command: command[0], 56 | name: command[0].split(' ')[0], 57 | arguments: command[0] 58 | .split(' ') 59 | .slice(1) 60 | .filter(arg => arg.match(/^<.*>$/)) 61 | .map(arg => arg.replace(/<|>/g, '')), 62 | aliases: command[3] 63 | })); 64 | } 65 | 66 | export default (function makeGetHandler() { 67 | let handler; 68 | return function getHandler() { 69 | if (!handler) { 70 | handler = new Handler(); 71 | } 72 | return handler; 73 | }; 74 | })(); 75 | -------------------------------------------------------------------------------- /src/cli/index.js: -------------------------------------------------------------------------------- 1 | import getParser from './parser'; 2 | import getHandler from './handler'; 3 | 4 | function cli() { 5 | const parser = getParser(); 6 | const handler = getHandler(); 7 | const argv = parser.parse(process.argv.slice(2)); 8 | handler.handle(argv, parser); 9 | } 10 | 11 | module.exports = cli; 12 | -------------------------------------------------------------------------------- /src/cli/parser.js: -------------------------------------------------------------------------------- 1 | import getYargs from './yargs'; 2 | 3 | const PACKAGE_KEY = 'blueprint'; 4 | const ENV_PREFIX = PACKAGE_KEY.toUpperCase(); 5 | 6 | const usage = `Usage: 7 | $0 [arguments] [options] 8 | $0 help `; 9 | 10 | export default function getParser(config = {}) { 11 | return getYargs() 12 | .usage(usage) 13 | .commandDir('cmds') 14 | .demandCommand(1, 'Provide a command to run') 15 | .recommendCommands() 16 | .strict() 17 | .help() 18 | .alias('help', 'h') 19 | .version() 20 | .alias('version', 'V') 21 | .global('version', false) 22 | .epilogue('Documentation: https://github.com/SpencerCDixon/redux-cli') 23 | .config(config) 24 | .pkgConf(PACKAGE_KEY) 25 | .env(ENV_PREFIX); 26 | } 27 | -------------------------------------------------------------------------------- /src/cli/yargs.js: -------------------------------------------------------------------------------- 1 | import Yargs from 'yargs/yargs'; 2 | 3 | export default function getYargs() { 4 | return resetYargs(Yargs()); 5 | } 6 | 7 | export function resetYargs(yargs) { 8 | return yargs 9 | .reset() 10 | .help(false) 11 | .version(false) 12 | .exitProcess(true) 13 | .wrap(yargs.terminalWidth()); 14 | } 15 | 16 | /* 17 | This is using a private method of yargs that it probably shouldn't. 18 | Primarily for testing, so that the message is included in the output parameter 19 | of yargs.parse(argv, (err, argv, output) => {}). 20 | 21 | If it's ever an issue it can be swapped out for a simple console.error. 22 | */ 23 | export function logYargs(yargs, message) { 24 | if (message) { 25 | yargs._getLoggerInstance().error(message); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | let config = { 4 | basePath: process.cwd(), 5 | pkgBasePath: path.dirname(module.id) 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/models/blueprint-collection.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import _map from 'lodash/map'; 4 | import _filter from 'lodash/filter'; 5 | import _isNil from 'lodash/isNil'; 6 | import _isBool from 'lodash/isBoolean'; 7 | import _isString from 'lodash/isString'; 8 | import _isArray from 'lodash/isArray'; 9 | import _flatten from 'lodash/flatten'; 10 | import _uniq from 'lodash/uniq'; 11 | import Blueprint from './blueprint'; 12 | 13 | export default class BlueprintCollection { 14 | constructor(pathList) { 15 | this.pathList = pathList; 16 | this.setSearchPaths(); 17 | } 18 | 19 | allPossiblePaths() { 20 | return _flatten( 21 | _map(this.pathList, (arr, base) => _map(arr, bp => expandPath(base, bp))) 22 | ); 23 | } 24 | 25 | setSearchPaths() { 26 | this.searchPaths = _uniq(_filter(this.allPossiblePaths(), validSearchDir)); 27 | } 28 | 29 | all() { 30 | // Is there a more idiomatic way to do this? I miss ruby's ||= 31 | if (this.allBlueprints) { 32 | return this.allBlueprints; 33 | } else { 34 | return (this.allBlueprints = this.discoverBlueprints()); 35 | } 36 | } 37 | 38 | generators() { 39 | // until we learn to tell generators apart from partials 40 | return _filter(this.all(), bp => bp.name); 41 | } 42 | 43 | allNames() { 44 | return _map(this.all(), bp => bp.name); 45 | } 46 | 47 | addBlueprint(path) { 48 | return Blueprint.load(path); 49 | } 50 | 51 | discoverBlueprints() { 52 | return _map(this.findBlueprints(), this.addBlueprint); 53 | } 54 | 55 | findBlueprints() { 56 | return _flatten( 57 | _map(this.searchPaths, dir => { 58 | const subdirs = _map(fs.readdirSync(dir), p => path.resolve(dir, p)); 59 | return _filter(subdirs, d => 60 | fs.existsSync(path.resolve(d, 'index.js')) 61 | ); 62 | }) 63 | ); 64 | } 65 | 66 | lookupAll(name) { 67 | return _filter(this.all(), bp => bp.name === name); 68 | } 69 | 70 | lookup(name) { 71 | return this.lookupAll(name)[0]; 72 | } 73 | } 74 | 75 | function validSearchDir(dir) { 76 | return fs.existsSync(dir) && fs.lstatSync(dir).isDirectory(); 77 | } 78 | 79 | export function expandPath(base, candidate) { 80 | let final; 81 | if (candidate[0] === '~') { 82 | const st = candidate[1] === path.sep ? 2 : 1; 83 | final = path.resolve(process.env.HOME, candidate.slice(st)); 84 | } else if (candidate[0] === path.sep) { 85 | final = path.resolve(candidate); 86 | // } else if (candidate[0] === '@') { 87 | // return path.join(npmPath,npm name, 'blueprints'); 88 | } else { 89 | final = path.resolve(base, candidate); 90 | } 91 | return final; 92 | } 93 | 94 | export function parseBlueprintSetting(setting) { 95 | if (_isArray(setting)) { 96 | return [...setting, './blueprints']; 97 | } else if (_isString(setting)) { 98 | return [setting, './blueprints']; 99 | } else if (_isBool(setting)) { 100 | return setting ? ['./blueprints'] : []; 101 | } else if (_isNil(setting)) { 102 | return ['./blueprints']; 103 | } else { 104 | // No numbers, 105 | // raise error here? 106 | // console.error('Unknown blueprint type'); 107 | return ['./blueprints']; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/models/blueprint.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import _ from 'lodash'; 3 | import walkSync from 'walk-sync'; 4 | import fs from 'fs'; 5 | 6 | import { fileExists } from '../util/fs'; 7 | import mixin from '../util/mixin'; 8 | import { normalizeCasing } from '../util/text-helper'; 9 | import FileInfo from './file-info'; 10 | import config from '../config'; 11 | 12 | const { basePath } = config; 13 | 14 | export default class Blueprint { 15 | constructor(blueprintPath) { 16 | this.path = blueprintPath; 17 | this.name = path.basename(blueprintPath); 18 | this.command = this.command || {}; // default if not set by mixin 19 | } 20 | 21 | // HOOK: this can be overridden 22 | description() { 23 | return `Generates a new ${this.name}`; 24 | } 25 | 26 | // HOOK: that can be overridden. Defaults to look in /files. 27 | filesPath() { 28 | return path.join(this.path, 'files'); 29 | } 30 | 31 | files() { 32 | if (this._files) { 33 | return this._files; 34 | } 35 | 36 | let filesPath = this.filesPath(); 37 | 38 | if (fileExists(filesPath)) { 39 | this._files = walkSync(filesPath); 40 | } else { 41 | this._files = []; 42 | } 43 | return this._files; 44 | } 45 | 46 | // load in the blueprint that was found, extend this class to load it 47 | static load(blueprintPath) { 48 | let Constructor; 49 | const constructorPath = path.resolve(blueprintPath, 'index.js'); 50 | 51 | if (fs.lstatSync(blueprintPath).isDirectory()) { 52 | if (fileExists(constructorPath)) { 53 | const blueprintModule = require(constructorPath); 54 | Constructor = mixin(Blueprint, blueprintModule); 55 | 56 | return new Constructor(blueprintPath); 57 | } 58 | } 59 | } 60 | 61 | _fileMapTokens(options) { 62 | const standardTokens = { 63 | __name__: options => { 64 | const name = options.entity.name; 65 | const casing = options.settings.getSetting('fileCasing') || 'default'; 66 | return normalizeCasing(name, casing); 67 | }, 68 | __path__: options => { 69 | return options.originalBlueprintName; 70 | }, 71 | __root__: options => { 72 | return options.settings.getSetting('sourceBase'); 73 | }, 74 | __test__: options => { 75 | return options.settings.getSetting('testBase'); 76 | } 77 | }; 78 | 79 | // HOOK: calling into the blueprints fileMapTokens() hook, passing along 80 | // an options hash coming from _generateFileMapVariables() 81 | const customTokens = this.fileMapTokens(options) || {}; 82 | return Object.assign({}, standardTokens, customTokens); 83 | } 84 | 85 | generateFileMap(fileMapOptions) { 86 | const tokens = this._fileMapTokens(fileMapOptions); 87 | const fileMapValues = _.values(tokens); 88 | const tokenValues = fileMapValues.map(token => token(fileMapOptions)); 89 | const tokenKeys = _.keys(tokens); 90 | return _.zipObject(tokenKeys, tokenValues); 91 | } 92 | 93 | // Set up options to be passed to fileMapTokens that get generated. 94 | _generateFileMapVariables(entityName, locals, options) { 95 | const originalBlueprintName = options.originalBlueprintName || this.name; 96 | const { settings, entity } = options; 97 | 98 | return { 99 | originalBlueprintName, 100 | settings, 101 | entity, 102 | locals 103 | }; 104 | } 105 | 106 | // Given a file and a fileMap from locals, convert path names 107 | // to be correct string 108 | mapFile(file, locals) { 109 | let pattern, i; 110 | const fileMap = locals.fileMap || { __name__: locals.camelEntityName }; 111 | for (i in fileMap) { 112 | pattern = new RegExp(i, 'g'); 113 | file = file.replace(pattern, fileMap[i]); 114 | } 115 | return file; 116 | } 117 | 118 | _locals(options) { 119 | const entityName = options.entity && options.entity.name; 120 | const customLocals = this.locals(options); 121 | const fileMapVariables = this._generateFileMapVariables( 122 | entityName, 123 | customLocals, 124 | options 125 | ); 126 | const fileMap = this.generateFileMap(fileMapVariables); 127 | 128 | const standardLocals = { 129 | pascalEntityName: normalizeCasing(entityName, 'pascal'), 130 | camelEntityName: normalizeCasing(entityName, 'camel'), 131 | snakeEntityName: normalizeCasing(entityName, 'snake'), 132 | dashesEntityName: normalizeCasing(entityName, 'dashes'), 133 | fileMap 134 | }; 135 | 136 | return Object.assign({}, standardLocals, customLocals); 137 | } 138 | 139 | _process(options, beforeHook, process, afterHook) { 140 | const locals = this._locals(options); 141 | return Promise.resolve() 142 | .then(beforeHook.bind(this, options, locals)) 143 | .then(process.bind(this, locals)) 144 | .then(afterHook.bind(this, options)); 145 | } 146 | 147 | processFiles(locals) { 148 | const files = this.files(); 149 | const fileInfos = files.map(file => this.buildFileInfo(locals, file)); 150 | this.ui.writeDebug(`built file infos: ${fileInfos.length}`); 151 | const filesToWrite = fileInfos.filter(info => info.isFile()); 152 | this.ui.writeDebug(`files to write: ${filesToWrite.length}`); 153 | filesToWrite.map(file => file.writeFile(this.dryRun)); 154 | } 155 | 156 | buildFileInfo(locals, file) { 157 | const mappedPath = this.mapFile(file, locals); 158 | this.ui.writeDebug(`mapped path: ${mappedPath}`); 159 | 160 | return new FileInfo({ 161 | ui: this.ui, 162 | templateVariables: locals, 163 | originalPath: this.srcPath(file), 164 | mappedPath: this.destPath(mappedPath), 165 | outputPath: this.destPath(file) 166 | }); 167 | } 168 | 169 | // where the files will be written to 170 | destPath(mappedPath) { 171 | return path.resolve(basePath, mappedPath); 172 | } 173 | 174 | // location of the string templates 175 | srcPath(file) { 176 | return path.resolve(this.filesPath(), file); 177 | } 178 | 179 | /* 180 | * install options: 181 | 182 | const blueprintOptions = { 183 | originalBlueprintName: name, 184 | ui: this.ui, 185 | settings: this.settings, 186 | entity: { 187 | name: cliArgs.entity.name, 188 | options cliArgs.entity.options 189 | } 190 | }; 191 | */ 192 | install(options) { 193 | const ui = (this.ui = options.ui); 194 | this.dryRun = options.dryRun; 195 | 196 | ui.writeInfo('installing blueprint...'); 197 | return this._process( 198 | options, 199 | this.beforeInstall, 200 | this.processFiles, 201 | this.afterInstall 202 | ).then(() => { 203 | ui.writeInfo('finished installing blueprint.'); 204 | }); 205 | } 206 | 207 | // uninstall() { 208 | // } 209 | 210 | // HOOKS: 211 | locals() {} 212 | fileMapTokens() {} 213 | beforeInstall() {} 214 | afterInstall() {} 215 | 216 | // TODO: add uninstall hooks once support for uninstall exists 217 | // beforeUninstall() {} 218 | // afterUninstall() {} 219 | 220 | // HOOK: for normalizing entity name that gets passed in as an arg 221 | // via the CLI 222 | // normalizeEntityName(options) { 223 | // return normalizeEntityName(name); 224 | // } 225 | } 226 | -------------------------------------------------------------------------------- /src/models/file-info.js: -------------------------------------------------------------------------------- 1 | import ejs from 'ejs'; 2 | import fs from 'fs'; 3 | import { outputFileSync } from 'fs-extra'; 4 | import { fileExists } from '../util/fs'; 5 | 6 | class FileInfo { 7 | constructor(args) { 8 | this.ui = args.ui; 9 | this.templateVariables = args.templateVariables; // locals passed to ejs template 10 | this.originalPath = args.originalPath; // path to template 11 | this.mappedPath = FileInfo.removeEjsExt(args.mappedPath); // destination path to be written to 12 | } 13 | 14 | static removeEjsExt(path) { 15 | return path.replace(/\.ejs$/i, ''); 16 | } 17 | 18 | writeFile(dryRun) { 19 | this.ui.writeDebug(`Attempting to write file: ${this.mappedPath}`); 20 | if (fileExists(this.mappedPath)) { 21 | this.ui.writeError( 22 | `Not writing file. File already exists at: ${this.mappedPath}` 23 | ); 24 | } else { 25 | const fileContent = this.renderTemplate(); 26 | this.ui.writeDebug(`fileContent: ${fileContent}`); 27 | 28 | if (!dryRun) { 29 | outputFileSync(this.mappedPath, fileContent); 30 | this.ui.writeCreate(this.mappedPath); 31 | } else { 32 | this.ui.writeWouldCreate(this.mappedPath); 33 | } 34 | } 35 | return; 36 | } 37 | 38 | renderTemplate() { 39 | let rendered; 40 | this.ui.writeDebug(`rendering template: ${this.originalPath}`); 41 | const template = fs.readFileSync(this.originalPath, 'utf8'); 42 | 43 | try { 44 | rendered = ejs.render(template, this.templateVariables); 45 | } catch (err) { 46 | this.ui.writeDebug('couldnt render'); 47 | err.message += 48 | ' (Error in blueprint template: ' + this.originalPath + ')'; 49 | this.ui.writeError(`error was: ${err.message}`); 50 | throw err; 51 | } 52 | return rendered; 53 | } 54 | 55 | isFile() { 56 | let fileCheck; 57 | try { 58 | fileCheck = fs.lstatSync(this.originalPath).isFile(); 59 | } catch (e) { 60 | if (e.code === 'ENOENT') { 61 | return false; 62 | } else { 63 | throw e; 64 | } 65 | } 66 | this.ui.writeDebug(`checking file: ${this.originalPath} - ${fileCheck}`); 67 | return fileCheck; 68 | } 69 | } 70 | 71 | export default FileInfo; 72 | -------------------------------------------------------------------------------- /src/models/project-settings.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import jf from 'jsonfile'; 3 | import { pwd } from 'shelljs'; 4 | import rc from 'rc'; 5 | import cc from 'rc/lib/utils'; 6 | import _zipObject from 'lodash/zipObject'; 7 | import _map from 'lodash/map'; 8 | 9 | import BlueprintCollection, { 10 | parseBlueprintSetting 11 | } from './blueprint-collection'; 12 | 13 | export default class ProjectSettings { 14 | // public & tested - maintain in 2.0 15 | constructor(defaultSettings = {}, args = null) { 16 | this.defaultSettings = defaultSettings; 17 | this.args = args; 18 | this.blueprintChunks = []; 19 | this.configChunks = []; 20 | this.myParse = this.myParse.bind(this); 21 | this.saveDefaults = this.saveDefaults.bind(this); 22 | this.loadSettings(); 23 | this.blueprints = new BlueprintCollection( 24 | _zipObject(this.configDirs(), this.blueprintChunks) 25 | ); 26 | } 27 | 28 | configDirs() { 29 | return _map(this.configFiles(), configFile => path.dirname(configFile)); 30 | } 31 | 32 | configFiles() { 33 | return _map(this.settings.configs, configFile => path.resolve(configFile)); 34 | } 35 | 36 | allConfigs() { 37 | const configs = _zipObject(this.settings.configs, this.configChunks); 38 | configs['__default__'] = this.defaultSettings; 39 | return configs; 40 | } 41 | 42 | // internal & tested 43 | // from #constructor 44 | loadSettings() { 45 | const startingSettings = JSON.parse(JSON.stringify(this.defaultSettings)); 46 | this.settings = rc('blueprint', startingSettings, this.args, this.myParse); 47 | } 48 | 49 | // internal & tested - maintain in 2.0 50 | // #settingsExist 51 | // #save 52 | settingsPath() { 53 | return path.join(pwd(), '.blueprintrc'); 54 | } 55 | 56 | //public & tested - maintain in 2.0 57 | getSetting(key) { 58 | return this.settings[key]; 59 | } 60 | 61 | //public & tested - maintain in 2.0 62 | getAllSettings() { 63 | return this.settings; 64 | } 65 | 66 | //deprecate. can't see the use of this, especially with nested settings 67 | //unused & tested 68 | setSetting(key, val) { 69 | this.settings[key] = val; 70 | } 71 | 72 | //internal & tested - maintain in 2.0 73 | setAllSettings(json) { 74 | this.settings = json; 75 | } 76 | 77 | // public - maintain in 2.0 78 | saveDefaults(defaultSettings = this.defaultSettings, savePath = false) { 79 | jf.writeFileSync(savePath || this.settingsPath(), defaultSettings); 80 | } 81 | 82 | // wrap the default rc parsing function with this 83 | // By default rc returns everything merged. Keeping 84 | // track of chunks allows us to associate a config 85 | // with the .blueprintrc file it came out of. 86 | myParse(rawContent) { 87 | const content = cc.parse(rawContent); 88 | this.configChunks.unshift(content); 89 | this.blueprintChunks.unshift(parseBlueprintSetting(content)); 90 | return content; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/models/sub-command.js: -------------------------------------------------------------------------------- 1 | import ProjectSettings from './project-settings'; 2 | import UI from './ui'; 3 | import figlet from 'figlet'; 4 | import { success } from '../util/text-helper'; 5 | 6 | class SubCommand { 7 | constructor(options = {}) { 8 | this.rawOptions = options; 9 | this.settings = options.settings || new ProjectSettings(); 10 | this.ui = options.ui || new UI(); 11 | 12 | this.environment = { 13 | ui: this.ui, 14 | settings: this.settings 15 | }; 16 | } 17 | 18 | run() { 19 | throw new Error('Subcommands must implement a run()'); 20 | } 21 | 22 | availableOptions() { 23 | throw new Error('Subcommands must implement an availableOptions()'); 24 | } 25 | 26 | cliLogo() { 27 | return success( 28 | figlet.textSync('Blueprint-CLI', { 29 | font: 'Doom', 30 | horizontalLayout: 'default', 31 | verticalLayout: 'default' 32 | }) 33 | ); 34 | } 35 | } 36 | 37 | export default SubCommand; 38 | -------------------------------------------------------------------------------- /src/models/task.js: -------------------------------------------------------------------------------- 1 | class Task { 2 | constructor(environment) { 3 | this.ui = environment.ui; 4 | this.settings = environment.settings; 5 | } 6 | 7 | run() { 8 | throw new Error('Tasks must implement run()'); 9 | } 10 | } 11 | 12 | export default Task; 13 | -------------------------------------------------------------------------------- /src/models/ui.js: -------------------------------------------------------------------------------- 1 | import { EOL } from 'os'; 2 | import chalk from 'chalk'; 3 | import elegantSpinner from 'elegant-spinner'; 4 | import logUpdate from 'log-update'; 5 | 6 | const frame = elegantSpinner(); 7 | const DEFAULT_WRITE_LEVEL = 'INFO'; 8 | const WRITE_LEVELS = { 9 | DEBUG: 1, 10 | INFO: 2, 11 | WARNING: 3, 12 | ERROR: 4 13 | }; 14 | 15 | class UI { 16 | constructor(options = {}) { 17 | this.inputStream = options.inputStream || process.stdin; 18 | this.outputStream = options.outputStream || process.stdout; 19 | this.errorStream = options.errorStream || process.stderr; 20 | 21 | this.writeLevel = options.writeLevel || DEFAULT_WRITE_LEVEL; 22 | this.streaming = false; 23 | } 24 | 25 | write(data, writeLevel) { 26 | if (writeLevel === 'ERROR') { 27 | this.errorStream.write(data); 28 | } else if (this.writeLevelVisible(writeLevel)) { 29 | this.outputStream.write(data); 30 | } 31 | } 32 | 33 | writeLine(text, writeLevel) { 34 | this.write(text + EOL, writeLevel); 35 | } 36 | 37 | writeInfo(text) { 38 | const content = chalk.blue(' info: ') + chalk.white(text); 39 | this.writeLine(content, 'INFO'); 40 | } 41 | 42 | writeDebug(text) { 43 | const content = chalk.gray(' debug: ') + chalk.white(text); 44 | this.writeLine(content, 'DEBUG'); 45 | } 46 | 47 | writeError(text) { 48 | const content = chalk.red(' error: ') + chalk.white(text); 49 | this.writeLine(content, 'ERROR'); 50 | } 51 | 52 | writeWarning(text) { 53 | const content = chalk.yellow(' warning: ') + chalk.white(text); 54 | this.writeLine(content, 'WARNING'); 55 | } 56 | 57 | writeCreate(text) { 58 | const content = chalk.green(' create: ') + chalk.white(text); 59 | this.writeLine(content, 'INFO'); 60 | } 61 | 62 | writeWouldCreate(text) { 63 | const content = chalk.green(' would create: ') + chalk.white(text); 64 | this.writeLine(content, 'INFO'); 65 | } 66 | 67 | writeLevelVisible(writeLevel = DEFAULT_WRITE_LEVEL) { 68 | return WRITE_LEVELS[writeLevel] >= WRITE_LEVELS[this.writeLevel]; 69 | } 70 | 71 | setWriteLevel(newLevel) { 72 | const allowedLevels = Object.keys(WRITE_LEVELS); 73 | if (allowedLevels.indexOf(newLevel) === -1) { 74 | throw new Error( 75 | `Unknown write level. Valid values are: ${allowedLevels.join(', ')}` 76 | ); 77 | } 78 | 79 | this.writeLevel = newLevel; 80 | } 81 | 82 | startProgress(string, customStream) { 83 | const stream = customStream || logUpdate.create(this.outputStream); 84 | if (this.writeLevelVisible(this.writeLevel)) { 85 | this.streaming = true; 86 | this.progressInterval = setInterval(() => { 87 | stream( 88 | ` ${chalk.green('loading:')} ${string} ${chalk.cyan.bold.dim( 89 | frame() 90 | )}` 91 | ); 92 | }, 100); 93 | } 94 | } 95 | 96 | stopProgress() { 97 | if (this.progressInterval) { 98 | this.streaming = false; 99 | clearInterval(this.progressInterval); 100 | } 101 | } 102 | } 103 | 104 | export default UI; 105 | -------------------------------------------------------------------------------- /src/prompts/initPrompt.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | const schema = { 4 | properties: { 5 | sourceBase: { 6 | description: chalk.blue('Path to your source code? (relative from root)'), 7 | type: 'string', 8 | required: true 9 | }, 10 | testBase: { 11 | description: chalk.blue('Path to your test code? (relative from root)'), 12 | type: 'string', 13 | required: true 14 | }, 15 | smartPath: { 16 | description: chalk.blue('Where is path to Smart/Container Components?'), 17 | type: 'string', 18 | required: true 19 | }, 20 | dumbPath: { 21 | description: chalk.blue('Where is path to Dumb/Pure Components?'), 22 | type: 'string', 23 | required: true 24 | }, 25 | fileCasing: { 26 | description: chalk.blue( 27 | 'How do you want file casing to be configured? (default|snake|pascal|camel)' 28 | ), 29 | pattern: /(default|snake|pascal|camel|dashes)/, 30 | required: true, 31 | type: 'string' 32 | } 33 | } 34 | }; 35 | 36 | export default schema; 37 | -------------------------------------------------------------------------------- /src/prompts/setup.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export const setupPrompt = (promptType, prompt) => { 4 | prompt.message = chalk.green(`${promptType}: `); 5 | prompt.delimiter = ''; 6 | prompt.start(); 7 | }; 8 | -------------------------------------------------------------------------------- /src/sub-commands/config.js: -------------------------------------------------------------------------------- 1 | import prettyjson from 'prettyjson'; 2 | import SubCommand from '../models/sub-command'; 3 | 4 | class Config extends SubCommand { 5 | constructor(options) { 6 | super(options); 7 | } 8 | 9 | printUserHelp() { 10 | this.ui.write('config command to display current configuration'); 11 | } 12 | 13 | run() { 14 | const finalConfig = Object.assign({}, this.settings.settings); 15 | delete finalConfig.configs; 16 | delete finalConfig.allConfigs; 17 | delete finalConfig['_']; 18 | this.ui.write(this.cliLogo() + '\n'); 19 | this.ui.writeInfo('Config Files'); 20 | console.log(prettyjson.render(this.settings.settings.configs, {}, 8)); 21 | // this.settings.settings.configs.forEach(configFile => {this.ui.writeInfo(` * ${configFile}`)}) 22 | this.ui.writeInfo('Config Data'); 23 | console.log(prettyjson.render(finalConfig, {}, 10)); 24 | this.ui.writeInfo('Blueprint Paths'); 25 | console.log(prettyjson.render(this.settings.blueprints.searchPaths, {}, 8)); 26 | this.ui.writeInfo('Blueprints'); 27 | console.log(prettyjson.render(this.settings.blueprints.allNames(), {}, 8)); 28 | } 29 | } 30 | 31 | export default Config; 32 | -------------------------------------------------------------------------------- /src/sub-commands/generate.js: -------------------------------------------------------------------------------- 1 | import SubCommand from '../models/sub-command'; 2 | import Blueprint from '../models/blueprint'; 3 | import GenerateFromBluePrint from '../tasks/generate-from-blueprint'; 4 | import chalk from 'chalk'; 5 | 6 | // Primary purpose is to take cli args and pass them through 7 | // to the proper task that will do the generation. 8 | // 9 | // Logic for displaying all blueprints and what their options 10 | // are will live in here. For now it's pretty baren. 11 | class Generate extends SubCommand { 12 | constructor(options) { 13 | super(options); 14 | this.generateTask = new GenerateFromBluePrint(this.environment); 15 | } 16 | 17 | printUserHelp() { 18 | const blueprints = Blueprint.list(); 19 | 20 | this.ui.writeLine('Available Blueprints:'); 21 | this.ui.writeLine('(sources on the top will override sources below)'); 22 | this.ui.writeLine(''); 23 | 24 | blueprints.forEach(blueprintSource => { 25 | this.ui.writeLine( 26 | ` ${chalk.blue('Blueprint Source')} ===> ${chalk.green( 27 | blueprintSource.source 28 | )}:` 29 | ); 30 | 31 | blueprintSource.blueprints.forEach(blueprint => { 32 | this.ui.writeLine(` ${blueprint.name} ${chalk.yellow('')}`); 33 | this.ui.writeLine(` ${chalk.gray(blueprint.description)}`); 34 | }); 35 | this.ui.writeLine(''); 36 | }); 37 | } 38 | 39 | run(blueprintName, cliArgs) { 40 | if (cliArgs.debug) { 41 | this.ui.setWriteLevel('DEBUG'); 42 | } 43 | 44 | this.generateTask.run(blueprintName, cliArgs); 45 | } 46 | } 47 | 48 | export default Generate; 49 | -------------------------------------------------------------------------------- /src/sub-commands/init.js: -------------------------------------------------------------------------------- 1 | import prompt from 'prompt'; 2 | import SubCommand from '../models/sub-command'; 3 | import initPrompt from '../prompts/initPrompt'; 4 | import { setupPrompt } from '../prompts/setup'; 5 | 6 | class Init extends SubCommand { 7 | constructor(options) { 8 | super(options); 9 | setupPrompt('initialization', prompt); 10 | } 11 | 12 | printUserHelp() { 13 | this.ui.write( 14 | 'initialization command to create a .blueprintrc which has project settings' 15 | ); 16 | } 17 | 18 | run() { 19 | this.ui.write(this.cliLogo()); 20 | prompt.get(initPrompt, (err, result) => { 21 | this.ui.writeInfo('Saving your settings...'); 22 | this.settings.saveDefaults(result); 23 | this.ui.writeCreate( 24 | '.blueprintrc with configuration saved in project root.' 25 | ); 26 | }); 27 | } 28 | } 29 | 30 | export default Init; 31 | -------------------------------------------------------------------------------- /src/sub-commands/new.js: -------------------------------------------------------------------------------- 1 | import { which, rm, exec } from 'shelljs'; 2 | import SubCommand from '../models/sub-command'; 3 | import CreateAndStepIntoDirectory from '../tasks/create-and-step-into-directory'; 4 | import GitPull from '../tasks/git-pull'; 5 | import ProjectSettings from '../models/project-settings'; 6 | 7 | // eventually allow users to create new projects based on a flag 8 | // ie. they can create a new react-redux-starter-kit or a new 9 | // universal react starter kit, etc. 10 | 11 | class New extends SubCommand { 12 | constructor(options) { 13 | super(options); 14 | this.createDirTask = new CreateAndStepIntoDirectory(this.environment); 15 | this.gitPullTask = new GitPull(this.environment); 16 | } 17 | 18 | printUserHelp() { 19 | this.ui.write('Command used for generating new redux projects'); 20 | } 21 | 22 | run(cliArgs) { 23 | this.confirmGit(); 24 | this.createDirTask.run(cliArgs).then(() => { 25 | let fetch_url; 26 | 27 | if (cliArgs.useBoilerplate) { 28 | fetch_url = 29 | 'https://github.com/SpencerCDixon/redux-cli-boilerplate.git'; 30 | } else if (cliArgs.useUIKit) { 31 | fetch_url = 32 | 'https://github.com/SpencerCDixon/redux-cli-ui-kit-boilerplate.git'; 33 | } else { 34 | fetch_url = 'https://github.com/davezuko/react-redux-starter-kit.git'; 35 | } 36 | 37 | if (cliArgs.useSsh) { 38 | this.ui.writeInfo('Using SSH to fetch repo'); 39 | 40 | if (cliArgs.useBoilerplate) { 41 | fetch_url = 'git@github.com:SpencerCDixon/redux-cli-boilerplate.git'; 42 | } else if (cliArgs.useUIKit) { 43 | fetch_url = 44 | 'https://github.com/SpencerCDixon/redux-cli-ui-kit-boilerplate.git'; 45 | } else { 46 | fetch_url = 'git@github.com:davezuko/react-redux-starter-kit.git'; 47 | } 48 | } 49 | this.gitPullTask.run(fetch_url).then(() => { 50 | this.createProjectSettings(); 51 | this.resetGitHistory(); 52 | }); 53 | }); 54 | } 55 | 56 | confirmGit() { 57 | if (!which('git')) { 58 | this.ui.writeError('This script requires you have git installed'); 59 | this.ui.writeInfo('If you have homebrew installed try: brew install git'); 60 | process.exit(1); 61 | } 62 | } 63 | 64 | // Should maybe prompt user for permission to do this since it's dangerous. 65 | resetGitHistory() { 66 | this.ui.writeInfo('Removing the starter kit .git folder'); 67 | rm('-rf', '.git'); 68 | exec('git init && git add -A && git commit -m"Initial commit"', { 69 | silent: true 70 | }); 71 | this.ui.writeCreate('Created new .git history for your project'); 72 | this.ui.writeInfo( 73 | 'Congrats! New Redux app ready to go. CLI generators configured and ready' + 74 | ' to go' 75 | ); 76 | } 77 | 78 | // All settings for react-redux-starter-kit live in this template so when 79 | // new projects get created users can immediately start using the CLI 80 | createProjectSettings() { 81 | this.ui.writeInfo('creating a default .blueprintrc for your project'); 82 | const settings = new ProjectSettings(); 83 | settings.saveDefault(); 84 | 85 | this.ui.writeCreate('.blueprintrc with starter kit settings saved.'); 86 | } 87 | } 88 | 89 | export default New; 90 | -------------------------------------------------------------------------------- /src/tasks/create-and-step-into-directory.js: -------------------------------------------------------------------------------- 1 | import { test, cd, exec } from 'shelljs'; 2 | import fs from 'fs'; 3 | import denodeify from 'denodeify'; 4 | 5 | import Task from '../models/task'; 6 | 7 | const mkdir = denodeify(fs.mkdir); 8 | 9 | export default class extends Task { 10 | constructor(environment) { 11 | super(environment); 12 | } 13 | 14 | run(options) { 15 | this.dirName = options.dirName; 16 | this.confirmDir(); 17 | 18 | this.ui.writeInfo('Creating new directory...'); 19 | return mkdir(this.dirName).then(() => { 20 | this.ui.writeCreate(`Created directory: ${this.dirName}`); 21 | cd(this.dirName); 22 | this.initGit(); 23 | }); 24 | } 25 | 26 | confirmDir() { 27 | if (test('-d', this.dirName)) { 28 | this.ui.writeError( 29 | `${this.dirName} directory already exists! Please choose another name` 30 | ); 31 | process.exit(1); 32 | } 33 | } 34 | 35 | initGit() { 36 | this.ui.writeInfo('Setting up tracking with git...'); 37 | exec('git init', { silent: true }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/tasks/generate-from-blueprint.js: -------------------------------------------------------------------------------- 1 | import Task from '../models/task'; 2 | 3 | export default class extends Task { 4 | constructor(environment) { 5 | super(environment); 6 | } 7 | 8 | // confirm blueprint exists 9 | // go fetch blueprint object 10 | // noramlize/setup args to be passed to install 11 | // install the blueprint 12 | run(blueprintName, cliArgs) { 13 | // if blueprint doesnt exist 14 | // this.ui.writeError( 15 | // 'this is not a valid blueprint. type help for help.. or w/e' 16 | // ); 17 | // process.exit(1); 18 | // } 19 | 20 | const mainBlueprint = this.lookupBlueprint(blueprintName); 21 | 22 | const entity = { 23 | name: cliArgs.entity.name, 24 | options: cliArgs.entity.options 25 | }; 26 | 27 | const blueprintOptions = { 28 | originalBlueprintName: blueprintName, 29 | ui: this.ui, 30 | settings: this.settings, 31 | dryRun: cliArgs.dryRun, 32 | entity 33 | }; 34 | 35 | mainBlueprint.install(blueprintOptions); 36 | } 37 | 38 | lookupBlueprint(name) { 39 | return this.settings.blueprints.lookup(name); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/tasks/git-pull.js: -------------------------------------------------------------------------------- 1 | import Task from '../models/task'; 2 | import denodeify from 'denodeify'; 3 | 4 | const exec = denodeify(require('child_process').exec); 5 | 6 | export default class extends Task { 7 | constructor(environment) { 8 | super(environment); 9 | } 10 | 11 | run(gitUrl) { 12 | const ui = this.ui; 13 | ui.startProgress(`Fetching ${gitUrl} from github.`); 14 | 15 | return exec(`git pull ${gitUrl}`, { 16 | silent: true 17 | }).then((err, stdout, stderr) => { 18 | ui.stopProgress(); 19 | 20 | if (err) { 21 | ui.writeError( 22 | 'Could not git-pull repository... please try again. Make sure you have internet access' 23 | ); 24 | ui.writeError(`Error code: ${err}`); 25 | ui.writeError(stdout); 26 | ui.writeError(stderr); 27 | process.exit(1); 28 | } 29 | ui.writeInfo('pulled down repo'); 30 | Promise.resolve(); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/util/fs.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | // import fse from 'fs-extra'; 4 | // import temp from 'temp'; 5 | // import denodeify from 'denodeify'; 6 | 7 | const rootPath = process.cwd(); 8 | // const mkdir = denodeify(fs.mkdir); 9 | // const mkTmpDir = denodeify(temp.mkdir); 10 | 11 | /* 12 | Node deprecated existsSync so this is a simple 13 | helper function wrapping try/catch around the new 14 | recommended approach of accessSync 15 | https://nodejs.org/api/fs.html#fs_fs_existssync_path 16 | */ 17 | export const fileExists = filename => { 18 | try { 19 | fs.accessSync(filename); 20 | return true; 21 | } catch (e) { 22 | if (e.code === 'ENOENT') { 23 | return false; 24 | } else { 25 | throw e; 26 | } 27 | } 28 | }; 29 | 30 | export const readFile = filename => { 31 | const filePath = path.join(rootPath, filename); 32 | return fs.readFileSync(filePath, 'utf8'); 33 | }; 34 | 35 | // Promise based fs helpers 36 | // currently not being used after blueprint refactor. Keeping in case 37 | // I want to use later... 38 | 39 | // export const dirExists = (dirPath) => { 40 | // return new Promise(resolve => { 41 | // fse.exists(dirPath, resolve); 42 | // }); 43 | // }; 44 | 45 | // export const mkTmpDirIn = (dirPath) => { 46 | // return dirExists(dirPath).then(doesExist => { 47 | // if (!doesExist) { 48 | // return mkdir(dirPath); 49 | // } 50 | // }).then(() => { 51 | // return mkTmpDir({ dir: dirPath}); 52 | // }); 53 | // }; 54 | -------------------------------------------------------------------------------- /src/util/mixin.js: -------------------------------------------------------------------------------- 1 | // Simple mixin utility that acts like 'extends' 2 | const mixin = (Parent, ...mixins) => { 3 | class Mixed extends Parent {} 4 | for (let mixin of mixins) { 5 | for (let prop in mixin) { 6 | Mixed.prototype[prop] = mixin[prop]; 7 | } 8 | } 9 | return Mixed; 10 | }; 11 | 12 | export default mixin; 13 | -------------------------------------------------------------------------------- /src/util/text-helper.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { depascalize, pascalize, camelize } from 'humps'; 3 | 4 | // Bootstrap inspired text color helpers for the command line. 5 | export const success = text => { 6 | return chalk.green(text); 7 | }; 8 | 9 | export const danger = text => { 10 | return chalk.red(text); 11 | }; 12 | 13 | export const warning = text => { 14 | return chalk.yellow(text); 15 | }; 16 | 17 | // Random string/text helpers 18 | export const normalizeComponentName = name => { 19 | return pascalize(name); 20 | }; 21 | 22 | export const normalizeDuckName = name => { 23 | return camelize(name); 24 | }; 25 | 26 | export const normalizeCasing = (string, casing) => { 27 | const types = ['default', 'snake', 'pascal', 'camel', 'dashes']; 28 | 29 | if (types.indexOf(casing) === -1) { 30 | throw new Error(`Casing must be one of: ${types.join(', ')} types`); 31 | } 32 | 33 | if (casing === 'snake') { 34 | return depascalize(pascalize(string)); 35 | } else if (casing === 'pascal') { 36 | return pascalize(string); 37 | } else if (casing === 'camel') { 38 | return camelize(string); 39 | } else if (casing === 'default') { 40 | return string; 41 | } else if (casing == 'dashes') { 42 | return depascalize(string, { separator: '-' }); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/version.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { readJsonSync } from 'fs-extra'; 3 | import config from './config'; 4 | 5 | const { pkgBasePath } = config; 6 | 7 | export const version = () => { 8 | const pkgPath = path.join(pkgBasePath, '../package.json'); 9 | return readJsonSync(pkgPath).version; 10 | }; 11 | -------------------------------------------------------------------------------- /templates/.blueprintrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceBase":"", 3 | "testBase":"", 4 | "smartPath":"", 5 | "dumbPath":"", 6 | "fileCasing":"" 7 | } 8 | -------------------------------------------------------------------------------- /templates/.starterrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceBase":"src", 3 | "testBase":"tests", 4 | "smartPath":"containers", 5 | "dumbPath":"components", 6 | "fileCasing":"default" 7 | } 8 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../.eslintrc", 3 | "env" : { 4 | "mocha" : true 5 | }, 6 | "globals" : { 7 | "jest" : false, 8 | "expect" : false, 9 | "should" : false, 10 | "sinon" : false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/cli/cmds/config.test.js: -------------------------------------------------------------------------------- 1 | import getParser from 'cli/parser'; 2 | import { lineRegEx } from '../../helpers/regex-utils'; 3 | import Config from 'sub-commands/config'; 4 | 5 | jest.mock('sub-commands/config'); 6 | 7 | describe('(CLI) Config', () => { 8 | let parser; 9 | 10 | beforeEach(() => { 11 | parser = getParser(); 12 | parser.$0 = 'bp'; 13 | }); 14 | 15 | describe('--help', () => { 16 | test('shows Usage', done => { 17 | parser.parse('help config', (err, argv, output) => { 18 | expect(err).to.be.undefined; 19 | expect(output).to.include('Usage:'); 20 | expect(output).to.match(lineRegEx('bp config')); 21 | done(); 22 | }); 23 | }); 24 | 25 | test("doesn't include --version", done => { 26 | parser.parse('help init', (err, argv, output) => { 27 | expect(output).to.not.include('--version, -V'); 28 | done(); 29 | }); 30 | }); 31 | }); 32 | 33 | describe('handler', () => { 34 | test('runs subCommand without arguments', done => { 35 | parser.parse('config', (err, argv, output) => { 36 | expect(Config.mock.instances.length).toEqual(1); 37 | expect(Config.mock.instances[0].run.mock.calls.length).toEqual(1); 38 | expect(Config.mock.instances[0].run.mock.calls[0]).toEqual([]); 39 | expect(output).toEqual(''); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/cli/cmds/generate.test.js: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs/yargs'; 2 | 3 | import { default as generate } from 'cli/cmds/generate'; 4 | import { lineRegEx } from '../../helpers/regex-utils'; 5 | 6 | describe('(CLI) Generate', () => { 7 | let parser; 8 | 9 | beforeEach(() => { 10 | parser = yargs().global('version', false); 11 | parser.$0 = 'bp'; 12 | }); 13 | 14 | const buildParser = () => parser.command(generate); 15 | 16 | describe('help generate', () => { 17 | test('shows Usage', done => { 18 | buildParser().parse('help generate', (err, argv, output) => { 19 | expect(output).to.match(lineRegEx('Generate:')); 20 | expect(output).to.match(lineRegEx('bp generate ')); 21 | expect(output).to.match(lineRegEx('bp help generate ')); 22 | done(); 23 | }); 24 | }); 25 | test('shows Generate Options', done => { 26 | buildParser().parse('help generate', (err, argv, output) => { 27 | expect(output).to.match(lineRegEx('Generate Options:')); 28 | expect(output).to.match( 29 | lineRegEx( 30 | '--dry-run, -d', 31 | "List files but don't generate them", 32 | '[boolean]' 33 | ) 34 | ); 35 | expect(output).to.match( 36 | lineRegEx( 37 | '--verbose, -v', 38 | 'Verbose output, including file contents', 39 | '[boolean]' 40 | ) 41 | ); 42 | done(); 43 | }); 44 | }); 45 | }); 46 | 47 | describe('generate ', () => { 48 | let spy; 49 | 50 | beforeEach(() => { 51 | spy = jest.spyOn(console, 'error').mockImplementation(() => {}); 52 | }); 53 | afterEach(() => { 54 | spy.mockRestore(); 55 | }); 56 | 57 | test('demands and ', done => { 58 | buildParser().parse('generate', (err, argv, output) => { 59 | expect(output).to.contain('Missing arguments and '); 60 | done(); 61 | }); 62 | }); 63 | test('demands blueprint ', done => { 64 | buildParser().parse('generate blueprint', (err, argv, output) => { 65 | expect(output).to.contain('Missing argument blueprint '); 66 | done(); 67 | }); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/cli/cmds/generate/build-blueprint-command.test.js: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs/yargs'; 2 | import buildBlueprintCommand from 'cli/cmds/generate/build-blueprint-command'; 3 | import { lineRegEx } from '../../../helpers/regex-utils'; 4 | 5 | describe('(CLI) buildBlueprintCommand', () => { 6 | let blueprint; 7 | let subCommand; 8 | let run; 9 | 10 | beforeEach(() => { 11 | blueprint = { 12 | name: 'test_blueprint', 13 | description: function() { 14 | return 'test description'; 15 | }, 16 | command: {}, 17 | settings: {} 18 | }; 19 | run = jest.fn(); 20 | subCommand = { run }; 21 | }); 22 | 23 | test('returns a yargs command object', () => { 24 | const command = buildBlueprintCommand(blueprint, subCommand); 25 | expect(Object.keys(command).sort()).toEqual( 26 | ['command', 'aliases', 'describe', 'builder', 'handler'].sort() 27 | ); 28 | }); 29 | 30 | describe('.command', () => { 31 | test('built from blueprint name', () => { 32 | const command = buildBlueprintCommand(blueprint, subCommand); 33 | expect(command.command).toEqual('test_blueprint '); 34 | }); 35 | test('adds [options] when blueprint.command.options provided', () => { 36 | blueprint.command = {}; 37 | blueprint.command.options = { foo: {} }; 38 | const command = buildBlueprintCommand(blueprint, subCommand); 39 | expect(command.command).toEqual('test_blueprint [options]'); 40 | }); 41 | }); 42 | 43 | describe('.describe', () => { 44 | test('uses Blueprint#description()', () => { 45 | const command = buildBlueprintCommand(blueprint, subCommand); 46 | expect(command.describe).toEqual(blueprint.description()); 47 | }); 48 | }); 49 | 50 | describe('.aliases', () => { 51 | test('defaults to empty array', () => { 52 | const command = buildBlueprintCommand(blueprint, subCommand); 53 | expect(command.aliases).toEqual([]); 54 | }); 55 | test('adds single alias from blueprint', () => { 56 | blueprint.command.aliases = 'lone'; 57 | const command = buildBlueprintCommand(blueprint, subCommand); 58 | expect(command.aliases).toEqual(['lone']); 59 | }); 60 | test('adds alias array from blueprint', () => { 61 | blueprint.command.aliases = ['one', 'two']; 62 | const command = buildBlueprintCommand(blueprint, subCommand); 63 | expect(command.aliases).toEqual(['one', 'two']); 64 | }); 65 | test('adds single alias from blueprint settings', () => { 66 | blueprint.settings.aliases = 'lone'; 67 | const command = buildBlueprintCommand(blueprint, subCommand); 68 | expect(command.aliases).toEqual(['lone']); 69 | }); 70 | test('adds alias array from blueprint settings', () => { 71 | blueprint.settings.aliases = ['one', 'two']; 72 | const command = buildBlueprintCommand(blueprint, subCommand); 73 | expect(command.aliases).toEqual(['one', 'two']); 74 | }); 75 | test('overrides blueprint alias with settings', () => { 76 | blueprint.command.aliases = ['command']; 77 | blueprint.settings.aliases = 'settings'; 78 | const command = buildBlueprintCommand(blueprint, subCommand); 79 | expect(command.aliases).toEqual(['settings']); 80 | }); 81 | }); 82 | 83 | describe('.builder', () => { 84 | let parser; 85 | 86 | beforeEach(() => { 87 | parser = yargs(); 88 | parser.$0 = 'bp'; 89 | }); 90 | 91 | const buildParser = () => 92 | buildBlueprintCommand(blueprint, subCommand).builder(parser); 93 | 94 | test('returns builder function', () => { 95 | const command = buildBlueprintCommand(blueprint, subCommand); 96 | expect(command.builder).toBeInstanceOf(Function); 97 | }); 98 | 99 | describe('Usage', () => { 100 | test('built from command', done => { 101 | buildParser().parse('help', (err, argv, output) => { 102 | expect(output).to.include('Blueprint:'); 103 | expect(output).to.include('bp generate test_blueprint '); 104 | expect(output).to.not.include('[options]'); 105 | done(); 106 | }); 107 | }); 108 | test('includes [options]', done => { 109 | blueprint.command.options = {}; 110 | buildParser().parse('help', (err, argv, output) => { 111 | expect(output).to.include('Blueprint:'); 112 | expect(output).to.include( 113 | 'bp generate test_blueprint [options]' 114 | ); 115 | done(); 116 | }); 117 | }); 118 | test('includes aliases', done => { 119 | blueprint.command.options = {}; 120 | blueprint.command.aliases = ['foo', 'bar']; 121 | buildParser().parse('help', (err, argv, output) => { 122 | expect(output).to.include('Blueprint:'); 123 | expect(output).to.include( 124 | 'bp generate test_blueprint [options]' 125 | ); 126 | expect(output).to.include('bp generate foo [options]'); 127 | expect(output).to.include('bp generate bar [options]'); 128 | done(); 129 | }); 130 | }); 131 | }); 132 | 133 | describe('Options', () => { 134 | test('built from command', done => { 135 | blueprint.command.options = { 136 | foo: { description: 'Enable foo', type: 'boolean' }, 137 | bar: { alias: 'b', description: 'Specify bar', type: 'string' } 138 | }; 139 | buildParser().parse('help', (err, argv, output) => { 140 | expect(output).to.include('Options:'); 141 | expect(output).to.match( 142 | lineRegEx('--foo', 'Enable foo', '[boolean]') 143 | ); 144 | expect(output).to.match( 145 | lineRegEx('--bar, -b', 'Specify bar', '[string]') 146 | ); 147 | done(); 148 | }); 149 | }); 150 | test('defaulted from settings', done => { 151 | blueprint.command.options = { 152 | fizz: { description: 'Enable fizz', type: 'boolean' }, 153 | buzz: { alias: 'b', description: 'Specify buzz', type: 'string' } 154 | }; 155 | blueprint.settings = { 156 | fizz: true 157 | }; 158 | buildParser().parse('help', (err, argv, output) => { 159 | expect(output).to.include('Options:'); 160 | expect(output).to.match( 161 | lineRegEx('--fizz', 'Enable fizz', '[boolean]', '[default: true]') 162 | ); 163 | expect(output).to.match( 164 | lineRegEx('--buzz, -b', 'Specify buzz', '[string]') 165 | ); 166 | done(); 167 | }); 168 | }); 169 | }); 170 | 171 | describe('#check', () => { 172 | let check; 173 | 174 | test('called during parsing', done => { 175 | blueprint.command = { 176 | check: (check = jest.fn(() => true)) 177 | }; 178 | buildParser().parse('', () => { 179 | expect(check).toHaveBeenCalled(); 180 | done(); 181 | }); 182 | }); 183 | test('halts parsing if check fails', done => { 184 | blueprint.command = { 185 | check: (check = jest.fn(() => { 186 | throw new Error('check failed'); 187 | })) 188 | }; 189 | buildParser().parse('', (err, argv, output) => { 190 | expect(check).toHaveBeenCalled(); 191 | expect(err.message).toEqual('check failed'); 192 | expect(output).to.contain('check failed'); 193 | done(); 194 | }); 195 | }); 196 | }); 197 | 198 | describe('Examples', () => { 199 | test('displays single example', done => { 200 | blueprint.command = { 201 | examples: 'this is how to do it' 202 | }; 203 | buildParser().parse('help', (err, argv, output) => { 204 | expect(output).to.contain('Examples:'); 205 | expect(output).to.contain('this is how to do it'); 206 | done(); 207 | }); 208 | }); 209 | test('displays array of examples', done => { 210 | blueprint.command = { 211 | examples: ['this is how to do it', 'or this'] 212 | }; 213 | buildParser().parse('help', (err, argv, output) => { 214 | expect(output).to.contain('Examples:'); 215 | expect(output).to.contain('this is how to do it'); 216 | expect(output).to.contain('or this'); 217 | done(); 218 | }); 219 | }); 220 | }); 221 | 222 | describe('Epilogue', () => { 223 | test('displays epilogue', done => { 224 | blueprint.command = { 225 | epilogue: 'See the manual for more details' 226 | }; 227 | buildParser().parse('help', (err, argv, output) => { 228 | expect(output).to.contain('See the manual for more details'); 229 | done(); 230 | }); 231 | }); 232 | test('displays epilog alias', done => { 233 | blueprint.command = { 234 | epilog: 'Alias is working' 235 | }; 236 | buildParser().parse('help', (err, argv, output) => { 237 | expect(output).to.contain('Alias is working'); 238 | done(); 239 | }); 240 | }); 241 | }); 242 | }); 243 | 244 | describe('.handler', () => { 245 | test('returns handler function', () => { 246 | const command = buildBlueprintCommand(blueprint, subCommand); 247 | expect(command.handler).toBeInstanceOf(Function); 248 | }); 249 | 250 | const callHandler = argv => 251 | buildBlueprintCommand(blueprint, subCommand).handler(argv); 252 | 253 | describe('#sanitize', () => { 254 | let sanitize; 255 | 256 | beforeEach(() => { 257 | sanitize = jest.fn(argv => argv); 258 | blueprint.command.sanitize = sanitize; 259 | }); 260 | 261 | test('called by handler if provided', () => { 262 | callHandler({}); 263 | expect(sanitize).toHaveBeenCalled(); 264 | expect(sanitize.mock.calls.length).toEqual(1); 265 | }); 266 | test('passed argv', () => { 267 | const argv = { foo: 'bar' }; 268 | callHandler(argv); 269 | expect(sanitize.mock.calls[0].length).toEqual(1); 270 | expect(sanitize.mock.calls[0][0]).toEqual(argv); 271 | }); 272 | test('passed argv merged into settings', () => { 273 | blueprint.settings = { foo: 'bar', fizz: 'buzz' }; 274 | const argv = { foo: 'baz' }; 275 | const merged = { foo: 'baz', fizz: 'buzz' }; 276 | callHandler(argv); 277 | expect(sanitize.mock.calls[0].length).toEqual(1); 278 | expect(sanitize.mock.calls[0][0]).toEqual(merged); 279 | }); 280 | }); 281 | 282 | describe('calls subCommand.run', () => { 283 | test('with 2 parameters', () => { 284 | callHandler({}); 285 | expect(run).toHaveBeenCalled(); 286 | expect(run.mock.calls.length).toEqual(1); 287 | expect(run.mock.calls[0].length).toEqual(2); 288 | }); 289 | test('1st parameter is blueprint.name', () => { 290 | callHandler({}); 291 | expect(run.mock.calls[0][0]).toEqual(blueprint.name); 292 | }); 293 | test('2nd parameter is object in cliArgs format', () => { 294 | callHandler({}); 295 | const parameter = run.mock.calls[0][1]; 296 | expect(Object.keys(parameter).sort()).toEqual( 297 | ['entity', 'dryRun', 'debug'].sort() 298 | ); 299 | const entity = parameter.entity; 300 | expect(Object.keys(entity).sort()).toEqual( 301 | ['name', 'options', 'rawArgs'].sort() 302 | ); 303 | }); 304 | test('cliArgs.entity.name passed ', () => { 305 | callHandler({ name: 'test_entity_name' }); 306 | const entity = run.mock.calls[0][1].entity; 307 | expect(entity.name).toEqual('test_entity_name'); 308 | }); 309 | test('cliArgs.entity.options passed argv', () => { 310 | const argv = { name: 'test_entity_name', foo: 'bar' }; 311 | callHandler(argv); 312 | const entity = run.mock.calls[0][1].entity; 313 | expect(entity.options).toEqual(argv); 314 | }); 315 | test('cliArgs.entity.options passed argv merged into settings', () => { 316 | blueprint.settings = { foo: 'bar', fizz: 'buzz' }; 317 | const argv = { name: 'test_entity_name', foo: 'baz' }; 318 | const merged = { name: 'test_entity_name', foo: 'baz', fizz: 'buzz' }; 319 | callHandler(argv); 320 | const entity = run.mock.calls[0][1].entity; 321 | expect(entity.options).toEqual(merged); 322 | }); 323 | test('cliArgs.entity.rawArgs passed argv', () => { 324 | const argv = { name: 'test_entity_name', foo: 'bar' }; 325 | callHandler(argv); 326 | const entity = run.mock.calls[0][1].entity; 327 | expect(entity.rawArgs).toEqual(argv); 328 | }); 329 | test('cliArgs.entity.rawArgs does not include settings', () => { 330 | blueprint.settings = { foo: 'bar', fizz: 'buzz' }; 331 | const argv = { name: 'test_entity_name', foo: 'baz' }; 332 | callHandler(argv); 333 | const entity = run.mock.calls[0][1].entity; 334 | expect(entity.rawArgs).toEqual(argv); 335 | }); 336 | test('cliArgs.dryRun true when --dry-run', () => { 337 | callHandler({ dryRun: true }); 338 | const cliArgs = run.mock.calls[0][1]; 339 | expect(cliArgs.dryRun).toBe(true); 340 | }); 341 | test('cliArgs.dryRun false when not --dry-run', () => { 342 | callHandler({ dryRun: undefined }); 343 | const cliArgs = run.mock.calls[0][1]; 344 | expect(cliArgs.dryRun).toBe(false); 345 | }); 346 | test('cliArgs.debug true when --verbose', () => { 347 | callHandler({ verbose: true }); 348 | const cliArgs = run.mock.calls[0][1]; 349 | expect(cliArgs.debug).toBe(true); 350 | }); 351 | test('cliArgs.debug false when not --verbose', () => { 352 | callHandler({ verbose: false }); 353 | const cliArgs = run.mock.calls[0][1]; 354 | expect(cliArgs.debug).toBe(false); 355 | }); 356 | }); 357 | }); 358 | }); 359 | -------------------------------------------------------------------------------- /test/cli/cmds/generate/build-blueprint-commands.test.js: -------------------------------------------------------------------------------- 1 | import buildBlueprintCommands from 'cli/cmds/generate/build-blueprint-commands'; 2 | import buildBlueprintCommand from 'cli/cmds/generate/build-blueprint-command'; 3 | import getEnvironment from 'cli/environment'; 4 | 5 | jest.mock('cli/environment'); 6 | jest.mock('cli/cmds/generate/build-blueprint-command'); 7 | 8 | const deepOption = { isDeep: true }; 9 | const mockEnvironment = { 10 | settings: { 11 | blueprints: { 12 | generators() { 13 | return [{ name: 'specific' }, { name: 'nooption' }]; 14 | } 15 | }, 16 | settings: { 17 | bp: { 18 | common: { 19 | overridden: false, 20 | keep: true 21 | }, 22 | specific: { 23 | overridden: true, 24 | uncommon: deepOption 25 | } 26 | } 27 | } 28 | } 29 | }; 30 | 31 | getEnvironment.mockImplementation(() => mockEnvironment); 32 | buildBlueprintCommand.mockImplementation(blueprint => blueprint); 33 | 34 | describe('(CLI) #buildBlueprintCommands', () => { 35 | it('returns array of objects', () => { 36 | const blueprintCommands = buildBlueprintCommands(); 37 | expect(blueprintCommands).toBeInstanceOf(Array); 38 | expect(blueprintCommands[0]).toBeInstanceOf(Object); 39 | }); 40 | it('builds command for each of environment.settings.blueprints.generators()', () => { 41 | const blueprintCommands = buildBlueprintCommands(); 42 | expect(blueprintCommands.length).toBe(2); 43 | expect(blueprintCommands.map(cmd => cmd.name)).toEqual([ 44 | 'specific', 45 | 'nooption' 46 | ]); 47 | }); 48 | it('assigns bp.common to blueprint.settings', () => { 49 | const blueprintCommands = buildBlueprintCommands(); 50 | const nooption = blueprintCommands[1]; 51 | expect(nooption.settings).toEqual( 52 | mockEnvironment.settings.settings.bp.common 53 | ); 54 | }); 55 | it('clones bp.common before assigning to blueprint.settings', () => { 56 | const blueprintCommands = buildBlueprintCommands(); 57 | const nooption = blueprintCommands[1]; 58 | expect(nooption.settings).not.toBe( 59 | mockEnvironment.settings.settings.bp.common 60 | ); 61 | }); 62 | it('merges bp.specific into bp.common and assigns to blueprint.settings', () => { 63 | const blueprintCommands = buildBlueprintCommands(); 64 | const specific = blueprintCommands[0]; 65 | expect(specific.settings).toEqual({ 66 | overridden: true, 67 | keep: true, 68 | uncommon: deepOption 69 | }); 70 | }); 71 | it('clones bp.specific before merging and assigning to blueprint.settings', () => { 72 | const blueprintCommands = buildBlueprintCommands(); 73 | const specific = blueprintCommands[0]; 74 | expect(specific.settings.uncommon).not.toBe(deepOption); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/cli/cmds/generate/handlers.test.js: -------------------------------------------------------------------------------- 1 | import Yargs from 'yargs/yargs'; 2 | import handlers, { helpers } from 'cli/cmds/generate/handlers'; 3 | import buildBlueprintCommands from 'cli/cmds/generate/build-blueprint-commands'; 4 | import getYargs, { logYargs } from 'cli/yargs'; 5 | import { lineRegEx } from '../../../helpers/regex-utils'; 6 | 7 | jest.mock('cli/cmds/generate/build-blueprint-commands'); 8 | jest.mock('cli/yargs'); 9 | 10 | const blueprintCommand = { 11 | command: 'blueprint [options]', 12 | aliases: ['bp'], 13 | describe: 'Generate a blueprint with specified hooks', 14 | builder: yargs => 15 | yargs.options({ 16 | description: { description: 'Override default description' } 17 | }), 18 | handler: jest.fn() 19 | }; 20 | 21 | const duckCommand = { 22 | command: 'duck [options]', 23 | aliases: [], 24 | describe: 'Generate a duck with types and action handlers', 25 | builder: yargs => 26 | yargs.options({ 27 | types: { description: 'Specify ACTION_TYPE constants', type: 'array' } 28 | }), 29 | handler: jest.fn() 30 | }; 31 | 32 | const blueprintCommands = [blueprintCommand, duckCommand]; 33 | 34 | buildBlueprintCommands.mockImplementation(() => blueprintCommands); 35 | 36 | describe('(CLI) Generate Handlers', () => { 37 | describe('#handlers.handleRun', () => { 38 | let parse = jest.fn(); 39 | 40 | beforeEach(() => { 41 | jest.spyOn(helpers, 'getBlueprintRunner').mockImplementation(() => ({ 42 | parse 43 | })); 44 | parse.mockReset(); 45 | }); 46 | afterEach(() => { 47 | helpers.getBlueprintRunner.mockRestore(); 48 | }); 49 | 50 | test('gets blueprint runner', () => { 51 | const yargs = {}; 52 | const argv = { blueprint: 'testing' }; 53 | handlers.handleRun(argv, yargs); 54 | expect(helpers.getBlueprintRunner).toHaveBeenCalled(); 55 | expect(helpers.getBlueprintRunner.mock.calls[0][0]).toBe(yargs); 56 | expect(helpers.getBlueprintRunner.mock.calls[0][1]).toBe('testing'); 57 | }); 58 | test('parses rawArgs with blueprint runner', () => { 59 | const yargs = {}; 60 | const argv = { blueprint: 'testing' }; 61 | handlers.handleRun(argv, yargs); 62 | expect(parse).toHaveBeenCalled(); 63 | expect(parse.mock.calls[0][0]).toEqual(process.argv.slice(3)); 64 | }); 65 | test('ignores unknown blueprint', () => { 66 | const yargs = {}; 67 | const argv = { blueprint: 'testing' }; 68 | const rawArgs = { callMe: false }; 69 | helpers.getBlueprintRunner.mockImplementation(() => undefined); 70 | handlers.handleRun(argv, yargs, rawArgs); 71 | expect(parse).not.toHaveBeenCalled(); 72 | }); 73 | }); 74 | 75 | describe('#handlers.handleHelp', () => { 76 | let showHelp = jest.fn(); 77 | 78 | beforeEach(() => { 79 | jest.spyOn(helpers, 'getBlueprintHelper').mockImplementation(() => ({ 80 | showHelp 81 | })); 82 | jest.spyOn(helpers, 'getBlueprintListHelper').mockImplementation(() => ({ 83 | showHelp 84 | })); 85 | showHelp.mockReset(); 86 | }); 87 | afterEach(() => { 88 | helpers.getBlueprintHelper.mockRestore(); 89 | helpers.getBlueprintListHelper.mockRestore(); 90 | }); 91 | 92 | test('gets blueprint list helper', () => { 93 | const yargs = {}; 94 | const argv = { _: ['generate'] }; 95 | handlers.handleHelp(argv, yargs); 96 | expect(helpers.getBlueprintHelper).not.toHaveBeenCalled(); 97 | expect(helpers.getBlueprintListHelper).toHaveBeenCalled(); 98 | expect(helpers.getBlueprintListHelper.mock.calls[0][0]).toBe(yargs); 99 | expect(showHelp).toHaveBeenCalled(); 100 | }); 101 | test('gets blueprint helper', () => { 102 | const yargs = {}; 103 | const argv = { _: ['generate', 'blueprint_name'] }; 104 | handlers.handleHelp(argv, yargs); 105 | expect(helpers.getBlueprintListHelper).not.toHaveBeenCalled(); 106 | expect(helpers.getBlueprintHelper).toHaveBeenCalled(); 107 | expect(helpers.getBlueprintHelper.mock.calls[0][0]).toBe(yargs); 108 | expect(helpers.getBlueprintHelper.mock.calls[0][1]).toBe( 109 | 'blueprint_name' 110 | ); 111 | expect(showHelp).toHaveBeenCalled(); 112 | }); 113 | test('ignores unknown blueprint', () => { 114 | const yargs = {}; 115 | const argv = { _: ['generate', 'unknown_blueprint_name'] }; 116 | helpers.getBlueprintHelper.mockImplementation(() => undefined); 117 | handlers.handleHelp(argv, yargs); 118 | expect(helpers.getBlueprintListHelper).not.toHaveBeenCalled(); 119 | expect(helpers.getBlueprintHelper).toHaveBeenCalled(); 120 | expect(showHelp).not.toHaveBeenCalled(); 121 | }); 122 | }); 123 | 124 | describe('#helpers.getBlueprintRunner', () => { 125 | beforeEach(() => { 126 | jest 127 | .spyOn(helpers, 'logMissingBlueprint') 128 | .mockImplementation(() => undefined); 129 | jest.spyOn(console, 'error').mockImplementation(() => undefined); 130 | }); 131 | afterEach(() => { 132 | helpers.logMissingBlueprint.mockRestore(); 133 | console.error.mockRestore(); 134 | }); 135 | 136 | test('returns runnable blueprint', done => { 137 | const yargs = Yargs(); 138 | const runner = helpers.getBlueprintRunner(yargs, 'blueprint'); 139 | expect(runner).toBe(yargs); 140 | runner.parse('blueprint testable', (err, argv, output) => { 141 | expect(blueprintCommand.handler).toHaveBeenCalled(); 142 | done(); 143 | }); 144 | }); 145 | 146 | test('logs unknown blueprint', () => { 147 | const yargs = Yargs(); 148 | const helper = helpers.getBlueprintRunner(yargs, 'unrunnable'); 149 | expect(helpers.logMissingBlueprint).toHaveBeenCalled(); 150 | expect(helpers.logMissingBlueprint.mock.calls[0][0]).toBe(yargs); 151 | expect(helpers.logMissingBlueprint.mock.calls[0][1]).toBe('unrunnable'); 152 | }); 153 | }); 154 | 155 | describe('#helpers.getBlueprintHelper', () => { 156 | beforeEach(() => { 157 | jest 158 | .spyOn(helpers, 'logMissingBlueprint') 159 | .mockImplementation(() => undefined); 160 | jest.spyOn(console, 'error').mockImplementation(() => undefined); 161 | }); 162 | afterEach(() => { 163 | helpers.logMissingBlueprint.mockRestore(); 164 | console.error.mockRestore(); 165 | }); 166 | 167 | test('builds help for blueprint', () => { 168 | const yargs = Yargs(); 169 | const helper = helpers.getBlueprintHelper(yargs, 'duck'); 170 | helper.showHelp(); 171 | expect(console.error).toHaveBeenCalled(); 172 | const help = console.error.mock.calls[0][0]; 173 | expect(help).toMatch(lineRegEx('Blueprint Options:')); 174 | expect(help).toMatch( 175 | lineRegEx('--types', 'Specify ACTION_TYPE constants', '[array]') 176 | ); 177 | }); 178 | 179 | test('logs unknown blueprint', () => { 180 | const yargs = Yargs(); 181 | const helper = helpers.getBlueprintHelper(yargs, 'existential'); 182 | expect(helpers.logMissingBlueprint).toHaveBeenCalled(); 183 | expect(helpers.logMissingBlueprint.mock.calls[0][0]).toBe(yargs); 184 | expect(helpers.logMissingBlueprint.mock.calls[0][1]).toBe('existential'); 185 | }); 186 | }); 187 | 188 | describe('#helpers.getBlueprintListHelper', () => { 189 | beforeEach(() => { 190 | jest.spyOn(console, 'error').mockImplementation(() => undefined); 191 | }); 192 | afterEach(() => { 193 | console.error.mockRestore(); 194 | }); 195 | 196 | test('builds help for all blueprint commands', () => { 197 | const yargs = Yargs(); 198 | const helper = helpers.getBlueprintListHelper(yargs); 199 | helper.showHelp(); 200 | expect(console.error).toHaveBeenCalled(); 201 | const help = console.error.mock.calls[0][0]; 202 | expect(help).toMatch(lineRegEx('Blueprints:')); 203 | expect(help).toMatch( 204 | lineRegEx( 205 | 'blueprint [options]', 206 | 'Generate a blueprint with specified hooks', 207 | '[aliases: bp]' 208 | ) 209 | ); 210 | expect(help).toMatch( 211 | lineRegEx( 212 | 'duck [options]', 213 | 'Generate a duck with types and action handlers' 214 | ) 215 | ); 216 | }); 217 | }); 218 | 219 | describe('#helpers.getBlueprintCommands', () => { 220 | test('returns array of blueprint commands', () => { 221 | const actualCommands = helpers.getBlueprintCommands(); 222 | expect(buildBlueprintCommands).toHaveBeenCalled(); 223 | expect(actualCommands).toBe(blueprintCommands); 224 | }); 225 | }); 226 | 227 | describe('#helpers.getBlueprintCommand', () => { 228 | test('returns existing command', () => { 229 | const actualCommand = helpers.getBlueprintCommand('duck'); 230 | expect(buildBlueprintCommands).toHaveBeenCalled(); 231 | expect(actualCommand).toBe(duckCommand); 232 | }); 233 | test('returns existing command by alias', () => { 234 | const actualCommand = helpers.getBlueprintCommand('bp'); 235 | expect(buildBlueprintCommands).toHaveBeenCalled(); 236 | expect(actualCommand).toBe(blueprintCommand); 237 | }); 238 | test('returns undefined for unknown blueprint', () => { 239 | const actualCommand = helpers.getBlueprintCommand('no_idea'); 240 | expect(buildBlueprintCommands).toHaveBeenCalled(); 241 | expect(actualCommand).toBe(undefined); 242 | }); 243 | }); 244 | 245 | describe('#helpers.logMissingBlueprint', () => { 246 | test('logYargs unknown blueprint', () => { 247 | const yargs = {}; 248 | const blueprintName = 'i_am_missing'; 249 | helpers.logMissingBlueprint(yargs, blueprintName); 250 | expect(logYargs).toHaveBeenCalled(); 251 | expect(logYargs.mock.calls[0][0]).toBe(yargs); 252 | expect(logYargs.mock.calls[0][1]).toEqual( 253 | "Unknown blueprint 'i_am_missing'" 254 | ); 255 | }); 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /test/cli/cmds/init.test.js: -------------------------------------------------------------------------------- 1 | import getParser from 'cli/parser'; 2 | import { lineRegEx } from '../../helpers/regex-utils'; 3 | import Init from 'sub-commands/init'; 4 | 5 | jest.mock('sub-commands/init'); 6 | 7 | describe('(CLI) Init', () => { 8 | let parser; 9 | 10 | beforeEach(() => { 11 | parser = getParser(); 12 | parser.$0 = 'bp'; 13 | }); 14 | 15 | describe('--help', () => { 16 | test('shows Usage', done => { 17 | parser.parse('help init', (err, argv, output) => { 18 | expect(err).to.be.undefined; 19 | expect(output).to.include('Usage:'); 20 | expect(output).to.match(lineRegEx('bp init')); 21 | done(); 22 | }); 23 | }); 24 | 25 | test("doesn't include --version", done => { 26 | parser.parse('help init', (err, argv, output) => { 27 | expect(output).to.not.include('--version, -V'); 28 | done(); 29 | }); 30 | }); 31 | }); 32 | 33 | describe('handler', () => { 34 | test('runs subCommand without arguments', done => { 35 | parser.parse('init', (err, argv, output) => { 36 | expect(Init.mock.instances.length).toEqual(1); 37 | expect(Init.mock.instances[0].run.mock.calls.length).toEqual(1); 38 | expect(Init.mock.instances[0].run.mock.calls[0]).toEqual([]); 39 | expect(output).toEqual(''); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/cli/cmds/new.test.js: -------------------------------------------------------------------------------- 1 | import getParser from 'cli/parser'; 2 | import { lineRegEx } from '../../helpers/regex-utils'; 3 | import New from 'sub-commands/new'; 4 | 5 | jest.mock('sub-commands/new'); 6 | 7 | describe('(CLI) New', () => { 8 | let parser; 9 | 10 | beforeEach(() => { 11 | parser = getParser(); 12 | parser.$0 = 'bp'; 13 | }); 14 | 15 | describe('--help', () => { 16 | test('shows Usage', done => { 17 | parser.parse('help new', (err, argv, output) => { 18 | expect(err).to.be.undefined; 19 | expect(output).to.include('Usage:'); 20 | expect(output).to.match(lineRegEx('bp new ')); 21 | expect(output).to.match( 22 | lineRegEx('Create a new project from react-redux-starter-kit') 23 | ); 24 | done(); 25 | }); 26 | }); 27 | 28 | describe('shows Options', () => { 29 | test('--use-ssh', done => { 30 | parser.parse('new --help', (err, argv, output) => { 31 | expect(output).to.match( 32 | lineRegEx( 33 | '--use-ssh, -S', 34 | 'Fetch starter kit over ssh', 35 | '[boolean]' 36 | ) 37 | ); 38 | done(); 39 | }); 40 | }); 41 | test('--use-boilerplate', done => { 42 | parser.parse('new --h', (err, argv, output) => { 43 | expect(output).to.match( 44 | lineRegEx( 45 | '--use-boilerplate, -B', 46 | 'Create from redux-cli-boilerplate', 47 | '[boolean]' 48 | ) 49 | ); 50 | done(); 51 | }); 52 | }); 53 | test('--use-uikit', done => { 54 | parser.parse('help new', (err, argv, output) => { 55 | expect(output).to.match( 56 | lineRegEx( 57 | '--use-uikit, -U', 58 | 'Create from redux-cli-ui-kit-boilerplate', 59 | '[boolean]' 60 | ) 61 | ); 62 | done(); 63 | }); 64 | }); 65 | 66 | test("doesn't include --version", done => { 67 | parser.parse('help new', (err, argv, output) => { 68 | expect(output).to.not.include('--version, -V'); 69 | done(); 70 | }); 71 | }); 72 | }); 73 | }); 74 | 75 | describe('checks options', () => { 76 | test('requires ', done => { 77 | parser.parse('new', err => { 78 | expect(err).to.be.instanceof(Object); 79 | expect(err.message).toEqual( 80 | 'Not enough non-option arguments: got 0, need at least 1' 81 | ); 82 | done(); 83 | }); 84 | }); 85 | 86 | test('refuses unknown argument', done => { 87 | parser.parse('new test_project --gibberish', err => { 88 | expect(err).to.be.instanceof(Object); 89 | expect(err.message).toEqual('Unknown argument: gibberish'); 90 | done(); 91 | }); 92 | }); 93 | 94 | test('-B and -U are mutually exclusive', done => { 95 | parser.parse('new test_project -BU', err => { 96 | expect(err).to.be.instanceof(Object); 97 | expect(err.message).toEqual( 98 | 'Only specify one of --use-boilerplate or --use-uikit' 99 | ); 100 | done(); 101 | }); 102 | }); 103 | }); 104 | 105 | describe('handler', () => { 106 | let runMock; 107 | 108 | beforeEach(() => { 109 | expect(New.mock.instances.length).toEqual(1); 110 | runMock = New.mock.instances[0].run; 111 | runMock.mockReset(); 112 | }); 113 | 114 | test('passes dirName to subCommand', done => { 115 | parser.parse('new proj', (err, argv, output) => { 116 | expect(runMock.mock.calls.length).toEqual(1); 117 | expect(runMock.mock.calls[0]).toEqual([ 118 | { 119 | dirName: 'proj', 120 | useSsh: false, 121 | useBoilerplate: false, 122 | useUIKit: false 123 | } 124 | ]); 125 | expect(output).toEqual(''); 126 | done(); 127 | }); 128 | }); 129 | 130 | test('passes useSsh to subCommand', done => { 131 | parser.parse('new proj --use-ssh', (err, argv, output) => { 132 | expect(runMock.mock.calls.length).toEqual(1); 133 | expect(runMock.mock.calls[0]).toEqual([ 134 | { 135 | dirName: 'proj', 136 | useSsh: true, 137 | useBoilerplate: false, 138 | useUIKit: false 139 | } 140 | ]); 141 | expect(output).toEqual(''); 142 | done(); 143 | }); 144 | }); 145 | 146 | test('passes useBoilerplate to subCommand', done => { 147 | parser.parse('new proj --use-boilerplate', (err, argv, output) => { 148 | expect(runMock.mock.calls.length).toEqual(1); 149 | expect(runMock.mock.calls[0]).toEqual([ 150 | { 151 | dirName: 'proj', 152 | useSsh: false, 153 | useBoilerplate: true, 154 | useUIKit: false 155 | } 156 | ]); 157 | expect(output).toEqual(''); 158 | done(); 159 | }); 160 | }); 161 | 162 | test('passes useUIKit to subCommand', done => { 163 | parser.parse('new proj --use-uikit', (err, argv, output) => { 164 | expect(runMock.mock.calls.length).toEqual(1); 165 | expect(runMock.mock.calls[0]).toEqual([ 166 | { 167 | dirName: 'proj', 168 | useSsh: false, 169 | useBoilerplate: false, 170 | useUIKit: true 171 | } 172 | ]); 173 | expect(output).toEqual(''); 174 | done(); 175 | }); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/cli/environment.test.js: -------------------------------------------------------------------------------- 1 | import getEnvironment from 'cli/environment'; 2 | import UI from 'models/ui'; 3 | import ProjectSettings from 'models/project-settings'; 4 | 5 | jest.mock('models/ui'); 6 | jest.mock('models/project-settings'); 7 | 8 | describe('(CLI) Environment', () => { 9 | describe('#getEnvironment', () => { 10 | it('returns { ui, settings }', () => { 11 | const env = getEnvironment(); 12 | expect(Object.keys(env).sort()).toEqual(['settings', 'ui']); 13 | }); 14 | it('returns { ui: UI, settings: ProjectSettings }', () => { 15 | const env = getEnvironment(); 16 | expect(env.ui).toBeInstanceOf(UI); 17 | expect(env.settings).toBeInstanceOf(ProjectSettings); 18 | }); 19 | it('returns a singleton', () => { 20 | const env1 = getEnvironment(); 21 | const env2 = getEnvironment(); 22 | expect(env2).toBe(env1); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/cli/handler.test.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import { resetYargs } from 'cli/yargs'; 3 | import getHandler, { 4 | Handler, 5 | resolveCommandAlias, 6 | getCommands 7 | } from 'cli/handler'; 8 | 9 | jest.mock('cli/yargs'); 10 | 11 | const yargsCommands = [ 12 | ['blueprint [options]', 'some description', false, ['bp']], 13 | ['duck ', 'some description', false, []] 14 | ]; 15 | 16 | const parser = { 17 | getUsageInstance: () => ({ 18 | getCommands: () => yargsCommands 19 | }) 20 | }; 21 | 22 | describe('(CLI) Handler', () => { 23 | describe('#getHandler', () => { 24 | test('returns a Handler', () => { 25 | const handler = getHandler(); 26 | expect(handler).toBeInstanceOf(Handler); 27 | }); 28 | test('returns a singleton', () => { 29 | const handler1 = getHandler(); 30 | const handler2 = getHandler(); 31 | expect(handler2).toBe(handler1); 32 | }); 33 | }); 34 | 35 | describe('#getCommands', () => { 36 | test('parses yargs array into description objects', () => { 37 | const commands = getCommands(parser); 38 | expect(commands).toBeInstanceOf(Array); 39 | expect(commands.length).toBe(2); 40 | expect(commands[0]).toBeInstanceOf(Object); 41 | }); 42 | test('description object keys', () => { 43 | const command = getCommands(parser)[0]; 44 | expect(Object.keys(command).sort()).toEqual( 45 | ['command', 'name', 'arguments', 'aliases'].sort() 46 | ); 47 | }); 48 | test('.command', () => { 49 | const commands = getCommands(parser).map(cmd => cmd.command); 50 | expect(commands).toEqual(['blueprint [options]', 'duck ']); 51 | }); 52 | test('.name', () => { 53 | const commands = getCommands(parser).map(cmd => cmd.name); 54 | expect(commands).toEqual(['blueprint', 'duck']); 55 | }); 56 | test('.arguments', () => { 57 | const commands = getCommands(parser).map(cmd => cmd.arguments); 58 | expect(commands).toEqual([['name'], ['name']]); 59 | }); 60 | test('.aliases', () => { 61 | const commands = getCommands(parser).map(cmd => cmd.aliases); 62 | expect(commands).toEqual([['bp'], []]); 63 | }); 64 | }); 65 | 66 | describe('#resolveCommandAlias', () => { 67 | test('resolves command by name', () => { 68 | const command = resolveCommandAlias('blueprint', parser); 69 | expect(command).toEqual('blueprint'); 70 | }); 71 | test('resolves command by alias', () => { 72 | const command = resolveCommandAlias('bp', parser); 73 | expect(command).toEqual('blueprint'); 74 | }); 75 | test('defaults to input command', () => { 76 | const command = resolveCommandAlias('gibberish', parser); 77 | expect(command).toEqual('gibberish'); 78 | }); 79 | }); 80 | 81 | describe('Handler', () => { 82 | test('manages helpEmitter', () => { 83 | const handler = new Handler(); 84 | const emitter = handler.helpEmitter; 85 | expect(emitter).toBeInstanceOf(EventEmitter); 86 | }); 87 | test('accepts helpEmitter', () => { 88 | const helpEmitter = new EventEmitter(); 89 | const handler = new Handler({ helpEmitter }); 90 | expect(handler.helpEmitter).toBe(helpEmitter); 91 | }); 92 | test('manages runEmitter', () => { 93 | const handler = new Handler(); 94 | const emitter = handler.runEmitter; 95 | expect(emitter).toBeInstanceOf(EventEmitter); 96 | }); 97 | test('accepts runEmitter', () => { 98 | const runEmitter = new EventEmitter(); 99 | const handler = new Handler({ runEmitter }); 100 | expect(handler.runEmitter).toBe(runEmitter); 101 | }); 102 | test('separate helpEmitter and runEmitter', () => { 103 | const handler = new Handler(); 104 | expect(handler.runEmitter).not.toBe(handler.helpEmitter); 105 | }); 106 | 107 | describe('#onHelp', () => { 108 | test('registers listener for key with helpEmitter', () => { 109 | const handler = new Handler(); 110 | const listener = jest.fn(); 111 | handler.onHelp('test', listener); 112 | expect(handler.helpEmitter.listeners('test').length).toBe(1); 113 | expect(handler.helpEmitter.listeners('test')[0]).toBe(listener); 114 | expect(handler.runEmitter.listeners('test').length).toBe(0); 115 | }); 116 | }); 117 | 118 | describe('#onRun', () => { 119 | test('registers listener for key with runEmitter', () => { 120 | const handler = new Handler(); 121 | const listener = jest.fn(); 122 | handler.onRun('test', listener); 123 | expect(handler.runEmitter.listeners('test').length).toBe(1); 124 | expect(handler.runEmitter.listeners('test')[0]).toBe(listener); 125 | expect(handler.helpEmitter.listeners('test').length).toBe(0); 126 | }); 127 | }); 128 | 129 | describe('#handle', () => { 130 | let handler; 131 | let helpEmit; 132 | let runEmit; 133 | let parse; 134 | 135 | beforeEach(() => { 136 | const helpEmitter = { 137 | emit: (helpEmit = jest.fn()) 138 | }; 139 | const runEmitter = { 140 | emit: (runEmit = jest.fn()) 141 | }; 142 | handler = new Handler({ helpEmitter, runEmitter }); 143 | 144 | parse = jest.fn(); 145 | resetYargs.mockImplementation(() => ({ parse })); 146 | }); 147 | 148 | test('does nothing without a command to handle', () => { 149 | const argv = { _: [] }; 150 | const handled = handler.handle(argv, parser); 151 | expect(handled).toBe(false); 152 | expect(helpEmit).not.toHaveBeenCalled(); 153 | expect(runEmit).not.toHaveBeenCalled(); 154 | }); 155 | test('emits a run command', () => { 156 | const argv = { _: ['something'] }; 157 | const handled = handler.handle(argv, parser); 158 | expect(handled).toBe(true); 159 | expect(helpEmit).not.toHaveBeenCalled(); 160 | expect(runEmit).toHaveBeenCalledWith('something', argv, parser); 161 | }); 162 | test('emits a help command', () => { 163 | const argv = { _: ['bp'], help: true }; 164 | const handled = handler.handle(argv, parser); 165 | expect(handled).toBe(true); 166 | expect(helpEmit).toHaveBeenCalledWith('blueprint', argv, parser); 167 | expect(runEmit).not.toHaveBeenCalled(); 168 | }); 169 | test('resets yargs before emitting help', () => { 170 | const argv = { _: ['bp'], help: true }; 171 | const handled = handler.handle(argv, parser); 172 | expect(resetYargs).toHaveBeenCalledWith(parser); 173 | expect(parse).toHaveBeenCalledWith(''); 174 | }); 175 | }); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /test/cli/parser.test.js: -------------------------------------------------------------------------------- 1 | import getParser from 'cli/parser'; 2 | import pkg from '../../package.json'; 3 | import { lineRegEx } from '../helpers/regex-utils'; 4 | 5 | let parser; 6 | 7 | function testErrorMessage(opts, message) { 8 | test('shows error message', done => { 9 | parser.parse(opts, (err, argv, output) => { 10 | expect(err).to.have.property('message'); 11 | expect(err.message).to.eql(message); 12 | expect(output).to.include(message); 13 | done(); 14 | }); 15 | }); 16 | } 17 | 18 | function testShowsHelp(opts, description = 'shows help') { 19 | test(description, done => { 20 | parser.parse(opts, (err, argv, output) => { 21 | expect(output).to.include('Usage:'); 22 | expect(output).to.include('Commands:'); 23 | expect(output).to.include('Options:'); 24 | done(); 25 | }); 26 | }); 27 | } 28 | 29 | describe('(CLI) Parser', () => { 30 | beforeEach(() => { 31 | parser = getParser(); 32 | parser.$0 = 'bp'; 33 | }); 34 | 35 | describe('no command', () => { 36 | testErrorMessage('', 'Provide a command to run'); 37 | testShowsHelp(''); 38 | }); 39 | 40 | describe('unknown command', () => { 41 | testErrorMessage('gibberish', 'Unknown argument: gibberish'); 42 | testShowsHelp('gibberish'); 43 | }); 44 | 45 | describe('nearly command', () => { 46 | xit('should suggest best command', () => { 47 | // yargs.recommendCommands() working in CLI but not in test setup 48 | // testErrorMessage('genrate', 'Did you mean generate?'); 49 | }); 50 | testShowsHelp('genrate'); 51 | }); 52 | 53 | describe('--version', () => { 54 | const testVersion = (opts, desc) => { 55 | test(desc, done => { 56 | parser.parse(opts, (err, argv, output) => { 57 | err = err || {}; 58 | expect(err.message).to.be.undefined; 59 | expect(err).toEqual({}); 60 | expect(output).to.eql(pkg.version); 61 | done(); 62 | }); 63 | }); 64 | }; 65 | 66 | testVersion('--version', 'returns package version'); 67 | testVersion('-V', 'returns with -V alias'); 68 | }); 69 | 70 | describe('--help', () => { 71 | testShowsHelp('--help'); 72 | testShowsHelp('-h', 'shows with -h alias'); 73 | testShowsHelp('help', 'shows with help argument'); 74 | 75 | describe('help content', () => { 76 | test('displays Usage', done => { 77 | parser.parse('', (err, argv, output) => { 78 | expect(output).to.include('Usage:'); 79 | expect(output).to.include('bp [arguments] [options]'); 80 | expect(output).to.include('bp help '); 81 | done(); 82 | }); 83 | }); 84 | 85 | test('displays Commands', done => { 86 | parser.parse('', (err, argv, output) => { 87 | expect(output).to.include('Commands:'); 88 | done(); 89 | }); 90 | }); 91 | 92 | test('displays generate command', done => { 93 | parser.parse('', (err, argv, output) => { 94 | expect(output).to.match( 95 | lineRegEx( 96 | 'generate ', 97 | 'Generate project file(s) from a blueprint', 98 | '[aliases: g, gen]' 99 | ) 100 | ); 101 | done(); 102 | }); 103 | }); 104 | 105 | test('displays init command', done => { 106 | parser.parse('', (err, argv, output) => { 107 | expect(output).to.match( 108 | lineRegEx('init', 'Initialize .blueprintrc for the current project') 109 | ); 110 | done(); 111 | }); 112 | }); 113 | 114 | test('hides new command (in v2.0)', done => { 115 | parser.parse('', (err, argv, output) => { 116 | expect(output).to.not.match( 117 | lineRegEx( 118 | 'new ', 119 | 'Create a new blueprint-enabled project' 120 | ) 121 | ); 122 | done(); 123 | }); 124 | }); 125 | 126 | test('displays Options', done => { 127 | parser.parse('', (err, argv, output) => { 128 | expect(output).to.include('Options:'); 129 | expect(output).to.match( 130 | lineRegEx('--help, -h', 'Show help', '[boolean]') 131 | ); 132 | expect(output).to.match( 133 | lineRegEx('--version, -V', 'Show version number', '[boolean]') 134 | ); 135 | done(); 136 | }); 137 | }); 138 | 139 | test('displays documentation epilogue', done => { 140 | parser.parse('', (err, argv, output) => { 141 | expect(output).to.match( 142 | lineRegEx( 143 | 'Documentation: https://github.com/SpencerCDixon/redux-cli' 144 | ) 145 | ); 146 | done(); 147 | }); 148 | }); 149 | }); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /test/fixtures/argv.blueprintrc: -------------------------------------------------------------------------------- 1 | { 2 | "location": "argv", 3 | "argvConfig": "ARGV", 4 | "blueprints": "argv" 5 | } 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/basic/files/expected-file.js: -------------------------------------------------------------------------------- 1 | // expected file to exist 2 | -------------------------------------------------------------------------------- /test/fixtures/basic/index.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/fixtures/blueprints/basic/files/expected-file.js: -------------------------------------------------------------------------------- 1 | // expected file to exist 2 | -------------------------------------------------------------------------------- /test/fixtures/blueprints/basic/index.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/fixtures/blueprints/duplicate/files/expected-file.js: -------------------------------------------------------------------------------- 1 | // expected file to exist 2 | -------------------------------------------------------------------------------- /test/fixtures/blueprints/duplicate/index.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/fixtures/env.blueprintrc: -------------------------------------------------------------------------------- 1 | { 2 | "envConfig": "Environment", 3 | "blueprints": [ 4 | "env" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/file-info-template.txt: -------------------------------------------------------------------------------- 1 | <%= name %> 2 | -------------------------------------------------------------------------------- /test/helpers/fs-helpers.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { fileExists } from 'util/fs'; 4 | import { flatten, includes } from 'lodash'; 5 | // import fse from 'fs-extra'; 6 | 7 | const rootPath = process.cwd(); 8 | 9 | export const expectFilesEqual = (pathToActual, pathToExpected) => { 10 | const actual = fs.readFileSync(pathToActual, 'utf8'); 11 | const expected = fs.readFileSync(pathToExpected, 'utf8'); 12 | 13 | expect(actual).to.eql(expected); 14 | }; 15 | 16 | export const expectFileToNotExist = pathToFile => { 17 | const exists = fileExists(pathToFile); 18 | expect(exists).to.be.false; 19 | }; 20 | 21 | // options can include arrays with contains/doesNotContain 22 | export const expectFile = (file, options) => { 23 | const filePath = path.join(rootPath, file); 24 | const exists = fileExists(filePath); 25 | 26 | expect(exists).to.equal(true, `expected ${file} to exist`); 27 | 28 | if (!options) { 29 | return; 30 | } 31 | 32 | const actual = fs.readFileSync(filePath, 'utf8'); 33 | 34 | if (options.contains) { 35 | flatten([options.contains]).forEach(expected => { 36 | let pass; 37 | 38 | if (expected.test) { 39 | pass = expected.test(actual); 40 | } else { 41 | pass = includes(actual, expected); 42 | } 43 | 44 | if (pass) { 45 | expect(true).to.equal(true, `expected ${file} to contain ${expected}`); 46 | } else { 47 | throw new EqualityError(`expected: ${file}`, actual, expected); 48 | } 49 | }); 50 | } 51 | }; 52 | 53 | function EqualityError(message, actual, expected) { 54 | this.message = message; 55 | this.actual = actual; 56 | this.expected = expected; 57 | this.showDiff = true; 58 | Error.captureStackTrace(this, module.exports); 59 | } 60 | 61 | EqualityError.prototype = Object.create(Error.prototype); 62 | EqualityError.prototype.name = 'EqualityError'; 63 | EqualityError.prototype.constructor = EqualityError; 64 | -------------------------------------------------------------------------------- /test/helpers/mock-settings.js: -------------------------------------------------------------------------------- 1 | class MockSettings { 2 | constructor(args) { 3 | const defaults = { 4 | sourceBase: './tmp/src', 5 | testBase: './tmp/test', 6 | fileCasing: 'default', 7 | fileExtension: 'js', 8 | wrapFilesInFolders: false 9 | }; 10 | this.settings = Object.assign({}, defaults, args); 11 | } 12 | 13 | getSetting(key) { 14 | return this.settings[key]; 15 | } 16 | } 17 | export default MockSettings; 18 | -------------------------------------------------------------------------------- /test/helpers/mock-ui.js: -------------------------------------------------------------------------------- 1 | import UI from 'models/ui'; 2 | import through from 'through'; 3 | 4 | export default class MockUI extends UI { 5 | constructor(writeLevel) { 6 | super({ 7 | inputStream: through(), 8 | outputStream: through(data => (this.output += data)), 9 | errorStream: through(data => (this.errors += data)) 10 | }); 11 | this.output = ''; 12 | this.errors = ''; 13 | this.writeLevel = writeLevel; 14 | } 15 | 16 | clear() { 17 | this.output = ''; 18 | this.errors = ''; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/helpers/regex-utils.js: -------------------------------------------------------------------------------- 1 | export const escapeRegEx = str => str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); 2 | 3 | // TODO: make this available if/when it's needed 4 | // export const escapedRegEx = str => new RegExp(escapeRegEx(str)); 5 | 6 | export const lineRegEx = (...pieces) => 7 | new RegExp('(^|\\n)\\s*' + pieces.map(escapeRegEx).join('\\s+') + '(\\n|$)'); 8 | -------------------------------------------------------------------------------- /test/models/blueprint-collection.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import BlueprintCollection, { 3 | expandPath, 4 | parseBlueprintSetting 5 | } from 'models/blueprint-collection'; 6 | import config from 'config'; 7 | import process from 'process'; 8 | 9 | const { basePath } = config; 10 | 11 | const paths = { 12 | [path.resolve(basePath, 'test')]: ['fixtures', '/ghi', 'doh'], 13 | [path.resolve(basePath, 'test/fixtures')]: [ 14 | '~doesNotExist7as84', 15 | './blueprints' 16 | ] 17 | }; 18 | 19 | describe('(Model) BlueprintCollection', () => { 20 | describe('#all()', () => { 21 | it('should return an array of all blueprints in searchPaths', () => { 22 | const blueprints = new BlueprintCollection(paths); 23 | const result = blueprints.all(); 24 | expect(result).to.be.an('Array'); 25 | expect(result).toHaveLength(3); 26 | expect(result[0].name).toEqual('basic'); 27 | expect(result[2].filesPath()).to.match(/fixtures\/blueprints\/duplicate/); 28 | }); 29 | }); 30 | 31 | describe('#generators()', () => { 32 | it('should return an array of all generator blueprints in searchPaths', () => { 33 | const blueprints = new BlueprintCollection(paths); 34 | const result = blueprints.generators(); 35 | expect(result).to.be.an('Array'); 36 | expect(result).toHaveLength(3); 37 | expect(result[0].name).toEqual('basic'); 38 | expect(result[2].filesPath()).to.match(/fixtures\/blueprints\/duplicate/); 39 | }); 40 | }); 41 | 42 | describe('#allPossiblePaths', () => { 43 | test('returns a an array of blueprint paths', () => { 44 | const blueprints = new BlueprintCollection(paths); 45 | const result = blueprints.allPossiblePaths(); 46 | 47 | expect(result[0]).toEqual(basePath + '/test/fixtures'); 48 | expect(result[1]).toEqual('/ghi'); 49 | expect(result[2]).toEqual(basePath + '/test/doh'); 50 | expect(result[3].slice(process.env.HOME.length)).toEqual( 51 | '/doesNotExist7as84' 52 | ); 53 | expect(result[4]).toEqual(basePath + '/test/fixtures/blueprints'); 54 | }); 55 | }); 56 | describe('setSearchPaths', () => { 57 | test('', () => { 58 | const blueprints = new BlueprintCollection(paths); 59 | const result = blueprints.searchPaths; 60 | 61 | expect(result[0]).toEqual(basePath + '/test/fixtures'); 62 | expect(result[1]).toEqual(basePath + '/test/fixtures/blueprints'); 63 | }); 64 | }); 65 | }); 66 | 67 | describe('#expandPath', () => { 68 | test('returns path if path starts with /', () => { 69 | const testPath = '/bruce'; 70 | const resultPath = expandPath(basePath, testPath); 71 | expect(resultPath).toEqual(testPath); 72 | }); 73 | 74 | test('returns path relative from home if path starts with ~', () => { 75 | const testPath = '~dick'; 76 | const resultPath = expandPath(basePath, testPath); 77 | const expectedPath = process.env.HOME + path.sep + 'dick'; 78 | expect(resultPath).toEqual(expectedPath); 79 | }); 80 | 81 | test('returns path relative from home if path starts with ~/', () => { 82 | const testPath = '~/barbara'; 83 | const resultPath = expandPath(basePath, testPath); 84 | const expectedPath = process.env.HOME + path.sep + 'barbara'; 85 | expect(resultPath).toEqual(expectedPath); 86 | }); 87 | 88 | test('returns path relative to basePath if does not start with "/" or "~"', () => { 89 | const testPath = 'alfred'; 90 | const resultPath = expandPath(basePath, testPath); 91 | const expectedPath = basePath + path.sep + 'alfred'; 92 | expect(resultPath).toEqual(expectedPath); 93 | }); 94 | }); 95 | 96 | describe('#lookupAll(name)', () => { 97 | test('returns empty array if blueprint for name', () => { 98 | const blueprints = new BlueprintCollection(paths); 99 | expect(blueprints.lookupAll('flyingGraysons')).toEqual([]); 100 | }); 101 | test('returns an array of blueprints matching name', () => { 102 | const blueprints = new BlueprintCollection(paths); 103 | const allBasic = blueprints.lookupAll('basic'); 104 | expect(allBasic).to.be.an('array'); 105 | expect(allBasic.length).toEqual(2); 106 | expect(allBasic[0].name).toEqual('basic'); 107 | expect(allBasic[1].name).toEqual('basic'); 108 | }); 109 | }); 110 | 111 | describe('#lookup(name)', () => { 112 | test('returns falsy if no blueprint for name', () => { 113 | const blueprints = new BlueprintCollection(paths); 114 | expect(blueprints.lookup('flyingGraysons')).to.be.falsy; 115 | }); 116 | test('returns a blueprint matching name', () => { 117 | const blueprints = new BlueprintCollection(paths); 118 | expect(blueprints.lookup('basic').name).toEqual('basic'); 119 | }); 120 | test('returns the first blueprint matching name', () => { 121 | const blueprints = new BlueprintCollection(paths); 122 | const basic = blueprints.lookup('basic'); 123 | expect(basic.name).toEqual('basic'); 124 | expect(blueprints.lookupAll('basic')[0]).toEqual(basic); 125 | }); 126 | }); 127 | 128 | describe('::parseBlueprintSetting', () => { 129 | const bpArr = ['./blueprints']; 130 | test('returns arr + bparr if is array', () => { 131 | const testSetting = ['jim']; 132 | const resultArr = parseBlueprintSetting(testSetting); 133 | const expectedArr = [...testSetting, ...bpArr]; 134 | expect(resultArr).toEqual(expectedArr); 135 | }); 136 | test('returns arr with name + bparr if is string', () => { 137 | const testSetting = 'leslie'; 138 | const resultArr = parseBlueprintSetting(testSetting); 139 | const expectedArr = [testSetting, ...bpArr]; 140 | expect(resultArr).toEqual(expectedArr); 141 | }); 142 | test('returns bpArr if is boolean and is true', () => { 143 | const testSetting = true; 144 | const resultArr = parseBlueprintSetting(testSetting); 145 | expect(resultArr).toEqual(bpArr); 146 | }); 147 | test('returns empty array if is boolean and is false', () => { 148 | const testSetting = false; 149 | const resultArr = parseBlueprintSetting(testSetting); 150 | expect(resultArr).toEqual([]); 151 | }); 152 | test('returns bpArr if is undefined', () => { 153 | const testSetting = null; 154 | const resultArr = parseBlueprintSetting(testSetting); 155 | expect(resultArr).toEqual(bpArr); 156 | }); 157 | test('returns bpArr if is number', () => { 158 | const testSetting = 42; 159 | const resultArr = parseBlueprintSetting(testSetting); 160 | expect(resultArr).toEqual(bpArr); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/models/blueprint.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import Blueprint from 'models/blueprint'; 3 | 4 | const fixtureBlueprints = path.resolve( 5 | __dirname, 6 | '..', 7 | 'fixtures', 8 | 'blueprints' 9 | ); 10 | const basicBlueprint = path.join(fixtureBlueprints, 'basic'); 11 | 12 | describe('(Model) Blueprint', () => { 13 | const blueprint = new Blueprint(basicBlueprint); 14 | 15 | describe('#description', () => { 16 | test('returns a description', () => { 17 | expect(blueprint.description()).to.match(/Generates a new basic/); 18 | }); 19 | }); 20 | 21 | describe('#filesPath', () => { 22 | test('returns a default of "files" ', () => { 23 | const expectedPath = path.join(basicBlueprint, 'files'); 24 | expect(blueprint.filesPath()).toEqual(expectedPath); 25 | }); 26 | }); 27 | 28 | describe('#files', () => { 29 | test('returns an array of files in blueprint', () => { 30 | const files = blueprint.files(); 31 | const expectedFiles = ['expected-file.js']; 32 | expect(files).toEqual(expectedFiles); 33 | }); 34 | 35 | test('defaults to empty array when no files', () => { 36 | const blueprint = new Blueprint('ridiculous/path/that/doesnt/exist'); 37 | expect(blueprint.files()).toEqual([]); 38 | }); 39 | }); 40 | 41 | describe('.load', () => { 42 | test('loads a blueprint from a path', () => { 43 | const blueprint = Blueprint.load(basicBlueprint); 44 | expect(blueprint.name).toEqual('basic'); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/models/file-info.test.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fse from 'fs-extra'; 3 | import fs from 'fs'; 4 | import FileInfo from 'models/file-info'; 5 | import MockUI from '../helpers/mock-ui'; 6 | import { expectFileToNotExist } from '../helpers/fs-helpers'; 7 | 8 | const originalPath = path.join( 9 | __dirname, 10 | '..', 11 | 'fixtures', 12 | 'file-info-template.txt' 13 | ); 14 | const mappedPath = path.join( 15 | __dirname, 16 | '..', 17 | '..', 18 | 'tmp', 19 | 'path-to-overwrite.txt' 20 | ); 21 | const ui = new MockUI('DEBUG'); 22 | 23 | describe('(Model) FileInfo', () => { 24 | beforeEach(() => { 25 | ui.clear(); 26 | }); 27 | 28 | describe('#isFile', () => { 29 | test('return true if original path is a file', () => { 30 | const info = new FileInfo({ 31 | templateVariables: {}, 32 | ui, 33 | originalPath, 34 | mappedPath 35 | }); 36 | expect(info.isFile()).toBe(true); 37 | }); 38 | 39 | test('returns false if original path is bogus', () => { 40 | const info = new FileInfo({ 41 | templateVariables: {}, 42 | ui, 43 | originalPath: path.join(__dirname, 'bogus', 'path', 'here'), 44 | mappedPath 45 | }); 46 | expect(info.isFile()).toBe(false); 47 | }); 48 | }); 49 | 50 | describe('#writeFile', () => { 51 | describe('when file already exists', () => { 52 | test('throws warning to ui and doesnt overwrite old file', () => { 53 | const existingPath = path.join( 54 | __dirname, 55 | '..', 56 | '..', 57 | 'tmp', 58 | 'path-to-overwrite-a.txt' 59 | ); 60 | fse.outputFileSync(existingPath, 'some contennt'); 61 | 62 | const info = new FileInfo({ 63 | templateVariables: {}, 64 | mappedPath: existingPath, 65 | ui, 66 | originalPath 67 | }); 68 | info.writeFile(); 69 | expect(ui.errors).toMatch(/Not writing file/); 70 | fse.remove(existingPath); 71 | }); 72 | }); 73 | 74 | describe('when no file exists', () => { 75 | test('writes the file', () => { 76 | const noFilePath = path.join( 77 | __dirname, 78 | '..', 79 | '..', 80 | 'tmp', 81 | 'path-to-overwrite-b.txt' 82 | ); 83 | const info = new FileInfo({ 84 | templateVariables: { name: 'rendered ejs string' }, 85 | mappedPath: noFilePath, 86 | ui, 87 | originalPath 88 | }); 89 | info.writeFile(); 90 | 91 | const expectedString = 'rendered ejs string\n'; 92 | const actualString = fs.readFileSync(noFilePath, 'utf8'); 93 | expect(expectedString).toEqual(actualString); 94 | expect(ui.output).toMatch(/create/); 95 | fse.remove(noFilePath); 96 | }); 97 | }); 98 | 99 | describe('when dry run option is enabled', () => { 100 | test('should not write the file', () => { 101 | const dryRunPath = path.join( 102 | __dirname, 103 | '..', 104 | '..', 105 | 'tmp', 106 | 'path-to-overwrite-c.txt' 107 | ); 108 | 109 | const info = new FileInfo({ 110 | templateVariables: { name: 'rendered ejs string' }, 111 | mappedPath: dryRunPath, 112 | ui, 113 | originalPath 114 | }); 115 | info.writeFile(true); 116 | 117 | expectFileToNotExist(dryRunPath); 118 | expect(ui.output).toMatch(/would create/); 119 | }); 120 | }); 121 | }); 122 | 123 | describe('#renderTempalte', () => { 124 | test('renders ejs template with the template variables', () => { 125 | const info = new FileInfo({ 126 | templateVariables: { name: 'rendered ejs string' }, 127 | ui, 128 | originalPath, 129 | mappedPath 130 | }); 131 | const expectedString = 'rendered ejs string\n'; 132 | expect(info.renderTemplate()).to.eq(expectedString); 133 | }); 134 | }); 135 | 136 | describe('::removeEjsExt', () => { 137 | test('it should not change any path that does not end in ejs', () => { 138 | const removeEjsExt = FileInfo.removeEjsExt; 139 | let path = '/test/path/file.js'; 140 | expect(removeEjsExt(path)).toEqual(path); 141 | path = '/test/path/file.foo.bar.ejs.html'; 142 | expect(removeEjsExt(path)).toEqual(path); 143 | }); 144 | test('it should remove the last and only the last ejs', () => { 145 | const removeEjsExt = FileInfo.removeEjsExt; 146 | let path = '/test/path/file.js'; 147 | expect(removeEjsExt(path + '.ejs')).toEqual(path); 148 | path = '/test/path/file.foo.bar.ejs.html'; 149 | expect(removeEjsExt(path + '.EJS')).toEqual(path); 150 | }); 151 | }); 152 | }); 153 | -------------------------------------------------------------------------------- /test/models/project-settings.test.js: -------------------------------------------------------------------------------- 1 | import ProjectSettings from 'models/project-settings'; 2 | import fs from 'fs'; 3 | import config from 'config'; 4 | 5 | const { basePath } = config; 6 | const settingsPath = basePath + '/.blueprintrc'; 7 | 8 | describe('ProjectSettings', () => { 9 | // this is the local path. intended to be root of a particular directory 10 | describe('#settingsPath', () => { 11 | it('returns current directory with .blueprintrc appended', () => { 12 | const settings = new ProjectSettings(); 13 | expect(settings.settingsPath()).to.eql(settingsPath); 14 | }); 15 | }); 16 | 17 | describe('#loadSettings', () => { 18 | it('loads settings from $CWD/.blueprintrc', () => { 19 | const settings = new ProjectSettings(); 20 | expect(settings.getSetting('location')).to.eql('project'); 21 | }); 22 | 23 | // inject a ENV variable and load settings. 24 | it('loads settings from .blueprintrc defined in process.env', () => { 25 | process.env['blueprint_config'] = 'test/fixtures/env.blueprintrc'; 26 | const settings = new ProjectSettings(); 27 | expect(settings.getSetting('envConfig')).to.eql('Environment'); 28 | delete process.env['blueprint_config']; 29 | // expect the file to be in the path of config files too 30 | }); 31 | 32 | // inject an ARGV and load settings. 33 | it('loads settings from .blueprintrc defined as ARGV', () => { 34 | const fakeArgv = { config: 'test/fixtures/argv.blueprintrc' }; 35 | const defaultSettings = { defaultOption: true }; 36 | const argvSettings = new ProjectSettings(defaultSettings, fakeArgv); 37 | expect(argvSettings.getSetting('argvConfig')).to.eql('ARGV'); 38 | }); 39 | 40 | it('collects __defaults__ in allConfigs', () => { 41 | const defaultSettings = { defaultOption: true }; 42 | const argvSettings = new ProjectSettings(defaultSettings); 43 | expect(argvSettings.allConfigs()['__default__']).to.eql(defaultSettings); 44 | }); 45 | 46 | it('collects all configurations into a object', () => { 47 | process.env['blueprint_config'] = 'test/fixtures/env.blueprintrc'; 48 | const fakeArgv = { config: 'test/fixtures/argv.blueprintrc' }; 49 | const defaultSettings = { defaultOption: true }; 50 | const allSettings = new ProjectSettings(defaultSettings, fakeArgv); 51 | const fileCount = allSettings.settings.configs.length; 52 | 53 | expect(allSettings.configChunks.length).to.eql(fileCount); 54 | expect(Object.keys(allSettings.allConfigs()).length).to.eql( 55 | fileCount + 1 56 | ); 57 | delete process.env['blueprint_config']; 58 | }); 59 | 60 | it('collects all blueprints into an array of arrays', () => { 61 | // How many do we have before 62 | const baseline = new ProjectSettings().blueprintChunks.length; 63 | 64 | process.env['blueprint_config'] = 'test/fixtures/env.blueprintrc'; 65 | const fakeArgv = { config: 'test/fixtures/argv.blueprintrc' }; 66 | const defaultSettings = { defaultOption: true }; 67 | const settings = new ProjectSettings(defaultSettings, fakeArgv); 68 | expect(settings.blueprintChunks.length).to.eql(baseline + 2); 69 | delete process.env['blueprint_config']; 70 | }); 71 | }); 72 | 73 | describe('#getSetting', () => { 74 | it('returns the value of that setting', () => { 75 | const mockedSettings = { 76 | testOne: 'works', 77 | testTwo: 'works as well!' 78 | }; 79 | const settings = new ProjectSettings(mockedSettings); 80 | 81 | expect(settings.getSetting('testOne')).to.eql('works'); 82 | expect(settings.getSetting('testTwo')).to.eql('works as well!'); 83 | }); 84 | }); 85 | 86 | // shouldn't need a file save to do this. plain getter 87 | describe('#getAllSettings', () => { 88 | it('returns json of all settings', () => { 89 | const mockedSettings = { 90 | testOne: 'works', 91 | testTwo: 'works as well!' 92 | }; 93 | const settings = new ProjectSettings(mockedSettings); 94 | const { testOne, testTwo } = settings.getAllSettings(); 95 | expect(testOne).to.eql('works'); 96 | expect(testTwo).to.eql('works as well!'); 97 | }); 98 | }); 99 | 100 | // deprecate for uselessness? 101 | describe('#setSetting', () => { 102 | it('sets new settings', () => { 103 | const mockedSettings = { testOne: 'works' }; 104 | const settings = new ProjectSettings(mockedSettings); 105 | expect(settings.getSetting('testOne')).to.eql('works'); 106 | 107 | settings.setSetting('testOne', 'new setting'); 108 | expect(settings.getSetting('testOne')).to.eql('new setting'); 109 | }); 110 | }); 111 | 112 | describe('#setAllSettings', () => { 113 | it('takes a javascript object and overrides current settings', () => { 114 | const preWrittenSettings = { testOne: 'some information' }; 115 | const settings = new ProjectSettings(preWrittenSettings); 116 | expect(settings.getSetting('testOne')).to.eql('some information'); 117 | const overrideAll = { 118 | testOne: 'new information', 119 | anotherKey: 'with some value' 120 | }; 121 | settings.setAllSettings(overrideAll); 122 | expect(settings.getAllSettings()).to.eql(overrideAll); 123 | }); 124 | }); 125 | 126 | describe('#saveDefault', () => { 127 | it('saves the current settings to the file', () => { 128 | const tmpPath = '/tmp/.blueprintrc'; 129 | const defaults = { testSaveDefault: 'new setting' }; 130 | const settings = new ProjectSettings(defaults); 131 | settings.saveDefaults(defaults, tmpPath); 132 | const newFile = fs.readFileSync(tmpPath, 'utf8'); 133 | expect(newFile).to.match(/testSaveDefault.+new setting/); 134 | }); 135 | }); 136 | 137 | describe('#configFiles', () => { 138 | it('returns an array of all config files read', () => { 139 | process.env['blueprint_config'] = 'test/fixtures/env.blueprintrc'; 140 | const fakeArgv = { config: 'test/fixtures/argv.blueprintrc' }; 141 | const defaultSettings = { defaultOption: true }; 142 | const settings = new ProjectSettings(defaultSettings, fakeArgv); 143 | const expectedFiles = [ 144 | settingsPath, 145 | basePath + '/test/fixtures/argv.blueprintrc', 146 | basePath + '/test/fixtures/env.blueprintrc' 147 | ]; 148 | expect(settings.configFiles()).to.include.members(expectedFiles); 149 | }); 150 | }); 151 | describe('#blueprints', () => { 152 | it('returns a BlueprintCollection', () => { 153 | process.env['blueprint_config'] = 'test/fixtures/env.blueprintrc'; 154 | const fakeArgv = { config: 'test/fixtures/argv.blueprintrc' }; 155 | const defaultSettings = { defaultOption: true }; 156 | const settings = new ProjectSettings(defaultSettings, fakeArgv); 157 | const blueprintPaths = settings.blueprints.searchPaths; 158 | const expectedFiles = [ 159 | basePath + '/blueprints', 160 | basePath + '/test/fixtures/blueprints' 161 | ]; 162 | expect(blueprintPaths).to.include.members(expectedFiles); 163 | }); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/models/sub-command.test.js: -------------------------------------------------------------------------------- 1 | import SubCommand from 'models/sub-command'; 2 | 3 | describe('(Model) SubCommand', () => { 4 | const command = new SubCommand(); 5 | 6 | describe('subclass override intereface', () => { 7 | test('throws if subclass doesnt have run()', () => { 8 | expect(() => command.run()).toThrowError(/must implement a run()/); 9 | }); 10 | 11 | test('throws if subclass doesnt have availbleOptions()', () => { 12 | expect(() => command.availableOptions()).toThrowError( 13 | /must implement an availableOptions()/ 14 | ); 15 | }); 16 | }); 17 | 18 | test('creates an environment which can be passed to tasks', () => { 19 | const options = { 20 | ui: 'cli interface', 21 | settings: 'project settings' 22 | }; 23 | const command = new SubCommand(options); 24 | expect(command.environment).toEqual(options); 25 | }); 26 | 27 | describe('cliLogo()', () => { 28 | test('returns a string', () => { 29 | const options = { 30 | ui: 'cli interface', 31 | settings: 'project settings' 32 | }; 33 | const command = new SubCommand(options); 34 | expect(command.cliLogo()).to.be.a('string'); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/models/task.test.js: -------------------------------------------------------------------------------- 1 | import Task from 'models/task'; 2 | 3 | describe('(Model) Task', () => { 4 | const environment = { 5 | ui: '', 6 | settings: '' 7 | }; 8 | const task = new Task(environment); 9 | 10 | describe('#run', () => { 11 | test('throws if no run() is present', () => { 12 | expect(() => task.run()).toThrowError(/Tasks must implement run()/); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/models/ui.test.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import MockUI from '../helpers/mock-ui'; 3 | import { EOL } from 'os'; 4 | 5 | describe('(Model) UI', () => { 6 | const ui = new MockUI('DEBUG'); 7 | 8 | beforeEach(() => { 9 | ui.clear(); 10 | }); 11 | 12 | describe('#write', () => { 13 | describe('when an error', () => { 14 | test('writes to errorStream if its an ERROR', () => { 15 | ui.write('some text', 'ERROR'); 16 | expect(ui.errors).toEqual('some text'); 17 | expect(ui.output).toEqual(''); 18 | }); 19 | }); 20 | }); 21 | 22 | describe('#writeLine', () => { 23 | test('appends EOL to text being written', () => { 24 | ui.writeLine('this is a line'); 25 | const expectedString = 'this is a line' + EOL; 26 | expect(ui.output).toEqual(expectedString); 27 | }); 28 | }); 29 | 30 | describe('helper writes', () => { 31 | const string = 'file was made here'; 32 | 33 | describe('#writeCreate', () => { 34 | test('prepends a green "create"', () => { 35 | ui.writeCreate(string); 36 | const expected = chalk.green(' create: ') + chalk.white(string); 37 | expect(ui.output).to.eq(expected + EOL); 38 | }); 39 | }); 40 | describe('#writeInfo', () => { 41 | test('prepends a blue "info"', () => { 42 | ui.writeInfo(string); 43 | const expected = chalk.blue(' info: ') + chalk.white(string); 44 | expect(ui.output).to.eq(expected + EOL); 45 | }); 46 | }); 47 | describe('#writeDebug', () => { 48 | test('prepends a gray "debug"', () => { 49 | ui.writeDebug(string); 50 | const expected = chalk.gray(' debug: ') + chalk.white(string); 51 | expect(ui.output).to.eq(expected + EOL); 52 | }); 53 | }); 54 | describe('#writeError', () => { 55 | test('prepends a red "error"', () => { 56 | ui.writeError(string); 57 | const expected = chalk.red(' error: ') + chalk.white(string); 58 | expect(ui.errors).to.eq(expected + EOL); 59 | }); 60 | }); 61 | describe('#writeWarning', () => { 62 | test('prepends a yellow "warning"', () => { 63 | ui.writeWarning(string); 64 | const expected = chalk.yellow(' warning: ') + chalk.white(string); 65 | expect(ui.output).to.eq(expected + EOL); 66 | }); 67 | }); 68 | describe('#writeCreate', () => { 69 | test('prepends a yellow "warning"', () => { 70 | ui.writeCreate(string); 71 | const expected = chalk.green(' create: ') + chalk.white(string); 72 | expect(ui.output).to.eq(expected + EOL); 73 | }); 74 | }); 75 | describe('#writeWouldCreate', () => { 76 | test('prepends a green "warning"', () => { 77 | ui.writeWouldCreate(string); 78 | const expected = chalk.green(' would create: ') + chalk.white(string); 79 | expect(ui.output).to.eq(expected + EOL); 80 | }); 81 | }); 82 | }); 83 | 84 | describe('#writeLevelVisible', () => { 85 | describe('when set to ERROR', () => { 86 | test('can only see ERROR messages', () => { 87 | const ui = new MockUI('ERROR'); 88 | expect(ui.writeLevelVisible('ERROR')).toBe(true); 89 | expect(ui.writeLevelVisible('WARNING')).toBe(false); 90 | expect(ui.writeLevelVisible('INFO')).toBe(false); 91 | expect(ui.writeLevelVisible('DEBUG')).toBe(false); 92 | }); 93 | }); 94 | 95 | describe('when set to WARNING', () => { 96 | test('can only see ERROR & WARNING messages', () => { 97 | const ui = new MockUI('WARNING'); 98 | expect(ui.writeLevelVisible('ERROR')).toBe(true); 99 | expect(ui.writeLevelVisible('WARNING')).toBe(true); 100 | expect(ui.writeLevelVisible('INFO')).toBe(false); 101 | expect(ui.writeLevelVisible('DEBUG')).toBe(false); 102 | }); 103 | }); 104 | 105 | describe('when set to INFO', () => { 106 | test('can only see ERROR/WARNING/INFO messages', () => { 107 | const ui = new MockUI('INFO'); 108 | expect(ui.writeLevelVisible('ERROR')).toBe(true); 109 | expect(ui.writeLevelVisible('WARNING')).toBe(true); 110 | expect(ui.writeLevelVisible('INFO')).toBe(true); 111 | expect(ui.writeLevelVisible('DEBUG')).toBe(false); 112 | }); 113 | }); 114 | 115 | describe('when set to DEBUG', () => { 116 | test('has complete visibility', () => { 117 | const ui = new MockUI('DEBUG'); 118 | expect(ui.writeLevelVisible('DEBUG')).toBe(true); 119 | expect(ui.writeLevelVisible('INFO')).toBe(true); 120 | expect(ui.writeLevelVisible('WARNING')).toBe(true); 121 | expect(ui.writeLevelVisible('ERROR')).toBe(true); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('#setWriteLevel', () => { 127 | test('can reset writeLevel', () => { 128 | expect(ui.writeLevel).toEqual('DEBUG'); 129 | ui.setWriteLevel('ERROR'); 130 | expect(ui.writeLevel).toEqual('ERROR'); 131 | }); 132 | 133 | test('throws when a bad writeLevel is passed in', () => { 134 | expect(() => ui.setWriteLevel('bogus')).toThrowError( 135 | /Valid values are: DEBUG, INFO, WARNING, ERROR/ 136 | ); 137 | }); 138 | }); 139 | 140 | describe('async progress bar', () => { 141 | describe('#startProgress', () => { 142 | test('starts streaming', () => { 143 | expect(ui.streaming).toBe(false); 144 | ui.startProgress('some async call'); 145 | expect(ui.streaming).toBe(true); 146 | }); 147 | 148 | test('calls stream every 100 ms', () => { 149 | const clock = sinon.useFakeTimers(); 150 | const spy = sinon.spy(); 151 | ui.startProgress('some async call', spy); 152 | clock.tick(101); 153 | expect(spy.calledOnce).toBe(true); 154 | clock.restore(); 155 | }); 156 | }); 157 | 158 | describe('#stopProgress', () => { 159 | test('clears interval when it exists', () => { 160 | ui.startProgress('some async call'); 161 | expect(ui.streaming).toBe(true); 162 | ui.stopProgress(); 163 | expect(ui.streaming).toBe(false); 164 | }); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /test/prompts/setup.test.js: -------------------------------------------------------------------------------- 1 | import { setupPrompt } from 'prompts/setup'; 2 | 3 | describe('(Prompts) #setupPrompt', () => { 4 | let start, prompt; 5 | 6 | beforeEach(() => { 7 | start = sinon.spy(); 8 | prompt = { start: start }; 9 | setupPrompt('testing', prompt); 10 | }); 11 | 12 | test('gives the prompt a custom message', () => { 13 | expect(prompt.message).toMatch(/testing/); 14 | }); 15 | 16 | test('sets the delimiter to be an empty string', () => { 17 | expect(prompt.delimiter).toEqual(''); 18 | }); 19 | 20 | test('starts the prompt', () => { 21 | expect(start.calledOnce).toBe(true); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import sinon from 'sinon'; 3 | // 4 | // global.expect = expect; 5 | global.sinon = sinon; 6 | 7 | // Make sure chai and jasmine ".not" play nice together 8 | const originalNot = Object.getOwnPropertyDescriptor( 9 | chai.Assertion.prototype, 10 | 'not' 11 | ).get; 12 | Object.defineProperty(chai.Assertion.prototype, 'not', { 13 | get() { 14 | Object.assign(this, this.assignedNot); 15 | return originalNot.apply(this); 16 | }, 17 | set(newNot) { 18 | this.assignedNot = newNot; 19 | return newNot; 20 | } 21 | }); 22 | 23 | // Combine both jest and chai matchers on expect 24 | const originalExpect = global.expect; 25 | 26 | global.expect = actual => { 27 | const originalMatchers = originalExpect(actual); 28 | const chaiMatchers = chai.expect(actual); 29 | const combinedMatchers = Object.assign(chaiMatchers, originalMatchers); 30 | return combinedMatchers; 31 | }; 32 | -------------------------------------------------------------------------------- /test/util/fs.test.js: -------------------------------------------------------------------------------- 1 | import { fileExists, readFile } from 'util/fs'; 2 | import fse from 'fs-extra'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | describe('(Util) fs', () => { 7 | describe('#fileExists', () => { 8 | test('returns true when file exists', () => { 9 | const finalPath = path.join(process.cwd(), 'tmp/example.js'); 10 | fse.outputFileSync(finalPath, 'path'); 11 | 12 | expect(fileExists(finalPath)).toBe(true); 13 | fse.removeSync(finalPath); 14 | }); 15 | 16 | test('returns false when file doesnt exist', () => { 17 | expect(fileExists('tmp/some/random/path')).toBe(false); 18 | }); 19 | 20 | test('throws error when not file present error', () => { 21 | const error = { 22 | message: 'random error', 23 | code: 'random code' 24 | }; 25 | sinon.stub(fs, 'accessSync').throws(error); 26 | 27 | try { 28 | fileExists('tmp/example.js'); 29 | expect('should not get here').toEqual(true); 30 | } catch (e) { 31 | expect(e.code).toEqual('random code'); 32 | } finally { 33 | fs.accessSync.restore(); 34 | } 35 | }); 36 | }); 37 | 38 | describe('#readFile', () => { 39 | const finalPath = path.join(process.cwd(), 'tmp/example.js'); 40 | 41 | beforeEach(() => { 42 | fse.outputFileSync(finalPath, 'file to be read'); 43 | }); 44 | 45 | afterEach(() => { 46 | fse.removeSync(finalPath); 47 | }); 48 | 49 | test('lets you pass in relative path', () => { 50 | const file = readFile('tmp/example.js'); 51 | expect(file).toEqual('file to be read'); 52 | }); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/util/mixin.test.js: -------------------------------------------------------------------------------- 1 | import mixin from 'util/mixin'; 2 | 3 | class Parent { 4 | testFunction() { 5 | return 'inside parent'; 6 | } 7 | } 8 | 9 | describe('(Util) mixin', () => { 10 | test('creates a new constructor with functions mixed in', () => { 11 | const Child = { 12 | testFunction: () => { 13 | return 'inside mixin'; 14 | } 15 | }; 16 | 17 | const Constructor = mixin(Parent, Child); 18 | const instance = new Constructor(); 19 | expect(instance.testFunction()).toEqual('inside mixin'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/util/text-helper.test.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import * as th from 'util/text-helper'; 3 | 4 | describe('(Util) text-helpers', () => { 5 | describe('#success', () => { 6 | test('applies green color to text', () => { 7 | const string = 'Successfully created something important'; 8 | const successString = th.success(string); 9 | 10 | expect(successString).to.eq(chalk.green(string)); 11 | expect(successString).to.not.eq(chalk.red(string)); 12 | }); 13 | }); 14 | 15 | describe('#danger', () => { 16 | test('applies red color to text', () => { 17 | const string = 'ERROR: something bad happened'; 18 | const errorString = th.danger(string); 19 | 20 | expect(errorString).to.eq(chalk.red(string)); 21 | expect(errorString).to.not.eq(chalk.green(string)); 22 | }); 23 | }); 24 | 25 | describe('#warning', () => { 26 | test('applies yellow color to text', () => { 27 | const string = 'WARNING: are you sure you want this?'; 28 | const warningString = th.warning(string); 29 | 30 | expect(warningString).to.eq(chalk.yellow(string)); 31 | expect(warningString).to.not.eq(chalk.red(string)); 32 | }); 33 | }); 34 | 35 | describe('#normalizeComponentName', () => { 36 | test('turns snake case into capitalized', () => { 37 | const string = 'my_component_name'; 38 | const expected = 'MyComponentName'; 39 | 40 | expect(th.normalizeComponentName(string)).toEqual(expected); 41 | }); 42 | 43 | test('turns dashes into capitalized', () => { 44 | const string = 'my-component-name'; 45 | const expected = 'MyComponentName'; 46 | 47 | expect(th.normalizeComponentName(string)).toEqual(expected); 48 | }); 49 | 50 | test('turns camelcase into capitalized', () => { 51 | const string = 'myComponent-name'; 52 | const expected = 'MyComponentName'; 53 | 54 | expect(th.normalizeComponentName(string)).toEqual(expected); 55 | }); 56 | }); 57 | 58 | describe('#normalizeDuckName', () => { 59 | test('camelizes snake case', () => { 60 | const string = 'my_duck'; 61 | const expected = 'myDuck'; 62 | 63 | expect(th.normalizeDuckName(string)).toEqual(expected); 64 | }); 65 | 66 | test('camelizes pascal case', () => { 67 | const string = 'MyDuck'; 68 | const expected = 'myDuck'; 69 | 70 | expect(th.normalizeDuckName(string)).toEqual(expected); 71 | }); 72 | 73 | test('camelizes dashes', () => { 74 | const string = 'my-duck'; 75 | const expected = 'myDuck'; 76 | 77 | expect(th.normalizeDuckName(string)).toEqual(expected); 78 | }); 79 | }); 80 | 81 | describe('#normalizeCasing', () => { 82 | const string = 'string-to-test'; 83 | 84 | test('converts to snake when settings are set to "snake"', () => { 85 | const expected = 'string_to_test'; 86 | expect(th.normalizeCasing(string, 'snake')).toEqual(expected); 87 | }); 88 | 89 | test('converts to PascalCase when settings are set to "pascal"', () => { 90 | const expected = 'StringToTest'; 91 | expect(th.normalizeCasing(string, 'pascal')).toEqual(expected); 92 | }); 93 | 94 | test('converts to camelCase when settings are set to "camel"', () => { 95 | const expected = 'stringToTest'; 96 | expect(th.normalizeCasing(string, 'camel')).toEqual(expected); 97 | }); 98 | 99 | test('converts to dashes-case when settings are set to "dashes"', () => { 100 | const expected = 'string-to-test'; 101 | expect(th.normalizeCasing(string, 'dashes')).toEqual(expected); 102 | }); 103 | 104 | test('leaves string alone when set to "default"', () => { 105 | expect(th.normalizeCasing(string, 'default')).toEqual(string); 106 | }); 107 | 108 | test('throws error if not one of the allowed conversions', () => { 109 | expect(() => th.normalizeCasing(string)).toThrowError( 110 | /Casing must be one of: default, snake, pascal, camel/ 111 | ); 112 | }); 113 | }); 114 | }); 115 | --------------------------------------------------------------------------------