UNPKG

codependence

Version:

Checks `codependencies` in package.json files to ensure dependencies are up-to-date 🤼‍♀️

362 lines (357 loc) 11.8 kB
#!/usr/bin/env node // src/program.ts import { program } from "commander"; import { cosmiconfigSync } from "cosmiconfig"; import ora from "ora"; import gradient2 from "gradient-string"; // src/scripts/utils.ts import gradient from "gradient-string"; var logger = ({ type, section = "", message, err = "", isDebugging = false }) => { const emoji = `\u{1F93C}\u200D\u2640\uFE0F`; const gap = ` => `; const debugMsg = isDebugging ? "debugging:" : ""; const sectionMsg = section.length ? `${section}:` : ""; const firstLine = `codependence:${debugMsg}${sectionMsg}`; const secondLine = message ? `${emoji}${gap}${message}` : ""; if (type === "error") { console.error(gradient.passion(firstLine)); if (secondLine) console.error(secondLine); if (err) console.error(err); } else if (type === "debug") { console.debug(gradient.passion(firstLine)); if (secondLine) console.debug(secondLine); } else if (type === "info") { console.info(gradient.teen(firstLine)); if (secondLine) console.info(secondLine); } else { console.log(gradient.teen(firstLine)); if (secondLine) console.log(secondLine); } }; var writeConsoleMsgs = (packageName, depList) => { if (!depList.length) return; Array.from(depList, ({ name: depName, expected, actual }) => { logger({ type: "log", section: packageName, message: `${depName} version is incorrect!` }); console.log(`\u{1F93C}\u200D\u2640\uFE0F => Found ${actual} and should be ${expected}`); }); }; // src/scripts/core.ts import { readFileSync, writeFileSync } from "fs"; import validatePackageName from "validate-npm-package-name"; import { execa } from "execa"; import fg from "fast-glob"; var { sync: glob } = fg; var constructVersionMap = async ({ codependencies, exec = execa, debug = false, yarnConfig = false, isTesting = false, validate = validatePackageName }) => { const updatedCodeDependencies = await Promise.all( codependencies.map(async (item) => { try { if (typeof item === "object" && Object.keys(item).length === 1) { return item; } else if (typeof item === "string" && item.length > 1 && !item.includes(" ")) { const { validForNewPackages, validForOldPackages, errors } = validate(item); const isValid = [validForNewPackages, validForOldPackages].every((valid) => valid === true); if (!isValid) throw new Error(errors?.join(", ")); const runner = !yarnConfig ? "npm" : "yarn"; const cmd = !yarnConfig ? ["view", item, "version", "latest"] : ["npm", "info", item, "--fields", "version", "--json"]; const { stdout = "" } = await exec(runner, cmd); const version = !yarnConfig ? stdout.replace("\n", "") : JSON.parse(stdout.replace("\n", ""))?.version; if (version) return { [item]: version }; throw `${version}`; } else { throw "invalid item type"; } } catch (err) { if (debug) logger({ type: "error", section: `constructVersionMap`, message: err.toString(), isDebugging: debug }); logger({ type: "error", section: `constructVersionMap`, message: `there was an error retrieving ${item}` }); console.error(`\u{1F93C}\u200D\u2640\uFE0F => Is \u261D\uFE0F a private package? Does that name look correct? \u{1F9D0}`); console.error( `\u{1F93C}\u200D\u2640\uFE0F => Read more about configuring dependencies here: https://github.com/yowainwright/codependence#debugging` ); if (isTesting) return {}; process.exit(1); } }) ); const versionMap = updatedCodeDependencies.reduce( (acc = {}, item) => { const [name] = Object.keys(item); const version = item?.[name]; return { ...acc, ...name && version ? { [name]: version } : {} }; }, {} ); return versionMap; }; var constructVersionTypes = (version) => { const versionCharacters = version.split(""); const [firstCharacter, ...rest] = versionCharacters; const specifier = ["^", "~"].includes(firstCharacter) ? firstCharacter : ""; const hasSpecifier = specifier.length === 1; const characters = rest.join(""); const exactVersion = hasSpecifier ? characters : version; const bumpVersion = version; return { bumpCharacter: specifier, bumpVersion, exactVersion }; }; var constructDepsToUpdateList = (dep = {}, versionMap) => { if (!Object.keys(dep).length) return []; const versionList = Object.keys(versionMap); return Object.entries(dep).map(([name, version]) => { const { exactVersion, bumpCharacter, bumpVersion } = constructVersionTypes(version); return { name, exactVersion, bumpCharacter, bumpVersion }; }).filter(({ name, exactVersion }) => versionList.includes(name) && versionMap[name] !== exactVersion).map(({ name, bumpCharacter, bumpVersion }) => ({ name, actual: bumpVersion, exact: versionMap[name], expected: `${bumpCharacter}${versionMap[name]}` })); }; var constructDeps = (json, depName, depList) => depList?.length ? depList.reduce( (newJson, { name, expected: version }) => { return { ...json[depName], ...newJson, [name]: version }; }, {} ) : json[depName]; var constructJson = (json, depsToUpdate, isDebugging = false) => { const { depList, devDepList, peerDepList } = depsToUpdate; const dependencies = constructDeps(json, "dependencies", depList); const devDependencies = constructDeps(json, "devDependencies", devDepList); const peerDependencies = constructDeps(json, "peerDependencies", peerDepList); if (isDebugging) { logger({ type: "debug", section: "constructJson", isDebugging }); console.debug({ dependencies, devDependencies, peerDependencies }); } return { ...json, ...dependencies ? { dependencies } : {}, ...devDependencies ? { devDependencies } : {}, ...peerDependencies ? { peerDependencies } : {} }; }; var checkDependenciesForVersion = (versionMap, json, options) => { const { name, dependencies, devDependencies, peerDependencies } = json; const { isUpdating, isDebugging, isSilent, isTesting } = options; if (!dependencies && !devDependencies && !peerDependencies) return false; const depList = constructDepsToUpdateList(dependencies, versionMap); const devDepList = constructDepsToUpdateList(devDependencies, versionMap); const peerDepList = constructDepsToUpdateList(peerDependencies, versionMap); if (isDebugging) { logger({ type: "debug", isDebugging, section: "checkDependenciesForVersion" }); console.debug({ depList, devDepList, peerDepList }); } if (!depList.length && !devDepList.length && !peerDepList.length) { return false; } if (!isSilent) Array.from([depList, devDepList, peerDepList], (list) => writeConsoleMsgs(name, list)); if (isUpdating) { const updatedJson = constructJson(json, { depList, devDepList, peerDepList }, isDebugging); const { path, ...newJson } = updatedJson; if (!isTesting) { writeFileSync(path, JSON.stringify(newJson, null, 2).concat("\n")); } else { logger({ type: "info", section: "checkDependenciesForVersion:test-writeFileSync:", message: path }); } } return true; }; var checkMatches = ({ versionMap, rootDir, files, isUpdating = false, isDebugging = false, isSilent = true, isCLI = false, isTesting = false }) => { const packagesNeedingUpdate = files.map((file) => { const path = `${rootDir}${file}`; const packageJson = readFileSync(path, "utf8"); const json = JSON.parse(packageJson); return { ...json, path }; }).filter( (json) => checkDependenciesForVersion(versionMap, json, { isUpdating, isDebugging, isSilent, isTesting }) ); if (isDebugging) { logger({ type: "debug", section: "checkMatches", isDebugging, message: "see updates" }); console.debug({ packagesNeedingUpdate }); } const isOutOfDate = packagesNeedingUpdate.length > 0; if (isOutOfDate && !isUpdating) { logger({ type: "error", message: "Dependencies are not correct. \u{1F61E}" }); if (isCLI) process.exit(1); } else if (isOutOfDate) { logger({ type: "info", message: "Dependencies were not correct but should be updated! Check your git status. \u{1F603}" }); } else { logger({ type: "log", message: "No dependency issues found! \u{1F44C}" }); } }; var checkFiles = async ({ codependencies, files: matchers = ["package.json"], rootDir = "./", ignore = ["**/node_modules/**"], update = false, debug = false, silent = false, isCLI = false, yarnConfig = false, isTesting = false }) => { try { const files = glob(matchers, { cwd: rootDir, ignore }); if (!codependencies) throw '"codependencies" are required'; const versionMap = await constructVersionMap({ codependencies, debug, yarnConfig, isTesting }); checkMatches({ versionMap, rootDir, files, isCLI, isSilent: silent, isUpdating: update, isDebugging: debug, isTesting }); } catch (err) { if (debug) { logger({ type: "error", isDebugging: true, section: "checkFiles", message: err.toString() }); } } }; var script = checkFiles; // src/program.ts async function action(options = {}) { const explorer = cosmiconfigSync("codependence"); const result = options?.searchPath ? explorer.search(options.searchPath) : explorer.search(); const { config: pathConfig = {} } = options?.config ? explorer.load(options?.config) : {}; const updatedConfig = { ...!Object.keys(pathConfig).length ? result?.config : {}, ...pathConfig?.codependence ? { ...pathConfig.codependence } : pathConfig, ...options, isCLI: true }; const { config: usedConfig, searchPath: usedSearchPath, isTestingCLI, isTestingAction, ...updatedOptions } = updatedConfig; if (isTestingCLI) { console.info({ updatedOptions }); return; } if (isTestingAction) return updatedOptions; try { if (!updatedOptions.codependencies) throw '"codependencies" is required'; const spinner = ora(`\u{1F93C}\u200D\u2640\uFE0F ${gradient2.teen(`codependence`)} wrestling... `).start(); await script(updatedOptions); spinner.succeed(`\u{1F93C}\u200D\u2640\uFE0F ${gradient2.teen(`codependence`)} pinned!`); } catch (err) { logger({ type: "error", section: "cli:error", message: err.toString() }); } } program.description( "Codependency, for code dependency. Checks `coDependencies` in package.json files to ensure dependencies are up-to-date" ).option("-t, --isTestingCLI", "enable CLI only testing").option("--isTesting", "enable running fn tests w/o overwriting").option("-f, --files [files...]", "file glob pattern").option("-u, --update", "update dependencies based on check").option("-r, --rootDir <rootDir>", "root directory to start search").option("-i, --ignore [ignore...]", "ignore glob pattern").option("--debug", "enable debugging").option("--silent", "enable mainly silent logging").option("-cds, --codependencies [codependencies...]", "deps to check").option("-c, --config <config>", "path to a config file").option("-s, --searchPath <searchPath>", "path to do a config file search").option("-y, --yarnConfig", "enable yarn config support").action(action).parse(process.argv); export { script };