Created
June 7, 2026 16:29
-
-
Save Armadillidiid/875fdbe62ce5a67ed13e6191c703be90 to your computer and use it in GitHub Desktop.
Resolves pnpm `catalog:` protocol specifiers in workspace `package.json` files into concrete versions from `pnpm-lock.yaml`. Runs at the start of Amplify's build because Amplify Node.js images don't support pnpm catalogs natively — rewriting them to literal versions lets `pnpm install --frozen-lockfile` work.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| // oxlint-disable typescript/no-unsafe-assignment, typescript/consistent-type-assertions | |
| import { readFileSync, readdirSync, writeFileSync, existsSync } from "fs"; | |
| import { join } from "path"; | |
| import yaml from "yaml"; | |
| interface CatalogEntry { | |
| specifier?: string; | |
| version?: string; | |
| } | |
| interface Catalogs { | |
| [packageName: string]: CatalogEntry; | |
| } | |
| interface Lockfile { | |
| catalogs?: { | |
| default?: Catalogs; | |
| }; | |
| } | |
| interface PackageJson { | |
| dependencies?: Record<string, string>; | |
| devDependencies?: Record<string, string>; | |
| } | |
| interface WorkspaceConfig { | |
| packages?: string[]; | |
| } | |
| function parseLockfile(content: string): Lockfile | undefined { | |
| const parsed = yaml.parse(content); | |
| if (typeof parsed !== "object" || parsed === null) return undefined; | |
| return parsed as Lockfile; | |
| } | |
| function parsePackageJson(content: string): PackageJson { | |
| const parsed = JSON.parse(content); | |
| return parsed as PackageJson; | |
| } | |
| function parseWorkspace(content: string): string[] { | |
| const parsed = yaml.parse(content) as WorkspaceConfig; | |
| return parsed.packages ?? []; | |
| } | |
| function resolveCatalog(specifier: string, pkgName: string): string { | |
| if (specifier.startsWith("catalog:")) { | |
| const catalogPkg = specifier.slice(8) || pkgName; | |
| if (!Object.hasOwn(catalogs, catalogPkg)) { | |
| console.warn( | |
| `Warning: Catalog entry for ${catalogPkg} not found, skipping`, | |
| ); | |
| return specifier; | |
| } | |
| const entry = catalogs[catalogPkg]; | |
| return entry.specifier || entry.version || specifier; | |
| } | |
| return specifier; | |
| } | |
| function processPackageJson(filePath: string): void { | |
| const pkg = parsePackageJson(readFileSync(filePath, "utf-8")); | |
| let modified = false; | |
| for (const key of ["dependencies", "devDependencies"] as const) { | |
| const deps = pkg[key]; | |
| if (!deps) continue; | |
| for (const [name, specifier] of Object.entries(deps)) { | |
| if (typeof specifier === "string" && specifier.startsWith("catalog:")) { | |
| deps[name] = resolveCatalog(specifier, name); | |
| modified = true; | |
| } | |
| } | |
| } | |
| if (modified) { | |
| writeFileSync(filePath, JSON.stringify(pkg, null, 2) + "\n"); | |
| console.log(`Resolved catalogs in ${filePath}`); | |
| } | |
| } | |
| function resolvePackages(pattern: string): string[] { | |
| const parts = pattern.split("/"); | |
| const results: string[] = []; | |
| if (parts.length === 1) { | |
| const pkgJson = join(workspaceRoot, parts[0], "package.json"); | |
| if (existsSync(pkgJson)) results.push(pkgJson); | |
| return results; | |
| } | |
| if (parts[1] === "*") { | |
| const dirPath = join(workspaceRoot, parts[0]); | |
| if (!existsSync(dirPath)) return results; | |
| for (const entry of readdirSync(dirPath, { withFileTypes: true })) { | |
| if (entry.isDirectory()) { | |
| const pkgJson = join( | |
| workspaceRoot, | |
| parts[0], | |
| entry.name, | |
| "package.json", | |
| ); | |
| if (existsSync(pkgJson)) results.push(pkgJson); | |
| } | |
| } | |
| return results; | |
| } | |
| if (parts.length === 2) { | |
| const pkgJson = join(workspaceRoot, parts[0], parts[1], "package.json"); | |
| if (existsSync(pkgJson)) results.push(pkgJson); | |
| return results; | |
| } | |
| const dirPath = join(workspaceRoot, parts[0], parts[1]); | |
| if (!existsSync(dirPath)) return results; | |
| for (const entry of readdirSync(dirPath, { withFileTypes: true })) { | |
| if (entry.isDirectory()) { | |
| const pkgJson = join( | |
| workspaceRoot, | |
| parts[0], | |
| parts[1], | |
| entry.name, | |
| "package.json", | |
| ); | |
| if (existsSync(pkgJson)) results.push(pkgJson); | |
| } | |
| } | |
| return results; | |
| } | |
| const workspaceRoot = process.cwd(); | |
| const lockfilePath = join(workspaceRoot, "pnpm-lock.yaml"); | |
| const workspacePath = join(workspaceRoot, "pnpm-workspace.yaml"); | |
| const lockfileParsed = parseLockfile(readFileSync(lockfilePath, "utf-8")); | |
| const catalogs: Catalogs = lockfileParsed?.catalogs?.default ?? {}; | |
| const workspacePatterns = parseWorkspace(readFileSync(workspacePath, "utf-8")); | |
| const packageJsonFiles = [ | |
| join(workspaceRoot, "package.json"), | |
| ...workspacePatterns.flatMap(resolvePackages), | |
| ]; | |
| for (const filePath of packageJsonFiles) { | |
| processPackageJson(filePath); | |
| } | |
| console.log("Done resolving catalogs."); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment