1import * as Schema from "effect/Schema";
2
3class CatalogDependencyResolutionError extends Schema.TaggedError<CatalogDependencyResolutionError>()(
4 "CatalogDependencyResolutionError",
5 {
6 workspacePackage: Schema.String,
7 dependencyName: Schema.String,
8 catalogSpec: Schema.String,
9 catalogKey: Schema.String,
10 },
11) {
12 override get message(): string {
13 return `Unable to resolve '${this.catalogSpec}' for ${this.workspacePackage} dependency '${this.dependencyName}'. Expected key '${this.catalogKey}' in root workspace catalog.`;
14 }
15}
16
17/**
18 * Resolve `catalog:` dependency specs using the workspace catalog.
19 *
20 * Pure function: returns a new record with every `catalog:…` value replaced by
21 * the concrete version string found in `catalog`. Throws on missing entries.
22 */
23export function resolveCatalogDependencies(
24 dependencies: Record<string, string>,
25 catalog: Record<string, string>,
26 workspacePackage: string,
27): Record<string, string> {
28 return Object.fromEntries(
29 Object.entries(dependencies).map(([name, spec]) => {
30 if (typeof spec !== "string" || !spec.startsWith("catalog:")) {
31 return [name, spec];
32 }
33
34 const catalogKey = spec.slice("catalog:".length).trim();
35 // Overrides can include a parent selector and a version range. A bare
36 // `catalog:` looks up the final package name, keeping its scope intact.
37 const selector = name.split(">").at(-1) ?? name;
38 const versionIndex = selector.indexOf("@", 1);
39 const packageName = versionIndex === -1 ? selector : selector.slice(0, versionIndex);
40 const lookupKey = catalogKey.length > 0 ? catalogKey : packageName;
41 const resolved = catalog[lookupKey];
42
43 if (typeof resolved !== "string" || resolved.length === 0) {
44 throw new CatalogDependencyResolutionError({
45 workspacePackage,
46 dependencyName: name,
47 catalogSpec: spec,
48 catalogKey: lookupKey,
49 });
50 }
51
52 return [name, resolved];
53 }),
54 );
55}