pncat
Version:
Enhanced pnpm catalogs management with advanced workspace dependency control.
990 lines (974 loc) • 34 kB
JavaScript
import process from 'node:process';
import * as p from '@clack/prompts';
import c from 'ansis';
import { cac } from 'cac';
import { writeFile, readFile } from 'node:fs/promises';
import { findUp } from 'find-up';
import { dirname, join } from 'pathe';
import { parsePnpmWorkspaceYaml } from 'pnpm-workspace-yaml';
import { a as DEPS_FIELDS, p as parseSpec, s as sortSpecs, P as PnpmCatalogManager, b as DEPENDENCIES_TYPE_SHORT_MAP, c as DEFAULT_CATALOG_OPTIONS, D as DEFAULT_CATALOG_RULES, M as MODE_CHOICES } from './shared/pncat.DZVqskSj.mjs';
import { execa } from 'execa';
import { existsSync } from 'node:fs';
import { writePackageJSON, readPackageJSON as readPackageJSON$1 } from 'pkg-types';
import { diffLines } from 'diff';
import deepmerge from 'deepmerge';
import { createConfigLoader } from 'unconfig';
import 'tinyglobby';
import 'semver';
const name = "pncat";
const version = "0.5.4";
async function findWorkspaceRoot() {
const workspaceYamlPath = await findWorkspaceYAML();
if (workspaceYamlPath)
return dirname(workspaceYamlPath);
return process.cwd();
}
async function findWorkspaceYAML() {
return await findUp("pnpm-workspace.yaml", { cwd: process.cwd() });
}
async function ensureWorkspaceYAML() {
let workspaceYamlPath = await findWorkspaceYAML();
if (!workspaceYamlPath) {
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("aborting"));
process.exit(1);
}
workspaceYamlPath = join(root, "pnpm-workspace.yaml");
await writeFile(workspaceYamlPath, "packages: []");
}
const workspaceYaml = parsePnpmWorkspaceYaml(await readFile(workspaceYamlPath, "utf-8"));
return { workspaceYaml, workspaceYamlPath };
}
function parseArgs(args) {
const options = {};
const deps = [];
let i = 0;
while (i < args.length) {
const arg = args[i];
if (arg === "--") {
deps.push(...args.slice(i + 1));
break;
}
if (arg.startsWith("--")) {
const key = arg.slice(2);
if (key.startsWith("no-")) {
options[key.slice(3)] = false;
i++;
} else if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
options[key] = args[i + 1];
i += 2;
} else {
options[key] = true;
i++;
}
} else if (arg.startsWith("-") && arg.length === 2) {
const key = arg.slice(1);
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
options[key] = args[i + 1];
i += 2;
} else {
options[key] = true;
i++;
}
} else {
deps.push(arg);
i++;
}
}
return { options, deps };
}
function parsePnpmOptions(args) {
const { deps } = parseArgs(args);
const isRecursive = ["--recursive", "-r"].some((i) => args.includes(i));
const isDev = ["--save-dev", "-D"].some((i) => args.includes(i));
const isOptional = ["--save-optional", "-O"].some((i) => args.includes(i));
const isProd = ["--save-prod", "-P"].some((i) => args.includes(i));
return {
deps,
isRecursive,
isDev,
isOptional,
isProd
};
}
async function runPnpmInstall(options = {}) {
const { cwd = process.cwd(), stdio = "inherit", silent = false } = options;
if (!silent)
p.outro("running pnpm install");
await execa("pnpm", ["install"], { stdio, cwd });
}
async function runPnpmRemove(dependencies, options = {}) {
const { cwd = process.cwd(), recursive = false, stdio = "inherit" } = options;
if (dependencies.length === 0)
return;
const args = ["remove", ...dependencies];
if (recursive)
args.push("--recursive");
await execa("pnpm", args, { stdio, cwd });
}
function highlightYAMLContent(content, indentSize = 2, highlight = false) {
if (content.trim() === "") {
return content;
}
const currentIndent = content.search(/\S/);
const indentLevel = Math.floor(currentIndent / indentSize);
const colonIndex = content.indexOf(":");
if (colonIndex === -1) {
return content;
}
const beforeColon = content.substring(0, colonIndex);
const afterColon = content.substring(colonIndex);
const ansi = highlight ? c.cyan : c.reset;
if (indentLevel === 0 || indentLevel === 1) {
const propertyName = beforeColon.trim();
const leadingSpaces = content.substring(0, content.indexOf(propertyName));
const versionMatch = afterColon.match(/:\s*(.+)/);
if (versionMatch && versionMatch[1].trim())
return leadingSpaces + ansi(propertyName) + c.dim(afterColon);
else
return leadingSpaces + ansi(propertyName) + c.dim(":");
} else {
const versionMatch = afterColon.match(/:\s*(.+)/);
if (versionMatch && versionMatch[1].trim())
return beforeColon + c.dim(afterColon);
else
return beforeColon + c.dim(":");
}
}
function diffYAML(original, updated, options = {}) {
const { indentSize = 2, verbose = false } = options;
const changed = diffLines(original, updated, {
ignoreNewlineAtEof: true
});
const diffs = [];
let lineNumber = 0;
changed.forEach((part) => {
const lines = part.value.split("\n");
if (lines[lines.length - 1] === "") {
lines.pop();
}
lines.forEach((line) => {
diffs.push({
content: line,
type: part.added ? "added" : part.removed ? "removed" : "unchanged",
lineNumber: lineNumber++
});
});
});
const changedLines = /* @__PURE__ */ new Set();
diffs.forEach((line, index) => {
if (line.type === "added" || line.type === "removed")
changedLines.add(index);
});
const lineHierarchy = [];
diffs.forEach((line, index) => {
const content = line.content;
if (content.trim() === "") {
lineHierarchy.push({ indentLevel: -1, parentIndices: [] });
return;
}
const currentIndent = content.search(/\S/);
const indentLevel = Math.floor(currentIndent / indentSize);
const parentIndices = [];
for (let i = index - 1; i >= 0; i--) {
const prevLine = diffs[i];
const prevHierarchy = lineHierarchy[i];
if (prevLine.content.trim() === "")
continue;
const prevIndent = prevLine.content.search(/\S/);
const prevIndentLevel = Math.floor(prevIndent / indentSize);
if (prevIndentLevel < indentLevel) {
parentIndices.unshift(i);
if (prevIndentLevel === indentLevel - 1) {
parentIndices.unshift(...prevHierarchy.parentIndices);
break;
}
}
}
lineHierarchy.push({ indentLevel, parentIndices });
});
const linesToKeep = /* @__PURE__ */ new Set();
if (verbose) {
diffs.forEach((_, index) => {
linesToKeep.add(index);
});
} else {
changedLines.forEach((lineIndex) => {
linesToKeep.add(lineIndex);
lineHierarchy[lineIndex].parentIndices.forEach((parentIndex) => {
linesToKeep.add(parentIndex);
});
});
}
let addedCount = 0;
let removedCount = 0;
diffs.forEach((line) => {
if (line.type === "added" || line.type === "removed") {
const content = line.content;
if (content.trim() === "")
return;
const currentIndent = content.search(/\S/);
const indentLevel = Math.floor(currentIndent / indentSize);
if (indentLevel >= 2 && content.includes(":")) {
const colonIndex = content.indexOf(":");
const afterColon = content.substring(colonIndex);
const versionMatch = afterColon.match(/:\s*(.+)/);
if (versionMatch && versionMatch[1].trim()) {
if (line.type === "added")
addedCount++;
else if (line.type === "removed")
removedCount++;
}
}
}
});
const summaryParts = [];
if (addedCount > 0) {
summaryParts.push(`${c.yellow(addedCount)} added`);
}
if (removedCount > 0) {
summaryParts.push(`${c.yellow(removedCount)} removed`);
}
const result = [];
let lastKeptIndex = -1;
diffs.forEach((line, index) => {
if (linesToKeep.has(index)) {
if (!verbose && lastKeptIndex !== -1 && index > lastKeptIndex + 1) {
const skippedCount = index - lastKeptIndex - 1;
result.push(c.dim`${c.yellow(skippedCount)} unchanged line${skippedCount > 1 ? "s" : ""}`);
}
let coloredLine = line.content;
if (line.type === "added") {
const highlightedContent = highlightYAMLContent(line.content, indentSize, verbose);
coloredLine = c.green(`+ ${highlightedContent}`);
} else if (line.type === "removed") {
const highlightedContent = highlightYAMLContent(line.content, indentSize, verbose);
coloredLine = c.red(`- ${highlightedContent}`);
} else {
const highlightedContent = highlightYAMLContent(line.content, indentSize, verbose);
coloredLine = ` ${highlightedContent}`;
}
result.push(coloredLine);
lastKeptIndex = index;
}
});
if (summaryParts.length > 0) {
result.push("");
result.push(summaryParts.join(" "));
}
return result.join("\n");
}
async function confirmWorkspaceChanges(modifier, options) {
const {
pnpmCatalogManager,
workspaceYaml,
workspaceYamlPath,
updatedPackages,
yes = false,
verbose = false,
bailout = true,
confirmMessage = "continue?",
completeMessage
} = options ?? {};
const commandOptions = pnpmCatalogManager.getOptions();
const rawContent = workspaceYaml.toString();
await modifier();
const content = workspaceYaml.toString();
if (rawContent === content) {
if (bailout) {
p.outro(c.yellow("no changes to pnpm-workspace.yaml"));
process.exit(0);
} else {
p.log.info(c.green("no changes to pnpm-workspace.yaml"));
}
}
const diff = diffYAML(rawContent, content, { verbose });
if (diff) {
p.note(c.reset(diff), c.dim(workspaceYamlPath));
if (!yes) {
const result = await p.confirm({ message: confirmMessage });
if (!result || p.isCancel(result)) {
p.outro(c.red("aborting"));
process.exit(1);
}
}
await writePnpmWorkspace(workspaceYamlPath, content);
}
if (updatedPackages)
await writePackageJSONs(updatedPackages);
if (completeMessage) {
if (commandOptions.install) {
p.log.info(c.green(completeMessage));
await runPnpmInstall({ cwd: pnpmCatalogManager.getCwd() });
} else {
p.outro(c.green(completeMessage));
}
}
}
async function writePnpmWorkspace(filePath, content) {
p.log.info("writing pnpm-workspace.yaml");
await writeFile(filePath, content, "utf-8");
}
async function readPackageJSON() {
const pkgPath = join(process.cwd(), "package.json");
if (!existsSync(pkgPath)) {
p.outro(c.red("no package.json found, aborting"));
process.exit(1);
}
const pkgJson = await readPackageJSON$1(pkgPath);
if (typeof pkgJson.name !== "string") {
p.outro(c.red("package.json is missing name, aborting"));
process.exit(1);
}
return { pkgPath, pkgJson };
}
async function writePackageJSONs(updatedPackages) {
if (Object.keys(updatedPackages).length === 0)
return;
p.log.info("writing package.json");
await Promise.all(
Object.values(updatedPackages).map((pkg) => writePackageJSON(pkg.filepath, cleanupPackageJSON(pkg.raw)))
);
}
function cleanupPackageJSON(pkgJson) {
for (const field of DEPS_FIELDS) {
const deps = pkgJson[field];
if (!deps)
continue;
if (Object.keys(deps).length === 0)
delete pkgJson[field];
}
return pkgJson;
}
function generateWorkspaceYAML(dependencies, workspaceYaml) {
const document = workspaceYaml.getDocument();
document.deleteIn(["catalog"]);
document.deleteIn(["catalogs"]);
const catalogs = {};
for (const dep of dependencies) {
if (!catalogs[dep.catalogName])
catalogs[dep.catalogName] = {};
catalogs[dep.catalogName][dep.name] = dep.specifier;
}
Object.entries(catalogs).sort((a, b) => a[0].localeCompare(b[0])).forEach(([catalogName, deps]) => {
Object.entries(deps).forEach(([name, specifier]) => {
if (catalogName === "default")
workspaceYaml.setPath(["catalog", name], specifier);
else
workspaceYaml.setPath(["catalogs", catalogName, name], specifier);
});
});
}
function cleanupWorkspaceYAML(workspaceYaml) {
const document = workspaceYaml.getDocument();
const workspaceJson = workspaceYaml.toJSON();
if (workspaceJson.catalog && !Object.keys(workspaceJson.catalog).length)
document.deleteIn(["catalog"]);
if (workspaceJson.catalogs) {
const emptyCatalogs = [];
for (const [catalogKey, catalogValue] of Object.entries(workspaceJson.catalogs)) {
if (!catalogValue || Object.keys(catalogValue).length === 0)
emptyCatalogs.push(catalogKey);
}
emptyCatalogs.forEach((key) => {
document.deleteIn(["catalogs", key]);
});
}
const updatedWorkspaceJson = workspaceYaml.toJSON();
if (!updatedWorkspaceJson.catalogs || Object.keys(updatedWorkspaceJson.catalogs).length === 0) {
document.deleteIn(["catalogs"]);
}
}
function removeWorkspaceYAMLDeps(dependencies, workspaceYaml) {
const document = workspaceYaml.getDocument();
dependencies.forEach((dep) => {
if (dep.catalogName === "default")
document.deleteIn(["catalog", dep.name]);
else
document.deleteIn(["catalogs", dep.catalogName, dep.name]);
});
cleanupWorkspaceYAML(workspaceYaml);
}
async function resolveAdd(args, context) {
const { options, workspaceYaml, pnpmCatalogManager } = context;
await pnpmCatalogManager.loadPackages();
const { deps, isDev } = parsePnpmOptions(args);
if (!deps.length) {
p.outro(c.red("no dependencies provided, aborting"));
process.exit(1);
}
const parsed = deps.map((x) => x.trim()).filter(Boolean).map(parseSpec);
const workspaceJson = workspaceYaml.toJSON();
const workspacePackages = pnpmCatalogManager.getWorkspacePackages();
const createDep = (dep) => {
return {
name: dep.name,
specifier: dep.specifier,
source: isDev ? "devDependencies" : "dependencies",
catalog: false,
catalogable: true,
catalogName: dep.catalogName
};
};
for (const dep of parsed) {
if (!dep.specifier && workspacePackages.includes(dep.name)) {
dep.specifier = "workspace:*";
dep.specifierSource ||= "workspace";
continue;
}
if (options.catalog)
dep.catalogName ||= options.catalog;
if (dep.specifier)
dep.specifierSource ||= "user";
if (!dep.specifier) {
const catalogs = workspaceYaml.getPackageCatalogs(dep.name);
if (catalogs[0]) {
dep.catalogName = catalogs[0];
dep.specifierSource ||= "catalog";
}
}
if (dep.catalogName && !dep.specifier) {
const spec = dep.catalogName === "default" ? workspaceJson?.catalog?.[dep.name] : workspaceJson?.catalogs?.[dep.catalogName]?.[dep.name];
if (spec) {
dep.specifier = spec;
}
}
if (!dep.specifier) {
const spinner = p.spinner({ indicator: "dots" });
spinner.start(`resolving ${c.cyan(dep.name)} from npm...`);
const { getLatestVersion } = await import('fast-npm-meta');
const { version } = await getLatestVersion(dep.name);
if (version) {
dep.specifier = `^${version}`;
dep.specifierSource ||= "npm";
spinner.stop(`${c.dim("resolved")} ${c.cyan(dep.name)}${c.dim(`@${c.green(dep.specifier)}`)}`);
} else {
spinner.stop(`failed to resolve ${c.cyan(dep.name)} from npm`);
p.outro(c.red("aborting"));
process.exit(1);
}
}
if (!dep.catalogName) {
dep.catalogName = options.catalog || pnpmCatalogManager.inferCatalogName(createDep(dep));
}
}
return { isDev, dependencies: parsed.map((dep) => createDep(dep)) };
}
async function resolveClean(context) {
const { pnpmCatalogManager } = context;
const packages = await pnpmCatalogManager.loadPackages();
const dependencies = [];
for (const pkg of packages) {
if (pkg.type === "package.json")
continue;
for (const dep of pkg.deps) {
const resolvedDep = pnpmCatalogManager.resolveDep(dep, false);
if (!pnpmCatalogManager.isDepInPackage(resolvedDep)) {
dependencies.push(resolvedDep);
}
}
}
return { dependencies };
}
async function resolveMigrate(context) {
const { options, pnpmCatalogManager } = context;
const packages = await pnpmCatalogManager.loadPackages();
const dependencies = /* @__PURE__ */ new Map();
const updatedPackages = /* @__PURE__ */ new Map();
const setDep = (dep) => {
if (!dependencies.has(dep.name))
dependencies.set(dep.name, /* @__PURE__ */ new Map());
const catalogDeps = dependencies.get(dep.name);
if (!catalogDeps.has(dep.catalogName))
catalogDeps.set(dep.catalogName, []);
catalogDeps.get(dep.catalogName).push(dep);
};
const setPackage = (dep, pkg) => {
if (!updatedPackages.has(pkg.name))
updatedPackages.set(pkg.name, structuredClone(pkg));
const pkgJson = updatedPackages.get(pkg.name);
pkgJson.raw[dep.source][dep.name] = dep.catalogName === "default" ? "catalog:" : `catalog:${dep.catalogName}`;
};
for (const pkg of packages) {
if (pkg.type === "pnpm-workspace.yaml")
continue;
for (const dep of pkg.deps) {
if (!dep.catalogable)
continue;
const resolvedDep = pnpmCatalogManager.resolveDep(dep);
setDep(resolvedDep);
if (resolvedDep.update)
setPackage(resolvedDep, pkg);
}
}
await resolveConflict(dependencies, options);
return {
dependencies: Array.from(dependencies.values()).flatMap((i) => Array.from(i.values()).flat()),
updatedPackages: Object.fromEntries(updatedPackages.entries())
};
}
async function resolveConflict(dependencies, options) {
const conflicts = [];
for (const [depName, catalogDeps] of dependencies) {
for (const [catalogName, deps] of catalogDeps) {
const specs = [...new Set(deps.map((i) => i.specifier))];
if (specs.length > 1) {
const specifiers = sortSpecs(specs);
conflicts.push({
depName,
catalogName,
specifiers,
resolvedSpecifier: specifiers[0]
});
} else {
const dep = deps[0];
dependencies.get(dep.name).set(dep.catalogName, [dep]);
}
}
}
if (conflicts.length === 0)
return;
p.log.warn(`\u{1F4E6} Found ${c.yellow(conflicts.length)} dependencies that need manual version selection`);
for (const item of conflicts) {
if (options.yes)
continue;
const result = await p.select({
message: c.yellow(`${item.depName} (${item.catalogName}):`),
options: item.specifiers.map((i) => ({
label: i,
value: i
})),
initialValue: item.resolvedSpecifier
});
if (!result || p.isCancel(result)) {
p.outro(c.red("aborting"));
process.exit(1);
}
item.resolvedSpecifier = result;
}
for (const item of conflicts) {
const deps = dependencies.get(item.depName).get(item.catalogName);
const dep = deps.find((dep2) => dep2.specifier === item.resolvedSpecifier);
dependencies.get(item.depName).set(item.catalogName, [dep]);
}
}
async function resolveRemove(args, context) {
const { pnpmCatalogManager } = context;
await pnpmCatalogManager.loadPackages();
const { deps, isRecursive } = parsePnpmOptions(args);
if (!deps.length) {
p.outro(c.red("no dependencies provided, aborting"));
process.exit(1);
}
const nonCatalogDeps = [];
const dependencies = [];
const updatedPackages = /* @__PURE__ */ new Map();
for (const dep of deps) {
const packages = pnpmCatalogManager.getDepPackages(dep);
if (!packages.length) {
p.outro(c.red(`${dep} is not used in any package, aborting`));
process.exit(1);
}
let catalogPkgs = packages.filter((i) => pnpmCatalogManager.isCatalogPackageName(i));
if (catalogPkgs.length === 0) {
nonCatalogDeps.push(dep);
}
if (catalogPkgs.length > 1) {
const result = await p.multiselect({
message: `${c.cyan(dep)} found in multiple catalogs, please select the catalog to remove it from`,
options: catalogPkgs.map((i) => ({
label: i,
value: i
})),
initialValues: catalogPkgs
});
if (!result || p.isCancel(result)) {
p.outro(c.red("no catalog selected, aborting"));
process.exit(1);
}
catalogPkgs = catalogPkgs.filter((i) => result.includes(i));
}
await Promise.all(catalogPkgs.map(async (catalog) => {
const rawDep = pnpmCatalogManager.getCatalogDep(dep, catalog);
if (rawDep) {
const { catalogDeletable } = await pnpmCatalogManager.removePackageDep(dep, rawDep.catalogName, isRecursive, updatedPackages);
if (catalogDeletable)
dependencies.push(rawDep);
}
}));
}
if (nonCatalogDeps.length) {
p.outro(c.yellow(`${nonCatalogDeps.join(", ")} is not used in any catalog`));
await runPnpmRemove(nonCatalogDeps, { cwd: process.cwd(), recursive: isRecursive });
}
return {
dependencies,
updatedPackages: Object.fromEntries(updatedPackages.entries())
};
}
async function resolveRevert(args, context) {
const { pnpmCatalogManager } = context;
const { deps } = parsePnpmOptions(args);
let pkgName;
if (deps.length) {
const { pkgJson } = await readPackageJSON();
pkgName = pkgJson.name;
}
const depFilter = (depName) => {
if (!deps.length)
return true;
return deps.includes(depName);
};
const pkgFilter = (name) => {
if (!pkgName)
return true;
return pkgName === name;
};
const packages = await pnpmCatalogManager.loadPackages();
const dependencies = [];
const updatedPackages = /* @__PURE__ */ new Map();
const setPackage = (dep, pkg) => {
if (!pkgFilter(pkg.name))
return;
if (!updatedPackages.has(pkg.name))
updatedPackages.set(pkg.name, structuredClone(pkg));
const pkgJson = updatedPackages.get(pkg.name);
pkgJson.raw[dep.source][dep.name] = dep.specifier;
};
for (const pkg of packages) {
if (pkg.type === "pnpm-workspace.yaml")
continue;
for (const dep of pkg.deps) {
if (!depFilter(dep.name))
continue;
const resolvedDep = pnpmCatalogManager.resolveDep(dep);
dependencies.push(resolvedDep);
setPackage(resolvedDep, pkg);
}
}
return {
isRevertAll: !deps.length,
dependencies,
updatedPackages: Object.fromEntries(updatedPackages.entries())
};
}
async function addCommand(options) {
const args = process.argv.slice(3);
if (args.length === 0) {
p.outro(c.red("no dependencies provided, aborting"));
process.exit(1);
}
const { pkgJson, pkgPath } = await readPackageJSON();
const { workspaceYaml, workspaceYamlPath } = await ensureWorkspaceYAML();
const pnpmCatalogManager = new PnpmCatalogManager(options);
const { isDev = false, dependencies = [] } = await resolveAdd(args, {
options,
pnpmCatalogManager,
workspaceYaml
});
const depsName = isDev ? "devDependencies" : "dependencies";
const depNameOppsite = isDev ? "dependencies" : "devDependencies";
const deps = pkgJson[depsName] ||= {};
for (const dep of dependencies) {
deps[dep.name] = dep.catalogName ? dep.catalogName === "default" ? "catalog:" : `catalog:${dep.catalogName}` : dep.specifier || "^0.0.0";
if (pkgJson[depNameOppsite]?.[dep.name])
delete pkgJson[depNameOppsite][dep.name];
}
const updatedPackages = {
[pkgJson.name]: { filepath: pkgPath, raw: pkgJson }
};
await confirmWorkspaceChanges(
async () => {
for (const dep of dependencies) {
if (dep.catalogName)
workspaceYaml.setPackage(dep.catalogName, dep.name, dep.specifier || "^0.0.0");
}
},
{
pnpmCatalogManager,
workspaceYaml,
workspaceYamlPath,
updatedPackages,
yes: options.yes,
verbose: options.verbose,
bailout: false,
completeMessage: "add complete"
}
);
}
async function cleanCommand(options) {
const workspaceYamlPath = await findWorkspaceYAML();
if (!workspaceYamlPath) {
p.outro(c.red("no pnpm-workspace.yaml found, aborting"));
process.exit(1);
}
const pnpmCatalogManager = new PnpmCatalogManager(options);
const { dependencies = [] } = await resolveClean({
pnpmCatalogManager
});
if (!dependencies.length) {
p.outro(c.yellow("no dependencies to clean, aborting"));
process.exit(0);
}
const { workspaceYaml } = await ensureWorkspaceYAML();
p.log.info(`\u{1F4E6} Found ${c.yellow(dependencies.length)} dependencies not in package.json`);
await confirmWorkspaceChanges(
async () => {
removeWorkspaceYAMLDeps(dependencies, workspaceYaml);
},
{
pnpmCatalogManager,
workspaceYaml,
workspaceYamlPath,
yes: options.yes,
verbose: options.verbose,
bailout: true,
completeMessage: "clean complete"
}
);
}
const MIN_DEP_NAME_WIDTH = 12;
const MIN_DEP_TYPE_WIDTH = 6;
const MIN_SPECIFIER_WIDTH = 10;
const MIN_CATALOG_WIDTH = 10;
function renderChanges(deps, updatedPackages) {
if (!deps.length) {
return "";
}
let maxDepNameWidth = MIN_DEP_NAME_WIDTH;
let maxDepTypeWidth = MIN_DEP_TYPE_WIDTH;
let maxSpecifierWidth = MIN_SPECIFIER_WIDTH;
let maxCatalogWidth = MIN_CATALOG_WIDTH;
for (const dep of deps) {
maxDepNameWidth = Math.max(maxDepNameWidth, dep.name.length);
maxDepTypeWidth = Math.max(maxDepTypeWidth, DEPENDENCIES_TYPE_SHORT_MAP[dep.source].length);
maxSpecifierWidth = Math.max(maxSpecifierWidth, (dep.specifier || "").length);
maxCatalogWidth = Math.max(maxCatalogWidth, dep.catalogName.length);
}
const depsByPackage = /* @__PURE__ */ new Map();
for (const dep of deps) {
for (const [pkgName, pkgMeta] of Object.entries(updatedPackages)) {
if (pkgMeta.deps.some((d) => d.name === dep.name && d.source === dep.source)) {
if (!depsByPackage.has(pkgName)) {
depsByPackage.set(pkgName, []);
}
depsByPackage.get(pkgName).push(dep);
break;
}
}
}
const lines = [];
for (const [pkgName, pkgMeta] of Object.entries(updatedPackages)) {
const pkgDeps = depsByPackage.get(pkgName) || [];
if (pkgDeps.length === 0)
continue;
lines.push(`${c.cyan(pkgName)} ${c.dim(pkgMeta.relative)}`);
lines.push("");
for (const dep of pkgDeps) {
const depName = dep.name.padEnd(maxDepNameWidth);
const depType = DEPENDENCIES_TYPE_SHORT_MAP[dep.source].padEnd(maxDepTypeWidth);
const depSpecifier = (dep.specifier || "").padStart(maxSpecifierWidth);
const catalogRef = (dep.catalogName === "default" ? "" : dep.catalogName).padEnd(maxCatalogWidth);
lines.push(` ${depName} ${c.dim(depType)} ${c.red(depSpecifier)} ${c.dim("\u2192")} catalog:${c.reset(c.green(catalogRef))}`);
}
lines.push("");
}
const pkgCount = Object.keys(updatedPackages).length;
lines.push(`${c.yellow(pkgCount)} package${pkgCount > 1 ? "s" : ""} ${c.yellow(deps.length)} dependenc${deps.length > 1 ? "ies" : "y"}`);
return lines.join("\n");
}
async function detectCommand(options) {
const pnpmCatalogManager = new PnpmCatalogManager(options);
const { dependencies = [], updatedPackages = {} } = await resolveMigrate({
options,
pnpmCatalogManager
});
const deps = dependencies.filter((i) => i.update);
if (!deps.length) {
p.outro(c.yellow("no dependencies to migrate, aborting"));
process.exit(0);
}
p.log.info(`\u{1F4E6} Found ${c.yellow(deps.length)} dependencies to migrate`);
let result = renderChanges(deps, updatedPackages);
if (result) {
result += `
run ${c.green("pncat migrate")}${options.force ? c.green(" -f") : ""} to apply changes`;
p.note(c.reset(result));
}
p.outro(c.green("detect complete"));
}
async function migrateCommand(options) {
const pnpmCatalogManager = new PnpmCatalogManager(options);
const { dependencies = [], updatedPackages = {} } = await resolveMigrate({
options,
pnpmCatalogManager
});
const { workspaceYaml, workspaceYamlPath } = await ensureWorkspaceYAML();
await confirmWorkspaceChanges(
async () => {
generateWorkspaceYAML(dependencies, workspaceYaml);
},
{
pnpmCatalogManager,
workspaceYaml,
workspaceYamlPath,
updatedPackages,
yes: options.yes,
verbose: options.verbose,
bailout: true,
completeMessage: "migrate complete"
}
);
}
async function removeCommand(options) {
const args = process.argv.slice(3);
if (args.length === 0) {
p.outro(c.red("no dependencies provided, aborting"));
process.exit(1);
}
const workspaceYamlPath = await findWorkspaceYAML();
if (!workspaceYamlPath) {
p.outro(c.red("no pnpm-workspace.yaml found, aborting"));
process.exit(1);
}
const { workspaceYaml } = await ensureWorkspaceYAML();
const pnpmCatalogManager = new PnpmCatalogManager(options);
const { dependencies = [], updatedPackages = {} } = await resolveRemove(args, {
pnpmCatalogManager});
await confirmWorkspaceChanges(
async () => {
removeWorkspaceYAMLDeps(dependencies, workspaceYaml);
},
{
pnpmCatalogManager,
workspaceYaml,
workspaceYamlPath,
updatedPackages,
yes: options.yes,
verbose: options.verbose,
bailout: false,
completeMessage: "remove complete"
}
);
}
async function revertCommand(options) {
const args = process.argv.slice(3);
const workspaceYamlPath = await findWorkspaceYAML();
if (!workspaceYamlPath) {
p.outro(c.red("no pnpm-workspace.yaml found, aborting"));
process.exit(1);
}
const { workspaceYaml } = await ensureWorkspaceYAML();
const pnpmCatalogManager = new PnpmCatalogManager(options);
const { isRevertAll, dependencies = [], updatedPackages = {} } = await resolveRevert(args, {
pnpmCatalogManager});
if (isRevertAll) {
if (!options.yes) {
const result = await p.confirm({
message: c.green("all catalog dependencies will be reverted, are you sure?")
});
if (!result || p.isCancel(result)) {
p.outro(c.red("aborting"));
process.exit(1);
}
}
const document = workspaceYaml.getDocument();
document.deleteIn(["catalog"]);
document.deleteIn(["catalogs"]);
await writePnpmWorkspace(workspaceYamlPath, workspaceYaml.toString());
await writePackageJSONs(updatedPackages);
} else {
await confirmWorkspaceChanges(
async () => {
removeWorkspaceYAMLDeps(dependencies, workspaceYaml);
},
{
pnpmCatalogManager,
workspaceYaml,
workspaceYamlPath,
updatedPackages,
yes: options.yes,
verbose: options.verbose,
bailout: true,
completeMessage: "revert complete"
}
);
}
}
function normalizeConfig(options) {
if ("default" in options)
options = options.default;
return options;
}
async function resolveConfig(options) {
const defaults = structuredClone(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();
const configOptions = config.sources.length ? normalizeConfig(config.config) : {};
const catalogRules = configOptions.catalogRules || structuredClone(DEFAULT_CATALOG_RULES) || [];
delete configOptions.catalogRules;
const merged = deepmerge(deepmerge(defaults, configOptions), options);
merged.cwd = merged.cwd || await findWorkspaceRoot();
if (typeof merged.catalog === "boolean")
delete merged.catalog;
merged.catalogRules = catalogRules;
return merged;
}
try {
const cli = cac(name);
cli.command("[mode]", "Enhanced pnpm catalogs management with advanced workspace dependency control").option("--catalog [name]", "Install from a specific catalog, auto detect if not provided").option("--recursive, -r", "Recursively search for package.json in subdirectories").option("--force, -f", "Force cataloging according to rules, ignoring original configurations").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("--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.)").option("--yes", "Skip prompt confirmation").option("--install", "Run pnpm install after command").option("--verbose", "Show complete pnpm-workspace.yaml instead of only the diff").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`${name} `}${c.dim`v${version}`}`);
const config = await resolveConfig(options);
switch (config.mode) {
case "detect":
await detectCommand(config);
break;
case "migrate":
await migrateCommand(config);
break;
case "add":
await addCommand(config);
break;
case "remove":
await removeCommand(config);
break;
case "clean":
await cleanCommand(config);
break;
case "revert":
await revertCommand(config);
break;
}
});
cli.help();
cli.version(version);
cli.parse();
} catch (error) {
console.error(error);
process.exit(1);
}