diff --git a/.vscode/launch.json b/.vscode/launch.json index 9088d098..46d35ec8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,6 +16,19 @@ "sourceMaps": true, "outputCapture": "std", "console": "integratedTerminal" + }, + { + // If there's an issue with running a beachball command, it's very likely due to using the + // local version of workspace-tools if it has breaking changes... + "type": "node", + "request": "launch", + "name": "Debug beachball", + "runtimeExecutable": "npm", + "cwd": "${workspaceFolder}", + "runtimeArgs": ["run-script", "checkchange"], + "sourceMaps": true, + "outputCapture": "std", + "console": "integratedTerminal" } ] } diff --git a/change/change-4de15c98-ca4c-479e-b502-2ef3e4caf154.json b/change/change-4de15c98-ca4c-479e-b502-2ef3e4caf154.json new file mode 100644 index 00000000..1e6ab0f9 --- /dev/null +++ b/change/change-4de15c98-ca4c-479e-b502-2ef3e4caf154.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "type": "minor", + "comment": "BREAKING: Various breaking changes to workspace package utilities. See readme for details.", + "packageName": "workspace-tools", + "email": "elcraig@microsoft.com", + "dependentChangeType": "patch" + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 63be8b91..c789c4ec 100644 --- a/package.json +++ b/package.json @@ -45,5 +45,13 @@ "ts-jest": "^29.1.0", "typedoc": "^0.25.2", "typescript": "~5.2.2" + }, + "resolutions": { + "**/beachball/workspace-tools": "0.40.2" + }, + "rationale": { + "resolutions": { + "**/beachball/workspace-tools": "Explicitly install an older version to avoid issues with local breaking changes" + } } } diff --git a/packages/workspace-tools/README.md b/packages/workspace-tools/README.md index 3dd9ef4a..232ab9cb 100644 --- a/packages/workspace-tools/README.md +++ b/packages/workspace-tools/README.md @@ -20,8 +20,46 @@ Override the `maxBuffer` value for git processes, for example if the repo is ver ### PREFERRED_WORKSPACE_MANAGER -Sometimes if multiple workspace/monorepo manager files are checked in, it's necessary to hint which manager is used: `npm`, `yarn`, `pnpm`, `rush`, or `lerna`. +Sometimes if multiple workspace/monorepo manager files are checked in, it's necessary to hint which manager is used: `npm`, `yarn`, `pnpm`, `rush`, or `lerna`. Some APIs also accept a `manager` parameter, which is now the preferred method when available. ### VERBOSE Log additional output from certain functions. + +## Breaking changes + +For details of changes in all versions, see the [changelog](https://github.com/microsoft/workspace-tools/blob/main/packages/workspace-tools/CHANGELOG.md). This only lists the most significant breaking API changes. + +### 0.41.0 + +The following APIs have been renamed for clarity, removed entirely, or consolidated: + +| Old (removed) | New | +| ----------------------------- | -------------------------------------- | +| `getWorkspaces` | `getWorkspaceInfos` | +| `getWorkspacesAsync` | `getWorkspaceInfosAsync` | +| `WorkspaceInfo` | `WorkspaceInfos` | +| `getWorkspaceRoot` | `getWorkspaceManagerRoot` | +| `listOfWorkspacePackageNames` | `workspaces.map(w => w.name)` | +| `getPnpmWorkspaceRoot` | `getWorkspaceManagerRoot(cwd, 'pnpm')` | +| `getRushWorkspaceRoot` | `getWorkspaceManagerRoot(cwd, 'rush')` | +| `getYarnWorkspaceRoot` | `getWorkspaceManagerRoot(cwd, 'yarn')` | +| `getPnpmWorkspaces` | `getWorkspaceInfos(cwd, 'pnpm')` | +| `getRushWorkspaces` | `getWorkspaceInfos(cwd, 'rush')` | +| `getYarnWorkspaces` | `getWorkspaceInfos(cwd, 'yarn')` | + +Other changes: + +- Several functions now return `string[] | undefined` instead of returning an empty array on error: + - `getAllPackageJsonFiles`, `getAllPackageJsonFilesAsync` + - `getWorkspacePackagePaths`, `getWorkspacePackagePathsAsync` + - `getWorkspaceInfos`, `getWorkspaceInfosAsync` +- `getWorkspaceManagerAndRoot` is now exported if you want to know the manager as well as the root +- Several functions now have a `manager` param to force using a specific manager: + - `getWorkspaceManagerRoot` + - `findProjectRoot` (falls back to the git root and throws if neither is found) + - `getWorkspacePackagePaths`, `getWorkspacePackagePathsAsync` + - `getWorkspacePatterns` (new) + - `getWorkspaceInfos`, `getWorkspaceInfosAsync` + - `getCatalogs` +- Some related files have been moved or renamed internally, so deep imports may be broken. Please check the current top-level API to see if the utility you were deep-importing is now exported, and file an issue if not. diff --git a/packages/workspace-tools/etc/workspace-tools.api.md b/packages/workspace-tools/etc/workspace-tools.api.md index 7b9df871..139a9e76 100644 --- a/packages/workspace-tools/etc/workspace-tools.api.md +++ b/packages/workspace-tools/etc/workspace-tools.api.md @@ -90,16 +90,16 @@ export function findGitRoot(cwd: string): string; export function findPackageRoot(cwd: string): string | undefined; // @public -export function findProjectRoot(cwd: string): string; +export function findProjectRoot(cwd: string, manager?: WorkspaceManager): string; // @public export function findWorkspacePath(workspaces: WorkspaceInfos, packageName: string): string | undefined; // @public -export function getAllPackageJsonFiles(cwd: string): string[]; +export function getAllPackageJsonFiles(cwd: string): string[] | undefined; // @public -export function getAllPackageJsonFilesAsync(cwd: string): Promise; +export function getAllPackageJsonFilesAsync(cwd: string): Promise; // Warning: (ae-forgotten-export) The symbol "GitBranchOptions" needs to be exported by the entry point index.d.ts // @@ -118,7 +118,7 @@ export function getBranchName(options: GitCommonOptions): string; export function getBranchName(cwd: string): string; // @public -export function getCatalogs(cwd: string): Catalogs | undefined; +export function getCatalogs(cwd: string, managerOverride?: WorkspaceManager): Catalogs | undefined; // @public export function getCatalogVersion(params: { @@ -243,10 +243,10 @@ export function getPackageInfo(cwd: string): PackageInfo | undefined; export function getPackageInfoAsync(cwd: string): Promise; // @public -export function getPackageInfos(cwd: string): PackageInfos; +export function getPackageInfos(cwd: string, managerOverride?: WorkspaceManager): PackageInfos; // @public -export function getPackageInfosAsync(cwd: string): Promise; +export function getPackageInfosAsync(cwd: string, managerOverride?: WorkspaceManager): Promise; // Warning: (ae-forgotten-export) The symbol "GetPackagesByFilesOptions" needs to be exported by the entry point index.d.ts // @@ -267,12 +267,6 @@ interface GetPackagesByFilesOptions { // @public @deprecated export function getParentBranch(cwd: string): string | null; -// @public @deprecated (undocumented) -export function getPnpmWorkspaceRoot(cwd: string): string; - -// @public -export function getPnpmWorkspaces(cwd: string): WorkspaceInfos; - // @public export function getRecentCommitMessages(options: GitBranchOptions): string[]; @@ -285,12 +279,6 @@ export function getRemoteBranch(options: GitBranchOptions): string | null; // @public @deprecated (undocumented) export function getRemoteBranch(branch: string, cwd: string): string | null; -// @public @deprecated (undocumented) -export function getRushWorkspaceRoot(cwd: string): string; - -// @public -export function getRushWorkspaces(cwd: string): WorkspaceInfos; - // @public export function getScopedPackages(search: string[], packages: { [pkg: string]: unknown; @@ -341,36 +329,32 @@ export function getUserEmail(options: GitCommonOptions): string | null; export function getUserEmail(cwd: string): string | null; // @public -export function getWorkspaceInfos(cwd: string): WorkspaceInfos; +export function getWorkspaceInfos(cwd: string, managerOverride?: WorkspaceManager): WorkspaceInfos | undefined; // @public -export function getWorkspaceInfosAsync(cwd: string): Promise; +export function getWorkspaceInfosAsync(cwd: string, managerOverride?: WorkspaceManager): Promise; -// Warning: (ae-forgotten-export) The symbol "WorkspaceManager" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "WorkspaceManagerAndRoot" needs to be exported by the entry point index.d.ts // // @public -export function getWorkspaceManagerRoot(cwd: string, preferredManager?: WorkspaceManager): string | undefined; - -// @public -export function getWorkspacePackagePaths(cwd: string): string[]; +export function getWorkspaceManagerAndRoot(cwd: string, cache?: Map, managerOverride?: WorkspaceManager): WorkspaceManagerAndRoot | undefined; // @public -export function getWorkspacePackagePathsAsync(cwd: string): Promise; +export function getWorkspaceManagerRoot(cwd: string, managerOverride?: WorkspaceManager): string | undefined; -// @public @deprecated -export function getWorkspaceRoot(cwd: string, preferredManager?: WorkspaceManager): string | undefined; - -// @public @deprecated (undocumented) -export const getWorkspaces: typeof getWorkspaceInfos; +// @public (undocumented) +export function getWorkspaceManagerRoot(cwd: string, managerOverride: WorkspaceManager | undefined, options?: { + throwOnError: true; +}): string; -// @public @deprecated (undocumented) -export const getWorkspacesAsync: typeof getWorkspaceInfosAsync; +// @public +export function getWorkspacePackagePaths(cwd: string, managerOverride?: WorkspaceManager): string[] | undefined; -// @public @deprecated (undocumented) -export function getYarnWorkspaceRoot(cwd: string): string; +// @public +export function getWorkspacePackagePathsAsync(cwd: string, managerOverride?: WorkspaceManager): Promise; // @public -export function getYarnWorkspaces(cwd: string): WorkspaceInfos; +export function getWorkspacePatterns(cwd: string, managerOverride?: WorkspaceManager): string[] | undefined; // @public export function git(args: string[], options?: GitOptions): GitProcessOutput; @@ -462,9 +446,6 @@ export function listAllTrackedFiles(options: { // @public @deprecated (undocumented) export function listAllTrackedFiles(patterns: string[], cwd: string): string[]; -// @public @deprecated (undocumented) -export function listOfWorkspacePackageNames(workspaces: WorkspaceInfos): string[]; - // @public (undocumented) export type LockDependency = { version: string; @@ -697,14 +678,17 @@ export function stageAndCommit(options: GitStageOptions & GitCommitOptions): voi // @public @deprecated (undocumented) export function stageAndCommit(patterns: string[], message: string, cwd: string, commitOptions?: string[]): void; -// @public @deprecated (undocumented) -export type WorkspaceInfo = WorkspaceInfos; - // @public export type WorkspaceInfos = WorkspacePackageInfo[]; // @public (undocumented) -type WorkspaceManager = "yarn" | "pnpm" | "rush" | "npm" | "lerna"; +export type WorkspaceManager = "yarn" | "pnpm" | "rush" | "npm" | "lerna"; + +// @public (undocumented) +interface WorkspaceManagerAndRoot { + manager: WorkspaceManager; + root: string; +} // @public export interface WorkspacePackageInfo { diff --git a/packages/workspace-tools/src/__tests__/workspaces/getCatalogs.test.ts b/packages/workspace-tools/src/__tests__/workspaces/getCatalogs.test.ts index 62503798..adcfbda9 100644 --- a/packages/workspace-tools/src/__tests__/workspaces/getCatalogs.test.ts +++ b/packages/workspace-tools/src/__tests__/workspaces/getCatalogs.test.ts @@ -1,12 +1,12 @@ +import { cleanupFixtures, setupFixture } from "@ws-tools/scripts/jest/setupFixture"; import fs from "fs"; import path from "path"; -import { cleanupFixtures, setupFixture } from "@ws-tools/scripts/jest/setupFixture"; -import type { Catalogs } from "../../types/Catalogs"; -import type { WorkspaceManager } from "../../workspaces/WorkspaceManager"; -import { getWorkspaceManagerAndRoot } from "../../workspaces/implementations"; import { getPackageInfo } from "../../getPackageInfo"; -import { getCatalogs } from "../../workspaces/getCatalogs"; +import type { Catalogs } from "../../types/Catalogs"; +import type { WorkspaceManager } from "../../types/WorkspaceManager"; import { catalogsToYaml } from "../../workspaces/catalogsToYaml"; +import { getCatalogs } from "../../workspaces/getCatalogs"; +import { getWorkspaceManagerAndRoot } from "../../workspaces/implementations"; // Samples from https://yarnpkg.com/features/catalogs const defaultCatalogs: Required> = { diff --git a/packages/workspace-tools/src/__tests__/workspaces/getWorkspaceManagerAndRoot.test.ts b/packages/workspace-tools/src/__tests__/workspaces/getWorkspaceManagerAndRoot.test.ts new file mode 100644 index 00000000..1b699333 --- /dev/null +++ b/packages/workspace-tools/src/__tests__/workspaces/getWorkspaceManagerAndRoot.test.ts @@ -0,0 +1,48 @@ +import { cleanupFixtures, setupFixture } from "@ws-tools/scripts/jest/setupFixture"; +import { getWorkspaceManagerAndRoot } from "../../workspaces/implementations/getWorkspaceManagerAndRoot"; + +describe("getWorkspaceManagerAndRoot", () => { + afterAll(() => { + cleanupFixtures(); + }); + + it("handles yarn monorepo", () => { + const repoRoot = setupFixture("monorepo"); + expect(getWorkspaceManagerAndRoot(repoRoot)).toEqual({ + root: repoRoot, + manager: "yarn", + }); + }); + + it("handles pnpm monorepo", () => { + const repoRoot = setupFixture("monorepo-pnpm"); + expect(getWorkspaceManagerAndRoot(repoRoot)).toEqual({ + root: repoRoot, + manager: "pnpm", + }); + }); + + it("handles rush monorepo", () => { + const repoRoot = setupFixture("monorepo-rush-pnpm"); + expect(getWorkspaceManagerAndRoot(repoRoot)).toEqual({ + root: repoRoot, + manager: "rush", + }); + }); + + it("handles npm monorepo", () => { + const repoRoot = setupFixture("monorepo-npm"); + expect(getWorkspaceManagerAndRoot(repoRoot)).toEqual({ + root: repoRoot, + manager: "npm", + }); + }); + + it("handles lerna monorepo", () => { + const repoRoot = setupFixture("monorepo-lerna-npm"); + expect(getWorkspaceManagerAndRoot(repoRoot)).toEqual({ + root: repoRoot, + manager: "lerna", + }); + }); +}); diff --git a/packages/workspace-tools/src/__tests__/workspaces/getWorkspaceRoot.test.ts b/packages/workspace-tools/src/__tests__/workspaces/getWorkspaceRoot.test.ts deleted file mode 100644 index 6dd127de..00000000 --- a/packages/workspace-tools/src/__tests__/workspaces/getWorkspaceRoot.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { cleanupFixtures, setupFixture } from "@ws-tools/scripts/jest/setupFixture"; -import { getWorkspaceManagerRoot } from "../../workspaces/getWorkspaceRoot"; - -describe("getWorkspaceManagerRoot", () => { - afterAll(() => { - cleanupFixtures(); - }); - - it("handles yarn monorepo", () => { - const repoRoot = setupFixture("monorepo"); - expect(getWorkspaceManagerRoot(repoRoot)).toBe(repoRoot); - }); - - it("handles pnpm monorepo", () => { - const repoRoot = setupFixture("monorepo-pnpm"); - expect(getWorkspaceManagerRoot(repoRoot)).toBe(repoRoot); - }); - - it("handles rush monorepo", () => { - const repoRoot = setupFixture("monorepo-rush-pnpm"); - expect(getWorkspaceManagerRoot(repoRoot)).toBe(repoRoot); - }); - - it("handles npm monorepo", () => { - const repoRoot = setupFixture("monorepo-npm"); - expect(getWorkspaceManagerRoot(repoRoot)).toBe(repoRoot); - }); - - it("handles lerna monorepo", () => { - const repoRoot = setupFixture("monorepo-lerna-npm"); - expect(getWorkspaceManagerRoot(repoRoot)).toBe(repoRoot); - }); -}); diff --git a/packages/workspace-tools/src/__tests__/workspaces/getWorkspaces.test.ts b/packages/workspace-tools/src/__tests__/workspaces/getWorkspaces.test.ts index 9bf29b90..83656cf4 100644 --- a/packages/workspace-tools/src/__tests__/workspaces/getWorkspaces.test.ts +++ b/packages/workspace-tools/src/__tests__/workspaces/getWorkspaces.test.ts @@ -1,78 +1,33 @@ -import path from "path"; - import { cleanupFixtures, setupFixture } from "@ws-tools/scripts/jest/setupFixture"; +import path from "path"; +import type { WorkspaceManager } from "../../types/WorkspaceManager"; +import { getWorkspaceInfos, getWorkspaceInfosAsync } from "../../workspaces/getWorkspaceInfos"; import { getWorkspaceManagerAndRoot } from "../../workspaces/implementations"; -import { getYarnWorkspaces, getYarnWorkspacesAsync } from "../../workspaces/implementations/yarn"; -import { getPnpmWorkspaces, getPnpmWorkspacesAsync } from "../../workspaces/implementations/pnpm"; -import { getRushWorkspaces, getRushWorkspacesAsync } from "../../workspaces/implementations/rush"; -import { getNpmWorkspaces, getNpmWorkspacesAsync } from "../../workspaces/implementations/npm"; -import { getLernaWorkspaces, getLernaWorkspacesAsync } from "../../workspaces/implementations/lerna"; -import type { WorkspaceManager } from "../../workspaces/WorkspaceManager"; -describe("getWorkspaces", () => { +describe("getWorkspaceInfos", () => { afterAll(() => { cleanupFixtures(); }); describe.each<{ manager: WorkspaceManager; - managerName: string; + desc: string; fixtureName: string; - getSync: typeof getYarnWorkspaces; - getAsync: typeof getYarnWorkspacesAsync; }>([ - { - manager: "yarn", - managerName: "yarn", - fixtureName: "monorepo", - getSync: getYarnWorkspaces, - getAsync: getYarnWorkspacesAsync, - }, - { - manager: "pnpm", - managerName: "pnpm", - fixtureName: "monorepo-pnpm", - getSync: getPnpmWorkspaces, - getAsync: getPnpmWorkspacesAsync, - }, - { - manager: "rush", - managerName: "rush + pnpm", - fixtureName: "monorepo-rush-pnpm", - getSync: getRushWorkspaces, - getAsync: getRushWorkspacesAsync, - }, - { - manager: "rush", - managerName: "rush + yarn", - fixtureName: "monorepo-rush-yarn", - getSync: getRushWorkspaces, - getAsync: getRushWorkspacesAsync, - }, - { - manager: "npm", - managerName: "npm", - fixtureName: "monorepo-npm", - getSync: getNpmWorkspaces, - getAsync: getNpmWorkspacesAsync, - }, - { - manager: "lerna", - managerName: "lerna + npm", - fixtureName: "monorepo-lerna-npm", - getSync: getLernaWorkspaces, - getAsync: getLernaWorkspacesAsync, - }, - ])("$managerName", ({ manager, fixtureName, getSync, getAsync }) => { - it.each([ - ["sync", getSync], - ["async", getAsync], - ])("gets workspace info (%s)", async (name, getWorkspaces) => { - const root = setupFixture(fixtureName); + { manager: "yarn", desc: "yarn", fixtureName: "monorepo" }, + { manager: "pnpm", desc: "pnpm", fixtureName: "monorepo-pnpm" }, + { manager: "rush", desc: "rush + pnpm", fixtureName: "monorepo-rush-pnpm" }, + { manager: "rush", desc: "rush + yarn", fixtureName: "monorepo-rush-yarn" }, + { manager: "npm", desc: "npm", fixtureName: "monorepo-npm" }, + { manager: "lerna", desc: "lerna + npm", fixtureName: "monorepo-lerna-npm" }, + ])("$desc", ({ manager, fixtureName }) => { + it.each(["sync", "async"] as const)("gets workspace info (%s)", async (syncAsync) => { + const getInfo = syncAsync === "sync" ? getWorkspaceInfos : getWorkspaceInfosAsync; + const root = setupFixture(fixtureName); expect(getWorkspaceManagerAndRoot(root, new Map())).toEqual({ manager, root }); - const workspacesPackageInfo = (await getWorkspaces(root)).sort((a, b) => a.name.localeCompare(b.name)); + const workspacesPackageInfo = (await getInfo(root, manager))?.sort((a, b) => a.name.localeCompare(b.name)); expect(workspacesPackageInfo).toMatchObject([ { name: "individual", path: path.join(root, "individual") }, diff --git a/packages/workspace-tools/src/getPackageInfo.ts b/packages/workspace-tools/src/getPackageInfo.ts index 8832e1f6..9dbdc9da 100644 --- a/packages/workspace-tools/src/getPackageInfo.ts +++ b/packages/workspace-tools/src/getPackageInfo.ts @@ -2,13 +2,13 @@ import fs from "fs"; import fsPromises from "fs/promises"; import path from "path"; import type { PackageInfo } from "./types/PackageInfo"; -import { infoFromPackageJson } from "./infoFromPackageJson"; import { logVerboseWarning } from "./logging"; /** * Read package.json from the given path if it exists. - * Logs a warning if it doesn't exist, or there's an error reading or parsing it. - * @returns The package info, or undefined if it doesn't exist or can't be read + * + * @returns The package info, or undefined if it doesn't exist or can't be read. + * (Logs verbose warnings instead of throwing on error.) */ export function getPackageInfo(cwd: string): PackageInfo | undefined { const packageJsonPath = path.join(cwd, "package.json"); @@ -19,7 +19,7 @@ export function getPackageInfo(cwd: string): PackageInfo | undefined { } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); - return infoFromPackageJson(packageJson, packageJsonPath); + return { ...packageJson, packageJsonPath }; } catch (e) { logVerboseWarning(`Error reading or parsing ${packageJsonPath}: ${(e as Error)?.message || e}`); } @@ -27,8 +27,9 @@ export function getPackageInfo(cwd: string): PackageInfo | undefined { /** * Read package.json from the given path if it exists. - * Logs a warning if it doesn't exist, or there's an error reading or parsing it. - * @returns The package info, or undefined if it doesn't exist or can't be read + * + * @returns The package info, or undefined if it doesn't exist or can't be read. + * (Logs verbose warnings instead of throwing on error.) */ export async function getPackageInfoAsync(cwd: string): Promise { const packageJsonPath = path.join(cwd, "package.json"); @@ -39,7 +40,7 @@ export async function getPackageInfoAsync(cwd: string): Promise { + const workspacePackages = await getWorkspaceInfosAsync(cwd, managerOverride); + return buildPackageInfos({ cwd, workspacePackages }); +} + +/** + * Convert an array of workspace package infos into a name-to-packageInfo map. + * If there are no workspace packages, reads the root package.json instead. */ -export async function getPackageInfosAsync(cwd: string): Promise { +function buildPackageInfos(params: { + cwd: string; + workspacePackages: WorkspacePackageInfo[] | undefined; +}): PackageInfos { + const { cwd, workspacePackages } = params; const packageInfos: PackageInfos = {}; - const workspacePackages = await getWorkspacesAsync(cwd); - if (workspacePackages.length) { + if (workspacePackages?.length) { for (const pkg of workspacePackages) { packageInfos[pkg.name] = pkg.packageJson; } } else { - const rootInfo = getPackageInfo(cwd); + const packageRoot = findPackageRoot(cwd); + const rootInfo = packageRoot && getPackageInfo(packageRoot); if (rootInfo) { packageInfos[rootInfo.name] = rootInfo; } diff --git a/packages/workspace-tools/src/getPackagePaths.ts b/packages/workspace-tools/src/getPackagePaths.ts deleted file mode 100644 index 14f1d6d8..00000000 --- a/packages/workspace-tools/src/getPackagePaths.ts +++ /dev/null @@ -1,51 +0,0 @@ -import path from "path"; -import glob, { type Options as GlobOptions } from "fast-glob"; -import { isCachingEnabled } from "./isCachingEnabled"; - -const packagePathsCache: { [root: string]: string[] } = {}; -const globOptions: GlobOptions = { - absolute: true, - ignore: ["**/node_modules/**", "**/__fixtures__/**"], - stats: false, -}; - -/** - * Given package folder globs (such as those from package.json `workspaces`) and a monorepo root - * directory, get absolute paths to actual package folders. - */ -export function getPackagePaths(root: string, packageGlobs: string[]): string[] { - if (isCachingEnabled() && packagePathsCache[root]) { - return packagePathsCache[root]; - } - - packagePathsCache[root] = glob - .sync(getPackageJsonGlobs(packageGlobs), { cwd: root, ...globOptions }) - .map(getResultPackagePath); - - return packagePathsCache[root]; -} - -/** - * Given package folder globs (such as those from package.json `workspaces`) and a monorepo root - * directory, get absolute paths to actual package folders. - */ -export async function getPackagePathsAsync(root: string, packageGlobs: string[]): Promise { - if (isCachingEnabled() && packagePathsCache[root]) { - return packagePathsCache[root]; - } - - packagePathsCache[root] = (await glob(getPackageJsonGlobs(packageGlobs), { cwd: root, ...globOptions })).map( - getResultPackagePath - ); - - return packagePathsCache[root]; -} - -function getPackageJsonGlobs(packageGlobs: string[]) { - return packageGlobs.map((glob) => path.join(glob, "package.json").replace(/\\/g, "/")); -} - -function getResultPackagePath(packageJsonPath: string) { - const packagePath = path.dirname(packageJsonPath); - return path.sep === "/" ? packagePath : packagePath.replace(/\//g, path.sep); -} diff --git a/packages/workspace-tools/src/index.ts b/packages/workspace-tools/src/index.ts index b4891662..f1ec1aa6 100644 --- a/packages/workspace-tools/src/index.ts +++ b/packages/workspace-tools/src/index.ts @@ -9,23 +9,17 @@ export { getScopedPackages } from "./scope"; export type { Catalog, Catalogs, NamedCatalogs } from "./types/Catalogs"; export type { PackageDependency, PackageGraph } from "./types/PackageGraph"; export type { PackageInfo, PackageInfos } from "./types/PackageInfo"; -export type { WorkspacePackageInfo, WorkspaceInfos, WorkspaceInfo } from "./types/WorkspaceInfo"; +export type { WorkspacePackageInfo, WorkspaceInfos } from "./types/WorkspaceInfo"; export { findWorkspacePath } from "./workspaces/findWorkspacePath"; -export { - getWorkspaces, - getWorkspacesAsync, - getWorkspaceInfos, - getWorkspaceInfosAsync, -} from "./workspaces/getWorkspaces"; +export { getWorkspaceInfos, getWorkspaceInfosAsync } from "./workspaces/getWorkspaceInfos"; export { getWorkspacePackagePaths, getWorkspacePackagePathsAsync } from "./workspaces/getWorkspacePackagePaths"; -export { getWorkspaceManagerRoot, getWorkspaceRoot } from "./workspaces/getWorkspaceRoot"; +export { getWorkspacePatterns } from "./workspaces/getWorkspacePatterns"; +export { getWorkspaceManagerAndRoot } from "./workspaces/implementations/getWorkspaceManagerAndRoot"; +export { getWorkspaceManagerRoot } from "./workspaces/getWorkspaceManagerRoot"; +export type { WorkspaceManager } from "./types/WorkspaceManager"; export { getPackageInfo, getPackageInfoAsync } from "./getPackageInfo"; -export { getPnpmWorkspaceRoot, getPnpmWorkspaces } from "./workspaces/implementations/pnpm"; -export { getRushWorkspaceRoot, getRushWorkspaces } from "./workspaces/implementations/rush"; -export { getYarnWorkspaceRoot, getYarnWorkspaces } from "./workspaces/implementations/yarn"; export { getChangedPackages, getChangedPackagesBetweenRefs } from "./workspaces/getChangedPackages"; export { getPackagesByFiles } from "./workspaces/getPackagesByFiles"; -export { listOfWorkspacePackageNames } from "./workspaces/listOfWorkspacePackageNames"; export { getAllPackageJsonFiles, getAllPackageJsonFilesAsync } from "./workspaces/getAllPackageJsonFiles"; export { catalogsToYaml } from "./workspaces/catalogsToYaml"; export { getCatalogVersion, isCatalogVersion } from "./workspaces/getCatalogVersion"; diff --git a/packages/workspace-tools/src/infoFromPackageJson.ts b/packages/workspace-tools/src/infoFromPackageJson.ts deleted file mode 100644 index 0759ffce..00000000 --- a/packages/workspace-tools/src/infoFromPackageJson.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { PackageInfo } from "./types/PackageInfo"; - -export function infoFromPackageJson( - packageJson: { - name: string; - version: string; - dependencies?: { - [dep: string]: string; - }; - devDependencies?: { - [dep: string]: string; - }; - peerDependencies?: { - [dep: string]: string; - }; - private?: boolean; - pipeline?: any; - scripts?: any; - }, - packageJsonPath: string -): PackageInfo { - return { - packageJsonPath, - ...packageJson, - }; -} diff --git a/packages/workspace-tools/src/paths.ts b/packages/workspace-tools/src/paths.ts index 49b63875..d76f84df 100644 --- a/packages/workspace-tools/src/paths.ts +++ b/packages/workspace-tools/src/paths.ts @@ -1,8 +1,9 @@ import path from "path"; import fs from "fs"; -import { getWorkspaceManagerRoot } from "./workspaces/getWorkspaceRoot"; +import { getWorkspaceManagerRoot } from "./workspaces/getWorkspaceManagerRoot"; import { git } from "./git"; import { logVerboseWarning } from "./logging"; +import type { WorkspaceManager } from "./types/WorkspaceManager"; /** * Starting from `cwd`, searches up the directory hierarchy for `filePath`. @@ -58,11 +59,13 @@ export function findPackageRoot(cwd: string) { * To skip the git root fallback, use `getWorkspaceManagerRoot`. Usually the monorepo manager root * is the same as the git root, but this may not be the case with multiple "monorepos" in a single * git repo, or in project structures with multiple languages where the JS is not at the root. + * + * @param manager Optional workspace/monorepo manager to look for specifically */ -export function findProjectRoot(cwd: string) { +export function findProjectRoot(cwd: string, manager?: WorkspaceManager) { let workspaceRoot: string | undefined; try { - workspaceRoot = getWorkspaceManagerRoot(cwd); + workspaceRoot = getWorkspaceManagerRoot(cwd, manager); if (!workspaceRoot) { logVerboseWarning(`Could not find workspace manager root for ${cwd}. Falling back to git root.`); } diff --git a/packages/workspace-tools/src/types/WorkspaceInfo.ts b/packages/workspace-tools/src/types/WorkspaceInfo.ts index 8eeced34..6e113750 100644 --- a/packages/workspace-tools/src/types/WorkspaceInfo.ts +++ b/packages/workspace-tools/src/types/WorkspaceInfo.ts @@ -3,8 +3,8 @@ import { PackageInfo } from "./PackageInfo"; /** * Info about a single package ("workspace" in npm/yarn/pnpm terms) within a monorepo. * - * Ideally this should be called just `WorkspaceInfo`, but that name was previously used for the - * aggregate type. + * (Ideally this should be called just `WorkspaceInfo`, since "workspace" and "package" mean + * the same thing, but that name was previously used for the aggregate type.) */ export interface WorkspacePackageInfo { /** Package name */ @@ -20,6 +20,3 @@ export interface WorkspacePackageInfo { * npm/yarn/pnpm terms) within a monorepo. */ export type WorkspaceInfos = WorkspacePackageInfo[]; - -/** @deprecated Use `WorkspaceInfos` */ -export type WorkspaceInfo = WorkspaceInfos; diff --git a/packages/workspace-tools/src/workspaces/WorkspaceManager.ts b/packages/workspace-tools/src/types/WorkspaceManager.ts similarity index 100% rename from packages/workspace-tools/src/workspaces/WorkspaceManager.ts rename to packages/workspace-tools/src/types/WorkspaceManager.ts diff --git a/packages/workspace-tools/src/workspaces/getAllPackageJsonFiles.ts b/packages/workspace-tools/src/workspaces/getAllPackageJsonFiles.ts index f91a800a..0342390f 100644 --- a/packages/workspace-tools/src/workspaces/getAllPackageJsonFiles.ts +++ b/packages/workspace-tools/src/workspaces/getAllPackageJsonFiles.ts @@ -2,17 +2,20 @@ import path from "path"; import { getWorkspacePackagePaths, getWorkspacePackagePathsAsync } from "./getWorkspacePackagePaths"; import { isCachingEnabled } from "../isCachingEnabled"; -const cache = new Map(); +const cache = new Map(); /** - * Get paths to every package.json in the monorepo, given a cwd. + * Get paths to every package.json in the monorepo, given a cwd, with caching. + * + * @returns Array of package.json paths, or undefined if there's any issue + * (logs verbose warnings instead of throwing on error) */ -export function getAllPackageJsonFiles(cwd: string): string[] { +export function getAllPackageJsonFiles(cwd: string): string[] | undefined { if (isCachingEnabled() && cache.has(cwd)) { return cache.get(cwd)!; } - const packageJsonFiles = getWorkspacePackagePaths(cwd).map((packagePath) => path.join(packagePath, "package.json")); + const packageJsonFiles = getWorkspacePackagePaths(cwd)?.map((packagePath) => path.join(packagePath, "package.json")); cache.set(cwd, packageJsonFiles); @@ -24,14 +27,17 @@ export function _resetPackageJsonFilesCache() { } /** - * Get paths to every package.json in the monorepo, given a cwd. + * Get paths to every package.json in the monorepo, given a cwd, with caching. + * + * @returns Array of package.json paths, or undefined if there's any issue + * (logs verbose warnings instead of throwing on error) */ -export async function getAllPackageJsonFilesAsync(cwd: string): Promise { +export async function getAllPackageJsonFilesAsync(cwd: string): Promise { if (isCachingEnabled() && cache.has(cwd)) { return cache.get(cwd)!; } - const packageJsonFiles = (await getWorkspacePackagePathsAsync(cwd)).map((packagePath) => + const packageJsonFiles = (await getWorkspacePackagePathsAsync(cwd))?.map((packagePath) => path.join(packagePath, "package.json") ); diff --git a/packages/workspace-tools/src/workspaces/getCatalogs.ts b/packages/workspace-tools/src/workspaces/getCatalogs.ts index adcbd3aa..85ecbf10 100644 --- a/packages/workspace-tools/src/workspaces/getCatalogs.ts +++ b/packages/workspace-tools/src/workspaces/getCatalogs.ts @@ -1,14 +1,28 @@ import type { Catalogs } from "../types/Catalogs"; +import type { WorkspaceManager } from "../types/WorkspaceManager"; import { getWorkspaceUtilities } from "./implementations"; +import { wrapWorkspaceUtility } from "./wrapWorkspaceUtility"; /** * Get version catalogs, if supported by the manager (only pnpm and yarn v4 as of writing). * Returns undefined if no catalogs are present or the manager doesn't support them. * @see https://pnpm.io/catalogs * @see https://yarnpkg.com/features/catalogs + * * @param cwd - Current working directory. It will search up from here to find the root, with caching. + * @param managerOverride Workspace/monorepo manager to use instead of auto-detecting + * + * @returns Catalogs if defined, or undefined if not available + * (logs verbose warnings instead of throwing on error) */ -export function getCatalogs(cwd: string): Catalogs | undefined { - const utils = getWorkspaceUtilities(cwd); - return utils?.getCatalogs?.(cwd); +export function getCatalogs(cwd: string, managerOverride?: WorkspaceManager): Catalogs | undefined { + return wrapWorkspaceUtility({ + cwd, + managerOverride, + description: "catalogs", + impl: ({ manager, root }) => { + // There is no default implementation for catalogs, since not all managers support it + return getWorkspaceUtilities(manager).getCatalogs?.({ root }); + }, + }); } diff --git a/packages/workspace-tools/src/workspaces/getPackagesByFiles.ts b/packages/workspace-tools/src/workspaces/getPackagesByFiles.ts index eb2662d1..d66ef21a 100644 --- a/packages/workspace-tools/src/workspaces/getPackagesByFiles.ts +++ b/packages/workspace-tools/src/workspaces/getPackagesByFiles.ts @@ -1,6 +1,6 @@ import micromatch from "micromatch"; import path from "path"; -import { getWorkspaces } from "./getWorkspaces"; +import { getWorkspaceInfos } from "./getWorkspaceInfos"; interface GetPackagesByFilesOptions { /** Monorepo root directory */ @@ -40,7 +40,7 @@ export function getPackagesByFiles( ({ root, files, ignoreGlobs, returnAllPackagesOnNoMatch } = cwdOrOptions); } - const workspaces = getWorkspaces(root); + const workspaces = getWorkspaceInfos(root) || []; const ignoreSet = new Set(ignoreGlobs?.length ? micromatch(files, ignoreGlobs) : []); const filteredFiles = files.filter((change) => !ignoreSet.has(change)); diff --git a/packages/workspace-tools/src/workspaces/getWorkspaceInfos.ts b/packages/workspace-tools/src/workspaces/getWorkspaceInfos.ts new file mode 100644 index 00000000..66f0f0bd --- /dev/null +++ b/packages/workspace-tools/src/workspaces/getWorkspaceInfos.ts @@ -0,0 +1,77 @@ +import { getPackageInfo, getPackageInfoAsync } from "../getPackageInfo"; +import type { WorkspaceInfos, WorkspacePackageInfo } from "../types/WorkspaceInfo"; +import type { WorkspaceManager } from "../types/WorkspaceManager"; +import { getWorkspacePackagePaths, getWorkspacePackagePathsAsync } from "./getWorkspacePackagePaths"; +import { wrapAsyncWorkspaceUtility, wrapWorkspaceUtility } from "./wrapWorkspaceUtility"; + +/** + * Get an array with names, paths, and package.json contents for each package ("workspace" in + * npm/yarn/pnpm terms) within a monorepo. The list of included packages is based on the + * workspace/monorepo manager's config file. + * + * Notes: + * - The workspace manager, root, and list of package paths for `cwd` are cached internally, + * but the package contents are not. + * - To get an object with package names as keys, use `getPackageInfos` instead. + * + * @param managerOverride Workspace/monorepo manager to use instead of auto-detecting + * + * @returns Array of workspace package infos, or undefined if not found (not a monorepo) + * or there's any issue. (Logs verbose warnings instead of throwing on error.) + */ +export function getWorkspaceInfos(cwd: string, managerOverride?: WorkspaceManager): WorkspaceInfos | undefined { + return wrapWorkspaceUtility({ + cwd, + managerOverride, + description: "workspace package infos", + impl: ({ manager }) => { + return getWorkspacePackagePaths(cwd, manager) + ?.map((packagePath) => { + // getPackageInfo logs a warning if it can't be read + const packageJson = getPackageInfo(packagePath); + return packageJson && { name: packageJson.name, path: packagePath, packageJson }; + }) + .filter(Boolean) as WorkspaceInfos | undefined; + }, + }); +} + +/** + * Get an array with names, paths, and package.json contents for each package ("workspace" in + * npm/yarn/pnpm terms) within a monorepo. The list of included packages is based on the + * workspace/monorepo manager's config file. + * + * Notes: + * - **WARNING**: As of writing, this will start promises to read all package.json files in + * parallel, without direct concurrency control. + * - The workspace manager, root, and list of package paths for `cwd` are cached internally, + * but the package contents are not. + * - To get an object with package names as keys, use `getPackageInfosAsync` instead. + * + * @param managerOverride Workspace/monorepo manager to use instead of auto-detecting + * + * @returns Array of workspace package infos, or undefined if not found (not a monorepo) + * or there's any issue. (Logs verbose warnings instead of throwing on error.) + */ +export async function getWorkspaceInfosAsync( + cwd: string, + managerOverride?: WorkspaceManager +): Promise { + return wrapAsyncWorkspaceUtility({ + cwd, + managerOverride, + description: "workspace package infos", + impl: async ({ manager }) => { + const packagePaths = await getWorkspacePackagePathsAsync(cwd, manager); + if (!packagePaths) return undefined; + + const workspacePkgPromises = packagePaths.map>(async (packagePath) => { + // getPackageInfoAsync logs a warning if it can't be read + const packageJson = await getPackageInfoAsync(packagePath); + return packageJson && { name: packageJson.name, path: packagePath, packageJson }; + }); + + return (await Promise.all(workspacePkgPromises)).filter(Boolean) as WorkspaceInfos; + }, + }); +} diff --git a/packages/workspace-tools/src/workspaces/getWorkspaceManagerRoot.ts b/packages/workspace-tools/src/workspaces/getWorkspaceManagerRoot.ts new file mode 100644 index 00000000..094c10b0 --- /dev/null +++ b/packages/workspace-tools/src/workspaces/getWorkspaceManagerRoot.ts @@ -0,0 +1,49 @@ +import { logVerboseWarning } from "../logging"; +import { getWorkspaceManagerAndRoot } from "./implementations"; +import type { WorkspaceManager } from "../types/WorkspaceManager"; + +/** + * Get the root directory of a monorepo, defined as the directory where the workspace/monorepo manager + * config file is located. (Does not rely in any way on git, and the result is cached by `cwd`.) + * + * @param cwd Start searching from here + * @param managerOverride Search for only this manager's config file + * + * @returns Workspace manager root directory. Returns undefined (and verbose logs) on error or if + * not found, unless `throwOnError` is set. + */ +export function getWorkspaceManagerRoot(cwd: string, managerOverride?: WorkspaceManager): string | undefined; +export function getWorkspaceManagerRoot( + cwd: string, + managerOverride: WorkspaceManager | undefined, + options?: { + /** Throw if there's an error or if the root is not found */ + throwOnError: true; + } +): string; +export function getWorkspaceManagerRoot( + cwd: string, + managerOverride?: WorkspaceManager, + options?: { throwOnError: true } +): string | undefined { + const logOrThrow = (message: string) => { + if (options?.throwOnError) { + throw new Error(message); + } else { + logVerboseWarning(message); + } + }; + + let root: string | undefined; + try { + root = getWorkspaceManagerAndRoot(cwd, undefined, managerOverride)?.root; + } catch (err) { + logOrThrow(`Error getting ${managerOverride || "workspace/monorepo manager"} root from ${cwd}: ${err}`); + return; + } + + if (!root) { + logOrThrow(`Could not find ${managerOverride || "workspace/monorepo manager"} root from ${cwd}`); + } + return root; +} diff --git a/packages/workspace-tools/src/workspaces/getWorkspacePackageInfo.ts b/packages/workspace-tools/src/workspaces/getWorkspacePackageInfo.ts deleted file mode 100644 index dd042138..00000000 --- a/packages/workspace-tools/src/workspaces/getWorkspacePackageInfo.ts +++ /dev/null @@ -1,75 +0,0 @@ -import path from "path"; -import fsPromises from "fs/promises"; -import type { WorkspacePackageInfo, WorkspaceInfos } from "../types/WorkspaceInfo"; -import { PackageInfo } from "../types/PackageInfo"; -import { logVerboseWarning } from "../logging"; -import { infoFromPackageJson } from "../infoFromPackageJson"; -import { getPackageInfo } from "../getPackageInfo"; - -/** - * Get an array with names, paths, and package.json contents for each of the given package paths - * ("workspace" paths in npm/yarn/pnpm terms) within a monorepo. - * - * This is an internal helper used by `getWorkspaces` implementations for different managers. - * - * @param packagePaths Paths to packages within a monorepo - * @returns Array of monorepo package infos - * @internal - */ -export function getWorkspacePackageInfo(packagePaths: string[]): WorkspaceInfos { - if (!packagePaths) { - return []; - } - - return packagePaths - .map((workspacePath) => { - const packageJson = getPackageInfo(workspacePath); - if (!packageJson) { - return null; // getPackageInfo already logged a warning - } - - return { - name: packageJson.name, - path: workspacePath, - packageJson, - }; - }) - .filter(Boolean) as WorkspaceInfos; -} - -/** - * Get an array with names, paths, and package.json contents for each of the given package paths - * ("workspace" paths in npm/yarn/pnpm terms) within a monorepo. - * - * NOTE: As of writing, this will start promises to read all package.json files in parallel, - * without direct concurrency control. - * - * This is an internal helper used by `getWorkspaces` implementations for different managers. - * - * @param packagePaths Paths to packages within a monorepo - * @returns Array of monorepo package infos - * @internal - */ -export async function getWorkspacePackageInfoAsync(packagePaths: string[]): Promise { - if (!packagePaths) { - return []; - } - - const workspacePkgPromises = packagePaths.map>(async (workspacePath) => { - const packageJsonPath = path.join(workspacePath, "package.json"); - - try { - const packageJson = JSON.parse(await fsPromises.readFile(packageJsonPath, "utf-8")) as PackageInfo; - return { - name: packageJson.name, - path: workspacePath, - packageJson: infoFromPackageJson(packageJson, packageJsonPath), - }; - } catch (err) { - logVerboseWarning(`Error reading or parsing ${packageJsonPath} while getting monorepo package info`, err); - return null; - } - }); - - return (await Promise.all(workspacePkgPromises)).filter(Boolean) as WorkspaceInfos; -} diff --git a/packages/workspace-tools/src/workspaces/getWorkspacePackagePaths.ts b/packages/workspace-tools/src/workspaces/getWorkspacePackagePaths.ts index 2abeaffd..2440e8de 100644 --- a/packages/workspace-tools/src/workspaces/getWorkspacePackagePaths.ts +++ b/packages/workspace-tools/src/workspaces/getWorkspacePackagePaths.ts @@ -1,29 +1,137 @@ -import { getWorkspaceManagerAndRoot, getWorkspaceUtilities } from "./implementations"; +import glob, { type Options as GlobOptions } from "fast-glob"; +import path from "path"; +import type { WorkspaceManager } from "../types/WorkspaceManager"; +import { getWorkspaceUtilities, type WorkspaceManagerAndRoot } from "./implementations"; +import { isCachingEnabled } from "../isCachingEnabled"; +import { wrapWorkspaceUtility, wrapAsyncWorkspaceUtility } from "./wrapWorkspaceUtility"; + +/** Mapping from root path to resolved package paths, or undefined if there was an error */ +const packagePathsCache = new Map(); + +const globOptions: GlobOptions = { + absolute: true, + ignore: ["**/node_modules/**", "**/__fixtures__/**"], + stats: false, +}; /** * Get a list of package folder paths in the monorepo. The list of included packages is based on * the manager's config file and matching package folders (which must contain package.json) on disk. + * + * (The list of package paths is cached per monorepo root if caching is enabled.) + * + * @param managerOverride Workspace/monorepo manager to use instead of auto-detecting + * + * @returns Package paths, or undefined if there's any issue + * (logs verbose warnings instead of throwing on error) */ -export function getWorkspacePackagePaths(cwd: string): string[] { - const utils = getWorkspaceUtilities(cwd); - return utils?.getWorkspacePackagePaths(cwd) || []; +export function getWorkspacePackagePaths(cwd: string, managerOverride?: WorkspaceManager): string[] | undefined { + return wrapWorkspaceUtility({ + cwd, + managerOverride, + description: "workspace package paths", + impl: ({ manager, root }) => { + const initialResult = getInitialPathsOrGlobs({ manager, root }); + if (!initialResult || "resolvedPaths" in initialResult) { + return initialResult?.resolvedPaths; + } + + try { + const globResults = glob.sync(initialResult.globs, { cwd: root, ...globOptions }); + return resolveAndCacheGlobResults({ root, globResults }); + } catch (err) { + isCachingEnabled() && packagePathsCache.set(root, undefined); + throw err; + } + }, + }); } /** * Get a list of package folder paths in the monorepo. The list of included packages is based on * the manager's config file and matching package folders (which must contain package.json) on disk. + * + * (The list of package paths is cached per monorepo root if caching is enabled.) + * + * @param managerOverride Workspace/monorepo manager to use instead of auto-detecting + * + * @returns Package paths, or undefined if there's any issue + * (logs verbose warnings instead of throwing on error) */ -export async function getWorkspacePackagePathsAsync(cwd: string): Promise { - const utils = getWorkspaceUtilities(cwd); +export async function getWorkspacePackagePathsAsync( + cwd: string, + managerOverride?: WorkspaceManager +): Promise { + return wrapAsyncWorkspaceUtility({ + cwd, + managerOverride, + description: "workspace package paths", + impl: async ({ manager, root }) => { + const initialResult = getInitialPathsOrGlobs({ manager, root }); + if (!initialResult || "resolvedPaths" in initialResult) { + return initialResult?.resolvedPaths; + } + + try { + const globResults = await glob(initialResult.globs, { cwd: root, ...globOptions }); + return resolveAndCacheGlobResults({ root, globResults }); + } catch (err) { + isCachingEnabled() && packagePathsCache.set(root, undefined); + throw err; + } + }, + }); +} - if (!utils) { - return []; +/** + * Handles the shared synchronous initial logic: + * - Used the cached result if available + * - Else, call `getWorkspacePackagePatterns` + * - If the results are relative paths (not patterns), resolve, cache, and return them + * - Else, return the prepared globs for sync or async resolution as appropriate. + */ +function getInitialPathsOrGlobs( + params: WorkspaceManagerAndRoot +): { globs: string[] } | { resolvedPaths: string[] } | undefined { + const canCache = isCachingEnabled(); + if (canCache && packagePathsCache.has(params.root)) { + // Return the cached result, even if it was a failure + const cachedPaths = packagePathsCache.get(params.root); + return cachedPaths && { resolvedPaths: cachedPaths }; } - if (!utils.getWorkspacePackagePathsAsync) { - const managerName = getWorkspaceManagerAndRoot(cwd)?.manager; - throw new Error(`${cwd} is using ${managerName} which has not been converted to async yet`); + const managerUtilities = getWorkspaceUtilities(params.manager); + const managerSetting = managerUtilities.getWorkspacePatterns({ root: params.root }); + + if (managerSetting?.type === "pattern") { + return { + globs: managerSetting.patterns.map((glob) => path.join(glob, "package.json").replace(/\\/g, "/")), + }; } - return utils.getWorkspacePackagePathsAsync(cwd); + if (managerSetting?.type === "path") { + const resolvedPaths = managerSetting.patterns.map((p) => path.resolve(params.root, p)); + if (canCache) { + packagePathsCache.set(params.root, resolvedPaths); + } + return { resolvedPaths }; + } + + return undefined; +} + +/** + * Given the results from globbing package.json files, resolve to package folder paths + * and cache the results. + */ +function resolveAndCacheGlobResults(params: { root: string; globResults: string[] }) { + const { root, globResults } = params; + const resolvedPaths = globResults.map((packageJsonPath) => { + const packagePath = path.dirname(packageJsonPath); + return path.sep === "/" ? packagePath : packagePath.replace(/\//g, path.sep); + }); + if (isCachingEnabled()) { + packagePathsCache.set(root, resolvedPaths); + } + return resolvedPaths; } diff --git a/packages/workspace-tools/src/workspaces/getWorkspacePatterns.ts b/packages/workspace-tools/src/workspaces/getWorkspacePatterns.ts new file mode 100644 index 00000000..0d70fdb9 --- /dev/null +++ b/packages/workspace-tools/src/workspaces/getWorkspacePatterns.ts @@ -0,0 +1,24 @@ +import type { WorkspaceManager } from "../types/WorkspaceManager"; +import { getWorkspaceUtilities } from "./implementations"; +import { wrapWorkspaceUtility } from "./wrapWorkspaceUtility"; + +/** + * Get the original glob patterns from the manager's workspaces config. + * (Calculation of the workspace manager and root for `cwd` is cached internally.) + * + * @param managerOverride Workspace/monorepo manager to use instead of auto-detecting + * + * @returns Array of patterns, or undefined if not available + * (logs verbose warnings instead of throwing on error) + */ +export function getWorkspacePatterns(cwd: string, managerOverride?: WorkspaceManager): string[] | undefined { + return wrapWorkspaceUtility({ + cwd, + managerOverride, + description: "workspace patterns", + impl: ({ manager, root }) => { + const managerUtilities = getWorkspaceUtilities(manager); + return managerUtilities.getWorkspacePatterns({ root })?.patterns; + }, + }); +} diff --git a/packages/workspace-tools/src/workspaces/getWorkspaceRoot.ts b/packages/workspace-tools/src/workspaces/getWorkspaceRoot.ts deleted file mode 100644 index 1c3ac8a3..00000000 --- a/packages/workspace-tools/src/workspaces/getWorkspaceRoot.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { WorkspaceManager } from "./WorkspaceManager"; -import { getWorkspaceManagerAndRoot } from "./implementations"; - -/** - * Get the root directory of a monorepo, defined as the directory where the workspace/monorepo manager - * config file is located. (Does not rely in any way on git, and the result is cached by `cwd`.) - * - * NOTE: "Workspace" here refers to the entire project/monorepo, not an individual package the way it does - * in e.g. npm/yarn/pnpm "workspaces." - * - * @param cwd Start searching from here - * @param preferredManager Search for only this manager's config file - * - * @deprecated Renamed to `getWorkspaceManagerRoot` to align "workspace" terminology with npm/yarn/pnpm. - * In most cases, you should use `findProjectRoot` instead since it falls back to the git root if no - * workspace manager is found (single-package projects). - */ -export function getWorkspaceRoot(cwd: string, preferredManager?: WorkspaceManager): string | undefined { - return getWorkspaceManagerRoot(cwd, preferredManager); -} - -/** - * Get the root directory of a monorepo, defined as the directory where the workspace/monorepo manager - * config file is located. (Does not rely in any way on git, and the result is cached by `cwd`.) - * - * @param cwd Start searching from here - * @param preferredManager Search for only this manager's config file - * @returns Workspace manager root directory, or undefined if not found - */ -export function getWorkspaceManagerRoot(cwd: string, preferredManager?: WorkspaceManager): string | undefined { - return getWorkspaceManagerAndRoot(cwd, undefined, preferredManager)?.root; -} diff --git a/packages/workspace-tools/src/workspaces/getWorkspaces.ts b/packages/workspace-tools/src/workspaces/getWorkspaces.ts deleted file mode 100644 index b7d8e630..00000000 --- a/packages/workspace-tools/src/workspaces/getWorkspaces.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getWorkspaceUtilities, getWorkspaceManagerAndRoot } from "./implementations"; -import type { WorkspaceInfos } from "../types/WorkspaceInfo"; - -/** - * Get an array with names, paths, and package.json contents for each package ("workspace" in - * npm/yarn/pnpm terms) within a monorepo. The list of included packages is based on the - * workspace/monorepo manager's config file. - */ -export function getWorkspaceInfos(cwd: string): WorkspaceInfos { - const utils = getWorkspaceUtilities(cwd); - return utils?.getWorkspaces(cwd) || []; -} - -/** @deprecated Use `getWorkspaceInfos` */ -export const getWorkspaces = getWorkspaceInfos; - -/** - * Get an array with names, paths, and package.json contents for each package ("workspace" in - * npm/yarn/pnpm terms) within a monorepo. The list of included packages is based on the - * workspace/monorepo manager's config file. - * - * NOTE: As of writing, this will start promises to read all package.json files in parallel, - * without direct concurrency control. - */ -export async function getWorkspaceInfosAsync(cwd: string): Promise { - const utils = getWorkspaceUtilities(cwd); - - if (!utils) { - return []; - } - - if (!utils.getWorkspacesAsync) { - const managerName = getWorkspaceManagerAndRoot(cwd)?.manager; - throw new Error(`${cwd} is using ${managerName} which has not been converted to async yet`); - } - - return utils.getWorkspacesAsync(cwd); -} - -/** @deprecated Use `getWorkspaceInfosAsync` */ -export const getWorkspacesAsync = getWorkspaceInfosAsync; diff --git a/packages/workspace-tools/src/workspaces/implementations/WorkspaceUtilities.ts b/packages/workspace-tools/src/workspaces/implementations/WorkspaceUtilities.ts new file mode 100644 index 00000000..c7212617 --- /dev/null +++ b/packages/workspace-tools/src/workspaces/implementations/WorkspaceUtilities.ts @@ -0,0 +1,28 @@ +import type { Catalogs } from "../../types/Catalogs"; + +/** + * Manager-specific implementations used internally by other workspace/monorepo utilities. + */ +export interface WorkspaceUtilities { + /** + * Get the original glob patterns or package paths from the manager's workspaces config. + * (If workspaces are defined in package.json `workspaces`, use `getPackageJsonWorkspacePatterns`.) + * + * @returns Object with the patterns, or undefined if not available (or it can throw + * if the patterns aren't available or there's an error) + */ + // use object params so it's obvious the root is expected + getWorkspacePatterns: (params: { root: string }) => + | { + patterns: string[]; + /** "pattern" means the strings may be globs, "path" means they're relative paths */ + type: "pattern" | "path"; + } + | undefined; + + /** + * Get version catalogs, if supported by the manager (only pnpm and yarn v4 as of writing). + * Returns undefined if not defined or not supported, or can throw an error. + */ + getCatalogs?: (params: { root: string }) => Catalogs | undefined; +} diff --git a/packages/workspace-tools/src/workspaces/implementations/getPackageJsonWorkspacePatterns.ts b/packages/workspace-tools/src/workspaces/implementations/getPackageJsonWorkspacePatterns.ts new file mode 100644 index 00000000..978ddf01 --- /dev/null +++ b/packages/workspace-tools/src/workspaces/implementations/getPackageJsonWorkspacePatterns.ts @@ -0,0 +1,35 @@ +import fs from "fs"; +import path from "path"; +import type { PackageInfo } from "../../types/PackageInfo"; +import type { WorkspaceUtilities } from "./WorkspaceUtilities"; + +type PackageJsonWithWorkspaces = Pick; + +/** + * Default implementation of `WorkspaceUtilities.getWorkspacePatterns`: + * read the `workspaces` field from package.json. + * + * Throws an error if the `workspaces` field is not found or invalid. + * (Note that this is expected for single-package repos.) + */ +export const getPackageJsonWorkspacePatterns: WorkspaceUtilities["getWorkspacePatterns"] = ({ root }) => { + const packageJsonFile = path.join(root, "package.json"); + + let packageJson: PackageJsonWithWorkspaces; + try { + packageJson = JSON.parse(fs.readFileSync(packageJsonFile, "utf-8")) as PackageJsonWithWorkspaces; + } catch { + throw new Error(`Could not read or parse ${packageJsonFile}`); + } + + const { workspaces } = packageJson; + if (!workspaces) { + throw new Error(`Could not find "workspaces" in ${packageJsonFile} (expected if this is not a monorepo)`); + } + + const patterns = Array.isArray(workspaces) ? workspaces : workspaces?.packages; + if (!patterns) { + throw new Error(`"workspaces" in ${packageJsonFile} does not define "packages"`); + } + return { patterns, type: "pattern" }; +}; diff --git a/packages/workspace-tools/src/workspaces/implementations/getWorkspaceManagerAndRoot.ts b/packages/workspace-tools/src/workspaces/implementations/getWorkspaceManagerAndRoot.ts index 15aacd54..bfe68521 100644 --- a/packages/workspace-tools/src/workspaces/implementations/getWorkspaceManagerAndRoot.ts +++ b/packages/workspace-tools/src/workspaces/implementations/getWorkspaceManagerAndRoot.ts @@ -1,7 +1,7 @@ import path from "path"; -import { searchUp } from "../../paths"; -import { WorkspaceManager } from "../WorkspaceManager"; import { isCachingEnabled } from "../../isCachingEnabled"; +import { searchUp } from "../../paths"; +import { WorkspaceManager } from "../../types/WorkspaceManager"; export interface WorkspaceManagerAndRoot { /** Workspace/monorepo manager name */ @@ -9,6 +9,7 @@ export interface WorkspaceManagerAndRoot { /** Monorepo root, where the manager configuration file is located */ root: string; } + const workspaceCache = new Map(); /** @@ -17,14 +18,14 @@ const workspaceCache = new Map(); * DO NOT REORDER! The order of keys determines the precedence of the files, which is * important for cases like lerna where lerna.json and e.g. yarn.lock may both exist. */ -const managerFiles = { +export const managerFiles = { // DO NOT REORDER! (see above) lerna: "lerna.json", rush: "rush.json", yarn: "yarn.lock", pnpm: "pnpm-workspace.yaml", npm: "package-lock.json", -}; +} as const; /** * Get the preferred workspace/monorepo manager based on `process.env.PREFERRED_WORKSPACE_MANAGER` @@ -37,35 +38,34 @@ export function getPreferredWorkspaceManager(): WorkspaceManager | undefined { /** * Get the workspace/monorepo manager name and root directory for `cwd`, with caching. - * Also respects the `process.env.PREFERRED_WORKSPACE_MANAGER` override, provided the relevant - * manager file exists. + * * @param cwd Directory to search up from * @param cache Optional override cache for testing - * @param preferredManager Optional override manager (if provided, only searches for this manager's file) + * @param managerOverride Optional override manager (if provided, only searches for this manager's file). + * Also respects `process.env.PREFERRED_WORKSPACE_MANAGER`. + * * @returns Workspace/monorepo manager and root, or undefined if it can't be determined */ export function getWorkspaceManagerAndRoot( cwd: string, cache?: Map, - preferredManager?: WorkspaceManager + managerOverride?: WorkspaceManager ): WorkspaceManagerAndRoot | undefined { cache = cache || workspaceCache; if (isCachingEnabled() && cache.has(cwd)) { return cache.get(cwd); } - preferredManager = preferredManager || getPreferredWorkspaceManager(); - const managerFile = searchUp( - (preferredManager && managerFiles[preferredManager]) || Object.values(managerFiles), - cwd - ); + managerOverride ??= getPreferredWorkspaceManager(); + const filesToSearch = managerOverride ? managerFiles[managerOverride] : Object.values(managerFiles); + const managerFile = searchUp(filesToSearch, cwd); if (managerFile) { const managerFileName = path.basename(managerFile); cache.set(cwd, { - manager: (Object.keys(managerFiles) as WorkspaceManager[]).find( - (name) => managerFiles[name] === managerFileName - )!, + manager: + managerOverride || + (Object.keys(managerFiles) as WorkspaceManager[]).find((name) => managerFiles[name] === managerFileName)!, root: path.dirname(managerFile), }); } else { diff --git a/packages/workspace-tools/src/workspaces/implementations/getWorkspaceUtilities.ts b/packages/workspace-tools/src/workspaces/implementations/getWorkspaceUtilities.ts index 6cd8e186..0f26afee 100644 --- a/packages/workspace-tools/src/workspaces/implementations/getWorkspaceUtilities.ts +++ b/packages/workspace-tools/src/workspaces/implementations/getWorkspaceUtilities.ts @@ -1,66 +1,33 @@ -import type { Catalogs } from "../../types/Catalogs"; -import type { WorkspaceInfos } from "../../types/WorkspaceInfo"; -import { getWorkspaceManagerAndRoot } from "./getWorkspaceManagerAndRoot"; -// These must be type imports to avoid parsing the additional deps at runtime -import type * as LernaUtilities from "./lerna"; -import type * as NpmUtilities from "./npm"; -import type * as PnpmUtilities from "./pnpm"; -import type * as RushUtilities from "./rush"; -import type * as YarnUtilities from "./yarn"; +import type { WorkspaceManager } from "../../types/WorkspaceManager"; +import type { WorkspaceUtilities } from "./WorkspaceUtilities"; -export interface WorkspaceUtilities { - /** - * Get an array of paths to packages ("workspaces") in the monorepo, based on the - * manager's config file. - * @returns Array of monorepo package paths, or an empty array on error - */ - getWorkspacePackagePaths: (cwd: string) => string[]; - /** - * Get an array of paths to packages ("workspaces") in the monorepo, based on the - * manager's config file. - * @returns Array of monorepo package paths, or an empty array on error - */ - getWorkspacePackagePathsAsync?: (cwd: string) => Promise; - /** - * Get an array with names, paths, and package.json contents for each package ("workspace") - * in a monorepo. - * @returns Array of monorepo package infos, or an empty array on error - */ - getWorkspaces: (cwd: string) => WorkspaceInfos; - /** - * Get an array with names, paths, and package.json contents for each package ("workspace") - * in a monorepo. - * @returns Array of monorepo package infos, or an empty array on error - */ - getWorkspacesAsync: (cwd: string) => Promise; - /** - * Get version catalogs, if supported by the manager (only pnpm and yarn v4 as of writing). - */ - getCatalogs?: (cwd: string) => Catalogs | undefined; -} +const utils: Partial> = {}; /** - * Get utility implementations for the workspace/monorepo manager of `cwd`. - * It will search up from `cwd` to find a manager file and monorepo root, with caching. - * Returns undefined if the manager can't be determined. + * Get utility implementations for the given workspace/monorepo manager. */ -export function getWorkspaceUtilities(cwd: string): WorkspaceUtilities | undefined { - const manager = getWorkspaceManagerAndRoot(cwd)?.manager; - +export function getWorkspaceUtilities(manager: WorkspaceManager): WorkspaceUtilities { switch (manager) { - case "yarn": - return require("./yarn") as typeof YarnUtilities; + case "npm": + utils.npm ??= (require("./npm") as typeof import("./npm")).npmUtilities; + break; case "pnpm": - return require("./pnpm") as typeof PnpmUtilities; + utils.pnpm ??= (require("./pnpm") as typeof import("./pnpm")).pnpmUtilities; + break; - case "rush": - return require("./rush") as typeof RushUtilities; + case "yarn": + utils.yarn ??= (require("./yarn") as typeof import("./yarn")).yarnUtilities; + break; - case "npm": - return require("./npm") as typeof NpmUtilities; + case "rush": + utils.rush ??= (require("./rush") as typeof import("./rush")).rushUtilities; + break; case "lerna": - return require("./lerna") as typeof LernaUtilities; + utils.lerna ??= (require("./lerna") as typeof import("./lerna")).lernaUtilities; + break; } + + return utils[manager]!; } diff --git a/packages/workspace-tools/src/workspaces/implementations/index.ts b/packages/workspace-tools/src/workspaces/implementations/index.ts index d8aebe9c..0bebc2b8 100644 --- a/packages/workspace-tools/src/workspaces/implementations/index.ts +++ b/packages/workspace-tools/src/workspaces/implementations/index.ts @@ -1,2 +1,2 @@ export { getWorkspaceManagerAndRoot, type WorkspaceManagerAndRoot } from "./getWorkspaceManagerAndRoot"; -export { getWorkspaceUtilities, type WorkspaceUtilities } from "./getWorkspaceUtilities"; +export { getWorkspaceUtilities } from "./getWorkspaceUtilities"; diff --git a/packages/workspace-tools/src/workspaces/implementations/lerna.ts b/packages/workspace-tools/src/workspaces/implementations/lerna.ts index 0645457b..4be4fa54 100644 --- a/packages/workspace-tools/src/workspaces/implementations/lerna.ts +++ b/packages/workspace-tools/src/workspaces/implementations/lerna.ts @@ -1,65 +1,66 @@ import fs from "fs"; import jju from "jju"; import path from "path"; -import { getPackagePaths } from "../../getPackagePaths"; -import type { WorkspaceInfos } from "../../types/WorkspaceInfo"; -import { getWorkspacePackageInfo, getWorkspacePackageInfoAsync } from "../getWorkspacePackageInfo"; -import { logVerboseWarning } from "../../logging"; -import { getWorkspaceManagerAndRoot } from "./getWorkspaceManagerAndRoot"; +import { isCachingEnabled } from "../../isCachingEnabled"; +import { managerFiles } from "./getWorkspaceManagerAndRoot"; +import type { WorkspaceUtilities } from "./WorkspaceUtilities"; -function getLernaRoot(cwd: string): string { - const root = getWorkspaceManagerAndRoot(cwd, undefined, "lerna")?.root; - if (!root) { - throw new Error("Could not find lerna root from " + cwd); - } - return root; -} +export const lernaUtilities: WorkspaceUtilities = { + getWorkspacePatterns: ({ root }) => { + const lernaJsonPath = path.join(root, managerFiles.lerna); + const lernaConfig = jju.parse(fs.readFileSync(lernaJsonPath, "utf-8")) as { packages?: string[] }; + if (lernaConfig.packages) { + return { patterns: lernaConfig.packages, type: "pattern" }; + } + + // Newer lerna versions also pick up workspaces from the package manager. + const actualManager = getActualManager({ root }); + if (!actualManager) { + throw new Error(`${lernaJsonPath} does not define "packages", and no known package manager was found.`); + } + + const managerUtils = getManagerUtils(actualManager); + return managerUtils.getWorkspacePatterns({ root }); + }, + + // lerna could theoretically use yarn or pnpm catalogs + getCatalogs: ({ root }) => { + const actualManager = getActualManager({ root }); + return actualManager && getManagerUtils(actualManager).getCatalogs?.({ root }); + }, +}; + +/** Mapping from lerna repo root to actual package manager */ +const managerCache = new Map(); /** - * Get paths for each package ("workspace") in a lerna monorepo. - * @returns Array of monorepo package paths, or an empty array on error + * Get the actual package manager used by a lerna monorepo (with caching). */ -export function getWorkspacePackagePaths(cwd: string): string[] { - try { - const root = getLernaRoot(cwd); - const lernaJsonPath = path.join(root, "lerna.json"); - const lernaConfig = jju.parse(fs.readFileSync(lernaJsonPath, "utf-8")); - return getPackagePaths(root, lernaConfig.packages); - } catch (err) { - logVerboseWarning(`Error getting lerna workspace package paths for ${cwd}`, err); - return []; +function getActualManager(params: { root: string }): "yarn" | "pnpm" | "npm" | undefined { + const { root } = params; + if (isCachingEnabled() && managerCache.has(root)) { + return managerCache.get(root); } -} -/** - * Get an array with names, paths, and package.json contents for each package ("workspace") - * in a lerna monorepo. - * @returns Array of monorepo package infos, or an empty array on error - */ -export function getLernaWorkspaces(cwd: string): WorkspaceInfos { - try { - const packagePaths = getWorkspacePackagePaths(cwd); - return getWorkspacePackageInfo(packagePaths); - } catch (err) { - logVerboseWarning(`Error getting lerna workspace package infos for ${cwd}`, err); - return []; + for (const manager of ["npm", "yarn", "pnpm"] as const) { + const managerPath = path.join(root, managerFiles[manager]); + if (fs.existsSync(managerPath)) { + managerCache.set(root, manager); + return manager; + } } + + managerCache.set(root, undefined); + return undefined; } -/** - * Get an array with names, paths, and package.json contents for each package ("workspace") - * in a lerna monorepo. - * @returns Array of monorepo package infos, or an empty array on error - */ -export async function getLernaWorkspacesAsync(cwd: string): Promise { - try { - const packagePaths = getWorkspacePackagePaths(cwd); - return getWorkspacePackageInfoAsync(packagePaths); - } catch (err) { - logVerboseWarning(`Error getting lerna workspace package infos for ${cwd}`, err); - return []; +function getManagerUtils(manager: "npm" | "yarn" | "pnpm"): WorkspaceUtilities { + switch (manager) { + case "npm": + return (require("./npm") as typeof import("./npm")).npmUtilities; + case "yarn": + return (require("./yarn") as typeof import("./yarn")).yarnUtilities; + case "pnpm": + return (require("./pnpm") as typeof import("./pnpm")).pnpmUtilities; } } - -export { getLernaWorkspaces as getWorkspaces }; -export { getLernaWorkspacesAsync as getWorkspacesAsync }; diff --git a/packages/workspace-tools/src/workspaces/implementations/npm.ts b/packages/workspace-tools/src/workspaces/implementations/npm.ts index 8ceaf8c9..f5e490d7 100644 --- a/packages/workspace-tools/src/workspaces/implementations/npm.ts +++ b/packages/workspace-tools/src/workspaces/implementations/npm.ts @@ -1,49 +1,7 @@ -import { getWorkspaceManagerAndRoot } from "."; -import type { WorkspaceInfos } from "../../types/WorkspaceInfo"; -import { - getWorkspaceInfoFromWorkspaceRoot, - getWorkspaceInfoFromWorkspaceRootAsync, - getPackagePathsFromWorkspaceRoot, - getPackagePathsFromWorkspaceRootAsync, -} from "./packageJsonWorkspaces"; +import { getPackageJsonWorkspacePatterns } from "./getPackageJsonWorkspacePatterns"; +import type { WorkspaceUtilities } from "./WorkspaceUtilities"; -function getNpmRoot(cwd: string): string { - const root = getWorkspaceManagerAndRoot(cwd, undefined, "npm")?.root; - if (!root) { - throw new Error("Could not find npm root from " + cwd); - } - return root; -} - -/** Get paths for each package ("workspace") in an npm monorepo. */ -export function getWorkspacePackagePaths(cwd: string): string[] { - const root = getNpmRoot(cwd); - return getPackagePathsFromWorkspaceRoot(root); -} - -/** Get paths for each package ("workspace") in an npm monorepo. */ -export function getWorkspacePackagePathsAsync(cwd: string): Promise { - const root = getNpmRoot(cwd); - return getPackagePathsFromWorkspaceRootAsync(root); -} - -/** - * Get an array with names, paths, and package.json contents for each package ("workspace") - * in an npm monorepo. - */ -export function getNpmWorkspaces(cwd: string): WorkspaceInfos { - const root = getNpmRoot(cwd); - return getWorkspaceInfoFromWorkspaceRoot(root); -} - -/** - * Get an array with names, paths, and package.json contents for each package ("workspace") - * in an npm monorepo. - */ -export function getNpmWorkspacesAsync(cwd: string): Promise { - const root = getNpmRoot(cwd); - return getWorkspaceInfoFromWorkspaceRootAsync(root); -} - -export { getNpmWorkspaces as getWorkspaces }; -export { getNpmWorkspacesAsync as getWorkspacesAsync }; +/** npm has no overrides of the default behaviors */ +export const npmUtilities: WorkspaceUtilities = { + getWorkspacePatterns: getPackageJsonWorkspacePatterns, +}; diff --git a/packages/workspace-tools/src/workspaces/implementations/packageJsonWorkspaces.ts b/packages/workspace-tools/src/workspaces/implementations/packageJsonWorkspaces.ts deleted file mode 100644 index 605df52b..00000000 --- a/packages/workspace-tools/src/workspaces/implementations/packageJsonWorkspaces.ts +++ /dev/null @@ -1,101 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { getPackagePaths, getPackagePathsAsync } from "../../getPackagePaths"; -import { getWorkspacePackageInfo, getWorkspacePackageInfoAsync } from "../getWorkspacePackageInfo"; -import { logVerboseWarning } from "../../logging"; -import type { PackageInfo } from "../../types/PackageInfo"; - -type PackageJsonWithWorkspaces = Pick; - -/** - * Read the monorepo root package.json and get the list of package globs from its `workspaces` property. - */ -function getPackages(root: string): string[] { - const packageJsonFile = path.join(root, "package.json"); - - let packageJson: PackageJsonWithWorkspaces; - try { - packageJson = JSON.parse(fs.readFileSync(packageJsonFile, "utf-8")) as PackageJsonWithWorkspaces; - } catch (e) { - throw new Error("Could not load package.json from monorepo root"); - } - - const { workspaces } = packageJson; - - if (Array.isArray(workspaces)) { - return workspaces; - } - - if (!workspaces?.packages) { - throw new Error("Could not find a workspaces object in package.json (expected if this is not a monorepo)"); - } - - return workspaces.packages; -} - -/** - * Read the `workspaces` property the monorepo root package.json, then process the workspace globs - * into absolute package paths. Returns an empty array if the `workspaces` property is not found or - * there's some other issue. - */ -export function getPackagePathsFromWorkspaceRoot(root: string) { - try { - const packageGlobs = getPackages(root); - return packageGlobs ? getPackagePaths(root, packageGlobs) : []; - } catch (err) { - logVerboseWarning(`Error getting package paths for ${root}`, err); - return []; - } -} - -/** - * Read the `workspaces` property the monorepo root package.json, then process the workspace globs - * into absolute package paths. Returns an empty array if the `workspaces` property is not found or - * there's some other issue. - */ -export async function getPackagePathsFromWorkspaceRootAsync(root: string): Promise { - try { - const packageGlobs = getPackages(root); - return packageGlobs ? getPackagePathsAsync(root, packageGlobs) : []; - } catch (err) { - logVerboseWarning(`Error getting package paths for ${root}`, err); - return []; - } -} - -/** - * Read the `workspaces` property the monorepo root package.json, then process the workspace globs - * into an array with names, paths, and package.json contents for each package (each "workspace" - * in npm/yarn/pnpm terms). - * - * @returns Array of monorepo package infos, or an empty array on error - */ -export function getWorkspaceInfoFromWorkspaceRoot(root: string) { - try { - const packagePaths = getPackagePathsFromWorkspaceRoot(root); - return getWorkspacePackageInfo(packagePaths); - } catch (err) { - logVerboseWarning(`Error getting workspace package infos for ${root}`, err); - return []; - } -} - -/** - * Read the `workspaces` property the monorepo root package.json, then process the workspace globs - * into an array with names, paths, and package.json contents for each package (each "workspace" - * in npm/yarn/pnpm terms). - * - * NOTE: As of writing, this will start promises to read all package.json files in parallel, - * without direct concurrency control. - * - * @returns Array of monorepo package infos, or an empty array on error - */ -export async function getWorkspaceInfoFromWorkspaceRootAsync(root: string) { - try { - const packagePaths = await getPackagePathsFromWorkspaceRootAsync(root); - return getWorkspacePackageInfoAsync(packagePaths); - } catch (err) { - logVerboseWarning(`Error getting workspace package infos for ${root}`, err); - return []; - } -} diff --git a/packages/workspace-tools/src/workspaces/implementations/pnpm.ts b/packages/workspace-tools/src/workspaces/implementations/pnpm.ts index 05607666..84f3824b 100644 --- a/packages/workspace-tools/src/workspaces/implementations/pnpm.ts +++ b/packages/workspace-tools/src/workspaces/implementations/pnpm.ts @@ -1,12 +1,8 @@ import path from "path"; - -import { getPackagePaths } from "../../getPackagePaths"; -import type { WorkspaceInfos } from "../../types/WorkspaceInfo"; -import { getWorkspacePackageInfo, getWorkspacePackageInfoAsync } from "../getWorkspacePackageInfo"; import { readYaml } from "../../lockfile/readYaml"; -import { logVerboseWarning } from "../../logging"; -import { getWorkspaceManagerAndRoot } from "./getWorkspaceManagerAndRoot"; -import type { Catalog, Catalogs, NamedCatalogs } from "../../types/Catalogs"; +import type { Catalog, NamedCatalogs } from "../../types/Catalogs"; +import { managerFiles } from "./getWorkspaceManagerAndRoot"; +import type { WorkspaceUtilities } from "./WorkspaceUtilities"; type PnpmWorkspaceYaml = { packages: string[]; @@ -15,74 +11,20 @@ type PnpmWorkspaceYaml = { catalogs?: NamedCatalogs; }; -function getPnpmRootAndYaml(cwd: string) { - const root = getPnpmWorkspaceRoot(cwd); - const pnpmWorkspacesFile = path.join(root, "pnpm-workspace.yaml"); - return { root, workspaceYaml: readYaml(pnpmWorkspacesFile) }; -} - -/** @deprecated Use `getWorkspaceManagerRoot` */ -export function getPnpmWorkspaceRoot(cwd: string): string { - const root = getWorkspaceManagerAndRoot(cwd, undefined, "pnpm")?.root; - if (!root) { - throw new Error("Could not find pnpm root from " + cwd); - } - return root; -} - -/** - * Get paths for each package ("workspace") in a pnpm monorepo. - * @returns Array of monorepo package paths, or an empty array on error - */ -export function getWorkspacePackagePaths(cwd: string): string[] { - try { - const { root, workspaceYaml } = getPnpmRootAndYaml(cwd); - - return getPackagePaths(root, workspaceYaml.packages); - } catch (err) { - logVerboseWarning(`Error getting pnpm workspace package paths for ${cwd}`, err); - return []; - } +function getPnpmWorkspaceYaml(params: { root: string }) { + const pnpmWorkspacesFile = path.join(params.root, managerFiles.pnpm); + return readYaml(pnpmWorkspacesFile); } -/** - * Get an array with names, paths, and package.json contents for each package ("workspace") - * in a pnpm monorepo. - * @returns Array of monorepo package infos, or an empty array on error - */ -export function getPnpmWorkspaces(cwd: string): WorkspaceInfos { - try { - const packagePaths = getWorkspacePackagePaths(cwd); - return getWorkspacePackageInfo(packagePaths); - } catch (err) { - logVerboseWarning(`Error getting pnpm workspace package infos for ${cwd}`, err); - return []; - } -} - -/** - * Get an array with names, paths, and package.json contents for each package ("workspace") - * in a pnpm monorepo. - * @returns Array of monorepo package infos, or an empty array on error - */ -export async function getPnpmWorkspacesAsync(cwd: string): Promise { - try { - const packagePaths = getWorkspacePackagePaths(cwd); - return getWorkspacePackageInfoAsync(packagePaths); - } catch (err) { - logVerboseWarning(`Error getting pnpm workspace package infos for ${cwd}`, err); - return []; - } -} +export const pnpmUtilities: WorkspaceUtilities = { + getWorkspacePatterns: (params) => { + const { packages } = getPnpmWorkspaceYaml(params); + return packages ? { patterns: packages, type: "pattern" } : undefined; + }, -/** - * Get version catalogs if present. - * Returns undefined if there's no catalog, or any issue reading or parsing. - * @see https://pnpm.io/catalogs - */ -export function getPnpmCatalogs(cwd: string): Catalogs | undefined { - try { - const { workspaceYaml } = getPnpmRootAndYaml(cwd); + // See https://pnpm.io/catalogs + getCatalogs: (params) => { + const workspaceYaml = getPnpmWorkspaceYaml(params); if (!workspaceYaml.catalog && !workspaceYaml.catalogs) { return undefined; } @@ -93,12 +35,5 @@ export function getPnpmCatalogs(cwd: string): Catalogs | undefined { default: workspaceYaml.catalog || namedDefaultCatalog, named: Object.keys(namedCatalogs).length ? namedCatalogs : undefined, }; - } catch (err) { - logVerboseWarning(`Error getting pnpm catalogs for ${cwd}`, err); - return undefined; - } -} - -export { getPnpmWorkspaces as getWorkspaces }; -export { getPnpmWorkspacesAsync as getWorkspacesAsync }; -export { getPnpmCatalogs as getCatalogs }; + }, +}; diff --git a/packages/workspace-tools/src/workspaces/implementations/rush.ts b/packages/workspace-tools/src/workspaces/implementations/rush.ts index 77785e5a..af7a4976 100644 --- a/packages/workspace-tools/src/workspaces/implementations/rush.ts +++ b/packages/workspace-tools/src/workspaces/implementations/rush.ts @@ -1,69 +1,16 @@ -import path from "path"; -import jju from "jju"; import fs from "fs"; +import jju from "jju"; +import path from "path"; +import { managerFiles } from "./getWorkspaceManagerAndRoot"; +import type { WorkspaceUtilities } from "./WorkspaceUtilities"; -import type { WorkspaceInfos } from "../../types/WorkspaceInfo"; -import { getWorkspacePackageInfo, getWorkspacePackageInfoAsync } from "../getWorkspacePackageInfo"; -import { logVerboseWarning } from "../../logging"; -import { getWorkspaceManagerAndRoot } from "./getWorkspaceManagerAndRoot"; - -/** @deprecated Use getWorkspaceRoot */ -export function getRushWorkspaceRoot(cwd: string): string { - const root = getWorkspaceManagerAndRoot(cwd, undefined, "rush")?.root; - if (!root) { - throw new Error("Could not find rush root from " + cwd); - } - return root; -} - -/** - * Get paths for each package ("workspace") in a rush monorepo. - * @returns Array of monorepo package paths, or an empty array on error - */ -export function getWorkspacePackagePaths(cwd: string): string[] { - try { - const root = getRushWorkspaceRoot(cwd); - const rushJsonPath = path.join(root, "rush.json"); - - const rushConfig: { projects: Array<{ projectFolder: string }> } = jju.parse( - fs.readFileSync(rushJsonPath, "utf-8") - ); - return rushConfig.projects.map((project) => path.join(root, project.projectFolder)); - } catch (err) { - logVerboseWarning(`Error getting rush workspace package paths for ${cwd}`, err); - return []; - } -} - -/** - * Get an array with names, paths, and package.json contents for each package ("workspace") - * in a rush monorepo. - * @returns Array of monorepo package infos, or an empty array on error - */ -export function getRushWorkspaces(cwd: string): WorkspaceInfos { - try { - const packagePaths = getWorkspacePackagePaths(cwd); - return getWorkspacePackageInfo(packagePaths); - } catch (err) { - logVerboseWarning(`Error getting rush workspace package infos for ${cwd}`, err); - return []; - } -} - -/** - * Get an array with names, paths, and package.json contents for each package ("workspace") - * in a rush monorepo. - * @returns Array of monorepo package infos, or an empty array on error - */ -export async function getRushWorkspacesAsync(cwd: string): Promise { - try { - const packagePaths = getWorkspacePackagePaths(cwd); - return getWorkspacePackageInfoAsync(packagePaths); - } catch (err) { - logVerboseWarning(`Error getting rush workspace package infos for ${cwd}`, err); - return []; - } -} - -export { getRushWorkspaces as getWorkspaces }; -export { getRushWorkspacesAsync as getWorkspacesAsync }; +export const rushUtilities: WorkspaceUtilities = { + getWorkspacePatterns: ({ root }) => { + const rushConfig = jju.parse(fs.readFileSync(path.join(root, managerFiles.rush), "utf-8")) as { + projects: Array<{ projectFolder: string }>; + }; + // The rush config "projects" are single folder paths + const patterns = rushConfig.projects.map((p) => p.projectFolder); + return { patterns, type: "path" }; + }, +}; diff --git a/packages/workspace-tools/src/workspaces/implementations/yarn.ts b/packages/workspace-tools/src/workspaces/implementations/yarn.ts index fc49c44d..87cefc04 100644 --- a/packages/workspace-tools/src/workspaces/implementations/yarn.ts +++ b/packages/workspace-tools/src/workspaces/implementations/yarn.ts @@ -1,73 +1,16 @@ import fs from "fs"; import path from "path"; -import { getWorkspaceManagerAndRoot } from "./index"; -import type { WorkspaceInfos } from "../../types/WorkspaceInfo"; -import { - getPackagePathsFromWorkspaceRoot, - getPackagePathsFromWorkspaceRootAsync, - getWorkspaceInfoFromWorkspaceRoot, - getWorkspaceInfoFromWorkspaceRootAsync, -} from "./packageJsonWorkspaces"; import { getPackageInfo } from "../../getPackageInfo"; -import type { Catalog, Catalogs, NamedCatalogs } from "../../types/Catalogs"; -import { logVerboseWarning } from "../../logging"; +import type { Catalog, NamedCatalogs } from "../../types/Catalogs"; import { readYaml } from "../../lockfile/readYaml"; +import type { WorkspaceUtilities } from "./WorkspaceUtilities"; +import { getPackageJsonWorkspacePatterns } from "./getPackageJsonWorkspacePatterns"; -/** @deprecated Use `getWorkspaceManagerRoot` */ -export function getYarnWorkspaceRoot(cwd: string): string { - const root = getWorkspaceManagerAndRoot(cwd, undefined, "yarn")?.root; - if (!root) { - throw new Error("Could not find yarn root from " + cwd); - } - return root; -} +export const yarnUtilities: WorkspaceUtilities = { + getWorkspacePatterns: getPackageJsonWorkspacePatterns, -/** - * Get paths for each package ("workspace") in a yarn monorepo. - * @returns Array of monorepo package paths, or an empty array on error - */ -export function getWorkspacePackagePaths(cwd: string): string[] { - const root = getYarnWorkspaceRoot(cwd); - return getPackagePathsFromWorkspaceRoot(root); -} - -/** - * Get paths for each package ("workspace") in a yarn monorepo. - * @returns Array of monorepo package paths, or an empty array on error - */ -export function getWorkspacePackagePathsAsync(cwd: string): Promise { - const root = getYarnWorkspaceRoot(cwd); - return getPackagePathsFromWorkspaceRootAsync(root); -} - -/** - * Get an array with names, paths, and package.json contents for each package ("workspace") - * in a yarn monorepo. - * @returns Array of monorepo package infos, or an empty array on error - */ -export function getYarnWorkspaces(cwd: string): WorkspaceInfos { - const root = getYarnWorkspaceRoot(cwd); - return getWorkspaceInfoFromWorkspaceRoot(root); -} - -/** - * Get an array with names, paths, and package.json contents for each package ("workspace") - * in a yarn monorepo. - * @returns Array of monorepo package infos, or an empty array on error - */ -export function getYarnWorkspacesAsync(cwd: string): Promise { - const root = getYarnWorkspaceRoot(cwd); - return getWorkspaceInfoFromWorkspaceRootAsync(root); -} - -/** - * Get version catalogs if present. - * Returns undefined if there's no catalog, or any issue reading or parsing. - * @see https://yarnpkg.com/features/catalogs - */ -export function getYarnCatalogs(cwd: string): Catalogs | undefined { - try { - const root = getYarnWorkspaceRoot(cwd); + // See https://yarnpkg.com/features/catalogs + getCatalogs: ({ root }) => { const yarnrcYmlPath = path.join(root, ".yarnrc.yml"); if (fs.existsSync(yarnrcYmlPath)) { const yarnrcYml = readYaml<{ catalog?: Catalog; catalogs?: NamedCatalogs }>(yarnrcYmlPath); @@ -91,12 +34,6 @@ export function getYarnCatalogs(cwd: string): Catalogs | undefined { }; } } - } catch (err) { - logVerboseWarning(`Error getting yarn catalogs for ${cwd}`, err); return undefined; - } -} - -export { getYarnWorkspaces as getWorkspaces }; -export { getYarnWorkspacesAsync as getWorkspacesAsync }; -export { getYarnCatalogs as getCatalogs }; + }, +}; diff --git a/packages/workspace-tools/src/workspaces/listOfWorkspacePackageNames.ts b/packages/workspace-tools/src/workspaces/listOfWorkspacePackageNames.ts deleted file mode 100644 index 1caf7a9a..00000000 --- a/packages/workspace-tools/src/workspaces/listOfWorkspacePackageNames.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { WorkspaceInfos } from "../types/WorkspaceInfo"; - -/** - * @deprecated Just write `workspaces.map(w => w.name)` directly - */ -export function listOfWorkspacePackageNames(workspaces: WorkspaceInfos): string[] { - return workspaces.map(({ name }) => name); -} diff --git a/packages/workspace-tools/src/workspaces/wrapWorkspaceUtility.ts b/packages/workspace-tools/src/workspaces/wrapWorkspaceUtility.ts new file mode 100644 index 00000000..cc436e69 --- /dev/null +++ b/packages/workspace-tools/src/workspaces/wrapWorkspaceUtility.ts @@ -0,0 +1,63 @@ +import { logVerboseWarning } from "../logging"; +import type { WorkspaceManager } from "../types/WorkspaceManager"; +import { getWorkspaceManagerAndRoot, type WorkspaceManagerAndRoot } from "./implementations"; + +interface WrappedUtilityParams { + /** Search for the root from here */ + cwd: string; + /** Optional manager to use instead of auto-detecting */ + managerOverride: WorkspaceManager | undefined; + /** What the utility is getting (for logging) */ + description: string; +} + +/** + * Wrap a workspace utility function with common error handling and messaging. + * Also handle getting the manager and root. + * + * @returns The utility's return value, or undefined on error (will verbose log on error) + */ +export function wrapWorkspaceUtility( + params: WrappedUtilityParams & { + /** Implementation of the utility, which receives the detected manager and root */ + impl: (params: WorkspaceManagerAndRoot) => TReturn | undefined; + } +): TReturn | undefined { + const { cwd, description, impl } = params; + let managerInfo: WorkspaceManagerAndRoot | undefined; + try { + managerInfo = getWorkspaceManagerAndRoot(cwd, undefined, params.managerOverride); + if (managerInfo) { + return impl(managerInfo); + } + } catch (err) { + const manager = params.managerOverride || managerInfo?.manager || "unknown manager"; + logVerboseWarning(`Error getting ${manager} ${description} for ${cwd}:`, err); + } +} + +/** + * Wrap a workspace utility function with common error handling and messaging. + * Also handle getting the manager and root. + * + * @returns The utility's return value, or undefined on error (will verbose log on error) + */ +export async function wrapAsyncWorkspaceUtility( + params: WrappedUtilityParams & { + /** Implementation of the utility, which receives the detected manager and root */ + impl: (params: WorkspaceManagerAndRoot) => Promise; + } +): Promise { + const { cwd, description, impl } = params; + let managerInfo: WorkspaceManagerAndRoot | undefined; + try { + managerInfo = getWorkspaceManagerAndRoot(cwd, undefined, params.managerOverride); + if (managerInfo) { + return await impl(managerInfo); + } + } catch (err) { + const manager = params.managerOverride || managerInfo?.manager || "unknown manager"; + logVerboseWarning(`Error getting ${manager} ${description} for ${cwd}:`, err); + } + return undefined; +} diff --git a/yarn.lock b/yarn.lock index a51523ee..6a9a4b4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3221,6 +3221,19 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== +workspace-tools@0.40.2: + version "0.40.2" + resolved "https://registry.yarnpkg.com/workspace-tools/-/workspace-tools-0.40.2.tgz#a141fa69f62ebf330c168871d1124b5a4de77280" + integrity sha512-k7bNIMxE38dRZc3kSLQvEEejZ1tnozPzHeCI4aQ0PrcyJoSOJ2yR6FCIdHpcs57UGZkB2HllVFFjnEB9R0G2Rw== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + fast-glob "^3.3.1" + git-url-parse "^16.0.0" + globby "^11.0.0" + jju "^1.4.0" + js-yaml "^4.1.0" + micromatch "^4.0.0" + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"