lion/packages-node/providence-analytics/src/cli/cli-helpers.js
2024-05-14 14:08:14 +02:00

212 lines
5.8 KiB
JavaScript

/* eslint-disable no-shadow */
import child_process from 'child_process'; // eslint-disable-line camelcase
import path from 'path';
import { optimisedGlob } from '../program/utils/optimised-glob.js';
import { toPosixPath } from '../program/utils/to-posix-path.js';
import { LogService } from '../program/core/LogService.js';
import { fsAdapter } from '../program/utils/fs-adapter.js';
/**
* @param {any[]} arr
* @returns {any[]}
*/
export function flatten(arr) {
return Array.prototype.concat.apply([], arr);
}
/**
* @param {string} v
* @returns {string[]}
*/
export function csToArray(v) {
return v.split(',').map(v => v.trim());
}
/**
* @param {string} v like 'js,html'
* @returns {string[]} like ['.js', '.html']
*/
export function extensionsFromCs(v) {
return csToArray(v).map(v => `.${v}`);
}
/**
* @param {*} m
* @returns
*/
export function setQueryMethod(m) {
const allowedMehods = ['grep', 'ast'];
if (allowedMehods.includes(m)) {
return m;
}
LogService.error(`Please provide one of the following methods: ${allowedMehods.join(', ')}`);
return undefined;
}
/**
* @param {string} targets
* @returns {Promise<string[]|undefined>}
*/
export async function pathsArrayFromCs(targets, cwd = process.cwd()) {
if (!targets) return undefined;
const resultPaths = [];
for (const t of targets.split(',')) {
if (t.startsWith('/')) {
resultPaths.push(t);
continue; // eslint-disable-line no-continue
}
if (t.includes('*')) {
const x = (await optimisedGlob(t, { cwd, absolute: true, onlyFiles: false })).map(
toPosixPath,
);
resultPaths.push(...x);
continue; // eslint-disable-line no-continue
}
resultPaths.push(toPosixPath(path.resolve(cwd, t.trim())));
}
return resultPaths;
}
/**
* @param {string} name collection name found in eCfg
* @param {'search-target'|'reference'} collectionType collection type
* @param {{searchTargetCollections: {[repo:string]:string[]}; referenceCollections:{[repo:string]:string[]}}} [eCfg] external configuration. Usually providence.conf.js
* @param {string} [cwd]
* @returns {Promise<string[]|undefined>}
*/
export async function pathsArrayFromCollectionName(
name,
collectionType = 'search-target',
eCfg = undefined,
cwd = process.cwd(),
) {
let collection;
if (collectionType === 'search-target') {
collection = eCfg?.searchTargetCollections;
} else if (collectionType === 'reference') {
collection = eCfg?.referenceCollections;
}
if (collection?.[name]) {
return pathsArrayFromCs(collection[name].join(','), cwd);
}
return undefined;
}
/**
* @param {string} processArgStr
* @param {object} [opts]
* @returns {Promise<{ code:number; output:string }>}
* @throws {Error}
*/
export function spawnProcess(processArgStr, opts) {
const processArgs = processArgStr.split(' ');
// eslint-disable-next-line camelcase
const proc = child_process.spawn(processArgs[0], processArgs.slice(1), opts);
/** @type {string} */
let output;
proc.stdout.on('data', data => {
output += data;
LogService.debug(data.toString());
});
return new Promise((resolve, reject) => {
proc.stderr.on('data', data => {
LogService.error(data.toString());
reject(data.toString());
});
proc.on('close', code => {
resolve({ code, output });
});
});
}
/**
* When providence is called from the root of a repo and no target is provided,
* this will provide the default fallback (the project itself)
* @param {string} cwd
* @returns {string[]}
*/
export function targetDefault(cwd) {
return [toPosixPath(cwd)];
}
/**
* @param {string} targetPath
* @param {((s:string) => boolean)|null} matcher
* @param {'npm'|'bower'} [mode]
*/
async function readPackageTree(targetPath, matcher, mode) {
const folderName = mode === 'npm' ? 'node_modules' : 'bower_components';
const potentialPaths = await optimisedGlob(`${folderName}/**/*`, {
onlyDirectories: true,
fs: fsAdapter.fs,
cwd: targetPath,
absolute: true,
});
const matchingPaths = potentialPaths.filter(potentialPath => {
// Only dirs that are direct children of node_modules. So '**/node_modules/a' will match, but '**/node_modules/a/b' won't
const [, projectName] =
toPosixPath(potentialPath).match(new RegExp(`^.*/${folderName}/([^/]*)$`)) || [];
return matcher ? matcher(projectName) : true;
});
return matchingPaths;
}
/**
* @param {string|undefined} matchPattern
*/
function getMatcher(matchPattern) {
if (!matchPattern) return null;
const isValidMatchPattern = matchPattern.startsWith('/') && matchPattern.endsWith('/');
if (!isValidMatchPattern) {
LogService.error(
`[appendProjectDependencyPaths] Please provide a matchPattern enclosed by '/'. Found: ${matchPattern}`,
);
return null;
}
return (/** @type {string} */ d) => {
const reString = matchPattern.slice(1, -1);
const result = new RegExp(reString).test(d);
LogService.debug(`[appendProjectDependencyPaths]: /${reString}/.test(${d} => ${result})`);
return result;
};
}
/**
* Returns all sub projects matching condition supplied in matchFn
* @param {string[]} rootPaths all search-target project paths
* @param {string} [matchPattern] base for RegExp
* @param {('npm'|'bower')[]} [modes]
*/
export async function appendProjectDependencyPaths(
rootPaths,
matchPattern,
modes = ['npm', 'bower'],
) {
const matcher = getMatcher(matchPattern);
/** @type {string[]} */
const depProjectPaths = [];
for (const targetPath of rootPaths) {
for (const mode of modes) {
depProjectPaths.push(...(await readPackageTree(targetPath, matcher, mode)));
}
}
return depProjectPaths.concat(rootPaths).map(toPosixPath);
}
export const _cliHelpersModule = {
appendProjectDependencyPaths,
pathsArrayFromCollectionName,
extensionsFromCs,
pathsArrayFromCs,
setQueryMethod,
targetDefault,
spawnProcess,
csToArray,
flatten,
};