cli.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. /* eslint-disable indent, no-process-exit */
  2. /**
  3. * Helper methods for running JSDoc on the command line.
  4. *
  5. * A few critical notes for anyone who works on this module:
  6. *
  7. * + The module should really export an instance of `cli`, and `props` should be properties of a
  8. * `cli` instance.
  9. *
  10. * @private
  11. */
  12. module.exports = (() => {
  13. const app = require('jsdoc/app');
  14. const env = require('jsdoc/env');
  15. const logger = require('jsdoc/util/logger');
  16. const stripBom = require('jsdoc/util/stripbom');
  17. const stripJsonComments = require('strip-json-comments');
  18. const Promise = require('bluebird');
  19. const props = {
  20. docs: [],
  21. packageJson: null,
  22. shouldExitWithError: false,
  23. tmpdir: null
  24. };
  25. const FATAL_ERROR_MESSAGE = 'Exiting JSDoc because an error occurred. See the previous log ' +
  26. 'messages for details.';
  27. const cli = {};
  28. // TODO: docs
  29. cli.setVersionInfo = () => {
  30. const fs = require('fs');
  31. const path = require('path');
  32. // allow this to throw--something is really wrong if we can't read our own package file
  33. const info = JSON.parse( stripBom.strip(fs.readFileSync(path.join(env.dirname, 'package.json'),
  34. 'utf8')) );
  35. env.version = {
  36. number: info.version,
  37. revision: new Date( parseInt(info.revision, 10) ).toUTCString()
  38. };
  39. return cli;
  40. };
  41. // TODO: docs
  42. cli.loadConfig = () => {
  43. const _ = require('underscore');
  44. const args = require('jsdoc/opts/args');
  45. const Config = require('jsdoc/config');
  46. let config;
  47. const fs = require('jsdoc/fs');
  48. const path = require('jsdoc/path');
  49. let confPath;
  50. let isFile;
  51. const defaultOpts = {
  52. destination: './out/',
  53. encoding: 'utf8'
  54. };
  55. try {
  56. env.opts = args.parse(env.args);
  57. }
  58. catch (e) {
  59. console.error(`${e.message}\n`);
  60. cli.printHelp().then(() => {
  61. cli.exit(1);
  62. });
  63. }
  64. confPath = env.opts.configure || path.join(env.dirname, 'conf.json');
  65. try {
  66. isFile = fs.statSync(confPath).isFile();
  67. }
  68. catch (e) {
  69. isFile = false;
  70. }
  71. if ( !isFile && !env.opts.configure ) {
  72. confPath = path.join(env.dirname, 'conf.json.EXAMPLE');
  73. }
  74. try {
  75. switch ( path.extname(confPath) ) {
  76. case '.js':
  77. config = require( path.resolve(confPath) ) || {};
  78. break;
  79. case '.json':
  80. case '.EXAMPLE':
  81. default:
  82. config = fs.readFileSync(confPath, 'utf8');
  83. break;
  84. }
  85. env.conf = new Config(config).get();
  86. }
  87. catch (e) {
  88. cli.exit(1, `Cannot parse the config file ${confPath}: ${e}\n${FATAL_ERROR_MESSAGE}`);
  89. }
  90. // look for options on the command line, in the config file, and in the defaults, in that order
  91. env.opts = _.defaults(env.opts, env.conf.opts, defaultOpts);
  92. return cli;
  93. };
  94. // TODO: docs
  95. cli.configureLogger = () => {
  96. function recoverableError() {
  97. props.shouldExitWithError = true;
  98. }
  99. function fatalError() {
  100. cli.exit(1);
  101. }
  102. if (env.opts.debug) {
  103. logger.setLevel(logger.LEVELS.DEBUG);
  104. }
  105. else if (env.opts.verbose) {
  106. logger.setLevel(logger.LEVELS.INFO);
  107. }
  108. if (env.opts.pedantic) {
  109. logger.once('logger:warn', recoverableError);
  110. logger.once('logger:error', fatalError);
  111. }
  112. else {
  113. logger.once('logger:error', recoverableError);
  114. }
  115. logger.once('logger:fatal', fatalError);
  116. return cli;
  117. };
  118. // TODO: docs
  119. cli.logStart = () => {
  120. logger.debug( cli.getVersion() );
  121. logger.debug('Environment info: %j', {
  122. env: {
  123. conf: env.conf,
  124. opts: env.opts
  125. }
  126. });
  127. };
  128. // TODO: docs
  129. cli.logFinish = () => {
  130. let delta;
  131. let deltaSeconds;
  132. if (env.run.finish && env.run.start) {
  133. delta = env.run.finish.getTime() - env.run.start.getTime();
  134. }
  135. if (delta !== undefined) {
  136. deltaSeconds = (delta / 1000).toFixed(2);
  137. logger.info('Finished running in %s seconds.', deltaSeconds);
  138. }
  139. };
  140. // TODO: docs
  141. cli.runCommand = cb => {
  142. let cmd;
  143. const opts = env.opts;
  144. if (opts.help) {
  145. cmd = cli.printHelp;
  146. }
  147. else if (opts.test) {
  148. cmd = cli.runTests;
  149. }
  150. else if (opts.version) {
  151. cmd = cli.printVersion;
  152. }
  153. else {
  154. cmd = cli.main;
  155. }
  156. cmd().then(errorCode => {
  157. if (!errorCode && props.shouldExitWithError) {
  158. errorCode = 1;
  159. }
  160. cb(errorCode);
  161. });
  162. };
  163. // TODO: docs
  164. cli.printHelp = () => {
  165. cli.printVersion();
  166. console.log( `\n${require('jsdoc/opts/args').help()}\n` );
  167. console.log('Visit https://jsdoc.app/ for more information.');
  168. return Promise.resolve(0);
  169. };
  170. // TODO: docs
  171. cli.runTests = () => {
  172. const path = require('jsdoc/path');
  173. const runner = Promise.promisify(require( path.join(env.dirname, 'test/runner') ));
  174. console.log('Running tests...');
  175. return runner();
  176. };
  177. // TODO: docs
  178. cli.getVersion = () => `JSDoc ${env.version.number} (${env.version.revision})`;
  179. // TODO: docs
  180. cli.printVersion = () => {
  181. console.log( cli.getVersion() );
  182. return Promise.resolve(0);
  183. };
  184. // TODO: docs
  185. cli.main = () => {
  186. cli.scanFiles();
  187. if (env.sourceFiles.length === 0) {
  188. console.log('There are no input files to process.');
  189. return Promise.resolve(0);
  190. } else {
  191. return cli.createParser()
  192. .parseFiles()
  193. .processParseResults()
  194. .then(() => {
  195. env.run.finish = new Date();
  196. return 0;
  197. });
  198. }
  199. };
  200. function readPackageJson(filepath) {
  201. const fs = require('jsdoc/fs');
  202. try {
  203. return stripJsonComments( fs.readFileSync(filepath, 'utf8') );
  204. }
  205. catch (e) {
  206. logger.error('Unable to read the package file "%s"', filepath);
  207. return null;
  208. }
  209. }
  210. function buildSourceList() {
  211. const Readme = require('jsdoc/readme');
  212. let packageJson;
  213. let readmeHtml;
  214. let sourceFile;
  215. let sourceFiles = env.opts._ ? env.opts._.slice(0) : [];
  216. if (env.conf.source && env.conf.source.include) {
  217. sourceFiles = sourceFiles.concat(env.conf.source.include);
  218. }
  219. // load the user-specified package/README files, if any
  220. if (env.opts.package) {
  221. packageJson = readPackageJson(env.opts.package);
  222. }
  223. if (env.opts.readme) {
  224. readmeHtml = new Readme(env.opts.readme).html;
  225. }
  226. // source files named `package.json` or `README.md` get special treatment, unless the user
  227. // explicitly specified a package and/or README file
  228. for (let i = 0, l = sourceFiles.length; i < l; i++) {
  229. sourceFile = sourceFiles[i];
  230. if ( !env.opts.package && /\bpackage\.json$/i.test(sourceFile) ) {
  231. packageJson = readPackageJson(sourceFile);
  232. sourceFiles.splice(i--, 1);
  233. }
  234. if ( !env.opts.readme && /(\bREADME|\.md)$/i.test(sourceFile) ) {
  235. readmeHtml = new Readme(sourceFile).html;
  236. sourceFiles.splice(i--, 1);
  237. }
  238. }
  239. props.packageJson = packageJson;
  240. env.opts.readme = readmeHtml;
  241. return sourceFiles;
  242. }
  243. // TODO: docs
  244. cli.scanFiles = () => {
  245. const Filter = require('jsdoc/src/filter').Filter;
  246. let filter;
  247. env.opts._ = buildSourceList();
  248. // are there any files to scan and parse?
  249. if (env.conf.source && env.opts._.length) {
  250. filter = new Filter(env.conf.source);
  251. env.sourceFiles = app.jsdoc.scanner.scan(env.opts._,
  252. (env.opts.recurse ? env.conf.recurseDepth : undefined), filter);
  253. }
  254. return cli;
  255. };
  256. function resolvePluginPaths(paths) {
  257. const path = require('jsdoc/path');
  258. const pluginPaths = [];
  259. paths.forEach(plugin => {
  260. const basename = path.basename(plugin);
  261. const dirname = path.dirname(plugin);
  262. const pluginPath = path.getResourcePath(dirname, basename);
  263. if (!pluginPath) {
  264. logger.error('Unable to find the plugin "%s"', plugin);
  265. return;
  266. }
  267. pluginPaths.push( pluginPath );
  268. });
  269. return pluginPaths;
  270. }
  271. cli.createParser = () => {
  272. const handlers = require('jsdoc/src/handlers');
  273. const parser = require('jsdoc/src/parser');
  274. const plugins = require('jsdoc/plugins');
  275. app.jsdoc.parser = parser.createParser(env.conf.parser);
  276. if (env.conf.plugins) {
  277. env.conf.plugins = resolvePluginPaths(env.conf.plugins);
  278. plugins.installPlugins(env.conf.plugins, app.jsdoc.parser);
  279. }
  280. handlers.attachTo(app.jsdoc.parser);
  281. return cli;
  282. };
  283. cli.parseFiles = () => {
  284. const augment = require('jsdoc/augment');
  285. const borrow = require('jsdoc/borrow');
  286. const Package = require('jsdoc/package').Package;
  287. let docs;
  288. let packageDocs;
  289. props.docs = docs = app.jsdoc.parser.parse(env.sourceFiles, env.opts.encoding);
  290. // If there is no package.json, just create an empty package
  291. packageDocs = new Package(props.packageJson);
  292. packageDocs.files = env.sourceFiles || [];
  293. docs.push(packageDocs);
  294. logger.debug('Adding inherited symbols, mixins, and interface implementations...');
  295. augment.augmentAll(docs);
  296. logger.debug('Adding borrowed doclets...');
  297. borrow.resolveBorrows(docs);
  298. logger.debug('Post-processing complete.');
  299. app.jsdoc.parser.fireProcessingComplete(docs);
  300. return cli;
  301. };
  302. cli.processParseResults = () => {
  303. if (env.opts.explain) {
  304. cli.dumpParseResults();
  305. return Promise.resolve();
  306. }
  307. else {
  308. cli.resolveTutorials();
  309. return cli.generateDocs();
  310. }
  311. };
  312. cli.dumpParseResults = () => {
  313. console.log(require('jsdoc/util/dumper').dump(props.docs));
  314. return cli;
  315. };
  316. cli.resolveTutorials = () => {
  317. const resolver = require('jsdoc/tutorial/resolver');
  318. if (env.opts.tutorials) {
  319. resolver.load(env.opts.tutorials);
  320. resolver.resolve();
  321. }
  322. return cli;
  323. };
  324. cli.generateDocs = () => {
  325. const path = require('jsdoc/path');
  326. const resolver = require('jsdoc/tutorial/resolver');
  327. const { taffy } = require('@jsdoc/salty');
  328. let template;
  329. env.opts.template = (() => {
  330. const publish = env.opts.template || 'templates/default';
  331. const templatePath = path.getResourcePath(publish);
  332. // if we didn't find the template, keep the user-specified value so the error message is
  333. // useful
  334. return templatePath || env.opts.template;
  335. })();
  336. try {
  337. template = require(`${env.opts.template}/publish`);
  338. }
  339. catch (e) {
  340. logger.fatal(`Unable to load template: ${e.message}` || e);
  341. }
  342. // templates should include a publish.js file that exports a "publish" function
  343. if (template.publish && typeof template.publish === 'function') {
  344. let publishPromise;
  345. logger.info('Generating output files...');
  346. publishPromise = template.publish(
  347. taffy(props.docs),
  348. env.opts,
  349. resolver.root
  350. );
  351. return Promise.resolve(publishPromise);
  352. }
  353. else {
  354. logger.fatal(`${env.opts.template} does not export a "publish" function. Global "publish" functions are no longer supported.`);
  355. }
  356. return Promise.resolve();
  357. };
  358. // TODO: docs
  359. cli.exit = (exitCode, message) => {
  360. if (exitCode > 0 && message) {
  361. console.error(message);
  362. }
  363. process.on('exit', () => { process.exit(exitCode); });
  364. };
  365. return cli;
  366. })();