lion/packages-node/providence-analytics/src/program/analyzers/find-customelements.js

129 lines
3.7 KiB
JavaScript

const pathLib = require('path');
const t = require('@babel/types');
const { default: traverse } = require('@babel/traverse');
const { Analyzer } = require('./helpers/Analyzer.js');
const { trackDownIdentifierFromScope } = require('./helpers/track-down-identifier.js');
const { aForEach } = require('../utils/async-array-utils.js');
function cleanup(transformedEntry) {
transformedEntry.forEach(definitionObj => {
if (definitionObj.__tmp) {
// eslint-disable-next-line no-param-reassign
delete definitionObj.__tmp;
}
});
return transformedEntry;
}
async function trackdownRoot(transformedEntry, relativePath, projectPath) {
const fullCurrentFilePath = pathLib.resolve(projectPath, relativePath);
await aForEach(transformedEntry, async definitionObj => {
const rootFile = await trackDownIdentifierFromScope(
definitionObj.__tmp.path,
definitionObj.constructorIdentifier,
fullCurrentFilePath,
projectPath,
);
// eslint-disable-next-line no-param-reassign
definitionObj.rootFile = rootFile;
});
return transformedEntry;
}
/**
* @desc Finds import specifiers and sources
* @param {BabelAst} ast
*/
function findCustomElementsPerAstEntry(ast) {
const definitions = [];
traverse(ast, {
CallExpression(path) {
let found = false;
// Doing it like this we detect 'customElements.define()',
// but also 'window.customElements.define()'
path.traverse({
MemberExpression(memberPath) {
if (memberPath.parentPath !== path) {
return;
}
const { node } = memberPath;
if (node.object.name === 'customElements' && node.property.name === 'define') {
found = true;
}
if (
node.object.object &&
node.object.object.name === 'window' &&
node.object.property.name === 'customElements' &&
node.property.name === 'define'
) {
found = true;
}
},
});
if (found) {
let tagName;
let constructorIdentifier;
if (t.isLiteral(path.node.arguments[0])) {
tagName = path.node.arguments[0].value;
} else {
// No Literal found. For now, we only mark them as '[variable]'
tagName = '[variable]';
}
if (path.node.arguments[1].type === 'Identifier') {
constructorIdentifier = path.node.arguments[1].name;
} else {
// We assume customElements.define('my-el', class extends HTMLElement {...})
constructorIdentifier = '[inline]';
}
definitions.push({ tagName, constructorIdentifier, __tmp: { path } });
}
},
});
return definitions;
}
class FindCustomelementsAnalyzer extends Analyzer {
constructor() {
super();
this.name = 'find-customelements';
}
/**
* @desc Finds export specifiers and sources
* @param {FindCustomelementsConfig} customConfig
*/
async execute(customConfig = {}) {
const cfg = {
targetProjectPath: 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 = findCustomElementsPerAstEntry(ast);
transformedEntry = await trackdownRoot(transformedEntry, relativePath, projectPath);
transformedEntry = cleanup(transformedEntry);
return { result: transformedEntry };
});
/**
* Finalize
*/
return this._finalize(queryOutput, cfg);
}
}
module.exports = FindCustomelementsAnalyzer;