const deepmerge = require('deepmerge'); const { ReportService } = require('./services/ReportService.js'); const { InputDataService } = require('./services/InputDataService.js'); const { LogService } = require('./services/LogService.js'); const { QueryService } = require('./services/QueryService.js'); const { aForEach } = require('./utils/async-array-utils.js'); // After handling a combo, we should know which project versions we have, since // the analyzer internally called createDataObject(which provides us the needed meta info). function addToSearchTargetDepsFile({ queryResult, queryConfig, providenceConfig }) { const currentSearchTarget = queryConfig.analyzerConfig.targetProjectPath; // eslint-disable-next-line array-callback-return, consistent-return providenceConfig.targetProjectRootPaths.some(rootRepo => { const rootProjectMeta = InputDataService.getProjectMeta(rootRepo); if (currentSearchTarget.startsWith(rootRepo)) { const { name: depName, version: depVersion } = queryResult.meta.analyzerMeta.targetProject; // TODO: get version of root project as well. For now, we're good with just the name // const rootProj = pathLib.basename(rootRepo); const depProj = `${depName}#${depVersion}`; // Write to file... TODO: add to array first ReportService.writeEntryToSearchTargetDepsFile(depProj, rootProjectMeta); return true; } }); } function report(queryResult, cfg) { if (cfg.report && !queryResult.meta.analyzerMeta.__fromCache) { const { identifier } = queryResult.meta.analyzerMeta; ReportService.writeToJson(queryResult, identifier, cfg.outputPath); } } /** * Creates unique QueryConfig for analyzer turn * @param {QueryConfig} queryConfig * @param {string} targetProjectPath * @param {string} referenceProjectPath */ function getSlicedQueryConfig(queryConfig, targetProjectPath, referenceProjectPath) { return { ...queryConfig, ...{ analyzerConfig: { ...queryConfig.analyzerConfig, ...{ ...(referenceProjectPath ? { referenceProjectPath } : {}), targetProjectPath, }, }, }, }; } /** * @desc definition "projectCombo": referenceProject#version + searchTargetProject#version * @param {QueryConfig} slicedQConfig * @param {cfg} object */ async function handleAnalyzerForProjectCombo(slicedQConfig, cfg) { const queryResult = await QueryService.astSearch(slicedQConfig, { gatherFilesConfig: cfg.gatherFilesConfig, gatherFilesConfigReference: cfg.gatherFilesConfigReference, skipCheckMatchCompatibility: cfg.skipCheckMatchCompatibility, ...slicedQConfig.analyzerConfig, }); if (queryResult) { report(queryResult, cfg); } return queryResult; } /** * @desc Here, we will match all our reference projects (exports) against all our search targets * (imports). * * This is an expensive operation. Therefore, we allow caching. * For each project, we store 'commitHash' and 'version' meta data. * For each combination of referenceProject#version and searchTargetProject#version we * will create a json output file. * For its filename, it will create a hash based on referenceProject#version + * searchTargetProject#version + cfg of analyzer. * Whenever the generated hash already exists in previously stored query results, * we don't have to regenerate it. * * All the json outputs can be aggregated in our dashboard and visually presented in * various ways. * * @param {QueryConfig} queryConfig * @param {ProvidenceConfig} cfg */ async function handleAnalyzer(queryConfig, cfg) { const queryResults = []; const { referenceProjectPaths, targetProjectPaths } = cfg; await aForEach(targetProjectPaths, async searchTargetProject => { if (referenceProjectPaths) { await aForEach(referenceProjectPaths, async ref => { // Create shallow cfg copy with just currrent reference folder const slicedQueryConfig = getSlicedQueryConfig(queryConfig, searchTargetProject, ref); const queryResult = await handleAnalyzerForProjectCombo(slicedQueryConfig, cfg); queryResults.push(queryResult); if (cfg.targetProjectRootPaths) { addToSearchTargetDepsFile({ queryResult, queryConfig: slicedQueryConfig, providenceConfig: cfg, }); } }); } else { const slicedQueryConfig = getSlicedQueryConfig(queryConfig, searchTargetProject); const queryResult = await handleAnalyzerForProjectCombo(slicedQueryConfig, cfg); queryResults.push(queryResult); if (cfg.targetProjectRootPaths) { addToSearchTargetDepsFile({ queryResult, queryConfig: slicedQueryConfig, providenceConfig: cfg, }); } } }); return queryResults; } async function handleFeature(queryConfig, cfg, inputData) { if (cfg.queryMethod === 'grep') { const queryResult = await QueryService.grepSearch(inputData, queryConfig, { gatherFilesConfig: cfg.gatherFilesConfig, gatherFilesConfigReference: cfg.gatherFilesConfigReference, }); return queryResult; } return undefined; } async function handleRegexSearch(queryConfig, cfg, inputData) { if (cfg.queryMethod === 'grep') { const queryResult = await QueryService.grepSearch(inputData, queryConfig, { gatherFilesConfig: cfg.gatherFilesConfig, gatherFilesConfigReference: cfg.gatherFilesConfigReference, }); return queryResult; } return undefined; } /** * @desc Creates a report with usage metrics, based on a queryConfig. * * @param {QueryConfig} queryConfig a query configuration object containing analyzerOptions. * @param {object} customConfig * @param {'ast'|'grep'} customConfig.queryMethod whether analyzer should be run or a grep should * be performed * @param {string[]} customConfig.targetProjectPaths search target projects. For instance * ['/path/to/app-a', '/path/to/app-b', ... '/path/to/app-z'] * @param {string[]} [customConfig.referenceProjectPaths] reference projects. Needed for 'match * analyzers', having `requiresReference: true`. For instance * ['/path/to/lib1', '/path/to/lib2'] * @param {GatherFilesConfig} [customConfig.gatherFilesConfig] * @param {boolean} [customConfig.report] * @param {boolean} [customConfig.debugEnabled] */ async function providenceMain(queryConfig, customConfig) { const cfg = deepmerge( { queryMethod: 'grep', // This is a merge of all 'main entry projects' // found in search-targets, including their children targetProjectPaths: null, referenceProjectPaths: null, // This will be needed to identify the parent/child relationship to write to // {outputFolder}/entryProjectDependencies.json, which will map // a project#version to [ depA#version, depB#version ] targetProjectRootPaths: null, gatherFilesConfig: {}, report: true, debugEnabled: false, writeLogFile: false, skipCheckMatchCompatibility: false, }, customConfig, ); if (cfg.debugEnabled) { LogService.debugEnabled = true; } if (cfg.referenceProjectPaths) { InputDataService.referenceProjectPaths = cfg.referenceProjectPaths; } let queryResults; if (queryConfig.type === 'ast-analyzer') { queryResults = await handleAnalyzer(queryConfig, cfg); } else { const inputData = InputDataService.createDataObject( cfg.targetProjectPaths, cfg.gatherFilesConfig, ); if (queryConfig.type === 'feature') { queryResults = await handleFeature(queryConfig, cfg, inputData); report(queryResults, cfg); } else if (queryConfig.type === 'search') { queryResults = await handleRegexSearch(queryConfig, cfg, inputData); report(queryResults, cfg); } } if (cfg.writeLogFile) { LogService.writeLogFile(); } return queryResults; } module.exports = { providence: providenceMain, };