/* eslint-disable no-console */ /** * @file Transform documentation source files into files suitable for publishing and optionally copy * the transformed files from the source directory to the directory used for the final, published * documentation directory. The list of files transformed and copied to final documentation * directory are logged to the console. If a file in the source directory has the same contents in * the final directory, nothing is done (the final directory is up-to-date). * @example * docs * Run with no option flags * * @example * docs --verify * If the --verify option is used, it only _verifies_ that the final directory has been updated with the transformed files in the source directory. * No files will be copied to the final documentation directory, but the list of files to be changed is shown on the console. * If the final documentation directory does not have the transformed files from source directory * - a message to the console will show that this command should be run without the --verify flag so that the final directory is updated, and * - it will return a fail exit code (1) * * @example * docs --git * If the --git option is used, the command `git add docs` will be run after all transformations (and/or verifications) have completed successfully * If not files were transformed, the git command is not run. * * @todo Ensure that the documentation source and final paths are correct by using process.cwd() to * get their absolute paths. Ensures that the location of those 2 directories is not dependent on * where this file resides. * * @todo Write a test file for this. (Will need to be able to deal .mts file. Jest has trouble with * it.) */ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; import { exec } from 'child_process'; import { globby } from 'globby'; import { JSDOM } from 'jsdom'; import type { Code, Root } from 'mdast'; import { join, dirname } from 'path'; // @ts-ignore import prettier from 'prettier'; import { remark } from 'remark'; // @ts-ignore import flatmap from 'unist-util-flatmap'; const SOURCE_DOCS_DIR = 'src/docs'; const FINAL_DOCS_DIR = 'docs'; const AUTOGENERATED_TEXT = `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. Please edit the corresponding file in ${SOURCE_DOCS_DIR}.`; const LOGMSG_TRANSFORMED = 'transformed'; const LOGMSG_TO_BE_TRANSFORMED = 'to be transformed'; const LOGMSG_COPIED = ` ...and copied to ${FINAL_DOCS_DIR}`; const WARN_DOCSDIR_DOESNT_MATCH = `Changed files were transformed in ${SOURCE_DOCS_DIR} but do not match the files in ${FINAL_DOCS_DIR}. Please run yarn docs:build after making changes to ${SOURCE_DOCS_DIR} to update the ${FINAL_DOCS_DIR} directory with the transformed files.`; const verifyOnly: boolean = process.argv.includes('--verify'); const git: boolean = process.argv.includes('--git'); // TODO: Read from .prettierrc? const prettierConfig: prettier.Config = { useTabs: false, tabWidth: 2, endOfLine: 'auto', printWidth: 100, singleQuote: true, }; let filesWereTransformed = false; /** * Given a source file name and path, return the documentation destination full path and file name * Create the destination path if it does not already exist. * * @param {string} file - Name of the file (including full path) * @returns {string} Name of the file with the path changed from the source directory to final * documentation directory * @todo Possible Improvement: combine with lint-staged to only copy files that have changed */ const changeToFinalDocDir = (file: string): string => { const newDir = file.replace(SOURCE_DOCS_DIR, FINAL_DOCS_DIR); mkdirSync(dirname(newDir), { recursive: true }); return newDir; }; /** * Log messages to the console showing if the transformed file copied to the final documentation * directory or still needs to be copied. * * @param {string} filename Name of the file that was transformed * @param {boolean} wasCopied Whether or not the file was copied */ const logWasOrShouldBeTransformed = (filename: string, wasCopied: boolean) => { let changeMsg: string; let logMsg: string; changeMsg = wasCopied ? LOGMSG_TRANSFORMED : LOGMSG_TO_BE_TRANSFORMED; logMsg = ` File ${changeMsg}: ${filename}`; if (wasCopied) { logMsg += LOGMSG_COPIED; } console.log(logMsg); }; /** * If the file contents were transformed, set the _filesWereTransformed_ flag to true and copy the * transformed contents to the final documentation directory if the doCopy flag is true. Log * messages to the console. * * @param {string} filename Name of the file that will be verified * @param {string} [transformedContent] New contents for the file * @param {boolean} [doCopy=false] Whether we should copy that transformedContents to the final * documentation directory. Default is `false` */ const copyTransformedContents = ( filename: string, doCopy: boolean = false, transformedContent?: string ) => { const fileInFinalDocDir = changeToFinalDocDir(filename); const existingBuffer = existsSync(fileInFinalDocDir) ? readFileSync(fileInFinalDocDir) : Buffer.from('#NEW FILE#'); const newBuffer = transformedContent ? Buffer.from(transformedContent) : readFileSync(filename); if (existingBuffer.equals(newBuffer)) { return; // Files are same, skip. } filesWereTransformed = true; if (doCopy) { writeFileSync(fileInFinalDocDir, newBuffer); } logWasOrShouldBeTransformed(fileInFinalDocDir, doCopy); }; const readSyncedUTF8file = (filename: string): string => { return readFileSync(filename, 'utf8'); }; /** * Transform a markdown file and write the transformed file to the directory for published * documentation * * 1. Add a `mermaid-example` block before every `mermaid` or `mmd` block On the docsify site (one * place where the documentation is published), this will show the code used for the mermaid * diagram * 2. Add the text that says the file is automatically generated * 3. Use prettier to format the file Verify that the file has been changed and write out the changes * * @param file {string} name of the file that will be verified */ const transformMarkdown = (file: string) => { const doc = readSyncedUTF8file(file); const ast: Root = remark.parse(doc); const out = flatmap(ast, (c: Code) => { if (c.type !== 'code' || !c.lang?.startsWith('mermaid')) { return [c]; } if (c.lang === 'mermaid' || c.lang === 'mmd') { c.lang = 'mermaid-example'; } return [c, Object.assign({}, c, { lang: 'mermaid' })]; }); // Add the AUTOGENERATED_TEXT to the start of the file const transformed = `${AUTOGENERATED_TEXT}\n${remark.stringify(out)}`; const formatted = prettier.format(transformed, { parser: 'markdown', ...prettierConfig, }); copyTransformedContents(file, !verifyOnly, formatted); }; /** * Transform an HTML file and write the transformed file to the directory for published * documentation * * - Add the text that says the file is automatically generated Verify that the file has been changed * and write out the changes * * @param filename {string} name of the HTML file to transform */ const transformHtml = (filename: string) => { /** * Insert the '...auto generated...' comment into an HTML file after the element * * @param fileName {string} file name that should have the comment inserted * @returns {string} The contents of the file with the comment inserted */ const insertAutoGeneratedComment = (fileName: string): string => { const fileContents = readSyncedUTF8file(fileName); const jsdom = new JSDOM(fileContents); const htmlDoc = jsdom.window.document; const autoGeneratedComment = jsdom.window.document.createComment(AUTOGENERATED_TEXT); const rootElement = htmlDoc.documentElement; rootElement.prepend(autoGeneratedComment); return jsdom.serialize(); }; const transformedHTML = insertAutoGeneratedComment(filename); const formattedHTML = prettier.format(transformedHTML, { parser: 'html', ...prettierConfig, }); copyTransformedContents(filename, !verifyOnly, formattedHTML); }; /** Main method (entry point) */ (async () => { const sourceDirGlob = join('.', SOURCE_DOCS_DIR, '**'); const includeFilesStartingWithDot = true; console.log('Transforming markdown files...'); const mdFiles = await globby([join(sourceDirGlob, '*.md')], { dot: includeFilesStartingWithDot }); mdFiles.forEach(transformMarkdown); console.log('Transforming html files...'); const htmlFiles = await globby([join(sourceDirGlob, '*.html')], { dot: includeFilesStartingWithDot, }); htmlFiles.forEach(transformHtml); console.log('Transforming all other files...'); const otherFiles = await globby([sourceDirGlob, '!**/*.md', '!**/*.html'], { dot: includeFilesStartingWithDot, }); otherFiles.forEach((file: string) => { copyTransformedContents(file, !verifyOnly); // no transformation }); if (filesWereTransformed) { if (verifyOnly) { console.log(WARN_DOCSDIR_DOESNT_MATCH); process.exit(1); } if (git) { console.log('Adding changes in ${FINAL_DOCS_DIR} folder to git'); exec('git add docs'); } } })();