diff --git a/packages/cli/catalog.test.ts b/packages/cli/catalog.test.ts new file mode 100644 index 00000000..014a9394 --- /dev/null +++ b/packages/cli/catalog.test.ts @@ -0,0 +1,53 @@ +import { it, expect } from "vitest"; +import { PackageJson } from "pkg-types"; +import { createResolver } from "./catalog"; + +it("pnpm catalogs", () => { + const json: PackageJson = { + dependencies: { + a: "catalog:", + b: "catalog:named", + }, + devDependencies: { + c: "catalog:", + d: "catalog:named", + }, + peerDependencies: { + e: "catalog:default", + f: "catalog:named1", + }, + }; + + createResolver({ + catalog: { + a: "1.0.0", + c: "2.0.0", + e: "3.0.0", + }, + catalogs: { + named: { + b: "1.0.0", + d: "2.0.0", + }, + named1: { + f: "1.0.0", + }, + }, + })(json); + expect(json).toMatchInlineSnapshot(` + { + "dependencies": { + "a": "1.0.0", + "b": "1.0.0", + }, + "devDependencies": { + "c": "2.0.0", + "d": "2.0.0", + }, + "peerDependencies": { + "e": "3.0.0", + "f": "1.0.0", + }, + } + `); +}); diff --git a/packages/cli/catalog.ts b/packages/cli/catalog.ts new file mode 100644 index 00000000..c2e64cc8 --- /dev/null +++ b/packages/cli/catalog.ts @@ -0,0 +1,66 @@ +import fs from "node:fs"; +import path from "node:path"; +import readYamlFile from "read-yaml-file"; +import { PackageJson } from "pkg-types"; + +interface PnpmWorkspaceYaml { + catalog?: Record; + catalogs?: Record>; +} + +// https://github.com/pnpm/rfcs/blob/main/text/0001-catalogs.md +export function createResolver(rootYaml: PnpmWorkspaceYaml) { + const catalog = rootYaml.catalog ?? {}; + const catalogs = rootYaml.catalogs ?? {}; + + function resolveVersion(name: string, version: string) { + if (!version.startsWith("catalog:")) { + return version; + } + + const useCatalog = version.slice("catalog:".length).trim(); + if (useCatalog.length === 0 || useCatalog === "default") { + const catalogVersion = catalog[name]; + if (!catalogVersion) { + throw new Error(`Missing pnpm catalog version for ${name}`); + } + return catalogVersion; + } + + const catalogVersion = catalogs[useCatalog]?.[name]; + if (!catalogVersion) { + throw new Error( + `Missing pnpm catalog version for ${name} in catalogs.${useCatalog}`, + ); + } + return catalogVersion; + } + + function resolveCatalogVersions(pJson: PackageJson) { + // TODO: should we also include overrides, resolutions, and pnpm.overrides? + for (const depObjKey of [ + "dependencies", + "devDependencies", + "peerDependencies", + ]) { + const depObj = pJson[depObjKey]; + if (!depObj) { + continue; + } + for (const depName of Object.keys(depObj)) { + depObj[depName] = resolveVersion(depName, depObj[depName]); + } + } + } + + return resolveCatalogVersions; +} + +export async function loadCatalogs(root = process.cwd()) { + const pnpmWorkspace = path.resolve(root, "pnpm-workspace.yaml"); + if (!fs.existsSync(pnpmWorkspace)) { + return undefined; + } + + return createResolver(await readYamlFile(pnpmWorkspace)); +} diff --git a/packages/cli/index.ts b/packages/cli/index.ts index 5f964014..238b4eaa 100644 --- a/packages/cli/index.ts +++ b/packages/cli/index.ts @@ -20,6 +20,7 @@ import pkg from "./package.json" with { type: "json" }; import { isBinaryFile } from "isbinaryfile"; import { writePackageJSON, type PackageJson } from "pkg-types"; import { createDefaultTemplate } from "./template"; +import { loadCatalogs } from "./catalog"; declare global { var API_URL: string; @@ -314,6 +315,9 @@ const main = defineCommand({ } } + const resolveCatalogVersions = isPnpm + ? await loadCatalogs(process.cwd()) + : null; const restoreMap = new Map< string, Awaited> @@ -333,6 +337,8 @@ const main = defineCommand({ continue; } + resolveCatalogVersions?.(pJson); + restoreMap.set( p, await writeDeps(p, pJsonContents, pJson, deps, realDeps), diff --git a/packages/cli/package.json b/packages/cli/package.json index f7b177ee..c9b757b5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -27,6 +27,7 @@ "isbinaryfile": "^5.0.2", "pkg-types": "^1.1.1", "query-registry": "^3.0.1", + "read-yaml-file": "^2.1.0", "tinyglobby": "^0.2.9" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2a93223..1b57819a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -155,6 +155,9 @@ importers: query-registry: specifier: ^3.0.1 version: 3.0.1 + read-yaml-file: + specifier: ^2.1.0 + version: 2.1.0 tinyglobby: specifier: ^0.2.9 version: 0.2.9 @@ -3520,6 +3523,10 @@ packages: resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} engines: {node: '>=8'} + read-yaml-file@2.1.0: + resolution: {integrity: sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==} + engines: {node: '>=10.13'} + readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} @@ -3854,6 +3861,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + strip-final-newline@2.0.0: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} @@ -7641,6 +7652,11 @@ snapshots: parse-json: 5.2.0 type-fest: 0.6.0 + read-yaml-file@2.1.0: + dependencies: + js-yaml: 4.1.0 + strip-bom: 4.0.0 + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -8045,6 +8061,8 @@ snapshots: strip-bom@3.0.0: {} + strip-bom@4.0.0: {} + strip-final-newline@2.0.0: {} strip-final-newline@3.0.0: {}