Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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