lion/packages-node/providence-analytics/src/program/analyzers/find-imports.js
2023-11-08 19:01:20 +01:00

184 lines
5.9 KiB
JavaScript

/* eslint-disable no-shadow, no-param-reassign */
const { default: traverse } = require('@babel/traverse');
const { isRelativeSourcePath } = require('../utils/relative-source-path.js');
const { normalizeSourcePaths } = require('./helpers/normalize-source-paths.js');
const { Analyzer } = require('../core/Analyzer.js');
const { LogService } = require('../core/LogService.js');
/**
* @typedef {import('@babel/types').File} File
* @typedef {import('@babel/types').Node} Node *
* @typedef {import('../types/core').AnalyzerName} AnalyzerName
* @typedef {import('../types/analyzers').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../types/analyzers').FindImportsAnalyzerEntry} FindImportsAnalyzerEntry
* @typedef {import('../types/core').PathRelativeFromProjectRoot} PathRelativeFromProjectRoot
*/
/**
* Options that allow to filter 'on a file basis'.
* We can also filter on the total result
*/
const /** @type {AnalyzerConfig} */ options = {
/**
* Only leaves entries with external sources:
* - keeps: '@open-wc/testing'
* - drops: '../testing'
* @param {FindImportsAnalyzerQueryOutput} result
* @param {string} targetSpecifier for instance 'LitElement'
*/
onlyExternalSources(result) {
return result.filter(entry => !isRelativeSourcePath(entry.source));
},
};
/**
* @param {Node} node
*/
function getImportOrReexportsSpecifiers(node) {
return node.specifiers.map(s => {
if (s.type === 'ImportDefaultSpecifier' || s.type === 'ExportDefaultSpecifier') {
return '[default]';
}
if (s.type === 'ImportNamespaceSpecifier' || s.type === 'ExportNamespaceSpecifier') {
return '[*]';
}
if ((s.imported && s.type === 'ImportNamespaceSpecifier') || s.type === 'ImportSpecifier') {
return s.imported.name;
}
if (s.exported && s.type === 'ExportNamespaceSpecifier') {
return s.exported.name;
}
return s.local.name;
});
}
/**
* Finds import specifiers and sources
* @param {File} ast
*/
function findImportsPerAstEntry(ast) {
LogService.debug(`Analyzer "find-imports": started findImportsPerAstEntry method`);
// https://github.com/babel/babel/blob/672a58660f0b15691c44582f1f3fdcdac0fa0d2f/packages/babel-core/src/transformation/index.ts#L110
// Visit AST...
/** @type {Partial<FindImportsAnalyzerEntry>[]} */
const transformedEntry = [];
traverse(ast, {
ImportDeclaration(path) {
const importSpecifiers = getImportOrReexportsSpecifiers(path.node);
if (!importSpecifiers.length) {
importSpecifiers.push('[file]'); // apparently, there was just a file import
}
const source = path.node.source.value;
const entry = { importSpecifiers, source };
if (path.node.assertions?.length) {
entry.assertionType = path.node.assertions[0].value?.value;
}
transformedEntry.push(entry);
},
// Dynamic imports
CallExpression(path) {
if (path.node.callee && path.node.callee.type === 'Import') {
// TODO: check for specifiers catched via obj destructuring?
// TODO: also check for ['file']
const importSpecifiers = ['[default]'];
let source = path.node.arguments[0].value;
if (!source) {
// TODO: with advanced retrieval, we could possibly get the value
source = '[variable]';
}
transformedEntry.push({ importSpecifiers, source });
}
},
ExportNamedDeclaration(path) {
if (!path.node.source) {
return; // we are dealing with a regular export, not a reexport
}
const importSpecifiers = getImportOrReexportsSpecifiers(path.node);
const source = path.node.source.value;
const entry = { importSpecifiers, source };
if (path.node.assertions?.length) {
entry.assertionType = path.node.assertions[0].value?.value;
}
transformedEntry.push(entry);
},
// ExportAllDeclaration(path) {
// if (!path.node.source) {
// return; // we are dealing with a regular export, not a reexport
// }
// const importSpecifiers = ['[*]'];
// const source = path.node.source.value;
// transformedEntry.push({ importSpecifiers, source });
// },
});
return transformedEntry;
}
class FindImportsAnalyzer extends Analyzer {
/** @type {AnalyzerName} */
static get analyzerName() {
return 'find-imports';
}
/**
* Finds import specifiers and sources
* @param {FindImportsConfig} customConfig
*/
async execute(customConfig = {}) {
/**
* @typedef FindImportsConfig
* @property {boolean} [keepInternalSources=false] by default, relative paths like '../x.js' are
* filtered out. This option keeps them.
* means that 'external-dep/file' will be resolved to 'external-dep/file.js' will both be stored
* as the latter
*/
const cfg = {
targetProjectPath: null,
// post process file
keepInternalSources: false,
...customConfig,
};
/**
* Prepare
*/
const analyzerResult = this._prepare(cfg);
if (analyzerResult) {
return analyzerResult;
}
/**
* Traverse
*/
const queryOutput = await this._traverse(async (ast, { relativePath }) => {
let transformedEntry = findImportsPerAstEntry(ast);
// Post processing based on configuration...
transformedEntry = await normalizeSourcePaths(
transformedEntry,
relativePath,
cfg.targetProjectPath,
);
if (!cfg.keepInternalSources) {
transformedEntry = options.onlyExternalSources(transformedEntry);
}
return { result: transformedEntry };
});
// if (cfg.sortBySpecifier) {
// queryOutput = sortBySpecifier.execute(queryOutput, {
// ...cfg,
// specifiersKey: 'importSpecifiers',
// });
// }
/**
* Finalize
*/
return this._finalize(queryOutput, cfg);
}
}
module.exports = FindImportsAnalyzer;