/* eslint-disable no-shadow, no-param-reassign */ const pathLib = require('path'); const { default: traverse } = require('@babel/traverse'); const { Analyzer } = require('./helpers/Analyzer.js'); const { trackDownIdentifier } = require('./helpers/track-down-identifier.js'); const { normalizeSourcePaths } = require('./helpers/normalize-source-paths.js'); const { aForEach } = require('../utils/async-array-utils.js'); /** @typedef {import('./helpers/track-down-identifier.js').RootFile} RootFile */ /** * @typedef {object} RootFileMapEntry * @property {string} currentFileSpecifier this is the local name in the file we track from * @property {RootFile} rootFile contains file(filePath) and specifier */ /** * @typedef {RootFileMapEntry[]} RootFileMap */ async function trackdownRoot(transformedEntry, relativePath, projectPath) { const fullCurrentFilePath = pathLib.resolve(projectPath, relativePath); await aForEach(transformedEntry, async specObj => { /** @type {RootFileMap} */ const rootFileMap = []; if (specObj.exportSpecifiers[0] === '[file]') { rootFileMap.push(undefined); } else { /** * './src/origin.js': `export class MyComp {}` * './index.js:' `export { MyComp as RenamedMyComp } from './src/origin'` * * Goes from specifier like 'RenamedMyComp' to object for rootFileMap like: * { * currentFileSpecifier: 'RenamedMyComp', * rootFile: { * file: './src/origin.js', * specifier: 'MyCompDefinition', * } * } */ await aForEach(specObj.exportSpecifiers, async (/** @type {string} */ specifier) => { let rootFile; let localMapMatch; if (specObj.localMap) { localMapMatch = specObj.localMap.find(m => m.exported === specifier); } // TODO: find out if possible to use trackDownIdentifierFromScope if (specObj.source) { // TODO: see if still needed: && (localMapMatch || specifier === '[default]') const importedIdentifier = (localMapMatch && localMapMatch.local) || specifier; rootFile = await trackDownIdentifier( specObj.source, importedIdentifier, fullCurrentFilePath, projectPath, ); /** @type {RootFileMapEntry} */ const entry = { currentFileSpecifier: specifier, rootFile, }; rootFileMap.push(entry); } else { /** @type {RootFileMapEntry} */ const entry = { currentFileSpecifier: specifier, rootFile: { file: '[current]', specifier }, }; rootFileMap.push(entry); } }); } specObj.rootFileMap = rootFileMap; }); return transformedEntry; } function cleanup(transformedEntry) { transformedEntry.forEach(specObj => { if (specObj.__tmp) { delete specObj.__tmp; } }); return transformedEntry; } /** * @returns {string[]} */ function getExportSpecifiers(node) { // handles default [export const g = 4]; if (node.declaration) { if (node.declaration.declarations) { return [node.declaration.declarations[0].id.name]; } if (node.declaration.id) { return [node.declaration.id.name]; } } // handles (re)named specifiers [export { x (as y)} from 'y']; return node.specifiers.map(s => { let specifier; if (s.exported) { // { x as y } specifier = s.exported.name; } else { // { x } specifier = s.local.name; } return specifier; }); } /** * @returns {object[]} */ function getLocalNameSpecifiers(node) { return node.specifiers .map(s => { if (s.exported && s.local && s.exported.name !== s.local.name) { return { local: s.local.name, exported: s.exported.name, }; } return undefined; }) .filter(s => s); } /** * @desc Finds import specifiers and sources for a given ast result * @param {BabelAst} ast * @param {boolean} searchForFileImports */ function findExportsPerAstEntry(ast, searchForFileImports) { // Visit AST... const transformedEntry = []; // Unfortunately, we cannot have async functions in babel traverse. // Therefore, we store a temp reference to path that we use later for // async post processing (tracking down original export Identifier) traverse(ast, { ExportNamedDeclaration(path) { const exportSpecifiers = getExportSpecifiers(path.node); const localMap = getLocalNameSpecifiers(path.node); const source = path.node.source && path.node.source.value; transformedEntry.push({ exportSpecifiers, localMap, source, __tmp: { path } }); }, ExportDefaultDeclaration(path) { const exportSpecifiers = ['[default]']; const source = path.node.declaration.name; transformedEntry.push({ exportSpecifiers, source, __tmp: { path } }); }, }); if (searchForFileImports) { // Always add an entry for just the file 'relativePath' // (since this also can be imported directly from a search target project) transformedEntry.push({ exportSpecifiers: ['[file]'], // source: relativePath, }); } return transformedEntry; } class FindExportsAnalyzer extends Analyzer { constructor() { super(); this.name = 'find-exports'; } /** * @desc Finds export specifiers and sources * @param {FindExportsConfig} customConfig */ async execute(customConfig = {}) { /** * @typedef FindExportsConfig * @property {boolean} [onlyInternalSources=false] * @property {{ [category]: (filePath) => boolean }} [customConfig.categories] object with * categories as keys and (not necessarily mutually exlusive) functions that define a category * @property {boolean} searchForFileImports Instead of only focusing on specifiers like * [import {specifier} 'lion-based-ui/foo.js'], also list [import 'lion-based-ui/foo.js'] as a result */ const cfg = { targetProjectPath: null, metaConfig: null, ...customConfig, }; /** * Prepare */ const analyzerResult = this._prepare(cfg); if (analyzerResult) { return analyzerResult; } /** * Traverse */ const projectPath = cfg.targetProjectPath; const queryOutput = await this._traverse(async (ast, { relativePath }) => { let transformedEntry = findExportsPerAstEntry(ast, cfg, relativePath, projectPath); transformedEntry = await normalizeSourcePaths(transformedEntry, relativePath, projectPath); transformedEntry = await trackdownRoot(transformedEntry, relativePath, projectPath); transformedEntry = cleanup(transformedEntry); return { result: transformedEntry }; }); /** * Finalize */ return this._finalize(queryOutput, cfg); } } module.exports = FindExportsAnalyzer;