Skip to content

Commit cb6cf4b

Browse files
fix: more finessing
1 parent a8b6c88 commit cb6cf4b

7 files changed

Lines changed: 114 additions & 61 deletions

File tree

src/docs.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,9 @@ export class Docs {
109109
// The topic ditamap with all of the subtopic links.
110110
events.emit('subtopics', topic, subTopicNames);
111111

112-
await this.factory.createTopicCommands(topic, topicMeta).write();
113-
await this.factory.createTopicIndex(topic, commandIds).write();
112+
const topicCommands = this.factory.createTopicCommands(topic, topicMeta);
113+
if (topicCommands) await topicCommands.write();
114+
await this.factory.createTopicIndex(topic, commandIds, topicMeta).write();
114115
return subTopicNames;
115116
}
116117

@@ -188,7 +189,7 @@ export class Docs {
188189
const commandIds = [...subtopics.values()]
189190
.flat()
190191
.filter((cmd) => !cmd.hidden || this.hidden)
191-
.map((cmd) => ({ id: ensureString(cmd.id), state: cmd.state }));
192+
.map((cmd) => ({ id: ensureString(cmd.id), state: cmd.state, deprecated: cmd.deprecated ?? false }));
192193
tocEntries.push({ topic, commandIds });
193194
}
194195

src/generator-factory.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import { TopicCommands } from './ditamap/topic-commands.js';
2323
import { TopicDitamap } from './ditamap/topic-ditamap.js';
2424
import { MarkdownCliReference } from './markdown/cli-reference.js';
2525
import { MarkdownCommand } from './markdown/command.js';
26-
import { MarkdownTopicCommands } from './markdown/topic-commands.js';
2726
import { MarkdownTopicIndex } from './markdown/topic-index.js';
2827
import { MarkdownToc } from './markdown/toc.js';
2928
import { CommandClass, SfTopic } from './utils.js';
@@ -35,16 +34,16 @@ type WritableWithFilename = Writable & { getFilename(): string };
3534

3635
export type TocTopicEntry = {
3736
topic: string;
38-
commandIds: Array<{ id: string; state?: string }>;
37+
commandIds: Array<{ id: string; state?: string; deprecated?: boolean }>;
3938
};
4039

4140
export type GeneratorFactory = {
4241
createCliReference(topics: string[]): Writable;
4342
createHelpReference(): Writable | null;
4443
createRootIndex(topics: string[]): Writable | null;
4544
createToc(topicEntries: TocTopicEntry[]): Writable | null;
46-
createTopicCommands(topic: string, topicMeta: SfTopic): Writable;
47-
createTopicIndex(topic: string, commandIds: string[]): Writable;
45+
createTopicCommands(topic: string, topicMeta: SfTopic): Writable | null;
46+
createTopicIndex(topic: string, commandIds: string[], topicMeta: SfTopic): Writable;
4847
createCommand(
4948
topic: string,
5049
subtopic: string | null,
@@ -54,7 +53,6 @@ export type GeneratorFactory = {
5453
};
5554

5655
export class DitaGeneratorFactory implements GeneratorFactory {
57-
// eslint-disable-next-line class-methods-use-this
5856
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
5957
public createCliReference(_topics: string[]): Writable {
6058
return new CLIReference();
@@ -80,8 +78,8 @@ export class DitaGeneratorFactory implements GeneratorFactory {
8078
return new TopicCommands(topic, topicMeta);
8179
}
8280

83-
// eslint-disable-next-line class-methods-use-this
84-
public createTopicIndex(topic: string, commandIds: string[]): Writable {
81+
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
82+
public createTopicIndex(topic: string, commandIds: string[], _topicMeta: SfTopic): Writable {
8583
return new TopicDitamap(topic, commandIds);
8684
}
8785

@@ -99,8 +97,9 @@ export class DitaGeneratorFactory implements GeneratorFactory {
9997
export class MarkdownGeneratorFactory implements GeneratorFactory {
10098
public constructor(private outputDir: string) {}
10199

102-
public createCliReference(topics: string[]): Writable {
103-
return new MarkdownCliReference(Ditamap.cliVersion, Ditamap.pluginVersions, topics, this.outputDir);
100+
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
101+
public createCliReference(_topics: string[]): Writable {
102+
return new MarkdownCliReference(Ditamap.cliVersion, Ditamap.pluginVersions, this.outputDir);
104103
}
105104

106105
// eslint-disable-next-line class-methods-use-this
@@ -117,12 +116,13 @@ export class MarkdownGeneratorFactory implements GeneratorFactory {
117116
return new MarkdownToc(topicEntries, this.outputDir);
118117
}
119118

120-
public createTopicCommands(topic: string, topicMeta: SfTopic): Writable {
121-
return new MarkdownTopicCommands(topic, topicMeta, this.outputDir);
119+
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
120+
public createTopicCommands(_topic: string, _topicMeta: SfTopic): null {
121+
return null;
122122
}
123123

124-
public createTopicIndex(topic: string, commandIds: string[]): Writable {
125-
return new MarkdownTopicIndex(topic, commandIds, this.outputDir);
124+
public createTopicIndex(topic: string, commandIds: string[], topicMeta: SfTopic): Writable {
125+
return new MarkdownTopicIndex(topic, commandIds, topicMeta, this.outputDir);
126126
}
127127

128128
public createCommand(

src/markdown/cli-reference.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export class MarkdownCliReference extends MarkdownBase {
2020
public constructor(
2121
private cliVersion: string,
2222
private pluginVersions: Array<{ name: string; version: string }>,
23-
private topics: string[],
2423
outputDir: string
2524
) {
2625
super(MarkdownBase.file('cli_reference'), outputDir);
@@ -36,7 +35,7 @@ export class MarkdownCliReference extends MarkdownBase {
3635
' scratch orgs and sandboxes, synchronize source to and from orgs, create and install packages, and more.'
3736
);
3837
lines.push('');
39-
lines.push(`Salesforce CLI version: \`${this.cliVersion}\`.`);
38+
lines.push(`Salesforce CLI version: \`${this.cliVersion}\``);
4039
lines.push('');
4140
if (this.pluginVersions.length > 0) {
4241
lines.push('## Plugin Versions');
@@ -48,14 +47,6 @@ export class MarkdownCliReference extends MarkdownBase {
4847
}
4948
lines.push('');
5049
}
51-
if (this.topics.length > 0) {
52-
lines.push('## Command Topic Index');
53-
lines.push('');
54-
for (const topic of this.topics.sort()) {
55-
lines.push(`- [${topic}](./${topic}/cli_reference_${topic}.md)`);
56-
}
57-
lines.push('');
58-
}
5950
return Promise.resolve(lines.join('\n'));
6051
}
6152
}

src/markdown/command.ts

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class MarkdownCommand extends MarkdownBase {
3434
private examples: ParsedExample[];
3535
private state: unknown;
3636
private deprecated: boolean;
37+
private deprecationDetails: { version?: string; to?: string } | null;
3738

3839
public constructor(
3940
topic: string,
@@ -81,6 +82,8 @@ export class MarkdownCommand extends MarkdownBase {
8182

8283
this.state = command.state ?? this.commandMeta.state;
8384
this.deprecated = (command.deprecated as boolean) ?? this.state === 'deprecated' ?? false;
85+
const dep = command.deprecated;
86+
this.deprecationDetails = dep && typeof dep === 'object' ? (dep as { version?: string; to?: string }) : null;
8487
}
8588

8689
protected async generate(): Promise<string> {
@@ -89,36 +92,28 @@ export class MarkdownCommand extends MarkdownBase {
8992

9093
const lines: string[] = [];
9194

92-
lines.push(`# ${this.commandName}`);
95+
const stateLabel = resolveStateLabel(this.state, this.deprecated);
96+
lines.push(`# ${this.commandName}${stateLabel ? ` (${stateLabel})` : ''}`);
9397
lines.push('');
9498

9599
if (this.summary) {
96100
lines.push(this.summary);
97101
lines.push('');
98102
}
99103

100-
if (this.deprecated) {
101-
lines.push('> **Deprecated**');
102-
lines.push('');
103-
} else if (this.state === 'beta') {
104-
lines.push('> **Beta**');
105-
lines.push('');
106-
} else if (this.state === 'preview') {
107-
lines.push('> **Preview**');
108-
lines.push('');
109-
} else if (this.state === 'closedPilot') {
110-
lines.push('> **Closed Pilot**');
111-
lines.push('');
112-
} else if (this.state === 'openPilot') {
113-
lines.push('> **Open Pilot**');
104+
const disclaimer = resolveDisclaimer(this.commandName, this.state, this.deprecated, this.deprecationDetails);
105+
if (disclaimer) {
106+
lines.push(':::note');
107+
lines.push(disclaimer);
108+
lines.push(':::');
114109
lines.push('');
115110
}
116111

117112
if (this.help.length > 0) {
118113
lines.push(`## Description for ${this.commandName}`);
119114
lines.push('');
120115
for (const paragraph of this.help) {
121-
lines.push(paragraph);
116+
lines.push(applyCodeFormatting(escapeAngleBrackets(paragraph)));
122117
lines.push('');
123118
}
124119
}
@@ -144,7 +139,7 @@ export class MarkdownCommand extends MarkdownBase {
144139
lines.push('## Flags');
145140
lines.push('');
146141
lines.push('| Flag | Description |');
147-
lines.push('|------|-------------|');
142+
lines.push('|----------------------------------------|-------------|');
148143
for (const param of parameters) {
149144
lines.push(renderFlagRow(param));
150145
}
@@ -155,6 +150,58 @@ export class MarkdownCommand extends MarkdownBase {
155150
}
156151
}
157152

153+
function escapeAngleBrackets(text: string): string {
154+
return text.replace(/</g, '&lt;').replace(/>/g, '&gt;');
155+
}
156+
157+
function applyCodeFormatting(text: string): string {
158+
// Wrap --flag-name tokens (not already in backticks)
159+
let result = text.replace(/(?<!`)--([\w-]+)(?!`)/g, '`--$1`');
160+
// Wrap file/directory paths: must be preceded by whitespace or opening punctuation (not part of a URL)
161+
// Matches: ./foo/bar, ../foo, foo/bar/baz — but not https://foo/bar
162+
result = result.replace(/(^|(?<=[\s(["]))(?!https?:\/\/)((?:\.{1,2}\/|[\w][\w-]*\/)[\w./-]+)/g, '$1`$2`');
163+
return result;
164+
}
165+
166+
function resolveStateLabel(state: unknown, deprecated: boolean): string | null {
167+
if (deprecated) return 'Deprecated';
168+
if (state === 'beta') return 'Beta';
169+
if (state === 'preview') return 'Developer Preview';
170+
if (state === 'closedPilot' || state === 'openPilot') return 'Pilot';
171+
return null;
172+
}
173+
174+
function resolveDisclaimer(
175+
commandName: string,
176+
state: unknown,
177+
deprecated: boolean,
178+
deprecationDetails: { version?: string; to?: string } | null
179+
): string | null {
180+
if (deprecated) {
181+
const versionNote = deprecationDetails?.version
182+
? ` and will be removed in v${deprecationDetails.version} or later`
183+
: '';
184+
const toNote = deprecationDetails?.to ? ` Use \`${deprecationDetails.to}\` instead.` : '';
185+
return `The command \`${commandName}\` has been deprecated${versionNote}.${toNote}`;
186+
}
187+
if (state === 'closedPilot') {
188+
// prettier-ignore
189+
return `We provide the \`${commandName}\` command to selected customers through an invitation-only pilot program that requires agreement to specific terms and conditions. Pilot programs are subject to change, and we can\x27t guarantee acceptance. The \`${commandName}\` command isn\x27t generally available unless or until Salesforce announces its general availability in documentation or in press releases or public statements. We can\x27t guarantee general availability within any particular time frame or at all. Make your purchase decisions only on the basis of generally available products and features.`;
190+
}
191+
if (state === 'openPilot') {
192+
// prettier-ignore
193+
return `We provide the \`${commandName}\` command to selected customers through a pilot program that requires agreement to specific terms and conditions. To be nominated to participate in the program, contact Salesforce. Pilot programs are subject to change, and we can\x27t guarantee acceptance. The \`${commandName}\` command isn\x27t generally available unless or until Salesforce announces its general availability in documentation or in press releases or public statements. We can\x27t guarantee general availability within any particular time frame or at all. Make your purchase decisions only on the basis of generally available products and features.`;
194+
}
195+
if (state === 'beta') {
196+
return 'This feature is a Beta Service. Customers may opt to try such Beta Service in its sole discretion. Any use of the Beta Service is subject to the applicable Beta Services Terms provided at [Agreements and Terms](https://www.salesforce.com/company/legal/agreements/).';
197+
}
198+
if (state === 'preview') {
199+
// prettier-ignore
200+
return 'This command is available as a developer preview. The command isn\'t generally available unless or until Salesforce announces its general availability in documentation or in press releases or public statements. All commands, parameters, and other features are subject to change or deprecation at any time, with or without notice. Don\'t implement functionality developed with these commands or tools.';
201+
}
202+
return null;
203+
}
204+
158205
function renderFlagRow(param: CommandParameterData): string {
159206
const flagLabel = renderFlagLabel(param);
160207
const desc = renderFlagDescription(param);
@@ -171,13 +218,18 @@ function renderFlagLabel(param: CommandParameterData): string {
171218

172219
function renderFlagDescription(param: CommandParameterData): string {
173220
const parts: string[] = [];
174-
if (!param.optional) parts.push('**Required.**');
175-
const desc = param.description.join(' ').replace(/\|/g, '&#124;');
176-
if (desc) parts.push(desc);
177-
if (param.defaultFlagValue) parts.push(`Default: \`${param.defaultFlagValue}\`.`);
178-
if (param.options?.length) parts.push(`Options: ${param.options.map((o) => `\`${o}\``).join(', ')}.`);
179221
if (param.deprecated) {
180-
parts.push(`Deprecated${param.deprecated.to ? `, use \`--${param.deprecated.to}\`` : ''}.`);
222+
const toNote = param.deprecated.to ? ` Use \`--${param.deprecated.to}\` instead.` : '';
223+
parts.push(`**This flag is deprecated.${toNote}**`);
181224
}
182-
return parts.join(' ').replace(/\n/g, ' ');
225+
if (!param.optional) parts.push('**Required**');
226+
if (param.options?.length) {
227+
parts.push(`**Valid Values:** ${param.options.map((o) => `\`${o}\``).join(', ')}`);
228+
}
229+
if (param.defaultFlagValue) parts.push(`**Default value:** \`${param.defaultFlagValue}\``);
230+
const desc = param.description
231+
.map((p) => applyCodeFormatting(escapeAngleBrackets(p.replace(/\|/g, '&#124;'))))
232+
.join('<br><br>');
233+
if (desc) parts.push(desc);
234+
return parts.join('<br>').replace(/\n/g, ' ');
183235
}

src/markdown/toc.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ import { MarkdownBase } from './markdown-base.js';
1919

2020
const STATE_LABELS: Record<string, string> = {
2121
beta: 'Beta',
22-
preview: 'Preview',
22+
preview: 'Developer Preview',
2323
closedPilot: 'Closed Pilot',
2424
openPilot: 'Open Pilot',
25+
deprecated: 'Deprecated',
2526
};
2627

2728
export class MarkdownToc extends MarkdownBase {
@@ -50,10 +51,14 @@ export class MarkdownToc extends MarkdownBase {
5051
lines.push(`- title: ${topic} Commands`);
5152
lines.push(` link: ${topic}/cli_reference_${topic}.md`);
5253
lines.push(' topics:');
53-
for (const { id, state } of [...commandIds].sort((a, b) => a.id.localeCompare(b.id))) {
54+
for (const { id, state, deprecated } of [...commandIds].sort((a, b) => a.id.localeCompare(b.id))) {
5455
const commandWithUnderscores = id.replace(/:/g, '_');
5556
const commandWithSpaces = id.replace(/:/g, ' ');
56-
const stateLabel = state && STATE_LABELS[state] ? ` (${STATE_LABELS[state]})` : '';
57+
const stateLabel = deprecated
58+
? ' (Deprecated)'
59+
: state && STATE_LABELS[state]
60+
? ` (${STATE_LABELS[state]})`
61+
: '';
5762
lines.push(` - title: ${commandWithSpaces}${stateLabel}`);
5863
lines.push(` link: ${topic}/cli_reference_${commandWithUnderscores}.md`);
5964
}

src/markdown/topic-index.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,29 @@
1515
*/
1616

1717
import { join } from 'node:path';
18+
import { SfTopic } from '../utils.js';
1819
import { MarkdownBase } from './markdown-base.js';
1920

2021
export class MarkdownTopicIndex extends MarkdownBase {
21-
public constructor(private topic: string, private commandIds: string[], outputDir: string) {
22+
public constructor(
23+
private topic: string,
24+
private commandIds: string[],
25+
private topicMeta: SfTopic,
26+
outputDir: string
27+
) {
2228
const filename = MarkdownBase.file(`cli_reference_${topic}`);
2329
super(filename, outputDir);
2430
this.destination = join(outputDir, topic, filename);
2531
}
2632

2733
protected generate(): Promise<string> {
2834
const lines: string[] = [];
29-
lines.push('');
3035
lines.push(`# ${this.topic} Commands`);
3136
lines.push('');
37+
if (this.topicMeta.description) {
38+
lines.push(this.topicMeta.description);
39+
lines.push('');
40+
}
3241
for (const id of [...this.commandIds].sort()) {
3342
const commandWithUnderscores = id.replace(/:/g, '_');
3443
const commandWithSpaces = id.replace(/:/g, ' ');

test/unit/markdown.test.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,17 +75,12 @@ describe('markdown output: plugin-auth and user', () => {
7575
expect(md.includes('<codeph')).to.be.false;
7676
});
7777

78-
it('creates a topic index file', () => {
78+
it('creates a topic index file with description and command links', () => {
7979
const md = loadMdFile(join('org', 'cli_reference_org.md'));
8080
expect(md.includes('# org Commands')).to.be.true;
8181
expect(md.includes('cli_reference_org_login_jwt.md')).to.be.true;
8282
});
8383

84-
it('creates a topic commands overview file', () => {
85-
const md = loadMdFile(join('org', 'cli_reference_org_commands.md'));
86-
expect(md.includes('# org Commands')).to.be.true;
87-
});
88-
8984
it('creates a toc file', () => {
9085
const toc = loadMdFile('sfclireference-toc.yml');
9186
expect(toc.includes('- title: Salesforce CLI Command Reference')).to.be.true;

0 commit comments

Comments
 (0)