Skip to content

Commit

Permalink
Merge pull request #519 from salesforcecli/mdonnalley/no-yeoman
Browse files Browse the repository at this point in the history
feat: no more yeoman
  • Loading branch information
WillieRuemmele authored Jul 29, 2024
2 parents 67b41fd + c0b4440 commit c7ac1da
Show file tree
Hide file tree
Showing 17 changed files with 918 additions and 682 deletions.
6 changes: 3 additions & 3 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"command": "dev:generate:command",
"flagAliases": [],
"flagChars": ["n"],
"flags": ["flags-dir", "force", "name", "nuts", "unit"],
"flags": ["dry-run", "flags-dir", "force", "name", "nuts", "unit"],
"plugin": "@salesforce/plugin-dev"
},
{
Expand All @@ -60,15 +60,15 @@
"command": "dev:generate:library",
"flagAliases": [],
"flagChars": [],
"flags": ["flags-dir"],
"flags": ["dry-run", "flags-dir"],
"plugin": "@salesforce/plugin-dev"
},
{
"alias": ["plugins:generate"],
"command": "dev:generate:plugin",
"flagAliases": [],
"flagChars": [],
"flags": ["flags-dir"],
"flags": ["dry-run", "flags-dir"],
"plugin": "@salesforce/plugin-dev"
}
]
6 changes: 5 additions & 1 deletion messages/dev.generate.command.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Generate a new sf command.

You must run this command from within a plugin directory, such as the directory created with the "sf dev generate plugin" command.

The command generates basic source files, messages (\*.md), and test files for your new command. The Typescript files contain import statements for the minimum required Salesforce libraries, and scaffold some basic code. The new type names come from the value you passed to the --name flag.
The command generates basic source files, messages (\*.md), and test files for your new command. The Typescript files contain import statements for the minimum required Salesforce libraries, and scaffold some basic code. The new type names come from the value you passed to the --name flag.

The command updates the package.json file, so if it detects conflicts with the existing file, you're prompted whether you want to overwrite the file. There are a number of package.json updates required for a new command, so we recommend you answer "y" so the command takes care of them all. If you answer "n", you must update the package.json file manually.

Expand All @@ -26,6 +26,10 @@ Generate a unit test file for the command.

Name of the new command. Use colons to separate the topic and command names.

# flags.dry-run.summary

Display the changes that would be made without writing them to disk.

# examples

- Generate the files for a new "sf my exciting command":
Expand Down
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
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,27 @@
"@salesforce/sf-plugins-core": "^11.1.1",
"@salesforce/ts-types": "^2.0.10",
"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.5",
"@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
148 changes: 142 additions & 6 deletions src/commands/dev/generate/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,64 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { dirname, join, relative, sep } from 'node:path';
import { Messages } from '@salesforce/core';
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { fileExists, generate } from '../../../util.js';
import defaultsDeep from 'lodash.defaultsdeep';
import { pascalCase } from 'change-case';
import { set } from '@salesforce/kit';
import { get } from '@salesforce/ts-types';
import shelljs from 'shelljs';
import { Generator } from '../../../generator.js';
import { Topic } from '../../../types.js';

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

/** returns the modifications that need to be made for the oclif pjson topics information. Returns an empty object for "don't change anything" */
export function addTopics(
newCmd: string,
existingTopics: Record<string, Topic>,
commands: string[] = []
): Record<string, Topic> {
const updated: Record<string, Topic> = {};

const paths = newCmd
.split(':')
// omit last word since it's not a topic, it's the command name
.slice(0, -1)
.map((_, index, array) => array.slice(0, index + 1).join('.'))
// reverse to build up the object from most specific to least
.reverse();

for (const p of paths) {
const pDepth = p.split('.').length;
// if new command if foo.bar and there are any commands in the foo topic, this should be marked external.
// if new command if foo.bar.baz and there are any commands in the foo.bar subtopic, it should be marked external.
const isExternal = commands.some((c) => c.split('.').slice(0, pDepth).join('.') === p);
const existing = get(updated, p);
if (existing) {
const merged = isExternal
? {
external: true,
subtopics: existing,
}
: {
description: get(existingTopics, `${p}.description`, `description for ${p}`),
subtopics: existing,
};
set(updated, p, merged);
} else {
const entry = isExternal
? { external: true }
: { description: get(existingTopics, `${p}.description`, `description for ${p}`) };
set(updated, p, entry);
}
}

return updated;
}

export default class GenerateCommand extends SfCommand<void> {
public static readonly enableJsonFlag = false;
public static readonly summary = messages.getMessage('summary');
Expand All @@ -27,6 +78,9 @@ export default class GenerateCommand extends SfCommand<void> {
force: Flags.boolean({
summary: messages.getMessage('flags.force.summary'),
}),
'dry-run': Flags.boolean({
summary: messages.getMessage('flags.dry-run.summary'),
}),
nuts: Flags.boolean({
summary: messages.getMessage('flags.nuts.summary'),
allowNo: true,
Expand All @@ -41,12 +95,94 @@ export default class GenerateCommand extends SfCommand<void> {

public async run(): Promise<void> {
const { flags } = await this.parse(GenerateCommand);
if (!(await fileExists('package.json'))) throw messages.createError('errors.InvalidDir');
await generate('command', {
name: flags.name,

const generator = new Generator({
force: flags.force,
nuts: flags.nuts,
unit: flags.unit,
dryRun: flags['dry-run'],
});
await generator.loadPjson();
if (!generator.pjson) throw messages.createError('errors.InvalidDir');

this.log(`Adding a command to ${generator.pjson.name}!`);

if (Object.keys(generator.pjson.devDependencies).includes('@salesforce/plugin-command-reference')) {
// Get a list of all commands in `sf`. We will use this to determine if a topic is internal or external.
const sfCommandsStdout = shelljs.exec('sf commands --json', { silent: true }).stdout;
const commandsJson = JSON.parse(sfCommandsStdout) as Array<{ id: string }>;
const commands = commandsJson.map((command) => command.id.replace(/:/g, '.').replace(/ /g, '.'));

const newTopics = addTopics(flags.name, generator.pjson.oclif.topics, commands);
defaultsDeep(generator.pjson.oclif.topics, newTopics);
} else {
const newTopics = addTopics(flags.name, generator.pjson.oclif.topics);
defaultsDeep(generator.pjson.oclif.topics, newTopics);
}

await generator.writePjson();

const cmdPath = flags.name.split(':').join('/');
const commandPath = `src/commands/${cmdPath}.ts`;
const className = pascalCase(flags.name);
const opts = {
className,
returnType: `${className}Result`,
commandPath,
year: new Date().getFullYear(),
pluginName: generator.pjson.name,
messageFile: flags.name.replace(/:/g, '.'),
};

// generate the command file
await generator.render(
generator.pjson.type === 'module' ? 'src/esm-command.ts.ejs' : 'src/cjs-command.ts.ejs',
commandPath,
opts
);

// generate the message file
await generator.render('messages/message.md.ejs', `messages/${flags.name.replace(/:/g, '.')}.md`);

// generate the nuts file
if (flags.nuts) {
await generator.render('test/command.nut.ts.ejs', `test/commands/${cmdPath}.nut.ts`, {
cmd: flags.name.replace(/:/g, ' '),
year: new Date().getFullYear(),
pluginName: generator.pjson.name,
messageFile: flags.name.replace(/:/g, '.'),
});
}

// generate the unit test file
if (flags.unit) {
const unitPath = `test/commands/${cmdPath}.test.ts`;
const relativeCmdPath = relative(dirname(unitPath), commandPath).replace('.ts', '').replaceAll(sep, '/');
await generator.render(
generator.pjson.type === 'module' ? 'test/esm-command.test.ts.ejs' : 'test/cjs-command.test.ts.ejs',
`test/commands/${cmdPath}.test.ts`,
{
className,
commandPath,
relativeCmdPath,
name: flags.name.replace(/:/g, ' '),
year: new Date().getFullYear(),
pluginName: generator.pjson.name,
}
);
}

// run the format, lint, and compile scripts
generator.execute('yarn format');
generator.execute('yarn lint -- --fix');
generator.execute('yarn compile');

const localExecutable = process.platform === 'win32' ? join('bin', 'dev.cmd') : join('bin', 'dev.js');

if (generator.pjson.scripts['test:deprecation-policy']) {
generator.execute(`${localExecutable} snapshot:generate`);
}

if (generator.pjson.scripts['test:json-schema']) {
generator.execute(`${localExecutable} schema:generate`);
}
}
}
101 changes: 96 additions & 5 deletions src/commands/dev/generate/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,115 @@
* 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 { 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 input from '@inquirer/input';
import { Generator } from '../../../generator.js';

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

const containsInvalidChars = (i: string): boolean =>
i.split('').some((part) => '!#$%^&*() ?/\\,.";\':|{}[]~`'.includes(part));

export default class GenerateLibrary extends SfCommand<void> {
public static enableJsonFlag = false;
public static readonly hidden = true;
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessages('examples');

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('library', { force: true });
const { flags } = await this.parse(GenerateLibrary);
this.log(`Time to build a library!${flags['dry-run'] ? ' (dry-run)' : ''}`);

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

const answers = {
scope: await input({
message: 'Npm Scope (should start with @)',
default: '@salesforce',
validate: (i: string): boolean | string => {
if (!i) return 'You must provide a scope.';
if (!i.startsWith('@')) return 'Scope must start with @.';
if (containsInvalidChars(i)) return 'Scope must not contain invalid characters.';
if (i.length < 2) return 'Scope length must be greater than one';
return true;
},
}),
name: await input({
message: 'Name',
validate: (i: string): boolean | string => {
if (!i) return 'You must provide a package name.';
if (containsInvalidChars(i)) return 'Name must not contain invalid characters.';
else return true;
},
}),
description: await input({ message: 'Description' }),
org: await input({
message: 'Github Org',
default: 'forcedotcom',
validate: (i: string): boolean | string => {
if (!i) return 'You must provide a Github Org.';
if (containsInvalidChars(i)) return 'Github Org must not contain invalid characters.';
else return true;
},
}),
};

const directory = resolve(answers.name);

generator.execute(`git clone git@github.com:forcedotcom/library-template.git ${directory}`);

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

generator.pjson.name = `${answers.scope}/${answers.name}`;
generator.pjson.description = answers.description;
generator.pjson.repository = `${answers.org}/${answers.name}`;
generator.pjson.homepage = `https://github.com/${answers.org}/${answers.name}`;
generator.pjson.bugs = { url: `https://github.com/${answers.org}/${answers.name}/issues` };

await generator.writePjson();

const cwd = `${process.cwd()}/${answers.name}`;
// Replace the message import
generator.replace({
files: `${cwd}/src/hello.ts`,
from: /@salesforce\/library-template/g,
to: `${answers.scope}/${answers.name}`,
});

generator.replace({
files: `${cwd}/**/*`,
from: /library-template/g,
to: answers.name,
});

generator.replace({
files: `${cwd}/**/*`,
from: /forcedotcom/g,
to: answers.org,
});

generator.replace({
files: `${cwd}/README.md`,
from: /@salesforce/g,
to: answers.scope,
});

generator.execute('yarn');
generator.execute('yarn build');
}
}
Loading

0 comments on commit c7ac1da

Please sign in to comment.