Skip to content

Commit c7ac1da

Browse files
Merge pull request #519 from salesforcecli/mdonnalley/no-yeoman
feat: no more yeoman
2 parents 67b41fd + c0b4440 commit c7ac1da

File tree

17 files changed

+918
-682
lines changed

17 files changed

+918
-682
lines changed

command-snapshot.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"command": "dev:generate:command",
4545
"flagAliases": [],
4646
"flagChars": ["n"],
47-
"flags": ["flags-dir", "force", "name", "nuts", "unit"],
47+
"flags": ["dry-run", "flags-dir", "force", "name", "nuts", "unit"],
4848
"plugin": "@salesforce/plugin-dev"
4949
},
5050
{
@@ -60,15 +60,15 @@
6060
"command": "dev:generate:library",
6161
"flagAliases": [],
6262
"flagChars": [],
63-
"flags": ["flags-dir"],
63+
"flags": ["dry-run", "flags-dir"],
6464
"plugin": "@salesforce/plugin-dev"
6565
},
6666
{
6767
"alias": ["plugins:generate"],
6868
"command": "dev:generate:plugin",
6969
"flagAliases": [],
7070
"flagChars": [],
71-
"flags": ["flags-dir"],
71+
"flags": ["dry-run", "flags-dir"],
7272
"plugin": "@salesforce/plugin-dev"
7373
}
7474
]

messages/dev.generate.command.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Generate a new sf command.
66

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

9-
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.
9+
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.
1010

1111
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.
1212

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

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

29+
# flags.dry-run.summary
30+
31+
Display the changes that would be made without writing them to disk.
32+
2933
# examples
3034

3135
- Generate the files for a new "sf my exciting command":

messages/dev.generate.library.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ When the command completes, your new library contains a few sample source and te
1111
# examples
1212

1313
- <%= config.bin %> <%= command.id %>
14+
15+
# flags.dry-run.summary
16+
17+
Display the changes that would be made without writing them to disk.

messages/dev.generate.plugin.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ When the command completes, your new plugin contains the source, message, and te
1111
# examples
1212

1313
- <%= config.bin %> <%= command.id %>
14+
15+
# flags.dry-run.summary
16+
17+
Display the changes that would be made without writing them to disk.

messages/plugin.generator.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
# info.start
2-
3-
Time to build an sf plugin!
4-
51
# question.internal
62

73
Are you building a plugin for an internal Salesforce team?

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,27 @@
1515
"@salesforce/sf-plugins-core": "^11.1.1",
1616
"@salesforce/ts-types": "^2.0.10",
1717
"change-case": "^5.4.2",
18+
"ejs": "^3.1.10",
1819
"fast-glob": "^3.3.2",
20+
"got": "^13",
1921
"graphology": "^0.25.4",
2022
"graphology-types": "^0.24.7",
2123
"js-yaml": "^4.1.0",
2224
"lodash.defaultsdeep": "^4.6.1",
25+
"proxy-agent": "^6.4.0",
2326
"replace-in-file": "^6.3.2",
2427
"shelljs": "^0.8.5",
25-
"yarn-deduplicate": "^6.0.2",
26-
"yeoman-environment": "^3.19.3",
27-
"yeoman-generator": "^5.10.0"
28+
"yarn-deduplicate": "^6.0.2"
2829
},
2930
"devDependencies": {
3031
"@oclif/plugin-command-snapshot": "^5.2.3",
3132
"@salesforce/cli-plugins-testkit": "^5.3.15",
3233
"@salesforce/dev-scripts": "^10.2.5",
3334
"@salesforce/plugin-command-reference": "^3.1.5",
35+
"@types/ejs": "^3.1.5",
3436
"@types/js-yaml": "^4.0.5",
3537
"@types/lodash.defaultsdeep": "^4.6.9",
3638
"@types/shelljs": "^0.8.14",
37-
"@types/yeoman-generator": "^5.2.14",
3839
"eslint-plugin-sf-plugin": "^1.18.8",
3940
"oclif": "^4.4.17",
4041
"strip-ansi": "^7.1.0",

src/commands/dev/generate/command.ts

Lines changed: 142 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,64 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8+
import { dirname, join, relative, sep } from 'node:path';
89
import { Messages } from '@salesforce/core';
910
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
10-
import { fileExists, generate } from '../../../util.js';
11+
import defaultsDeep from 'lodash.defaultsdeep';
12+
import { pascalCase } from 'change-case';
13+
import { set } from '@salesforce/kit';
14+
import { get } from '@salesforce/ts-types';
15+
import shelljs from 'shelljs';
16+
import { Generator } from '../../../generator.js';
17+
import { Topic } from '../../../types.js';
1118

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

22+
/** returns the modifications that need to be made for the oclif pjson topics information. Returns an empty object for "don't change anything" */
23+
export function addTopics(
24+
newCmd: string,
25+
existingTopics: Record<string, Topic>,
26+
commands: string[] = []
27+
): Record<string, Topic> {
28+
const updated: Record<string, Topic> = {};
29+
30+
const paths = newCmd
31+
.split(':')
32+
// omit last word since it's not a topic, it's the command name
33+
.slice(0, -1)
34+
.map((_, index, array) => array.slice(0, index + 1).join('.'))
35+
// reverse to build up the object from most specific to least
36+
.reverse();
37+
38+
for (const p of paths) {
39+
const pDepth = p.split('.').length;
40+
// if new command if foo.bar and there are any commands in the foo topic, this should be marked external.
41+
// if new command if foo.bar.baz and there are any commands in the foo.bar subtopic, it should be marked external.
42+
const isExternal = commands.some((c) => c.split('.').slice(0, pDepth).join('.') === p);
43+
const existing = get(updated, p);
44+
if (existing) {
45+
const merged = isExternal
46+
? {
47+
external: true,
48+
subtopics: existing,
49+
}
50+
: {
51+
description: get(existingTopics, `${p}.description`, `description for ${p}`),
52+
subtopics: existing,
53+
};
54+
set(updated, p, merged);
55+
} else {
56+
const entry = isExternal
57+
? { external: true }
58+
: { description: get(existingTopics, `${p}.description`, `description for ${p}`) };
59+
set(updated, p, entry);
60+
}
61+
}
62+
63+
return updated;
64+
}
65+
1566
export default class GenerateCommand extends SfCommand<void> {
1667
public static readonly enableJsonFlag = false;
1768
public static readonly summary = messages.getMessage('summary');
@@ -27,6 +78,9 @@ export default class GenerateCommand extends SfCommand<void> {
2778
force: Flags.boolean({
2879
summary: messages.getMessage('flags.force.summary'),
2980
}),
81+
'dry-run': Flags.boolean({
82+
summary: messages.getMessage('flags.dry-run.summary'),
83+
}),
3084
nuts: Flags.boolean({
3185
summary: messages.getMessage('flags.nuts.summary'),
3286
allowNo: true,
@@ -41,12 +95,94 @@ export default class GenerateCommand extends SfCommand<void> {
4195

4296
public async run(): Promise<void> {
4397
const { flags } = await this.parse(GenerateCommand);
44-
if (!(await fileExists('package.json'))) throw messages.createError('errors.InvalidDir');
45-
await generate('command', {
46-
name: flags.name,
98+
99+
const generator = new Generator({
47100
force: flags.force,
48-
nuts: flags.nuts,
49-
unit: flags.unit,
101+
dryRun: flags['dry-run'],
50102
});
103+
await generator.loadPjson();
104+
if (!generator.pjson) throw messages.createError('errors.InvalidDir');
105+
106+
this.log(`Adding a command to ${generator.pjson.name}!`);
107+
108+
if (Object.keys(generator.pjson.devDependencies).includes('@salesforce/plugin-command-reference')) {
109+
// Get a list of all commands in `sf`. We will use this to determine if a topic is internal or external.
110+
const sfCommandsStdout = shelljs.exec('sf commands --json', { silent: true }).stdout;
111+
const commandsJson = JSON.parse(sfCommandsStdout) as Array<{ id: string }>;
112+
const commands = commandsJson.map((command) => command.id.replace(/:/g, '.').replace(/ /g, '.'));
113+
114+
const newTopics = addTopics(flags.name, generator.pjson.oclif.topics, commands);
115+
defaultsDeep(generator.pjson.oclif.topics, newTopics);
116+
} else {
117+
const newTopics = addTopics(flags.name, generator.pjson.oclif.topics);
118+
defaultsDeep(generator.pjson.oclif.topics, newTopics);
119+
}
120+
121+
await generator.writePjson();
122+
123+
const cmdPath = flags.name.split(':').join('/');
124+
const commandPath = `src/commands/${cmdPath}.ts`;
125+
const className = pascalCase(flags.name);
126+
const opts = {
127+
className,
128+
returnType: `${className}Result`,
129+
commandPath,
130+
year: new Date().getFullYear(),
131+
pluginName: generator.pjson.name,
132+
messageFile: flags.name.replace(/:/g, '.'),
133+
};
134+
135+
// generate the command file
136+
await generator.render(
137+
generator.pjson.type === 'module' ? 'src/esm-command.ts.ejs' : 'src/cjs-command.ts.ejs',
138+
commandPath,
139+
opts
140+
);
141+
142+
// generate the message file
143+
await generator.render('messages/message.md.ejs', `messages/${flags.name.replace(/:/g, '.')}.md`);
144+
145+
// generate the nuts file
146+
if (flags.nuts) {
147+
await generator.render('test/command.nut.ts.ejs', `test/commands/${cmdPath}.nut.ts`, {
148+
cmd: flags.name.replace(/:/g, ' '),
149+
year: new Date().getFullYear(),
150+
pluginName: generator.pjson.name,
151+
messageFile: flags.name.replace(/:/g, '.'),
152+
});
153+
}
154+
155+
// generate the unit test file
156+
if (flags.unit) {
157+
const unitPath = `test/commands/${cmdPath}.test.ts`;
158+
const relativeCmdPath = relative(dirname(unitPath), commandPath).replace('.ts', '').replaceAll(sep, '/');
159+
await generator.render(
160+
generator.pjson.type === 'module' ? 'test/esm-command.test.ts.ejs' : 'test/cjs-command.test.ts.ejs',
161+
`test/commands/${cmdPath}.test.ts`,
162+
{
163+
className,
164+
commandPath,
165+
relativeCmdPath,
166+
name: flags.name.replace(/:/g, ' '),
167+
year: new Date().getFullYear(),
168+
pluginName: generator.pjson.name,
169+
}
170+
);
171+
}
172+
173+
// run the format, lint, and compile scripts
174+
generator.execute('yarn format');
175+
generator.execute('yarn lint -- --fix');
176+
generator.execute('yarn compile');
177+
178+
const localExecutable = process.platform === 'win32' ? join('bin', 'dev.cmd') : join('bin', 'dev.js');
179+
180+
if (generator.pjson.scripts['test:deprecation-policy']) {
181+
generator.execute(`${localExecutable} snapshot:generate`);
182+
}
183+
184+
if (generator.pjson.scripts['test:json-schema']) {
185+
generator.execute(`${localExecutable} schema:generate`);
186+
}
51187
}
52188
}

src/commands/dev/generate/library.ts

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,115 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8+
import { join, resolve } from 'node:path';
89
import { Messages } from '@salesforce/core';
9-
import { SfCommand } from '@salesforce/sf-plugins-core';
10-
import { generate } from '../../../util.js';
10+
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
11+
import input from '@inquirer/input';
12+
import { Generator } from '../../../generator.js';
1113

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

17+
const containsInvalidChars = (i: string): boolean =>
18+
i.split('').some((part) => '!#$%^&*() ?/\\,.";\':|{}[]~`'.includes(part));
19+
1520
export default class GenerateLibrary extends SfCommand<void> {
1621
public static enableJsonFlag = false;
1722
public static readonly hidden = true;
1823
public static readonly summary = messages.getMessage('summary');
1924
public static readonly description = messages.getMessage('description');
2025
public static readonly examples = messages.getMessages('examples');
2126

22-
public static readonly flags = {};
27+
public static readonly flags = {
28+
'dry-run': Flags.boolean({
29+
summary: messages.getMessage('flags.dry-run.summary'),
30+
}),
31+
};
2332

24-
// eslint-disable-next-line class-methods-use-this
2533
public async run(): Promise<void> {
26-
await generate('library', { force: true });
34+
const { flags } = await this.parse(GenerateLibrary);
35+
this.log(`Time to build a library!${flags['dry-run'] ? ' (dry-run)' : ''}`);
36+
37+
const generator = new Generator({
38+
dryRun: flags['dry-run'],
39+
});
40+
41+
const answers = {
42+
scope: await input({
43+
message: 'Npm Scope (should start with @)',
44+
default: '@salesforce',
45+
validate: (i: string): boolean | string => {
46+
if (!i) return 'You must provide a scope.';
47+
if (!i.startsWith('@')) return 'Scope must start with @.';
48+
if (containsInvalidChars(i)) return 'Scope must not contain invalid characters.';
49+
if (i.length < 2) return 'Scope length must be greater than one';
50+
return true;
51+
},
52+
}),
53+
name: await input({
54+
message: 'Name',
55+
validate: (i: string): boolean | string => {
56+
if (!i) return 'You must provide a package name.';
57+
if (containsInvalidChars(i)) return 'Name must not contain invalid characters.';
58+
else return true;
59+
},
60+
}),
61+
description: await input({ message: 'Description' }),
62+
org: await input({
63+
message: 'Github Org',
64+
default: 'forcedotcom',
65+
validate: (i: string): boolean | string => {
66+
if (!i) return 'You must provide a Github Org.';
67+
if (containsInvalidChars(i)) return 'Github Org must not contain invalid characters.';
68+
else return true;
69+
},
70+
}),
71+
};
72+
73+
const directory = resolve(answers.name);
74+
75+
generator.execute(`git clone [email protected]:forcedotcom/library-template.git ${directory}`);
76+
77+
generator.cwd = join(process.cwd(), answers.name);
78+
await generator.remove('.git');
79+
generator.execute('git init');
80+
await generator.loadPjson();
81+
82+
generator.pjson.name = `${answers.scope}/${answers.name}`;
83+
generator.pjson.description = answers.description;
84+
generator.pjson.repository = `${answers.org}/${answers.name}`;
85+
generator.pjson.homepage = `https://github.com/${answers.org}/${answers.name}`;
86+
generator.pjson.bugs = { url: `https://github.com/${answers.org}/${answers.name}/issues` };
87+
88+
await generator.writePjson();
89+
90+
const cwd = `${process.cwd()}/${answers.name}`;
91+
// Replace the message import
92+
generator.replace({
93+
files: `${cwd}/src/hello.ts`,
94+
from: /@salesforce\/library-template/g,
95+
to: `${answers.scope}/${answers.name}`,
96+
});
97+
98+
generator.replace({
99+
files: `${cwd}/**/*`,
100+
from: /library-template/g,
101+
to: answers.name,
102+
});
103+
104+
generator.replace({
105+
files: `${cwd}/**/*`,
106+
from: /forcedotcom/g,
107+
to: answers.org,
108+
});
109+
110+
generator.replace({
111+
files: `${cwd}/README.md`,
112+
from: /@salesforce/g,
113+
to: answers.scope,
114+
});
115+
116+
generator.execute('yarn');
117+
generator.execute('yarn build');
27118
}
28119
}

0 commit comments

Comments
 (0)