Skip to content

Commit

Permalink
feat: generate plugin without yeoman
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Jul 26, 2024
1 parent 33274d1 commit 6cc0f88
Show file tree
Hide file tree
Showing 12 changed files with 540 additions and 506 deletions.
4 changes: 4 additions & 0 deletions messages/dev.generate.library.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ When the command completes, your new library contains a few sample source and te
# examples

- <%= config.bin %> <%= command.id %>

# flags.dry-run.summary

Display the changes that would be made without writing them to disk.
4 changes: 4 additions & 0 deletions messages/dev.generate.plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ When the command completes, your new plugin contains the source, message, and te
# examples

- <%= config.bin %> <%= command.id %>

# flags.dry-run.summary

Display the changes that would be made without writing them to disk.
4 changes: 0 additions & 4 deletions messages/plugin.generator.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
# info.start

Time to build an sf plugin!

# question.internal

Are you building a plugin for an internal Salesforce team?
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,25 @@
"change-case": "^5.4.2",
"ejs": "^3.1.10",
"fast-glob": "^3.3.2",
"got": "^13",
"graphology": "^0.25.4",
"graphology-types": "^0.24.7",
"js-yaml": "^4.1.0",
"lodash.defaultsdeep": "^4.6.1",
"proxy-agent": "^6.4.0",
"replace-in-file": "^6.3.2",
"shelljs": "^0.8.5",
"yarn-deduplicate": "^6.0.2",
"yeoman-environment": "^3.19.3",
"yeoman-generator": "^5.10.0"
"yarn-deduplicate": "^6.0.2"
},
"devDependencies": {
"@oclif/plugin-command-snapshot": "^5.2.3",
"@salesforce/cli-plugins-testkit": "^5.3.15",
"@salesforce/dev-scripts": "^10.2.4",
"@salesforce/plugin-command-reference": "^3.1.5",
"@types/ejs": "^3.1.5",
"@types/js-yaml": "^4.0.5",
"@types/lodash.defaultsdeep": "^4.6.9",
"@types/shelljs": "^0.8.14",
"@types/yeoman-generator": "^5.2.14",
"eslint-plugin-sf-plugin": "^1.18.8",
"oclif": "^4.4.17",
"strip-ansi": "^7.1.0",
Expand Down
9 changes: 6 additions & 3 deletions src/commands/dev/generate/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ export default class GenerateLibrary extends SfCommand<void> {
public static readonly examples = messages.getMessages('examples');

public static readonly flags = {
'dry-run': Flags.boolean(),
'dry-run': Flags.boolean({
summary: messages.getMessage('flags.dry-run.summary'),
}),
};

public async run(): Promise<void> {
const { flags } = await this.parse(GenerateLibrary);
this.log('Time to build a library!');
this.log(`Time to build a library!${flags['dry-run'] ? ' (dry-run)' : ''}`);

const generator = new Generator({
dryRun: flags['dry-run'],
Expand Down Expand Up @@ -73,7 +75,8 @@ export default class GenerateLibrary extends SfCommand<void> {
generator.execute(`git clone git@github.com:forcedotcom/library-template.git ${directory}`);

generator.cwd = join(process.cwd(), answers.name);
await generator.remove(resolve(directory, '.git'));
await generator.remove('.git');
generator.execute('git init');
await generator.loadPjson();

generator.pjson.name = `${answers.scope}/${answers.name}`;
Expand Down
188 changes: 183 additions & 5 deletions src/commands/dev/generate/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,71 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { join, resolve } from 'node:path';
import { createRequire } from 'node:module';
import { Messages } from '@salesforce/core';
import { SfCommand } from '@salesforce/sf-plugins-core';
import { generate } from '../../../util.js';
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
import { ProxyAgent } from 'proxy-agent';
import shelljs from 'shelljs';
import got from 'got';
import confirm from '@inquirer/confirm';
import input from '@inquirer/input';
import select from '@inquirer/select';
import { Generator } from '../../../generator.js';
import { validatePluginName } from '../../../util.js';
import { stringToChoice } from '../../../prompts/functions.js';
import { NYC, PackageJson } from '../../../types.js';

Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-dev', 'dev.generate.plugin');
const generateMessages = Messages.loadMessages('@salesforce/plugin-dev', 'plugin.generator');

async function fetchGithubUserFromAPI(): Promise<{ login: string; name: string } | undefined> {
const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
if (!token) return;

const headers = {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${token}`,
};

try {
const { login, name } = await got('https://api.github.com/user', {
headers,
agent: { https: new ProxyAgent() },
}).json<{
login: string;
name: string;
}>();
return { login, name };
} catch {
// ignore
}
}

function fetchGithubUserFromGit(): string | undefined {
try {
const result = shelljs.exec('git config --get user.name', { silent: true });
return result.stdout.trim();
} catch {
// ignore
}
}

async function fetchGithubUser(): Promise<{ login?: string; name: string | undefined } | undefined> {
return (await fetchGithubUserFromAPI()) ?? { name: fetchGithubUserFromGit() };
}

function determineDefaultAuthor(
user: { login?: string; name: string | undefined } | undefined,
defaultValue: string
): string {
const { login, name } = user ?? { login: undefined, name: undefined };
if (name && login) return `${name} @${login}`;
if (name) return name;
if (login) return `@${login}`;
return defaultValue;
}

export default class GeneratePlugin extends SfCommand<void> {
public static enableJsonFlag = false;
Expand All @@ -19,10 +78,129 @@ export default class GeneratePlugin extends SfCommand<void> {
public static readonly examples = messages.getMessages('examples');
public static readonly aliases = ['plugins:generate'];
public static readonly deprecateAliases = true;
public static readonly flags = {};
public static readonly flags = {
'dry-run': Flags.boolean({
summary: messages.getMessage('flags.dry-run.summary'),
}),
};

// eslint-disable-next-line class-methods-use-this
public async run(): Promise<void> {
await generate('plugin', { force: true });
const { flags } = await this.parse(GeneratePlugin);
this.log(`Time to build a plugin!${flags['dry-run'] ? ' (dry-run)' : ''}`);

const generator = new Generator({
dryRun: flags['dry-run'],
});

const githubUsername = await fetchGithubUser();
const internal = await confirm({ message: generateMessages.getMessage('question.internal') });

const answers = {
internal,
name: await input({
message: generateMessages.getMessage(internal ? 'question.internal.name' : 'question.external.name'),
validate: (i: string): boolean | string => {
const result = validatePluginName(i, internal ? '2PP' : '3PP');
if (result) return true;

return generateMessages.getMessage(internal ? 'error.Invalid2ppName' : 'error.Invalid3ppName');
},
}),
description: await input({ message: generateMessages.getMessage('question.description') }),
...(!internal
? {
author: await input({
message: generateMessages.getMessage('question.author'),
default: determineDefaultAuthor(githubUsername, 'Author Name'),
}),
codeCoverage: await select({
message: generateMessages.getMessage('question.code-coverage'),
default: '50%',
choices: ['0%', '25%', '50%', '75%', '90%', '100%'].map(stringToChoice),
}),
}
: {}),
};

const directory = resolve(answers.name);
const templateRepo = answers.internal
? 'git clone https://github.com/salesforcecli/plugin-template-sf.git'
: 'git clone https://github.com/salesforcecli/plugin-template-sf-external.git';

generator.execute(`${templateRepo} "${directory}"`);

generator.cwd = directory;
await generator.remove('.git');
await generator.loadPjson();

generator.execute('git init');

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

if (answers.author) {
updated.author = answers.author;
}

generator.pjson = {
...generator.pjson,
...updated,
};

await generator.writePjson();

if (!answers.internal && answers.codeCoverage) {
const nycConfig = await generator.readJson<NYC>('.nycrc');
const codeCoverage = Number.parseInt(answers.codeCoverage.replace('%', ''), 10);
nycConfig['check-coverage'] = true;
nycConfig.lines = codeCoverage;
nycConfig.statements = codeCoverage;
nycConfig.functions = codeCoverage;
nycConfig.branches = codeCoverage;
await generator.writeJson('.nycrc', nycConfig);
}

generator.replace({
files: `${generator.cwd}/**/*`,
from: answers.internal ? /plugin-template-sf/g : /plugin-template-sf-external/g,
to: answers.name,
});

if (!answers.internal) {
await generator.remove('CODE_OF_CONDUCT.md');
await generator.remove('LICENSE.txt');
}

try {
// Try to dedupe yarn.lock before installing dependencies.
const require = createRequire(import.meta.url);
const yarnDedupePath = require.resolve('.bin/yarn-deduplicate');
generator.execute(yarnDedupePath);
} catch {
// do nothing
}

try {
generator.execute('yarn install');
} catch (e) {
// Run yarn install in case dev-scripts detected changes during yarn build.
generator.execute('yarn install');
}

generator.execute('yarn build');

if (answers.internal) {
const dev = process.platform === 'win32' ? 'dev.cmd' : 'dev.js';
generator.execute(`${join(resolve(generator.cwd), 'bin', dev)} schema generate`);
}
}
}
81 changes: 51 additions & 30 deletions src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,37 @@ export class Generator {
shelljs.exec(cmd, { cwd: this.cwd });
}

public async loadPjson(): Promise<PackageJson> {
try {
this.pjson = JSON.parse(await readFile(join(this.cwd, 'package.json'), 'utf-8')) as PackageJson;
} catch {
if (this.dryRun) {
this.pjson = {
name: '',
description: '',
dependencies: {},
devDependencies: {},
files: [],
bugs: '',
homepage: '',
repository: '',
oclif: {
bin: '',
topics: {},
dirname: '',
},
author: '',
// @ts-expect-error - not all properties are required
scripts: {},
// @ts-expect-error - not all properties are required
wireit: {},
};
}
}

return this.pjson;
}

public async writePjson(): Promise<void> {
const updating = colorize('yellow', 'Updating');
if (this.dryRun) {
Expand Down Expand Up @@ -119,44 +150,34 @@ export class Generator {
}

public async remove(path: string): Promise<void> {
const fullPath = join(this.cwd, path);
const removing = colorize('yellow', 'Removing');
if (this.dryRun) {
this.ux.log(`[DRY RUN] ${removing} ${path}`);
this.ux.log(`[DRY RUN] ${removing} ${fullPath}`);
return;
}

this.ux.log(`${removing} ${path}`);
await rm(path, { recursive: true });
this.ux.log(`${removing} ${fullPath}`);
await rm(fullPath, { recursive: true, force: true });
}

public async loadPjson(): Promise<PackageJson> {
try {
this.pjson = JSON.parse(await readFile(join(this.cwd, 'package.json'), 'utf-8')) as PackageJson;
} catch {
if (this.dryRun) {
this.pjson = {
name: '',
description: '',
dependencies: {},
devDependencies: {},
files: [],
bugs: '',
homepage: '',
repository: '',
oclif: {
bin: '',
topics: {},
dirname: '',
},
author: '',
// @ts-expect-error - not all properties are required
scripts: {},
// @ts-expect-error - not all properties are required
wireit: {},
};
}
public async readJson<T>(path: string): Promise<T> {
if (this.dryRun) {
return {} as T;
}

return this.pjson;
return JSON.parse(await readFile(join(this.cwd, path), 'utf-8')) as T;
}

public async writeJson<T>(path: string, data: T): Promise<void> {
const fullPath = join(this.cwd, path);
const writing = colorize('yellow', 'Writing');
if (this.dryRun) {
this.ux.log(`[DRY RUN] ${writing} to ${fullPath}`);
return;
}

this.ux.log(`${writing} to ${fullPath}`);
await writeFile(fullPath, JSON.stringify(data, null, 2));
}
}
Loading

0 comments on commit 6cc0f88

Please sign in to comment.