fix: support export maps for match-* analyzers

This commit is contained in:
Thijs Louisse 2021-11-12 18:15:28 +01:00
parent c59dda0c93
commit 1e8839f2fd
12 changed files with 703 additions and 340 deletions

View file

@ -0,0 +1,5 @@
---
'providence-analytics': patch
---
Support export maps for match-\* analyzers

View file

@ -35,7 +35,7 @@
"@babel/register": "^7.5.5",
"@babel/traverse": "^7.5.5",
"@babel/types": "^7.9.0",
"@rollup/plugin-node-resolve": "^7.1.1",
"@rollup/plugin-node-resolve": "^13.0.6",
"@typescript-eslint/typescript-estree": "^3.0.0",
"anymatch": "^3.1.1",
"chalk": "^4.1.0",
@ -48,6 +48,7 @@
"inquirer": "^7.0.0",
"is-negated-glob": "^1.0.0",
"lit-element": "~2.4.0",
"mock-require": "^3.0.3",
"ora": "^3.4.0",
"parse5": "^5.1.1",
"read-package-tree": "5.3.1",

View file

@ -1,79 +1,51 @@
const fs = require('fs');
const pathLib = require('path');
const { isRelativeSourcePath } = require('../../utils/relative-source-path.js');
const { LogService } = require('../../services/LogService.js');
const { resolveImportPath } = require('../../utils/resolve-import-path.js');
/**
* TODO: Use utils/resolve-import-path for 100% accuracy
*
* - from: 'reference-project/foo.js'
* - to: './foo.js'
* When we need to resolve to the main entry:
* - from: 'reference-project'
* - to: './index.js' (or other file specified in package.json 'main')
* @param {object} config
* @param {string} config.requestedExternalSource
* @param {{name, mainEntry}} config.externalProjectMeta
* @param {string} config.externalRootPath
* @returns {string|null}
* @param {string} importee like '@lion/core/myFile.js'
* @returns {string} project name ('@lion/core')
*/
function fromImportToExportPerspective({
requestedExternalSource,
externalProjectMeta,
externalRootPath,
}) {
if (isRelativeSourcePath(requestedExternalSource)) {
LogService.warn('[fromImportToExportPerspective] Please only provide external import paths');
return null;
}
const scopedProject = requestedExternalSource[0] === '@';
function getProjectFromImportee(importee) {
const scopedProject = importee[0] === '@';
// 'external-project/src/file.js' -> ['external-project', 'src', file.js']
let splitSource = requestedExternalSource.split('/');
let splitSource = importee.split('/');
if (scopedProject) {
// '@external/project'
splitSource = [splitSource.slice(0, 2).join('/'), ...splitSource.slice(2)];
}
// ['external-project', 'src', 'file.js'] -> 'external-project'
const project = splitSource.slice(0, 1).join('/');
// ['external-project', 'src', 'file.js'] -> 'src/file.js'
const localPath = splitSource.slice(1).join('/');
if (externalProjectMeta.name !== project) {
return project;
}
/**
* Gets local path from reference project
*
* - from: 'reference-project/foo'
* - to: './foo.js'
* When we need to resolve to the main entry:
* - from: 'reference-project'
* - to: './index.js' (or other file specified in package.json 'main')
* @param {object} config
* @param {string} config.importee 'reference-project/foo.js'
* @param {string} config.importer '/my/project/importing-file.js'
* @returns {Promise<string|null>} './foo.js'
*/
async function fromImportToExportPerspective({ importee, importer }) {
if (isRelativeSourcePath(importee)) {
LogService.warn('[fromImportToExportPerspective] Please only provide external import paths');
return null;
}
if (localPath) {
// like '@open-wc/x/y.js'
// Now, we need to resolve to a file or path. Even though a path can contain '.',
// we still need to check if we're not dealing with a folder.
// - '@open-wc/x/y.js' -> '@open-wc/x/y.js' or... '@open-wc/x/y.js/index.js' ?
// - or 'lion-based-ui/test' -> 'lion-based-ui/test/index.js' or 'lion-based-ui/test' ?
if (externalRootPath) {
const pathToCheck = pathLib.resolve(externalRootPath, `./${localPath}`);
const absolutePath = await resolveImportPath(importee, importer);
const projectName = getProjectFromImportee(importee);
if (fs.existsSync(pathToCheck)) {
const stat = fs.statSync(pathToCheck);
if (stat && stat.isFile()) {
return `./${localPath}`; // '/path/to/lion-based-ui/fol.der' is a file
}
return `./${localPath}/index.js`; // '/path/to/lion-based-ui/fol.der' is a folder
// eslint-disable-next-line no-else-return
} else if (fs.existsSync(`${pathToCheck}.js`)) {
return `./${localPath}.js`; // '/path/to/lion-based-ui/fol.der' is file '/path/to/lion-based-ui/fol.der.js'
}
} else {
return `./${localPath}`;
}
} else {
// like '@lion/core'
let mainEntry = externalProjectMeta.mainEntry || 'index.js';
if (!mainEntry.startsWith('./')) {
mainEntry = `./${mainEntry}`;
}
return mainEntry;
}
return null;
// from /my/reference/project/packages/foo/index.js to './packages/foo/index.js'
return absolutePath
? absolutePath.replace(new RegExp(`^.*/${projectName}/?(.*)$`), './$1')
: null;
}
module.exports = { fromImportToExportPerspective };

View file

@ -1,3 +1,5 @@
/* eslint-disable no-continue */
const pathLib = require('path');
/* eslint-disable no-shadow, no-param-reassign */
const FindImportsAnalyzer = require('./find-imports.js');
const FindExportsAnalyzer = require('./find-exports.js');
@ -5,19 +7,13 @@ const { Analyzer } = require('./helpers/Analyzer.js');
const { fromImportToExportPerspective } = require('./helpers/from-import-to-export-perspective.js');
/**
* @desc Helper method for matchImportsPostprocess. Modifies its resultsObj
* @param {object} resultsObj
* @param {string} exportId like 'myExport::./reference-project/my/export.js::my-project'
* @param {Set<string>} filteredList
* @typedef {import('../types/find-imports').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @typedef {import('../types/find-exports').FindExportsAnalyzerResult} FindExportsAnalyzerResult
* @typedef {import('../types/find-exports').IterableFindExportsAnalyzerEntry} IterableFindExportsAnalyzerEntry
* @typedef {import('../types/find-imports').IterableFindImportsAnalyzerEntry} IterableFindImportsAnalyzerEntry
* @typedef {import('../types/match-imports').ConciseMatchImportsAnalyzerResult} ConciseMatchImportsAnalyzerResult
* @typedef {import('../types/core').PathRelativeFromRoot} PathRelativeFromRoot
*/
function storeResult(resultsObj, exportId, filteredList, meta) {
if (!resultsObj[exportId]) {
// eslint-disable-next-line no-param-reassign
resultsObj[exportId] = { meta };
}
// eslint-disable-next-line no-param-reassign
resultsObj[exportId].files = [...(resultsObj[exportId].files || []), ...Array.from(filteredList)];
}
/**
* Needed in case fromImportToExportPerspective does not have a
@ -33,166 +29,227 @@ function compareImportAndExportPaths(exportPath, translatedImportPath) {
);
}
/**
* Convert to more easily iterable object
*
* From:
* ```js
* [
* "file": "./file-1.js",
* "result": [{
* "exportSpecifiers": [ "a", "b"],
* "localMap": [{...},{...}],
* "source": null,
* "rootFileMap": [{"currentFileSpecifier": "a", "rootFile": { "file": "[current]", "specifier": "a" }}]
* }, ...],
* ```
* To:
* ```js
* [{
* "file": ""./file-1.js",
* "exportSpecifier": "a",
* "localMap": {...},
* "source": null,
* "rootFileMap": {...}
* },
* {{
* "file": ""./file-1.js",
* "exportSpecifier": "b",
* "localMap": {...},
* "source": null,
* "rootFileMap": {...}
* }}],
*
* @param {FindExportsAnalyzerResult} exportsAnalyzerResult
*/
function transformIntoIterableFindExportsOutput(exportsAnalyzerResult) {
/** @type {IterableFindExportsAnalyzerEntry[]} */
const iterableEntries = [];
for (const { file, result } of exportsAnalyzerResult.queryOutput) {
for (const { exportSpecifiers, source, rootFileMap, localMap, meta } of result) {
if (!exportSpecifiers) {
break;
}
for (const exportSpecifier of exportSpecifiers) {
const i = exportSpecifiers.indexOf(exportSpecifier);
/** @type {IterableFindExportsAnalyzerEntry} */
const resultEntry = {
file,
specifier: exportSpecifier,
source,
rootFile: rootFileMap ? rootFileMap[i] : undefined,
localSpecifier: localMap ? localMap[i] : undefined,
meta,
};
iterableEntries.push(resultEntry);
}
}
}
return iterableEntries;
}
/**
* Convert to more easily iterable object
*
* From:
* ```js
* [
* "file": "./file-1.js",
* "result": [{
* "importSpecifiers": [ "a", "b" ],
* "source": "exporting-ref-project",
* "normalizedSource": "exporting-ref-project"
* }], ,
* ```
* To:
* ```js
* [{
* "file": ""./file-1.js",
* "importSpecifier": "a",,
* "source": "exporting-ref-project",
* "normalizedSource": "exporting-ref-project"
* },
* {{
* "file": ""./file-1.js",
* "importSpecifier": "b",,
* "source": "exporting-ref-project",
* "normalizedSource": "exporting-ref-project"
* }}],
*
* @param {FindImportsAnalyzerResult} importsAnalyzerResult
*/
function transformIntoIterableFindImportsOutput(importsAnalyzerResult) {
/** @type {IterableFindImportsAnalyzerEntry[]} */
const iterableEntries = [];
for (const { file, result } of importsAnalyzerResult.queryOutput) {
for (const { importSpecifiers, source, normalizedSource } of result) {
if (!importSpecifiers) {
break;
}
for (const importSpecifier of importSpecifiers) {
/** @type {IterableFindImportsAnalyzerEntry} */
const resultEntry = {
file,
specifier: importSpecifier,
source,
normalizedSource,
};
iterableEntries.push(resultEntry);
}
}
}
return iterableEntries;
}
/**
* Makes a concise results array a 'compatible resultsArray' (compatible with dashbaord + tests + ...?)
* @param {object[]} conciseResultsArray
* @param {string} importProject
*/
function createCompatibleMatchImportsResult(conciseResultsArray, importProject) {
const compatibleResult = [];
for (const matchedExportEntry of conciseResultsArray) {
const [name, filePath, project] = matchedExportEntry.exportSpecifier.id.split('::');
const exportSpecifier = {
...matchedExportEntry.exportSpecifier,
name,
filePath,
project,
};
compatibleResult.push({
exportSpecifier,
matchesPerProject: [{ project: importProject, files: matchedExportEntry.importProjectFiles }],
});
}
return compatibleResult;
}
/**
* @param {FindExportsAnalyzerResult} exportsAnalyzerResult
* @param {FindImportsAnalyzerResult} importsAnalyzerResult
* @param {matchImportsConfig} customConfig
* @returns {AnalyzerResult}
* @returns {Promise<AnalyzerResult>}
*/
function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerResult, customConfig) {
async function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerResult, customConfig) {
const cfg = {
...customConfig,
};
/**
* Step 1: a 'flat' data structure
* @desc Create a key value storage map for exports/imports matches
* - key: `${exportSpecifier}::${normalizedSource}::${project}` from reference project
* - value: an array of import file matches like `${targetProject}::${normalizedSource}`
* @example
* {
* 'myExport::./reference-project/my/export.js::my-project' : {
* meta: {...},
* files: [
* 'target-project-a::./import/file.js',
* 'target-project-b::./another/import/file.js'
* ],
* ]}
* }
*/
const resultsObj = {};
// TODO: What if this info is retrieved from cached importProject/target project?
const importProjectPath = cfg.targetProjectPath;
exportsAnalyzerResult.queryOutput.forEach(exportEntry => {
const exportsProjectObj = exportsAnalyzerResult.analyzerMeta.targetProject;
const iterableFindExportsOutput = transformIntoIterableFindExportsOutput(exportsAnalyzerResult);
const iterableFindImportsOutput = transformIntoIterableFindImportsOutput(importsAnalyzerResult);
// Look for all specifiers that are exported, like [import {specifier} 'lion-based-ui/foo.js']
exportEntry.result.forEach(exportEntryResult => {
if (!exportEntryResult.exportSpecifiers) {
return;
/** @type {ConciseMatchImportsAnalyzerResult} */
const conciseResultsArray = [];
for (const exportEntry of iterableFindExportsOutput) {
for (const importEntry of iterableFindImportsOutput) {
/**
* 1. Does target import ref specifier?
*
* Example context (read by 'find-imports'/'find-exports' analyzers)
* - export (/folder/exporting-file.js):
* `export const x = 'foo'`
* - import (target-project-a/importing-file.js):
* `import { x, y } from '@reference-repo/folder/exporting-file.js'`
* Example variables (extracted by 'find-imports'/'find-exports' analyzers)
* - exportSpecifier: 'x'
* - importSpecifiers: ['x', 'y']
* @type {boolean}
*/
const hasExportSpecifierImported =
exportEntry.specifier === importEntry.specifier || importEntry.specifier === '[*]';
if (!hasExportSpecifierImported) {
continue;
}
exportEntryResult.exportSpecifiers.forEach(exportSpecifier => {
// Get all unique imports (name::source::project combinations) that match current exportSpecifier
const filteredImportsList = new Set();
const exportId = `${exportSpecifier}::${exportEntry.file}::${exportsProjectObj.name}`;
// eslint-disable-next-line no-shadow
// importsAnalyzerResult.queryOutput.forEach(({ entries, project }) => {
const importProject = importsAnalyzerResult.analyzerMeta.targetProject.name;
importsAnalyzerResult.queryOutput.forEach(({ result, file }) =>
result.forEach(importEntryResult => {
/**
* @example
* Example context (read by 'find-imports'/'find-exports' analyzers)
* - export (/folder/exporting-file.js):
* `export const x = 'foo'`
* - import (target-project-a/importing-file.js):
* `import { x, y } from '@reference-repo/folder/exporting-file.js'`
* Example variables (extracted by 'find-imports'/'find-exports' analyzers)
* - exportSpecifier: 'x'
* - importSpecifiers: ['x', 'y']
*/
const hasExportSpecifierImported =
// ['x', 'y'].includes('x')
importEntryResult.importSpecifiers.includes(exportSpecifier) ||
importEntryResult.importSpecifiers.includes('[*]');
if (!hasExportSpecifierImported) {
return;
}
/**
* @example
* exportFile './foo.js'
* => export const z = 'bar'
* importFile 'importing-target-project/file.js'
* => import { z } from '@reference/foo.js'
*/
const fromImportToExport = fromImportToExportPerspective({
requestedExternalSource: importEntryResult.normalizedSource,
externalProjectMeta: exportsProjectObj,
externalRootPath: cfg.referenceProjectResult ? null : cfg.referenceProjectPath,
});
const isFromSameSource = compareImportAndExportPaths(
exportEntry.file,
fromImportToExport,
);
if (!isFromSameSource) {
return;
}
// TODO: transitive deps recognition? Could also be distinct post processor
filteredImportsList.add(`${importProject}::${file}`);
}),
);
storeResult(resultsObj, exportId, filteredImportsList, exportEntry.meta);
/**
* 2. Are we from the same source?
* A.k.a. is source required by target the same as the one found in target.
* (we know the specifier name is tha same, now we need to check the file as well.)
*
* Example:
* exportFile './foo.js'
* => export const z = 'bar'
* importFile 'importing-target-project/file.js'
* => import { z } from '@reference/foo.js'
* @type {PathRelativeFromRoot}
*/
const fromImportToExport = await fromImportToExportPerspective({
importee: importEntry.normalizedSource,
importer: pathLib.resolve(importProjectPath, importEntry.file),
});
});
});
const isFromSameSource = compareImportAndExportPaths(exportEntry.file, fromImportToExport);
if (!isFromSameSource) {
continue;
}
/**
* Step 2: a rich data structure
* @desc Transform resultObj from step 1 into an array of objects
* @example
* [{
* exportSpecifier: {
* // name under which it is registered in npm ("name" attr in package.json)
* name: 'RefClass',
* project: 'exporting-ref-project',
* filePath: './ref-src/core.js',
* id: 'RefClass::ref-src/core.js::exporting-ref-project',
* meta: {...},
*
* // most likely via post processor
* },
* // All the matched targets (files importing the specifier), ordered per project
* matchesPerProject: [
* {
* project: 'importing-target-project',
* files: [
* './target-src/indirect-imports.js',
* ...
* ],
* },
* ...
* ],
* }]
*/
const resultsArray = Object.entries(resultsObj)
.map(([id, flatResult]) => {
const [exportSpecifierName, filePath, project] = id.split('::');
const { meta } = flatResult;
/**
* 3. When above checks pass, we have a match.
* Add it to the results array
*/
const id = `${exportEntry.specifier}::${exportEntry.file}::${exportsAnalyzerResult.analyzerMeta.targetProject.name}`;
const resultForCurrentExport = conciseResultsArray.find(entry => entry.id === id);
if (resultForCurrentExport) {
resultForCurrentExport.importProjectFiles.push(importEntry.file);
} else {
conciseResultsArray.push({
exportSpecifier: { id, ...(exportEntry.meta ? { meta: exportEntry.meta } : {}) },
importProjectFiles: [importEntry.file],
});
}
}
}
const exportSpecifier = {
name: exportSpecifierName,
project,
filePath,
id,
...(meta || {}),
};
const matchesPerProject = [];
flatResult.files.forEach(projectFile => {
// eslint-disable-next-line no-shadow
const [project, file] = projectFile.split('::');
let projectEntry = matchesPerProject.find(m => m.project === project);
if (!projectEntry) {
matchesPerProject.push({ project, files: [] });
projectEntry = matchesPerProject[matchesPerProject.length - 1];
}
projectEntry.files.push(file);
});
return {
exportSpecifier,
matchesPerProject,
};
})
.filter(r => Object.keys(r.matchesPerProject).length);
return /** @type {AnalyzerResult} */ resultsArray;
const importProject = importsAnalyzerResult.analyzerMeta.targetProject.name;
return /** @type {AnalyzerResult} */ createCompatibleMatchImportsResult(
conciseResultsArray,
importProject,
);
}
class MatchImportsAnalyzer extends Analyzer {
@ -236,6 +293,7 @@ class MatchImportsAnalyzer extends Analyzer {
* Prepare
*/
const analyzerResult = this._prepare(cfg);
if (analyzerResult) {
return analyzerResult;
}
@ -263,7 +321,11 @@ class MatchImportsAnalyzer extends Analyzer {
});
}
const queryOutput = matchImportsPostprocess(referenceProjectResult, targetProjectResult, cfg);
const queryOutput = await matchImportsPostprocess(
referenceProjectResult,
targetProjectResult,
cfg,
);
/**
* Finalize

View file

@ -1,3 +1,5 @@
/* eslint-disable no-continue */
const pathLib = require('path');
/* eslint-disable no-shadow, no-param-reassign */
const FindClassesAnalyzer = require('./find-classes.js');
const FindExportsAnalyzer = require('./find-exports.js');
@ -63,12 +65,13 @@ function storeResult(resultsObj, exportId, filteredList, meta) {
* @param {MatchSubclassesConfig} customConfig
* @returns {AnalyzerResult}
*/
function matchSubclassesPostprocess(
async function matchSubclassesPostprocess(
exportsAnalyzerResult,
targetClassesAnalyzerResult,
refClassesAResult,
customConfig,
) {
// eslint-disable-next-line no-unused-vars
const cfg = {
...customConfig,
};
@ -91,17 +94,17 @@ function matchSubclassesPostprocess(
*/
const resultsObj = {};
exportsAnalyzerResult.queryOutput.forEach(exportEntry => {
for (const exportEntry of exportsAnalyzerResult.queryOutput) {
const exportsProjectObj = exportsAnalyzerResult.analyzerMeta.targetProject;
const exportsProjectName = exportsProjectObj.name;
// Look for all specifiers that are exported, like [import {specifier} 'lion-based-ui/foo.js']
exportEntry.result.forEach(exportEntryResult => {
for (const exportEntryResult of exportEntry.result) {
if (!exportEntryResult.exportSpecifiers) {
return;
continue;
}
exportEntryResult.exportSpecifiers.forEach(exportSpecifier => {
for (const exportSpecifier of exportEntryResult.exportSpecifiers) {
// Get all unique imports (name::source::project combinations) that match current
// exportSpecifier
const filteredImportsList = new Set();
@ -109,8 +112,13 @@ function matchSubclassesPostprocess(
// eslint-disable-next-line no-shadow
const importProject = targetClassesAnalyzerResult.analyzerMeta.targetProject.name;
targetClassesAnalyzerResult.queryOutput.forEach(({ result, file }) =>
result.forEach(classEntryResult => {
// TODO: What if this info is retrieved from cached importProject/target project?
const importProjectPath = cfg.targetProjectPath;
for (const { result, file } of targetClassesAnalyzerResult.queryOutput) {
// targetClassesAnalyzerResult.queryOutput.forEach(({ result, file }) =>
for (const classEntryResult of result) {
// result.forEach(classEntryResult => {
/**
* @example
* Example context (read by 'find-classes'/'find-exports' analyzers)
@ -133,7 +141,7 @@ function matchSubclassesPostprocess(
);
if (!classMatch) {
return;
continue;
}
/**
@ -147,11 +155,10 @@ function matchSubclassesPostprocess(
*/
const isFromSameSource =
exportEntry.file ===
fromImportToExportPerspective({
requestedExternalSource: classMatch.rootFile.file,
externalProjectMeta: exportsProjectObj,
externalRootPath: cfg.referenceProjectPath,
});
(await fromImportToExportPerspective({
importee: classMatch.rootFile.file,
importer: pathLib.resolve(importProjectPath, file),
}));
if (classMatch && isFromSameSource) {
const memberOverrides = getMemberOverrides(
@ -166,12 +173,12 @@ function matchSubclassesPostprocess(
memberOverrides,
});
}
}),
);
}
}
storeResult(resultsObj, exportId, filteredImportsList, exportEntry.meta);
});
});
});
}
}
}
/**
* Step 2: a rich data structure
@ -313,7 +320,7 @@ class MatchSubclassesAnalyzer extends Analyzer {
skipCheckMatchCompatibility: cfg.skipCheckMatchCompatibility,
});
const queryOutput = matchSubclassesPostprocess(
const queryOutput = await matchSubclassesPostprocess(
exportsAnalyzerResult,
targetClassesAnalyzerResult,
refClassesAnalyzerResult,

View file

@ -4,29 +4,29 @@
*/
const pathLib = require('path');
const nodeResolvePackageJson = require('@rollup/plugin-node-resolve/package.json');
const createRollupResolve = require('@rollup/plugin-node-resolve');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const { LogService } = require('../services/LogService.js');
const fakePluginContext = {
meta: {
rollupVersion: nodeResolvePackageJson.peerDependencies.rollup,
rollupVersion: '^2.42.0', // nodeResolvePackageJson.peerDependencies.rollup,
},
resolve: () => {},
warn(...msg) {
LogService.warn('[resolve-import-path]: ', ...msg);
},
};
/**
* @desc based on importee (in a statement "import {x} from '@lion/core'", "@lion/core" is an
* importee), which can be a bare module specifier, a filename without extension, or a folder
* Based on importee (in a statement "import {x} from '@lion/core'", "@lion/core" is an
* importee), which can be a bare module specifier, a filename without extension, or a folder
* name without an extension.
* @param {string} importee source like '@lion/core'
* @param {string} importer importing file, like '/my/project/importing-file.js'
* @returns {string} the resolved file system path, like '/my/project/node_modules/@lion/core/index.js'
*/
async function resolveImportPath(importee, importer, opts = {}) {
const rollupResolve = createRollupResolve({
const rollupResolve = nodeResolve({
rootDir: pathLib.dirname(importer),
// allow resolving polyfills for nodejs libs
preferBuiltins: false,
@ -38,7 +38,7 @@ async function resolveImportPath(importee, importer, opts = {}) {
(opts && opts.customResolveOptions && opts.customResolveOptions.preserveSymlinks) || false;
rollupResolve.buildStart.call(fakePluginContext, { preserveSymlinks });
const result = await rollupResolve.resolveId.call(fakePluginContext, importee, importer);
const result = await rollupResolve.resolveId.call(fakePluginContext, importee, importer, {});
if (!result || !result.id) {
// throw new Error(`importee ${importee} not found in filesystem.`);
LogService.warn(`importee ${importee} not found in filesystem for importer '${importer}'.`);

View file

@ -1,9 +1,27 @@
const path = require('path');
// eslint-disable-next-line import/no-extraneous-dependencies
const mockFs = require('mock-fs');
const path = require('path');
const mockRequire = require('mock-require');
function mock(obj) {
mockFs(obj);
Object.entries(obj).forEach(([key, value]) => {
if (key.endsWith('.json')) {
mockRequire(key, JSON.parse(value));
} else {
mockRequire(key, value);
}
});
}
mock.restore = () => {
mockFs.restore();
mockRequire.stopAll();
};
/**
* @desc Makes sure that, whenever the main program (providence) calls
* Makes sure that, whenever the main program (providence) calls
* "InputDataService.createDataObject", it gives back a mocked response.
* @param {string[]|object} files all the code that will be run trhough AST
* @param {object} [cfg]
@ -13,7 +31,7 @@ const path = require('path');
* paths match with the indexes of the files
* @param {object} existingMock config for mock-fs, so the previous config is not overridden
*/
function mockProject(files, cfg = {}, existingMock = {}) {
function getMockObjectForProject(files, cfg = {}, existingMock = {}) {
const projName = cfg.projectName || 'fictional-project';
const projPath = cfg.projectPath || '/fictional/project';
@ -50,17 +68,32 @@ function mockProject(files, cfg = {}, existingMock = {}) {
}
const totalMock = {
...existingMock, // can only add to mock-fs, not expand existing config?
...optionalPackageJson,
...existingMock, // can only add to mock-fs, not expand existing config?
...createFilesObjForFolder(files),
};
mockFs(totalMock);
return totalMock;
}
/**
* Makes sure that, whenever the main program (providence) calls
* "InputDataService.createDataObject", it gives back a mocked response.
* @param {string[]|object} files all the code that will be run trhough AST
* @param {object} [cfg]
* @param {string} [cfg.projectName='fictional-project']
* @param {string} [cfg.projectPath='/fictional/project']
* @param {string[]} [cfg.filePaths=`[/fictional/project/test-file-${i}.js]`] The indexes of the file
* paths match with the indexes of the files
* @param {object} existingMock config for mock-fs, so the previous config is not overridden
*/
function mockProject(files, cfg = {}, existingMock = {}) {
const obj = getMockObjectForProject(files, cfg, existingMock);
mockFs(obj);
return obj;
}
function restoreMockedProjects() {
mockFs.restore();
mock.restore();
}
function getEntry(queryResult, index = 0) {
@ -71,6 +104,23 @@ function getEntries(queryResult) {
return queryResult.queryOutput;
}
function createPackageJson({ filePaths, codeSnippets, projectName, refProjectName, refVersion }) {
const targetHasPackageJson = filePaths.includes('./package.json');
// Make target depend on ref
if (targetHasPackageJson) {
return;
}
const pkgJson = {
name: projectName,
version: '1.0.0',
};
if (refProjectName && refVersion) {
pkgJson.dependencies = { [refProjectName]: refVersion };
}
codeSnippets.push(JSON.stringify(pkgJson));
filePaths.push('./package.json');
}
/**
* Requires two config objects (see match-imports and match-subclasses tests)
* and based on those, will use mock-fs package to mock them in the file system.
@ -86,22 +136,25 @@ function mockTargetAndReferenceProject(searchTargetProject, referenceProject) {
const targetcodeSnippets = searchTargetProject.files.map(f => f.code);
const targetFilePaths = searchTargetProject.files.map(f => f.file);
const refVersion = referenceProject.version || '1.0.0';
const refcodeSnippets = referenceProject.files.map(f => f.code);
const refFilePaths = referenceProject.files.map(f => f.file);
const targetHasPackageJson = targetFilePaths.includes('./package.json');
// Make target depend on ref
if (!targetHasPackageJson) {
targetcodeSnippets.push(`{
"name": "${targetProjectName}" ,
"version": "1.0.0",
"dependencies": {
"${refProjectName}": "${refVersion}"
}
}`);
targetFilePaths.push('./package.json');
}
createPackageJson({
filePaths: targetFilePaths,
codeSnippets: targetcodeSnippets,
projectName: targetProjectName,
refProjectName,
refVersion,
});
createPackageJson({
filePaths: refFilePaths,
codeSnippets: refcodeSnippets,
projectName: refProjectName,
});
// Create target mock
const targetMock = mockProject(targetcodeSnippets, {
const targetMock = getMockObjectForProject(targetcodeSnippets, {
filePaths: targetFilePaths,
projectName: targetProjectName,
projectPath: searchTargetProject.path || 'fictional/target/project',

View file

@ -568,7 +568,7 @@ describe('CLI helpers', () => {
};
const theirProject = {
path: '/their-components',
path: '/my-components/node_modules/their-components',
name: 'their-components',
files: Object.entries(theirProjectFiles).map(([file, code]) => ({ file, code })),
};
@ -582,7 +582,7 @@ describe('CLI helpers', () => {
mockTargetAndReferenceProject(theirProject, myProject);
const result = await getExtendDocsResults({
referenceProjectPaths: ['/their-components'],
referenceProjectPaths: [theirProject.path],
prefixCfg: { from: 'their', to: 'my' },
extensions: ['.js'],
cwd: '/my-components',

View file

@ -21,12 +21,12 @@ const {
const matchImportsQueryConfig = QueryService.getQueryConfigFromAnalyzer('match-imports');
const _providenceCfg = {
targetProjectPaths: ['/importing/target/project'],
referenceProjectPaths: ['/exporting/ref/project'],
referenceProjectPaths: ['/importing/target/project/node_modules/exporting-ref-project'],
};
// 1. Reference input data
const referenceProject = {
path: '/exporting/ref/project',
path: '/importing/target/project/node_modules/exporting-ref-project',
name: 'exporting-ref-project',
files: [
// This file contains all 'original' exported definitions
@ -253,58 +253,206 @@ describe('Analyzer "match-imports"', () => {
}
describe('Extracting exports', () => {
it(`identifies all direct export specifiers consumed by "importing-target-project"`, async () => {
mockTargetAndReferenceProject(searchTargetProject, referenceProject);
await providence(matchImportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
expectedExportIdsDirect.forEach(directId => {
expect(
queryResult.queryOutput.find(
exportMatchResult => exportMatchResult.exportSpecifier.id === directId,
),
).not.to.equal(undefined, `id '${directId}' not found`);
it(`identifies all direct export specifiers consumed by target`, async () => {
const refProject = {
path: '/target/node_modules/ref',
name: 'ref',
files: [{ file: './direct.js', code: `export default function x() {};` }],
};
const targetProject = {
path: '/target',
name: 'target',
files: [{ file: './index.js', code: `import myFn from 'ref/direct.js';` }],
};
mockTargetAndReferenceProject(targetProject, refProject);
await providence(matchImportsQueryConfig, {
targetProjectPaths: [targetProject.path],
referenceProjectPaths: [refProject.path],
});
const queryResult = queryResults[0];
expect(queryResult.queryOutput).eql([
{
exportSpecifier: {
filePath: './direct.js',
id: '[default]::./direct.js::ref',
name: '[default]',
project: 'ref',
},
matchesPerProject: [{ files: ['./index.js'], project: 'target' }],
},
]);
});
it(`identifies all indirect export specifiers consumed by "importing-target-project"`, async () => {
mockTargetAndReferenceProject(searchTargetProject, referenceProject);
await providence(matchImportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
expectedExportIdsIndirect.forEach(indirectId => {
expect(
queryResult.queryOutput.find(
exportMatchResult => exportMatchResult.exportSpecifier.id === indirectId,
),
).not.to.equal(undefined, `id '${indirectId}' not found`);
it(`identifies all indirect (transitive) export specifiers consumed by target`, async () => {
const refProject = {
path: '/target/node_modules/ref',
name: 'ref',
files: [
{ file: './direct.js', code: `export function x() {};` },
{ file: './indirect.js', code: `export { x } from './direct.js';` },
],
};
const targetProject = {
path: '/target',
name: 'target',
files: [{ file: './index.js', code: `import { x } from 'ref/indirect.js';` }],
};
mockTargetAndReferenceProject(targetProject, refProject);
await providence(matchImportsQueryConfig, {
targetProjectPaths: [targetProject.path],
referenceProjectPaths: [refProject.path],
});
const queryResult = queryResults[0];
expect(queryResult.queryOutput).eql([
{
exportSpecifier: {
filePath: './indirect.js',
id: 'x::./indirect.js::ref',
name: 'x',
project: 'ref',
},
matchesPerProject: [{ files: ['./index.js'], project: 'target' }],
},
]);
});
it(`matches namespaced specifiers consumed by "importing-target-project"`, async () => {
mockTargetAndReferenceProject(searchTargetProject, referenceProject);
await providence(matchImportsQueryConfig, _providenceCfg);
it(`matches namespaced specifiers consumed by target`, async () => {
const refProject = {
path: '/target/node_modules/ref',
name: 'ref',
files: [
{ file: './namespaced.js', code: `export function x() {}; export function y() {};` },
],
};
const targetProject = {
path: '/target',
name: 'target',
files: [{ file: './index.js', code: `import * as xy from 'ref/namespaced.js';` }],
};
mockTargetAndReferenceProject(targetProject, refProject);
await providence(matchImportsQueryConfig, {
targetProjectPaths: [targetProject.path],
referenceProjectPaths: [refProject.path],
});
const queryResult = queryResults[0];
expectedExportIdsNamespaced.forEach(exportedSpecifierId => {
expect(
queryResult.queryOutput.find(
exportMatchResult => exportMatchResult.exportSpecifier.id === exportedSpecifierId,
),
).not.to.equal(undefined, `id '${exportedSpecifierId}' not found`);
expect(queryResult.queryOutput).eql([
{
exportSpecifier: {
filePath: './namespaced.js',
id: 'x::./namespaced.js::ref',
name: 'x',
project: 'ref',
},
matchesPerProject: [{ files: ['./index.js'], project: 'target' }],
},
{
exportSpecifier: {
filePath: './namespaced.js',
id: 'y::./namespaced.js::ref',
name: 'y',
project: 'ref',
},
matchesPerProject: [{ files: ['./index.js'], project: 'target' }],
},
{
exportSpecifier: {
filePath: './namespaced.js',
id: '[file]::./namespaced.js::ref',
name: '[file]',
project: 'ref',
},
matchesPerProject: [{ files: ['./index.js'], project: 'target' }],
},
]);
});
describe('Inside small example project', () => {
it(`identifies all direct export specifiers consumed by "importing-target-project"`, async () => {
mockTargetAndReferenceProject(searchTargetProject, referenceProject);
await providence(matchImportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
// console.log(JSON.stringify(queryResult.queryOutput, null, 2));
expectedExportIdsDirect.forEach(directId => {
expect(
queryResult.queryOutput.find(
exportMatchResult => exportMatchResult.exportSpecifier.id === directId,
),
).not.to.equal(undefined, `id '${directId}' not found`);
});
});
it(`identifies all indirect export specifiers consumed by "importing-target-project"`, async () => {
mockTargetAndReferenceProject(searchTargetProject, referenceProject);
await providence(matchImportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
expectedExportIdsIndirect.forEach(indirectId => {
expect(
queryResult.queryOutput.find(
exportMatchResult => exportMatchResult.exportSpecifier.id === indirectId,
),
).not.to.equal(undefined, `id '${indirectId}' not found`);
});
});
it(`matches namespaced specifiers consumed by "importing-target-project"`, async () => {
mockTargetAndReferenceProject(searchTargetProject, referenceProject);
await providence(matchImportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
expectedExportIdsNamespaced.forEach(exportedSpecifierId => {
expect(
queryResult.queryOutput.find(
exportMatchResult => exportMatchResult.exportSpecifier.id === exportedSpecifierId,
),
).not.to.equal(undefined, `id '${exportedSpecifierId}' not found`);
});
});
});
});
describe('Matching', () => {
it(`produces a list of all matches, sorted by project`, async () => {
mockTargetAndReferenceProject(searchTargetProject, referenceProject);
await providence(matchImportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
expectedExportIdsDirect.forEach(targetId => {
testMatchedEntry(targetId, queryResult, ['./target-src/direct-imports.js']);
/**
* N.B. output structure could be simplified, since there is
* For now, we keep it, so integration with dashboard stays intact.
* TODO:
* - write tests for dashboard transform logic
* - simplify output for match-* analyzers
* - adjust dashboard transfrom logic
*/
const refProject = {
path: '/target/node_modules/ref',
name: 'ref',
files: [{ file: './direct.js', code: `export default function x() {};` }],
};
const targetProject = {
path: '/target',
name: 'target',
files: [{ file: './index.js', code: `import myFn from 'ref/direct.js';` }],
};
mockTargetAndReferenceProject(targetProject, refProject);
await providence(matchImportsQueryConfig, {
targetProjectPaths: [targetProject.path],
referenceProjectPaths: [refProject.path],
});
const queryResult = queryResults[0];
expect(queryResult.queryOutput[0].matchesPerProject).eql([
{ files: ['./index.js'], project: 'target' },
]);
});
expectedExportIdsIndirect.forEach(targetId => {
testMatchedEntry(targetId, queryResult, ['./target-src/indirect-imports.js']);
describe('Inside small example project', () => {
it(`produces a list of all matches, sorted by project`, async () => {
mockTargetAndReferenceProject(searchTargetProject, referenceProject);
await providence(matchImportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
expectedExportIdsDirect.forEach(targetId => {
testMatchedEntry(targetId, queryResult, ['./target-src/direct-imports.js']);
});
expectedExportIdsIndirect.forEach(targetId => {
testMatchedEntry(targetId, queryResult, ['./target-src/indirect-imports.js']);
});
});
});
});

View file

@ -15,12 +15,6 @@ const {
restoreSuppressNonCriticalLogs,
} = require('../../../test-helpers/mock-log-service-helpers.js');
const matchPathsQueryConfig = QueryService.getQueryConfigFromAnalyzer('match-paths');
const _providenceCfg = {
targetProjectPaths: ['/importing/target/project'],
referenceProjectPaths: ['/exporting/ref/project'],
};
describe('Analyzer "match-paths"', () => {
const originalReferenceProjectPaths = InputDataService.referenceProjectPaths;
const queryResults = [];
@ -48,8 +42,8 @@ describe('Analyzer "match-paths"', () => {
});
const referenceProject = {
path: '/exporting/ref/project',
name: 'exporting-ref-project',
path: '/importing/target/project/node_modules/reference-project',
name: 'reference-project',
files: [
{
file: './ref-src/core.js',
@ -90,8 +84,8 @@ describe('Analyzer "match-paths"', () => {
file: './target-src/ExtendRefRenamedClass.js',
code: `
// renamed import (indirect, needs transitivity check)
import { RefRenamedClass } from 'exporting-ref-project/reexport.js';
import defaultExport from 'exporting-ref-project/reexport.js';
import { RefRenamedClass } from 'reference-project/reexport.js';
import defaultExport from 'reference-project/reexport.js';
/**
* This should result in:
@ -110,10 +104,10 @@ describe('Analyzer "match-paths"', () => {
file: './target-src/direct-imports.js',
code: `
// a direct named import
import { RefClass } from 'exporting-ref-project/ref-src/core.js';
import { RefClass } from 'reference-project/ref-src/core.js';
// a direct default import
import RefDefault from 'exporting-ref-project/reexport.js';
import RefDefault from 'reference-project/reexport.js';
/**
* This should result in:
@ -148,6 +142,12 @@ describe('Analyzer "match-paths"', () => {
],
};
const matchPathsQueryConfig = QueryService.getQueryConfigFromAnalyzer('match-paths');
const _providenceCfg = {
targetProjectPaths: [searchTargetProject.path],
referenceProjectPaths: [referenceProject.path],
};
describe('Variables', () => {
const expectedMatches = [
{
@ -161,7 +161,7 @@ describe('Analyzer "match-paths"', () => {
to: './target-src/ExtendRefRenamedClass.js',
},
{
from: 'exporting-ref-project/reexport.js',
from: 'reference-project/reexport.js',
to: './target-src/ExtendRefRenamedClass.js',
},
],
@ -182,11 +182,11 @@ describe('Analyzer "match-paths"', () => {
to: './index.js',
},
{
from: 'exporting-ref-project/reexport.js',
from: 'reference-project/reexport.js',
to: './index.js',
},
{
from: 'exporting-ref-project/ref-src/core.js',
from: 'reference-project/ref-src/core.js',
to: './index.js',
},
],
@ -203,7 +203,7 @@ describe('Analyzer "match-paths"', () => {
to: './target-src/direct-imports.js',
},
{
from: 'exporting-ref-project/ref-src/core.js',
from: 'reference-project/ref-src/core.js',
to: './target-src/direct-imports.js',
},
],
@ -220,7 +220,7 @@ describe('Analyzer "match-paths"', () => {
describe('Features', () => {
const refProj = {
path: '/exporting/ref/project',
path: '/importing/target/project/node_modules/reference-project',
name: 'reference-project',
files: [
{
@ -376,7 +376,7 @@ describe('Analyzer "match-paths"', () => {
describe('Options', () => {
const refProj = {
path: '/exporting/ref/project',
path: '/importing/target/project/node_modules/reference-project',
name: 'reference-project',
files: [
{
@ -446,8 +446,8 @@ describe('Analyzer "match-paths"', () => {
describe('Tags', () => {
// eslint-disable-next-line no-shadow
const referenceProject = {
path: '/exporting/ref/project',
name: 'exporting-ref-project',
path: '/importing/target/project/node_modules/reference-project',
name: 'reference-project',
files: [
{
file: './customelementDefinitions.js',
@ -493,7 +493,7 @@ describe('Analyzer "match-paths"', () => {
{
file: './extendedClassDefinitions.js',
code: `
export { El1, El2 } from 'exporting-ref-project/classDefinitions.js';
export { El1, El2 } from 'reference-project/classDefinitions.js';
export class ExtendedEl1 extends El1 {}
`,
@ -517,7 +517,7 @@ describe('Analyzer "match-paths"', () => {
paths: [
{ from: './customelementDefinitions.js', to: './extendedCustomelementDefinitions.js' },
{
from: 'exporting-ref-project/customelementDefinitions.js',
from: 'reference-project/customelementDefinitions.js',
to: './extendedCustomelementDefinitions.js',
},
],
@ -528,7 +528,7 @@ describe('Analyzer "match-paths"', () => {
paths: [
{ from: './customelementDefinitions.js', to: './extendedCustomelementDefinitions.js' },
{
from: 'exporting-ref-project/customelementDefinitions.js',
from: 'reference-project/customelementDefinitions.js',
to: './extendedCustomelementDefinitions.js',
},
],
@ -642,7 +642,7 @@ describe('Analyzer "match-paths"', () => {
await providence(matchPathsQueryConfig, _providenceCfg);
const queryResult = queryResults[0];
expect(queryResult.queryOutput[0].tag.paths[1]).to.eql({
from: 'exporting-ref-project/customelementDefinitions.js',
from: 'reference-project/customelementDefinitions.js',
to: './extendedCustomelementDefinitions.js',
});
});
@ -692,7 +692,7 @@ describe('Analyzer "match-paths"', () => {
to: './target-src/ExtendRefRenamedClass.js',
},
{
from: 'exporting-ref-project/reexport.js',
from: 'reference-project/reexport.js',
to: './target-src/ExtendRefRenamedClass.js',
},
],
@ -713,11 +713,11 @@ describe('Analyzer "match-paths"', () => {
to: './index.js',
},
{
from: 'exporting-ref-project/reexport.js',
from: 'reference-project/reexport.js',
to: './index.js',
},
{
from: 'exporting-ref-project/ref-src/core.js',
from: 'reference-project/ref-src/core.js',
to: './index.js',
},
],
@ -734,7 +734,7 @@ describe('Analyzer "match-paths"', () => {
to: './target-src/direct-imports.js',
},
{
from: 'exporting-ref-project/ref-src/core.js',
from: 'reference-project/ref-src/core.js',
to: './target-src/direct-imports.js',
},
],
@ -748,7 +748,7 @@ describe('Analyzer "match-paths"', () => {
to: './tag-extended.js',
},
{
from: 'exporting-ref-project/tag.js',
from: 'reference-project/tag.js',
to: './tag-extended.js',
},
],

View file

@ -15,15 +15,9 @@ const {
restoreSuppressNonCriticalLogs,
} = require('../../../test-helpers/mock-log-service-helpers.js');
const matchSubclassesQueryConfig = QueryService.getQueryConfigFromAnalyzer('match-subclasses');
const _providenceCfg = {
targetProjectPaths: ['/importing/target/project'],
referenceProjectPaths: ['/exporting/ref/project'],
};
// 1. Reference input data
const referenceProject = {
path: '/exporting/ref/project',
path: '/importing/target/project/node_modules/exporting-ref-project',
name: 'exporting-ref-project',
files: [
// This file contains all 'original' exported definitions
@ -92,6 +86,12 @@ const searchTargetProject = {
],
};
const matchSubclassesQueryConfig = QueryService.getQueryConfigFromAnalyzer('match-subclasses');
const _providenceCfg = {
targetProjectPaths: [searchTargetProject.path],
referenceProjectPaths: [referenceProject.path],
};
// 2. Extracted specifiers (by find-exports analyzer)
const expectedExportIdsIndirect = ['RefRenamedClass::./index.js::exporting-ref-project'];

View file

@ -0,0 +1,115 @@
const { expect } = require('chai');
const {
mockProject,
restoreMockedProjects,
mockTargetAndReferenceProject,
} = require('../../../test-helpers/mock-project-helpers.js');
const { resolveImportPath } = require('../../../src/program/utils/resolve-import-path.js');
describe('resolveImportPath', () => {
afterEach(() => {
restoreMockedProjects();
});
it(`resolves file in same project`, async () => {
mockProject(
{
'./src/declarationOfMyClass.js': `
export class MyClass extends HTMLElement {}
`,
'./currentFile.js': `
import { MyClass } from './src/declarationOfMyClass';
`,
},
{
projectName: 'my-project',
projectPath: '/my/project',
},
);
const foundPath = await resolveImportPath(
'./src/declarationOfMyClass',
'/my/project/currentFile.js',
);
expect(foundPath).to.equal('/my/project/src/declarationOfMyClass.js');
});
it(`resolves file in different projects`, async () => {
const targetProject = {
path: '/target/node_modules/ref',
name: 'ref',
files: [
{
file: './index.js',
code: `
export const x = 10;
`,
},
],
};
const referenceProject = {
path: '/target',
name: 'target',
files: [
// This file contains all 'original' exported definitions
{
file: './a.js',
code: `
import { x } from 'ref';
`,
},
],
};
mockTargetAndReferenceProject(targetProject, referenceProject);
const foundPath = await resolveImportPath('ref', '/target/a.js');
expect(foundPath).to.equal('/target/node_modules/ref/index.js');
});
it(`resolves export maps`, async () => {
const targetProject = {
path: '/target/node_modules/ref',
name: 'ref',
files: [
{
file: './packages/x/index.js',
code: `
export const x = 10;
`,
},
{
file: './package.json',
code: JSON.stringify({
name: 'ref',
exports: {
'./x': './packages/x/index.js',
},
}),
},
],
};
const referenceProject = {
path: '/target',
name: 'target',
files: [
// This file contains all 'original' exported definitions
{
file: './a.js',
code: `
import { x } from 'ref/x';
`,
},
],
};
mockTargetAndReferenceProject(targetProject, referenceProject);
const foundPath = await resolveImportPath('ref/x', '/target/a.js');
expect(foundPath).to.equal('/target/node_modules/ref/packages/x/index.js');
});
/**
* All edge cases are covered by https://github.com/rollup/plugins/tree/master/packages/node-resolve/test
*/
});