Skip to content
Open
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@ package.json.bak.

/tmp
/test/tmp
/test/tmp-md
6 changes: 5 additions & 1 deletion messages/main.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ fail the command if there are any warnings

# flags.ditamap-suffix.summary

unique suffix to append to generated ditamap
unique suffix to append to generated DITA files

# flags.output-format.summary

output format for generated documentation; 'dita' (default) generates DITA XML files, 'markdown' generates Markdown files

# flags.config-path.summary

Expand Down
14 changes: 13 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@
],
"output": [],
"dependencies": [
"test:command-reference"
"test:command-reference",
"test:command-reference-markdown"
]
},
"test:command-reference": {
Expand Down Expand Up @@ -196,6 +197,17 @@
"messages/**/*.md"
],
"output": []
},
"test:command-reference-markdown": {
"command": "node --loader ts-node/esm --no-warnings=ExperimentalWarning \"./bin/dev.js\" commandreference generate --plugins auth --plugins user --output-format markdown --output-dir test/tmp-md",
"files": [
"src/**/*.ts",
"messages/**",
"package.json"
],
"output": [
"test/tmp-md"
]
}
}
}
15 changes: 12 additions & 3 deletions src/commands/commandreference/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ export default class CommandReferenceGenerate extends SfCommand<CommandReference
summary: messages.getMessage('flags.config-path.summary'),
char: 'c',
}),
'output-format': Flags.string({
summary: messages.getMessage('flags.output-format.summary'),
options: ['dita', 'markdown'],
default: 'dita',
}),
};

private loadedConfig!: Interfaces.Config;
Expand Down Expand Up @@ -146,9 +151,13 @@ export default class CommandReferenceGenerate extends SfCommand<CommandReference
const commands = await this.loadCommands(plugins);
const topicMetadata = this.loadTopicMetadata(commands);
const cliMeta = this.loadCliMeta();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const docs = new Docs(Ditamap.outputDir, flags.hidden, topicMetadata, cliMeta);
const docs = new Docs(
Ditamap.outputDir,
flags['output-format'] as 'dita' | 'markdown',
flags.hidden,
topicMetadata ?? new Map<string, never>(),
cliMeta
);

events.on('topic', ({ topic }: { topic: string }) => {
this.log(chalk.green(`Generating topic '${topic}'`));
Expand Down
100 changes: 100 additions & 0 deletions src/ditamap/command-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2026, Salesforce, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Dictionary, Optional } from '@salesforce/ts-types';
import { CommandParameterData, replaceConfigVariables } from '../utils.js';

export type FlagInfo = {
hidden: boolean;
description: string;
summary: string;
required: boolean;
kind: string;
type: string;
defaultHelpValue?: string;
default: string | (() => Promise<string>);
aliases?: string[];
options?: string[];
char?: string;
deprecated?: { version: string; to: string };
};

export const getDefault = async (flag: FlagInfo, flagName: string): Promise<string> => {
if (!flag) {
return '';
}
if (flagName === 'target-org' || flagName === 'target-dev-hub') {
return '';
}
if (typeof flag.default === 'function') {
try {
const help = await flag.default();
return help.includes('[object Object]') ? '' : help ?? '';
} catch {
return '';
}
} else {
return flag.default;
}
};

export const flagIsDefined = (input: [string, Optional<FlagInfo>]): input is [string, FlagInfo] =>
input[1] !== undefined;

export const buildDescription =
(commandName: string) =>
(binary: string) =>
(flag: FlagInfo): string[] => {
const description = replaceConfigVariables(
Array.isArray(flag?.description) ? flag?.description.join('\n') : flag?.description ?? '',
binary,
commandName
);
return formatParagraphs(
flag.summary ? `${replaceConfigVariables(flag.summary, binary, commandName)}\n${description}` : description
);
};

export const formatParagraphs = (textToFormat?: string): string[] =>
textToFormat ? textToFormat.split('\n').filter((n) => n !== '') : [];

export const readBinary = (commandMeta: Record<string, unknown>): string =>
'binary' in commandMeta && typeof commandMeta.binary === 'string' ? commandMeta.binary : 'unknown';

export const buildCommandParameters = async (
commandName: string,
binary: string,
flags: Dictionary<FlagInfo>
): Promise<CommandParameterData[]> => {
const descriptionBuilder = buildDescription(commandName)(binary);
return Promise.all(
[...Object.entries(flags)]
.filter(flagIsDefined)
.filter(([, flag]) => !flag.hidden)
.map(
async ([flagName, flag]) =>
({
...flag,
name: flagName,
description: descriptionBuilder(flag),
optional: !flag.required,
kind: flag.kind ?? flag.type,
hasValue: flag.type !== 'boolean',
defaultFlagValue: await getDefault(flag, flagName),
} satisfies CommandParameterData)
)
);
};
81 changes: 4 additions & 77 deletions src/ditamap/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,10 @@
*/

import { join } from 'node:path';
import { asString, Dictionary, ensureObject, ensureString, Optional } from '@salesforce/ts-types';
import { CommandClass, CommandData, CommandParameterData, punctuate, replaceConfigVariables } from '../utils.js';
import { asString, Dictionary, ensureObject, ensureString } from '@salesforce/ts-types';
import { CommandClass, CommandData, punctuate, replaceConfigVariables } from '../utils.js';
import { Ditamap } from './ditamap.js';

type FlagInfo = {
hidden: boolean;
description: string;
summary: string;
required: boolean;
kind: string;
type: string;
defaultHelpValue?: string;
default: string | (() => Promise<string>);
};

const getDefault = async (flag: FlagInfo, flagName: string): Promise<string> => {
if (!flag) {
return '';
}
if (flagName === 'target-org' || flagName === 'target-dev-hub') {
// special handling to prevent global/local default usernames from appearing in the docs, but they do appear in user's help
return '';
}
if (typeof flag.default === 'function') {
try {
const help = await flag.default();
return help.includes('[object Object]') ? '' : help ?? '';
} catch {
return '';
}
} else {
return flag.default;
}
};
import { buildCommandParameters, FlagInfo, readBinary, formatParagraphs } from './command-helpers.js';

export class Command extends Ditamap {
private flags: Dictionary<FlagInfo>;
Expand Down Expand Up @@ -131,57 +101,14 @@ export class Command extends Ditamap {
this.destination = join(Ditamap.outputDir, topic, filename);
}

public async getParametersForTemplate(flags: Dictionary<FlagInfo>): Promise<CommandParameterData[]> {
const descriptionBuilder = buildDescription(this.commandName)(readBinary(this.commandMeta));
return Promise.all(
[...Object.entries(flags)]
.filter(flagIsDefined)
.filter(([, flag]) => !flag.hidden)
.map(
async ([flagName, flag]) =>
({
...flag,
name: flagName,
description: descriptionBuilder(flag),
optional: !flag.required,
kind: flag.kind ?? flag.type,
hasValue: flag.type !== 'boolean',
defaultFlagValue: await getDefault(flag, flagName),
} satisfies CommandParameterData)
)
);
}

// eslint-disable-next-line class-methods-use-this
public getTemplateFileName(): string {
return 'command.hbs';
}

protected async transformToDitamap(): Promise<string> {
const parameters = await this.getParametersForTemplate(this.flags);
const parameters = await buildCommandParameters(this.commandName, readBinary(this.commandMeta), this.flags);
this.data = Object.assign({}, this.data, { parameters });
return super.transformToDitamap();
}
}

const flagIsDefined = (input: [string, Optional<FlagInfo>]): input is [string, FlagInfo] => input[1] !== undefined;

const buildDescription =
(commandName: string) =>
(binary: string) =>
(flag: FlagInfo): string[] => {
const description = replaceConfigVariables(
Array.isArray(flag?.description) ? flag?.description.join('\n') : flag?.description ?? '',
binary,
commandName
);
return formatParagraphs(
flag.summary ? `${replaceConfigVariables(flag.summary, binary, commandName)}\n${description}` : description
);
};

const formatParagraphs = (textToFormat?: string): string[] =>
textToFormat ? textToFormat.split('\n').filter((n) => n !== '') : [];

const readBinary = (commandMeta: Record<string, unknown>): string =>
'binary' in commandMeta && typeof commandMeta.binary === 'string' ? commandMeta.binary : 'unknown';
Loading
Loading