documentation
Version:
a documentation generator
357 lines (327 loc) • 9.99 kB
JavaScript
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;
}