UNPKG

pncat

Version:

Enhanced pnpm catalogs management with advanced workspace dependency control.

907 lines (886 loc) 30.9 kB
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); }