/* eslint-disable indent, no-process-exit */ /** * Helper methods for running JSDoc on the command line. * * A few critical notes for anyone who works on this module: * * + The module should really export an instance of `cli`, and `props` should be properties of a * `cli` instance. * * @private */ module.exports = (() => { const app = require('jsdoc/app'); const env = require('jsdoc/env'); const logger = require('jsdoc/util/logger'); const stripBom = require('jsdoc/util/stripbom'); const stripJsonComments = require('strip-json-comments'); const Promise = require('bluebird'); const props = { docs: [], packageJson: null, shouldExitWithError: false, tmpdir: null }; const FATAL_ERROR_MESSAGE = 'Exiting JSDoc because an error occurred. See the previous log ' + 'messages for details.'; const cli = {}; // TODO: docs cli.setVersionInfo = () => { const fs = require('fs'); const path = require('path'); // allow this to throw--something is really wrong if we can't read our own package file const info = JSON.parse( stripBom.strip(fs.readFileSync(path.join(env.dirname, 'package.json'), 'utf8')) ); env.version = { number: info.version, revision: new Date( parseInt(info.revision, 10) ).toUTCString() }; return cli; }; // TODO: docs cli.loadConfig = () => { const _ = require('underscore'); const args = require('jsdoc/opts/args'); const Config = require('jsdoc/config'); let config; const fs = require('jsdoc/fs'); const path = require('jsdoc/path'); let confPath; let isFile; const defaultOpts = { destination: './out/', encoding: 'utf8' }; try { env.opts = args.parse(env.args); } catch (e) { console.error(`${e.message}\n`); cli.printHelp().then(() => { cli.exit(1); }); } confPath = env.opts.configure || path.join(env.dirname, 'conf.json'); try { isFile = fs.statSync(confPath).isFile(); } catch (e) { isFile = false; } if ( !isFile && !env.opts.configure ) { confPath = path.join(env.dirname, 'conf.json.EXAMPLE'); } try { switch ( path.extname(confPath) ) { case '.js': config = require( path.resolve(confPath) ) || {}; break; case '.json': case '.EXAMPLE': default: config = fs.readFileSync(confPath, 'utf8'); break; } env.conf = new Config(config).get(); } catch (e) { cli.exit(1, `Cannot parse the config file ${confPath}: ${e}\n${FATAL_ERROR_MESSAGE}`); } // look for options on the command line, in the config file, and in the defaults, in that order env.opts = _.defaults(env.opts, env.conf.opts, defaultOpts); return cli; }; // TODO: docs cli.configureLogger = () => { function recoverableError() { props.shouldExitWithError = true; } function fatalError() { cli.exit(1); } if (env.opts.debug) { logger.setLevel(logger.LEVELS.DEBUG); } else if (env.opts.verbose) { logger.setLevel(logger.LEVELS.INFO); } if (env.opts.pedantic) { logger.once('logger:warn', recoverableError); logger.once('logger:error', fatalError); } else { logger.once('logger:error', recoverableError); } logger.once('logger:fatal', fatalError); return cli; }; // TODO: docs cli.logStart = () => { logger.debug( cli.getVersion() ); logger.debug('Environment info: %j', { env: { conf: env.conf, opts: env.opts } }); }; // TODO: docs cli.logFinish = () => { let delta; let deltaSeconds; if (env.run.finish && env.run.start) { delta = env.run.finish.getTime() - env.run.start.getTime(); } if (delta !== undefined) { deltaSeconds = (delta / 1000).toFixed(2); logger.info('Finished running in %s seconds.', deltaSeconds); } }; // TODO: docs cli.runCommand = cb => { let cmd; const opts = env.opts; if (opts.help) { cmd = cli.printHelp; } else if (opts.test) { cmd = cli.runTests; } else if (opts.version) { cmd = cli.printVersion; } else { cmd = cli.main; } cmd().then(errorCode => { if (!errorCode && props.shouldExitWithError) { errorCode = 1; } cb(errorCode); }); }; // TODO: docs cli.printHelp = () => { cli.printVersion(); console.log( `\n${require('jsdoc/opts/args').help()}\n` ); console.log('Visit https://jsdoc.app/ for more information.'); return Promise.resolve(0); }; // TODO: docs cli.runTests = () => { const path = require('jsdoc/path'); const runner = Promise.promisify(require( path.join(env.dirname, 'test/runner') )); console.log('Running tests...'); return runner(); }; // TODO: docs cli.getVersion = () => `JSDoc ${env.version.number} (${env.version.revision})`; // TODO: docs cli.printVersion = () => { console.log( cli.getVersion() ); return Promise.resolve(0); }; // TODO: docs cli.main = () => { cli.scanFiles(); if (env.sourceFiles.length === 0) { console.log('There are no input files to process.'); return Promise.resolve(0); } else { return cli.createParser() .parseFiles() .processParseResults() .then(() => { env.run.finish = new Date(); return 0; }); } }; function readPackageJson(filepath) { const fs = require('jsdoc/fs'); try { return stripJsonComments( fs.readFileSync(filepath, 'utf8') ); } catch (e) { logger.error('Unable to read the package file "%s"', filepath); return null; } } function buildSourceList() { const Readme = require('jsdoc/readme'); let packageJson; let readmeHtml; let sourceFile; let sourceFiles = env.opts._ ? env.opts._.slice(0) : []; if (env.conf.source && env.conf.source.include) { sourceFiles = sourceFiles.concat(env.conf.source.include); } // load the user-specified package/README files, if any if (env.opts.package) { packageJson = readPackageJson(env.opts.package); } if (env.opts.readme) { readmeHtml = new Readme(env.opts.readme).html; } // source files named `package.json` or `README.md` get special treatment, unless the user // explicitly specified a package and/or README file for (let i = 0, l = sourceFiles.length; i < l; i++) { sourceFile = sourceFiles[i]; if ( !env.opts.package && /\bpackage\.json$/i.test(sourceFile) ) { packageJson = readPackageJson(sourceFile); sourceFiles.splice(i--, 1); } if ( !env.opts.readme && /(\bREADME|\.md)$/i.test(sourceFile) ) { readmeHtml = new Readme(sourceFile).html; sourceFiles.splice(i--, 1); } } props.packageJson = packageJson; env.opts.readme = readmeHtml; return sourceFiles; } // TODO: docs cli.scanFiles = () => { const Filter = require('jsdoc/src/filter').Filter; let filter; env.opts._ = buildSourceList(); // are there any files to scan and parse? if (env.conf.source && env.opts._.length) { filter = new Filter(env.conf.source); env.sourceFiles = app.jsdoc.scanner.scan(env.opts._, (env.opts.recurse ? env.conf.recurseDepth : undefined), filter); } return cli; }; function resolvePluginPaths(paths) { const path = require('jsdoc/path'); const pluginPaths = []; paths.forEach(plugin => { const basename = path.basename(plugin); const dirname = path.dirname(plugin); const pluginPath = path.getResourcePath(dirname, basename); if (!pluginPath) { logger.error('Unable to find the plugin "%s"', plugin); return; } pluginPaths.push( pluginPath ); }); return pluginPaths; } cli.createParser = () => { const handlers = require('jsdoc/src/handlers'); const parser = require('jsdoc/src/parser'); const plugins = require('jsdoc/plugins'); app.jsdoc.parser = parser.createParser(env.conf.parser); if (env.conf.plugins) { env.conf.plugins = resolvePluginPaths(env.conf.plugins); plugins.installPlugins(env.conf.plugins, app.jsdoc.parser); } handlers.attachTo(app.jsdoc.parser); return cli; }; cli.parseFiles = () => { const augment = require('jsdoc/augment'); const borrow = require('jsdoc/borrow'); const Package = require('jsdoc/package').Package; let docs; let packageDocs; props.docs = docs = app.jsdoc.parser.parse(env.sourceFiles, env.opts.encoding); // If there is no package.json, just create an empty package packageDocs = new Package(props.packageJson); packageDocs.files = env.sourceFiles || []; docs.push(packageDocs); logger.debug('Adding inherited symbols, mixins, and interface implementations...'); augment.augmentAll(docs); logger.debug('Adding borrowed doclets...'); borrow.resolveBorrows(docs); logger.debug('Post-processing complete.'); app.jsdoc.parser.fireProcessingComplete(docs); return cli; }; cli.processParseResults = () => { if (env.opts.explain) { cli.dumpParseResults(); return Promise.resolve(); } else { cli.resolveTutorials(); return cli.generateDocs(); } }; cli.dumpParseResults = () => { console.log(require('jsdoc/util/dumper').dump(props.docs)); return cli; }; cli.resolveTutorials = () => { const resolver = require('jsdoc/tutorial/resolver'); if (env.opts.tutorials) { resolver.load(env.opts.tutorials); resolver.resolve(); } return cli; }; cli.generateDocs = () => { const path = require('jsdoc/path'); const resolver = require('jsdoc/tutorial/resolver'); const { taffy } = require('@jsdoc/salty'); let template; env.opts.template = (() => { const publish = env.opts.template || 'templates/default'; const templatePath = path.getResourcePath(publish); // if we didn't find the template, keep the user-specified value so the error message is // useful return templatePath || env.opts.template; })(); try { template = require(`${env.opts.template}/publish`); } catch (e) { logger.fatal(`Unable to load template: ${e.message}` || e); } // templates should include a publish.js file that exports a "publish" function if (template.publish && typeof template.publish === 'function') { let publishPromise; logger.info('Generating output files...'); publishPromise = template.publish( taffy(props.docs), env.opts, resolver.root ); return Promise.resolve(publishPromise); } else { logger.fatal(`${env.opts.template} does not export a "publish" function. Global "publish" functions are no longer supported.`); } return Promise.resolve(); }; // TODO: docs cli.exit = (exitCode, message) => { if (exitCode > 0 && message) { console.error(message); } process.on('exit', () => { process.exit(exitCode); }); }; return cli; })();