Skip to content
Open
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
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