Skip to content

Commit 7f5a71d

Browse files
feat: update to produce markdown output
1 parent 4ff5ed2 commit 7f5a71d

16 files changed

Lines changed: 893 additions & 22 deletions

CLAUDE.md

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Purpose
6+
7+
This plugin generates the Salesforce CLI command reference documentation guide (https://developer.salesforce.com/docs/atlas.en-us.sfdx_cli_reference.meta/sfdx_cli_reference/). It's an oclif plugin that reads command metadata from other Salesforce CLI plugins and transforms them into DITA XML format for publication.
8+
9+
## Common Commands
10+
11+
### Build and Development
12+
13+
```bash
14+
# Build the project (compile + lint)
15+
yarn build
16+
17+
# Compile TypeScript only
18+
yarn compile
19+
20+
# Run linter
21+
yarn lint
22+
23+
# Format code
24+
yarn format
25+
```
26+
27+
### Testing
28+
29+
```bash
30+
# Run all tests (includes test:compile, test:only, and lint)
31+
yarn test
32+
33+
# Run only unit tests (skips compilation and lint)
34+
yarn test:only
35+
36+
# Run a single test file
37+
yarn mocha test/unit/utils.test.ts
38+
```
39+
40+
### Local Development
41+
42+
```bash
43+
# Link plugin to local Salesforce CLI for testing
44+
sf plugins link .
45+
46+
# Run command using dev executable
47+
./bin/dev.js commandreference generate --plugins auth
48+
49+
# Generate documentation for multiple plugins
50+
sf commandreference generate --plugins auth user deploy-retrieve --output-dir ./test-output
51+
```
52+
53+
### Generate Command Reference
54+
55+
```bash
56+
# Generate docs for specific plugins (default: DITA XML format)
57+
sf commandreference generate --plugins auth user
58+
59+
# Generate docs in Markdown format
60+
sf commandreference generate --plugins auth --format markdown
61+
62+
# Generate docs for all plugins
63+
sf commandreference generate --all
64+
65+
# Generate docs with error on warnings (useful in CI)
66+
sf commandreference generate --plugins auth --error-on-warnings
67+
68+
# Install JIT plugins (needed for comprehensive doc generation)
69+
sf jit install
70+
```
71+
72+
## Code Architecture
73+
74+
### Core Components
75+
76+
**Docs class (`src/docs.ts`)**
77+
78+
- Entry point for documentation generation
79+
- Orchestrates the entire documentation generation process
80+
- Groups commands by topics and subtopics using `groupTopicsAndSubtopics()`
81+
- Iterates through all topics and calls `populateTopic()` for each
82+
- Each topic generates multiple DITA XML files through various ditamap classes
83+
84+
**Command Reference Generator (`src/commands/commandreference/generate.ts`)**
85+
86+
- Main CLI command implementation
87+
- Loads plugin configurations and metadata from oclif
88+
- Resolves plugin dependencies and child plugins
89+
- Extracts topic metadata from plugin `package.json` oclif configurations
90+
- Loads all commands from specified plugins
91+
- Initializes Docs class and triggers generation
92+
93+
**Ditamap Base Class (`src/ditamap/ditamap.ts`)**
94+
95+
- Abstract base class for all DITA XML generators
96+
- Uses Handlebars templates from `templates/` directory
97+
- Manages file paths, naming conventions, and output directory
98+
- Registers Handlebars helpers for XML generation (xmlFile, uniqueId, isCodeBlock, etc.)
99+
- Static properties like `outputDir`, `suffix`, `cliVersion` are shared across all ditamap instances
100+
101+
### Ditamap Hierarchy
102+
103+
The plugin generates multiple types of DITA files through specialized ditamap classes:
104+
105+
- **BaseDitamap** (`src/ditamap/base-ditamap.ts`): Top-level ditamap that references all topic ditamaps
106+
- **CLIReference** (`src/ditamap/cli-reference.ts`): CLI version and plugin version metadata
107+
- **HelpReference** (`src/ditamap/help-reference.ts`): General help content
108+
- **TopicDitamap** (`src/ditamap/topic-ditamap.ts`): Maps for each topic, referencing all commands in that topic
109+
- **TopicCommands** (`src/ditamap/topic-commands.ts`): Topic-level command listing pages
110+
- **Command** (`src/ditamap/command.ts`): Individual command documentation pages with flags, examples, descriptions
111+
112+
### Handlebars Templates
113+
114+
All templates are in `templates/` directory:
115+
116+
- `base_ditamap.hbs`: Top-level namespace ditamap
117+
- `cli_reference_xml.hbs`: CLI and plugin version information
118+
- `cli_reference_help.hbs`: Help reference page
119+
- `cli_reference_topic_commands.hbs`: Topic overview pages
120+
- `topic_ditamap.hbs`: Topic-level ditamaps
121+
- `command.hbs`: Individual command documentation (flags, examples, descriptions)
122+
123+
### Topic and Command Metadata
124+
125+
Topics and subtopics are defined in each plugin's `package.json` under the `oclif.topics` section:
126+
127+
```json
128+
{
129+
"oclif": {
130+
"topics": {
131+
"commandreference": {
132+
"description": "generate the Salesforce CLI command reference guide.",
133+
"longDescription": "..."
134+
}
135+
}
136+
}
137+
}
138+
```
139+
140+
The generator reads this metadata to:
141+
142+
- Organize commands into hierarchical topics/subtopics
143+
- Extract descriptions and long descriptions
144+
- Determine command state (beta, pilot, deprecated)
145+
- Handle hidden topics/commands based on the `--hidden` flag
146+
147+
### Command Processing Flow
148+
149+
1. `CommandReferenceGenerate.run()` loads all plugins and commands
150+
2. Calls `Docs.build()` with the command list
151+
3. `Docs.populateTemplate()` groups commands into topics/subtopics
152+
4. For each topic, `Docs.populateTopic()` generates ditamaps
153+
5. For each command, `Docs.populateCommand()` creates command XML via `Command` class
154+
6. Each ditamap class extends `Ditamap` and uses corresponding `.hbs` template
155+
7. Handlebars transforms data into DITA XML format
156+
8. Files are written to the output directory (default: `./tmp/root`)
157+
158+
### Event System
159+
160+
The plugin uses Node's EventEmitter (`events` in `src/utils.ts`) to communicate status:
161+
162+
- `'topic'`: Emitted when processing a new topic
163+
- `'subtopics'`: Emitted with subtopic names
164+
- `'warning'`: Emitted for metadata issues or validation warnings
165+
166+
### Wireit Task Dependencies
167+
168+
This project uses Wireit for build orchestration. Key task dependencies:
169+
170+
- `build` depends on `compile` and `lint`
171+
- `test` depends on `test:compile`, `test:only`, and `lint`
172+
- `test:only` depends on `test:command-reference` (generates test fixtures by running the command)
173+
- Tests run using Mocha with `ts-node/esm` loader for ESM support
174+
175+
### TypeScript Configuration
176+
177+
- Uses ESM modules (`"type": "module"` in package.json)
178+
- Extends strict TypeScript configuration from `@salesforce/dev-config`
179+
- Source in `src/`, compiled output in `lib/`
180+
- Mocha tests use `ts-node/esm` loader for direct TypeScript execution
181+
182+
## Output Formats
183+
184+
The plugin supports two output formats via the `--format` flag:
185+
186+
### DITA XML (default)
187+
188+
- Original format for Salesforce documentation pipeline
189+
- Uses templates in `templates/` directory
190+
- Generates XML files following the DITA specification
191+
- Output includes `.ditamap` files for documentation aggregation
192+
193+
### Markdown
194+
195+
- Alternative format for easier reading and contribution
196+
- Uses templates in `templates/markdown/` directory
197+
- Generates `.md` files with GitHub-flavored Markdown
198+
- Creates README.md files for topic/command navigation
199+
- Useful for hosting docs on GitHub or other Markdown-friendly platforms
200+
201+
Both formats use the same data pipeline and Handlebars templating system. The Markdown generator classes (`src/markdown/`) mirror the DITA generator classes (`src/ditamap/`) in structure.
202+
203+
## Important Notes
204+
205+
- The plugin reads metadata from other plugins' `package.json` oclif configurations - it doesn't generate its own command metadata
206+
- Warnings are emitted when plugins lack required metadata (topic descriptions, command metadata)
207+
- The `--error-on-warnings` flag is useful in CI to enforce metadata completeness
208+
- Commands are grouped by namespace (e.g., `force:org:create` → topic: `force`, subtopic: `org`)
209+
- The `--ditamap-suffix` flag allows multiple doc sets to coexist (default: "unified")
210+
- The plugin supports generating documentation for external/JIT plugins via the `jit install` command
211+
- Both DITA and Markdown formats share the same suffix configuration for consistency

src/commands/commandreference/generate.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { Messages, SfError } from '@salesforce/core';
2323
import { AnyJson, ensure } from '@salesforce/ts-types';
2424
import chalk from 'chalk';
2525
import { Ditamap } from '../../ditamap/ditamap.js';
26+
import { Markdoc } from '../../markdown/markdoc.js';
2627
import { Docs } from '../../docs.js';
2728
import { CliMeta, CommandClass, events, SfTopic, SfTopics } from '../../utils.js';
2829

@@ -63,6 +64,12 @@ export default class CommandReferenceGenerate extends SfCommand<CommandReference
6364
summary: messages.getMessage('flags.ditamap-suffix.summary'),
6465
default: Ditamap.SUFFIX,
6566
}),
67+
format: Flags.string({
68+
char: 'f',
69+
summary: 'output format for the generated documentation',
70+
options: ['dita', 'markdown'],
71+
default: 'dita',
72+
}),
6673
hidden: Flags.boolean({ summary: messages.getMessage('flags.hidden.summary') }),
6774
'error-on-warnings': Flags.boolean({
6875
summary: messages.getMessage('flags.error-on-warnings.summary'),
@@ -133,22 +140,36 @@ export default class CommandReferenceGenerate extends SfCommand<CommandReference
133140
.join(', ')}`
134141
);
135142

136-
Ditamap.outputDir = flags['output-dir'];
137-
138-
Ditamap.cliVersion = this.loadedConfig.version.replace(/-[0-9a-zA-Z]+$/, '');
139-
Ditamap.plugins = this.pluginMap(plugins);
140-
Ditamap.pluginVersions = plugins.map((name) => {
143+
const outputDir = flags['output-dir'];
144+
const cliVersion = this.loadedConfig.version.replace(/-[0-9a-zA-Z]+$/, '');
145+
const pluginsMap = this.pluginMap(plugins);
146+
const pluginVersions = plugins.map((name) => {
141147
const plugin = this.getPlugin(name);
142148
const version = plugin?.version;
143149
if (!version) throw new Error(`No version found for plugin ${name}`);
144150
return { name, version };
145151
});
152+
153+
if (flags.format === 'markdown') {
154+
Markdoc.outputDir = outputDir;
155+
Markdoc.cliVersion = cliVersion;
156+
Markdoc.plugins = pluginsMap;
157+
Markdoc.pluginVersions = pluginVersions;
158+
Markdoc.suffix = flags['ditamap-suffix'];
159+
} else {
160+
Ditamap.outputDir = outputDir;
161+
Ditamap.cliVersion = cliVersion;
162+
Ditamap.plugins = pluginsMap;
163+
Ditamap.pluginVersions = pluginVersions;
164+
Ditamap.suffix = flags['ditamap-suffix'];
165+
}
166+
146167
const commands = await this.loadCommands(plugins);
147168
const topicMetadata = this.loadTopicMetadata(commands);
148169
const cliMeta = this.loadCliMeta();
149170
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
150171
// @ts-ignore
151-
const docs = new Docs(Ditamap.outputDir, flags.hidden, topicMetadata, cliMeta);
172+
const docs = new Docs(outputDir, flags.hidden, topicMetadata, cliMeta, flags.format as 'dita' | 'markdown');
152173

153174
events.on('topic', ({ topic }: { topic: string }) => {
154175
this.log(chalk.green(`Generating topic '${topic}'`));
@@ -162,7 +183,7 @@ export default class CommandReferenceGenerate extends SfCommand<CommandReference
162183
});
163184

164185
await docs.build(commands);
165-
this.log(`\nWrote generated doc to ${Ditamap.outputDir}`);
186+
this.log(`\nWrote generated doc to ${outputDir}`);
166187

167188
if (flags['error-on-warnings'] && warnings.length > 0) {
168189
throw new SfError(`Found ${warnings.length} warnings.`);

src/docs.ts

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@ import fs from 'node:fs/promises';
1818
import { AnyJson, ensureString } from '@salesforce/ts-types';
1919
import chalk from 'chalk';
2020
import { BaseDitamap } from './ditamap/base-ditamap.js';
21-
import { CLIReference } from './ditamap/cli-reference.js';
22-
import { Command } from './ditamap/command.js';
23-
import { TopicCommands } from './ditamap/topic-commands.js';
21+
import { CLIReference as DitaCLIReference } from './ditamap/cli-reference.js';
22+
import { Command as DitaCommand } from './ditamap/command.js';
23+
import { TopicCommands as DitaTopicCommands } from './ditamap/topic-commands.js';
2424
import { TopicDitamap } from './ditamap/topic-ditamap.js';
25+
import { HelpReference as DitaHelpReference } from './ditamap/help-reference.js';
26+
import { BaseIndex } from './markdown/base-index.js';
27+
import { CLIReference as MarkdownCLIReference } from './markdown/cli-reference.js';
28+
import { Command as MarkdownCommand } from './markdown/command.js';
29+
import { TopicCommands as MarkdownTopicCommands } from './markdown/topic-commands.js';
30+
import { TopicIndex } from './markdown/topic-index.js';
31+
import { HelpReference as MarkdownHelpReference } from './markdown/help-reference.js';
2532
import { CliMeta, events, punctuate, SfTopic, SfTopics, CommandClass } from './utils.js';
26-
import { HelpReference } from './ditamap/help-reference.js';
2733

2834
type TopicsByTopicsByTopLevel = Map<string, Map<string, CommandClass[]>>;
2935

@@ -41,7 +47,8 @@ export class Docs {
4147
private outputDir: string,
4248
private hidden: boolean,
4349
private topicMeta: SfTopics,
44-
private cliMeta: CliMeta
50+
private cliMeta: CliMeta,
51+
private format: 'dita' | 'markdown' = 'dita'
4552
) {}
4653

4754
public async build(commands: CommandClass[]): Promise<void> {
@@ -110,8 +117,13 @@ export class Docs {
110117
// The topic ditamap with all of the subtopic links.
111118
events.emit('subtopics', topic, subTopicNames);
112119

113-
await new TopicCommands(topic, topicMeta).write();
114-
await new TopicDitamap(topic, commandIds).write();
120+
if (this.format === 'markdown') {
121+
await new MarkdownTopicCommands(topic, topicMeta).write();
122+
await new TopicIndex(topic, commandIds).write();
123+
} else {
124+
await new DitaTopicCommands(topic, topicMeta).write();
125+
await new TopicDitamap(topic, commandIds).write();
126+
}
115127
return subTopicNames;
116128
}
117129

@@ -177,11 +189,15 @@ export class Docs {
177189
private async populateTemplate(commands: CommandClass[]): Promise<void> {
178190
const topicsAndSubtopics = this.groupTopicsAndSubtopics(commands);
179191

180-
await new CLIReference().write();
181-
await new HelpReference().write();
182-
183-
// Generate one base file with all top-level topics.
184-
await new BaseDitamap(Array.from(topicsAndSubtopics.keys())).write();
192+
if (this.format === 'markdown') {
193+
await new MarkdownCLIReference().write();
194+
await new MarkdownHelpReference().write();
195+
await new BaseIndex(Array.from(topicsAndSubtopics.keys())).write();
196+
} else {
197+
await new DitaCLIReference().write();
198+
await new DitaHelpReference().write();
199+
await new BaseDitamap(Array.from(topicsAndSubtopics.keys())).write();
200+
}
185201

186202
for (const [topic, subtopics] of topicsAndSubtopics.entries()) {
187203
events.emit('topic', { topic });
@@ -240,8 +256,14 @@ export class Docs {
240256
return '';
241257
}
242258

243-
const commandDitamap = new Command(topic, subtopic, command, commandMeta);
244-
await commandDitamap.write();
245-
return commandDitamap.getFilename();
259+
if (this.format === 'markdown') {
260+
const commandMarkdown = new MarkdownCommand(topic, subtopic, command, commandMeta);
261+
await commandMarkdown.write();
262+
return commandMarkdown.getFilename();
263+
} else {
264+
const commandDitamap = new DitaCommand(topic, subtopic, command, commandMeta);
265+
await commandDitamap.write();
266+
return commandDitamap.getFilename();
267+
}
246268
}
247269
}

0 commit comments

Comments
 (0)