diff --git a/.changeset/large-plants-sit.md b/.changeset/large-plants-sit.md new file mode 100644 index 00000000..c1241a9b --- /dev/null +++ b/.changeset/large-plants-sit.md @@ -0,0 +1,7 @@ +--- +"@manypkg/get-packages": major +"@manypkg/find-root": major +"@manypkg/tools": major +--- + +Fixed an issue with projects using npm workspaces being recognized as yarn projects diff --git a/__fixtures__/basic-npm/package-lock.json b/__fixtures__/basic-npm/package-lock.json new file mode 100644 index 00000000..e69de29b diff --git a/__fixtures__/basic-npm/package.json b/__fixtures__/basic-npm/package.json new file mode 100644 index 00000000..52e8d8bb --- /dev/null +++ b/__fixtures__/basic-npm/package.json @@ -0,0 +1,7 @@ +{ + "name": "@manypkg/basic-npm-fixture", + "version": "1.0.0", + "workspaces": [ + "packages/*" + ] +} diff --git a/__fixtures__/basic-npm/packages/package-one/package.json b/__fixtures__/basic-npm/packages/package-one/package.json new file mode 100644 index 00000000..4bb8df3c --- /dev/null +++ b/__fixtures__/basic-npm/packages/package-one/package.json @@ -0,0 +1,4 @@ +{ + "name": "@manypkg/basic-fixture-pkg-one", + "version": "1.0.0" +} diff --git a/__fixtures__/basic-npm/packages/package-one/src/index.js b/__fixtures__/basic-npm/packages/package-one/src/index.js new file mode 100644 index 00000000..e69de29b diff --git a/__fixtures__/basic-with-scripts/yarn.lock b/__fixtures__/basic-with-scripts/yarn.lock new file mode 100644 index 00000000..e69de29b diff --git a/__fixtures__/basic/yarn.lock b/__fixtures__/basic/yarn.lock new file mode 100644 index 00000000..e69de29b diff --git a/__fixtures__/local-deps-cycle/yarn.lock b/__fixtures__/local-deps-cycle/yarn.lock new file mode 100644 index 00000000..e69de29b diff --git a/__fixtures__/no-name-field/yarn.lock b/__fixtures__/no-name-field/yarn.lock new file mode 100644 index 00000000..e69de29b diff --git a/__fixtures__/npm-workspace-base/package-lock.json b/__fixtures__/npm-workspace-base/package-lock.json new file mode 100644 index 00000000..e69de29b diff --git a/__fixtures__/npm-workspace-base/package.json b/__fixtures__/npm-workspace-base/package.json new file mode 100644 index 00000000..6e593ba1 --- /dev/null +++ b/__fixtures__/npm-workspace-base/package.json @@ -0,0 +1,9 @@ +{ + "private": true, + "name": "npm-workspace-base", + "description": "Base npm workspace work", + "version": "1.0.0", + "workspaces": [ + "packages/*" + ] +} diff --git a/__fixtures__/npm-workspace-base/packages/pkg-a/package.json b/__fixtures__/npm-workspace-base/packages/pkg-a/package.json new file mode 100644 index 00000000..ba221203 --- /dev/null +++ b/__fixtures__/npm-workspace-base/packages/pkg-a/package.json @@ -0,0 +1,7 @@ +{ + "name": "npm-workspace-base-pkg-a", + "version": "1.0.0", + "dependencies": { + "npm-workspace-base-pkg-b": "1.0.0" + } +} diff --git a/__fixtures__/npm-workspace-base/packages/pkg-b/package.json b/__fixtures__/npm-workspace-base/packages/pkg-b/package.json new file mode 100644 index 00000000..44e92522 --- /dev/null +++ b/__fixtures__/npm-workspace-base/packages/pkg-b/package.json @@ -0,0 +1,4 @@ +{ + "name": "npm-workspace-base-pkg-b", + "version": "1.0.0" +} diff --git a/__fixtures__/yarn-workspace-base/yarn.lock b/__fixtures__/yarn-workspace-base/yarn.lock new file mode 100644 index 00000000..e69de29b diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index 599b018e..b6a5b9c8 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -18,6 +18,7 @@ export async function install(toolType: string, cwd: string) { const cliRunners: Record = { bolt: "bolt", lerna: "lerna", + npm: "npm", pnpm: "pnpm", root: "yarn", rush: "rushx", @@ -26,7 +27,7 @@ export async function install(toolType: string, cwd: string) { await exec( cliRunners[toolType], - toolType === "pnpm" + toolType === "npm" || toolType === "pnpm" ? ["install"] : toolType === "lerna" ? ["bootstrap", "--since", "HEAD"] diff --git a/packages/find-root/src/index.test.ts b/packages/find-root/src/index.test.ts index 635f7a10..961e6422 100644 --- a/packages/find-root/src/index.test.ts +++ b/packages/find-root/src/index.test.ts @@ -3,7 +3,13 @@ import fixturez from "fixturez"; import path from "node:path"; import { findRoot, findRootSync } from "./index.ts"; -import { LernaTool, PnpmTool, RootTool, YarnTool } from "@manypkg/tools"; +import { + LernaTool, + NpmTool, + PnpmTool, + RootTool, + YarnTool, +} from "@manypkg/tools"; let f = fixturez(__dirname); @@ -21,6 +27,17 @@ const runTests = (findRoot: FindRoot) => { }); }); + test("it returns the root of an npm monorepo", async () => { + let tmpPath = f.copy("basic-npm"); + let monorepoRoot = await findRoot( + path.join(tmpPath, "packages", "package-one", "src") + ); + expect(monorepoRoot).toEqual({ + tool: NpmTool.type, + rootDir: tmpPath, + }); + }); + test("it returns the root of a lerna monorepo", async () => { let tmpPath = f.copy("basic-lerna"); let monorepoRoot = await findRoot( diff --git a/packages/find-root/src/index.ts b/packages/find-root/src/index.ts index 8c449b88..3f79d5ff 100644 --- a/packages/find-root/src/index.ts +++ b/packages/find-root/src/index.ts @@ -1,16 +1,16 @@ -import path from "node:path"; import fs from "node:fs"; import fsp from "node:fs/promises"; - +import path from "node:path"; import { - type Tool, - RootTool, - type MonorepoRoot, BoltTool, LernaTool, + NpmTool, PnpmTool, + RootTool, RushTool, YarnTool, + type MonorepoRoot, + type Tool, } from "@manypkg/tools"; /** @@ -23,6 +23,7 @@ import { export const DEFAULT_TOOLS: Tool[] = [ YarnTool, PnpmTool, + NpmTool, LernaTool, RushTool, BoltTool, diff --git a/packages/get-packages/src/index.test.ts b/packages/get-packages/src/index.test.ts index a6582496..e000ceb8 100644 --- a/packages/get-packages/src/index.test.ts +++ b/packages/get-packages/src/index.test.ts @@ -29,6 +29,27 @@ let runTests = (getPackages: GetPackages) => { } }); + it("should resolve workspaces for npm", async () => { + const dir = f.copy("npm-workspace-base"); + + // Test for both root and subdirectories + for (const location of [".", "packages", "packages/pkg-a"]) { + const allPackages = await getPackages(path.join(dir, location)); + + if (allPackages.packages === null) { + return expect(allPackages.packages).not.toBeNull(); + } + + expect(allPackages.packages[0].packageJson.name).toEqual( + "npm-workspace-base-pkg-a" + ); + expect(allPackages.packages[1].packageJson.name).toEqual( + "npm-workspace-base-pkg-b" + ); + expect(allPackages.tool.type).toEqual("npm"); + } + }); + it("should resolve yarn workspaces if the yarn option is passed and packages field is used", async () => { const allPackages = await getPackages(f.copy("yarn-workspace-base")); diff --git a/packages/tools/src/NpmTool.ts b/packages/tools/src/NpmTool.ts new file mode 100644 index 00000000..92dd09fd --- /dev/null +++ b/packages/tools/src/NpmTool.ts @@ -0,0 +1,119 @@ +import path from "node:path"; +import fs from "node:fs"; +import fsp from "node:fs/promises"; +import { F_OK } from "node:constants"; + +import { + InvalidMonorepoError, + type PackageJSON, + type Packages, + type Tool, +} from "./Tool.ts"; +import { + expandPackageGlobs, + expandPackageGlobsSync, +} from "./expandPackageGlobs.ts"; +import { readJson, readJsonSync } from "./utils.ts"; + +export interface NpmPackageJSON extends PackageJSON { + workspaces?: string[]; +} + +export const NpmTool: Tool = { + type: "npm", + + async isMonorepoRoot(directory: string): Promise { + try { + const [pkgJson] = await Promise.all([ + readJson(directory, "package.json") as Promise, + fsp.access(path.join(directory, "package-lock.json"), F_OK), + ]); + if (pkgJson.workspaces) { + if (Array.isArray(pkgJson.workspaces)) { + return true; + } + } + } catch (err) { + if (err && (err as { code: string }).code === "ENOENT") { + return false; + } + throw err; + } + return false; + }, + + isMonorepoRootSync(directory: string): boolean { + try { + fs.accessSync(path.join(directory, "package-lock.json"), F_OK); + const pkgJson = readJsonSync(directory, "package.json") as NpmPackageJSON; + if (pkgJson.workspaces) { + if (Array.isArray(pkgJson.workspaces)) { + return true; + } + } + } catch (err) { + if (err && (err as { code: string }).code === "ENOENT") { + return false; + } + throw err; + } + return false; + }, + + async getPackages(directory: string): Promise { + const rootDir = path.resolve(directory); + + try { + const pkgJson = (await readJson( + rootDir, + "package.json" + )) as NpmPackageJSON; + const packageGlobs: string[] = pkgJson.workspaces!; + + return { + tool: NpmTool, + packages: await expandPackageGlobs(packageGlobs, rootDir), + rootPackage: { + dir: rootDir, + relativeDir: ".", + packageJson: pkgJson, + }, + rootDir, + }; + } catch (err) { + if (err && (err as { code: string }).code === "ENOENT") { + throw new InvalidMonorepoError( + `Directory ${rootDir} is not a valid ${NpmTool.type} monorepo root` + ); + } + throw err; + } + }, + + getPackagesSync(directory: string): Packages { + const rootDir = path.resolve(directory); + + try { + const pkgJson = readJsonSync(rootDir, "package.json") as NpmPackageJSON; + const packageGlobs: string[] = pkgJson.workspaces!; + + return { + tool: NpmTool, + packages: expandPackageGlobsSync(packageGlobs, rootDir), + rootPackage: { + dir: rootDir, + relativeDir: ".", + packageJson: pkgJson, + }, + rootDir, + }; + } catch (err) { + if (err && (err as { code: string }).code === "ENOENT") { + throw new InvalidMonorepoError( + `Directory ${rootDir} is not a valid ${NpmTool.type} monorepo root` + ); + } + throw err; + } + }, +}; diff --git a/packages/tools/src/RushTool.ts b/packages/tools/src/RushTool.ts index 3b75a8c8..4f204bd7 100644 --- a/packages/tools/src/RushTool.ts +++ b/packages/tools/src/RushTool.ts @@ -1,14 +1,15 @@ -import path from "node:path"; +import jju from "jju"; +import { F_OK } from "node:constants"; import fs from "node:fs"; import fsp from "node:fs/promises"; -import jju from "jju"; +import path from "node:path"; import { - type Tool, + InvalidMonorepoError, type Package, type PackageJSON, type Packages, - InvalidMonorepoError, + type Tool, } from "./Tool.ts"; import { readJson, readJsonSync } from "./utils.ts"; @@ -26,7 +27,7 @@ export const RushTool: Tool = { async isMonorepoRoot(directory: string): Promise { try { - await fsp.readFile(path.join(directory, "rush.json"), "utf8"); + await fsp.access(path.join(directory, "rush.json"), F_OK); return true; } catch (err) { if (err && (err as { code: string }).code === "ENOENT") { @@ -38,7 +39,7 @@ export const RushTool: Tool = { isMonorepoRootSync(directory: string): boolean { try { - fs.readFileSync(path.join(directory, "rush.json"), "utf8"); + fs.accessSync(path.join(directory, "rush.json"), F_OK); return true; } catch (err) { if (err && (err as { code: string }).code === "ENOENT") { diff --git a/packages/tools/src/YarnTool.ts b/packages/tools/src/YarnTool.ts index ed226746..bdbdb388 100644 --- a/packages/tools/src/YarnTool.ts +++ b/packages/tools/src/YarnTool.ts @@ -1,4 +1,7 @@ import path from "node:path"; +import fs from "node:fs"; +import fsp from "node:fs/promises"; +import { F_OK } from "node:constants"; import { type Tool, @@ -21,10 +24,10 @@ export const YarnTool: Tool = { async isMonorepoRoot(directory: string): Promise { try { - const pkgJson = (await readJson( - directory, - "package.json" - )) as YarnPackageJSON; + const [pkgJson] = await Promise.all([ + readJson(directory, "package.json") as Promise, + fsp.access(path.join(directory, "yarn.lock"), F_OK), + ]); if (pkgJson.workspaces) { if ( Array.isArray(pkgJson.workspaces) || @@ -44,6 +47,7 @@ export const YarnTool: Tool = { isMonorepoRootSync(directory: string): boolean { try { + fs.accessSync(path.join(directory, "yarn.lock"), F_OK); const pkgJson = readJsonSync( directory, "package.json" diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index ad24b891..34e9e6d1 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -1,6 +1,7 @@ export * from "./Tool.ts"; export { BoltTool } from "./BoltTool.ts"; export { LernaTool } from "./LernaTool.ts"; +export { NpmTool } from "./NpmTool.ts"; export { PnpmTool } from "./PnpmTool.ts"; export { RootTool } from "./RootTool.ts"; export { RushTool } from "./RushTool.ts";