codependence
Version:
Checks `codependencies` in package.json files to ensure dependencies are up-to-date 🤼♀️
362 lines (357 loc) • 11.8 kB
JavaScript
// 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
};