Skip to content

Commit

Permalink
feat: use external template for 3PP plugin generator (#143)
Browse files Browse the repository at this point in the history
* feat: use external template for 3PP plugin generator

* chore: more tests

* chore: more tests

* fix: add back nyc block
  • Loading branch information
mdonnalley authored Sep 20, 2022
1 parent 05c60cd commit f002e6a
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 115 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
62 changes: 10 additions & 52 deletions src/generators/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -153,62 +157,15 @@ 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<NYC>(path.join(this.env.cwd, '.nycrc'));
const codeCoverage = Number.parseInt(this.answers.codeCoverage.replace('%', ''), 10);
nycConfig['check-coverage'] = true;
nycConfig.lines = codeCoverage;
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'));
Expand All @@ -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,
});
}
Expand All @@ -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 });
}
}
Expand Down
195 changes: 144 additions & 51 deletions test/commands/dev/generate/command.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestSession> {
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<PackageJson>(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<PackageJson>(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<PackageJson>(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<PackageJson>(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',
},
},
},
});
});
});
});
Expand Down
22 changes: 11 additions & 11 deletions test/commands/dev/generate/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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'));
Expand All @@ -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');
Expand Down Expand Up @@ -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'));
Expand All @@ -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',
Expand All @@ -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'),
Expand Down

0 comments on commit f002e6a

Please sign in to comment.