Skip to content

Instantly share code, notes, and snippets.

@Armadillidiid
Created June 7, 2026 16:29
Show Gist options
  • Select an option

  • Save Armadillidiid/875fdbe62ce5a67ed13e6191c703be90 to your computer and use it in GitHub Desktop.

Select an option

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.
#!/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