diff --git a/.blueprintrc b/.blueprintrc new file mode 100644 index 0000000..1b4a8b6 --- /dev/null +++ b/.blueprintrc @@ -0,0 +1,9 @@ +{ + "sourceBase": "src", + "testBase": "test", + "smartPath": "container", + "dumbPath": "component", + "fileCasing": "default", + "location": "project", + "blueprints": true +} diff --git a/.gitignore b/.gitignore index 1814834..35bc4a5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ npm-debug.log coverage/ tmp/ .reduxrc +yarn-error.log diff --git a/blueprints/blueprint/files/blueprints/__name__/index.js b/blueprints/blueprint/files/blueprints/__name__/index.ejs similarity index 100% rename from blueprints/blueprint/files/blueprints/__name__/index.js rename to blueprints/blueprint/files/blueprints/__name__/index.ejs diff --git a/design.md b/design.md index 920cdbf..6590f90 100644 --- a/design.md +++ b/design.md @@ -15,6 +15,8 @@ SubCommands. ## Design Decisions 2.0 +Rename to blueprint-cli + Take the foundation laid by 1.0 and extend the capabilities. Blueprints are most useful when able to be shared, copied and customized. @@ -25,8 +27,8 @@ Provide a way to copy blueprints into the default directory in order to increase the ease of customizing your own version of a default or shared blueprint. -Enhance the .reduxrc experience. Add the ability to have home directory -and ENV var defined locations. Allow merging of multiple .reduxrc files. +Enhance the .blueprintrc experience. Add the ability to have home directory +and ENV var defined locations. Allow merging of multiple .blueprintrc files. Allow defining blueprint directories in the file. Enhance the Generator experience. Look to Ruby on Rails for inspiration. diff --git a/package.json b/package.json index 97865bb..2171b84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux-cli", - "version": "2.0.0-0.1.0", + "version": "2.0.0-0.2.1", "description": "An opinionated CLI to make working on redux apps much faster.", "main": "bin/bp.js", "engine": { @@ -11,11 +11,11 @@ "posttest": "npm run lint", "test:nocov": "jest --coverage=false --forceExit", "test:cov": "jest --forceExit", - "test:watch": "jest --forceExit --watch --notify", + "test:watch": "jest --forceExit --coverageReporters html --watch --notify", "start": "npm run build:watch", "build": "babel src -d lib", "build:watch": "babel src --watch -d lib", - "lint": "eslint ./src ./test ./blueprints", + "lint": "eslint ./src ./test ./blueprints/*/index.js", "clean": "rimraf lib", "publish:patch": "npm run clean && npm run build && npm version patch && npm publish", "publish:minor": "npm run clean && npm run build && npm version minor && npm publish" @@ -24,6 +24,7 @@ "redux", "react", "cli", + "blueprint", "generator", "react.js", "kit", @@ -39,7 +40,6 @@ }, "dependencies": { "chalk": "^1.1.1", - "commander": "^2.9.0", "denodeify": "^1.2.1", "ejs": "^2.4.1", "elegant-spinner": "^1.0.1", @@ -50,6 +50,7 @@ "lodash": "^4.5.1", "log-update": "^1.0.2", "minimist": "^1.2.0", + "prettyjson": "^1.2.1", "prompt": "^1.0.0", "rc": "^1.2.1", "shelljs": "^0.6.0", diff --git a/readme.md b/readme.md index 309c134..c2f5a32 100644 --- a/readme.md +++ b/readme.md @@ -5,28 +5,28 @@ [![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) ``` -______ _ _____ _ _____ +______ _ _____ _ _____ | ___ \ | | / __ \| | |_ _| -| |_/ /___ __| |_ ___ ________| / \/| | | | -| // _ \/ _` | | | \ \/ /______| | | | | | -| |\ \ __/ (_| | |_| |> < | \__/\| |_____| |_ -\_| \_\___|\__,_|\__,_/_/\_\ \____/\_____/\___/ +| |_/ /___ __| |_ ___ ________| / \/| | | | +| // _ \/ _` | | | \ \/ /______| | | | | | +| |\ \ __/ (_| | |_| |> < | \__/\| |_____| |_ +\_| \_\___|\__,_|\__,_/_/\_\ \____/\_____/\___/ ``` ## Quick Start ```javascript -npm i redux-cli -g // install redux-cli globally -redux new // create a new redux project -redux new -S // OR use ssh to pull project down (if ssh setup with github) -redux init // OR configure a current project to use the CLI +npm i redux-cli -g // install blueprint-cli globally +blueprint new // create a new blueprint project +blueprint new -S // OR use ssh to pull project down (if ssh setup with github) +blueprint init // OR configure a current project to use the CLI // Start generating components/tests and save time \(• ◡ •)/ //(g is alias for generate) -redux g dumb SimpleButton +blueprint g dumb SimpleButton ``` -![Redux CLI Usage Gif](redux-cli.gif) +![Blueprint CLI Usage Gif](redux-cli.gif) ## Table Of Contents @@ -41,17 +41,17 @@ redux g dumb SimpleButton 8. [Changelog](#changelog) ### Getting Started -Running `redux new ` will pull down the amazing [Redux Starter Kit](https://github.com/davezuko/react-redux-starter-kit) and -initialize a new git repo. Running `new` will automatically set up a `.reduxrc` +Running `blueprint new ` will pull down the amazing [Redux Starter Kit](https://github.com/davezuko/react-redux-starter-kit) and +initialize a new git repo. Running `new` will automatically set up a `.blueprintrc` to work with this specific starter kit. If you want to integrate the CLI in an existing project or store your components in different paths please see [config existing project](#config-existing-project) ### Config Existing Project There is an `init` subcommand for you to specify all paths to where components -live in your project. The `init` command just creates a `.reduxrc` in your -project root. If you want to you can just create the `.reduxrc` manually. +live in your project. The `init` command just creates a `.blueprintrc` in your +project root. If you want to you can just create the `.blueprintrc` manually. -Final `.reduxrc` might look like this: +Final `.blueprintrc` might look like this: ```javascript { @@ -61,18 +61,18 @@ Final `.reduxrc` might look like this: "dumbPath":"components", "fileCasing": "default" } -``` +``` -**Note on configuration**: +**Note on configuration**: This project tries to walk on a fine line between convention and configuration. Since the majority of React applications will separate their smart/dumb components if you pass in those paths you'll get those generators for free. However, some of the other generators might not write files to the exact paths that you use for your project. It's easy to override the CLI generators with -your own so that the generators will write files to the correct location. +your own so that the generators will write files to the correct location. [See: creating custom blueprints](#creating-blueprints). -Alternatively, if you use this CLI as a result of `redux new ` the +Alternatively, if you use this CLI as a result of `blueprint new ` the starter kit will come pre-configured with a bunch of blueprints (generators) that work out of the gate. Currently, I'm working on a PR for the `react-redux-starter-kit` with a bunch of blueprints. More starter kits and @@ -87,53 +87,53 @@ blueprints to come! |**dumbPath**|✓|where you keep your dumb (pure) components (relative of sourceBase)| |**fileCasing**|✓|how do you want generated files to be named (pascal/camel/snake/dashes/default)| -#### .reduxrc -It's possible to put `.reduxrc` files in other locations to better share -configs. It looks for files in the following locations and deep merges the -files it finds together. The defaultSettings will be overwritten by any -following options while the `--config=path/to/file` option will override -everything. +#### .blueprintrc +It's possible to put `.blueprintrc` files in other locations to better share +configs. It looks for files in the following locations and deep merges the +files it finds together. The defaultSettings will be overwritten by any +following options while the `--config=path/to/file` option will override +everything. -See the whole list and more ENV tricks -at [rc](https://github.com/dominictarr/rc) +See the whole list and more ENV tricks +at [rc](https://github.com/dominictarr/rc) 1. defaultSettings -2. /etc/redux/config -3. /etc/reduxrc -4. ~/.config/redux/config -5. ~/.config/redux -6. ~/.redux/config -7. ~/.reduxrc -9. $REDUX_CONFIG +2. /etc/blueprint/config +3. /etc/blueprintrc +4. ~/.config/blueprint/config +5. ~/.config/blueprint +6. ~/.blueprint/config +7. ~/.blueprintrc +9. $BLUEPRINT_CONFIG 10. --config=path/to/file -**Note** - All files found at these locations will have their objects deep +**Note** - All files found at these locations will have their objects deep merged together. Later file override earlier ones ### Commands |Command|Description|Alias| |---|---|---| -|`redux new `|creates a new redux project|| -|`redux init`|configure an existing redux app to use the CLI|| -|`redux generate `|generates files and tests for you automatically|`redux g`| -|`redux help g`|show all generators you have available|| +|`blueprint new `|creates a new blueprint project|| +|`blueprint init`|configure an existing blueprint app to use the CLI|| +|`blueprint generate `|generates files and tests for you automatically|`blueprint g`| +|`blueprint help g`|show all generators you have available|| ### Generators |Name|Description|Options| |---|---|---| -|`redux g dumb `|generates a dumb component and test file|| -|`redux g smart `|generates a smart connected component and test file|| -|`redux g form
`|generates a form component (assumes redux-form)|| -|`redux g duck `|generates a redux duck and test file|| +|`blueprint g dumb `|generates a dumb component and test file|| +|`blueprint g smart `|generates a smart connected component and test file|| +|`blueprint g form `|generates a form component (assumes redux-form)|| +|`blueprint g duck `|generates a redux duck and test file|| You can also see what files would get created with the `--dry-run` option like so: ``` -redux g dumb MyNewComponent --dry-run +blueprint g dumb MyNewComponent --dry-run // Output: @@ -147,36 +147,36 @@ redux g dumb MyNewComponent --dry-run Below are some examples of using the generator to speed up development: ``` -// generates a dumb component -redux g dumb SimpleButton +// generates a dumb component +blueprint g dumb SimpleButton -// generates a smart component -redux g smart CommentContainer +// generates a smart component +blueprint g smart CommentContainer // generate a redux-form with tags in render statement -redux g form ContactForm +blueprint g form ContactForm // generate a Redux 'duck' (reducer, constants, action creators) -redux g duck todos +blueprint g duck todos ``` ### Creating Blueprints -Blueprints are template generators with optional custom install logic. +Blueprints are template generators with optional custom install logic. -`redux generate` comes with a default set of blueprints. Every project has +`blueprint generate` comes with a default set of blueprints. Every project has their own configuration & needs, therefore blueprints have been made easy to override and extend. **Preliminary steps**: -1. Create a `blueprints` folder in your root directory. Redux CLI will search -for blueprints there _first_ before generating blueprints that come by default. +1. Create a `blueprints` folder in your root directory. Blueprint CLI will search +for blueprints there _first_ before generating blueprints that come by default. 2. Create a sub directory inside `blueprints` for the new blueprint OR use the -blueprint generator (super meta I know) that comes with Redux CLI by typing: -`redux g blueprint `. +blueprint generator (super meta I know) that comes with Blueprint CLI by typing: +`blueprint g blueprint `. 3. If you created the directory yourself than make sure to create a `index.js` file that exports your blueprint and a `files` folder with what you want -generated. +generated. **Customizing the blueprint**: @@ -199,29 +199,29 @@ blueprints/smart `files` contains templates for all the files to be generated into your project. -The `__name__` token is subtituted with the +The `__name__` token is subtituted with the entity name at install time. Entity names can be configued in either PascalCase, snake_case, camelCase, or dashes-case so teams can customize their file names accordingly. By default, the `__name__` will return whatever is entered in the generate CLI command. For example, when the user -invokes `redux g smart commentContainer` then `__name__` becomes +invokes `blueprint g smart commentContainer` then `__name__` becomes `commentContainer`. The `__root__` token is subsituted with the absolute path to your source. -Whatever path is in your `.reduxrc`'s `sourceBase` will be used here. +Whatever path is in your `.blueprintrc`'s `sourceBase` will be used here. The `__test__` token is substitued with the absolute path to your tests. -Whatever path is in your `.reduxrc`'s `testBase` will be used here. +Whatever path is in your `.blueprintrc`'s `testBase` will be used here. The `__path__` token is substituted with the blueprint name at install time. For example, when the user invokes -`redux generate smart foo` then `__path__` becomes -`smart`. +`blueprint generate smart foo` then `__path__` becomes +`smart`. The `__smart__` token is a custom token I added in the `index.js` it pulls from -your `.reduxrc` configuration file to use whatever you have set as your +your `.blueprintrc` configuration file to use whatever you have set as your `smartPath`. #### Template Variables (AKA Locals) @@ -252,7 +252,7 @@ export default <%= pascalEntityName %>; ``` `<%= pascalEntityName %>` is replaced with the real -value at install time. If we were to type: `redux g dumb simple-button` all +value at install time. If we were to type: `blueprint g dumb simple-button` all instances of `<%= pascalEntityName %>` would be converted to: `SimpleButton`. The following template variables are provided by default: @@ -270,7 +270,7 @@ described below. Custom installation (and soon uninstallation) behaviour can be added by overriding the hooks documented below. `index.js` should export a plain object, which will extend the prototype of the -`Blueprint` class. +`Blueprint` class. ```js module.exports = { @@ -321,7 +321,7 @@ containing general and entity-specific options. When the following is called on the command line: ```sh -redux g dumb foo --html=button --debug +blueprint g dumb foo --html=button --debug ``` The object passed to `locals` looks like this: @@ -378,7 +378,7 @@ blueprint's folder. Called before any of the template files are processed and receives the the `options` and `locals` hashes as parameters. Typically used for validating any additional command line options or for any asynchronous -setup that is needed. +setup that is needed. ### Contributing This CLI is very much in the beginning phases and I would love to have people @@ -403,8 +403,8 @@ npm start // to compile src into lib npm test // make sure all tests are passing // to test the cli in the local directory you can: -npm link // will install the npm package locally so you can run 'redux ' -redux +npm link // will install the npm package locally so you can run 'blueprint ' +blueprint ``` ### Package Utility Scripts: diff --git a/src/cli/cmds/config.js b/src/cli/cmds/config.js new file mode 100644 index 0000000..fb3278b --- /dev/null +++ b/src/cli/cmds/config.js @@ -0,0 +1,11 @@ +import Config from '../../sub-commands/config'; +const subCommand = new Config(); + +const usage = 'Usage:\n $0 config'; + +module.exports = { + command: 'config', + describe: 'Display current configuration', + builder: yargs => yargs.usage(usage), + handler: () => subCommand.run() +}; \ No newline at end of file diff --git a/src/cli/cmds/generate/build-blueprint-commands.js b/src/cli/cmds/generate/build-blueprint-commands.js index 221db71..ce70524 100644 --- a/src/cli/cmds/generate/build-blueprint-commands.js +++ b/src/cli/cmds/generate/build-blueprint-commands.js @@ -1,24 +1,21 @@ -import Blueprint from '../../../models/blueprint'; import Generate from '../../../sub-commands/generate'; import buildBlueprintCommand from './build-blueprint-command'; const subCommand = new Generate(); -/* - TODO: refactor to use new BlueprintCollection, should just - be a simple map to buildBlueprintCommand -*/ +const settings = subCommand.environment.settings.settings || {}; +const blueprints = subCommand.environment.settings.blueprints; +settings.bp = settings.bp || {}; + const buildBlueprintCommands = () => - Blueprint.loadRunnable().map(blueprint => { + blueprints.generators().map(blueprint => { loadBlueprintSettings(blueprint); return buildBlueprintCommand(blueprint, subCommand); }); export default buildBlueprintCommands; -const settings = subCommand.environment.settings.settings || {}; -settings.bp = settings.bp || {}; const loadBlueprintSettings = blueprint => { const blueprintSettings = getBlueprintSettings(blueprint); blueprint.settings = blueprintSettings; diff --git a/src/models/blueprint-collection.js b/src/models/blueprint-collection.js new file mode 100644 index 0000000..2608353 --- /dev/null +++ b/src/models/blueprint-collection.js @@ -0,0 +1,105 @@ +import path from 'path'; +import fs from 'fs'; +import _map from 'lodash/map'; +import _filter from 'lodash/filter'; +import _isNil from 'lodash/isNil'; +import _isBool from 'lodash/isBoolean'; +import _isString from 'lodash/isString'; +import _isArray from 'lodash/isArray'; +import _flatten from 'lodash/flatten'; +import _uniq from 'lodash/uniq'; +import Blueprint from './blueprint'; + +export default class BlueprintCollection { + constructor (pathList) { + this.pathList = pathList; + this.setSearchPaths(); + } + + allPossiblePaths () { + return _flatten( + _map( + this.pathList, + (arr, base) => _map(arr, (bp) => expandPath(base, bp))) + ); + } + + setSearchPaths () { + this.searchPaths = _uniq(_filter(this.allPossiblePaths(), validSearchDir)); + } + + all () { + // Is there a more idiomatic way to do this? I miss ruby's ||= + if (this.allBlueprints) { + return this.allBlueprints; + } else { + return this.allBlueprints = this.discoverBlueprints(); + } + } + + generators () { + // until we learn to tell generators apart from partials + return _filter(this.all(), (bp) => bp.name); + } + + allNames () { + return _map(this.all(), (bp) => bp.name); + } + + + addBlueprint (path) { + return Blueprint.load(path); + } + + discoverBlueprints () { + return _map(this.findBlueprints(), this.addBlueprint); + } + + findBlueprints () { + return _flatten( + _map( + this.searchPaths, + (dir) => { + const subdirs = _map(fs.readdirSync(dir), (p) => path.resolve(dir, p)); + return _filter(subdirs, (d) => fs.existsSync(path.resolve(d, 'index.js'))); + } + ) + ); + } +} + +function validSearchDir (dir) { + return fs.existsSync(dir) && fs.lstatSync(dir).isDirectory(); +} + +export function expandPath (base, candidate) { + let final; + if (candidate[0] === '~') { + const st = candidate[1] === path.sep ? 2 : 1; + final = path.resolve(process.env.HOME, candidate.slice(st)); + } else if (candidate[0] === path.sep) { + final = path.resolve(candidate); + // } else if (candidate[0] === '@') { + // return path.join(npmPath,npm name, 'blueprints'); + } else { + final = path.resolve(base, candidate); + } + return final; +} + +export function parseBlueprintSetting (setting) { + if (_isArray(setting)) { + return [...setting, './blueprints']; + } else if (_isString(setting)) { + return [setting, './blueprints']; + } else if (_isBool(setting)) { + return setting ? ['./blueprints'] : []; + } else if (_isNil(setting)) { + return ['./blueprints']; + } else { + // No numbers, + // raise error here? + // console.error('Unknown blueprint type'); + return ['./blueprints']; + } +} diff --git a/src/models/blueprint.js b/src/models/blueprint.js index 0d13646..b62ccca 100644 --- a/src/models/blueprint.js +++ b/src/models/blueprint.js @@ -11,22 +11,6 @@ import config from '../config'; const { basePath } = config; -function generateLookupPaths(lookupPaths) { - lookupPaths = lookupPaths || []; - lookupPaths = lookupPaths.concat(Blueprint.defaultLookupPaths()); - return _.uniq(lookupPaths); -} - -function dir(fullPath) { - if (fileExists(fullPath)) { - return fs.readdirSync(fullPath).map(function(fileName) { - return path.join(fullPath, fileName); - }); - } else { - return []; - } -} - export default class Blueprint { constructor(blueprintPath) { this.path = blueprintPath; @@ -57,33 +41,6 @@ export default class Blueprint { return this._files; } - static defaultLookupPaths() { - return [ - path.resolve(path.join(basePath, 'blueprints')), - path.resolve(__dirname, '..', '..', 'blueprints') - ]; - } - // find blueprint given a path or return error - // look inside current project first and then redux-cli defaults - static lookup(name, options = {}) { - const lookupPaths = generateLookupPaths(options.paths); - - let lookupPath; - let blueprintPath; - - for (let i = 0; (lookupPath = lookupPaths[i]); i++) { - blueprintPath = path.resolve(lookupPath, name); - - if (fileExists(blueprintPath)) { - return Blueprint.load(blueprintPath); - } - } - - if (!options.ignoreMissing) { - throw new Error('Unknown blueprint: ' + name); - } - } - // load in the blueprint that was found, extend this class to load it static load(blueprintPath) { let Constructor; @@ -99,85 +56,6 @@ export default class Blueprint { } } - static loadAll(options = {}) { - return generateLookupPaths(options.paths).map(lookupPath => { - const blueprintFiles = dir(lookupPath); - const packagePath = path.join(lookupPath, '../package.json'); - let source; - - if (fileExists(packagePath)) { - source = require(packagePath).name; - } else { - source = path.basename(path.join(lookupPath, '..')); - } - - const blueprints = blueprintFiles.map(filePath => this.load(filePath)); - - return { - source, - blueprints: _.compact(blueprints) - }; - }); - } - - static loadRunnable(options = {}) { - const blueprints = this.loadAll(options); - const runnable = []; - - const alreadyAdded = blueprint => - runnable.find(existing => existing.name === blueprint.name); - - blueprints.forEach(source => { - source.blueprints.forEach(blueprint => { - if (!alreadyAdded(blueprint)) { - runnable.push(blueprint); - } - }); - }); - - return runnable; - } - - // TODO: refactor list to use loadAll or loadRunnable - static list(options = {}) { - return generateLookupPaths(options.paths).map(lookupPath => { - const blueprintFiles = dir(lookupPath); - const packagePath = path.join(lookupPath, '../package.json'); - let source; - - if (fileExists(packagePath)) { - source = require(packagePath).name; - } else { - source = path.basename(path.join(lookupPath, '..')); - } - - const blueprints = blueprintFiles.map(filePath => { - const blueprint = this.load(filePath); - - if (blueprint) { - let description; - const name = blueprint.name; - - if (blueprint.description) { - description = blueprint.description(); - } else { - description = 'N/A'; - } - - return { - name, - description - }; - } - }); - - return { - source, - blueprints: _.compact(blueprints) - }; - }); - } - _fileMapTokens(options) { const standardTokens = { __name__: (options) => { diff --git a/src/models/project-settings.js b/src/models/project-settings.js index 4c372c1..f8153cc 100644 --- a/src/models/project-settings.js +++ b/src/models/project-settings.js @@ -2,43 +2,51 @@ import path from 'path'; import jf from 'jsonfile'; import { pwd } from 'shelljs'; import rc from 'rc'; +import cc from 'rc/lib/utils'; +import _zipObject from 'lodash/zipObject'; +import _map from 'lodash/map'; -/* - Look into using Yam for finding settings so it will get the first - .reduxrc it finds and use that for project settings just like how - eslintrc and ember-cli works. -*/ - -/* - 2.0 TODO - - Use rc to enable multiple .reduxrc files to be located and merged into a - single object. - - rc: https://www.npmjs.com/package/rc -*/ +import BlueprintCollection, { parseBlueprintSetting } from './blueprint-collection'; export default class ProjectSettings { // public & tested - maintain in 2.0 constructor (defaultSettings = {}, args = null) { this.defaultSettings = defaultSettings; this.args = args; + this.blueprintChunks = []; + this.configChunks = []; + this.myParse = this.myParse.bind(this); + this.saveDefaults = this.saveDefaults.bind(this); this.loadSettings(); + this.blueprints = new BlueprintCollection(_zipObject(this.configDirs(), this.blueprintChunks)); + } + + configDirs () { + return _map(this.configFiles(), (configFile) => path.dirname(configFile)); + } + + configFiles () { + return _map(this.settings.configs, (configFile) => path.resolve(configFile)); + } + + allConfigs () { + const configs = _zipObject(this.settings.configs, this.configChunks); + configs['__default__'] = this.defaultSettings; + return configs; } // internal & tested // from #constructor loadSettings () { - this.settings = rc('redux', this.defaultSettings, this.args); - // if the config file list is empty, and the config is only the default - // maybe save or prompt to save the default config file + const startingSettings = JSON.parse(JSON.stringify(this.defaultSettings)); + this.settings = rc('blueprint', startingSettings, this.args, this.myParse); } // internal & tested - maintain in 2.0 // #settingsExist // #save settingsPath () { - return path.join(pwd(), '.reduxrc'); + return path.join(pwd(), '.blueprintrc'); } //public & tested - maintain in 2.0 @@ -63,7 +71,18 @@ export default class ProjectSettings { } // public - maintain in 2.0 - saveDefaults (defaultSettings = this.defaultSettings) { - jf.writeFileSync(this.settingsPath(), defaultSettings); + saveDefaults (defaultSettings = this.defaultSettings, savePath = false) { + jf.writeFileSync(savePath || this.settingsPath(), defaultSettings); + } + + // wrap the default rc parsing function with this + // By default rc returns everything merged. Keeping + // track of chunks allows us to associate a config + // with the .blueprintrc file it came out of. + myParse (rawContent) { + const content = cc.parse(rawContent); + this.configChunks.unshift(content); + this.blueprintChunks.unshift(parseBlueprintSetting(content)); + return content; } } diff --git a/src/models/sub-command.js b/src/models/sub-command.js index 715f576..e237b8a 100644 --- a/src/models/sub-command.js +++ b/src/models/sub-command.js @@ -1,5 +1,7 @@ import ProjectSettings from './project-settings'; import UI from './ui'; +import figlet from 'figlet'; +import { success } from '../util/text-helper'; class SubCommand { constructor(options = {}) { @@ -20,6 +22,16 @@ class SubCommand { availableOptions() { throw new Error('Subcommands must implement an availableOptions()'); } + + cliLogo () { + return success( + figlet.textSync('Blueprint-CLI', { + font: 'Doom', + horizontalLayout: 'default', + verticalLayout: 'default' + }) + ); + } } export default SubCommand; diff --git a/src/sub-commands/config.js b/src/sub-commands/config.js new file mode 100644 index 0000000..e8a0515 --- /dev/null +++ b/src/sub-commands/config.js @@ -0,0 +1,33 @@ +import prettyjson from 'prettyjson'; +import SubCommand from '../models/sub-command'; + +class Config extends SubCommand { + constructor () { + super(); + } + + printUserHelp () { + this.ui.write( + 'config command to display current configuration' + ); + } + + run () { + const finalConfig = Object.assign({}, this.settings.settings); + delete finalConfig.configs; + delete finalConfig.allConfigs; + delete finalConfig['_']; + this.ui.write(this.cliLogo() + '\n'); + this.ui.writeInfo('Config Files'); + console.log(prettyjson.render(this.settings.settings.configs, {}, 8)); + // this.settings.settings.configs.forEach(configFile => {this.ui.writeInfo(` * ${configFile}`)}) + this.ui.writeInfo('Config Data'); + console.log(prettyjson.render(finalConfig, {}, 10)); + this.ui.writeInfo('Blueprint Paths'); + console.log(prettyjson.render(this.settings.blueprints.searchPaths, {}, 8)); + this.ui.writeInfo('Blueprints'); + console.log(prettyjson.render(this.settings.blueprints.allNames(), {}, 8)); + } +} + +export default Config; diff --git a/src/sub-commands/init.js b/src/sub-commands/init.js index 90d6219..4b682ee 100644 --- a/src/sub-commands/init.js +++ b/src/sub-commands/init.js @@ -1,11 +1,7 @@ import prompt from 'prompt'; -import figlet from 'figlet'; - import SubCommand from '../models/sub-command'; - import initPrompt from '../prompts/initPrompt'; import { setupPrompt } from '../prompts/setup'; -import { success } from '../util/text-helper'; class Init extends SubCommand { constructor () { @@ -15,7 +11,7 @@ class Init extends SubCommand { printUserHelp () { this.ui.write( - 'initialization command to create a .reduxrc which has project settings' + 'initialization command to create a .blueprintrc which has project settings' ); } @@ -24,19 +20,9 @@ class Init extends SubCommand { prompt.get(initPrompt, (err, result) => { this.ui.writeInfo('Saving your settings...'); this.settings.saveDefaults(result); - this.ui.writeCreate('.reduxrc with configuration saved in project root.'); + this.ui.writeCreate('.blueprintrc with configuration saved in project root.'); }); } - - cliLogo () { - return success( - figlet.textSync('Redux-CLI', { - font: 'Doom', - horizontalLayout: 'default', - verticalLayout: 'default' - }) - ); - } } export default Init; diff --git a/src/sub-commands/new.js b/src/sub-commands/new.js index 7152e25..3a8d052 100644 --- a/src/sub-commands/new.js +++ b/src/sub-commands/new.js @@ -77,11 +77,11 @@ class New extends SubCommand { // All settings for react-redux-starter-kit live in this template so when // new projects get created users can immediately start using the CLI createProjectSettings () { - this.ui.writeInfo('creating a default .reduxrc for your project'); + this.ui.writeInfo('creating a default .blueprintrc for your project'); const settings = new ProjectSettings(); settings.saveDefault(); - this.ui.writeCreate('.reduxrc with starter kit settings saved.'); + this.ui.writeCreate('.blueprintrc with starter kit settings saved.'); } } diff --git a/templates/.reduxrc b/templates/.blueprintrc similarity index 100% rename from templates/.reduxrc rename to templates/.blueprintrc diff --git a/test/cli/cmds/config.test.js b/test/cli/cmds/config.test.js new file mode 100644 index 0000000..5061fbd --- /dev/null +++ b/test/cli/cmds/config.test.js @@ -0,0 +1,44 @@ +import { getParser } from 'cli/parser'; +import { lineRegEx } from '../../helpers/regex-utils'; +import Config from 'sub-commands/config'; + +jest.mock('sub-commands/config'); + +describe('(CLI) Config', () => { + let parser; + + beforeEach(() => { + parser = getParser(); + parser.$0 = 'bp'; + }); + + describe('--help', () => { + test('shows Usage', done => { + parser.parse('help config', (err, argv, output) => { + expect(err).to.be.undefined; + expect(output).to.include('Usage:'); + expect(output).to.match(lineRegEx('bp config')); + done(); + }); + }); + + test("doesn't include --version", done => { + parser.parse('help init', (err, argv, output) => { + expect(output).to.not.include('--version, -V'); + done(); + }); + }); + }); + + describe('handler', () => { + test('runs subCommand without arguments', done => { + parser.parse('config', (err, argv, output) => { + expect(Config.mock.instances.length).toEqual(1); + expect(Config.mock.instances[0].run.mock.calls.length).toEqual(1); + expect(Config.mock.instances[0].run.mock.calls[0]).toEqual([]); + expect(output).toEqual(''); + done(); + }); + }); + }); +}); diff --git a/test/fixtures/argv.blueprintrc b/test/fixtures/argv.blueprintrc new file mode 100644 index 0000000..1243c7b --- /dev/null +++ b/test/fixtures/argv.blueprintrc @@ -0,0 +1,7 @@ +{ + "location": "argv", + "argvConfig": "ARGV", + "blueprints": "argv" +} + + diff --git a/test/fixtures/argv.reduxrc b/test/fixtures/argv.reduxrc deleted file mode 100644 index 23e6028..0000000 --- a/test/fixtures/argv.reduxrc +++ /dev/null @@ -1 +0,0 @@ -{"argvConfig": "ARGV"} diff --git a/test/fixtures/blueprints/duplicate/files/expected-file.js b/test/fixtures/blueprints/duplicate/files/expected-file.js new file mode 100644 index 0000000..a6d5869 --- /dev/null +++ b/test/fixtures/blueprints/duplicate/files/expected-file.js @@ -0,0 +1 @@ +// expected file to exist diff --git a/test/fixtures/blueprints/duplicate/index.js b/test/fixtures/blueprints/duplicate/index.js new file mode 100644 index 0000000..f4d6253 --- /dev/null +++ b/test/fixtures/blueprints/duplicate/index.js @@ -0,0 +1,3 @@ +module.exports = { + +}; diff --git a/test/fixtures/env.blueprintrc b/test/fixtures/env.blueprintrc new file mode 100644 index 0000000..7eba0b7 --- /dev/null +++ b/test/fixtures/env.blueprintrc @@ -0,0 +1,6 @@ +{ + "envConfig": "Environment", + "blueprints": [ + "env" + ] +} diff --git a/test/fixtures/env.reduxrc b/test/fixtures/env.reduxrc deleted file mode 100644 index 097065c..0000000 --- a/test/fixtures/env.reduxrc +++ /dev/null @@ -1 +0,0 @@ -{"envConfig": "Environment"} diff --git a/test/models/blueprint-collection.test.js b/test/models/blueprint-collection.test.js new file mode 100644 index 0000000..5a1b6e4 --- /dev/null +++ b/test/models/blueprint-collection.test.js @@ -0,0 +1,134 @@ +import path from 'path'; +import BlueprintCollection, { expandPath, parseBlueprintSetting } from 'models/blueprint-collection'; +import config from 'config'; +import process from 'process'; + +const {basePath} = config; + +const paths = { + [path.resolve(basePath, 'test')]: [ + 'fixtures', + '/ghi', + 'doh' + ], + [path.resolve(basePath, 'test/fixtures')]: [ + '~doesNotExist7as84', + './blueprints' + ] +}; + +describe('(Model) BlueprintCollection', () => { + describe('#all()', () => { + it('should return an array of all blueprints in searchPaths', () => { + const blueprints = new BlueprintCollection(paths); + const result = blueprints.all(); + expect(result).to.be.an('Array'); + expect(result).toHaveLength(2); + expect(result[0].name).toEqual('basic'); + expect(result[1].filesPath()).to.match(/fixtures\/blueprints\/duplicate/); + }); + }); + + describe('#generators()', () => { + it('should return an array of all generator blueprints in searchPaths', () => { + const blueprints = new BlueprintCollection(paths); + const result = blueprints.generators(); + expect(result).to.be.an('Array'); + expect(result).toHaveLength(2); + expect(result[0].name).toEqual('basic'); + expect(result[1].filesPath()).to.match(/fixtures\/blueprints\/duplicate/); + }); + }); + + + describe('#allPossiblePaths', () => { + test('returns a an array of blueprint paths', () => { + const blueprints = new BlueprintCollection(paths); + const result = blueprints.allPossiblePaths(); + + expect(result[0]).toEqual(basePath + '/test/fixtures'); + expect(result[1]).toEqual('/ghi'); + expect(result[2]).toEqual(basePath + '/test/doh'); + expect(result[3].slice(process.env.HOME.length)).toEqual('/doesNotExist7as84'); + expect(result[4]).toEqual(basePath + '/test/fixtures/blueprints'); + }); + }); + describe('setSearchPaths', () => { + test('', () => { + const blueprints = new BlueprintCollection(paths); + const result = blueprints.searchPaths; + + expect(result[0]).toEqual(basePath + '/test/fixtures'); + expect(result[1]).toEqual(basePath + '/test/fixtures/blueprints'); + + }); + }); +}); + +describe('#expandPath', () => { + + test('returns path if path starts with /', () => { + const testPath = '/bruce'; + const resultPath = expandPath(basePath, testPath); + expect(resultPath).toEqual(testPath); + }); + + test('returns path relative from home if path starts with ~', () => { + const testPath = '~dick'; + const resultPath = expandPath(basePath, testPath); + const expectedPath = process.env.HOME + path.sep + 'dick'; + expect(resultPath).toEqual(expectedPath); + }); + + test('returns path relative from home if path starts with ~/', () => { + const testPath = '~/barbara'; + const resultPath = expandPath(basePath, testPath); + const expectedPath = process.env.HOME + path.sep + 'barbara'; + expect(resultPath).toEqual(expectedPath); + }); + + test('returns path relative to basePath if does not start with "/" or "~"', () => { + const testPath = 'alfred'; + const resultPath = expandPath(basePath, testPath); + const expectedPath = basePath + path.sep + 'alfred'; + expect(resultPath).toEqual(expectedPath); + }); + + +}); + +describe('::parseBlueprintSetting', () => { + const bpArr = ['./blueprints']; + test('returns arr + bparr if is array', () => { + const testSetting = ['jim']; + const resultArr = parseBlueprintSetting(testSetting); + const expectedArr = [...testSetting, ...bpArr]; + expect(resultArr).toEqual(expectedArr); + }); + test('returns arr with name + bparr if is string', () => { + const testSetting = 'leslie'; + const resultArr = parseBlueprintSetting(testSetting); + const expectedArr = [testSetting, ...bpArr]; + expect(resultArr).toEqual(expectedArr); + }); + test('returns bpArr if is boolean and is true', () => { + const testSetting = true; + const resultArr = parseBlueprintSetting(testSetting); + expect(resultArr).toEqual(bpArr); + }); + test('returns empty array if is boolean and is false', () => { + const testSetting = false; + const resultArr = parseBlueprintSetting(testSetting); + expect(resultArr).toEqual([]); + }); + test('returns bpArr if is undefined', () => { + const testSetting = null; + const resultArr = parseBlueprintSetting(testSetting); + expect(resultArr).toEqual(bpArr); + }); + test('returns bpArr if is number', () => { + const testSetting = 42; + const resultArr = parseBlueprintSetting(testSetting); + expect(resultArr).toEqual(bpArr); + }); +}); diff --git a/test/models/blueprint.test.js b/test/models/blueprint.test.js index 8947f44..730aaed 100644 --- a/test/models/blueprint.test.js +++ b/test/models/blueprint.test.js @@ -1,8 +1,5 @@ import path from 'path'; import Blueprint from 'models/blueprint'; -import config from 'config'; - -const { basePath } = config; const fixtureBlueprints = path.resolve(__dirname, '..', 'fixtures', 'blueprints'); const basicBlueprint = path.join(fixtureBlueprints, 'basic'); @@ -10,6 +7,12 @@ const basicBlueprint = path.join(fixtureBlueprints, 'basic'); describe('(Model) Blueprint', () => { const blueprint = new Blueprint(basicBlueprint); + describe('#description',()=>{ + test('returns a description',()=>{ + expect(blueprint.description()).to.match(/Generates a new basic/); + }); + }); + describe('#filesPath', () => { test('returns a default of "files" ', () => { const expectedPath = path.join(basicBlueprint, 'files'); @@ -32,27 +35,9 @@ describe('(Model) Blueprint', () => { }); }); - describe('.defaultLookupPaths', () => { - test( - 'returns an array with all potential paths blueprints can live', - () => { - const expectedFiles = [ - path.join(basePath, 'blueprints'), - path.join(__dirname, '..', '..', 'blueprints') - ]; - expect(Blueprint.defaultLookupPaths()).toEqual(expectedFiles); - } - ); - }); - - describe('.lookup', () => { - test('throws error when it cant find blueprint', () => { - expect(() => Blueprint.lookup('sdlfkjskf')).toThrowError(/Unknown blueprint:/); - }); - - test('it returns loaded blueprint when found', () => { - const blueprint = Blueprint.lookup(basicBlueprint); - expect(blueprint.path).toEqual(basicBlueprint); + describe('.load', () => { + test('loads a blueprint from a path', () => { + const blueprint = Blueprint.load(basicBlueprint); expect(blueprint.name).toEqual('basic'); }); }); diff --git a/test/models/project-settings.test.js b/test/models/project-settings.test.js index 42f3d78..a00aeaa 100644 --- a/test/models/project-settings.test.js +++ b/test/models/project-settings.test.js @@ -1,53 +1,72 @@ import ProjectSettings from 'models/project-settings'; import fs from 'fs'; -import fse from 'fs-extra'; import config from 'config'; -import { fileExists } from 'util/fs'; const {basePath} = config; -const settingsPath = basePath + '/.reduxrc'; +const settingsPath = basePath + '/.blueprintrc'; describe('ProjectSettings', () => { - beforeEach(() => { - fileExists(settingsPath) && fse.removeSync(settingsPath); - }); - - afterEach(() => { - fileExists(settingsPath) && fse.removeSync(settingsPath); - }); - const settings = new ProjectSettings(); - // this is the local path. intended to be root of a particular directory describe('#settingsPath', () => { - it('returns current directory with .reduxrc appended', () => { + it('returns current directory with .blueprintrc appended', () => { + const settings = new ProjectSettings(); expect(settings.settingsPath()).to.eql(settingsPath); }); }); describe('#loadSettings', () => { - it('loads settings from $CWD/.reduxrc', () => { - fs.writeFileSync( - settingsPath, JSON.stringify({test: 'works!'}) - ); + it('loads settings from $CWD/.blueprintrc', () => { const settings = new ProjectSettings(); - expect(settings.getSetting('test')).to.eql('works!'); + expect(settings.getSetting('location')).to.eql('project'); }); // inject a ENV variable and load settings. - it('loads settings from .reduxrc defined in process.env', () => { - process.env['redux_config'] = 'test/fixtures/env.reduxrc'; + it('loads settings from .blueprintrc defined in process.env', () => { + process.env['blueprint_config'] = 'test/fixtures/env.blueprintrc'; const settings = new ProjectSettings(); expect(settings.getSetting('envConfig')).to.eql('Environment'); + delete process.env['blueprint_config']; // expect the file to be in the path of config files too }); // inject an ARGV and load settings. - it('loads settings from .reduxrc defined as ARGV', () => { - const fakeArgv = {config: basePath + '/test/fixtures/argv.reduxrc'}; + it('loads settings from .blueprintrc defined as ARGV', () => { + const fakeArgv = {config: 'test/fixtures/argv.blueprintrc'}; const defaultSettings = {defaultOption: true}; const argvSettings = new ProjectSettings(defaultSettings, fakeArgv); expect(argvSettings.getSetting('argvConfig')).to.eql('ARGV'); }); + + it('collects __defaults__ in allConfigs', () => { + const defaultSettings = {defaultOption: true}; + const argvSettings = new ProjectSettings(defaultSettings); + expect(argvSettings.allConfigs()['__default__']).to.eql(defaultSettings); + }); + + it('collects all configurations into a object', () => { + process.env['blueprint_config'] = 'test/fixtures/env.blueprintrc'; + const fakeArgv = {config: 'test/fixtures/argv.blueprintrc'}; + const defaultSettings = {defaultOption: true}; + const allSettings = new ProjectSettings(defaultSettings, fakeArgv); + const fileCount = allSettings.settings.configs.length; + + expect(allSettings.configChunks.length).to.eql(fileCount); + expect(Object.keys(allSettings.allConfigs()).length).to.eql(fileCount + 1); + delete process.env['blueprint_config']; + }); + + it('collects all blueprints into an array of arrays', () => { + // How many do we have before + const baseline = (new ProjectSettings).blueprintChunks.length; + + process.env['blueprint_config'] = 'test/fixtures/env.blueprintrc'; + const fakeArgv = {config: 'test/fixtures/argv.blueprintrc'}; + const defaultSettings = {defaultOption: true}; + const settings = new ProjectSettings(defaultSettings, fakeArgv); + + expect(settings.blueprintChunks.length).to.eql(baseline + 2); + delete process.env['blueprint_config']; + }); }); describe('#getSetting', () => { @@ -105,12 +124,41 @@ describe('ProjectSettings', () => { describe('#saveDefault', () => { it('saves the current settings to the file', () => { + const tmpPath = '/tmp/.blueprintrc'; const defaults = {'testSaveDefault': 'new setting'}; const settings = new ProjectSettings(defaults); - settings.saveDefaults(); - const newFile = fs.readFileSync(settingsPath, 'utf8'); - expect(newFile).to.match(/testSaveDefault/); - expect(newFile).to.match(/new setting/); + settings.saveDefaults(defaults, tmpPath); + const newFile = fs.readFileSync(tmpPath, 'utf8'); + expect(newFile).to.match(/testSaveDefault.+new setting/); + }); + }); + + describe('#configFiles', () => { + it('returns an array of all config files read', () => { + process.env['blueprint_config'] = 'test/fixtures/env.blueprintrc'; + const fakeArgv = {config: 'test/fixtures/argv.blueprintrc'}; + const defaultSettings = {defaultOption: true}; + const settings = new ProjectSettings(defaultSettings, fakeArgv); + const expectedFiles = [ + settingsPath, + basePath + '/test/fixtures/argv.blueprintrc', + basePath + '/test/fixtures/env.blueprintrc' + ]; + expect(settings.configFiles()).to.include.members(expectedFiles); + }); + }); + describe('#blueprints', () => { + it('returns a BlueprintCollection', () => { + process.env['blueprint_config'] = 'test/fixtures/env.blueprintrc'; + const fakeArgv = {config: 'test/fixtures/argv.blueprintrc'}; + const defaultSettings = {defaultOption: true}; + const settings = new ProjectSettings(defaultSettings, fakeArgv); + const blueprintPaths = settings.blueprints.searchPaths; + const expectedFiles = [ + basePath + '/blueprints', + basePath + '/test/fixtures/blueprints' + ]; + expect(blueprintPaths).to.include.members(expectedFiles); }); }); }); diff --git a/test/models/sub-command.test.js b/test/models/sub-command.test.js index 41adf50..bdbe3af 100644 --- a/test/models/sub-command.test.js +++ b/test/models/sub-command.test.js @@ -21,4 +21,16 @@ describe('(Model) SubCommand', () => { const command = new SubCommand(options); expect(command.environment).toEqual(options); }); + + describe('cliLogo()', () => { + test('returns a string', () => { + const options = { + ui: 'cli interface', + settings: 'project settings' + }; + const command = new SubCommand(options); + expect(command.cliLogo()).to.be.a('string'); + + }); + }); }); diff --git a/yarn.lock b/yarn.lock index cd45065..1684bc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -500,6 +500,10 @@ babel-plugin-jest-hoist@^21.0.2: version "21.0.2" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-21.0.2.tgz#cfdce5bca40d772a056cb8528ad159c7bb4bb03d" +babel-plugin-syntax-object-rest-spread@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + babel-plugin-transform-es2015-arrow-functions@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" @@ -668,6 +672,13 @@ babel-plugin-transform-es2015-unicode-regex@^6.24.1: babel-runtime "^6.22.0" regexpu-core "^2.0.0" +babel-plugin-transform-object-rest-spread@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06" + dependencies: + babel-plugin-syntax-object-rest-spread "^6.8.0" + babel-runtime "^6.26.0" + babel-plugin-transform-regenerator@^6.24.1: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" @@ -3230,6 +3241,13 @@ pretty-format@^21.1.0: ansi-regex "^3.0.0" ansi-styles "^3.2.0" +prettyjson@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prettyjson/-/prettyjson-1.2.1.tgz#fcffab41d19cab4dfae5e575e64246619b12d289" + dependencies: + colors "^1.1.2" + minimist "^1.2.0" + private@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/private/-/private-0.1.6.tgz#55c6a976d0f9bafb9924851350fe47b9b5fbb7c1" @@ -4286,7 +4304,7 @@ yargs-parser@^7.0.0: dependencies: camelcase "^4.1.0" -yargs@^9.0.0: +yargs@^9.0.0, yargs@^9.0.1: version "9.0.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c" dependencies: