Skip to content

Commit

Permalink
Allow the caller to override the list of supported tools (#167)
Browse files Browse the repository at this point in the history
* Provide an interface for overriding the tool implementation list

* Feedback

* Use an options interface instead of a parameter

* Apply suggestions from code review

---------

Co-authored-by: Mateusz Burzyński <[email protected]>
  • Loading branch information
elliot-nelson and Andarist authored Jan 31, 2023
1 parent 1bff0a2 commit bf586f5
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/many-pumpkin-behavior.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@manypkg/find-root": minor
---

The `findRoot` and `findRootSync` methods now take an optional list of `Tool` implementations, allowing the caller to restrict the desired types of monorepo discovered, or provide a custom monorepo tool implementation.
5 changes: 5 additions & 0 deletions .changeset/odd-lies-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@manypkg/get-packages": minor
---

The `getPackages` and `getPackagesSync` methods now take an optional list of `Tool` implementations, allowing the caller to restrict the desired types of monorepo discovered, or provide a custom monorepo tool implementation.
11 changes: 11 additions & 0 deletions packages/find-root/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ const runTests = (findRoot: FindRoot) => {
rootDir: tmpPath,
});
});

test("it will not find a lerna monorepo if only PnpmTool is allowed", async () => {
let tmpPath = f.copy("basic-lerna");
await expect(async () =>
findRoot(path.join(tmpPath, "packages", "package-one", "src"), {
tools: [PnpmTool],
})
).rejects.toThrowError(
/No monorepo matching the list of supported monorepos could be found upwards from directory /
);
});
};

describe("findRoot", () => {
Expand Down
64 changes: 58 additions & 6 deletions packages/find-root/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,65 @@ import {
* monorepo implementations first, with tools based on custom file schemas
* checked last.
*/
const defaultOrder: Tool[] = [YarnTool, PnpmTool, LernaTool, RushTool];
const DEFAULT_TOOLS: Tool[] = [
YarnTool,
PnpmTool,
LernaTool,
RushTool,
RootTool,
];

const isNoEntryError = (err: unknown): boolean =>
!!err && typeof err === "object" && "code" in err && err.code === "ENOENT";

export class NoPkgJsonFound extends Error {
directory: string;
constructor(directory: string) {
super(`No package.json could be found upwards from directory ${directory}`);
this.directory = directory;
}
}

export class NoMatchingMonorepoFound extends Error {
directory: string;
constructor(directory: string) {
super(
`No package.json could be found upwards from the directory ${directory}`
`No monorepo matching the list of supported monorepos could be found upwards from directory ${directory}`
);
this.directory = directory;
}
}

export async function findRoot(cwd: string): Promise<MonorepoRoot> {
/**
* Configuration options for `findRoot` and `findRootSync` functions.
*/
export interface FindRootOptions {
/**
* Override the list of monorepo tool implementations that are used during the search.
*/
tools?: Tool[];
}

/**
* Given a starting folder, search that folder and its parents until a supported monorepo
* is found, and return a `MonorepoRoot` object with the discovered directory and a
* corresponding monorepo `Tool` object.
*
* By default, all predefined `Tool` implementations are included in the search -- the
* caller can provide a list of desired tools to restrict the types of monorepos discovered,
* or to provide a custom tool implementation.
*/
export async function findRoot(
cwd: string,
options: FindRootOptions = {}
): Promise<MonorepoRoot> {
let monorepoRoot: MonorepoRoot | undefined;
const tools = options.tools || DEFAULT_TOOLS;

await findUp(
async (directory) => {
return Promise.all(
defaultOrder.map(async (tool): Promise<MonorepoRoot | undefined> => {
tools.map(async (tool): Promise<MonorepoRoot | undefined> => {
if (await tool.isMonorepoRoot(directory)) {
return {
tool: tool,
Expand All @@ -64,6 +101,10 @@ export async function findRoot(cwd: string): Promise<MonorepoRoot> {
return monorepoRoot;
}

if (!tools.includes(RootTool)) {
throw new NoMatchingMonorepoFound(cwd);
}

// If there is no monorepo root, but we can find a single package json file, we will
// return a "RootTool" repo, which is the special case where we just have a root package
// with no monorepo implementation (i.e.: a normal package folder).
Expand Down Expand Up @@ -91,12 +132,19 @@ export async function findRoot(cwd: string): Promise<MonorepoRoot> {
};
}

export function findRootSync(cwd: string): MonorepoRoot {
/**
* A synchronous version of {@link findRoot}.
*/
export function findRootSync(
cwd: string,
options: FindRootOptions = {}
): MonorepoRoot {
let monorepoRoot: MonorepoRoot | undefined;
const tools = options.tools || DEFAULT_TOOLS;

findUpSync(
(directory) => {
for (const tool of defaultOrder) {
for (const tool of tools) {
if (tool.isMonorepoRootSync(directory)) {
monorepoRoot = {
tool: tool,
Expand All @@ -113,6 +161,10 @@ export function findRootSync(cwd: string): MonorepoRoot {
return monorepoRoot;
}

if (!tools.includes(RootTool)) {
throw new NoMatchingMonorepoFound(cwd);
}

// If there is no monorepo root, but we can find a single package json file, we will
// return a "RootTool" repo, which is the special case where we just have a root package
// with no monorepo implementation (i.e.: a normal package folder).
Expand Down
35 changes: 29 additions & 6 deletions packages/get-packages/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import path from "path";
import { findRoot, findRootSync } from "@manypkg/find-root";
import { Packages, MonorepoRoot } from "@manypkg/tools";
import { findRoot, findRootSync, FindRootOptions } from "@manypkg/find-root";
import { Packages, MonorepoRoot, Tool } from "@manypkg/tools";

export type { Tool, Package, Packages } from "@manypkg/tools";

Expand All @@ -16,17 +16,40 @@ export class PackageJsonMissingNameError extends Error {
}
}

export async function getPackages(dir: string): Promise<Packages> {
const monorepoRoot: MonorepoRoot = await findRoot(dir);
/**
* Configuration options for `getPackages` and `getPackagesSync` functions.
*/
export interface GetPackagesOptions extends FindRootOptions {}

/**
* Given a starting folder, search that folder and its parents until a supported monorepo
* is found, and return the collection of packages and a corresponding monorepo `Tool`
* object.
*
* By default, all predefined `Tool` implementations are included in the search -- the
* caller can provide a list of desired tools to restrict the types of monorepos discovered,
* or to provide a custom tool implementation.
*/
export async function getPackages(
dir: string,
options?: GetPackagesOptions
): Promise<Packages> {
const monorepoRoot: MonorepoRoot = await findRoot(dir, options);
const packages: Packages = await monorepoRoot.tool.getPackages(dir);

validatePackages(packages);

return packages;
}

export function getPackagesSync(dir: string): Packages {
const monorepoRoot: MonorepoRoot = findRootSync(dir);
/**
* A synchronous version of {@link getPackages}.
*/
export function getPackagesSync(
dir: string,
options?: GetPackagesOptions
): Packages {
const monorepoRoot: MonorepoRoot = findRootSync(dir, options);
const packages: Packages = monorepoRoot.tool.getPackagesSync(dir);

validatePackages(packages);
Expand Down

0 comments on commit bf586f5

Please sign in to comment.