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/register": "^7.5.5",
"@babel/traverse": "^7.5.5", "@babel/traverse": "^7.5.5",
"@babel/types": "^7.9.0", "@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", "@typescript-eslint/typescript-estree": "^3.0.0",
"anymatch": "^3.1.1", "anymatch": "^3.1.1",
"chalk": "^4.1.0", "chalk": "^4.1.0",
@ -48,6 +48,7 @@
"inquirer": "^7.0.0", "inquirer": "^7.0.0",
"is-negated-glob": "^1.0.0", "is-negated-glob": "^1.0.0",
"lit-element": "~2.4.0", "lit-element": "~2.4.0",
"mock-require": "^3.0.3",
"ora": "^3.4.0", "ora": "^3.4.0",
"parse5": "^5.1.1", "parse5": "^5.1.1",
"read-package-tree": "5.3.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 { isRelativeSourcePath } = require('../../utils/relative-source-path.js');
const { LogService } = require('../../services/LogService.js'); const { LogService } = require('../../services/LogService.js');
const { resolveImportPath } = require('../../utils/resolve-import-path.js');
/** /**
* TODO: Use utils/resolve-import-path for 100% accuracy * @param {string} importee like '@lion/core/myFile.js'
* * @returns {string} project name ('@lion/core')
* - 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}
*/ */
function fromImportToExportPerspective({ function getProjectFromImportee(importee) {
requestedExternalSource, const scopedProject = importee[0] === '@';
externalProjectMeta,
externalRootPath,
}) {
if (isRelativeSourcePath(requestedExternalSource)) {
LogService.warn('[fromImportToExportPerspective] Please only provide external import paths');
return null;
}
const scopedProject = requestedExternalSource[0] === '@';
// 'external-project/src/file.js' -> ['external-project', 'src', file.js'] // 'external-project/src/file.js' -> ['external-project', 'src', file.js']
let splitSource = requestedExternalSource.split('/'); let splitSource = importee.split('/');
if (scopedProject) { if (scopedProject) {
// '@external/project' // '@external/project'
splitSource = [splitSource.slice(0, 2).join('/'), ...splitSource.slice(2)]; splitSource = [splitSource.slice(0, 2).join('/'), ...splitSource.slice(2)];
} }
// ['external-project', 'src', 'file.js'] -> 'external-project' // ['external-project', 'src', 'file.js'] -> 'external-project'
const project = splitSource.slice(0, 1).join('/'); 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; return null;
} }
if (localPath) { const absolutePath = await resolveImportPath(importee, importer);
// like '@open-wc/x/y.js' const projectName = getProjectFromImportee(importee);
// 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}`);
if (fs.existsSync(pathToCheck)) { // from /my/reference/project/packages/foo/index.js to './packages/foo/index.js'
const stat = fs.statSync(pathToCheck); return absolutePath
if (stat && stat.isFile()) { ? absolutePath.replace(new RegExp(`^.*/${projectName}/?(.*)$`), './$1')
return `./${localPath}`; // '/path/to/lion-based-ui/fol.der' is a file : null;
}
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;
} }
module.exports = { fromImportToExportPerspective }; module.exports = { fromImportToExportPerspective };

View file

@ -1,3 +1,5 @@
/* eslint-disable no-continue */
const pathLib = require('path');
/* eslint-disable no-shadow, no-param-reassign */ /* eslint-disable no-shadow, no-param-reassign */
const FindImportsAnalyzer = require('./find-imports.js'); const FindImportsAnalyzer = require('./find-imports.js');
const FindExportsAnalyzer = require('./find-exports.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'); const { fromImportToExportPerspective } = require('./helpers/from-import-to-export-perspective.js');
/** /**
* @desc Helper method for matchImportsPostprocess. Modifies its resultsObj * @typedef {import('../types/find-imports').FindImportsAnalyzerResult} FindImportsAnalyzerResult
* @param {object} resultsObj * @typedef {import('../types/find-exports').FindExportsAnalyzerResult} FindExportsAnalyzerResult
* @param {string} exportId like 'myExport::./reference-project/my/export.js::my-project' * @typedef {import('../types/find-exports').IterableFindExportsAnalyzerEntry} IterableFindExportsAnalyzerEntry
* @param {Set<string>} filteredList * @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 * Needed in case fromImportToExportPerspective does not have a
@ -33,56 +29,168 @@ 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 {FindExportsAnalyzerResult} exportsAnalyzerResult
* @param {FindImportsAnalyzerResult} importsAnalyzerResult * @param {FindImportsAnalyzerResult} importsAnalyzerResult
* @param {matchImportsConfig} customConfig * @param {matchImportsConfig} customConfig
* @returns {AnalyzerResult} * @returns {Promise<AnalyzerResult>}
*/ */
function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerResult, customConfig) { async function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerResult, customConfig) {
const cfg = { const cfg = {
...customConfig, ...customConfig,
}; };
// TODO: What if this info is retrieved from cached importProject/target project?
const importProjectPath = cfg.targetProjectPath;
const iterableFindExportsOutput = transformIntoIterableFindExportsOutput(exportsAnalyzerResult);
const iterableFindImportsOutput = transformIntoIterableFindImportsOutput(importsAnalyzerResult);
/** @type {ConciseMatchImportsAnalyzerResult} */
const conciseResultsArray = [];
for (const exportEntry of iterableFindExportsOutput) {
for (const importEntry of iterableFindImportsOutput) {
/** /**
* Step 1: a 'flat' data structure * 1. Does target import ref specifier?
* @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 = {};
exportsAnalyzerResult.queryOutput.forEach(exportEntry => {
const exportsProjectObj = exportsAnalyzerResult.analyzerMeta.targetProject;
// Look for all specifiers that are exported, like [import {specifier} 'lion-based-ui/foo.js']
exportEntry.result.forEach(exportEntryResult => {
if (!exportEntryResult.exportSpecifiers) {
return;
}
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) * Example context (read by 'find-imports'/'find-exports' analyzers)
* - export (/folder/exporting-file.js): * - export (/folder/exporting-file.js):
* `export const x = 'foo'` * `export const x = 'foo'`
@ -91,108 +199,57 @@ function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerResult, c
* Example variables (extracted by 'find-imports'/'find-exports' analyzers) * Example variables (extracted by 'find-imports'/'find-exports' analyzers)
* - exportSpecifier: 'x' * - exportSpecifier: 'x'
* - importSpecifiers: ['x', 'y'] * - importSpecifiers: ['x', 'y']
* @type {boolean}
*/ */
const hasExportSpecifierImported = const hasExportSpecifierImported =
// ['x', 'y'].includes('x') exportEntry.specifier === importEntry.specifier || importEntry.specifier === '[*]';
importEntryResult.importSpecifiers.includes(exportSpecifier) ||
importEntryResult.importSpecifiers.includes('[*]');
if (!hasExportSpecifierImported) { if (!hasExportSpecifierImported) {
return; continue;
} }
/** /**
* @example * 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' * exportFile './foo.js'
* => export const z = 'bar' * => export const z = 'bar'
* importFile 'importing-target-project/file.js' * importFile 'importing-target-project/file.js'
* => import { z } from '@reference/foo.js' * => import { z } from '@reference/foo.js'
* @type {PathRelativeFromRoot}
*/ */
const fromImportToExport = fromImportToExportPerspective({ const fromImportToExport = await fromImportToExportPerspective({
requestedExternalSource: importEntryResult.normalizedSource, importee: importEntry.normalizedSource,
externalProjectMeta: exportsProjectObj, importer: pathLib.resolve(importProjectPath, importEntry.file),
externalRootPath: cfg.referenceProjectResult ? null : cfg.referenceProjectPath,
}); });
const isFromSameSource = compareImportAndExportPaths( const isFromSameSource = compareImportAndExportPaths(exportEntry.file, fromImportToExport);
exportEntry.file,
fromImportToExport,
);
if (!isFromSameSource) { if (!isFromSameSource) {
return; continue;
} }
// TODO: transitive deps recognition? Could also be distinct post processor
filteredImportsList.add(`${importProject}::${file}`);
}),
);
storeResult(resultsObj, exportId, filteredImportsList, exportEntry.meta);
});
});
});
/** /**
* Step 2: a rich data structure * 3. When above checks pass, we have a match.
* @desc Transform resultObj from step 1 into an array of objects * Add it to the results array
* @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) const id = `${exportEntry.specifier}::${exportEntry.file}::${exportsAnalyzerResult.analyzerMeta.targetProject.name}`;
.map(([id, flatResult]) => { const resultForCurrentExport = conciseResultsArray.find(entry => entry.id === id);
const [exportSpecifierName, filePath, project] = id.split('::'); if (resultForCurrentExport) {
const { meta } = flatResult; resultForCurrentExport.importProjectFiles.push(importEntry.file);
} else {
const exportSpecifier = { conciseResultsArray.push({
name: exportSpecifierName, exportSpecifier: { id, ...(exportEntry.meta ? { meta: exportEntry.meta } : {}) },
project, importProjectFiles: [importEntry.file],
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 { const importProject = importsAnalyzerResult.analyzerMeta.targetProject.name;
exportSpecifier, return /** @type {AnalyzerResult} */ createCompatibleMatchImportsResult(
matchesPerProject, conciseResultsArray,
}; importProject,
}) );
.filter(r => Object.keys(r.matchesPerProject).length);
return /** @type {AnalyzerResult} */ resultsArray;
} }
class MatchImportsAnalyzer extends Analyzer { class MatchImportsAnalyzer extends Analyzer {
@ -236,6 +293,7 @@ class MatchImportsAnalyzer extends Analyzer {
* Prepare * Prepare
*/ */
const analyzerResult = this._prepare(cfg); const analyzerResult = this._prepare(cfg);
if (analyzerResult) { if (analyzerResult) {
return 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 * Finalize

View file

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

View file

@ -4,21 +4,21 @@
*/ */
const pathLib = require('path'); const pathLib = require('path');
const nodeResolvePackageJson = require('@rollup/plugin-node-resolve/package.json'); const { nodeResolve } = require('@rollup/plugin-node-resolve');
const createRollupResolve = require('@rollup/plugin-node-resolve');
const { LogService } = require('../services/LogService.js'); const { LogService } = require('../services/LogService.js');
const fakePluginContext = { const fakePluginContext = {
meta: { meta: {
rollupVersion: nodeResolvePackageJson.peerDependencies.rollup, rollupVersion: '^2.42.0', // nodeResolvePackageJson.peerDependencies.rollup,
}, },
resolve: () => {},
warn(...msg) { warn(...msg) {
LogService.warn('[resolve-import-path]: ', ...msg); LogService.warn('[resolve-import-path]: ', ...msg);
}, },
}; };
/** /**
* @desc based on importee (in a statement "import {x} from '@lion/core'", "@lion/core" is an * 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 * importee), which can be a bare module specifier, a filename without extension, or a folder
* name without an extension. * name without an extension.
* @param {string} importee source like '@lion/core' * @param {string} importee source like '@lion/core'
@ -26,7 +26,7 @@ const fakePluginContext = {
* @returns {string} the resolved file system path, like '/my/project/node_modules/@lion/core/index.js' * @returns {string} the resolved file system path, like '/my/project/node_modules/@lion/core/index.js'
*/ */
async function resolveImportPath(importee, importer, opts = {}) { async function resolveImportPath(importee, importer, opts = {}) {
const rollupResolve = createRollupResolve({ const rollupResolve = nodeResolve({
rootDir: pathLib.dirname(importer), rootDir: pathLib.dirname(importer),
// allow resolving polyfills for nodejs libs // allow resolving polyfills for nodejs libs
preferBuiltins: false, preferBuiltins: false,
@ -38,7 +38,7 @@ async function resolveImportPath(importee, importer, opts = {}) {
(opts && opts.customResolveOptions && opts.customResolveOptions.preserveSymlinks) || false; (opts && opts.customResolveOptions && opts.customResolveOptions.preserveSymlinks) || false;
rollupResolve.buildStart.call(fakePluginContext, { preserveSymlinks }); 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) { if (!result || !result.id) {
// throw new Error(`importee ${importee} not found in filesystem.`); // throw new Error(`importee ${importee} not found in filesystem.`);
LogService.warn(`importee ${importee} not found in filesystem for importer '${importer}'.`); 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 // eslint-disable-next-line import/no-extraneous-dependencies
const mockFs = require('mock-fs'); 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. * "InputDataService.createDataObject", it gives back a mocked response.
* @param {string[]|object} files all the code that will be run trhough AST * @param {string[]|object} files all the code that will be run trhough AST
* @param {object} [cfg] * @param {object} [cfg]
@ -13,7 +31,7 @@ const path = require('path');
* paths match with the indexes of the files * paths match with the indexes of the files
* @param {object} existingMock config for mock-fs, so the previous config is not overridden * @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 projName = cfg.projectName || 'fictional-project';
const projPath = cfg.projectPath || '/fictional/project'; const projPath = cfg.projectPath || '/fictional/project';
@ -50,17 +68,32 @@ function mockProject(files, cfg = {}, existingMock = {}) {
} }
const totalMock = { const totalMock = {
...existingMock, // can only add to mock-fs, not expand existing config?
...optionalPackageJson, ...optionalPackageJson,
...existingMock, // can only add to mock-fs, not expand existing config?
...createFilesObjForFolder(files), ...createFilesObjForFolder(files),
}; };
mockFs(totalMock);
return 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() { function restoreMockedProjects() {
mockFs.restore(); mock.restore();
} }
function getEntry(queryResult, index = 0) { function getEntry(queryResult, index = 0) {
@ -71,6 +104,23 @@ function getEntries(queryResult) {
return queryResult.queryOutput; 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) * 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. * 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 targetcodeSnippets = searchTargetProject.files.map(f => f.code);
const targetFilePaths = searchTargetProject.files.map(f => f.file); const targetFilePaths = searchTargetProject.files.map(f => f.file);
const refVersion = referenceProject.version || '1.0.0'; 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'); createPackageJson({
// Make target depend on ref filePaths: targetFilePaths,
if (!targetHasPackageJson) { codeSnippets: targetcodeSnippets,
targetcodeSnippets.push(`{ projectName: targetProjectName,
"name": "${targetProjectName}" , refProjectName,
"version": "1.0.0", refVersion,
"dependencies": { });
"${refProjectName}": "${refVersion}"
} createPackageJson({
}`); filePaths: refFilePaths,
targetFilePaths.push('./package.json'); codeSnippets: refcodeSnippets,
} projectName: refProjectName,
});
// Create target mock // Create target mock
const targetMock = mockProject(targetcodeSnippets, { const targetMock = getMockObjectForProject(targetcodeSnippets, {
filePaths: targetFilePaths, filePaths: targetFilePaths,
projectName: targetProjectName, projectName: targetProjectName,
projectPath: searchTargetProject.path || 'fictional/target/project', projectPath: searchTargetProject.path || 'fictional/target/project',

View file

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

View file

@ -21,12 +21,12 @@ const {
const matchImportsQueryConfig = QueryService.getQueryConfigFromAnalyzer('match-imports'); const matchImportsQueryConfig = QueryService.getQueryConfigFromAnalyzer('match-imports');
const _providenceCfg = { const _providenceCfg = {
targetProjectPaths: ['/importing/target/project'], targetProjectPaths: ['/importing/target/project'],
referenceProjectPaths: ['/exporting/ref/project'], referenceProjectPaths: ['/importing/target/project/node_modules/exporting-ref-project'],
}; };
// 1. Reference input data // 1. Reference input data
const referenceProject = { const referenceProject = {
path: '/exporting/ref/project', path: '/importing/target/project/node_modules/exporting-ref-project',
name: 'exporting-ref-project', name: 'exporting-ref-project',
files: [ files: [
// This file contains all 'original' exported definitions // This file contains all 'original' exported definitions
@ -253,10 +253,125 @@ describe('Analyzer "match-imports"', () => {
} }
describe('Extracting exports', () => { describe('Extracting exports', () => {
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 (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 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];
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 () => { it(`identifies all direct export specifiers consumed by "importing-target-project"`, async () => {
mockTargetAndReferenceProject(searchTargetProject, referenceProject); mockTargetAndReferenceProject(searchTargetProject, referenceProject);
await providence(matchImportsQueryConfig, _providenceCfg); await providence(matchImportsQueryConfig, _providenceCfg);
const queryResult = queryResults[0]; const queryResult = queryResults[0];
// console.log(JSON.stringify(queryResult.queryOutput, null, 2));
expectedExportIdsDirect.forEach(directId => { expectedExportIdsDirect.forEach(directId => {
expect( expect(
queryResult.queryOutput.find( queryResult.queryOutput.find(
@ -292,8 +407,40 @@ describe('Analyzer "match-imports"', () => {
}); });
}); });
}); });
});
describe('Matching', () => { describe('Matching', () => {
it(`produces a list of all matches, sorted by project`, async () => {
/**
* 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' },
]);
});
describe('Inside small example project', () => {
it(`produces a list of all matches, sorted by project`, async () => { it(`produces a list of all matches, sorted by project`, async () => {
mockTargetAndReferenceProject(searchTargetProject, referenceProject); mockTargetAndReferenceProject(searchTargetProject, referenceProject);
await providence(matchImportsQueryConfig, _providenceCfg); await providence(matchImportsQueryConfig, _providenceCfg);
@ -308,6 +455,7 @@ describe('Analyzer "match-imports"', () => {
}); });
}); });
}); });
});
describe('Configuration', () => { describe('Configuration', () => {
it(`allows to provide results of FindExportsAnalyzer and FindImportsAnalyzer`, async () => { it(`allows to provide results of FindExportsAnalyzer and FindImportsAnalyzer`, async () => {

View file

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

View file

@ -15,15 +15,9 @@ const {
restoreSuppressNonCriticalLogs, restoreSuppressNonCriticalLogs,
} = require('../../../test-helpers/mock-log-service-helpers.js'); } = 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 // 1. Reference input data
const referenceProject = { const referenceProject = {
path: '/exporting/ref/project', path: '/importing/target/project/node_modules/exporting-ref-project',
name: 'exporting-ref-project', name: 'exporting-ref-project',
files: [ files: [
// This file contains all 'original' exported definitions // 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) // 2. Extracted specifiers (by find-exports analyzer)
const expectedExportIdsIndirect = ['RefRenamedClass::./index.js::exporting-ref-project']; 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
*/
});