From d95cb520f1c9c51d34751cc6be636b67f7610c41 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Tue, 26 Dec 2023 15:32:13 -0500 Subject: [PATCH 1/3] Add jsdoc-cli-wrapper.js This is a pure Node.js implementation of the original bin/jsdoc Bash shell wrapper. It's more robust and more portable, including to Windows. My next move is to extract this into its own published npm package. --- .../main/frontend/bin/jsdoc-cli-wrapper.js | 176 ++++++++++++++++++ strcalc/src/main/frontend/package.json | 2 +- 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100755 strcalc/src/main/frontend/bin/jsdoc-cli-wrapper.js diff --git a/strcalc/src/main/frontend/bin/jsdoc-cli-wrapper.js b/strcalc/src/main/frontend/bin/jsdoc-cli-wrapper.js new file mode 100755 index 0000000..0d1eee1 --- /dev/null +++ b/strcalc/src/main/frontend/bin/jsdoc-cli-wrapper.js @@ -0,0 +1,176 @@ +#!/usr/bin/env node +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +/** + * @file JSDoc command line interface wrapper. + * + * Removes the existing destination directory if it exists, runs JSDoc, and + * emits the relative path to the generated index.html file. + * @author Mike Bland + */ + +import { spawn } from 'node:child_process' +import { access, readdir, readFile, rm } from 'node:fs/promises' +import path from 'node:path' +import { exit, stdout } from 'node:process' + +try { + const jsdocArgs = process.argv.slice(2) + const {exitCode, indexHtml} = await runJsdoc(jsdocArgs, process.env) + if (indexHtml !== undefined) stdout.write(`${indexHtml}\n`) + exit(exitCode) + +} catch (err) { + console.error(err) + exit(1) +} + +/** + * Result of the `jsdoc` execution + * @typedef {object} RunJsdocResults + * @property {number} exitCode - 0 on success, nonzero on failure + * @property {string} indexHtml - path to the generated index.html file + */ + +/** + * Removes the existing JSDoc directory, runs `jsdoc`, and emits the result path + * @param {string[]} argv - JSDoc command line interface arguments + * @param {object} env - environment variables, presumably process.env + * @returns {Promise} - result of `jsdoc` execution + */ +async function runJsdoc(argv, env) { + let jsdocPath + + try { + jsdocPath = await getPath('jsdoc', env) + } catch { + return Promise.reject( + 'Run \'pnpm add -g jsdoc\' to install JSDoc: https://jsdoc.app\n' + ) + } + + const {destination, willGenerate} = await analyzeArgv(argv) + + if (willGenerate) await rm(destination, {force: true, recursive: true}) + + const exitCode = await new Promise(resolve => { + spawn(jsdocPath, argv, {stdio: 'inherit'}) + .on('close', code => resolve(code)) + }) + + try { + if (exitCode === 0 && willGenerate) { + return {exitCode, indexHtml: await findFile(destination, 'index.html')} + } + } catch { + // If jsdoc finds no input files, it won't create the destination directory. + // It will print "There are no input files to process." and exit with 0. + } + return {exitCode} +} + +/** + * Returns the full path to the specified command + * @param {string} cmdName - command to find in env.PATH + * @param {object} env - environment variables, presumably process.env + * @param {string} env.PATH - the PATH environment variable + * @returns {Promise} - path to the command + */ +async function getPath(cmdName, env) { + for (const p of env.PATH.split(path.delimiter)) { + const candidate = path.join(p, cmdName) + try { + await access(candidate) + return candidate + } catch { /* try next candidate */ } + } + return Promise.reject(`${cmdName} not found in PATH`) +} + +/** + * Results from analyzing JSDoc command line arguments + * @typedef {object} ArgvResults + * @property {string} destination - the JSDoc destination directory + * @property {boolean} willGenerate - true unless --help or --version present + */ + +/** + * Analyzes JSDoc CLI args to determine if JSDoc will generate docs and where + * @param {string[]} argv - JSDoc command line interface arguments + * @returns {Promise} - analysis results + */ +async function analyzeArgv(argv) { + let destination = undefined + let willGenerate = true + let cmdLineDest = false + + for (let i = 0; i !== argv.length; ++i) { + const arg = argv[i] + const nextArg = argv[i+1] + let config = null + + switch (arg) { + case '-c': + case '--configure': + if (!cmdLineDest && nextArg !== undefined) { + config = JSON.parse(await readFile(nextArg)) + if (config.opts !== undefined) { + destination = config.opts.destination + } + } + break + + case '-d': + case '--destination': + if (nextArg !== undefined && !nextArg.startsWith('-')) { + destination = nextArg + cmdLineDest = true + } + break + + case '-h': + case '--help': + case '-v': + case '--version': + willGenerate = false + break + } + } + + // "out" is the JSDoc default directory. + destination ??= 'out' + return {willGenerate, destination} +} + +/** + * Searches for filename within a directory tree via breadth-first search + * @param {string} dirname - current directory to search + * @param {string} filename - name of file to find + * @returns {Promise} - path to filename within dirname + */ +async function findFile(dirname, filename) { + const childDirs = [dirname] + let curDir + + while ((curDir = childDirs.shift()) !== undefined) { + // This should be `for await (const entry of readdir(...))`: + // + // - https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/for-await...of + // + // But Node 20.10.0 errors with: + // + // TypeError: readdir(...) is not a function or its return value is not + // async iterable + const entries = await readdir(curDir, {withFileTypes: true}) + for (const entry of entries) { + const childPath = path.join(curDir, entry.name) + if (entry.name === filename) return childPath + if (entry.isDirectory()) childDirs.push(childPath) + } + } + return Promise.reject(`failed to find ${filename} in ${dirname}`) +} diff --git a/strcalc/src/main/frontend/package.json b/strcalc/src/main/frontend/package.json index 4f008be..52b3d4d 100644 --- a/strcalc/src/main/frontend/package.json +++ b/strcalc/src/main/frontend/package.json @@ -29,7 +29,7 @@ "test-ui": "vitest --ui --coverage", "test-ci": "eslint --color --max-warnings 0 . && vitest run --config ci/vitest.config.js && vitest run --config ci/vitest.config.browser.js", "coverage": "vitest run --coverage", - "jsdoc": "bin/jsdoc -c ./jsdoc.json ." + "jsdoc": "bin/jsdoc-cli-wrapper.js -c ./jsdoc.json ." }, "devDependencies": { "@rollup/pluginutils": "^5.1.0", From 046fb5c70cfaac10ee0e286ac5bebe4a44b8519f Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Tue, 26 Dec 2023 16:04:05 -0500 Subject: [PATCH 2/3] Get 'pnpm jsdoc' working on Windows For one thing, Windows can't interpret the `#!/usr/bin/env node` header, nor can it grok the `bin/jsdoc-cli-wrapper.js` path directly. Prefixing the script with `node` fixes the second problem. Updating getPath() to detect the 'win32' platform and adding the .CMD extension to the candidate path fixes the first. This is because 'pnpm install' will create a CMD.EXE wrapper for the installed script on Windows. --- .../main/frontend/bin/jsdoc-cli-wrapper.js | 19 +++++++++++++------ strcalc/src/main/frontend/package.json | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/strcalc/src/main/frontend/bin/jsdoc-cli-wrapper.js b/strcalc/src/main/frontend/bin/jsdoc-cli-wrapper.js index 0d1eee1..630056a 100755 --- a/strcalc/src/main/frontend/bin/jsdoc-cli-wrapper.js +++ b/strcalc/src/main/frontend/bin/jsdoc-cli-wrapper.js @@ -19,8 +19,9 @@ import path from 'node:path' import { exit, stdout } from 'node:process' try { - const jsdocArgs = process.argv.slice(2) - const {exitCode, indexHtml} = await runJsdoc(jsdocArgs, process.env) + const {exitCode, indexHtml} = await runJsdoc( + process.argv.slice(2), process.env, process.platform + ) if (indexHtml !== undefined) stdout.write(`${indexHtml}\n`) exit(exitCode) @@ -40,13 +41,14 @@ try { * Removes the existing JSDoc directory, runs `jsdoc`, and emits the result path * @param {string[]} argv - JSDoc command line interface arguments * @param {object} env - environment variables, presumably process.env + * @param {string} platform - the process.platform string * @returns {Promise} - result of `jsdoc` execution */ -async function runJsdoc(argv, env) { +async function runJsdoc(argv, env, platform) { let jsdocPath try { - jsdocPath = await getPath('jsdoc', env) + jsdocPath = await getPath('jsdoc', env, platform) } catch { return Promise.reject( 'Run \'pnpm add -g jsdoc\' to install JSDoc: https://jsdoc.app\n' @@ -78,11 +80,16 @@ async function runJsdoc(argv, env) { * @param {string} cmdName - command to find in env.PATH * @param {object} env - environment variables, presumably process.env * @param {string} env.PATH - the PATH environment variable + * @param {string} platform - the process.platform string * @returns {Promise} - path to the command */ -async function getPath(cmdName, env) { +async function getPath(cmdName, env, platform) { for (const p of env.PATH.split(path.delimiter)) { - const candidate = path.join(p, cmdName) + // pnpm will install both the original script and versions ending with .CMD + // and .ps1. We'll just default to .CMD. + const extension = (platform === 'win32') ? '.CMD' : '' + const candidate = path.join(p, cmdName) + extension + try { await access(candidate) return candidate diff --git a/strcalc/src/main/frontend/package.json b/strcalc/src/main/frontend/package.json index 52b3d4d..c96be32 100644 --- a/strcalc/src/main/frontend/package.json +++ b/strcalc/src/main/frontend/package.json @@ -29,7 +29,7 @@ "test-ui": "vitest --ui --coverage", "test-ci": "eslint --color --max-warnings 0 . && vitest run --config ci/vitest.config.js && vitest run --config ci/vitest.config.browser.js", "coverage": "vitest run --coverage", - "jsdoc": "bin/jsdoc-cli-wrapper.js -c ./jsdoc.json ." + "jsdoc": "node bin/jsdoc-cli-wrapper.js -c ./jsdoc.json ." }, "devDependencies": { "@rollup/pluginutils": "^5.1.0", From 895e74401600b6045ed57b68207765f8fef55b57 Mon Sep 17 00:00:00 2001 From: Mike Bland Date: Tue, 26 Dec 2023 16:47:31 -0500 Subject: [PATCH 3/3] Disable coverage for bin/jsdoc-cli-wrapper.js Don't want to drag down the coverage percentage for the main application. But I am about to break both jsdoc-cli-wrapper.js and rollup-plugin-handlebars-precompile into their own npm packages very soon. --- strcalc/src/main/frontend/vite.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/strcalc/src/main/frontend/vite.config.js b/strcalc/src/main/frontend/vite.config.js index 47812c3..7aba655 100644 --- a/strcalc/src/main/frontend/vite.config.js +++ b/strcalc/src/main/frontend/vite.config.js @@ -88,9 +88,9 @@ export default defineConfig({ outputFile: buildDir('test-results/test-frontend/TESTS-TestSuites.xml'), coverage: { reportsDirectory: buildDir('reports/frontend/coverage'), - // Remove 'exclude:' once rollup-plugin-handlebars-precompile moves - // into its own repository. - exclude: [ ...configDefaults.coverage.exclude, 'plugins/*' ] + // Remove 'exclude:' once rollup-plugin-handlebars-precompile + // and bin/jsdoc-cli-wrapper.js move into their own repositories. + exclude: [ ...configDefaults.coverage.exclude, 'plugins/*', 'bin/*' ] }, browser: { name: 'chrome',