diff --git a/.changeset/afraid-peaches-type.md b/.changeset/afraid-peaches-type.md new file mode 100644 index 000000000..120156eb9 --- /dev/null +++ b/.changeset/afraid-peaches-type.md @@ -0,0 +1,5 @@ +--- +'providence-analytics': patch +--- + +Support export maps for match-\* analyzers diff --git a/packages-node/providence-analytics/package.json b/packages-node/providence-analytics/package.json index 99f8be8a6..371db4a56 100644 --- a/packages-node/providence-analytics/package.json +++ b/packages-node/providence-analytics/package.json @@ -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", diff --git a/packages-node/providence-analytics/src/program/analyzers/helpers/from-import-to-export-perspective.js b/packages-node/providence-analytics/src/program/analyzers/helpers/from-import-to-export-perspective.js index 16cb89920..4c9cb2bda 100644 --- a/packages-node/providence-analytics/src/program/analyzers/helpers/from-import-to-export-perspective.js +++ b/packages-node/providence-analytics/src/program/analyzers/helpers/from-import-to-export-perspective.js @@ -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} './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 }; diff --git a/packages-node/providence-analytics/src/program/analyzers/match-imports.js b/packages-node/providence-analytics/src/program/analyzers/match-imports.js index 448839e07..1cfb4b1ea 100644 --- a/packages-node/providence-analytics/src/program/analyzers/match-imports.js +++ b/packages-node/providence-analytics/src/program/analyzers/match-imports.js @@ -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} 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} */ -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 diff --git a/packages-node/providence-analytics/src/program/analyzers/match-subclasses.js b/packages-node/providence-analytics/src/program/analyzers/match-subclasses.js index d9c665382..865ed324f 100644 --- a/packages-node/providence-analytics/src/program/analyzers/match-subclasses.js +++ b/packages-node/providence-analytics/src/program/analyzers/match-subclasses.js @@ -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, diff --git a/packages-node/providence-analytics/src/program/utils/resolve-import-path.js b/packages-node/providence-analytics/src/program/utils/resolve-import-path.js index 48fc8089d..419c8feb4 100644 --- a/packages-node/providence-analytics/src/program/utils/resolve-import-path.js +++ b/packages-node/providence-analytics/src/program/utils/resolve-import-path.js @@ -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}'.`); diff --git a/packages-node/providence-analytics/test-helpers/mock-project-helpers.js b/packages-node/providence-analytics/test-helpers/mock-project-helpers.js index 73f48fe97..4a6ea98e8 100644 --- a/packages-node/providence-analytics/test-helpers/mock-project-helpers.js +++ b/packages-node/providence-analytics/test-helpers/mock-project-helpers.js @@ -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', diff --git a/packages-node/providence-analytics/test-node/cli/cli.test.js b/packages-node/providence-analytics/test-node/cli/cli.test.js index 231b224be..5080d8f14 100644 --- a/packages-node/providence-analytics/test-node/cli/cli.test.js +++ b/packages-node/providence-analytics/test-node/cli/cli.test.js @@ -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', diff --git a/packages-node/providence-analytics/test-node/program/analyzers/match-imports.test.js b/packages-node/providence-analytics/test-node/program/analyzers/match-imports.test.js index 1b342c973..56fb75610 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/match-imports.test.js +++ b/packages-node/providence-analytics/test-node/program/analyzers/match-imports.test.js @@ -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']); + }); }); }); }); diff --git a/packages-node/providence-analytics/test-node/program/analyzers/match-paths.test.js b/packages-node/providence-analytics/test-node/program/analyzers/match-paths.test.js index 78aac693b..f7ccd5196 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/match-paths.test.js +++ b/packages-node/providence-analytics/test-node/program/analyzers/match-paths.test.js @@ -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', }, ], diff --git a/packages-node/providence-analytics/test-node/program/analyzers/match-subclasses.test.js b/packages-node/providence-analytics/test-node/program/analyzers/match-subclasses.test.js index 4f6d8a70d..22895b73d 100644 --- a/packages-node/providence-analytics/test-node/program/analyzers/match-subclasses.test.js +++ b/packages-node/providence-analytics/test-node/program/analyzers/match-subclasses.test.js @@ -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']; diff --git a/packages-node/providence-analytics/test-node/program/utils/resolve-import-path.test.js b/packages-node/providence-analytics/test-node/program/utils/resolve-import-path.test.js new file mode 100644 index 000000000..81ed3fd88 --- /dev/null +++ b/packages-node/providence-analytics/test-node/program/utils/resolve-import-path.test.js @@ -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 + */ +});