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

W-16025394 feat: no more yeoman #519

Merged
merged 6 commits into from
Jul 29, 2024
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
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.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
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 [email protected]: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
Loading