diff --git a/package.json b/package.json index 566eaec1..10f850be 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "pretest": "sf-compile-test", "test": "sf-test --timeout 600000", "test:deprecation-policy": "./bin/dev snapshot:compare", - "test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel", + "test:nuts": "nyc mocha \"test/**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel", "version": "oclif readme" }, "publishConfig": { diff --git a/src/generators/plugin.ts b/src/generators/plugin.ts index edabc289..d30fd555 100644 --- a/src/generators/plugin.ts +++ b/src/generators/plugin.ts @@ -110,7 +110,11 @@ export default class Plugin extends Generator { ]); const directory = path.resolve(this.answers.name); - exec(`git clone https://github.com/salesforcecli/plugin-template-sf.git ${directory}`); + + const templateRepo = this.answers.internal + ? 'git clone https://github.com/salesforcecli/plugin-template-sf.git' + : 'git clone https://github.com/salesforcecli/plugin-template-sf-external.git'; + exec(`${templateRepo} ${directory}`); try { fs.rmSync(`${path.resolve(this.answers.name, '.git')}`, { recursive: true }); } catch { @@ -153,38 +157,7 @@ export default class Plugin extends Generator { const final = Object.assign({}, pjson, updated); - if (!this.answers.internal) { - // If we are building a 3PP plugin, we don't want to set defaults for these properties. - // We could ask these questions in the prompt, but that would be too many questions for a good UX. - // We want developers to be able to quickly get up and running with their plugin. - delete final.homepage; - delete final.repository; - delete final.bugs; - - // 3PP plugins don't need these tests. - delete final.scripts['test:json-schema']; - delete final.scripts['test:deprecation-policy']; - delete final.scripts['test:command-reference']; - final.scripts.posttest = 'yarn lint'; - - // 3PP plugins don't need these either. - // Can't use the class's this.fs since it doesn't delete the directory, just the files in it. - fs.rmSync(this.destinationPath('./schemas'), { recursive: true }); - fs.rmSync(this.destinationPath('./.git2gus'), { recursive: true }); - fs.rmSync(this.destinationPath('./.github'), { recursive: true }); - fs.rmSync(this.destinationPath('./command-snapshot.json')); - fs.rmSync(this.destinationPath('./CODE_OF_CONDUCT.md')); - fs.rmSync(this.destinationPath('./SECURITY.md')); - - // Remove /schemas from the published files. - final.files = final.files.filter((f) => f !== '/schemas'); - - this.fs.delete(this.destinationPath('./.circleci/config.yml')); - this.fs.copy( - this.destinationPath('./.circleci/external.config.yml'), - this.destinationPath('./.circleci/config.yml') - ); - + if (!this.answers.internal && this.answers.codeCoverage) { const nycConfig = readJson(path.join(this.env.cwd, '.nycrc')); const codeCoverage = Number.parseInt(this.answers.codeCoverage.replace('%', ''), 10); nycConfig['check-coverage'] = true; @@ -192,23 +165,7 @@ export default class Plugin extends Generator { nycConfig.statements = codeCoverage; nycConfig.functions = codeCoverage; nycConfig.branches = codeCoverage; - delete nycConfig.extends; - this.fs.writeJSON(this.destinationPath('.nycrc'), nycConfig); - - // Remove the eslint-config-salesforce-internal from eslint config. - replace.sync({ - files: `${this.env.cwd}/.eslintrc.js`, - from: /'eslint-config-salesforce-license',\s/g, - to: '', - }); - - // Remove the copyright header from the generated files. - replace.sync({ - files: `${this.env.cwd}/**/*`, - from: /\/\*\n\s\*\sCopyright([\S\s]*?)\s\*\/\n\n/g, - to: '', - }); } this.fs.delete(this.destinationPath('./.circleci/external.config.yml')); @@ -217,7 +174,7 @@ export default class Plugin extends Generator { replace.sync({ files: `${this.env.cwd}/**/*`, - from: this.answers.internal ? /plugin-template-sf/g : /@salesforce\/plugin-template-sf/g, + from: this.answers.internal ? /plugin-template-sf/g : /plugin-template-sf-external/g, to: this.answers.name, }); } @@ -226,9 +183,10 @@ export default class Plugin extends Generator { exec('git init', { cwd: this.env.cwd }); exec('yarn', { cwd: this.env.cwd }); exec('yarn build', { cwd: this.env.cwd }); - // Run yarn install in case dev-scripts detected changes during yarn build. - exec('yarn install', { cwd: this.env.cwd }); + if (this.answers.internal) { + // Run yarn install in case dev-scripts detected changes during yarn build. + exec('yarn install', { cwd: this.env.cwd }); exec(`${path.join(path.resolve(this.env.cwd), 'bin', 'dev')} schema generate`, { cwd: this.env.cwd }); } } diff --git a/test/commands/dev/generate/command.nut.ts b/test/commands/dev/generate/command.nut.ts index 033f92dc..106e8d95 100644 --- a/test/commands/dev/generate/command.nut.ts +++ b/test/commands/dev/generate/command.nut.ts @@ -13,76 +13,169 @@ import { exec } from 'shelljs'; import { PackageJson } from '../../../../src/types'; import { readJson, fileExists } from '../../../../src/util'; +async function setup(repo: string): Promise { + env.setString('TESTKIT_EXECUTABLE_PATH', path.join(process.cwd(), 'bin', 'dev')); + const session = await TestSession.create({ + project: { + gitClone: repo, + }, + }); + exec('yarn', { cwd: session.project.dir, silent: true }); + exec('yarn build', { cwd: session.project.dir, silent: true }); + return session; +} + describe('dev generate command NUTs', () => { let session: TestSession; let pluginExecutable: string; - before(async () => { - env.setString('TESTKIT_EXECUTABLE_PATH', path.join(process.cwd(), 'bin', 'dev')); - session = await TestSession.create({ - project: { - gitClone: 'https://github.com/salesforcecli/plugin-template-sf.git', - }, + describe('2PP', () => { + before(async () => { + session = await setup('https://github.com/salesforcecli/plugin-template-sf.git'); + pluginExecutable = path.join(session.project.dir, 'bin', 'dev'); }); - pluginExecutable = path.join(session.project.dir, 'bin', 'dev'); - execCmd('yarn', { cwd: session.project.dir }); - execCmd('yarn build', { cwd: session.project.dir }); - }); - after(async () => { - await session?.clean(); - }); + after(async () => { + await session?.clean(); + }); - describe('generated command', () => { - const name = 'do:awesome:stuff'; - const command = `dev generate command --name ${name} --force --nuts --unit`; + describe('generated command', () => { + const name = 'do:awesome:stuff'; + const command = `dev generate command --name ${name} --force --nuts --unit`; - before(async () => { - execCmd(command, { ensureExitCode: 0, cli: 'sf', cwd: session.project.dir }); - }); + before(async () => { + execCmd(command, { ensureExitCode: 0, cli: 'sf', cwd: session.project.dir }); + }); - it('should generate a command that can be executed', () => { - const result = exec(`${pluginExecutable} do awesome stuff --name Astro`, { silent: true }); - expect(result.code).to.equal(0); - expect(result.stdout).to.contain('hello Astro'); - }); + it('should generate a command that can be executed', () => { + const result = exec(`${pluginExecutable} do awesome stuff --name Astro`, { silent: true }); + expect(result.code).to.equal(0); + expect(result.stdout).to.contain('hello Astro'); + }); - it('should generate a markdown message file', async () => { - const messagesFile = path.join(session.project.dir, 'messages', `${name.replace(/:/g, '.')}.md`); - expect(fileExists(messagesFile)).to.be.true; + it('should generate a markdown message file', async () => { + const messagesFile = path.join(session.project.dir, 'messages', `${name.replace(/:/g, '.')}.md`); + expect(fileExists(messagesFile)).to.be.true; + }); + + it('should generate a passing NUT', async () => { + const parts = name.split(':'); + const cmd = parts.pop(); + const nutFile = path.join(session.project.dir, 'test', 'commands', ...parts, `${cmd}.nut.ts`); + expect(fileExists(nutFile)).to.be.true; + + const result = exec('yarn test:nuts', { cwd: session.project.dir, silent: true }); + expect(result.code).to.equal(0); + expect(result.stdout).include(`${name.replace(/:/g, ' ')} NUTs`); + }); + + it('should generate a passing unit test', async () => { + const parts = name.split(':'); + const cmd = parts.pop(); + const unitTestFile = path.join(session.project.dir, 'test', 'commands', ...parts, `${cmd}.test.ts`); + expect(fileExists(unitTestFile)).to.be.true; + + const result = exec('yarn test', { cwd: session.project.dir, silent: true }); + expect(result.code).to.equal(0); + expect(result.stdout).include(name.replace(/:/g, ' ')); + }); + + it('should add new topics in package.json', async () => { + const packageJson = readJson(path.join(session.project.dir, 'package.json')); + expect(packageJson.oclif.topics.do).to.deep.equal({ + description: 'description for do', + subtopics: { + awesome: { + description: 'description for do.awesome', + }, + }, + }); + }); }); - it('should generate a passing NUT', async () => { - const parts = name.split(':'); - const cmd = parts.pop(); - const nutFile = path.join(session.project.dir, 'test', 'commands', ...parts, `${cmd}.nut.ts`); - expect(fileExists(nutFile)).to.be.true; + describe('generated command under existing topic', () => { + const name = 'deploy:awesome:stuff'; + const command = `dev generate command --name ${name} --force --nuts --unit`; - const result = exec('yarn test:nuts', { cwd: session.project.dir, silent: true }); - expect(result.code).to.equal(0); - expect(result.stdout).include(`${name.replace(/:/g, ' ')} NUTs`); + before(async () => { + execCmd(command, { ensureExitCode: 0, cli: 'sf', cwd: session.project.dir }); + }); + + it('should add new topics in package.json', async () => { + const packageJson = readJson(path.join(session.project.dir, 'package.json')); + expect(packageJson.oclif.topics.deploy).to.deep.equal({ + external: true, + subtopics: { + awesome: { + description: 'description for deploy.awesome', + }, + }, + }); + }); }); + }); - it('should generate a passing unit test', async () => { - const parts = name.split(':'); - const cmd = parts.pop(); - const unitTestFile = path.join(session.project.dir, 'test', 'commands', ...parts, `${cmd}.test.ts`); - expect(fileExists(unitTestFile)).to.be.true; + describe('3PP', () => { + before(async () => { + session = await setup('https://github.com/salesforcecli/plugin-template-sf-external.git'); + pluginExecutable = path.join(session.project.dir, 'bin', 'dev'); + }); - const result = exec('yarn test', { cwd: session.project.dir, silent: true }); - expect(result.code).to.equal(0); - expect(result.stdout).include(name.replace(/:/g, ' ')); + after(async () => { + await session?.clean(); }); - it('should update topics in package.json', async () => { - const packageJson = readJson(path.join(session.project.dir, 'package.json')); - expect(packageJson.oclif.topics.do).to.deep.equal({ - description: 'description for do', - subtopics: { - awesome: { - description: 'description for do.awesome', + describe('generated command', () => { + const name = 'do:awesome:stuff'; + const command = `dev generate command --name ${name} --force --nuts --unit`; + + before(async () => { + execCmd(command, { ensureExitCode: 0, cli: 'sf', cwd: session.project.dir }); + }); + + it('should generate a command that can be executed', () => { + const result = exec(`${pluginExecutable} do awesome stuff --name Astro`, { silent: true }); + expect(result.code).to.equal(0); + expect(result.stdout).to.contain('hello Astro'); + }); + + it('should generate a markdown message file', async () => { + const messagesFile = path.join(session.project.dir, 'messages', `${name.replace(/:/g, '.')}.md`); + expect(fileExists(messagesFile)).to.be.true; + }); + + it('should generate a passing NUT', async () => { + const parts = name.split(':'); + const cmd = parts.pop(); + const nutFile = path.join(session.project.dir, 'test', 'commands', ...parts, `${cmd}.nut.ts`); + expect(fileExists(nutFile)).to.be.true; + + const result = exec('yarn test:nuts', { cwd: session.project.dir, silent: true }); + expect(result.code).to.equal(0); + expect(result.stdout).include(`${name.replace(/:/g, ' ')} NUTs`); + }); + + it('should generate a passing unit test', async () => { + const parts = name.split(':'); + const cmd = parts.pop(); + const unitTestFile = path.join(session.project.dir, 'test', 'commands', ...parts, `${cmd}.test.ts`); + expect(fileExists(unitTestFile)).to.be.true; + + const result = exec('yarn test', { cwd: session.project.dir, silent: true }); + expect(result.code).to.equal(0); + expect(result.stdout).include(name.replace(/:/g, ' ')); + }); + + it('should add new topics in package.json', async () => { + const packageJson = readJson(path.join(session.project.dir, 'package.json')); + expect(packageJson.oclif.topics.do).to.deep.equal({ + description: 'description for do', + subtopics: { + awesome: { + description: 'description for do.awesome', + }, }, - }, + }); }); }); }); diff --git a/test/commands/dev/generate/plugin.test.ts b/test/commands/dev/generate/plugin.test.ts index 64b7b43a..451955bc 100644 --- a/test/commands/dev/generate/plugin.test.ts +++ b/test/commands/dev/generate/plugin.test.ts @@ -12,9 +12,7 @@ import { readJson } from '../../../../src/util'; import { PackageJson } from '../../../../src/types'; describe('dev generate plugin', () => { - // This test fails because the generator fails to remove the copyright headers on windows. - // Once we move to a separate 3PP template, this will no longer be a problem. - (process.platform !== 'win32' ? it : it.skip)('should generate a 3PP plugin', async () => { + it('should generate a 3PP plugin', async () => { const runResult = await helpers .run(path.join(__dirname, '..', '..', '..', '..', 'src', 'generators', 'plugin.ts')) .withPrompts({ @@ -28,6 +26,7 @@ describe('dev generate plugin', () => { runResult.assertFile(path.join(runResult.cwd, 'my-plugin', 'package.json')); runResult.assertFile(path.join(runResult.cwd, 'my-plugin', 'src', 'commands', 'hello', 'world.ts')); runResult.assertNoFile(path.join(runResult.cwd, 'my-plugin', 'CODE_OF_CONDUCT.md')); + runResult.assertNoFile(path.join(runResult.cwd, 'my-plugin', 'LICENSE.txt')); runResult.assertNoFile(path.join(runResult.cwd, 'my-plugin', 'command-snapshot.json')); runResult.assertNoFile(path.join(runResult.cwd, 'my-plugin', 'schemas', 'hello-world.json')); runResult.assertNoFile(path.join(runResult.cwd, 'my-plugin', '.git2gus', 'config.json')); @@ -38,12 +37,11 @@ describe('dev generate plugin', () => { expect(packageJson.description).to.equal('my plugin description'); const scripts = Object.keys(packageJson.scripts); - const keys = Object.keys(packageJson); - expect(scripts).to.not.include('test:json-schema'); expect(scripts).to.not.include('test:deprecation-policy'); expect(scripts).to.not.include('test:command-reference'); - expect(packageJson.scripts.posttest).to.equal('yarn lint'); + + const keys = Object.keys(packageJson); expect(keys).to.not.include('homepage'); expect(keys).to.not.include('repository'); expect(keys).to.not.include('bugs'); @@ -74,6 +72,9 @@ describe('dev generate plugin', () => { runResult.assertFile(path.join(runResult.cwd, 'plugin-test', 'CODE_OF_CONDUCT.md')); runResult.assertFile(path.join(runResult.cwd, 'plugin-test', 'command-snapshot.json')); runResult.assertFile(path.join(runResult.cwd, 'plugin-test', 'schemas', 'hello-world.json')); + runResult.assertFile(path.join(runResult.cwd, 'plugin-test', 'schemas', 'hooks', 'sf-env-list.json')); + runResult.assertFile(path.join(runResult.cwd, 'plugin-test', 'schemas', 'hooks', 'sf-env-display.json')); + runResult.assertFile(path.join(runResult.cwd, 'plugin-test', 'schemas', 'hooks', 'sf-deploy.json')); runResult.assertFile(path.join(runResult.cwd, 'plugin-test', '.git2gus', 'config.json')); runResult.assertFile(path.join(runResult.cwd, 'plugin-test', 'src', 'hooks', 'envList.ts')); @@ -85,6 +86,10 @@ describe('dev generate plugin', () => { expect(packageJson.name).to.equal('@salesforce/plugin-test'); expect(packageJson.author).to.equal('Salesforce'); expect(packageJson.description).to.equal('my plugin description'); + expect(packageJson.bugs).to.equal('https://github.com/forcedotcom/cli/issues'); + expect(packageJson.repository).to.equal('salesforcecli/plugin-test'); + expect(packageJson.homepage).to.equal('https://github.com/salesforcecli/plugin-test'); + expect(packageJson.oclif.hooks).to.deep.equal({ 'sf:env:list': './lib/hooks/envList', 'sf:env:display': './lib/hooks/envDisplay', @@ -93,14 +98,9 @@ describe('dev generate plugin', () => { }); const scripts = Object.keys(packageJson.scripts); - const keys = Object.keys(packageJson); - expect(scripts).to.include('test:json-schema'); expect(scripts).to.include('test:deprecation-policy'); expect(scripts).to.include('test:command-reference'); - expect(keys).to.include('homepage'); - expect(keys).to.include('repository'); - expect(keys).to.include('bugs'); runResult.assertFileContent( path.join(runResult.cwd, 'plugin-test', 'src', 'commands', 'hello', 'world.ts'),