diff --git a/.circleci/config.yml b/.circleci/config.yml index f5c0809f9..ef99bcda4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,6 +67,17 @@ jobs: - run: .circleci/setup_git - run: yarn exec nps test.command - store_test_results: *store_test_results + node-latest-hook: + <<: *lint + docker: + - image: node:8 + steps: &hook_steps + - checkout + - restore_cache: *restore_cache + - attach_workspace: {at: node_modules} + - run: .circleci/setup_git + - run: yarn exec nps test.hook + - store_test_results: *store_test_results node-8-base: &node_8 <<: *lint docker: @@ -87,6 +98,9 @@ jobs: node-8-command: <<: *node_8 steps: *command_steps + node-8-hook: + <<: *node_8 + steps: *hook_steps cache: <<: *lint @@ -149,8 +163,10 @@ workflows: - node-latest-plugin: {requires: [lint] } - node-latest-multi: {requires: [lint] } - node-latest-command: {requires: [lint] } + - node-latest-hook: {requires: [lint] } - node-8-base: {requires: [lint] } - node-8-single: {requires: [lint] } - node-8-plugin: {requires: [lint] } - node-8-multi: {requires: [lint] } - node-8-command: {requires: [lint] } + - node-8-hook: {requires: [lint] } diff --git a/appveyor.yml b/appveyor.yml index 54d4bd703..2fded46f6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,6 +7,7 @@ environment: - TEST_TYPE: plugin - TEST_TYPE: multi - TEST_TYPE: command + - TEST_TYPE: hook cache: - '%LOCALAPPDATA%\Yarn -> appveyor.yml' - node_modules -> yarn.lock diff --git a/package-scripts.js b/package-scripts.js index c14db1e1d..f61e6e3b6 100644 --- a/package-scripts.js +++ b/package-scripts.js @@ -15,7 +15,7 @@ sh.set('-e') setColors(['dim']) -const testTypes = ['base', 'plugin', 'single', 'multi', 'command'] +const testTypes = ['base', 'plugin', 'single', 'multi', 'command', 'hook'] const tests = testTypes.map(cmd => { const {silent} = sh.config sh.config.silent = true diff --git a/package.json b/package.json index c41292ef9..2de2492e7 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "devDependencies": { "@oclif/dev-cli": "^1.3.1", "@oclif/tslint": "^1.0.2", - "@types/lodash": "^4.14.105", + "@types/lodash": "^4.14.106", "@types/read-pkg": "^3.0.0", "@types/shelljs": "^0.7.8", "@types/yeoman-generator": "^2.0.1", @@ -36,7 +36,7 @@ "fancy-test": "^1.0.1", "fs-extra": "^5.0.0", "globby": "^8.0.1", - "mocha": "^5.0.4", + "mocha": "^5.0.5", "npm-run-path": "^2.0.2", "nps": "^5.8.2", "shelljs": "^0.8.1", diff --git a/src/commands/command.ts b/src/commands/command.ts index b281a6bc5..9f78b4b66 100644 --- a/src/commands/command.ts +++ b/src/commands/command.ts @@ -25,6 +25,6 @@ export default abstract class AppCommand extends Base { name: args.name, defaults: flags.defaults, force: flags.force - }) + } as Options) } } diff --git a/src/commands/hook.ts b/src/commands/hook.ts new file mode 100644 index 000000000..21ec38007 --- /dev/null +++ b/src/commands/hook.ts @@ -0,0 +1,33 @@ +import {flags} from '@oclif/command' + +import Base from '../command_base' + +export interface Options { + name: string + defaults?: boolean + force?: boolean + event: string +} + +export default abstract class HookCommand extends Base { + static description = 'add a hook to an existing CLI or plugin' + + static flags = { + defaults: flags.boolean({description: 'use defaults for every setting'}), + force: flags.boolean({description: 'overwrite existing files'}), + event: flags.string({description: 'event to run hook on', default: 'init'}), + } + static args = [ + {name: 'name', description: 'name of hook (snake_case)', required: true} + ] + + async run() { + const {flags, args} = this.parse(HookCommand) + await super.generate('hook', { + name: args.name, + event: flags.event, + defaults: flags.defaults, + force: flags.force, + } as Options) + } +} diff --git a/src/generators/hook.ts b/src/generators/hook.ts new file mode 100644 index 000000000..6554993b0 --- /dev/null +++ b/src/generators/hook.ts @@ -0,0 +1,51 @@ +// tslint:disable no-floating-promises +// tslint:disable no-console + +import * as _ from 'lodash' +import * as path from 'path' +import * as Generator from 'yeoman-generator' +import yosay = require('yosay') + +import {Options} from '../commands/hook' + +const {version} = require('../../package.json') + +class HookGenerator extends Generator { + pjson!: any + + get _path() { return this.options.name.split(':').join('/') } + get _ts() { return this.pjson.devDependencies.typescript } + get _ext() { return this._ts ? 'ts' : 'js' } + get _mocha() { return this.pjson.devDependencies.mocha } + + constructor(args: any, public options: Options) { + super(args, options) + } + + async prompting() { + this.pjson = this.fs.readJSON('package.json') + this.pjson.oclif = this.pjson.oclif || {} + if (!this.pjson) throw new Error('not in a project directory') + this.log(yosay(`Adding a ${this.options.event} hook to ${this.pjson.name} Version: ${version}`)) + } + + writing() { + this.sourceRoot(path.join(__dirname, '../../templates')) + this.fs.copyTpl(this.templatePath(`src/hook.${this._ext}.ejs`), this.destinationPath(`src/hooks/${this.options.event}/${this.options.name}.${this._ext}`), this) + if (this._mocha) { + this.fs.copyTpl(this.templatePath(`test/hook.test.${this._ext}.ejs`), this.destinationPath(`test/commands/${this._path}.test.${this._ext}`), this) + } + this.pjson.oclif = this.pjson.oclif || {} + let hooks = this.pjson.oclif.hooks = this.pjson.oclif.hooks || {} + let p = `./${this._ts ? 'lib' : 'src'}/hooks/${this.options.event}/${this.options.name}.js` + if (hooks[this.options.event]) { + hooks[this.options.event] = _.castArray(hooks[this.options.event]) + hooks[this.options.event].push(p) + } else { + this.pjson.oclif.hooks[this.options.event] = p + } + this.fs.writeJSON(this.destinationPath('./package.json'), this.pjson) + } +} + +export = HookGenerator diff --git a/templates/plugin/test/hooks/init.test.js b/templates/plugin/test/hooks/init.test.js deleted file mode 100644 index d90d771ae..000000000 --- a/templates/plugin/test/hooks/init.test.js +++ /dev/null @@ -1,9 +0,0 @@ -const {expect, test} = require('@oclif/test') - -describe('hooks', () => { - test - .stdout() - .hook('init', {id: 'mycommand'}) - .do(output => expect(output.stdout).to.contain('example hook running mycommand')) - .it('shows a message') -}) diff --git a/templates/src/hook.js.ejs b/templates/src/hook.js.ejs new file mode 100644 index 000000000..559c743f0 --- /dev/null +++ b/templates/src/hook.js.ejs @@ -0,0 +1,3 @@ +module.exports = async function (opts) { + process.stdout.write(`example hook running ${opts.id}\n`) +} diff --git a/templates/src/hook.ts.ejs b/templates/src/hook.ts.ejs new file mode 100644 index 000000000..673f56cab --- /dev/null +++ b/templates/src/hook.ts.ejs @@ -0,0 +1,7 @@ +import {Hook} from '@oclif/config' + +const hook: Hook<'<%- options.event %>'> = async function (opts) { + process.stdout.write(`example hook running ${opts.id}\n`) +} + +export default hook diff --git a/templates/test/hook.test.js.ejs b/templates/test/hook.test.js.ejs new file mode 100644 index 000000000..c91b560aa --- /dev/null +++ b/templates/test/hook.test.js.ejs @@ -0,0 +1,9 @@ +const {expect, test} = require('@oclif/test') + +describe('hooks', () => { + test + .stdout() + .hook('init', {id: 'mycommand'}) + .do(output => expect(output.stdout).to.contain('example hook running mycommand')) + .it('shows a message') +}) diff --git a/templates/plugin/test/hooks/init.test.ts b/templates/test/hook.test.ts.ejs similarity index 100% rename from templates/plugin/test/hooks/init.test.ts rename to templates/test/hook.test.ts.ejs diff --git a/test/commands/hook/everything.test.js b/test/commands/hook/everything.test.js new file mode 100644 index 000000000..30c30f3ad --- /dev/null +++ b/test/commands/hook/everything.test.js @@ -0,0 +1 @@ +require('../../run')(module.filename) diff --git a/test/commands/hook/mocha.test.js b/test/commands/hook/mocha.test.js new file mode 100644 index 000000000..30c30f3ad --- /dev/null +++ b/test/commands/hook/mocha.test.js @@ -0,0 +1 @@ +require('../../run')(module.filename) diff --git a/test/run.js b/test/run.js index 484b6b340..4eeb05f2a 100644 --- a/test/run.js +++ b/test/run.js @@ -84,6 +84,15 @@ module.exports = file => { sh.exec('node ./bin/run foo:bar:baz --help') sh.exec('yarn run prepublishOnly') break + case 'hook': + build('plugin', name) + generate('hook myhook --defaults --force') + // TODO: remove this compilation step + sh.exec('tsc') + sh.exec('yarn test') + sh.exec('node ./bin/run hello') + sh.exec('yarn run prepublishOnly') + break } }) .it([cmd, name].join(':')) diff --git a/yarn.lock b/yarn.lock index 82018fe76..5141107e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -114,9 +114,9 @@ "@types/rx" "*" "@types/through" "*" -"@types/lodash@^4.14.105": - version "4.14.105" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.105.tgz#9fcc4627a1f98f8f8fce79ddb2bff4afd97e959b" +"@types/lodash@^4.14.106": + version "4.14.106" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.106.tgz#6093e9a02aa567ddecfe9afadca89e53e5dce4dd" "@types/minimatch@*": version "3.0.3" @@ -2073,9 +2073,9 @@ mkdirp@0.5.1, mkdirp@^0.5.0, mkdirp@^0.5.1: dependencies: minimist "0.0.8" -mocha@^5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.0.4.tgz#6b7aa328472da1088e69d47e75925fd3a3bb63c6" +mocha@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.0.5.tgz#e228e3386b9387a4710007a641f127b00be44b52" dependencies: browser-stdout "1.3.1" commander "2.11.0"