@puls-atlas/cli
Version:
The Puls Atlas CLI tool for managing Atlas projects
322 lines • 11.3 kB
JavaScript
import fs from 'fs';
import path from 'path';
import * as parser from '@babel/parser';
import traverse from '@babel/traverse';
import { isString } from 'es-toolkit/compat';
const MODULE_EXTENSIONS = ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.mts', '.cts', '.tsx'];
const PARSER_OPTIONS = {
plugins: ['jsx', 'typescript'],
sourceType: 'unambiguous'
};
const getIdentifierNamesFromPattern = node => {
if (!node) {
return [];
}
if (node.type === 'Identifier') {
return [node.name];
}
if (node.type === 'AssignmentPattern') {
return getIdentifierNamesFromPattern(node.left);
}
if (node.type === 'RestElement') {
return getIdentifierNamesFromPattern(node.argument);
}
if (node.type === 'ArrayPattern') {
return node.elements.flatMap(element => getIdentifierNamesFromPattern(element));
}
if (node.type === 'ObjectPattern') {
return node.properties.flatMap(property => {
if (property.type === 'RestElement') {
return getIdentifierNamesFromPattern(property.argument);
}
return getIdentifierNamesFromPattern(property.value);
});
}
return [];
};
const getExportedName = exportedNode => {
if (!exportedNode) {
return null;
}
if (exportedNode.type === 'Identifier') {
return exportedNode.name;
}
if (exportedNode.type === 'StringLiteral') {
return exportedNode.value;
}
return null;
};
const isIdentifier = (node, name) => node?.type === 'Identifier' && node.name === name;
const isModuleExportsExpression = node => node?.type === 'MemberExpression' && !node.computed && isIdentifier(node.object, 'module') && isIdentifier(node.property, 'exports');
const unwrapExpression = node => {
let currentNode = node;
while (currentNode) {
if (currentNode.type === 'ParenthesizedExpression' || currentNode.type === 'TSAsExpression' || currentNode.type === 'TSSatisfiesExpression' || currentNode.type === 'TSNonNullExpression' || currentNode.type === 'TSTypeAssertion') {
currentNode = currentNode.expression;
continue;
}
return currentNode;
}
return currentNode;
};
const isRequireCallExpression = node => {
const expressionNode = unwrapExpression(node);
return expressionNode?.type === 'CallExpression' && isIdentifier(expressionNode.callee, 'require') && expressionNode.arguments.length === 1 && expressionNode.arguments[0]?.type === 'StringLiteral';
};
const getObjectPropertyName = property => {
if (property?.type !== 'ObjectProperty') {
return null;
}
if (property.key.type === 'Identifier') {
return property.key.name;
}
if (property.key.type === 'StringLiteral') {
return property.key.value;
}
return null;
};
const getMemberExpressionPropertyName = node => {
if (node?.type !== 'MemberExpression') {
return null;
}
if (!node.computed && node.property.type === 'Identifier') {
return node.property.name;
}
if (node.computed && node.property.type === 'StringLiteral') {
return node.property.value;
}
return null;
};
const resolveModulePath = (specifier, sourcePath, fsImpl = fs) => {
if (!isString(specifier) || !specifier.startsWith('.')) {
return null;
}
const absolutePath = path.resolve(path.dirname(sourcePath), specifier);
const candidates = [];
const hasKnownExtension = MODULE_EXTENSIONS.some(extension => absolutePath.endsWith(extension));
if (hasKnownExtension) {
candidates.push(absolutePath);
} else {
candidates.push(...MODULE_EXTENSIONS.map(extension => `${absolutePath}${extension}`));
candidates.push(...MODULE_EXTENSIONS.map(extension => path.join(absolutePath, `index${extension}`)));
}
return candidates.find(candidate => fsImpl.existsSync(candidate)) ?? null;
};
const collectTopLevelInitializers = programNode => {
const topLevelInitializers = new Map();
for (const statement of programNode?.body ?? []) {
if (statement.type !== 'VariableDeclaration') {
continue;
}
for (const declarator of statement.declarations) {
if (declarator.id.type !== 'Identifier' || !declarator.init) {
continue;
}
topLevelInitializers.set(declarator.id.name, declarator.init);
}
}
return topLevelInitializers;
};
const addNestedExportNames = (exportNames, exportName, nestedExportNames) => {
if (nestedExportNames.length === 0) {
exportNames.add(exportName);
return;
}
for (const nestedExportName of nestedExportNames) {
exportNames.add(`${exportName}.${nestedExportName}`);
}
};
const collectNestedExportNames = (node, context, bindingPath = new Set()) => {
const expressionNode = unwrapExpression(node);
if (!expressionNode) {
return [];
}
if (expressionNode.type === 'ObjectExpression') {
return expressionNode.properties.flatMap(property => {
const propertyName = getObjectPropertyName(property);
if (!propertyName) {
return [];
}
const nestedExportNames = collectNestedExportNames(property.value, context, bindingPath);
if (nestedExportNames.length === 0) {
return [propertyName];
}
return nestedExportNames.map(nestedExportName => `${propertyName}.${nestedExportName}`);
});
}
if (expressionNode.type === 'Identifier') {
if (bindingPath.has(expressionNode.name)) {
return [];
}
const initializer = context.topLevelInitializers.get(expressionNode.name);
if (!initializer) {
return [];
}
bindingPath.add(expressionNode.name);
const nestedExportNames = collectNestedExportNames(initializer, context, bindingPath);
bindingPath.delete(expressionNode.name);
return nestedExportNames;
}
if (context.options.recursive === true && isRequireCallExpression(expressionNode)) {
const [sourceNode] = expressionNode.arguments;
const resolvedPath = resolveModulePath(sourceNode.value, context.sourcePath, context.fsImpl);
if (!resolvedPath) {
return [];
}
return collectExportsFromFile(resolvedPath, context.options, {
fsImpl: context.fsImpl,
parserImpl: context.parserImpl,
traverseImpl: context.traverseImpl,
visited: context.visited
}).filter(name => name !== 'default');
}
return [];
};
const addDeclarationExports = (declaration, exportNames, context) => {
if (!declaration) {
return;
}
if (declaration.type === 'VariableDeclaration') {
for (const declarator of declaration.declarations) {
if (declarator.id.type === 'Identifier') {
const nestedExportNames = collectNestedExportNames(declarator.init, context);
addNestedExportNames(exportNames, declarator.id.name, nestedExportNames);
continue;
}
for (const name of getIdentifierNamesFromPattern(declarator.id)) {
exportNames.add(name);
}
}
return;
}
if (declaration.id?.name) {
exportNames.add(declaration.id.name);
}
};
const collectExportsFromFile = (sourcePath, options = {}, {
fsImpl = fs,
parserImpl = parser,
traverseImpl = traverse,
visited = new Set()
} = {}) => {
if (!fsImpl.existsSync(sourcePath)) {
throw new Error(`File does not exist: ${sourcePath}`);
}
const normalizedPath = path.resolve(sourcePath);
if (visited.has(normalizedPath)) {
return [];
}
visited.add(normalizedPath);
const code = fsImpl.readFileSync(normalizedPath, 'utf-8');
const ast = parserImpl.parse(code, PARSER_OPTIONS);
const exportNames = new Set();
const exportAllSources = [];
const topLevelInitializers = collectTopLevelInitializers(ast.program);
const traverseAst = traverseImpl.default ?? traverseImpl;
const nestedExportContext = {
fsImpl,
options,
parserImpl,
sourcePath: normalizedPath,
topLevelInitializers,
traverseImpl,
visited
};
traverseAst(ast, {
AssignmentExpression: expressionPath => {
const {
left,
right
} = expressionPath.node;
if (left.type === 'MemberExpression') {
const directExportName = getMemberExpressionPropertyName(left);
const nestedExportNames = collectNestedExportNames(right, nestedExportContext);
if (directExportName && isIdentifier(left.object, 'exports')) {
addNestedExportNames(exportNames, directExportName, nestedExportNames);
return;
}
if (directExportName && isModuleExportsExpression(left.object)) {
addNestedExportNames(exportNames, directExportName, nestedExportNames);
return;
}
}
if (isIdentifier(left, 'exports') || isModuleExportsExpression(left)) {
for (const name of collectNestedExportNames(right, nestedExportContext)) {
exportNames.add(name);
}
}
},
ExportAllDeclaration: exportPath => {
if (options.recursive === true && exportPath.node.source?.value) {
exportAllSources.push(exportPath.node.source.value);
}
},
ExportDefaultDeclaration: () => {
exportNames.add('default');
},
ExportNamedDeclaration: exportPath => {
const {
declaration,
source,
specifiers
} = exportPath.node;
const sourceSpecifier = source?.value;
addDeclarationExports(declaration, exportNames, nestedExportContext);
for (const specifier of specifiers) {
const exportedName = getExportedName(specifier.exported);
if (!exportedName) {
continue;
}
if (specifier.type === 'ExportNamespaceSpecifier') {
if (options.recursive !== true || sourceSpecifier === null || sourceSpecifier === undefined) {
exportNames.add(exportedName);
continue;
}
const resolvedPath = resolveModulePath(sourceSpecifier, normalizedPath, fsImpl);
if (!resolvedPath) {
exportNames.add(exportedName);
continue;
}
const nestedExportNames = collectExportsFromFile(resolvedPath, options, {
fsImpl,
parserImpl,
traverseImpl,
visited
}).filter(name => name !== 'default');
addNestedExportNames(exportNames, exportedName, nestedExportNames);
continue;
}
if (sourceSpecifier !== null && sourceSpecifier !== undefined) {
exportNames.add(exportedName);
continue;
}
const nestedExportNames = collectNestedExportNames(specifier.local, nestedExportContext);
if (nestedExportNames.length > 0) {
addNestedExportNames(exportNames, exportedName, nestedExportNames);
continue;
}
exportNames.add(exportedName);
}
}
});
if (options.recursive === true) {
for (const specifier of exportAllSources) {
const resolvedPath = resolveModulePath(specifier, normalizedPath, fsImpl);
if (!resolvedPath) {
continue;
}
for (const name of collectExportsFromFile(resolvedPath, options, {
fsImpl,
parserImpl,
traverseImpl,
visited
})) {
if (name !== 'default') {
exportNames.add(name);
}
}
}
}
return [...exportNames].sort((left, right) => left.localeCompare(right));
};
export const listModuleExports = (sourcePath, options = {}, dependencies = {}) => collectExportsFromFile(sourcePath, options, dependencies);