pncat
Version:
Enhanced pnpm catalogs management with advanced workspace dependency control.
907 lines (886 loc) • 30.9 kB
JavaScript
import process from 'node:process';
import * as p from '@clack/prompts';
import c from 'ansis';
import { cac } from 'cac';
import { execa } from 'execa';
import { writeFile, readFile } from 'node:fs/promises';
import { findUp } from 'find-up';
import { dirname, join, resolve } from 'pathe';
import { readPackageJSON, writePackageJSON } from 'pkg-types';
import { parsePnpmWorkspaceYaml } from 'pnpm-workspace-yaml';
import { existsSync } from 'node:fs';
import { glob } from 'tinyglobby';
import { D as DEFAULT_CATALOG_RULES } from './shared/pncat.TYZ7XF3W.mjs';
import deepmerge from 'deepmerge';
import { createConfigLoader } from 'unconfig';
const version = "0.2.3";
async function ensurePnpmWorkspaceYAML() {
let pnpmWorkspaceYamlPath = await findUp("pnpm-workspace.yaml", { cwd: process.cwd() });
if (!pnpmWorkspaceYamlPath) {
const root = await findUp([".git", "pnpm-lock.yaml"], { cwd: process.cwd() }).then((r) => r ? dirname(r) : process.cwd());
p.log.warn(c.yellow("No pnpm-workspace.yaml found"));
const result = await p.confirm({
message: `do you want to create it under project root ${c.dim(root)} ?`
});
if (!result) {
p.outro(c.red("no pnpm-workspace.yaml found, aborting"));
process.exit(1);
}
pnpmWorkspaceYamlPath = join(root, "pnpm-workspace.yaml");
await writeFile(pnpmWorkspaceYamlPath, "packages: []");
}
const context = parsePnpmWorkspaceYaml(await readFile(pnpmWorkspaceYamlPath, "utf-8"));
return {
context,
pnpmWorkspaceYamlPath
};
}
async function ensurePackage(pkg, isDev = true) {
const root = await findUp([".git", "package.json"], { cwd: process.cwd() }).then((r) => r ? dirname(r) : process.cwd());
const packageJSONPath = join(root, "package.json");
const pkgJson = await readPackageJSON(packageJSONPath);
if ([
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies"
].some((depNames) => pkgJson[depNames]?.[pkg])) {
return;
}
const spinner = p.spinner({ indicator: "dots" });
spinner.start(`resolving ${c.cyan(pkg)} from npm...`);
const { getLatestVersion } = await import('fast-npm-meta');
const result = await getLatestVersion(pkg);
const depsName = isDev ? "devDependencies" : "dependencies";
pkgJson[depsName] ??= {};
if (result.version) {
const specifier = `^${result.version}`;
pkgJson[depsName][pkg] = specifier;
spinner.stop(c.gray(`resolved ${c.cyan(pkg)}@${c.green(specifier)}`));
} else {
spinner.stop();
p.outro(c.red(`failed to resolve ${c.cyan(pkg)} from npm`));
process.exit(1);
}
await writePackageJSON(packageJSONPath, pkgJson);
p.log.success(c.green(`Change wrote to package.json`));
await execa("pnpm", ["install"], {
stdio: "inherit",
cwd: process.cwd()
});
p.log.success(c.green(`Setup completed`));
}
async function addCommand(_options) {
await ensurePackage("@antfu/nip");
await import('@antfu/nip');
await execa("nip", process.argv.slice(3), {
stdio: "inherit"
});
p.log.success("add complete");
}
const MODE_CHOICES = ["detect", "migrate", "add", "remove", "clean", "revert"];
const DEPS_FIELDS = [
"dependencies",
"devDependencies",
"peerDependencies",
"optionalDependencies",
"packageManager",
"pnpm.overrides",
"resolutions",
"overrides",
"pnpm-workspace"
];
const DEFAULT_COMMON_OPTIONS = {
cwd: "",
recursive: true,
force: false,
ignorePaths: "",
ignoreOtherWorkspaces: true,
include: "",
exclude: "",
depFields: {
packageManager: false
},
allowedProtocols: ["workspace", "link", "file"],
catalogRules: DEFAULT_CATALOG_RULES,
specifierOptions: {
skipComplexRanges: true,
allowPreReleases: true,
allowWildcards: false
}
};
const DEFAULT_CATALOG_OPTIONS = {
...DEFAULT_COMMON_OPTIONS,
mode: "detect",
yes: false
};
const DEFAULT_IGNORE_PATHS = [
"**/node_modules/**",
"**/dist/**",
"**/public/**",
"**/fixture/**",
"**/fixtures/**"
];
const DEP_TYPE_GROUP_NAME_MAP = {
dependencies: "prod",
devDependencies: "dev",
peerDependencies: "peer",
optionalDependencies: "optional"
};
function toArray(array) {
array = array ?? [];
return Array.isArray(array) ? array : [array];
}
function escapeRegExp(str) {
return str.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
}
function filterToRegex(str) {
if (str.startsWith("/")) {
const endIndex = str.lastIndexOf("/");
const regexp = str.substring(1, endIndex);
const flags = str.substring(endIndex + 1, str.length);
return new RegExp(regexp, flags);
}
return new RegExp(`^${escapeRegExp(str).replace(/\*+/g, ".*?")}$`);
}
function parseFilter(str, defaultValue = true) {
if (!str || str.length === 0)
return () => defaultValue;
const regex = toArray(str).flatMap((i) => i.split(",")).map(filterToRegex);
return (name) => {
for (const reg of regex) {
if (reg.test(name))
return true;
}
return false;
};
}
function specifierFilter(specifier, options) {
const {
skipComplexRanges = true,
skipRangeTypes = [],
allowPreReleases = true,
allowWildcards = false
} = options ?? {};
if (!specifier.trim())
return false;
if (skipRangeTypes.length > 0) {
for (const type of skipRangeTypes) {
if (type === "||" && specifier.includes("||"))
return false;
if (type === "-" && specifier.includes(" - "))
return false;
if (type === ">=" && specifier.startsWith(">="))
return false;
if (type === "<=" && specifier.startsWith("<="))
return false;
if (type === ">" && specifier.startsWith(">"))
return false;
if (type === "<" && specifier.startsWith("<"))
return false;
if (type === "x" && specifier.includes("x"))
return false;
if (type === "*" && specifier === "*")
return false;
if (type === "pre-release" && specifier.includes("-"))
return false;
}
return true;
}
if (skipComplexRanges) {
const isComplex = specifier.includes("||") || specifier.includes(" - ") || /^[><=]/.test(specifier);
if (isComplex)
return false;
}
if (!allowPreReleases && specifier.includes("-")) {
return false;
}
if (!allowWildcards && (specifier.includes("x") || specifier === "*")) {
return false;
}
return true;
}
function createDependenciesFilter(include, exclude, options) {
const i = parseFilter(include, true);
const e = parseFilter(exclude, false);
return (name, specifier) => !e(name) && i(name) && specifierFilter(specifier, options);
}
function flatten(obj, parents = []) {
if (!obj)
return obj;
let flattenData = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value === "object")
flattenData = { ...flattenData, ...flatten(value, [...parents, key]) };
else if (typeof value === "string")
flattenData[key] = { specifier: value, parents };
}
return flattenData;
}
function getByPath(obj, path) {
return flatten(path.split(".").reduce((o, i) => o?.[i], obj));
}
function parseDependency(name, specifier, type, shouldCatalog, parents) {
return {
name,
specifier,
parents,
source: type,
// when `catalog` marked to `false`, it will be bypassed on resolving
catalog: shouldCatalog(name, specifier)
};
}
function parseDependencies(pkg, type, shouldCatalog) {
return Object.entries(getByPath(pkg, type) || {}).map(([name, { specifier, parents }]) => parseDependency(name, specifier, type, shouldCatalog, parents));
}
async function loadPackageJSON(relative, options, shouldCatalog) {
const filepath = resolve(options.cwd ?? "", relative);
const raw = await readPackageJSON(filepath);
const deps = [];
for (const key of DEPS_FIELDS) {
if (options.depFields?.[key] !== false) {
if (key === "packageManager") {
if (raw.packageManager) {
const [name, specifier] = raw.packageManager.split("@");
deps.push(parseDependency(name, `^${specifier.split("+")[0]}`, "packageManager", shouldCatalog));
}
} else {
deps.push(...parseDependencies(raw, key, shouldCatalog));
}
}
}
return [
{
name: raw.name,
private: !!raw.private,
version: raw.version,
type: "package.json",
relative,
filepath,
raw,
deps
}
];
}
async function loadPnpmWorkspace(relative, options, shouldCatalog) {
const filepath = resolve(options.cwd ?? "", relative);
const rawText = await readFile(filepath, "utf-8");
const context = parsePnpmWorkspaceYaml(rawText);
const raw = context.getDocument().toJSON();
const catalogs = [];
function createPnpmWorkspaceEntry(name, map) {
const deps = Object.entries(map).map(([pkg, specifier]) => parseDependency(pkg, specifier, "pnpm-workspace", shouldCatalog));
return {
name,
relative,
filepath,
type: "pnpm-workspace.yaml",
raw,
context,
deps
};
}
if (raw.catalog) {
catalogs.push(
createPnpmWorkspaceEntry("pnpm-catalog:default", raw.catalog)
);
}
if (raw.catalogs) {
for (const key of Object.keys(raw.catalogs)) {
catalogs.push(
createPnpmWorkspaceEntry(`pnpm-catalog:${key}`, raw.catalogs[key])
);
}
}
if (raw.overrides) {
catalogs.push(
createPnpmWorkspaceEntry("pnpm-workspace:overrides", raw.overrides)
);
}
return catalogs;
}
async function loadPackage(relative, options, shouldCatalog) {
if (relative.endsWith("pnpm-workspace.yaml"))
return loadPnpmWorkspace(relative, options, shouldCatalog);
return loadPackageJSON(relative, options, shouldCatalog);
}
async function loadPackages(options) {
let packagesNames = [];
const cwd = resolve(options.cwd || process.cwd());
const filter = createDependenciesFilter(options.include, options.exclude, options.specifierOptions);
if (options.recursive) {
packagesNames = await glob("**/package.json", {
ignore: DEFAULT_IGNORE_PATHS.concat(options.ignorePaths || []),
cwd: options.cwd,
onlyFiles: true,
dot: false,
expandDirectories: false
});
packagesNames.sort((a, b) => a.localeCompare(b));
} else {
packagesNames = ["package.json"];
}
if (options.ignoreOtherWorkspaces) {
packagesNames = (await Promise.all(
packagesNames.map(async (packagePath) => {
if (!packagePath.includes("/"))
return [packagePath];
const absolute = join(cwd, packagePath);
const gitDir = await findUp(".git", { cwd: absolute, stopAt: cwd });
if (gitDir && dirname(gitDir) !== cwd)
return [];
const pnpmWorkspace = await findUp("pnpm-workspace.yaml", { cwd: absolute, stopAt: cwd });
if (pnpmWorkspace && dirname(pnpmWorkspace) !== cwd)
return [];
return [packagePath];
})
)).flat();
}
if (existsSync(join(cwd, "pnpm-workspace.yaml"))) {
packagesNames.unshift("pnpm-workspace.yaml");
}
const packages = (await Promise.all(
packagesNames.map(
(relative) => loadPackage(relative, options, filter)
)
)).flat();
return packages;
}
async function Scanner(options, callbacks = {}) {
const packages = await loadPackages(options);
callbacks.afterPackagesLoaded?.(packages);
for (const pkg of packages) {
callbacks.beforePackageStart?.(pkg);
await callbacks.onPackageResolved?.(pkg);
callbacks.afterPackageEnd?.(pkg);
}
callbacks.afterPackagesEnd?.(packages);
return {
packages
};
}
function safeYAMLDeleteIn(doc, path) {
if (doc.hasIn(path)) {
doc.deleteIn(path);
}
}
function highlightYAML(yamlContent) {
const lines = yamlContent.split("\n");
let indentLevel = 0;
const indentSize = 2;
return lines.map((line) => {
if (line.trim() === "")
return line;
const currentIndent = line.search(/\S/);
const newIndentLevel = Math.floor(currentIndent / indentSize);
const specifierMatch = line.match(/(:)\s*(['"])?([~^<>=]*\d[\w.\-]*)(['"])?/);
if (specifierMatch) {
const beforeSpecifier = line.substring(0, specifierMatch.index + 1);
const openingQuote = specifierMatch[2] || "";
const specifier = specifierMatch[3];
const closingQuote = specifierMatch[4] || "";
return `${c.cyan(beforeSpecifier)} ${openingQuote}${c.green(specifier)}${closingQuote}`;
}
indentLevel = newIndentLevel;
const colors = [c.magenta, c.yellow];
const color = colors[Math.min(indentLevel, colors.length - 1)] || c.reset;
return color(line);
}).join("\n");
}
async function cleanCommand(options) {
const pnpmWorkspaceYamlPath = await findUp("pnpm-workspace.yaml", { cwd: process.cwd() });
if (!pnpmWorkspaceYamlPath) {
p.outro(c.red("no pnpm-workspace.yaml found, aborting"));
process.exit(1);
}
await Scanner(
options,
{
afterPackagesLoaded: async (pkgs) => {
const depsRecord = {};
const deletableCatalogs = [];
for (const pkg of pkgs) {
if (pkg.type === "pnpm-workspace.yaml")
continue;
for (const dep of pkg.deps) {
depsRecord[dep.name] ??= [];
depsRecord[dep.name].push(dep);
}
}
const pnpmWorkspacePackages = pkgs.filter((pkg) => pkg.type === "pnpm-workspace.yaml");
for (const pkg of pnpmWorkspacePackages) {
for (const dep of pkg.deps) {
const catalogSpecifier = pkg.name.replace("pnpm-catalog:", "catalog:");
if (!depsRecord[dep.name] || !depsRecord[dep.name].some((d) => d.specifier === catalogSpecifier)) {
deletableCatalogs.push({
catalogName: pkg.name.replace("pnpm-catalog:", ""),
name: dep.name,
specifier: dep.specifier
});
}
}
}
if (!deletableCatalogs.length) {
p.outro(c.yellow("No deletable catalog found"));
return;
}
p.note(
c.reset(deletableCatalogs.map((item) => {
return `${c.yellow(item.catalogName)}: ${c.cyan(item.name)} (${c.green(item.specifier)})`;
}).join("\n")),
`\u{1F4E6} Found ${deletableCatalogs.length} deletable catalogs:`
);
if (!options.yes) {
const result = await p.confirm({
message: c.green("Do you want to continue?")
});
if (!result || p.isCancel(result)) {
p.outro(c.red("aborting"));
process.exit(1);
}
}
const { context } = await ensurePnpmWorkspaceYAML();
const document = context.getDocument();
deletableCatalogs.forEach((catalog) => {
if (catalog.catalogName === "default")
safeYAMLDeleteIn(document, ["catalog", catalog.name]);
safeYAMLDeleteIn(document, ["catalogs", catalog.catalogName, catalog.name]);
});
p.log.info("writing pnpm-workspace.yaml");
await writeFile(pnpmWorkspaceYamlPath, context.toString(), "utf-8");
p.log.success("clean complete");
p.outro("running pnpm install");
await execa("pnpm", ["install"], {
stdio: "inherit",
cwd: process.cwd()
});
}
}
);
}
async function detectCommand(options) {
const catalogableDeps = [];
await Scanner(
options,
{
onPackageResolved: async (pkg) => {
if (pkg.type === "pnpm-workspace.yaml")
return;
for (const dep of pkg.deps) {
if (!dep.catalog)
continue;
if (options.allowedProtocols.some((p2) => dep.specifier.startsWith(p2)))
continue;
if (dep.specifier.startsWith("catalog:"))
continue;
catalogableDeps.push({
...dep,
packageName: pkg.name,
relativePath: pkg.relative
});
}
},
afterPackagesEnd: () => {
const catalogableDepsRecord = catalogableDeps.reduce((acc, dep) => {
if (!acc[dep.name])
acc[dep.name] = [];
acc[dep.name].push(dep);
return acc;
}, {});
if (!catalogableDeps.length) {
p.outro(c.yellow("No catalogable dependencies found"));
return;
}
const contents = [];
Object.keys(catalogableDepsRecord).sort().forEach((name) => {
catalogableDepsRecord[name].sort((a, b) => a.packageName.localeCompare(b.packageName));
});
for (const [name, deps] of Object.entries(catalogableDepsRecord)) {
contents.push(c.cyan(`${name} (${deps.length}):`));
for (const dep of deps) {
contents.push(` ${c.yellow(dep.packageName)} (${c.dim(dep.relativePath)}): ${c.green(dep.specifier)}`);
}
}
p.note(c.reset(contents.join("\n")), `\u{1F4E6} Found ${catalogableDeps.length} catalogable dependencies:`);
p.outro("detect complete");
}
}
);
}
function getDepCatalogName(dep, options) {
for (const rule of options.catalogRules ?? []) {
const { name, match } = rule;
if (Array.isArray(match)) {
if (match.some((m) => typeof m === "string" ? dep.name === m : m.test(dep.name)))
return name;
} else if (typeof match === "string" && dep.name === match) {
return name;
} else if (match instanceof RegExp && match.test(dep.name)) {
return name;
}
}
return DEP_TYPE_GROUP_NAME_MAP[dep.source] || "default";
}
async function migrateCommand(options) {
let pnpmWorkspacePackages = [];
const resolvedCatalogs = {};
const resolvedPackageJson = {};
function getCatalog(depName) {
const target = Object.entries(resolvedCatalogs).find(([, deps]) => deps[depName]);
if (target) {
const [name, deps] = target;
return {
name,
specifier: deps[depName]
};
}
const pnpmWorkspace = pnpmWorkspacePackages.find((ws) => ws.deps.some((dep2) => dep2.name === depName));
if (!pnpmWorkspace)
return;
const dep = pnpmWorkspace.deps.find((dep2) => dep2.name === depName);
if (!dep)
return;
const catalogName = options.force ? getDepCatalogName(dep, options) : pnpmWorkspace.name.replace("pnpm-catalog:", "");
return {
name: catalogName,
specifier: dep.specifier
};
}
function traversePkgs(pkgs, source) {
for (const pkg of pkgs) {
if (pkg.type === "pnpm-workspace.yaml")
continue;
for (const dep of pkg.deps) {
if (dep.source !== source)
continue;
if (!dep.catalog)
continue;
if (options.allowedProtocols.some((p2) => dep.specifier.startsWith(p2)))
continue;
const catalog = getCatalog(dep.name);
let catalogName = null;
if (catalog) {
catalogName = catalog.name === "default" ? getDepCatalogName(dep, options) : catalog.name;
resolvedCatalogs[catalogName] ??= {};
resolvedCatalogs[catalogName][dep.name] = catalog.specifier;
} else {
catalogName = getDepCatalogName(dep, options);
resolvedCatalogs[catalogName] ??= {};
resolvedCatalogs[catalogName][dep.name] = dep.specifier;
}
}
}
}
await Scanner(
options,
{
afterPackagesLoaded: (pkgs) => {
pnpmWorkspacePackages = pkgs.filter((pkg) => pkg.type === "pnpm-workspace.yaml");
DEPS_FIELDS.forEach((source) => {
traversePkgs(pkgs, source);
});
},
onPackageResolved: async (pkg) => {
if (pkg.type === "pnpm-workspace.yaml")
return;
const content = pkg.raw;
for (const dep of pkg.deps) {
if (!dep.catalog)
continue;
if (options.allowedProtocols.some((p2) => dep.specifier.startsWith(p2)))
continue;
const catalog = getCatalog(dep.name);
if (!catalog)
continue;
content[dep.source][dep.name] = `catalog:${catalog.name}`;
}
resolvedPackageJson[pkg.filepath] = content;
},
afterPackagesEnd: async (_pkgs) => {
const { context, pnpmWorkspaceYamlPath } = await ensurePnpmWorkspaceYAML();
const document = context.getDocument();
safeYAMLDeleteIn(document, ["catalog"]);
safeYAMLDeleteIn(document, ["catalogs"]);
Object.entries(resolvedCatalogs).sort((a, b) => a[0].localeCompare(b[0])).forEach(([catalogName, deps]) => {
Object.entries(deps).forEach(([depName, specifier]) => {
if (catalogName === "default") {
context.setPath(["catalog", depName], specifier);
return;
}
context.setPath(["catalogs", catalogName, depName], specifier);
});
});
const content = context.toString();
p.note(c.reset(highlightYAML(content)), `${c.cyan("pnpm-workspace.yaml")} (${c.dim(pnpmWorkspaceYamlPath)})`);
if (!options.yes) {
const result = await p.confirm({
message: c.green("looks good?")
});
if (!result || p.isCancel(result)) {
p.outro(c.red("aborting"));
process.exit(1);
}
}
p.log.info("writing pnpm-workspace.yaml");
await writeFile(pnpmWorkspaceYamlPath, content, "utf-8");
p.log.info("writing package.json");
await Promise.all(Object.entries(resolvedPackageJson).map(([filepath, content2]) => {
return writePackageJSON(filepath, content2);
}));
p.log.success("migrate complete");
p.outro("running pnpm install");
await execa("pnpm", ["install"], {
stdio: "inherit",
cwd: process.cwd()
});
}
}
);
}
async function removeCommand(options) {
const names = process.argv.slice(3);
const optionalCatalogs = {};
if (!names.length) {
p.outro(c.red("no package name provided, aborting"));
process.exit(1);
}
const pnpmWorkspaceYamlPath = await findUp("pnpm-workspace.yaml", { cwd: process.cwd() });
if (!pnpmWorkspaceYamlPath) {
p.outro(c.red("no pnpm-workspace.yaml found, aborting"));
process.exit(1);
}
const { context } = await ensurePnpmWorkspaceYAML();
const document = context.getDocument();
async function resolveCatalogSelect(name, catalog) {
p.log.info(`${c.cyan(name)} found in ${c.yellow(catalog.catalogName)}`);
if (!options.yes) {
const result = await p.confirm({
message: c.green("remove from catalog and package.json?")
});
if (!result || p.isCancel(result)) {
p.outro(c.red("aborting"));
process.exit(1);
}
}
const catalogName = catalog.catalogName.replace("pnpm-catalog:", "");
if (catalogName === "default")
safeYAMLDeleteIn(document, ["catalog", catalog.name]);
else
safeYAMLDeleteIn(document, ["catalogs", catalogName, catalog.name]);
return {
name,
specifier: catalogName === "default" ? `catalog:` : `catalog:${catalogName}`
};
}
await Scanner(
options,
{
afterPackagesLoaded: async (pkgs) => {
const pnpmWorkspacePackages = pkgs.filter((pkg) => pkg.type === "pnpm-workspace.yaml");
for (const pkg of pnpmWorkspacePackages) {
for (const dep of pkg.deps) {
if (names.includes(dep.name)) {
optionalCatalogs[dep.name] ??= [];
optionalCatalogs[dep.name].push({
name: dep.name,
catalogName: pkg.name,
specifier: dep.specifier
});
}
}
}
const pendingDeps = [];
await Promise.all(names.map(async (name) => {
const catalogOptions = optionalCatalogs[name];
if (!catalogOptions) {
p.outro(`${c.cyan(name)} not found in pnpm-workspace.yaml, running pnpm remove ${c.cyan(name)}`);
await execa("pnpm", ["remove", name, options.recursive ? "--recursive" : ""], {
stdio: "inherit",
cwd: process.cwd()
});
return;
}
if (catalogOptions.length === 1) {
pendingDeps.push(await resolveCatalogSelect(name, catalogOptions[0]));
return;
}
const result = await p.select({
message: `${c.cyan(name)} found in multiple catalogs, please select one`,
options: catalogOptions.map((catalog) => ({
value: catalog.catalogName,
label: catalog.catalogName
}))
});
const selected = catalogOptions.find((catalog) => catalog.catalogName === result);
if (!selected || !result || typeof result === "symbol") {
p.outro(c.red("invalid catalog"));
process.exit(1);
}
pendingDeps.push(await resolveCatalogSelect(name, selected));
}));
p.log.info("writing pnpm-workspace.yaml");
await writeFile(pnpmWorkspaceYamlPath, context.toString(), "utf-8");
p.log.info("writing package.json");
for (const pkg of pkgs) {
if (pkg.type === "pnpm-workspace.yaml")
continue;
let changed = false;
const content = pkg.raw;
for (const dep of pkg.deps) {
const pending = pendingDeps.find((pending2) => pending2.name === dep.name);
if (pending && dep.specifier === pending?.specifier) {
delete content[dep.source][dep.name];
changed = true;
}
}
if (changed) {
await writePackageJSON(pkg.filepath, content);
}
}
p.log.success("remove complete");
p.outro("running pnpm install");
await execa("pnpm", ["install"], {
stdio: "inherit",
cwd: process.cwd()
});
}
}
);
}
async function revertCommand(options) {
const catalogSpecifiedRecord = {};
const resolvedPackageJson = {};
await Scanner(
options,
{
afterPackagesLoaded: (pkgs) => {
const pnpmWorkspacePackages = pkgs.filter((pkg) => pkg.type === "pnpm-workspace.yaml");
for (const pkg of pnpmWorkspacePackages) {
for (const dep of pkg.deps) {
const catalogSpecifier = pkg.name.replace("pnpm-catalog:", "catalog:");
catalogSpecifiedRecord[dep.name] ??= {};
catalogSpecifiedRecord[dep.name][catalogSpecifier] = dep.specifier;
}
}
},
onPackageResolved: (pkg) => {
if (pkg.type === "pnpm-workspace.yaml")
return;
const content = pkg.raw;
for (const dep of pkg.deps) {
if (!dep.specifier.includes("catalog:"))
continue;
content[dep.source][dep.name] = catalogSpecifiedRecord[dep.name][dep.specifier];
}
resolvedPackageJson[pkg.filepath] = content;
},
afterPackagesEnd: async (_pkgs) => {
const { context, pnpmWorkspaceYamlPath } = await ensurePnpmWorkspaceYAML();
const document = context.getDocument();
safeYAMLDeleteIn(document, ["catalog"]);
safeYAMLDeleteIn(document, ["catalogs"]);
if (!options.yes) {
const result = await p.confirm({
message: c.green("All catalog dependencies will be removed from pnpm-workspace.yaml, are you sure?")
});
if (!result || p.isCancel(result)) {
p.outro(c.red("aborting"));
process.exit(1);
}
}
p.log.info("writing pnpm-workspace.yaml");
await writeFile(pnpmWorkspaceYamlPath, context.toString(), "utf-8");
p.log.info("writing package.json");
await Promise.all(Object.entries(resolvedPackageJson).map(([filepath, content]) => {
return writePackageJSON(filepath, content);
}));
p.log.success("revert complete");
p.outro("running pnpm install");
execa("pnpm", ["install"], {
stdio: "inherit",
cwd: process.cwd()
});
}
}
);
}
function sortCatalogRules(rules) {
return rules.sort(
(a, b) => (a.priority ?? Infinity) - (b.priority ?? Infinity)
);
}
function normalizeConfig(options) {
if ("default" in options)
options = options.default;
return options;
}
async function resolveConfig(options) {
const defaults = { ...DEFAULT_CATALOG_OPTIONS };
options = normalizeConfig(options);
const loader = createConfigLoader({
sources: [
{
files: [
"pncat.config"
]
},
{
files: [
".pncatrc"
],
extensions: ["json", ""]
}
],
cwd: options.cwd || process.cwd(),
merge: false
});
const config = await loader.load();
if (!config.sources.length)
return deepmerge(defaults, options);
const configOptions = normalizeConfig(config.config);
const catalogRules = configOptions.catalogRules ?? defaults.catalogRules ?? [];
delete configOptions.catalogRules;
const merged = deepmerge(deepmerge(defaults, configOptions), options);
merged.catalogRules = sortCatalogRules(catalogRules);
return merged;
}
try {
const cli = cac("pncat");
cli.command("[mode]", "Enhanced pnpm catalogs management with advanced workspace dependency control").option("--recursive, -r", "recursively search for package.json in subdirectories").option("--force, -f", "force cataloging according to rules, ignoring original configurations").option("--ignore-paths <paths>", "ignore paths for search package.json").option("--ignore-other-workspaces", "ignore package.json that in other workspaces (with their own .git,pnpm-workspace.yaml,etc.)", { default: true }).option("--include, -n <deps>", "only included dependencies will be checked for catalog").option("--exclude, -x <deps>", "exclude dependencies to be checked, will override --include options").option("--yes", "Skip prompt confirmation").allowUnknownOptions().action(async (mode, options) => {
if (mode) {
if (!MODE_CHOICES.includes(mode)) {
console.error(`Invalid mode: ${mode}. Please use one of the following: ${MODE_CHOICES.join("|")}`);
process.exit(1);
}
options.mode = mode;
}
p.intro(`${c.yellow`pncat `}${c.dim`v${version}`}`);
const resolved = await resolveConfig(options);
switch (resolved.mode) {
case "detect":
await detectCommand(resolved);
break;
case "migrate":
await migrateCommand(resolved);
break;
case "add":
await addCommand(resolved);
break;
case "remove":
await removeCommand(resolved);
break;
case "clean":
await cleanCommand(resolved);
break;
case "revert":
await revertCommand(resolved);
break;
}
});
cli.help();
cli.version(version);
cli.parse();
} catch (error) {
console.error(error);
process.exit(1);
}