markdown-code-example-inserter
Version:
Syncs code examples with markdown documentation.
157 lines (156 loc) • 5.68 kB
JavaScript
import { extractRelevantArgs, replaceWithWindowsPathIfNeeded } from '@augment-vir/node';
import { glob } from 'glob';
import { existsSync } from 'node:fs';
import { relative, resolve } from 'node:path';
import { createOrderedLogging } from '../augments/console.js';
import { MarkdownCodeExampleInserterError } from '../errors/markdown-code-example-inserter.error.js';
import { OutOfDateInsertedCodeError } from '../errors/out-of-date-inserted-code.error.js';
import { isCodeUpdated, writeAllExamples } from '../example-inserter/example-inserter.js';
/**
* Flag for setting a specific index file.
*
* @category Internals
*/
export const forceIndexTrigger = '--index';
const ignoreTrigger = '--ignore';
const silentTrigger = '--silent';
const checkOnlyTrigger = '--check';
/**
* Parse all CLI args out of a raw CLI arg string.
*
* @category Internals
*/
export async function parseArgs(rawArgs, filePath) {
const args = extractRelevantArgs({
rawArgs,
binName: 'md-code',
fileName: filePath || 'cli.js',
});
let forceIndex = undefined;
let silent = false;
const inputFiles = [];
const globs = [];
const ignoreList = [];
let checkOnly = false;
let lastArgWasForceIndexTrigger = false;
let lastArgWasIgnoreTrigger = false;
args.forEach((arg) => {
if (arg === forceIndexTrigger && forceIndex != undefined) {
throw new MarkdownCodeExampleInserterError('Cannot have multiple index paths');
}
else if (arg === forceIndexTrigger) {
lastArgWasForceIndexTrigger = true;
}
else if (lastArgWasForceIndexTrigger) {
forceIndex = replaceWithWindowsPathIfNeeded(arg);
lastArgWasForceIndexTrigger = false;
}
else if (arg === ignoreTrigger) {
lastArgWasIgnoreTrigger = true;
}
else if (arg === checkOnlyTrigger && checkOnly) {
throw new MarkdownCodeExampleInserterError(`${checkOnlyTrigger} accidentally duplicated in your inputs`);
}
else if (arg === checkOnlyTrigger) {
checkOnly = true;
}
else if (lastArgWasIgnoreTrigger) {
ignoreList.push(arg);
lastArgWasIgnoreTrigger = false;
}
else if (arg === silentTrigger) {
silent = true;
}
else if (existsSync(arg)) {
inputFiles.push(relative(process.cwd(), arg));
}
else {
globs.push(arg);
}
});
await Promise.all(globs.map(async (globString) => {
const paths = await glob(globString, {
ignore: [
...ignoreList,
'**/node_modules/**',
],
nodir: true,
follow: true,
nocase: true,
});
if (paths.length) {
inputFiles.push(...paths.map((path) => {
return relative(process.cwd(), path);
}));
}
}));
const uniqueFiles = Array.from(new Set(inputFiles)).sort();
return {
forceIndex,
silent,
checkOnly,
files: uniqueFiles,
};
}
/**
* Run the `md-code` CLI.
*
* @category Main
*/
export async function runCli({ cwd = process.cwd(), rawArgs, cliFilePath = 'cli.js', }) {
const args = await parseArgs(rawArgs, cliFilePath);
if (!args.files.length) {
throw new MarkdownCodeExampleInserterError('No markdown files given to insert code into.');
}
if (!args.silent) {
if (args.checkOnly) {
console.info(`Checking that code in markdown is up to date:`);
}
else {
console.info(`Inserting code into markdown:`);
}
}
const errors = [];
const orderedLog = createOrderedLogging();
await Promise.all(args.files.map(async (relativeFilePath, index) => {
try {
if (args.checkOnly) {
const upToDate = await isCodeUpdated(resolve(relativeFilePath), cwd, args.forceIndex);
if (upToDate) {
if (!args.silent) {
orderedLog(index, console.info, ` ${relativeFilePath}: up to date`);
}
}
else {
if (!args.silent) {
orderedLog(index, console.error, ` ${relativeFilePath}: NOT up to date`);
}
errors.push(new OutOfDateInsertedCodeError(`${relativeFilePath} is not update to date.`));
}
}
else {
if (!args.silent) {
orderedLog(index, console.info, ` ${relativeFilePath}`);
}
await writeAllExamples(resolve(relativeFilePath), cwd, args.forceIndex);
}
}
catch (error) {
const errorWrapper = new MarkdownCodeExampleInserterError(`Errored on ${relativeFilePath}: ${String(error)}`);
console.error(errorWrapper.message);
errors.push(errorWrapper);
}
}));
if (errors.length) {
if (
/** Weird necessary as cast to prevent TypeScript's over-exuberant type guarding. */
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
errors.every((error) => error instanceof OutOfDateInsertedCodeError)) {
throw new OutOfDateInsertedCodeError('Code in Markdown file(s) is out of date. Run without --check to update.');
}
else {
errors.forEach((error) => console.error(error));
throw new MarkdownCodeExampleInserterError(`Code insertion into Markdown failed.`);
}
}
}