Skip to content

Commit 0e3d9ac

Browse files
authored
Merge pull request #20 from syi0808/feat/multi-ecosystem-detection
feat: support multi-ecosystem detection in a single directory
2 parents a367cca + d633b47 commit 0e3d9ac

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1281
-601
lines changed

packages/core/src/changeset/changelog.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { packageKey } from "../utils/package-key.js";
12
import type { BumpType, Changeset } from "./parser.js";
23

34
export { writeChangelogToFile } from "../changelog/file.js";
@@ -74,13 +75,16 @@ export function deduplicateEntries(
7475

7576
export function buildChangelogEntries(
7677
changesets: Changeset[],
77-
packagePath: string,
78+
targetKey: string,
7879
): ChangelogEntry[] {
7980
const entries: ChangelogEntry[] = [];
8081

8182
for (const changeset of changesets) {
8283
for (const release of changeset.releases) {
83-
if (release.path === packagePath) {
84+
const releaseKey = release.ecosystem
85+
? packageKey({ path: release.path, ecosystem: release.ecosystem })
86+
: release.path;
87+
if (releaseKey === targetKey || release.path === targetKey) {
8488
entries.push({
8589
summary: changeset.summary,
8690
type: release.type,

packages/core/src/changeset/parser.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { parse as parseYaml } from "yaml";
2+
import type { EcosystemKey } from "../ecosystem/catalog.js";
23

34
export type BumpType = "patch" | "minor" | "major";
45

56
export interface Release {
67
path: string;
8+
ecosystem?: EcosystemKey;
79
type: BumpType;
810
}
911

@@ -43,8 +45,26 @@ export function parseChangeset(
4345
`Invalid bump type "${type}" for package "${key}" in "${fileName}". Expected: patch, minor, or major.`,
4446
);
4547
}
46-
const path = resolveKey ? resolveKey(key) : key;
47-
releases.push({ path, type: type as BumpType });
48+
const resolvedKey = resolveKey ? resolveKey(key) : key;
49+
50+
// Parse path::ecosystem format
51+
const separatorIndex = resolvedKey.lastIndexOf("::");
52+
let releasePath: string;
53+
let ecosystem: EcosystemKey | undefined;
54+
if (separatorIndex !== -1) {
55+
releasePath = resolvedKey.slice(0, separatorIndex);
56+
ecosystem = resolvedKey.slice(separatorIndex + 2);
57+
if (!releasePath || !ecosystem) {
58+
throw new Error(
59+
`Invalid package key "${resolvedKey}" in "${fileName}". Expected "path::ecosystem" with non-empty path and ecosystem.`,
60+
);
61+
}
62+
} else {
63+
releasePath = resolvedKey;
64+
ecosystem = undefined;
65+
}
66+
67+
releases.push({ path: releasePath, ecosystem, type: type as BumpType });
4868
}
4969
}
5070

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,45 @@
1-
import type { ResolvedPackageConfig } from "../config/types.js";
1+
import type { EcosystemKey } from "../ecosystem/catalog.js";
2+
import { packageKey } from "../utils/package-key.js";
23

34
export function createKeyResolver(
4-
packages: Pick<ResolvedPackageConfig, "name" | "path">[],
5+
packages: { name: string; path: string; ecosystem: EcosystemKey }[],
56
): (key: string) => string {
6-
const nameToPath = new Map(packages.map((p) => [p.name, p.path]));
7-
const validPaths = new Set(packages.map((p) => p.path));
7+
const validKeys = new Set(packages.map((p) => packageKey(p)));
8+
const pathEcosystems = new Map<string, EcosystemKey[]>();
9+
const nameEcosystems = new Map<string, EcosystemKey[]>();
10+
for (const p of packages) {
11+
const existingPath = pathEcosystems.get(p.path) ?? [];
12+
if (!existingPath.includes(p.ecosystem)) existingPath.push(p.ecosystem);
13+
pathEcosystems.set(p.path, existingPath);
14+
15+
const existingName = nameEcosystems.get(p.name) ?? [];
16+
if (!existingName.includes(p.ecosystem)) existingName.push(p.ecosystem);
17+
nameEcosystems.set(p.name, existingName);
18+
}
819

920
return (key: string): string => {
10-
if (validPaths.has(key)) return key;
11-
const resolved = nameToPath.get(key);
12-
if (resolved) return resolved;
21+
if (validKeys.has(key)) return key;
22+
const ecosystems = pathEcosystems.get(key);
23+
if (ecosystems) {
24+
if (ecosystems.length === 1) {
25+
return `${key}::${ecosystems[0]}`;
26+
}
27+
throw new Error(
28+
`Ambiguous changeset key "${key}": directory contains multiple ecosystems (${ecosystems.join(", ")}). ` +
29+
`Use "${key}::${ecosystems[0]}" or "${key}::${ecosystems[1]}" to specify.`,
30+
);
31+
}
32+
const nameEcos = nameEcosystems.get(key);
33+
if (nameEcos) {
34+
if (nameEcos.length === 1) {
35+
const pkg = packages.find((p) => p.name === key);
36+
if (pkg) return packageKey(pkg);
37+
}
38+
throw new Error(
39+
`Ambiguous changeset key "${key}": name is shared across ecosystems (${nameEcos.join(", ")}). ` +
40+
`Use the path::ecosystem format to specify.`,
41+
);
42+
}
1343
return key;
1444
};
1545
}

packages/core/src/changeset/status.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import process from "node:process";
2+
import { packageKey } from "../utils/package-key.js";
23
import { maxBump } from "./bump-utils.js";
34
import type { BumpType, Changeset } from "./parser.js";
45
import { readChangesets } from "./reader.js";
@@ -24,14 +25,17 @@ export function getStatus(
2425

2526
for (const changeset of changesets) {
2627
for (const release of changeset.releases) {
27-
const existing = packages.get(release.path);
28+
const key = release.ecosystem
29+
? packageKey({ path: release.path, ecosystem: release.ecosystem })
30+
: release.path;
31+
const existing = packages.get(key);
2832

2933
if (existing) {
3034
existing.bumpType = maxBump(existing.bumpType, release.type);
3135
existing.changesetCount += 1;
3236
existing.summaries.push(changeset.summary);
3337
} else {
34-
packages.set(release.path, {
38+
packages.set(key, {
3539
bumpType: release.type,
3640
changesetCount: 1,
3741
summaries: [changeset.summary],

packages/core/src/changeset/version.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import process from "node:process";
22
import { inc } from "semver";
3+
import { packageKey } from "../utils/package-key.js";
34
import { maxBump } from "./bump-utils.js";
45
import type { BumpType } from "./parser.js";
56
import { readChangesets } from "./reader.js";
@@ -20,13 +21,16 @@ export function calculateVersionBumps(
2021

2122
for (const changeset of changesets) {
2223
for (const release of changeset.releases) {
23-
if (!currentVersions.has(release.path)) continue;
24+
const key = release.ecosystem
25+
? packageKey({ path: release.path, ecosystem: release.ecosystem })
26+
: release.path;
27+
if (!currentVersions.has(key)) continue;
2428

25-
const existing = bumpTypes.get(release.path);
29+
const existing = bumpTypes.get(key);
2630
if (existing) {
27-
bumpTypes.set(release.path, maxBump(existing, release.type));
31+
bumpTypes.set(key, maxBump(existing, release.type));
2832
} else {
29-
bumpTypes.set(release.path, release.type);
33+
bumpTypes.set(key, release.type);
3034
}
3135
}
3236
}

packages/core/src/changeset/writer.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,10 @@ export function generateChangesetContent(
9090
if (releases.length > 0) {
9191
const yamlObj: Record<string, string> = {};
9292
for (const release of releases) {
93-
yamlObj[release.path] = release.type;
93+
const key = release.ecosystem
94+
? `${release.path}::${release.ecosystem}`
95+
: release.path;
96+
yamlObj[key] = release.type;
9497
}
9598
content += stringifyYaml(yamlObj);
9699
}

packages/core/src/config/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { CompressOption, ReleaseAssetEntry } from "../assets/types.js";
22
import type { BumpType } from "../changeset/parser.js";
3+
import type { EcosystemKey } from "../ecosystem/catalog.js";
34
import type { PubmPlugin } from "../plugin/types.js";
45
import type { RegistryType } from "../types/options.js";
56

@@ -26,11 +27,12 @@ export interface PackageConfig {
2627
}
2728

2829
export interface ResolvedPackageConfig
29-
extends Omit<PackageConfig, "registries"> {
30+
extends Omit<PackageConfig, "registries" | "ecosystem"> {
3031
name: string;
3132
version: string;
3233
dependencies: string[];
3334
registries: RegistryType[];
35+
ecosystem: EcosystemKey;
3436
registryVersions?: Map<RegistryType, string>;
3537
}
3638

packages/core/src/context.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { RollbackTracker } from "./utils/rollback.js";
88
export interface SingleVersionPlan {
99
mode: "single";
1010
version: string;
11-
packagePath: string;
11+
packageKey: string;
1212
}
1313

1414
export interface FixedVersionPlan {
@@ -42,15 +42,12 @@ export function resolveVersion(
4242
return picker(plan.packages);
4343
}
4444

45-
export function getPackageVersion(
46-
ctx: PubmContext,
47-
packagePath: string,
48-
): string {
45+
export function getPackageVersion(ctx: PubmContext, key: string): string {
4946
const plan = ctx.runtime.versionPlan;
5047
if (plan) {
5148
if (plan.mode === "single") return plan.version;
5249
if (plan.mode === "fixed") return plan.version;
53-
return plan.packages.get(packagePath) ?? "";
50+
return plan.packages.get(key) ?? "";
5451
}
5552
return "";
5653
}

packages/core/src/ecosystem/catalog.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ export class EcosystemCatalog {
2424
return this.descriptors.get(key);
2525
}
2626

27-
async detect(packagePath: string): Promise<EcosystemDescriptor | null> {
27+
async detectAll(packagePath: string): Promise<EcosystemDescriptor[]> {
28+
const results: EcosystemDescriptor[] = [];
2829
for (const descriptor of this.descriptors.values()) {
2930
if (await descriptor.detect(packagePath)) {
30-
return descriptor;
31+
results.push(descriptor);
3132
}
3233
}
33-
return null;
34+
return results;
3435
}
3536

3637
all(): EcosystemDescriptor[] {

packages/core/src/ecosystem/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { ecosystemCatalog } from "./catalog.js";
22
import type { Ecosystem } from "./ecosystem.js";
33

4+
/**
5+
* Detects the ecosystem for a single package path, returning only the first
6+
* detected ecosystem. This is a convenience API for single-ecosystem packages.
7+
* For packages that may belong to multiple ecosystems, use
8+
* `ecosystemCatalog.detectAll()` instead.
9+
*/
410
export async function detectEcosystem(
511
packagePath: string,
612
): Promise<Ecosystem | null> {
7-
const detected = await ecosystemCatalog.detect(packagePath);
8-
if (detected) {
9-
return new detected.ecosystemClass(packagePath);
13+
const detected = await ecosystemCatalog.detectAll(packagePath);
14+
if (detected.length > 0) {
15+
return new detected[0].ecosystemClass(packagePath);
1016
}
1117

1218
return null;

0 commit comments

Comments
 (0)