Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: make plugin generator 3PP friendly #104

Merged
merged 7 commits into from
Aug 30, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@salesforce/kit": "^1.6.0",
"@salesforce/sf-plugins-core": "^1.13.0",
"change-case": "^4.1.2",
"got": "^11.8.5",
"replace-in-file": "^6.3.2",
"shelljs": "^0.8.5",
"tslib": "^2",
Expand Down
52 changes: 51 additions & 1 deletion src/generators/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
import * as path from 'path';
import * as Generator from 'yeoman-generator';
import yosay = require('yosay');
import got from 'got';
import { pascalCase } from 'change-case';
import { PackageJson } from '../types';
import { set } from '@salesforce/kit';
import { get } from '@salesforce/ts-types';
import { PackageJson, Topic } from '../types';

// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-assignment
const { version } = require('../../package.json');
Expand All @@ -20,6 +23,40 @@ export interface CommandGeneratorOptions extends Generator.GeneratorOptions {
unit: boolean;
}

export function addTopics(newCmd: string, commands: string[] = []): Record<string, Topic> {
const updated: Record<string, Topic> = {};

const paths: string[] = [];
const parts = newCmd.split(':').slice(0, -1);
while (parts.length > 0) {
const name = parts.join('.');
if (name) paths.push(name);
parts.pop();
}

for (const p of paths) {
const isExternal = commands.includes(p);
const existing = get(updated, p);
if (existing) {
const merged = isExternal
? {
external: true,
subtopics: existing,
}
: {
description: `description for ${p}`,
subtopics: existing,
};
set(updated, p, merged);
} else {
const entry = isExternal ? { external: true } : { description: `description for ${p}` };
set(updated, p, entry);
}
}

return updated;
}

export default class Command extends Generator {
public declare options: CommandGeneratorOptions;
public pjson!: PackageJson;
Expand All @@ -32,6 +69,19 @@ export default class Command extends Generator {
public async prompting(): Promise<void> {
this.pjson = this.fs.readJSON('package.json') as unknown as PackageJson;
this.log(yosay(`Adding a command to ${this.pjson.name} Version: ${version as string}`));

if (this.pjson.scripts['test:command-reference']) {
const commandSnapshotUrl = 'https://raw.githubusercontent.com/salesforcecli/cli/main/command-snapshot.json';
const commandSnapshot = await got(commandSnapshotUrl).json<Array<{ command: string }>>();
const commands = commandSnapshot.map((c) => c.command.replace(/:/g, '.'));
const newTopics = addTopics(this.options.name, commands);
this.pjson.oclif.topics = { ...this.pjson.oclif.topics, ...newTopics };
} else {
const newTopics = addTopics(this.options.name);
this.pjson.oclif.topics = { ...this.pjson.oclif.topics, ...newTopics };
}

this.fs.writeJSON('package.json', this.pjson);
}

public writing(): void {
Expand Down
125 changes: 109 additions & 16 deletions src/generators/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,24 @@ import yosay = require('yosay');
import { exec } from 'shelljs';
import replace = require('replace-in-file');
import { camelCase } from 'change-case';
import { Hook, PackageJson } from '../types';
import { Hook, NYC, 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');

type PluginAnswers = {
internal: boolean;
name: string;
description: string;
hooks?: Hook[];
author?: string;
codeCoverage?: string;
};

export default class Plugin extends Generator {
private answers!: {
name: string;
description: string;
hooks: Hook[];
};
private answers!: PluginAnswers;
private githubUsername!: string;

public constructor(args: string | string[], opts: Generator.GeneratorOptions) {
super(args, opts);
Expand All @@ -34,24 +40,54 @@ export default class Plugin extends Generator {
const msg = 'Time to build an sf plugin!';

this.log(yosay(`${msg} Version: ${version as string}`));
this.githubUsername = await this.user.github.username();

this.answers = await this.prompt<{ name: string; description: string; hooks: Hook[] }>([
this.answers = await this.prompt<PluginAnswers>([
{
type: 'confirm',
name: 'internal',
message: 'Are you building a plugin for an internal Salesforce team?',
},
{
type: 'input',
name: 'name',
message: 'Name (must start with plugin-)',
validate: (input: string): boolean => /plugin-[a-z]+$/.test(input),
when: (answers: { internal: boolean }): boolean => answers.internal,
},
{
type: 'input',
name: 'name',
message: 'Name',
validate: (input: string): boolean => Boolean(input),
when: (answers: { internal: boolean }): boolean => !answers.internal,
},
{
type: 'input',
name: 'description',
message: 'Description',
},
{
type: 'input',
name: 'author',
message: 'author',
default: this.githubUsername,
when: (answers: { internal: boolean }): boolean => !answers.internal,
},
{
type: 'list',
name: 'codeCoverage',
message: 'What % code coverage do you want to enforce',
default: '50%',
choices: ['0%', '25%', '50%', '75%', '90%', '100%'],
when: (answers: { internal: boolean }): boolean => !answers.internal,
},
{
type: 'checkbox',
name: 'hooks',
message: 'Which commands do you plan to extend',
choices: Object.values(Hook),
when: (answers: { internal: boolean }): boolean => answers.internal,
},
]);

Expand All @@ -66,7 +102,7 @@ export default class Plugin extends Generator {
let pjson = readJson<PackageJson>(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[];
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(
Expand All @@ -78,24 +114,81 @@ export default class Plugin extends Generator {
pjson = addHookToPackageJson(hook, filename, pjson);
}

const updated = {
name: `@salesforce/${this.answers.name}`,
repository: `salesforcecli/${this.answers.name}`,
homepage: `https://github.com/salesforcecli/${this.answers.name}`,
description: this.answers.description,
};
const updated: Partial<PackageJson> = this.answers.internal
? {
name: `@salesforce/${this.answers.name}`,
repository: `salesforcecli/${this.answers.name}`,
homepage: `https://github.com/salesforcecli/${this.answers.name}`,
description: this.answers.description,
}
: {
name: this.answers.name,
description: this.answers.description,
};

if (this.answers.author) updated.author = this.answers.author;

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'));

// 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')
);

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;

this.fs.writeJSON(this.destinationPath('.nycrc'), nycConfig);
}

this.fs.delete(this.destinationPath('./.circleci/external.config.yml'));

this.fs.writeJSON(this.destinationPath('./package.json'), final);

replace.sync({
files: `${this.env.cwd}/**/*`,
from: /plugin-template-sf/g,
from: this.answers.internal ? /plugin-template-sf/g : /@salesforce\/plugin-template-sf/g,
to: this.answers.name,
});
}

public end(): void {
exec('git init', { cwd: this.env.cwd });
exec('yarn build', { cwd: this.env.cwd });
exec(`${path.join(path.resolve(this.env.cwd), 'bin', 'dev')} schema generate`, { 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) {
exec(`${path.join(path.resolve(this.env.cwd), 'bin', 'dev')} schema generate`, { cwd: this.env.cwd });
}
}
}
29 changes: 27 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,43 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

export interface PackageJson {
export type Topic = {
description?: string;
external?: boolean;
subtopics: Topic;
};

export type NYC = {
extends: string;
lines: number;
statements: number;
branches: number;
functions: number;
};

export type PackageJson = {
name: string;
devDependencies: Record<string, string>;
dependencies: Record<string, string>;
files: string[];
oclif: {
bin: string;
dirname: string;
hooks: Record<string, string | string[]>;
topics: Record<string, Topic>;
};
repository: string;
homepage: string;
}
bugs: string;
author: string;
description: string;
scripts: {
posttest: string;
'test:command-reference': string;
'test:deprecation-policy': string;
'test:json-schema': string;
};
};

export enum Hook {
'sf:env:list' = 'sf env list',
Expand Down
Loading