diff --git a/.changeset/sharp-rules-tell.md b/.changeset/sharp-rules-tell.md new file mode 100644 index 000000000..9f8c6de83 --- /dev/null +++ b/.changeset/sharp-rules-tell.md @@ -0,0 +1,5 @@ +--- +'providence-analytics': patch +--- + +feat: allow target dependencies via cli diff --git a/packages/providence-analytics/src/cli/cli-helpers.js b/packages/providence-analytics/src/cli/cli-helpers.js index f113a088e..ef868d9e7 100644 --- a/packages/providence-analytics/src/cli/cli-helpers.js +++ b/packages/providence-analytics/src/cli/cli-helpers.js @@ -60,7 +60,7 @@ function pathsArrayFromCs(t, cwd = process.cwd()) { * @param {object} eCfg external configuration. Usually providence.conf.js * @returns {string[]} */ -function pathsArrayFromCollectionName(name, colType = 'search-target', eCfg) { +function pathsArrayFromCollectionName(name, colType = 'search-target', eCfg, cwd) { let collection; if (colType === 'search-target') { collection = eCfg.searchTargetCollections; @@ -68,7 +68,7 @@ function pathsArrayFromCollectionName(name, colType = 'search-target', eCfg) { collection = eCfg.referenceCollections; } if (collection && collection[name]) { - return pathsArrayFromCs(collection[name].join(',')); + return pathsArrayFromCs(collection[name].join(','), cwd); } return undefined; } @@ -107,10 +107,20 @@ function targetDefault() { /** * @desc Returns all sub projects matching condition supplied in matchFn * @param {string[]} searchTargetPaths all search-target project paths - * @param {function} matchFn filters out packages we're interested in + * @param {string} matchPattern base for RegExp * @param {string[]} modes */ -async function appendProjectDependencyPaths(rootPaths, matchFn, modes = ['npm', 'bower']) { +async function appendProjectDependencyPaths(rootPaths, matchPattern, modes = ['npm', 'bower']) { + let matchFn; + if (matchPattern) { + if (matchPattern.startsWith('/') && matchPattern.endsWith('/')) { + matchFn = (_, d) => new RegExp(matchPattern.slice(1, -1)).test(d); + } else { + LogService.error( + `[appendProjectDependencyPaths] Please provide a matchPattern enclosed by '/'. Found: ${matchPattern}`, + ); + } + } const depProjectPaths = []; await aForEach(rootPaths, async targetPath => { await aForEach(modes, async mode => { diff --git a/packages/providence-analytics/src/cli/cli.js b/packages/providence-analytics/src/cli/cli.js index 8b6871df0..ab9648c1a 100755 --- a/packages/providence-analytics/src/cli/cli.js +++ b/packages/providence-analytics/src/cli/cli.js @@ -8,22 +8,18 @@ const providenceModule = require('../program/providence.js'); const { LogService } = require('../program/services/LogService.js'); const { QueryService } = require('../program/services/QueryService.js'); const { InputDataService } = require('../program/services/InputDataService.js'); -const { promptAnalyzerMenu, promptAnalyzerConfigMenu } = require('./prompt-analyzer-menu.js'); -const { - extensionsFromCs, - setQueryMethod, - targetDefault, - appendProjectDependencyPaths, - installDeps, - pathsArrayFromCollectionName, - pathsArrayFromCs, -} = require('./cli-helpers.js'); +const promptModule = require('./prompt-analyzer-menu.js'); +const cliHelpers = require('./cli-helpers.js'); const extendDocsModule = require('./generate-extend-docs-data.js'); + +const { extensionsFromCs, setQueryMethod, targetDefault, installDeps } = cliHelpers; + const { version } = require('../../package.json'); -async function cli({ cwd, addProjectDependencyPaths } = {}) { +async function cli({ cwd } = {}) { let resolveCli; let rejectCli; + const cliPromise = new Promise((resolve, reject) => { resolveCli = resolve; rejectCli = reject; @@ -39,7 +35,6 @@ async function cli({ cwd, addProjectDependencyPaths } = {}) { let regexSearchOptions; const externalConfig = InputDataService.getExternalConfig(); - console.log('externalConfig', externalConfig); async function getQueryInputData( /* eslint-disable no-shadow */ @@ -48,7 +43,6 @@ async function cli({ cwd, addProjectDependencyPaths } = {}) { featureOptions, analyzerOptions, /* eslint-enable no-shadow */ - showAnalyzerConfigMenu, ) { let queryConfig = null; let queryMethod = null; @@ -64,11 +58,14 @@ async function cli({ cwd, addProjectDependencyPaths } = {}) { } else if (searchMode === 'analyzer-query') { let { name, config } = analyzerOptions; if (!name) { - const answers = await promptAnalyzerMenu(); + const answers = await promptModule.promptAnalyzerMenu(); name = answers.analyzerName; } - if (showAnalyzerConfigMenu && !config) { - const answers = await promptAnalyzerConfigMenu(name, analyzerOptions.promptOptionalConfig); + if (!config) { + const answers = await promptModule.promptAnalyzerConfigMenu( + name, + analyzerOptions.promptOptionalConfig, + ); config = answers.analyzerConfig; } // Will get metaConfig from ./providence.conf.js @@ -97,11 +94,18 @@ async function cli({ cwd, addProjectDependencyPaths } = {}) { referencePaths = commander.referenceCollection || commander.referencePaths; } - let extendedSearchTargets; - if (addProjectDependencyPaths) { - extendedSearchTargets = await appendProjectDependencyPaths(searchTargetPaths); + /** + * May or may not include dependencies of search target + * @type {string[]} + */ + let totalSearchTargets; + if (commander.targetDependencies !== undefined) { + totalSearchTargets = await cliHelpers.appendProjectDependencyPaths( + searchTargetPaths, + commander.targetDependencies, + ); } else { - extendedSearchTargets = searchTargetPaths; + totalSearchTargets = searchTargetPaths; } // TODO: filter out: @@ -120,7 +124,7 @@ async function cli({ cwd, addProjectDependencyPaths } = {}) { }, debugEnabled: commander.debug, queryMethod, - targetProjectPaths: extendedSearchTargets, + targetProjectPaths: totalSearchTargets, referenceProjectPaths: referencePaths, targetProjectRootPaths: searchTargetPaths, writeLogFile: commander.writeLogFile, @@ -157,7 +161,7 @@ async function cli({ cwd, addProjectDependencyPaths } = {}) { '-t, --search-target-paths [targets]', `path(s) to project(s) on which analysis/querying should take place. Requires a list of comma seperated values relative to project root`, - v => pathsArrayFromCs(v, cwd), + v => cliHelpers.pathsArrayFromCs(v, cwd), targetDefault(), ) .option( @@ -165,31 +169,39 @@ async function cli({ cwd, addProjectDependencyPaths } = {}) { `path(s) to project(s) which serve as a reference (applicable for certain analyzers like 'match-imports'). Requires a list of comma seperated values relative to project root (like 'node_modules/lion-based-ui, node_modules/lion-based-ui-labs').`, - v => pathsArrayFromCs(v, cwd), + v => cliHelpers.pathsArrayFromCs(v, cwd), InputDataService.referenceProjectPaths, ) .option('-w, --whitelist [whitelist]', `whitelisted paths, like './src, ./packages/*'`, v => - pathsArrayFromCs(v, cwd), + cliHelpers.pathsArrayFromCs(v, cwd), ) .option( '--whitelist-reference [whitelist-reference]', `whitelisted paths for reference, like './src, ./packages/*'`, - v => pathsArrayFromCs(v, cwd), + v => cliHelpers.pathsArrayFromCs(v, cwd), ) .option( '--search-target-collection [collection-name]', `path(s) to project(s) which serve as a reference (applicable for certain analyzers like 'match-imports'). Should be a collection defined in providence.conf.js as paths relative to project root.`, - v => pathsArrayFromCollectionName(v, 'search-target', externalConfig), + v => cliHelpers.pathsArrayFromCollectionName(v, 'search-target', externalConfig), ) .option( '--reference-collection [collection-name]', `path(s) to project(s) on which analysis/querying should take place. Should be a collection defined in providence.conf.js as paths relative to project root.`, - v => pathsArrayFromCollectionName(v, 'reference', externalConfig), + v => cliHelpers.pathsArrayFromCollectionName(v, 'reference', externalConfig), ) - .option('--write-log-file', `Writes all logs to 'providence.log' file`); + .option('--write-log-file', `Writes all logs to 'providence.log' file`) + .option( + '--target-dependencies [target-dependencies]', + `For all search targets, will include all its dependencies + (node_modules and bower_components). When --target-dependencies is applied + without argument, it will act as boolean and include all dependencies. + When a regex is supplied like --target-dependencies /^my-brand-/, it will filter + all packages that comply with the regex`, + ); commander .command('search ') diff --git a/packages/providence-analytics/src/program/analyzers/helpers/Analyzer.js b/packages/providence-analytics/src/program/analyzers/helpers/Analyzer.js index 97d8f4e7a..e71811189 100644 --- a/packages/providence-analytics/src/program/analyzers/helpers/Analyzer.js +++ b/packages/providence-analytics/src/program/analyzers/helpers/Analyzer.js @@ -9,26 +9,6 @@ const { InputDataService } = require('../../services/InputDataService.js'); const { aForEach } = require('../../utils/async-array-utils.js'); const { getFilePathRelativeFromRoot } = require('../../utils/get-file-path-relative-from-root.js'); -/** - * @desc Gets a cached result from ReportService. Since ReportService slightly modifies analyzer - * output, we 'unwind' before we return... - * @param {object} config - * @param {string} config.analyzerName - * @param {string} config.identifier - */ -function getCachedAnalyzerResult({ analyzerName, identifier }) { - const cachedResult = ReportService.getCachedResult({ analyzerName, identifier }); - if (!cachedResult) { - return; - } - LogService.success(`cached version found for ${identifier}`); - - const { queryOutput } = cachedResult; - const { analyzerMeta } = cachedResult.meta; - analyzerMeta.__fromCache = true; - return { analyzerMeta, queryOutput }; // eslint-disable-line consistent-return -} - /** * @desc analyzes one entry: the callback can traverse a given ast for each entry * @param {AstDataProject[]} astDataProjects @@ -48,7 +28,7 @@ async function analyzePerAstEntry(projectData, astAnalysis) { } /** - * @desc This method ensures that the result returned by an analyzer always has a consitent format, + * @desc This method ensures that the result returned by an analyzer always has a consistent format. * By returning the configuration for the queryOutput, it will be possible to run later queries * under the same circumstances * @param {array} queryOutput @@ -86,6 +66,14 @@ function ensureAnalyzerResultFormat(queryOutput, configuration, analyzer) { delete aResult.analyzerMeta.configuration.referenceProjectPath; delete aResult.analyzerMeta.configuration.targetProjectPath; + const { referenceProjectResult, targetProjectResult } = aResult.analyzerMeta.configuration; + + if (referenceProjectResult) { + delete aResult.analyzerMeta.configuration.referenceProjectResult; + } else if (targetProjectResult) { + delete aResult.analyzerMeta.configuration.targetProjectResult; + } + if (Array.isArray(aResult.queryOutput)) { aResult.queryOutput.forEach(projectOutput => { if (projectOutput.project) { @@ -123,6 +111,16 @@ function checkForMatchCompatibility(referencePath, targetPath) { return { compatible: true }; } +/** + * If in json format, 'unwind' to be compatible for analysis... + * @param {AnalyzerResult} targetOrReferenceProjectResult + */ +function unwindJsonResult(targetOrReferenceProjectResult) { + const { queryOutput } = targetOrReferenceProjectResult; + const { analyzerMeta } = targetOrReferenceProjectResult.meta; + return { queryOutput, analyzerMeta }; +} + class Analyzer { constructor() { this.requiredAst = 'babel'; @@ -132,15 +130,39 @@ class Analyzer { return false; } + /** + * In a MatchAnalyzer, two Analyzers (a reference and targer) are run. + * For instance, in a MatchImportsAnalyzer, a FindExportsAnalyzer and FinImportsAnalyzer are run. + * Their results can be provided as config params. + * If they are stored in json format, 'unwind' them to be compatible for analysis... + * @param {MatchAnalyzerConfig} cfg + */ + static __unwindProvidedResults(cfg) { + if (cfg.targetProjectResult && !cfg.targetProjectResult.analyzerMeta) { + cfg.targetProjectResult = unwindJsonResult(cfg.targetProjectResult); + } + if (cfg.referenceProjectResult && !cfg.referenceProjectResult.analyzerMeta) { + cfg.referenceProjectResult = unwindJsonResult(cfg.referenceProjectResult); + } + } + /** * @param {AnalyzerConfig} cfg * @returns {CachedAnalyzerResult|undefined} */ _prepare(cfg) { - this.targetProjectMeta = InputDataService.getProjectMeta(cfg.targetProjectPath, true); + this.constructor.__unwindProvidedResults(cfg); - if (cfg.referenceProjectPath) { + if (!cfg.targetProjectResult) { + this.targetProjectMeta = InputDataService.getProjectMeta(cfg.targetProjectPath, true); + } else { + this.targetProjectMeta = cfg.targetProjectResult.analyzerMeta.targetProject; + } + + if (cfg.referenceProjectPath && !cfg.referenceProjectResult) { this.referenceProjectMeta = InputDataService.getProjectMeta(cfg.referenceProjectPath, true); + } else if (cfg.referenceProjectResult) { + this.referenceProjectMeta = cfg.referenceProjectResult.analyzerMeta.targetProject; } /** @@ -152,9 +174,9 @@ class Analyzer { analyzerConfig: cfg, }); + // If we have a provided result cfg.referenceProjectResult, we assume the providing + // party provides compatible results for now... if (cfg.referenceProjectPath) { - this.referenceProjectMeta = InputDataService.getProjectMeta(cfg.referenceProjectPath, true); - const { compatible, reason } = checkForMatchCompatibility( cfg.referenceProjectPath, cfg.targetProjectPath, @@ -176,7 +198,7 @@ class Analyzer { /** * See if we maybe already have our result in cache in the file-system. */ - const cachedResult = getCachedAnalyzerResult({ + const cachedResult = Analyzer._getCachedAnalyzerResult({ analyzerName: this.name, identifier: this.identifier, }); @@ -186,13 +208,16 @@ class Analyzer { } LogService.info(`starting ${LogService.pad(this.name, 16)} for ${this.identifier}`); + /** * Get reference and search-target data */ - this.targetData = InputDataService.createDataObject( - [cfg.targetProjectPath], - cfg.gatherFilesConfig, - ); + if (!cfg.targetProjectResult) { + this.targetData = InputDataService.createDataObject( + [cfg.targetProjectPath], + cfg.gatherFilesConfig, + ); + } if (cfg.referenceProjectPath) { this.referenceData = InputDataService.createDataObject( @@ -251,6 +276,27 @@ class Analyzer { */ return this._finalize(queryOutput, cfg); } + + /** + * @desc Gets a cached result from ReportService. Since ReportService slightly modifies analyzer + * output, we 'unwind' before we return... + * @param {object} config + * @param {string} config.analyzerName + * @param {string} config.identifier + * @returns {AnalyzerResult|undefined} + */ + static _getCachedAnalyzerResult({ analyzerName, identifier }) { + const cachedResult = ReportService.getCachedResult({ analyzerName, identifier }); + if (!cachedResult) { + return undefined; + } + LogService.success(`cached version found for ${identifier}`); + + /** @type {AnalyzerResult} */ + const result = unwindJsonResult(cachedResult); + result.analyzerMeta.__fromCache = true; + return result; + } } module.exports = { Analyzer }; diff --git a/packages/providence-analytics/src/program/analyzers/helpers/from-import-to-export-perspective.js b/packages/providence-analytics/src/program/analyzers/helpers/from-import-to-export-perspective.js index 2ca5d353a..16cb89920 100644 --- a/packages/providence-analytics/src/program/analyzers/helpers/from-import-to-export-perspective.js +++ b/packages/providence-analytics/src/program/analyzers/helpers/from-import-to-export-perspective.js @@ -49,17 +49,21 @@ function fromImportToExportPerspective({ // 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' ? - const pathToCheck = pathLib.resolve(externalRootPath, `./${localPath}`); + if (externalRootPath) { + const pathToCheck = pathLib.resolve(externalRootPath, `./${localPath}`); - 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 + 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' } - 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' diff --git a/packages/providence-analytics/src/program/analyzers/match-imports.js b/packages/providence-analytics/src/program/analyzers/match-imports.js index e258c6f8c..8ec7f3f7f 100644 --- a/packages/providence-analytics/src/program/analyzers/match-imports.js +++ b/packages/providence-analytics/src/program/analyzers/match-imports.js @@ -19,6 +19,20 @@ function storeResult(resultsObj, exportId, filteredList, meta) { resultsObj[exportId].files = [...(resultsObj[exportId].files || []), ...Array.from(filteredList)]; } +/** + * Needed in case fromImportToExportPerspective does not have a + * externalRootPath supplied. + * @param {string} exportPath exportEntry.file + * @param {string} translatedImportPath result of fromImportToExportPerspective + */ +function compareImportAndExportPaths(exportPath, translatedImportPath) { + return ( + exportPath === translatedImportPath || + exportPath === `${translatedImportPath}.js` || + exportPath === `${translatedImportPath}/index.js` + ); +} + /** * @param {FindExportsAnalyzerResult} exportsAnalyzerResult * @param {FindImportsAnalyzerResult} importsAnalyzerResult @@ -83,6 +97,10 @@ function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerResult, c importEntryResult.importSpecifiers.includes(exportSpecifier) || importEntryResult.importSpecifiers.includes('[*]'); + if (!hasExportSpecifierImported) { + return; + } + /** * @example * exportFile './foo.js' @@ -90,22 +108,23 @@ function matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerResult, c * importFile 'importing-target-project/file.js' * => import { z } from '@reference/foo.js' */ - const isFromSameSource = - exportEntry.file === - fromImportToExportPerspective({ - requestedExternalSource: importEntryResult.normalizedSource, - externalProjectMeta: exportsProjectObj, - externalRootPath: cfg.referenceProjectPath, - }); + const fromImportToExport = fromImportToExportPerspective({ + requestedExternalSource: importEntryResult.normalizedSource, + externalProjectMeta: exportsProjectObj, + externalRootPath: cfg.referenceProjectResult ? null : cfg.referenceProjectPath, + }); + const isFromSameSource = compareImportAndExportPaths( + exportEntry.file, + fromImportToExport, + ); - // TODO: transitive deps recognition. Could also be distinct post processor - // // export { z } from '../foo.js' - // // import { z } from '@reference/foo.js' - // (exportEntryResult.normalizedSource === importEntryResult.normalizedSource) - - if (hasExportSpecifierImported && isFromSameSource) { - filteredImportsList.add(`${importProject}::${file}`); + if (!isFromSameSource) { + return; } + + // TODO: transitive deps recognition? Could also be distinct post processor + + filteredImportsList.add(`${importProject}::${file}`); }), ); storeResult(resultsObj, exportId, filteredImportsList, exportEntry.meta); @@ -201,15 +220,15 @@ class MatchImportsAnalyzer extends Analyzer { * @property {GatherFilesConfig} [gatherFilesConfig] * @property {array} [referenceProjectPath] reference paths * @property {array} [targetProjectPath] search target paths - * @property {FindExportsAnalyzerResult} [exportsAnalyzerResult] - * @property {FindImportsAnalyzerResult} [importsAnalyzerResult] + * @property {FindImportsAnalyzerResult} [targetProjectResult] + * @property {FindExportsAnalyzerResult} [referenceProjectResult] */ const cfg = { gatherFilesConfig: {}, referenceProjectPath: null, targetProjectPath: null, - exportsAnalyzerResult: null, - importsAnalyzerResult: null, + targetProjectResult: null, + referenceProjectResult: null, ...customConfig, }; @@ -224,25 +243,25 @@ class MatchImportsAnalyzer extends Analyzer { /** * Traverse */ - let { exportsAnalyzerResult } = cfg; - if (!exportsAnalyzerResult) { + let { referenceProjectResult } = cfg; + if (!referenceProjectResult) { const findExportsAnalyzer = new FindExportsAnalyzer(); - exportsAnalyzerResult = await findExportsAnalyzer.execute({ + referenceProjectResult = await findExportsAnalyzer.execute({ metaConfig: cfg.metaConfig, targetProjectPath: cfg.referenceProjectPath, }); } - let { importsAnalyzerResult } = cfg; - if (!importsAnalyzerResult) { + let { targetProjectResult } = cfg; + if (!targetProjectResult) { const findImportsAnalyzer = new FindImportsAnalyzer(); - importsAnalyzerResult = await findImportsAnalyzer.execute({ + targetProjectResult = await findImportsAnalyzer.execute({ metaConfig: cfg.metaConfig, targetProjectPath: cfg.targetProjectPath, }); } - const queryOutput = matchImportsPostprocess(exportsAnalyzerResult, importsAnalyzerResult, cfg); + const queryOutput = matchImportsPostprocess(referenceProjectResult, targetProjectResult, cfg); /** * Finalize diff --git a/packages/providence-analytics/src/program/analyzers/match-paths.js b/packages/providence-analytics/src/program/analyzers/match-paths.js index 5ed4b5c95..229152260 100644 --- a/packages/providence-analytics/src/program/analyzers/match-paths.js +++ b/packages/providence-analytics/src/program/analyzers/match-paths.js @@ -329,7 +329,7 @@ function matchPathsPostprocess( } /** - * Designed to work in conjunction with npm package `extend-docs`. + * Designed to work in conjunction with npm package `babel-plugin-extend-docs`. * It will lookup all class exports from reference project A (and store their available paths) and * matches them against all imports of project B that extend exported class (and store their * available paths). @@ -426,7 +426,8 @@ class MatchPathsAnalyzer extends Analyzer { const targetMatchSubclassesResult = await targetMatchSubclassesAnalyzer.execute({ targetProjectPath: cfg.targetProjectPath, referenceProjectPath: cfg.referenceProjectPath, - gatherFilesConfig: cfg.gatherFilesConfigReference, + gatherFilesConfig: cfg.gatherFilesConfig, + gatherFilesConfigReference: cfg.gatherFilesConfigReference, }); // [A2] @@ -434,6 +435,7 @@ class MatchPathsAnalyzer extends Analyzer { /** @type {FindExportsAnalyzerResult} */ const targetExportsResult = await targetFindExportsAnalyzer.execute({ targetProjectPath: cfg.targetProjectPath, + gatherFilesConfig: cfg.gatherFilesConfig, }); // [A3] @@ -441,6 +443,7 @@ class MatchPathsAnalyzer extends Analyzer { /** @type {FindExportsAnalyzerResult} */ const refFindExportsResult = await refFindExportsAnalyzer.execute({ targetProjectPath: cfg.referenceProjectPath, + gatherFilesConfig: cfg.gatherFilesConfigReference, }); /** @@ -448,14 +451,14 @@ class MatchPathsAnalyzer extends Analyzer { * Automatically generate a mapping from lion docs import paths to extension layer * import paths. To be served to extend-docs * - * [1] Find path variable.to 'WolfCheckbox' + * [B1] Find path variable.to 'WolfCheckbox' * Run 'match-subclasses' for target project: we find the 'rootFilePath' of class definition, * Result: './packages/wolf-checkbox/WolfCheckbox.js' - * [B1] Find export path of 'wolf-checkbox' - * Run 'find-customelements' on target project and match rootFile of [A1] with rootFile of + * [B2] Find export path of 'wolf-checkbox' + * Run 'find-customelements' on target project and match rootFile of [B1] with rootFile of * constructor. * Result: './wolf-checkbox.js' - * [B2] Find export path of 'lion-checkbox' + * [B3] Find export path of 'lion-checkbox' * Run 'find-customelements' and find-exports (for rootpath) on reference project and match * rootFile of constructor with rootFiles of where LionCheckbox is defined. * Result: './packages/checkbox/lion-checkbox.js', @@ -467,6 +470,7 @@ class MatchPathsAnalyzer extends Analyzer { /** @type {FindCustomelementsAnalyzerResult} */ const targetFindCustomelementsResult = await targetFindCustomelementsAnalyzer.execute({ targetProjectPath: cfg.targetProjectPath, + gatherFilesConfig: cfg.gatherFilesConfig, }); // [B2] @@ -474,6 +478,7 @@ class MatchPathsAnalyzer extends Analyzer { /** @type {FindCustomelementsAnalyzerResult} */ const refFindCustomelementsResult = await refFindCustomelementsAnalyzer.execute({ targetProjectPath: cfg.referenceProjectPath, + gatherFilesConfig: cfg.gatherFilesConfigReference, }); // refFindExportsAnalyzer was already created in A3 @@ -483,7 +488,6 @@ class MatchPathsAnalyzer extends Analyzer { let queryOutput = matchPathsPostprocess( targetMatchSubclassesResult, targetExportsResult, - // refImportsResult, targetFindCustomelementsResult, refFindCustomelementsResult, refFindExportsResult, diff --git a/packages/providence-analytics/src/program/utils/async-array-utils.js b/packages/providence-analytics/src/program/utils/async-array-utils.js index 27312e8f6..a188899b7 100644 --- a/packages/providence-analytics/src/program/utils/async-array-utils.js +++ b/packages/providence-analytics/src/program/utils/async-array-utils.js @@ -1,7 +1,7 @@ /** * @desc Readable way to do an async forEach - * Since predictability mathers, all array items will be handled in a queue; - * one after anotoher + * Since predictability matters, all array items will be handled in a queue, + * one after another * @param {array} array * @param {function} callback */ @@ -13,8 +13,8 @@ async function aForEach(array, callback) { } /** * @desc Readable way to do an async forEach - * Since predictability mathers, all array items will be handled in a queue; - * one after anotoher + * If predictability does not matter, this method will traverse array items concurrently, + * leading to a better performance * @param {array} array * @param {function} callback */ @@ -23,7 +23,7 @@ async function aForEachNonSequential(array, callback) { } /** * @desc Readable way to do an async map - * Since predictability is crucial for a map, all array items will be handled in a queue; + * Since predictability is crucial for a map, all array items will be handled in a queue, * one after anotoher * @param {array} array * @param {function} callback diff --git a/packages/providence-analytics/test-node/cli/cli.test.js b/packages/providence-analytics/test-node/cli/cli.test.js index 7fb19d554..c94e987df 100644 --- a/packages/providence-analytics/test-node/cli/cli.test.js +++ b/packages/providence-analytics/test-node/cli/cli.test.js @@ -1,6 +1,7 @@ const sinon = require('sinon'); const pathLib = require('path'); const { expect } = require('chai'); +const commander = require('commander'); const { mockProject, restoreMockedProjects, @@ -13,147 +14,339 @@ const { suppressNonCriticalLogs, restoreSuppressNonCriticalLogs, } = require('../../test-helpers/mock-log-service-helpers.js'); - +const { InputDataService } = require('../../src/program/services/InputDataService.js'); const { QueryService } = require('../../src/program/services/QueryService.js'); - const providenceModule = require('../../src/program/providence.js'); const extendDocsModule = require('../../src/cli/generate-extend-docs-data.js'); - -const dummyAnalyzer = require('../../test-helpers/templates/analyzer-template.js'); +const cliHelpersModule = require('../../src/cli/cli-helpers.js'); const { cli } = require('../../src/cli/cli.js'); -const { pathsArrayFromCs } = require('../../src/cli/cli-helpers.js'); +const promptAnalyzerModule = require('../../src/cli/prompt-analyzer-menu.js'); + +const { + pathsArrayFromCs, + pathsArrayFromCollectionName, + appendProjectDependencyPaths, +} = cliHelpersModule; const queryResults = []; const rootDir = pathLib.resolve(__dirname, '../../'); +const externalCfgMock = { + searchTargetCollections: { + 'lion-collection': [ + './providence-input-data/search-targets/example-project-a', + './providence-input-data/search-targets/example-project-b', + // ...etc + ], + }, + referenceCollections: { + 'lion-based-ui-collection': [ + './providence-input-data/references/lion-based-ui', + './providence-input-data/references/lion-based-ui-labs', + ], + }, +}; + +async function runCli(args, cwd) { + process.argv = [...process.argv.slice(0, 2), ...args.split(' ')]; + await cli({ cwd }); +} + describe('Providence CLI', () => { + let providenceStub; + let promptCfgStub; + let iExtConfStub; + let promptStub; + let qConfStub; + before(() => { - suppressNonCriticalLogs(); + // Prevent MaxListenersExceededWarning + commander.setMaxListeners(100); + mockWriteToJson(queryResults); + suppressNonCriticalLogs(); mockProject( { './src/OriginalComp.js': `export class OriginalComp {}`, './src/inbetween.js': `export { OriginalComp as InBetweenComp } from './OriginalComp.js'`, './index.js': `export { InBetweenComp as MyComp } from './src/inbetween.js'`, + './node_modules/dependency-a/index.js': '', + './bower_components/dependency-b/index.js': '', }, { projectName: 'example-project', projectPath: '/mocked/path/example-project', }, ); + + providenceStub = sinon.stub(providenceModule, 'providence').returns( + new Promise(resolve => { + resolve(); + }), + ); + + promptCfgStub = sinon + .stub(promptAnalyzerModule, 'promptAnalyzerConfigMenu') + .returns({ analyzerConfig: { con: 'fig' } }); + + iExtConfStub = sinon.stub(InputDataService, 'getExternalConfig').returns(externalCfgMock); + + promptStub = sinon + .stub(promptAnalyzerModule, 'promptAnalyzerMenu') + .returns({ analyzerName: 'mock-analyzer' }); + + qConfStub = sinon.stub(QueryService, 'getQueryConfigFromAnalyzer').returns({ + analyzer: { + name: 'mock-analyzer', + requiresReference: true, + }, + }); }); after(() => { + commander.setMaxListeners(10); + restoreSuppressNonCriticalLogs(); - restoreWriteToJson(); restoreMockedProjects(); - }); + restoreWriteToJson(); - let providenceStub; - let qConfStub; - beforeEach(() => { - qConfStub = sinon.stub(QueryService, 'getQueryConfigFromAnalyzer').returns({ analyzer: {} }); - providenceStub = sinon.stub(providenceModule, 'providence').returns(Promise.resolve()); - }); - - afterEach(() => { providenceStub.restore(); + promptCfgStub.restore(); + iExtConfStub.restore(); + promptStub.restore(); qConfStub.restore(); }); - async function runCli(args, cwd) { - process.argv = [...process.argv.slice(0, 2), ...args.split(' ')]; - await cli({ cwd, addProjectDependencyPaths: false }); - } - - const analyzCmd = 'analyze find-exports'; - - it('creates a QueryConfig', async () => { - await runCli('analyze find-exports -t /mocked/path/example-project'); - expect(qConfStub.called).to.be.true; - expect(qConfStub.args[0][0]).to.equal('find-exports'); + afterEach(() => { + providenceStub.resetHistory(); + promptCfgStub.resetHistory(); + iExtConfStub.resetHistory(); + promptStub.resetHistory(); + qConfStub.resetHistory(); }); + const analyzeCmd = 'analyze mock-analyzer'; + it('calls providence', async () => { - await runCli(`${analyzCmd} -t /mocked/path/example-project`); + await runCli(`${analyzeCmd} -t /mocked/path/example-project`); expect(providenceStub.called).to.be.true; }); + it('creates a QueryConfig', async () => { + await runCli(`${analyzeCmd} -t /mocked/path/example-project`); + expect(qConfStub.called).to.be.true; + expect(qConfStub.args[0][0]).to.equal('mock-analyzer'); + }); + describe('Global options', () => { + let pathsArrayFromCollectionStub; + let pathsArrayFromCsStub; + let appendProjectDependencyPathsStub; + + before(() => { + pathsArrayFromCsStub = sinon + .stub(cliHelpersModule, 'pathsArrayFromCs') + .returns(['/mocked/path/example-project']); + pathsArrayFromCollectionStub = sinon + .stub(cliHelpersModule, 'pathsArrayFromCollectionName') + .returns(['/mocked/path/example-project']); + appendProjectDependencyPathsStub = sinon + .stub(cliHelpersModule, 'appendProjectDependencyPaths') + .returns([ + '/mocked/path/example-project', + '/mocked/path/example-project/node_modules/mock-dep-a', + '/mocked/path/example-project/bower_components/mock-dep-b', + ]); + }); + + after(() => { + pathsArrayFromCsStub.restore(); + pathsArrayFromCollectionStub.restore(); + appendProjectDependencyPathsStub.restore(); + }); + + afterEach(() => { + pathsArrayFromCsStub.resetHistory(); + pathsArrayFromCollectionStub.resetHistory(); + appendProjectDependencyPathsStub.resetHistory(); + }); + it('"-e --extensions"', async () => { - await runCli(`${analyzCmd} --extensions bla,blu`); + await runCli(`${analyzeCmd} -e bla,blu`); + expect(providenceStub.args[0][1].gatherFilesConfig.extensions).to.eql(['.bla', '.blu']); + + providenceStub.resetHistory(); + + await runCli(`${analyzeCmd} --extensions bla,blu`); expect(providenceStub.args[0][1].gatherFilesConfig.extensions).to.eql(['.bla', '.blu']); }); - describe('"-t", "--search-target-paths"', async () => { - it('allows absolute paths', async () => { - await runCli(`${analyzCmd} -t /mocked/path/example-project`, rootDir); - expect(providenceStub.args[0][1].targetProjectPaths).to.eql([ - '/mocked/path/example-project', - ]); - }); + it('"-t --search-target-paths"', async () => { + await runCli(`${analyzeCmd} -t /mocked/path/example-project`, rootDir); + expect(pathsArrayFromCsStub.args[0][0]).to.equal('/mocked/path/example-project'); + expect(providenceStub.args[0][1].targetProjectPaths).to.eql(['/mocked/path/example-project']); - it('allows relative paths', async () => { - await runCli( - `${analyzCmd} -t ./test-helpers/project-mocks/importing-target-project`, - rootDir, - ); - expect(providenceStub.args[0][1].targetProjectPaths).to.eql([ - `${rootDir}/test-helpers/project-mocks/importing-target-project`, - ]); + pathsArrayFromCsStub.resetHistory(); + providenceStub.resetHistory(); - await runCli( - `${analyzCmd} -t test-helpers/project-mocks/importing-target-project`, - rootDir, - ); - expect(providenceStub.args[0][1].targetProjectPaths).to.eql([ - `${rootDir}/test-helpers/project-mocks/importing-target-project`, - ]); - }); - - // TODO: globbing via cli-helpers doesn't work for some reason when run in this test - it.skip('allows globs', async () => { - await runCli(`${analyzCmd} -t test-helpers/*`, rootDir); - expect(providenceStub.args[0][1].targetProjectPaths).to.eql([ - `${process.cwd()}/needed-for-test/pass-glob`, - ]); - }); + await runCli(`${analyzeCmd} --search-target-paths /mocked/path/example-project`, rootDir); + expect(pathsArrayFromCsStub.args[0][0]).to.equal('/mocked/path/example-project'); + expect(providenceStub.args[0][1].targetProjectPaths).to.eql(['/mocked/path/example-project']); }); - it('"-r", "--reference-paths"', async () => {}); - it('"--search-target-collection"', async () => {}); - it('"--reference-collection"', async () => {}); + it('"-r --reference-paths"', async () => { + await runCli(`${analyzeCmd} -r /mocked/path/example-project`, rootDir); + expect(pathsArrayFromCsStub.args[0][0]).to.equal('/mocked/path/example-project'); + expect(providenceStub.args[0][1].referenceProjectPaths).to.eql([ + '/mocked/path/example-project', + ]); - it.skip('"-R --verbose-report"', async () => {}); - it.skip('"-D", "--debug"', async () => {}); + pathsArrayFromCsStub.resetHistory(); + providenceStub.resetHistory(); + + await runCli(`${analyzeCmd} --reference-paths /mocked/path/example-project`, rootDir); + expect(pathsArrayFromCsStub.args[0][0]).to.equal('/mocked/path/example-project'); + expect(providenceStub.args[0][1].referenceProjectPaths).to.eql([ + '/mocked/path/example-project', + ]); + }); + + it('"--search-target-collection"', async () => { + await runCli(`${analyzeCmd} --search-target-collection lion-collection`, rootDir); + expect(pathsArrayFromCollectionStub.args[0][0]).to.equal('lion-collection'); + expect(providenceStub.args[0][1].targetProjectPaths).to.eql(['/mocked/path/example-project']); + }); + + it('"--reference-collection"', async () => { + await runCli(`${analyzeCmd} --reference-collection lion-based-ui-collection`, rootDir); + expect(pathsArrayFromCollectionStub.args[0][0]).to.equal('lion-based-ui-collection'); + expect(providenceStub.args[0][1].referenceProjectPaths).to.eql([ + '/mocked/path/example-project', + ]); + }); + + it('"-w --whitelist"', async () => { + await runCli(`${analyzeCmd} -w /mocked/path/example-project`, rootDir); + expect(pathsArrayFromCsStub.args[0][0]).to.equal('/mocked/path/example-project'); + expect(providenceStub.args[0][1].gatherFilesConfig.filter).to.eql([ + '/mocked/path/example-project', + ]); + + pathsArrayFromCsStub.resetHistory(); + providenceStub.resetHistory(); + + await runCli(`${analyzeCmd} --whitelist /mocked/path/example-project`, rootDir); + expect(pathsArrayFromCsStub.args[0][0]).to.equal('/mocked/path/example-project'); + expect(providenceStub.args[0][1].gatherFilesConfig.filter).to.eql([ + '/mocked/path/example-project', + ]); + }); + + it('"--whitelist-reference"', async () => { + await runCli(`${analyzeCmd} --whitelist-reference /mocked/path/example-project`, rootDir); + expect(pathsArrayFromCsStub.args[0][0]).to.equal('/mocked/path/example-project'); + expect(providenceStub.args[0][1].gatherFilesConfigReference.filter).to.eql([ + '/mocked/path/example-project', + ]); + }); + + it('"-D --debug"', async () => { + await runCli(`${analyzeCmd} -D`, rootDir); + expect(providenceStub.args[0][1].debugEnabled).to.equal(true); + + providenceStub.resetHistory(); + + await runCli(`${analyzeCmd} --debug`, rootDir); + expect(providenceStub.args[0][1].debugEnabled).to.equal(true); + }); + + it('--write-log-file"', async () => { + await runCli(`${analyzeCmd} --write-log-file`, rootDir); + expect(providenceStub.args[0][1].writeLogFile).to.equal(true); + }); + + it('--target-dependencies"', async () => { + await runCli(`${analyzeCmd}`, rootDir); + expect(appendProjectDependencyPathsStub.called).to.be.false; + + appendProjectDependencyPathsStub.resetHistory(); + providenceStub.resetHistory(); + + await runCli(`${analyzeCmd} --target-dependencies`, rootDir); + expect(appendProjectDependencyPathsStub.called).to.be.true; + expect(providenceStub.args[0][1].targetProjectPaths).to.eql([ + '/mocked/path/example-project', + '/mocked/path/example-project/node_modules/mock-dep-a', + '/mocked/path/example-project/bower_components/mock-dep-b', + ]); + }); + + it('--target-dependencies /^with-regex/"', async () => { + await runCli(`${analyzeCmd} --target-dependencies /^mock-/`, rootDir); + expect(appendProjectDependencyPathsStub.args[0][1]).to.equal('/^mock-/'); + }); }); describe('Commands', () => { describe('Analyze', () => { it('calls providence', async () => { - expect(typeof dummyAnalyzer.name).to.equal('string'); + await runCli(`${analyzeCmd}`, rootDir); + expect(providenceStub.called).to.be.true; }); + describe('Options', () => { - it('"-o", "--prompt-optional-config"', async () => {}); - it('"-c", "--config"', async () => {}); + it('"-o --prompt-optional-config"', async () => { + await runCli(`analyze -o`, rootDir); + expect(promptStub.called).to.be.true; + + promptStub.resetHistory(); + + await runCli(`analyze --prompt-optional-config`, rootDir); + expect(promptStub.called).to.be.true; + }); + + it('"-c --config"', async () => { + await runCli(`analyze mock-analyzer -c {"a":"2"}`, rootDir); + expect(qConfStub.args[0][0]).to.equal('mock-analyzer'); + expect(qConfStub.args[0][1]).to.eql({ a: '2', metaConfig: undefined }); + + qConfStub.resetHistory(); + + await runCli(`analyze mock-analyzer --config {"a":"2"}`, rootDir); + expect(qConfStub.args[0][0]).to.equal('mock-analyzer'); + expect(qConfStub.args[0][1]).to.eql({ a: '2', metaConfig: undefined }); + }); + + it('calls "promptAnalyzerConfigMenu" without config given', async () => { + await runCli(`analyze mock-analyzer`, rootDir); + expect(promptCfgStub.called).to.be.true; + }); }); }); - describe('Query', () => {}); - describe('Search', () => {}); + + describe.skip('Query', () => {}); + describe.skip('Search', () => {}); + describe('Manage', () => {}); + describe('Extend docs', () => { let extendDocsStub; - beforeEach(() => { + + before(() => { extendDocsStub = sinon .stub(extendDocsModule, 'launchProvidenceWithExtendDocs') .returns(Promise.resolve()); }); - afterEach(() => { + after(() => { extendDocsStub.restore(); }); + afterEach(() => { + extendDocsStub.resetHistory(); + }); + it('allows configuration', async () => { await runCli( [ @@ -176,8 +369,8 @@ describe('Providence CLI', () => { }, outputFolder: '/outp', extensions: ['.bla'], - whitelist: [`${process.cwd()}/wl`], - whitelistReference: [`${process.cwd()}/wlr`], + whitelist: [`${rootDir}/wl`], + whitelistReference: [`${rootDir}/wlr`], }); }); }); @@ -203,8 +396,102 @@ describe('CLI helpers', () => { it('allows globs', async () => { expect(pathsArrayFromCs('test-helpers/project-mocks*', rootDir)).to.eql([ - `${process.cwd()}/test-helpers/project-mocks`, - `${process.cwd()}/test-helpers/project-mocks-analyzer-outputs`, + `${rootDir}/test-helpers/project-mocks`, + `${rootDir}/test-helpers/project-mocks-analyzer-outputs`, + ]); + }); + + it('allows multiple comma separated paths', async () => { + const paths = + 'test-helpers/project-mocks*, ./test-helpers/project-mocks/importing-target-project,/mocked/path/example-project'; + expect(pathsArrayFromCs(paths, rootDir)).to.eql([ + `${rootDir}/test-helpers/project-mocks`, + `${rootDir}/test-helpers/project-mocks-analyzer-outputs`, + `${rootDir}/test-helpers/project-mocks/importing-target-project`, + '/mocked/path/example-project', + ]); + }); + }); + + describe('pathsArrayFromCollectionName', () => { + it('gets collections from external target config', async () => { + expect( + pathsArrayFromCollectionName('lion-collection', 'search-target', externalCfgMock, rootDir), + ).to.eql( + externalCfgMock.searchTargetCollections['lion-collection'].map(p => + pathLib.join(rootDir, p), + ), + ); + }); + + it('gets collections from external reference config', async () => { + expect( + pathsArrayFromCollectionName( + 'lion-based-ui-collection', + 'reference', + externalCfgMock, + rootDir, + ), + ).to.eql( + externalCfgMock.referenceCollections['lion-based-ui-collection'].map(p => + pathLib.join(rootDir, p), + ), + ); + }); + }); + + describe('appendProjectDependencyPaths', () => { + before(() => { + mockWriteToJson(queryResults); + suppressNonCriticalLogs(); + + mockProject( + { + './src/OriginalComp.js': `export class OriginalComp {}`, + './src/inbetween.js': `export { OriginalComp as InBetweenComp } from './OriginalComp.js'`, + './index.js': `export { InBetweenComp as MyComp } from './src/inbetween.js'`, + './node_modules/dependency-a/index.js': '', + './bower_components/dependency-b/index.js': '', + }, + { + projectName: 'example-project', + projectPath: '/mocked/path/example-project', + }, + ); + }); + + it('adds bower and node dependencies', async () => { + const result = await appendProjectDependencyPaths(['/mocked/path/example-project']); + expect(result).to.eql([ + '/mocked/path/example-project/node_modules/dependency-a', + '/mocked/path/example-project/bower_components/dependency-b', + '/mocked/path/example-project', + ]); + }); + + it('allows a regex filter', async () => { + const result = await appendProjectDependencyPaths(['/mocked/path/example-project'], '/b$/'); + expect(result).to.eql([ + '/mocked/path/example-project/bower_components/dependency-b', + '/mocked/path/example-project', + ]); + }); + + it('allows to filter out only npm or bower deps', async () => { + const result = await appendProjectDependencyPaths(['/mocked/path/example-project'], null, [ + 'npm', + ]); + expect(result).to.eql([ + '/mocked/path/example-project/node_modules/dependency-a', + '/mocked/path/example-project', + ]); + + const result2 = await appendProjectDependencyPaths(['/mocked/path/example-project'], null, [ + 'bower', + ]); + expect(result2).to.eql([ + '/mocked/path/example-project/bower_components/dependency-b', + '/mocked/path/example-project', ]); }); }); diff --git a/packages/providence-analytics/test-node/program/analyzers/match-imports.test.js b/packages/providence-analytics/test-node/program/analyzers/match-imports.test.js index fd131203c..1b342c973 100644 --- a/packages/providence-analytics/test-node/program/analyzers/match-imports.test.js +++ b/packages/providence-analytics/test-node/program/analyzers/match-imports.test.js @@ -312,17 +312,18 @@ describe('Analyzer "match-imports"', () => { describe('Configuration', () => { it(`allows to provide results of FindExportsAnalyzer and FindImportsAnalyzer`, async () => { mockTargetAndReferenceProject(searchTargetProject, referenceProject); - const importsAnalyzerResult = await new FindImportsAnalyzer().execute({ + const findImportsResult = await new FindImportsAnalyzer().execute({ targetProjectPath: searchTargetProject.path, }); - const exportsAnalyzerResult = await new FindExportsAnalyzer().execute({ + const findExportsResult = await new FindExportsAnalyzer().execute({ targetProjectPath: referenceProject.path, }); - await providence(matchImportsQueryConfig, { - ..._providenceCfg, - importsAnalyzerResult, - exportsAnalyzerResult, + + const matchImportsQueryConfigExt = QueryService.getQueryConfigFromAnalyzer('match-imports', { + targetProjectResult: findImportsResult, + referenceProjectResult: findExportsResult, }); + await providence(matchImportsQueryConfigExt, _providenceCfg); const queryResult = queryResults[0]; expectedExportIdsDirect.forEach(targetId => { @@ -333,5 +334,8 @@ describe('Analyzer "match-imports"', () => { testMatchedEntry(targetId, queryResult, ['./target-src/indirect-imports.js']); }); }); + + // TODO: Test this unwind functionality in a generic MatchAnalyzer test + it.skip(`allows to provide results of FindExportsAnalyzer and FindImportsAnalyzer from external jsons`, async () => {}); }); }); diff --git a/packages/providence-analytics/test-node/program/services/InputDataService.test.js b/packages/providence-analytics/test-node/program/services/InputDataService.test.js index 6392cdea3..2a5536168 100644 --- a/packages/providence-analytics/test-node/program/services/InputDataService.test.js +++ b/packages/providence-analytics/test-node/program/services/InputDataService.test.js @@ -118,7 +118,7 @@ describe('InputDataService', () => { ]); }); - it('allows passing excludeFolders', async () => { + it('allows passing excluded folders', async () => { const globOutput = InputDataService.gatherFilesFromDir('/fictional/project', { extensions: ['.html', '.js'], filter: ['!nested/**'],