lion/packages-node/providence-analytics/src/program/analyzers/match-paths.js
2023-11-08 19:02:51 +01:00

515 lines
17 KiB
JavaScript

/* eslint-disable no-shadow, no-param-reassign */
import MatchSubclassesAnalyzer from './match-subclasses.js';
import FindExportsAnalyzer from './find-exports.js';
import FindCustomelementsAnalyzer from './find-customelements.js';
import { Analyzer } from '../core/Analyzer.js';
/**
* @typedef {import('../../../types/index.js').AnalyzerName} AnalyzerName
* @typedef {import('../../../types/index.js').FindExportsAnalyzerResult} FindExportsAnalyzerResult
* @typedef {import('../../../types/index.js').FindCustomelementsAnalyzerResult} FindCustomelementsAnalyzerResult
* @typedef {import('../../../types/index.js').MatchSubclassesAnalyzerResult} MatchSubclassesAnalyzerResult
* @typedef {import('../../../types/index.js').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../../../types/index.js').MatchedExportSpecifier} MatchedExportSpecifier
* @typedef {import('../../../types/index.js').RootFile} RootFile
*/
/**
* For prefix `{ from: 'lion', to: 'wolf' }`
*
* Keeps
* - WolfCheckbox (extended from LionCheckbox)
* - wolf-checkbox (extended from lion-checkbox)
*
* Removes
* - SheepCheckbox (extended from LionCheckbox)
* - WolfTickButton (extended from LionCheckbox)
* - sheep-checkbox (extended from lion-checkbox)
* - etc...
* @param {MatchPathsAnalyzerOutputFile[]} queryOutput
* @param {{from:string, to:string}} prefix
*/
function filterPrefixMatches(queryOutput, prefix) {
const capitalize = prefix => `${prefix[0].toUpperCase()}${prefix.slice(1)}`;
const filteredQueryOutput = queryOutput
.map(e => {
let keepVariable = false;
let keepTag = false;
if (e.variable) {
const fromUnprefixed = e.variable.from.replace(capitalize(prefix.from), '');
const toUnprefixed = e.variable.to.replace(capitalize(prefix.to), '');
keepVariable = fromUnprefixed === toUnprefixed;
}
if (e.tag) {
const fromUnprefixed = e.tag.from.replace(prefix.from, '');
const toUnprefixed = e.tag.to.replace(prefix.to, '');
keepTag = fromUnprefixed === toUnprefixed;
}
return {
name: e.name,
...(keepVariable ? { variable: e.variable } : {}),
...(keepTag ? { tag: e.tag } : {}),
};
})
.filter(e => e.tag || e.variable);
return filteredQueryOutput;
}
/**
*
* @param {MatchedExportSpecifier} matchSubclassesExportSpecifier
* @param {FindExportsAnalyzerResult} refFindExportsResult
* @returns {RootFile|undefined}
*/
function getExportSpecifierRootFile(matchSubclassesExportSpecifier, refFindExportsResult) {
/* eslint-disable arrow-body-style */
/** @type {RootFile} */
let rootFile;
refFindExportsResult.queryOutput.some(exportEntry => {
return exportEntry.result.some(exportEntryResult => {
if (!exportEntryResult.exportSpecifiers) {
return false;
}
/** @type {RootFile} */
exportEntryResult.exportSpecifiers.some(exportSpecifierString => {
const { name, filePath } = matchSubclassesExportSpecifier;
if (name === exportSpecifierString && filePath === exportEntry.file) {
const entry = exportEntryResult.rootFileMap.find(
rfEntry => rfEntry.currentFileSpecifier === name,
);
if (entry) {
rootFile = entry.rootFile;
if (rootFile.file === '[current]') {
rootFile.file = filePath;
}
}
}
return false;
});
return Boolean(rootFile);
});
});
return rootFile;
/* eslint-enable arrow-body-style */
}
function getClosestToRootTargetPath(targetPaths, targetExportsResult) {
let targetPath;
const { mainEntry } = targetExportsResult.analyzerMeta.targetProject;
if (targetPaths.includes(mainEntry)) {
targetPath = mainEntry;
} else {
// sort targetPaths: paths closest to root 'win'
[targetPath] = targetPaths.sort((a, b) => a.split('/').length - b.split('/').length);
}
return targetPath;
}
/**
*
* @param {FindExportsAnalyzerResult} targetExportsResult
* @param {FindExportsAnalyzerResult} refFindExportsResult
* @param {string} targetMatchedFile file where `toClass` from match-subclasses is defined
* @param {string} fromClass Identifier exported by reference project, for instance LionCheckbox
* @param {string} toClass Identifier exported by target project, for instance WolfCheckbox
* @param {string} refProjectName for instance @lion/checkbox
*/
function getVariablePaths(
targetExportsResult,
refFindExportsResult,
targetMatchedFile,
fromClass,
toClass,
refProjectName,
) {
/* eslint-disable arrow-body-style */
/**
* finds all paths that export WolfCheckbox
* @example ['./src/WolfCheckbox.js', './index.js']
* @type {string[]}
*/
const targetPaths = [];
targetExportsResult.queryOutput.forEach(({ file: targetExportsFile, result }) => {
// Find the FindExportAnalyzerEntry with the same rootFile as the rootPath of match-subclasses
// (targetMatchedFile)
const targetPathMatch = result.find(targetExportsEntry => {
return targetExportsEntry.rootFileMap.find(rootFileMapEntry => {
if (!rootFileMapEntry) {
return false;
}
const { rootFile } = rootFileMapEntry;
if (rootFile.specifier !== toClass) {
return false;
}
if (rootFile.file === '[current]') {
return targetExportsFile === targetMatchedFile;
}
return rootFile.file === targetMatchedFile;
});
});
if (targetPathMatch) {
targetPaths.push(targetExportsFile);
}
});
if (!targetPaths.length) {
return undefined; // there would be nothing to replace
}
const targetPath = getClosestToRootTargetPath(targetPaths, targetExportsResult);
// [A3]
/**
* finds all paths that import LionCheckbox
* @example ['./packages/checkbox/src/LionCheckbox.js', './index.js']
* @type {string[]}
*/
const refPaths = [];
refFindExportsResult.queryOutput.forEach(({ file, result }) => {
const refPathMatch = result.find(entry => {
if (entry.exportSpecifiers.includes(fromClass)) {
return true;
}
// if we're dealing with `export {x as y}`...
if (entry.localMap && entry.localMap.find(({ exported }) => exported === fromClass)) {
return true;
}
return false;
});
if (refPathMatch) {
refPaths.push(file);
}
});
const paths = refPaths.map(refP => ({ from: refP, to: targetPath }));
// Add all paths with project prefix as well.
const projectPrefixedPaths = paths.map(({ from, to }) => {
return { from: `${refProjectName}/${from.slice(2)}`, to };
});
return [...paths, ...projectPrefixedPaths];
/* eslint-enable arrow-body-style */
}
/**
*
* @param {FindCustomelementsAnalyzerResult} targetFindCustomelementsResult
* @param {FindCustomelementsAnalyzerResult} refFindCustomelementsResult
* @param {FindExportsAnalyzerResult} refFindExportsResult
* @param {string} targetMatchedFile file where `toClass` from match-subclasses is defined
* @param {string} toClass Identifier exported by target project, for instance `WolfCheckbox`
* @param {MatchSubclassEntry} matchSubclassEntry
*/
function getTagPaths(
targetFindCustomelementsResult,
refFindCustomelementsResult,
refFindExportsResult,
targetMatchedFile,
toClass,
matchSubclassEntry,
) {
/* eslint-disable arrow-body-style */
let targetResult;
targetFindCustomelementsResult.queryOutput.some(({ file, result }) => {
const targetPathMatch = result.find(entry => {
const sameRoot = entry.rootFile.file === targetMatchedFile;
const sameIdentifier = entry.rootFile.specifier === toClass;
return sameRoot && sameIdentifier;
});
if (targetPathMatch) {
targetResult = { file, tagName: targetPathMatch.tagName };
return true;
}
return false;
});
let refResult;
refFindCustomelementsResult.queryOutput.some(({ file, result }) => {
const refPathMatch = result.find(entry => {
const matchSubclassSpecifierRootFile = getExportSpecifierRootFile(
matchSubclassEntry.exportSpecifier,
refFindExportsResult,
);
if (!matchSubclassSpecifierRootFile) {
return false;
}
const sameRoot = entry.rootFile.file === matchSubclassSpecifierRootFile.file;
const sameIdentifier = entry.rootFile.specifier === matchSubclassEntry.exportSpecifier.name;
return sameRoot && sameIdentifier;
});
if (refPathMatch) {
refResult = { file, tagName: refPathMatch.tagName };
return true;
}
return false;
});
return { targetResult, refResult };
/* eslint-enable arrow-body-style */
}
/**
* @param {MatchSubclassesAnalyzerResult} targetMatchSubclassesResult
* @param {FindExportsAnalyzerResult} targetExportsResult
* @param {FindCustomelementsAnalyzerResult} targetFindCustomelementsResult
* @param {FindCustomelementsAnalyzerResult} refFindCustomelementsResult
* @param {FindExportsAnalyzerResult} refFindExportsResult
* @returns {AnalyzerQueryResult}
*/
function matchPathsPostprocess(
targetMatchSubclassesResult,
targetExportsResult,
targetFindCustomelementsResult,
refFindCustomelementsResult,
refFindExportsResult,
refProjectName,
) {
/** @type {AnalyzerQueryResult} */
const resultsArray = [];
targetMatchSubclassesResult.queryOutput.forEach(matchSubclassEntry => {
const fromClass = matchSubclassEntry.exportSpecifier.name;
matchSubclassEntry.matchesPerProject.forEach(projectMatch => {
projectMatch.files.forEach(({ identifier: toClass, file: targetMatchedFile }) => {
const resultEntry = {
name: fromClass,
};
// [A] Get variable paths
const paths = getVariablePaths(
targetExportsResult,
refFindExportsResult,
targetMatchedFile,
fromClass,
toClass,
refProjectName,
);
if (paths && paths.length) {
resultEntry.variable = {
from: fromClass,
to: toClass,
paths,
};
}
// [B] Get tag paths
const { targetResult, refResult } = getTagPaths(
targetFindCustomelementsResult,
refFindCustomelementsResult,
refFindExportsResult,
targetMatchedFile,
toClass,
matchSubclassEntry,
);
if (refResult && targetResult) {
resultEntry.tag = {
from: refResult.tagName,
to: targetResult.tagName,
paths: [
{ from: refResult.file, to: targetResult.file },
{ from: `${refProjectName}/${refResult.file.slice(2)}`, to: targetResult.file },
],
};
}
if (resultEntry.variable || resultEntry.tag) {
resultsArray.push(resultEntry);
}
});
});
});
return resultsArray;
}
/**
* Designed to work in conjunction with npm package `babel-plugin-extend-docs`.
* It will lookup all class exports from reference project A (and store their available paths) and
* matches them against all imports of project B that extend exported class (and store their
* available paths).
*
* @example
* [
* ...
* {
* name: 'LionCheckbox',
* variable: {
* from: 'LionCheckbox',
* to: 'WolfCheckbox',
* paths: [
* { from: './index.js', to: './index.js' },
* { from: './src/LionCheckbox.js', to: './index.js' },
* { from: '@lion/checkbox-group', to: './index.js' },
* { from: '@lion/checkbox-group/src/LionCheckbox.js', to: './index.js' },
* ],
* },
* tag: {
* from: 'lion-checkbox',
* to: 'wolf-checkbox',
* paths: [
* { from: './lion-checkbox.js', to: './wolf-checkbox.js' },
* { from: '@lion/checkbox-group/lion-checkbox.js', to: './wolf-checkbox.js' },
* ],
* }
* },
* ...
* ]
*/
export default class MatchPathsAnalyzer extends Analyzer {
/** @type {AnalyzerName} */
static analyzerName = 'match-paths';
static requiresReference = true;
/**
* @param {MatchClasspathsConfig} customConfig
*/
async execute(customConfig = {}) {
/**
* @typedef MatchClasspathsConfig
* @property {GatherFilesConfig} [gatherFilesConfig]
* @property {GatherFilesConfig} [gatherFilesConfigReference]
* @property {string} [referenceProjectPath] reference path
* @property {string} [targetProjectPath] search target path
* @property {{ from: string, to: string }} [prefix]
*/
const cfg = {
gatherFilesConfig: {},
gatherFilesConfigReference: {},
referenceProjectPath: null,
targetProjectPath: null,
prefix: null,
...customConfig,
};
/**
* Prepare
*/
const analyzerResult = this._prepare(cfg);
if (analyzerResult) {
return analyzerResult;
}
/**
* ## Goal A: variable
* Automatically generate a mapping from lion docs import paths to extension layer
* import paths. To be served to extend-docs
*
* ## Traversal steps
*
* [A1] Find path variable.to 'WolfCheckbox'
* Run 'match-subclasses' for target project: we find the 'rootFilePath' of class definition,
* which will be matched against the rootFiles found in [A2]
* Result: './packages/wolf-checkbox/WolfCheckbox.js'
* [A2] Find root export path under which 'WolfCheckbox' is exported
* Run 'find-exports' on target: we find all paths like ['./index.js', './src/WolfCheckbox.js']
* Result: './index.js'
* [A3] Find all exports of LionCheckbox
* Run 'find-exports' for reference project
* Result: ['./src/LionCheckbox.js', './index.js']
* [A4] Match data and create a result object "variable"
*/
// [A1]
const targetMatchSubclassesAnalyzer = new MatchSubclassesAnalyzer();
/** @type {MatchSubclassesAnalyzerResult} */
const targetMatchSubclassesResult = await targetMatchSubclassesAnalyzer.execute({
targetProjectPath: cfg.targetProjectPath,
referenceProjectPath: cfg.referenceProjectPath,
gatherFilesConfig: cfg.gatherFilesConfig,
gatherFilesConfigReference: cfg.gatherFilesConfigReference,
skipCheckMatchCompatibility: cfg.skipCheckMatchCompatibility,
suppressNonCriticalLogs: true,
});
// [A2]
const targetFindExportsAnalyzer = new FindExportsAnalyzer();
/** @type {FindExportsAnalyzerResult} */
const targetExportsResult = await targetFindExportsAnalyzer.execute({
targetProjectPath: cfg.targetProjectPath,
gatherFilesConfig: cfg.gatherFilesConfig,
skipCheckMatchCompatibility: cfg.skipCheckMatchCompatibility,
suppressNonCriticalLogs: true,
});
// [A3]
const refFindExportsAnalyzer = new FindExportsAnalyzer();
/** @type {FindExportsAnalyzerResult} */
const refFindExportsResult = await refFindExportsAnalyzer.execute({
targetProjectPath: cfg.referenceProjectPath,
gatherFilesConfig: cfg.gatherFilesConfigReference,
skipCheckMatchCompatibility: cfg.skipCheckMatchCompatibility,
suppressNonCriticalLogs: true,
});
/**
* ## Goal B: tag
* Automatically generate a mapping from lion docs import paths to extension layer
* import paths. To be served to extend-docs
*
* [B1] Find path variable.to 'WolfCheckbox'
* Run 'match-subclasses' for target project: we find the 'rootFilePath' of class definition,
* Result: './packages/wolf-checkbox/WolfCheckbox.js'
* [B2] Find export path of 'wolf-checkbox'
* Run 'find-customelements' on target project and match rootFile of [B1] with rootFile of
* constructor.
* Result: './wolf-checkbox.js'
* [B3] Find export path of 'lion-checkbox'
* Run 'find-customelements' and find-exports (for rootpath) on reference project and match
* rootFile of constructor with rootFiles of where LionCheckbox is defined.
* Result: './packages/checkbox/lion-checkbox.js',
* [B4] Match data and create a result object "tag"
*/
// [B1]
const targetFindCustomelementsAnalyzer = new FindCustomelementsAnalyzer();
/** @type {FindCustomelementsAnalyzerResult} */
const targetFindCustomelementsResult = await targetFindCustomelementsAnalyzer.execute({
targetProjectPath: cfg.targetProjectPath,
gatherFilesConfig: cfg.gatherFilesConfig,
skipCheckMatchCompatibility: cfg.skipCheckMatchCompatibility,
suppressNonCriticalLogs: true,
});
// [B2]
const refFindCustomelementsAnalyzer = new FindCustomelementsAnalyzer();
/** @type {FindCustomelementsAnalyzerResult} */
const refFindCustomelementsResult = await refFindCustomelementsAnalyzer.execute({
targetProjectPath: cfg.referenceProjectPath,
gatherFilesConfig: cfg.gatherFilesConfigReference,
skipCheckMatchCompatibility: cfg.skipCheckMatchCompatibility,
suppressNonCriticalLogs: true,
});
// refFindExportsAnalyzer was already created in A3
// Use one of the reference analyzer instances to get the project name
const refProjectName = refFindExportsAnalyzer.targetProjectMeta.name;
let queryOutput = matchPathsPostprocess(
targetMatchSubclassesResult,
targetExportsResult,
targetFindCustomelementsResult,
refFindCustomelementsResult,
refFindExportsResult,
refProjectName,
);
if (cfg.prefix) {
queryOutput = filterPrefixMatches(queryOutput, cfg.prefix);
}
/**
* Finalize
*/
return this._finalize(queryOutput, cfg);
}
}