513 lines
17 KiB
JavaScript
513 lines
17 KiB
JavaScript
/* eslint-disable no-shadow, no-param-reassign */
|
|
const MatchSubclassesAnalyzer = require('./match-subclasses.js');
|
|
const FindExportsAnalyzer = require('./find-exports.js');
|
|
const FindCustomelementsAnalyzer = require('./find-customelements.js');
|
|
const { Analyzer } = require('./helpers/Analyzer.js');
|
|
|
|
/** @typedef {import('./types').FindExportsAnalyzerResult} FindExportsAnalyzerResult */
|
|
/** @typedef {import('./types').FindCustomelementsAnalyzerResult} FindCustomelementsAnalyzerResult */
|
|
/** @typedef {import('./types').MatchSubclassesAnalyzerResult} MatchSubclassesAnalyzerResult */
|
|
/** @typedef {import('./types').FindImportsAnalyzerResult} FindImportsAnalyzerResult */
|
|
/** @typedef {import('./types').MatchedExportSpecifier} MatchedExportSpecifier */
|
|
/** @typedef {import('./types').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 {AnalyzerResult}
|
|
*/
|
|
function matchPathsPostprocess(
|
|
targetMatchSubclassesResult,
|
|
targetExportsResult,
|
|
targetFindCustomelementsResult,
|
|
refFindCustomelementsResult,
|
|
refFindExportsResult,
|
|
refProjectName,
|
|
) {
|
|
/** @type {AnalyzerResult} */
|
|
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' },
|
|
* ],
|
|
* }
|
|
* },
|
|
* ...
|
|
* ]
|
|
*/
|
|
class MatchPathsAnalyzer extends Analyzer {
|
|
constructor() {
|
|
super();
|
|
this.name = 'match-paths';
|
|
}
|
|
|
|
static get requiresReference() {
|
|
return 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,
|
|
});
|
|
|
|
// [A2]
|
|
const targetFindExportsAnalyzer = new FindExportsAnalyzer();
|
|
/** @type {FindExportsAnalyzerResult} */
|
|
const targetExportsResult = await targetFindExportsAnalyzer.execute({
|
|
targetProjectPath: cfg.targetProjectPath,
|
|
gatherFilesConfig: cfg.gatherFilesConfig,
|
|
skipCheckMatchCompatibility: cfg.skipCheckMatchCompatibility,
|
|
});
|
|
|
|
// [A3]
|
|
const refFindExportsAnalyzer = new FindExportsAnalyzer();
|
|
/** @type {FindExportsAnalyzerResult} */
|
|
const refFindExportsResult = await refFindExportsAnalyzer.execute({
|
|
targetProjectPath: cfg.referenceProjectPath,
|
|
gatherFilesConfig: cfg.gatherFilesConfigReference,
|
|
skipCheckMatchCompatibility: cfg.skipCheckMatchCompatibility,
|
|
});
|
|
|
|
/**
|
|
* ## 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,
|
|
});
|
|
|
|
// [B2]
|
|
const refFindCustomelementsAnalyzer = new FindCustomelementsAnalyzer();
|
|
/** @type {FindCustomelementsAnalyzerResult} */
|
|
const refFindCustomelementsResult = await refFindCustomelementsAnalyzer.execute({
|
|
targetProjectPath: cfg.referenceProjectPath,
|
|
gatherFilesConfig: cfg.gatherFilesConfigReference,
|
|
skipCheckMatchCompatibility: cfg.skipCheckMatchCompatibility,
|
|
});
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
module.exports = MatchPathsAnalyzer;
|