Skip to content

Commit

Permalink
Merge pull request #79 from mbland/jsdoc-cli-wrapper-js
Browse files Browse the repository at this point in the history
Add jsdoc-cli-wrapper.js
  • Loading branch information
mbland authored Dec 26, 2023
2 parents 1d62ea7 + 895e744 commit 82dded5
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 4 deletions.
183 changes: 183 additions & 0 deletions strcalc/src/main/frontend/bin/jsdoc-cli-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/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 <[email protected]>
*/

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 {exitCode, indexHtml} = await runJsdoc(
process.argv.slice(2), process.env, process.platform
)
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
* @param {string} platform - the process.platform string
* @returns {Promise<RunJsdocResults>} - result of `jsdoc` execution
*/
async function runJsdoc(argv, env, platform) {
let jsdocPath

try {
jsdocPath = await getPath('jsdoc', env, platform)
} 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
* @param {string} platform - the process.platform string
* @returns {Promise<string>} - path to the command
*/
async function getPath(cmdName, env, platform) {
for (const p of env.PATH.split(path.delimiter)) {
// 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
} 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<ArgvResults>} - 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<string>} - 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}`)
}
2 changes: 1 addition & 1 deletion strcalc/src/main/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "node bin/jsdoc-cli-wrapper.js -c ./jsdoc.json ."
},
"devDependencies": {
"@rollup/pluginutils": "^5.1.0",
Expand Down
6 changes: 3 additions & 3 deletions strcalc/src/main/frontend/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit 82dded5

Please sign in to comment.