Skip to content

Commit 25c678c

Browse files
committed
fix: handle multi-ecosystem collisions in path-to-key maps and package lookups
Several places used one-to-one Map<path, key> or find() that silently dropped entries when multiple ecosystems shared the same directory. Changed to one-to-many maps and filter() to handle all ecosystems. Also added name ambiguity detection in createKeyResolver and fixed fallback displays leaking internal path::ecosystem format.
1 parent b30aaaa commit 25c678c

File tree

7 files changed

+72
-35
lines changed

7 files changed

+72
-35
lines changed

packages/core/src/changeset/resolve.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,32 @@ import { packageKey } from "../utils/package-key.js";
44
export function createKeyResolver(
55
packages: { name: string; path: string; ecosystem: EcosystemKey }[],
66
): (key: string) => string {
7-
const nameToKey = new Map(packages.map((p) => [p.name, packageKey(p)]));
87
const validKeys = new Set(packages.map((p) => packageKey(p)));
98
const pathEcosystems = new Map<string, EcosystemKey[]>();
9+
const nameEcosystems = new Map<string, EcosystemKey[]>();
1010
for (const p of packages) {
11-
const existing = pathEcosystems.get(p.path) ?? [];
12-
existing.push(p.ecosystem);
13-
pathEcosystems.set(p.path, existing);
11+
const existingPath = pathEcosystems.get(p.path) ?? [];
12+
existingPath.push(p.ecosystem);
13+
pathEcosystems.set(p.path, existingPath);
14+
15+
const existingName = nameEcosystems.get(p.name) ?? [];
16+
existingName.push(p.ecosystem);
17+
nameEcosystems.set(p.name, existingName);
1418
}
1519

1620
return (key: string): string => {
1721
if (validKeys.has(key)) return key;
18-
const fromName = nameToKey.get(key);
19-
if (fromName) return fromName;
22+
const nameEcos = nameEcosystems.get(key);
23+
if (nameEcos) {
24+
if (nameEcos.length === 1) {
25+
const pkg = packages.find((p) => p.name === key);
26+
if (pkg) return packageKey(pkg);
27+
}
28+
throw new Error(
29+
`Ambiguous changeset key "${key}": name is shared across ecosystems (${nameEcos.join(", ")}). ` +
30+
`Use the path::ecosystem format to specify.`,
31+
);
32+
}
2033
const ecosystems = pathEcosystems.get(key);
2134
if (ecosystems) {
2235
if (ecosystems.length === 1) {

packages/core/src/tasks/prompts/independent-mode.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,12 @@ export async function handleMultiPackage(
6363
): Promise<void> {
6464
const graph = buildGraphFromPackages(packageInfos);
6565
const currentVersions = new Map(packageInfos.map((p) => [p.path, p.version]));
66-
const pathToKey = new Map(packageInfos.map((p) => [p.path, packageKey(p)]));
66+
const pathToKeys = new Map<string, string[]>();
67+
for (const p of packageInfos) {
68+
const existing = pathToKeys.get(p.path) ?? [];
69+
existing.push(packageKey(p));
70+
pathToKeys.set(p.path, existing);
71+
}
6772
const recommendations = await analyzeAllSources(ctx);
6873

6974
// CI mode: auto-accept
@@ -74,17 +79,19 @@ export async function handleMultiPackage(
7479
if (!current) continue;
7580
const newVer = semver.inc(current, rec.bumpType);
7681
if (newVer) {
77-
const key = pathToKey.get(rec.packagePath) ?? rec.packagePath;
78-
packages.set(key, newVer);
82+
const keys = pathToKeys.get(rec.packagePath) ?? [rec.packagePath];
83+
for (const key of keys) {
84+
packages.set(key, newVer);
85+
}
7986
}
8087
}
8188
ctx.runtime.versionPlan = buildVersionPlan(
8289
ctx.config.versioning ?? "independent",
8390
packages,
8491
);
8592
ctx.runtime.changesetConsumed = recommendations.some((r) => {
86-
const key = pathToKey.get(r.packagePath) ?? r.packagePath;
87-
return r.source === "changeset" && packages.has(key);
93+
const keys = pathToKeys.get(r.packagePath) ?? [r.packagePath];
94+
return r.source === "changeset" && keys.some((k) => packages.has(k));
8895
});
8996
return;
9097
}
@@ -127,8 +134,10 @@ export async function handleMultiPackage(
127134
if (!current) continue;
128135
const newVer = semver.inc(current, rec.bumpType);
129136
if (newVer) {
130-
const key = pathToKey.get(rec.packagePath) ?? rec.packagePath;
131-
selectedVersions.set(key, newVer);
137+
const keys = pathToKeys.get(rec.packagePath) ?? [rec.packagePath];
138+
for (const key of keys) {
139+
selectedVersions.set(key, newVer);
140+
}
132141
}
133142
}
134143
} else {
@@ -178,8 +187,8 @@ export async function handleMultiPackage(
178187
ctx.runtime.changesetConsumed = recommendations.some((r) => {
179188
if (r.source !== "changeset" || !plan || !("packages" in plan))
180189
return false;
181-
const key = pathToKey.get(r.packagePath) ?? r.packagePath;
182-
return plan.packages.has(key);
190+
const keys = pathToKeys.get(r.packagePath) ?? [r.packagePath];
191+
return keys.some((k) => plan.packages.has(k));
183192
});
184193
return;
185194
}
@@ -191,8 +200,8 @@ export async function handleMultiPackage(
191200
selectedVersions,
192201
);
193202
ctx.runtime.changesetConsumed = recommendations.some((r) => {
194-
const key = pathToKey.get(r.packagePath) ?? r.packagePath;
195-
return r.source === "changeset" && selectedVersions.has(key);
203+
const keys = pathToKeys.get(r.packagePath) ?? [r.packagePath];
204+
return r.source === "changeset" && keys.some((k) => selectedVersions.has(k));
196205
});
197206
}
198207

@@ -330,8 +339,8 @@ export async function handleIndependentMode(
330339
currentVersions.get(pkgPath) ??
331340
(packageVersionByPath.get(pkgPath) as string);
332341
const patchVersion = new SemVer(currentVersion).inc("patch").toString();
333-
const pkg = packageInfos.find((p) => p.path === pkgPath);
334-
if (pkg) {
342+
const pkgs = packageInfos.filter((p) => p.path === pkgPath);
343+
for (const pkg of pkgs) {
335344
versions.set(packageKey(pkg), patchVersion);
336345
}
337346
}

packages/core/src/tasks/runner-utils/rollback-handlers.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ export function isReleaseExcluded(
1818
}
1919

2020
export function getPackageName(ctx: PubmContext, key: string): string {
21-
return ctx.config.packages.find((p) => packageKey(p) === key)?.name ?? key;
21+
return (
22+
ctx.config.packages.find((p) => packageKey(p) === key)?.name ??
23+
pathFromKey(key)
24+
);
2225
}
2326

2427
export function requireVersionPlan(ctx: PubmContext) {

packages/core/src/tasks/runner-utils/version-pr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export function buildPrBodyFromContext(
8686
if (plan.mode === "independent") {
8787
for (const [key, pkgVersion] of plan.packages) {
8888
const pkgConfig = ctx.config.packages.find((p) => packageKey(p) === key);
89-
const name = pkgConfig?.name ?? key;
89+
const name = pkgConfig?.name ?? pathFromKey(key);
9090
packages.push({ name, version: pkgVersion, bump: "" });
9191

9292
const changelogDir = pkgConfig

packages/core/src/tasks/snapshot-runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ export async function runSnapshotPipeline(
220220
if (plan.mode === "independent") {
221221
for (const [key, pkgVersion] of plan.packages) {
222222
const pkgName =
223-
ctx.config.packages.find((p) => p.path === pathFromKey(key))
223+
ctx.config.packages.find((p) => packageKey(p) === key)
224224
?.name ?? pathFromKey(key);
225225
const tagName = `${pkgName}@${pkgVersion}`;
226226
task.output = t("task.snapshot.creatingTag", { tag: tagName });

packages/pubm/src/cli.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -320,10 +320,13 @@ export function createProgram(): Command {
320320
const currentVersions = new Map(
321321
resolvedConfig.packages.map((p) => [p.path, p.version]),
322322
);
323-
// pathToKey maps filesystem path → packageKey for building the version plan
324-
const pathToKey = new Map(
325-
resolvedConfig.packages.map((p) => [p.path, packageKey(p)]),
326-
);
323+
// pathToKeys maps filesystem path → packageKey[] for building the version plan
324+
const pathToKeys = new Map<string, string[]>();
325+
for (const p of resolvedConfig.packages) {
326+
const existing = pathToKeys.get(p.path) ?? [];
327+
existing.push(packageKey(p));
328+
pathToKeys.set(p.path, existing);
329+
}
327330

328331
const sources: VersionSource[] = [];
329332
const versionSources = resolvedConfig.versionSources ?? "all";
@@ -355,8 +358,14 @@ export function createProgram(): Command {
355358
if (!currentVersion) continue;
356359
const newVersion = semver.inc(currentVersion, rec.bumpType);
357360
// Use packageKey as the map key for the version plan
358-
const key = pathToKey.get(rec.packagePath) ?? rec.packagePath;
359-
if (newVersion) packages.set(key, newVersion);
361+
const keys = pathToKeys.get(rec.packagePath) ?? [
362+
rec.packagePath,
363+
];
364+
if (newVersion) {
365+
for (const key of keys) {
366+
packages.set(key, newVersion);
367+
}
368+
}
360369
}
361370

362371
if (packages.size === 1) {

packages/pubm/src/commands/version-cmd.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,16 @@ export async function runVersionCommand(
8989
if (!currentVersion) continue;
9090
const newVersion = inc(currentVersion, rec.bumpType);
9191
if (!newVersion) continue;
92-
const pkg = config.packages.find((p) => p.path === rec.packagePath);
93-
if (!pkg) continue;
94-
bumps.set(packageKey(pkg), {
95-
currentVersion,
96-
newVersion,
97-
bumpType: rec.bumpType,
98-
});
92+
const matchingPkgs = config.packages.filter(
93+
(p) => p.path === rec.packagePath,
94+
);
95+
for (const pkg of matchingPkgs) {
96+
bumps.set(packageKey(pkg), {
97+
currentVersion,
98+
newVersion,
99+
bumpType: rec.bumpType,
100+
});
101+
}
99102
}
100103

101104
if (bumps.size === 0) {

0 commit comments

Comments
 (0)