272 lines
8.6 KiB
JavaScript
272 lines
8.6 KiB
JavaScript
/* eslint-disable no-shadow, no-param-reassign */
|
|
import pathLib from 'path';
|
|
import babelTraverse from '@babel/traverse';
|
|
import { Analyzer } from '../core/Analyzer.js';
|
|
import { trackDownIdentifier } from './helpers/track-down-identifier.js';
|
|
import { normalizeSourcePaths } from './helpers/normalize-source-paths.js';
|
|
import { getReferencedDeclaration } from '../utils/get-source-code-fragment-of-declaration.js';
|
|
import { LogService } from '../core/LogService.js';
|
|
|
|
/**
|
|
* @typedef {import('@babel/types').File} File
|
|
* @typedef {import('@babel/types').Node} Node
|
|
* @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName
|
|
* @typedef {import('../../../types/index.js').FindExportsAnalyzerResult} FindExportsAnalyzerResult
|
|
* @typedef {import('../../../types/index.js').FindExportsAnalyzerEntry} FindExportsAnalyzerEntry
|
|
* @typedef {import('../../../types/index.js').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
|
|
* @typedef {import('./helpers/track-down-identifier.js').RootFile} RootFile
|
|
* @typedef {object} RootFileMapEntry
|
|
* @typedef {string} currentFileSpecifier this is the local name in the file we track from
|
|
* @typedef {RootFile} rootFile contains file(filePath) and specifier
|
|
* @typedef {RootFileMapEntry[]} RootFileMap
|
|
* @typedef {{ exportSpecifiers:string[]; localMap: object; source:string, __tmp: { path:string } }} FindExportsSpecifierObj
|
|
*/
|
|
|
|
/**
|
|
* @param {FindExportsSpecifierObj[]} transformedFile
|
|
*/
|
|
async function trackdownRoot(transformedFile, relativePath, projectPath) {
|
|
const fullCurrentFilePath = pathLib.resolve(projectPath, relativePath);
|
|
for (const specObj of transformedFile) {
|
|
/** @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',
|
|
* }
|
|
* }
|
|
*/
|
|
for (const specifier of specObj.exportSpecifiers) {
|
|
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?.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 transformedFile;
|
|
}
|
|
|
|
function cleanup(transformedFile) {
|
|
transformedFile.forEach(specObj => {
|
|
if (specObj.__tmp) {
|
|
delete specObj.__tmp;
|
|
}
|
|
});
|
|
return transformedFile;
|
|
}
|
|
|
|
/**
|
|
* @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 === 'default' ? '[default]' : 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 {
|
|
// if reserved keyword 'default' is used, translate it into 'providence keyword'
|
|
local: s.local.name === 'default' ? '[default]' : s.local.name,
|
|
exported: s.exported.name,
|
|
};
|
|
}
|
|
return undefined;
|
|
})
|
|
.filter(s => s);
|
|
}
|
|
|
|
const isImportingSpecifier = pathOrNode =>
|
|
pathOrNode.type === 'ImportDefaultSpecifier' || pathOrNode.type === 'ImportSpecifier';
|
|
|
|
/**
|
|
* Finds import specifiers and sources for a given ast result
|
|
* @param {File} babelAst
|
|
* @param {FindExportsConfig} config
|
|
*/
|
|
function findExportsPerAstFile(babelAst, { skipFileImports }) {
|
|
LogService.debug(`Analyzer "find-exports": started findExportsPerAstFile method`);
|
|
|
|
// Visit AST...
|
|
|
|
/** @type {FindExportsSpecifierObj[]} */
|
|
const transformedFile = [];
|
|
// 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)
|
|
let globalScopeBindings;
|
|
|
|
babelTraverse.default(babelAst, {
|
|
Program: {
|
|
enter(babelPath) {
|
|
const body = babelPath.get('body');
|
|
if (body.length) {
|
|
globalScopeBindings = body[0].scope.bindings;
|
|
}
|
|
},
|
|
},
|
|
ExportNamedDeclaration(astPath) {
|
|
const exportSpecifiers = getExportSpecifiers(astPath.node);
|
|
const localMap = getLocalNameSpecifiers(astPath.node);
|
|
const source = astPath.node.source?.value;
|
|
const entry = { exportSpecifiers, localMap, source, __tmp: { astPath } };
|
|
if (astPath.node.assertions?.length) {
|
|
entry.assertionType = astPath.node.assertions[0].value?.value;
|
|
}
|
|
transformedFile.push(entry);
|
|
},
|
|
ExportDefaultDeclaration(defaultExportPath) {
|
|
const exportSpecifiers = ['[default]'];
|
|
let source;
|
|
if (defaultExportPath.node.declaration?.type !== 'Identifier') {
|
|
source = defaultExportPath.node.declaration.name;
|
|
} else {
|
|
const importOrDeclPath = getReferencedDeclaration({
|
|
referencedIdentifierName: defaultExportPath.node.declaration.name,
|
|
globalScopeBindings,
|
|
});
|
|
if (isImportingSpecifier(importOrDeclPath)) {
|
|
source = importOrDeclPath.parentPath.node.source.value;
|
|
}
|
|
}
|
|
transformedFile.push({ exportSpecifiers, source, __tmp: { astPath: defaultExportPath } });
|
|
},
|
|
});
|
|
|
|
if (!skipFileImports) {
|
|
// Always add an entry for just the file 'relativePath'
|
|
// (since this also can be imported directly from a search target project)
|
|
transformedFile.push({
|
|
exportSpecifiers: ['[file]'],
|
|
// source: relativePath,
|
|
});
|
|
}
|
|
|
|
return transformedFile;
|
|
}
|
|
|
|
export default class FindExportsAnalyzer extends Analyzer {
|
|
/** @type {AnalyzerName} */
|
|
static analyzerName = 'find-exports';
|
|
|
|
/** @type {'babel'|'swc-to-babel'} */
|
|
requiredAst = 'swc-to-babel';
|
|
|
|
/**
|
|
* Finds export specifiers and sources
|
|
* @param {FindExportsConfig} customConfig
|
|
*/
|
|
async execute(customConfig = {}) {
|
|
/**
|
|
* @typedef FindExportsConfig
|
|
* @property {boolean} [onlyInternalSources=false]
|
|
* @property {boolean} [skipFileImports=false] Instead of both focusing on specifiers like
|
|
* [import {specifier} 'lion-based-ui/foo.js'], and [import 'lion-based-ui/foo.js'] as a result,
|
|
* not list file exports
|
|
*/
|
|
const cfg = {
|
|
targetProjectPath: null,
|
|
skipFileImports: false,
|
|
...customConfig,
|
|
};
|
|
|
|
/**
|
|
* Prepare
|
|
*/
|
|
const cachedAnalyzerResult = this._prepare(cfg);
|
|
if (cachedAnalyzerResult) {
|
|
return cachedAnalyzerResult;
|
|
}
|
|
|
|
/**
|
|
* Traverse
|
|
*/
|
|
const projectPath = cfg.targetProjectPath;
|
|
|
|
const traverseEntryFn = async (ast, { relativePath }) => {
|
|
let transformedFile = findExportsPerAstFile(ast, cfg);
|
|
|
|
transformedFile = await normalizeSourcePaths(transformedFile, relativePath, projectPath);
|
|
transformedFile = await trackdownRoot(transformedFile, relativePath, projectPath);
|
|
transformedFile = cleanup(transformedFile);
|
|
|
|
return { result: transformedFile };
|
|
};
|
|
|
|
const queryOutput = await this._traverse({
|
|
traverseEntryFn,
|
|
filePaths: cfg.targetFilePaths,
|
|
projectPath: cfg.targetProjectPath,
|
|
});
|
|
|
|
/**
|
|
* Finalize
|
|
*/
|
|
return this._finalize(queryOutput, cfg);
|
|
}
|
|
}
|