UNPKG

esdoc

Version:

Good Documentation Generator For JavaScript

369 lines (297 loc) 10.9 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _fsExtra = require('fs-extra'); var _fsExtra2 = _interopRequireDefault(_fsExtra); var _path = require('path'); var _path2 = _interopRequireDefault(_path); var _assert = require('assert'); var _assert2 = _interopRequireDefault(_assert); var _colorLogger = require('color-logger'); var _colorLogger2 = _interopRequireDefault(_colorLogger); var _ASTUtil = require('./Util/ASTUtil.js'); var _ASTUtil2 = _interopRequireDefault(_ASTUtil); var _ESParser = require('./Parser/ESParser'); var _ESParser2 = _interopRequireDefault(_ESParser); var _PathResolver = require('./Util/PathResolver.js'); var _PathResolver2 = _interopRequireDefault(_PathResolver); var _DocFactory = require('./Factory/DocFactory.js'); var _DocFactory2 = _interopRequireDefault(_DocFactory); var _InvalidCodeLogger = require('./Util/InvalidCodeLogger.js'); var _InvalidCodeLogger2 = _interopRequireDefault(_InvalidCodeLogger); var _Plugin = require('./Plugin/Plugin.js'); var _Plugin2 = _interopRequireDefault(_Plugin); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /** * API Documentation Generator. * * @example * let config = {source: './src', destination: './esdoc'}; * ESDoc.generate(config, (results, config)=>{ * console.log(results); * }); */ class ESDoc { /** * Generate documentation. * @param {ESDocConfig} config - config for generation. */ static generate(config) { (0, _assert2.default)(config.source); (0, _assert2.default)(config.destination); this._checkOldConfig(config); _Plugin2.default.init(config.plugins); _Plugin2.default.onStart(); config = _Plugin2.default.onHandleConfig(config); this._setDefaultConfig(config); _colorLogger2.default.debug = !!config.debug; const includes = config.includes.map(v => new RegExp(v)); const excludes = config.excludes.map(v => new RegExp(v)); let packageName = null; let mainFilePath = null; if (config.package) { try { const packageJSON = _fsExtra2.default.readFileSync(config.package, { encode: 'utf8' }); const packageConfig = JSON.parse(packageJSON); packageName = packageConfig.name; mainFilePath = packageConfig.main; } catch (e) { // ignore } } let results = []; const asts = []; const sourceDirPath = _path2.default.resolve(config.source); this._walk(config.source, filePath => { const relativeFilePath = _path2.default.relative(sourceDirPath, filePath); let match = false; for (const reg of includes) { if (relativeFilePath.match(reg)) { match = true; break; } } if (!match) return; for (const reg of excludes) { if (relativeFilePath.match(reg)) return; } console.log(`parse: ${filePath}`); const temp = this._traverse(config.source, filePath, packageName, mainFilePath); if (!temp) return; results.push(...temp.results); if (config.outputAST) { asts.push({ filePath: `source${_path2.default.sep}${relativeFilePath}`, ast: temp.ast }); } }); // config.index if (config.index) { results.push(this._generateForIndex(config)); } // config.package if (config.package) { results.push(this._generateForPackageJSON(config)); } results = this._resolveDuplication(results); results = _Plugin2.default.onHandleDocs(results); // index.json { const dumpPath = _path2.default.resolve(config.destination, 'index.json'); _fsExtra2.default.outputFileSync(dumpPath, JSON.stringify(results, null, 2)); } // ast, array will be empty if config.outputAST is false - resulting in skipping the loop for (const ast of asts) { const json = JSON.stringify(ast.ast, null, 2); const filePath = _path2.default.resolve(config.destination, `ast/${ast.filePath}.json`); _fsExtra2.default.outputFileSync(filePath, json); } // publish this._publish(config); _Plugin2.default.onComplete(); } /** * check ESDoc config. and if it is old, exit with warning message. * @param {ESDocConfig} config - check config * @private */ static _checkOldConfig(config) { let exit = false; const keys = [['access', 'esdoc-standard-plugin'], ['autoPrivate', 'esdoc-standard-plugin'], ['unexportedIdentifier', 'esdoc-standard-plugin'], ['undocumentIdentifier', 'esdoc-standard-plugin'], ['builtinExternal', 'esdoc-standard-plugin'], ['coverage', 'esdoc-standard-plugin'], ['test', 'esdoc-standard-plugin'], ['title', 'esdoc-standard-plugin'], ['manual', 'esdoc-standard-plugin'], ['lint', 'esdoc-standard-plugin'], ['includeSource', 'esdoc-exclude-source-plugin'], ['styles', 'esdoc-inject-style-plugin'], ['scripts', 'esdoc-inject-script-plugin'], ['experimentalProposal', 'esdoc-ecmascript-proposal-plugin']]; for (const [key, plugin] of keys) { if (key in config) { console.log(`error: config.${key} is invalid. Please use ${plugin}. how to migration: https://esdoc.org/manual/migration.html`); exit = true; } } if (exit) process.exit(1); } /** * set default config to specified config. * @param {ESDocConfig} config - specified config. * @private */ static _setDefaultConfig(config) { if (!config.includes) config.includes = ['\\.js$']; if (!config.excludes) config.excludes = ['\\.config\\.js$', '\\.test\\.js$']; if (!config.index) config.index = './README.md'; if (!config.package) config.package = './package.json'; if (!('outputAST' in config)) config.outputAST = true; } /** * walk recursive in directory. * @param {string} dirPath - target directory path. * @param {function(entryPath: string)} callback - callback for find file. * @private */ static _walk(dirPath, callback) { const entries = _fsExtra2.default.readdirSync(dirPath); for (const entry of entries) { const entryPath = _path2.default.resolve(dirPath, entry); const stat = _fsExtra2.default.statSync(entryPath); if (stat.isFile()) { callback(entryPath); } else if (stat.isDirectory()) { this._walk(entryPath, callback); } } } /** * traverse doc comment in JavaScript file. * @param {string} inDirPath - root directory path. * @param {string} filePath - target JavaScript file path. * @param {string} [packageName] - npm package name of target. * @param {string} [mainFilePath] - npm main file path of target. * @returns {Object} - return document that is traversed. * @property {DocObject[]} results - this is contained JavaScript file. * @property {AST} ast - this is AST of JavaScript file. * @private */ static _traverse(inDirPath, filePath, packageName, mainFilePath) { _colorLogger2.default.i(`parsing: ${filePath}`); let ast; try { ast = _ESParser2.default.parse(filePath); } catch (e) { _InvalidCodeLogger2.default.showFile(filePath, e); return null; } const pathResolver = new _PathResolver2.default(inDirPath, filePath, packageName, mainFilePath); const factory = new _DocFactory2.default(ast, pathResolver); _ASTUtil2.default.traverse(ast, (node, parent) => { try { factory.push(node, parent); } catch (e) { _InvalidCodeLogger2.default.show(filePath, node); throw e; } }); return { results: factory.results, ast: ast }; } /** * generate index doc * @param {ESDocConfig} config * @returns {Tag} * @private */ static _generateForIndex(config) { let indexContent = ''; if (_fsExtra2.default.existsSync(config.index)) { indexContent = _fsExtra2.default.readFileSync(config.index, { encode: 'utf8' }).toString(); } else { console.log(`warning: ${config.index} is not found. Please check config.index.`); } const tag = { kind: 'index', content: indexContent, longname: _path2.default.resolve(config.index), name: config.index, static: true, access: 'public' }; return tag; } /** * generate package doc * @param {ESDocConfig} config * @returns {Tag} * @private */ static _generateForPackageJSON(config) { let packageJSON = ''; let packagePath = ''; try { packageJSON = _fsExtra2.default.readFileSync(config.package, { encoding: 'utf-8' }); packagePath = _path2.default.resolve(config.package); } catch (e) { // ignore } const tag = { kind: 'packageJSON', content: packageJSON, longname: packagePath, name: _path2.default.basename(packagePath), static: true, access: 'public' }; return tag; } /** * resolve duplication docs * @param {Tag[]} docs * @returns {Tag[]} * @private */ static _resolveDuplication(docs) { const memberDocs = docs.filter(doc => doc.kind === 'member'); const removeIds = []; for (const memberDoc of memberDocs) { // member duplicate with getter/setter/method. // when it, remove member. // getter/setter/method are high priority. const sameLongnameDoc = docs.find(doc => doc.longname === memberDoc.longname && doc.kind !== 'member'); if (sameLongnameDoc) { removeIds.push(memberDoc.__docId__); continue; } const dup = docs.filter(doc => doc.longname === memberDoc.longname && doc.kind === 'member'); if (dup.length > 1) { const ids = dup.map(v => v.__docId__); ids.sort((a, b) => { return a < b ? -1 : 1; }); ids.shift(); removeIds.push(...ids); } } return docs.filter(doc => !removeIds.includes(doc.__docId__)); } /** * publish content * @param {ESDocConfig} config * @private */ static _publish(config) { try { const write = (filePath, content, option) => { const _filePath = _path2.default.resolve(config.destination, filePath); content = _Plugin2.default.onHandleContent(content, _filePath); console.log(`output: ${_filePath}`); _fsExtra2.default.outputFileSync(_filePath, content, option); }; const copy = (srcPath, destPath) => { const _destPath = _path2.default.resolve(config.destination, destPath); console.log(`output: ${_destPath}`); _fsExtra2.default.copySync(srcPath, _destPath); }; const read = filePath => { const _filePath = _path2.default.resolve(config.destination, filePath); return _fsExtra2.default.readFileSync(_filePath).toString(); }; _Plugin2.default.onPublish(write, copy, read); } catch (e) { _InvalidCodeLogger2.default.showError(e); process.exit(1); } } } exports.default = ESDoc;