diff --git a/messages/dev.generate.command.md b/messages/dev.generate.command.md index 43364176..8dd9eb90 100644 --- a/messages/dev.generate.command.md +++ b/messages/dev.generate.command.md @@ -18,10 +18,10 @@ Generate a NUT test file for the command. Generate a unit test file for the command. -# args.name.description +# flags.name.description Name of the new command. Must be separated by colons. # examples -- <%= config.bin %> <%= command.id %> my:command +- <%= config.bin %> <%= command.id %> --name my:command diff --git a/messages/dev.generate.hook.md b/messages/dev.generate.hook.md index 7cc4dc4a..dfada33d 100644 --- a/messages/dev.generate.hook.md +++ b/messages/dev.generate.hook.md @@ -16,4 +16,4 @@ Event to run hook on. # examples -- <%= config.bin %> <%= command.id %> my:command +- <%= config.bin %> <%= command.id %> --event sf:env:display diff --git a/messages/dev.generate.plugin.md b/messages/dev.generate.plugin.md index ad6cf39d..8797cc4c 100644 --- a/messages/dev.generate.plugin.md +++ b/messages/dev.generate.plugin.md @@ -6,14 +6,6 @@ Generate a new sf plugin. This will clone the template repo 'salesforcecli/plugin-template-sf' and update package properties -# args.name.description - -Directory name of new plugin. Must begin with "plugin-" - # examples -- <%= config.bin %> <%= command.id %> plugin-awesome - -# error.InvalidPluginName - -%s must begin with "plugin-" +- <%= config.bin %> <%= command.id %> diff --git a/src/commands/dev/generate/command.ts b/src/commands/dev/generate/command.ts index c440d232..eada2565 100644 --- a/src/commands/dev/generate/command.ts +++ b/src/commands/dev/generate/command.ts @@ -6,17 +6,23 @@ */ import { Flags } from '@oclif/core'; import { Messages } from '@salesforce/core'; -import { GeneratorCommand } from '../../../generatorCommand'; +import { SfCommand } from '@salesforce/sf-plugins-core'; +import { generate } from '../../../util'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-dev', 'dev.generate.command'); -export default class GenerateCommand extends GeneratorCommand { +export default class GenerateCommand extends SfCommand { + public static enableJsonFlag = false; public static summary = messages.getMessage('summary'); public static description = messages.getMessage('description'); public static examples = messages.getMessages('examples'); public static flags = { + name: Flags.string({ + required: true, + description: messages.getMessage('flags.name.description'), + }), force: Flags.boolean({ description: messages.getMessage('flags.force.description'), }), @@ -32,12 +38,10 @@ export default class GenerateCommand extends GeneratorCommand { }), }; - public static args = [{ name: 'name', required: true, description: messages.getMessage('args.name.description') }]; - public async run(): Promise { - const { args, flags } = await this.parse(GenerateCommand); - await super.generate('command', { - name: args.name, + const { flags } = await this.parse(GenerateCommand); + generate('command', { + name: flags.name, force: flags.force, nuts: flags.nuts, unit: flags.unit, diff --git a/src/commands/dev/generate/hook.ts b/src/commands/dev/generate/hook.ts index 4c7a98c9..c5fde021 100644 --- a/src/commands/dev/generate/hook.ts +++ b/src/commands/dev/generate/hook.ts @@ -6,12 +6,15 @@ */ import { Flags } from '@oclif/core'; import { Messages } from '@salesforce/core'; -import { GeneratorCommand } from '../../../generatorCommand'; +import { SfCommand } from '@salesforce/sf-plugins-core'; +import { generate } from '../../../util'; +import { Hook } from '../../../types'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-dev', 'dev.generate.hook'); -export default class GenerateHook extends GeneratorCommand { +export default class GenerateHook extends SfCommand { + public static enableJsonFlag = false; public static summary = messages.getMessage('summary'); public static description = messages.getMessage('description'); public static examples = messages.getMessages('examples'); @@ -22,14 +25,14 @@ export default class GenerateHook extends GeneratorCommand { }), event: Flags.string({ description: messages.getMessage('flags.event.description'), - options: ['sf:env:display', 'sf:env:list', 'sf:deploy', 'sf:logout'], + options: Object.keys(Hook), required: true, }), }; public async run(): Promise { const { flags } = await this.parse(GenerateHook); - await super.generate('hook', { + generate('hook', { force: flags.force, event: flags.event, }); diff --git a/src/commands/dev/generate/plugin.ts b/src/commands/dev/generate/plugin.ts index c27c0bd1..3f3ee42e 100644 --- a/src/commands/dev/generate/plugin.ts +++ b/src/commands/dev/generate/plugin.ts @@ -5,31 +5,22 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { Messages } from '@salesforce/core'; -import { GeneratorCommand } from '../../../generatorCommand'; +import { SfCommand } from '@salesforce/sf-plugins-core'; +import { generate } from '../../../util'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-dev', 'dev.generate.plugin'); -export default class GeneratePlugin extends GeneratorCommand { +export default class GeneratePlugin extends SfCommand { + public static enableJsonFlag = false; public static summary = messages.getMessage('summary'); public static description = messages.getMessage('description'); public static examples = messages.getMessages('examples'); public static flags = {}; - public static args = [{ name: 'name', required: true, description: messages.getMessage('args.name.description') }]; - + // eslint-disable-next-line @typescript-eslint/require-await public async run(): Promise { - const { args } = await this.parse(GeneratePlugin); - const pluginName = args.name as string; - - if (!pluginName.startsWith('plugin-')) { - throw messages.createError('error.InvalidPluginName', [pluginName]); - } - - await super.generate('plugin', { - name: pluginName, - force: true, - }); + generate('plugin', { force: true }); } } diff --git a/src/generatorCommand.ts b/src/generatorCommand.ts deleted file mode 100644 index 4b87eee8..00000000 --- a/src/generatorCommand.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2022, salesforce.com, inc. - * All rights reserved. - * Licensed under the BSD 3-Clause license. - * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause - */ -import { SfCommand } from '@salesforce/sf-plugins-core'; -import { createEnv } from 'yeoman-environment'; - -/* eslint-disable @typescript-eslint/await-thenable */ -/* eslint-disable @typescript-eslint/ban-ts-comment */ - -export abstract class GeneratorCommand extends SfCommand { - public static enableJsonFlag = false; - // eslint-disable-next-line @typescript-eslint/require-await - protected async generate(type: string, generatorOptions: Record = {}): Promise { - const env = createEnv(); - env.register(require.resolve(`./generators/${type}`), `sf:${type}`); - // @ts-ignore - await env.run(`sf:${type}`, generatorOptions); - } -} diff --git a/src/generators/hook.ts b/src/generators/hook.ts index 52e76d6b..a54f77de 100644 --- a/src/generators/hook.ts +++ b/src/generators/hook.ts @@ -9,31 +9,28 @@ import * as path from 'path'; import { camelCase } from 'change-case'; import * as Generator from 'yeoman-generator'; import yosay = require('yosay'); -import { PackageJson } from '../types'; +import { Hook, PackageJson } from '../types'; +import { addHookToPackageJson, readJson } from '../util'; // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment const { version } = require('../../package.json'); -export interface CommandGeneratorOptions extends Generator.GeneratorOptions { +export interface HookGeneratorOptions extends Generator.GeneratorOptions { name: string; event: string; } -function toArray(item: string | string[]): string[] { - return Array.isArray(item) ? item : [item]; -} - -export default class Command extends Generator { - public options: CommandGeneratorOptions; +export default class HookGenerator extends Generator { + public options: HookGeneratorOptions; public pjson!: PackageJson; - public constructor(args: string | string[], opts: CommandGeneratorOptions) { + public constructor(args: string | string[], opts: HookGeneratorOptions) { super(args, opts); } // eslint-disable-next-line @typescript-eslint/require-await public async prompting(): Promise { - this.pjson = this.fs.readJSON('package.json') as unknown as PackageJson; + this.pjson = readJson(path.join(this.env.cwd, 'package.json')); this.log(yosay(`Adding a ${this.options.event} hook to ${this.pjson.name} Version: ${version as string}`)); } @@ -46,22 +43,7 @@ export default class Command extends Generator { { year: new Date().getFullYear() } ); - // TODO - // this.fs.copyTpl( - // this.templatePath('test/hook.test.ts.ejs'), - // this.destinationPath(`test/hooks/${this.options.event}/${this.options.name}.test.ts`), - // this - // ); - - this.pjson.oclif.hooks = this.pjson.oclif.hooks || {}; - const hooks = this.pjson.oclif.hooks; - const p = `./lib/hooks/${filename}`; - if (hooks[this.options.event]) { - hooks[this.options.event] = [...toArray(hooks[this.options.event]), p]; - } else { - this.pjson.oclif.hooks[this.options.event] = p; - } - + this.pjson = addHookToPackageJson(this.options.event as Hook, filename, this.pjson); this.fs.writeJSON(this.destinationPath('./package.json'), this.pjson); } } diff --git a/src/generators/plugin.ts b/src/generators/plugin.ts index 9fa25336..949381be 100644 --- a/src/generators/plugin.ts +++ b/src/generators/plugin.ts @@ -11,53 +11,77 @@ import * as Generator from 'yeoman-generator'; import yosay = require('yosay'); import { exec } from 'shelljs'; import replace = require('replace-in-file'); -import { PackageJson } from '../types'; +import { camelCase } from 'change-case'; +import { Hook, PackageJson } from '../types'; +import { addHookToPackageJson, readJson } from '../util'; // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment const { version } = require('../../package.json'); -export interface PluginGeneratorOptions extends Generator.GeneratorOptions { - name: string; -} - export default class Plugin extends Generator { - private name: string; private answers!: { + name: string; description: string; + hooks: Hook[]; }; - public constructor(args: string | string[], opts: PluginGeneratorOptions) { + public constructor(args: string | string[], opts: Generator.GeneratorOptions) { super(args, opts); - this.name = opts.name; this.env.options.nodePackageManager = 'yarn'; } public async prompting(): Promise { const msg = 'Time to build an sf plugin!'; - const directory = path.resolve(this.name); this.log(yosay(`${msg} Version: ${version as string}`)); - exec(`git clone git@github.com:salesforcecli/plugin-template-sf.git ${directory}`); - fs.rmSync(`${path.resolve(this.name, '.git')}`, { recursive: true }); - this.destinationRoot(directory); - this.env.cwd = this.destinationPath(); - this.answers = await this.prompt<{ description: string }>([ + this.answers = await this.prompt<{ name: string; description: string; hooks: Hook[] }>([ + { + type: 'input', + name: 'name', + message: 'Name (must start with plugin-)', + validate: (input: string): boolean => /plugin-[a-z]+$/.test(input), + }, { type: 'input', name: 'description', - message: 'description', + message: 'Description', + }, + { + type: 'checkbox', + name: 'hooks', + message: 'Which commands do you plan to extend', + choices: Object.values(Hook), }, ]); + + const directory = path.resolve(this.answers.name); + exec(`git clone git@github.com:salesforcecli/plugin-template-sf.git ${directory}`); + fs.rmSync(`${path.resolve(this.answers.name, '.git')}`, { recursive: true }); + this.destinationRoot(directory); + this.env.cwd = this.destinationPath(); } public writing(): void { - const pjsonRaw = fs.readFileSync(path.join(this.env.cwd, 'package.json'), 'utf-8'); - const pjson = JSON.parse(pjsonRaw) as PackageJson; + let pjson = readJson(path.join(this.env.cwd, 'package.json')); + + this.sourceRoot(path.join(__dirname, '../../templates')); + const hooks = this.answers.hooks.map((h) => h.split(' ').join(':')) as Hook[]; + for (const hook of hooks) { + const filename = camelCase(hook.replace('sf:', '')); + this.fs.copyTpl( + this.templatePath(`src/hooks/${hook.replace(/:/g, '.')}.ts.ejs`), + this.destinationPath(`src/hooks/${filename}.ts`), + { year: new Date().getFullYear() } + ); + + pjson = addHookToPackageJson(hook, filename, pjson); + } + const updated = { - name: `@salesforce/${this.name}`, - repository: `salesforcecli/${this.name}`, - homepage: `https://github.com/salesforcecli/${this.name}`, + name: `@salesforce/${this.answers.name}`, + repository: `salesforcecli/${this.answers.name}`, + homepage: `https://github.com/salesforcecli/${this.answers.name}`, description: this.answers.description, }; const final = Object.assign({}, pjson, updated); @@ -66,7 +90,11 @@ export default class Plugin extends Generator { replace.sync({ files: `${this.env.cwd}/**/*`, from: /plugin-template-sf/g, - to: this.name, + to: this.answers.name, }); } + + public end(): void { + exec(`${path.join(path.resolve(this.env.cwd), 'bin', 'dev')} schema generate`, { cwd: this.env.cwd }); + } } diff --git a/src/types.ts b/src/types.ts index 263188c4..de4ffaa8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,3 +17,10 @@ export interface PackageJson { repository: string; homepage: string; } + +export enum Hook { + 'sf:env:list' = 'sf env list', + 'sf:env:display' = 'sf env display', + 'sf:deploy' = 'sf deploy', + 'sf:logout' = 'sf logout', +} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 00000000..43e13a37 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +import * as fs from 'fs'; +import { createEnv } from 'yeoman-environment'; +import { Hook, PackageJson } from './types'; + +/* eslint-disable @typescript-eslint/ban-ts-comment */ + +export function generate(type: string, generatorOptions: Record = {}): void { + const env = createEnv(); + env.register(require.resolve(`./generators/${type}`), `sf:${type}`); + // @ts-ignore + env.run(`sf:${type}`, generatorOptions); +} + +export function readJson(filePath: string): T { + const pjsonRaw = fs.readFileSync(filePath, 'utf-8'); + return JSON.parse(pjsonRaw) as T; +} + +export function toArray(item: string | string[]): string[] { + return Array.isArray(item) ? item : [item]; +} + +export function addHookToPackageJson(hook: Hook, filename: string, pjson: PackageJson): PackageJson { + pjson.oclif.hooks = pjson.oclif.hooks || {}; + const p = `./lib/hooks/${filename}`; + if (pjson.oclif.hooks[hook]) { + pjson.oclif.hooks[hook] = [...toArray(pjson.oclif.hooks[hook]), p]; + } else { + pjson.oclif.hooks[hook] = p; + } + return pjson; +} diff --git a/templates/test/hook.test.ts.ejs b/templates/test/hook.test.ts.ejs deleted file mode 100644 index 28fb6c93..00000000 --- a/templates/test/hook.test.ts.ejs +++ /dev/null @@ -1,9 +0,0 @@ -import {expect, test} from '@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') -})