diff --git a/.changeset/rush-ws-2024-0-29-16-13-31.md b/.changeset/rush-ws-2024-0-29-16-13-31.md new file mode 100644 index 00000000..3eed2b65 --- /dev/null +++ b/.changeset/rush-ws-2024-0-29-16-13-31.md @@ -0,0 +1,5 @@ +--- +"@chronus/chronus": patch +--- + +Add support for rush.js diff --git a/packages/chronus/src/workspace-manager/index.ts b/packages/chronus/src/workspace-manager/index.ts index 2df830d0..2318b620 100644 --- a/packages/chronus/src/workspace-manager/index.ts +++ b/packages/chronus/src/workspace-manager/index.ts @@ -1,2 +1,3 @@ export * from "./npm.js"; -export * from "./pnpm.js"; +export { createPnpmWorkspaceManager } from "./pnpm.js"; +export { createRushWorkspaceManager } from "./rush.js"; diff --git a/packages/chronus/src/workspace-manager/pnpm.ts b/packages/chronus/src/workspace-manager/pnpm.ts index 06131bc8..66e09f4b 100644 --- a/packages/chronus/src/workspace-manager/pnpm.ts +++ b/packages/chronus/src/workspace-manager/pnpm.ts @@ -1,14 +1,7 @@ import { load } from "js-yaml"; -import { - ChronusError, - isDefined, - isPathAccessible, - joinPaths, - lookup, - resolvePath, - type ChronusHost, -} from "../utils/index.js"; +import { ChronusError, isPathAccessible, joinPaths, lookup, type ChronusHost } from "../utils/index.js"; import type { Package, Workspace, WorkspaceManager } from "./types.js"; +import { findPackagesFromPattern } from "./utils.js"; const workspaceFileName = "pnpm-workspace.yaml"; interface PnpmWorkspaceConfig { @@ -23,7 +16,7 @@ export function createPnpmWorkspaceManager(host: ChronusHost): WorkspaceManager return isPathAccessible(host, path); }); if (root === undefined) { - throw new Error(`Cannot find pnpm-workspace.yaml in a parent folder to ${dir}`); + throw new ChronusError(`Cannot find ${workspaceFileName} in a parent folder to ${dir}`); } const workspaceFilePath = joinPaths(root, workspaceFileName); @@ -31,10 +24,10 @@ export function createPnpmWorkspaceManager(host: ChronusHost): WorkspaceManager const config: PnpmWorkspaceConfig = load(file.content) as any; if (config.packages === undefined) { - throw new ChronusError("packages entry missing in pnpm-workspace.yaml"); + throw new ChronusError(`packages entry missing in ${workspaceFileName}`); } if (Array.isArray(config.packages) === false) { - throw new ChronusError("packages is not an array in pnpm-workspace.yaml"); + throw new ChronusError(`packages is not an array in ${workspaceFileName}`); } const packages: Package[] = ( await Promise.all(config.packages.map((pattern) => findPackagesFromPattern(host, root, pattern))) @@ -46,29 +39,3 @@ export function createPnpmWorkspaceManager(host: ChronusHost): WorkspaceManager }, }; } - -export async function findPackagesFromPattern(host: ChronusHost, root: string, pattern: string): Promise { - const packageRoots = await host.glob(pattern, { - baseDir: root, - onlyDirectories: true, - }); - - const packages = await Promise.all(packageRoots.map((x) => tryLoadNodePackage(host, root, x))); - return packages.filter(isDefined); -} - -async function tryLoadNodePackage(host: ChronusHost, root: string, relativePath: string): Promise { - const pkgJsonPath = resolvePath(root, relativePath, "package.json"); - if (await isPathAccessible(host, pkgJsonPath)) { - const file = await host.readFile(pkgJsonPath); - const pkgJson = JSON.parse(file.content); - return { - name: pkgJson.name, - version: pkgJson.version, - relativePath: relativePath, - manifest: pkgJson, - }; - } else { - return undefined; - } -} diff --git a/packages/chronus/src/workspace-manager/rush.test.ts b/packages/chronus/src/workspace-manager/rush.test.ts new file mode 100644 index 00000000..7dd8861d --- /dev/null +++ b/packages/chronus/src/workspace-manager/rush.test.ts @@ -0,0 +1,62 @@ +import { dump } from "js-yaml"; +import { beforeEach, describe, expect, it } from "vitest"; +import { createTestHost, type TestHost } from "../testing/test-host.js"; +import { createRushWorkspaceManager } from "./rush.js"; +import type { WorkspaceManager } from "./types.js"; + +describe("rush", () => { + let host: TestHost; + let rush: WorkspaceManager; + beforeEach(async () => { + host = createTestHost({}); + + rush = createRushWorkspaceManager(host.host); + }); + + it("finds 0 packages when workspace has none", async () => { + host.addFile( + "proj/rush.json", + dump({ + projects: [], + }), + ); + const workspace = await rush.load("proj"); + expect(workspace.packages).toEqual([]); + }); + + it("finds all packages", async () => { + host.addFile( + "proj/rush.json", + dump({ + projects: [ + { + packageName: "pkg-a", + projectFolder: "packages/pkg-a", + shouldPublish: true, + }, + { + packageName: "pkg-b", + projectFolder: "packages/pkg-b", + shouldPublish: true, + }, + ], + }), + ); + host.addFile("proj/packages/pkg-a/package.json", JSON.stringify({ name: "pkg-a", version: "1.0.0" })); + host.addFile("proj/packages/pkg-b/package.json", JSON.stringify({ name: "pkg-b", version: "1.2.0" })); + const workspace = await rush.load("proj"); + expect(workspace.packages).toHaveLength(2); + expect(workspace.packages[0]).toEqual({ + name: "pkg-a", + version: "1.0.0", + relativePath: "packages/pkg-a", + manifest: { name: "pkg-a", version: "1.0.0" }, + }); + expect(workspace.packages[1]).toEqual({ + name: "pkg-b", + version: "1.2.0", + relativePath: "packages/pkg-b", + manifest: { name: "pkg-b", version: "1.2.0" }, + }); + }); +}); diff --git a/packages/chronus/src/workspace-manager/rush.ts b/packages/chronus/src/workspace-manager/rush.ts new file mode 100644 index 00000000..3b4d474f --- /dev/null +++ b/packages/chronus/src/workspace-manager/rush.ts @@ -0,0 +1,48 @@ +import { load } from "js-yaml"; +import { ChronusError, isDefined, isPathAccessible, joinPaths, lookup, type ChronusHost } from "../utils/index.js"; +import type { Package, Workspace, WorkspaceManager } from "./types.js"; +import { tryLoadNodePackage } from "./utils.js"; + +const workspaceFileName = "rush.json"; + +interface RushJson { + readonly projects: RushProject[]; +} + +interface RushProject { + readonly packageName: string; + readonly projectFolder: string; + readonly shouldPublish?: boolean; +} + +export function createRushWorkspaceManager(host: ChronusHost): WorkspaceManager { + return { + async load(dir: string): Promise { + const root = await lookup(dir, (current) => { + const path = joinPaths(current, workspaceFileName); + return isPathAccessible(host, path); + }); + if (root === undefined) { + throw new ChronusError(`Cannot find ${workspaceFileName} in a parent folder to ${dir}`); + } + const workspaceFilePath = joinPaths(root, workspaceFileName); + + const file = await host.readFile(workspaceFilePath); + const config: RushJson = load(file.content) as any; + + if (config.projects === undefined) { + throw new ChronusError(`projects entry missing in ${workspaceFileName}`); + } + if (Array.isArray(config.projects) === false) { + throw new ChronusError(`projects is not an array in ${workspaceFileName}`); + } + const packages: Package[] = ( + await Promise.all(config.projects.map((pattern) => tryLoadNodePackage(host, root, pattern.projectFolder))) + ).filter(isDefined); + return { + path: root, + packages, + }; + }, + }; +} diff --git a/packages/chronus/src/workspace-manager/utils.ts b/packages/chronus/src/workspace-manager/utils.ts new file mode 100644 index 00000000..c6f37444 --- /dev/null +++ b/packages/chronus/src/workspace-manager/utils.ts @@ -0,0 +1,34 @@ +import { isPathAccessible } from "../utils/fs-utils.js"; +import type { ChronusHost } from "../utils/host.js"; +import { isDefined, resolvePath } from "../utils/index.js"; +import type { Package } from "./types.js"; + +export async function findPackagesFromPattern(host: ChronusHost, root: string, pattern: string): Promise { + const packageRoots = await host.glob(pattern, { + baseDir: root, + onlyDirectories: true, + }); + + const packages = await Promise.all(packageRoots.map((x) => tryLoadNodePackage(host, root, x))); + return packages.filter(isDefined); +} + +export async function tryLoadNodePackage( + host: ChronusHost, + root: string, + relativePath: string, +): Promise { + const pkgJsonPath = resolvePath(root, relativePath, "package.json"); + if (await isPathAccessible(host, pkgJsonPath)) { + const file = await host.readFile(pkgJsonPath); + const pkgJson = JSON.parse(file.content); + return { + name: pkgJson.name, + version: pkgJson.version, + relativePath: relativePath, + manifest: pkgJson, + }; + } else { + return undefined; + } +}