UNPKG

@puls-atlas/cli

Version:

The Puls Atlas CLI tool for managing Atlas projects

322 lines 11.3 kB
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);