UNPKG

documentation

Version:
357 lines (327 loc) 9.99 kB
import babelTraverse from '@babel/traverse'; import isJSDocComment from '../is_jsdoc_comment.js'; import t from '@babel/types'; import nodePath from 'path'; import fs from 'fs'; import { parseToAst } from '../parsers/parse_to_ast.js'; import findTarget from '../infer/finders.js'; import { createRequire } from 'module'; const require = createRequire(import.meta.url); const traverse = babelTraverse.default || babelTraverse; /** * Iterate through the abstract syntax tree, finding ES6-style exports, * and inserting blank comments into documentation.js's processing stream. * Through inference steps, these comments gain more information and are automatically * documented as well as we can. * @param {Object} config * @param {Object} [config.extensions] extensions to try when resolving * @param {Object} ast the babel-parsed syntax tree * @param {Object} data the name of the file * @param {Function} addComment a method that creates a new comment if necessary * @returns {Array<Object>} comments * @private */ export default function walkExported( config /* { extensions?: string[] } */, ast, data /*: { file: string } */, addComment ) { const newResults = []; const filename = data.file; const dataCache = new Map(); function addBlankComment(data, path, node) { return addComment(data, '', node.loc, path, node.loc, true); } function getComments(data, path) { const comments = (path.node.leadingComments || []).filter(isJSDocComment); if (!comments.length) { // If this is the first declarator we check for comments on the VariableDeclaration. if ( t.isVariableDeclarator(path) && path.parentPath.get('declarations')[0] === path ) { return getComments(data, path.parentPath); } const added = addBlankComment(data, path, path.node); return added ? [added] : []; } return comments .map(function (comment) { return addComment( data, comment.value, comment.loc, path, path.node.loc, true ); }) .filter(Boolean); } function addComments(data, path, overrideName) { const comments = getComments(data, path); if (overrideName) { comments.forEach(function (comment) { comment.name = overrideName; }); } newResults.push.apply(newResults, comments); } traverse(ast, { Statement(path) { path.skip(); }, ExportDeclaration(path) { const declaration = path.get('declaration'); if (t.isDeclaration(declaration)) { traverseExportedSubtree(declaration, data, addComments); return path.skip(); } if (path.isExportDefaultDeclaration()) { if (declaration.isIdentifier()) { const binding = declaration.scope.getBinding(declaration.node.name); traverseExportedSubtree(binding.path, data, addComments); return path.skip(); } traverseExportedSubtree(declaration, data, addComments); return path.skip(); } if (t.isExportNamedDeclaration(path)) { const specifiers = path.get('specifiers'); const source = path.node.source; const exportKind = path.node.exportKind; specifiers.forEach(specifier => { let specData = data; let local; if (t.isExportDefaultSpecifier(specifier)) { local = 'default'; } else { // ExportSpecifier local = specifier.node.local.name; } const exported = specifier.node.exported.name; let bindingPath; if (source) { const tmp = findExportDeclaration( dataCache, local, exportKind, filename, source.value, config.extensions ); bindingPath = tmp.ast; specData = tmp.data; } else if (exportKind === 'value') { bindingPath = path.scope.getBinding(local).path; } else if (exportKind === 'type') { bindingPath = findLocalType(path.scope, local); } else { throw new Error('Unreachable'); } if (bindingPath === undefined) { throw new Error( `Unable to find the value ${exported} in ${specData.file}` ); } traverseExportedSubtree(bindingPath, specData, addComments, exported); }); return path.skip(); } } }); return newResults; } function traverseExportedSubtree(path, data, addComments, overrideName) { let attachCommentPath = path; if (path.parentPath && path.parentPath.isExportDeclaration()) { attachCommentPath = path.parentPath; } addComments(data, attachCommentPath, overrideName); let target = findTarget(path); if (!target) { return; } if (t.isVariableDeclarator(target) && target.has('init')) { target = target.get('init'); } if (target.isClass() || target.isObjectExpression()) { target.traverse({ Property(path) { addComments(data, path); path.skip(); }, Method(path) { // Don't explicitly document constructor methods: their // parameters are output as part of the class itself. if (path.node.kind !== 'constructor') { addComments(data, path); } path.skip(); } }); } } function resolveFile(filePath, extensions = []) { try { // First try resolving the file with the default extensions. return require.resolve(filePath); } catch { // If that fails, try resolving the file with the extensions passed in. } // Then try all other extensions in order. for (const extension of extensions) { try { return require.resolve( `${filePath}${extension.startsWith('.') ? extension : `.${extension}`}` ); } catch { continue; } } throw new Error( `Could not resolve \`${filePath}\` with any of the extensions: ${[ ...require.extensions, ...extensions ].join(', ')}` ); } function getCachedData(dataCache, filePath, extensions) { const path = resolveFile(filePath, extensions); let value = dataCache.get(path); if (!value) { const input = fs.readFileSync(path, 'utf-8'); const ast = parseToAst(input, path); value = { data: { file: path, source: input }, ast }; dataCache.set(path, value); } return value; } // Loads a module and finds the exported declaration. function findExportDeclaration( dataCache, name, exportKind, referrer, filename, extensions ) { const depPath = nodePath.resolve(nodePath.dirname(referrer), filename); const tmp = getCachedData(dataCache, depPath, extensions); const ast = tmp.ast; let data = tmp.data; let rv; traverse(ast, { Statement(path) { path.skip(); }, ExportDeclaration(path) { if (name === 'default' && path.isExportDefaultDeclaration()) { rv = path.get('declaration'); path.stop(); } else if (path.isExportNamedDeclaration()) { const declaration = path.get('declaration'); if (t.isDeclaration(declaration)) { let bindingName; if ( declaration.isFunctionDeclaration() || declaration.isClassDeclaration() || declaration.isTypeAlias() || declaration.isOpaqueType() ) { bindingName = declaration.node.id.name; } else if (declaration.isVariableDeclaration()) { // TODO: Multiple declarations. bindingName = declaration.node.declarations[0].id.name; } if (name === bindingName) { rv = declaration; path.stop(); } else { path.skip(); } return; } // export {x as y} // export {x as y} from './file.js' const specifiers = path.get('specifiers'); const source = path.node.source; for (let i = 0; i < specifiers.length; i++) { const specifier = specifiers[i]; let local, exported; if (t.isExportDefaultSpecifier(specifier)) { // export x from ... local = 'default'; exported = specifier.node.exported.name; } else { // ExportSpecifier local = specifier.node.local.name; exported = specifier.node.exported.name; } if (exported === name) { if (source) { // export {local as exported} from './file.js'; const tmp = findExportDeclaration( dataCache, local, exportKind, depPath, source.value, extensions ); rv = tmp.ast; data = tmp.data; if (!rv) { throw new Error(`${name} is not exported by ${depPath}`); } } else { // export {local as exported} if (exportKind === 'value') { rv = path.scope.getBinding(local).path; } else { rv = findLocalType(path.scope, local); } if (!rv) { throw new Error(`${depPath} has no binding for ${name}`); } } path.stop(); return; } } } } }); return { ast: rv, data }; } // Since we cannot use scope.getBinding for types this walks the current scope looking for a // top-level type alias. function findLocalType(scope, local) { let rv; scope.path.traverse({ Statement(path) { path.skip(); }, TypeAlias(path) { if (path.node.id.name === local) { rv = path; path.stop(); } else { path.skip(); } } }); return rv; }