import child_process from 'child_process'; // eslint-disable-line camelcase import pathLib from 'path'; import fs from 'fs'; import commander from 'commander'; import providenceModule from '../program/providence.js'; import { LogService } from '../program/core/LogService.js'; import { QueryService } from '../program/core/QueryService.js'; import { InputDataService } from '../program/core/InputDataService.js'; import promptModule from './prompt-analyzer-menu.js'; import cliHelpers from './cli-helpers.js'; import extendDocsModule from './launch-providence-with-extend-docs.js'; import { toPosixPath } from '../program/utils/to-posix-path.js'; import { getCurrentDir } from '../program/utils/get-current-dir.mjs'; import { dashboardServer } from '../../dashboard/server.mjs'; const { version } = JSON.parse( fs.readFileSync(pathLib.resolve(getCurrentDir(import.meta.url), '../../package.json'), 'utf8'), ); const { extensionsFromCs, setQueryMethod, targetDefault, installDeps } = cliHelpers; /** * @param {{cwd?:string; argv: string[]; providenceConf?: object}} cfg */ export async function cli({ cwd = process.cwd(), providenceConf, argv = process.argv }) { /** @type {(value: any) => void} */ let resolveCli; /** @type {(reason?: any) => void} */ let rejectCli; const cliPromise = new Promise((resolve, reject) => { resolveCli = resolve; rejectCli = reject; }); /** @type {'analyzer'|'queryString'} */ let searchMode; /** @type {object} */ let analyzerOptions; /** @type {object} */ let featureOptions; /** @type {object} */ let regexSearchOptions; // TODO: change back to "InputDataService.getExternalConfig();" once full package ESM const externalConfig = providenceConf; async function getQueryConfigAndMeta( /* eslint-disable no-shadow */ searchMode, regexSearchOptions, featureOptions, analyzerOptions, /* eslint-enable no-shadow */ ) { let queryConfig = null; let queryMethod = null; if (searchMode === 'search-query') { queryConfig = QueryService.getQueryConfigFromRegexSearchString( regexSearchOptions.regexString, ); queryMethod = 'grep'; } else if (searchMode === 'feature-query') { queryConfig = QueryService.getQueryConfigFromFeatureString(featureOptions.queryString); queryMethod = 'grep'; } else if (searchMode === 'analyzer-query') { let { name, config } = analyzerOptions; if (!name) { const answers = await promptModule.promptAnalyzerMenu(); name = answers.analyzerName; } if (!config) { const answers = await promptModule.promptAnalyzerConfigMenu( name, analyzerOptions.promptOptionalConfig, ); config = answers.analyzerConfig; } // Will get metaConfig from ./providence.conf.js const metaConfig = externalConfig ? externalConfig.metaConfig : {}; config = { ...config, metaConfig }; queryConfig = QueryService.getQueryConfigFromAnalyzer(name, config); queryMethod = 'ast'; } else { LogService.error('Please define a feature, analyzer or search'); process.exit(1); } return { queryConfig, queryMethod }; } async function launchProvidence() { const { queryConfig, queryMethod } = await getQueryConfigAndMeta( searchMode, regexSearchOptions, featureOptions, analyzerOptions, ); const searchTargetPaths = commander.searchTargetCollection || commander.searchTargetPaths; let referencePaths; if (queryConfig.analyzer.requiresReference) { referencePaths = commander.referenceCollection || commander.referencePaths; } /** * 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 { totalSearchTargets = searchTargetPaths; } // TODO: filter out: // - dependencies listed in reference (?) Or at least, inside match-imports, make sure that // we do not test against ourselves... // - providenceModule.providence(queryConfig, { gatherFilesConfig: { extensions: commander.extensions, allowlistMode: commander.allowlistMode, allowlist: commander.allowlist, }, gatherFilesConfigReference: { extensions: commander.extensions, allowlistMode: commander.allowlistModeReference, allowlist: commander.allowlistReference, }, debugEnabled: commander.debug, queryMethod, targetProjectPaths: totalSearchTargets, referenceProjectPaths: referencePaths, targetProjectRootPaths: searchTargetPaths, writeLogFile: commander.writeLogFile, skipCheckMatchCompatibility: commander.skipCheckMatchCompatibility, measurePerformance: commander.measurePerf, addSystemPathsInResult: commander.addSystemPaths, }); } async function manageSearchTargets(options) { const basePath = pathLib.join(__dirname, '../..'); if (options.update) { LogService.info('git submodule update --init --recursive'); // eslint-disable-next-line camelcase const updateResult = child_process.execSync('git submodule update --init --recursive', { cwd: basePath, }); LogService.info(String(updateResult)); } if (options.deps) { await installDeps(commander.searchTargetPaths); } if (options.createVersionHistory) { await installDeps(commander.searchTargetPaths); } } commander .version(version, '-v, --version') .option('-e, --extensions [extensions]', 'extensions like "js,html"', extensionsFromCs, [ '.js', '.html', ]) .option('-D, --debug', 'shows extensive logging') .option( '-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 => cliHelpers.pathsArrayFromCs(v, cwd), targetDefault(cwd), ) .option( '-r, --reference-paths [references]', `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 => cliHelpers.pathsArrayFromCs(v, cwd), InputDataService.referenceProjectPaths, ) .option('-a, --allowlist [allowlist]', `allowlisted paths, like 'src/**/*, packages/**/*'`, v => cliHelpers.csToArray(v), ) .option( '--allowlist-reference [allowlist-reference]', `allowed paths for reference, like 'src/**/*, packages/**/*'`, v => cliHelpers.csToArray(v), ) .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 => 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 => cliHelpers.pathsArrayFromCollectionName(v, 'reference', externalConfig), ) .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`, ) .option( '--allowlist-mode [allowlist-mode]', `Depending on whether we are dealing with a published artifact (a dependency installed via npm) or a git repository, different paths will be automatically put in the appropiate mode. A mode of 'npm' will look at the package.json "files" entry and a mode of 'git' will look at '.gitignore' entry. A mode of 'export-map' will look for all paths exposed via an export map. The mode will be auto detected, but can be overridden via this option.`, ) .option( '--allowlist-mode-reference [allowlist-mode-reference]', `allowlist mode applied to refernce project`, ) .option( '--skip-check-match-compatibility', `skips semver checks, handy for forward compatible libs or libs below v1`, ) .option('--measure-perf', 'Logs the completion time in seconds') .option('--add-system-paths', 'Adds system paths to results'); commander .command('search ') .alias('s') .description('perfoms regex search string like "my-.*-comp"') .action((regexString, options) => { searchMode = 'search-query'; regexSearchOptions = options; regexSearchOptions.regexString = regexString; launchProvidence().then(resolveCli).catch(rejectCli); }); commander .command('feature ') .alias('f') .description('query like "tg-icon[size=xs]"') .option('-m, --method [method]', 'query method: "grep" or "ast"', setQueryMethod, 'grep') .action((queryString, options) => { searchMode = 'feature-query'; featureOptions = options; featureOptions.queryString = queryString; launchProvidence().then(resolveCli).catch(rejectCli); }); commander .command('analyze [analyzer-name]') .alias('a') .description( `predefined "query" for ast analysis. Can be a script found in program/analyzers, like "find-imports"`, ) .option( '-o, --prompt-optional-config', `by default, only required configuration options are asked for. When this flag is provided, optional configuration options are shown as well`, ) .option('-c, --config [config]', 'configuration object for analyzer', c => JSON.parse(c)) .action((analyzerName, options) => { searchMode = 'analyzer-query'; analyzerOptions = options; analyzerOptions.name = analyzerName; launchProvidence().then(resolveCli).catch(rejectCli); }); commander .command('extend-docs') .alias('e') .description( `Generates data for "babel-extend-docs" plugin. These data are generated by the "match-paths" plugin, which automatically resolves import paths from reference projects (say [@lion/input, @lion/textarea, ...etc]) to a target project (say "wolf-ui").`, ) .option( '--prefix-from [prefix-from]', `Prefix for components of reference layer. By default "lion"`, a => a, 'lion', ) .option( '--prefix-to [prefix-to]', `Prefix for components of reference layer. For instance "wolf"`, ) .option( '--output-folder [output-folder]', `This is the file path where the result file "providence-extend-docs-data.json" will be written to`, p => toPosixPath(pathLib.resolve(process.cwd(), p.trim())), process.cwd(), ) .action(options => { if (!options.prefixTo) { LogService.error(`Please provide a "prefix to" like '--prefix-to "myprefix"'`); process.exit(1); } if (!commander.referencePaths) { LogService.error(`Please provide referencePaths path like '-r "node_modules/@lion/*"'`); process.exit(1); } const prefixCfg = { from: options.prefixFrom, to: options.prefixTo }; extendDocsModule .launchProvidenceWithExtendDocs({ referenceProjectPaths: commander.referencePaths, prefixCfg, outputFolder: options.outputFolder, extensions: commander.extensions, allowlist: commander.allowlist, allowlistReference: commander.allowlistReference, skipCheckMatchCompatibility: commander.skipCheckMatchCompatibility, cwd, }) .then(resolveCli) .catch(rejectCli); }); commander .command('manage-projects') .description( `Before running a query, be sure to have search-targets up to date (think of npm/bower dependencies, latest version etc.)`, ) .option('-u, --update', 'gets latest of all search-targets and references') .option('-d, --deps', 'installs npm/bower dependencies of search-targets') .option('-h, --create-version-history', 'gets latest of all search-targets and references') .action(options => { manageSearchTargets(options); }); commander .command('dashboard') .description( `Runs an interactive dashboard that shows all aggregated data from proivdence-output, configured via providence.conf`, ) .action(() => { dashboardServer.start(); }); commander.parse(argv); await cliPromise; }