Home Manual Reference Source Test Repository

src/ESDoc.js

import es6shim from 'core-js/shim';
import fs from 'fs';
import path from 'path';
import assert from 'assert';
import logger from 'color-logger';
import ASTUtil from './Util/ASTUtil.js';
import ESParser from './Parser/ESParser';
import PathResolver from './Util/PathResolver.js';
import DocFactory from './Factory/DocFactory.js';
import TestDocFactory from './Factory/TestDocFactory.js';
import InvalidCodeLogger from './Util/InvalidCodeLogger.js';
import Plugin from './Plugin/Plugin.js';

/**
 * API Documentation Generator.
 *
 * @example
 * let config = {source: './src', destination: './esdoc'};
 * ESDoc.generate(config, (results, config)=>{
 *   console.log(results);
 * });
 */
export default class ESDoc {
  /**
   * Generate documentation.
   * @param {ESDocConfig} config - config for generation.
   * @param {function(results: Object[], asts: Object[], config: ESDocConfig)} publisher - callback for output html.
   */
  static generate(config, publisher) {
    assert(typeof publisher === 'function');
    assert(config.source);
    assert(config.destination);

    Plugin.init(config.plugins);
    Plugin.onStart();
    config = Plugin.onHandleConfig(config);

    this._setDefaultConfig(config);
    this._deprecatedConfig(config);

    logger.debug = !!config.debug;
    let includes = config.includes.map((v) => new RegExp(v));
    let excludes = config.excludes.map((v) => new RegExp(v));

    let packageName = null;
    let mainFilePath = null;
    if (config.package) {
      try {
        let packageJSON = fs.readFileSync(config.package, {encode: 'utf8'});
        let packageConfig = JSON.parse(packageJSON);
        packageName = packageConfig.name;
        mainFilePath = packageConfig.main;
      } catch (e) {
        // ignore
      }
    }

    let results = [];
    let asts = [];
    let sourceDirPath = path.resolve(config.source);

    this._walk(config.source, (filePath)=>{
      const relativeFilePath = path.relative(sourceDirPath, filePath);
      let match = false;
      for (let reg of includes) {
        if (relativeFilePath.match(reg)) {
          match = true;
          break;
        }
      }
      if (!match) return;

      for (let reg of excludes) {
        if (relativeFilePath.match(reg)) return;
      }

      let temp = this._traverse(config.source, filePath, packageName, mainFilePath);
      if (!temp) return;
      results.push(...temp.results);

      asts.push({filePath: 'source' + path.sep + relativeFilePath, ast: temp.ast});
    });

    if (config.builtinExternal) {
      this._useBuiltinExternal(results);
    }

    if (config.test) {
      this._generateForTest(config, results, asts);
    }

    results = Plugin.onHandleTag(results);

    publisher(results, asts, config);

    Plugin.onComplete();
  }

  /**
   * Generate document from test code.
   * @param {ESDocConfig} config - config for generating.
   * @param {DocObject[]} results - push DocObject to this.
   * @param {AST[]} asts - push ast to this.
   * @private
   */
  static _generateForTest(config, results, asts) {
    let includes = config.test.includes.map((v) => new RegExp(v));
    let excludes = config.test.excludes.map((v) => new RegExp(v));
    let sourceDirPath = path.resolve(config.test.source);

    this._walk(config.test.source, (filePath)=>{
      const relativeFilePath = path.relative(sourceDirPath, filePath);
      let match = false;
      for (let reg of includes) {
        if (relativeFilePath.match(reg)) {
          match = true;
          break;
        }
      }
      if (!match) return;

      for (let reg of excludes) {
        if (relativeFilePath.match(reg)) return;
      }

      let temp = this._traverseForTest(config.test.type, config.test.source, filePath);
      if (!temp) return;
      results.push(...temp.results);

      asts.push({filePath: 'test' + path.sep + relativeFilePath, ast: temp.ast});
    });
  }

  /**
   * set default config to specified config.
   * @param {ESDocConfig} config - specified config.
   * @private
   */
  static _setDefaultConfig(config) {
    if (!config.includes) config.includes = ['\\.(js|es6)$'];

    if (!config.excludes) config.excludes = ['\\.config\\.(js|es6)$'];

    if (!config.access) config.access = ['public', 'protected'];

    if (!('autoPrivate' in config)) config.autoPrivate = true;

    if (!('unexportIdentifier' in config)) config.unexportIdentifier = false;

    if (!('builtinExternal' in config)) config.builtinExternal = true;

    if (!('undocumentIdentifier' in config)) config.undocumentIdentifier = true;

    if (!('coverage' in config)) config.coverage = true;

    if (!('includeSource' in config)) config.includeSource = true;

    if (!('lint' in config)) config.lint = true;

    if (!config.index) config.index = './README.md';

    if (!config.package) config.package = './package.json';

    if (!config.styles) config.styles = [];

    if (!config.scripts) config.scripts = [];

    if (config.test) {
      assert(config.test.type);
      assert(config.test.source);
      if (!config.test.includes) config.test.includes = ['(spec|Spec|test|Test)\\.(js|es6)$'];
      if (!config.test.excludes) config.test.excludes = ['\\.config\\.(js|es6)$'];
    }
  }

  static _deprecatedConfig(config) {
    if (config.importPathPrefix) {
      console.log('[deprecated] config.importPathPrefix is deprecated. use esdoc-importpath-plugin(https://github.com/esdoc/esdoc-importpath-plugin)');
    }
  }

  /**
   * Use built-in external document.
   * built-in external has number, string, boolean, etc...
   * @param {DocObject[]} results - this method pushes DocObject to this param.
   * @private
   * @see {@link src/BuiltinExternal/ECMAScriptExternal.js}
   */
  static _useBuiltinExternal(results) {
    let dirPath = path.resolve(__dirname, './BuiltinExternal/');
    this._walk(dirPath, (filePath)=>{
      let temp = this._traverse(dirPath, filePath);
      temp.results.forEach((v)=> v.builtinExternal = true);
      let res = temp.results.filter(v => v.kind === 'external');
      results.push(...res);
    });
  }

  /**
   * walk recursive in directory.
   * @param {string} dirPath - target directory path.
   * @param {function(entryPath: string)} callback - callback for find file.
   * @private
   */
  static _walk(dirPath, callback) {
    let entries = fs.readdirSync(dirPath);

    for (let entry of entries) {
      let entryPath = path.resolve(dirPath, entry);
      let stat = fs.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) {
    logger.i(`parsing: ${filePath}`);
    let ast;
    try {
      ast = ESParser.parse(filePath);
    } catch(e) {
      InvalidCodeLogger.showFile(filePath, e);
      return null;
    }

    let pathResolver = new PathResolver(inDirPath, filePath, packageName, mainFilePath);
    let factory = new DocFactory(ast, pathResolver);

    ASTUtil.traverse(ast, (node, parent)=>{
      try {
        factory.push(node, parent);
      } catch(e) {
        InvalidCodeLogger.show(filePath, node);
        throw e;
      }
    });

    return {results: factory.results, ast: ast};
  }

  /**
   * traverse doc comment in test code file.
   * @param {string} type - test code type.
   * @param {string} inDirPath - root directory path.
   * @param {string} filePath - target test code file path.
   * @returns {Object} return document info that is traversed.
   * @property {DocObject[]} results - this is contained test code.
   * @property {AST} ast - this is AST of test code.
   * @private
   */
  static _traverseForTest(type, inDirPath, filePath) {
    let ast;
    try {
      ast = ESParser.parse(filePath);
    } catch(e) {
      InvalidCodeLogger.showFile(filePath, e);
      return null;
    }
    let pathResolver = new PathResolver(inDirPath, filePath);
    let factory = new TestDocFactory(type, ast, pathResolver);

    ASTUtil.traverse(ast, (node, parent)=>{
      try {
        factory.push(node, parent);
      } catch(e) {
        InvalidCodeLogger.show(filePath, node);
        throw e;
      }
    });

    return {results: factory.results, ast: ast};
  }
}