224 lines
6.8 KiB
JavaScript
224 lines
6.8 KiB
JavaScript
/* 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;
|