From c8933b1eaf944afbf7badc9583dcb4fb65b39444 Mon Sep 17 00:00:00 2001 From: "lz.piotr" Date: Tue, 25 Oct 2022 12:59:55 +0200 Subject: [PATCH 1/2] feat: add yarn support --- README.md | 5 +- src/index.ts | 26 ++++++--- src/pnpmWorkspace.ts | 5 ++ src/types.ts | 5 ++ src/yarnWorkspace.ts | 123 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 src/yarnWorkspace.ts diff --git a/README.md b/README.md index 94e4b36..67fc66a 100644 --- a/README.md +++ b/README.md @@ -59,4 +59,7 @@ Moreover, deployment statuses are reported as successful even when builds are sk Then, it proceeds to check whether the given package or any of its dependencies were modified since the last commit with the use of `git diff "HEAD^" "HEAD" --quiet`. -Currently, only `pnpm` workspaces are supported but more is on the roadmap. +Currently, only `pnpm` and `yarn` workspaces are supported but more is on the roadmap. + +### Yarn workspaces +If you are using `yarn` workspaces pass `--yarn` flag \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ec0ea69..9271659 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,10 @@ #!/usr/bin/env node -import { existsSync } from "node:fs"; import Path from "node:path"; import { promiseErrorToSettled } from "./utils.js"; -import { - readWorkspaceDirs, - readWorkspaceSettings, - resolveWorkspaceDeps, -} from "./pnpmWorkspace.js"; +import * as pnpm from "./pnpmWorkspace.js"; +import * as yarn from "./yarnWorkspace.js"; import { compare } from "./git.js"; import { debug } from "./debug.js"; import { parseArgs } from "./parseArgs.js"; @@ -25,9 +21,20 @@ const configuration = parseArgs({ type: "boolean", default: false, }, + yarn: { + type: "boolean", + default: false, + }, }, }); +const { + readWorkspaceDirs, + readWorkspaceSettings, + resolveWorkspaceDeps, + isRootDir, +} = configuration.values.yarn ? yarn : pnpm; + const log = debug(configuration.values.verbose); const [gitFromPointer = "HEAD^", gitToPointer = "HEAD"] = @@ -38,7 +45,7 @@ log({ gitFromPointer, gitToPointer }); const rootDir = cwd .split(Path.sep) .map((_, idx) => Path.join(cwd, "../".repeat(idx))) - .find((path) => existsSync(Path.join(path, "pnpm-workspace.yaml"))); + .find((path) => isRootDir(path)); log({ rootDir }); @@ -47,11 +54,16 @@ if (!rootDir) { } const workspaceSettings = await readWorkspaceSettings({ rootDir, cwd }); + +log(workspaceSettings); + const workspaceDeps = resolveWorkspaceDeps( workspaceSettings.workspaces, workspaceSettings.currentWorkspace, ); +log({ workspaceDeps }); + const workspaceDepsPaths = workspaceDeps .map((name) => workspaceSettings.workspaces[name]?.packagePath) .filter((path): path is string => typeof path === "string"); diff --git a/src/pnpmWorkspace.ts b/src/pnpmWorkspace.ts index a6581c4..40a1a81 100644 --- a/src/pnpmWorkspace.ts +++ b/src/pnpmWorkspace.ts @@ -1,5 +1,6 @@ import { readFile, readdir } from "node:fs/promises"; import Path from "node:path"; +import { existsSync } from "node:fs"; import { fileExist, readJson } from "./utils.js"; import { Ctx, Workspace, PackageJson } from "./types.js"; @@ -95,3 +96,7 @@ function getWorkspaceDeps(pkg: PackageJson) { .map(([name]) => name); return [...new Set(deps)]; } + +export function isRootDir(path: string) { + return existsSync(Path.join(path, "pnpm-workspace.yaml")); +} diff --git a/src/types.ts b/src/types.ts index 13a1148..a7b776f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,3 +14,8 @@ export type Workspace = { packagePath: string; dependsOn: string[]; }; + +export type WorkspaceSettings = { + workspaces: Record; + currentWorkspace: Workspace; +}; diff --git a/src/yarnWorkspace.ts b/src/yarnWorkspace.ts new file mode 100644 index 0000000..7468912 --- /dev/null +++ b/src/yarnWorkspace.ts @@ -0,0 +1,123 @@ +import { readFile, readdir } from "node:fs/promises"; +import Path from "node:path"; +import { existsSync, readFileSync } from "node:fs"; +import { fileExist, readJson } from "./utils.js"; +import { Ctx, Workspace, WorkspaceSettings, PackageJson } from "./types.js"; + +export async function readWorkspaceSettings({ + rootDir, + cwd, +}: Ctx): Promise { + const workspaceDirs = await readWorkspaceDirs({ rootDir, cwd }); + + const workspaces = (await Promise.all(workspaceDirs.map(findPackagesInDir))) + .flat() + .map((w) => [w.name, w] as const); + const currentWorkspace = workspaces.find( + ([, w]) => w.packagePath === cwd, + )?.[1]; + + if (!currentWorkspace) { + throw new Error(`Couldn't find currentWorkspace: ${cwd}`); + } + const { name } = currentWorkspace; + if (!name) { + throw new Error(`Workspace must have name: ${cwd}`); + } + + return withoutNodeModules({ + workspaces: Object.fromEntries(workspaces), + currentWorkspace: { ...currentWorkspace, name }, + }); +} + +export function resolveWorkspaceDeps( + allWorkspaces: Record, + { dependsOn }: Workspace, +): string[] { + return [ + ...new Set([ + ...dependsOn, + ...dependsOn.flatMap((d) => + allWorkspaces[d] + ? resolveWorkspaceDeps(allWorkspaces, allWorkspaces[d]!) + : [], + ), + ]), + ]; +} + +export async function readWorkspaceDirs({ rootDir }: Ctx) { + const workspaceSettingsPath = Path.join(rootDir, "package.json"); + const workspaceSettings = await readFile(workspaceSettingsPath, "utf-8"); + const workspaces: string[] = JSON.parse(workspaceSettings).workspaces || []; + + return ( + workspaces + .filter((glob): glob is string => typeof glob === "string") + // @todo support exclusions? + .filter((glob) => !glob.startsWith("!")) + .map((glob) => glob.replace(/\/\*{1,2}$/, "")) + .map((path) => Path.join(rootDir, path)) + ); +} + +async function findPackagesInDir(path: string) { + const directories = await readdir(path); + const packages = await Promise.all( + directories.map(async (dir) => { + const packagePath = Path.join(path, dir); + const packageJsonPath = Path.join(packagePath, "package.json"); + const exists = await fileExist(packageJsonPath); + return { packagePath, packageJsonPath, exists }; + }), + ); + + return await Promise.all( + packages + .filter(({ exists }) => exists) + .map(async ({ packagePath, packageJsonPath }) => { + const pkg = await readJson(packageJsonPath); + const dependsOn = getWorkspaceDeps(pkg); + return { dependsOn, name: pkg.name, packagePath }; + }), + ); +} + +function getWorkspaceDeps(pkg: PackageJson) { + const deps = [ + ...Object.entries(pkg.dependencies ?? {}), + ...Object.entries(pkg.devDependencies ?? {}), + ].map(([name]) => name); + return [...new Set(deps)]; +} + +function withoutNodeModules(settings: WorkspaceSettings): WorkspaceSettings { + const allowedDependencies = Object.keys(settings.workspaces); + + function withoutNodeModulesDeps(workspace: Workspace): Workspace { + return { + ...workspace, + dependsOn: workspace.dependsOn.filter((pkgName) => + allowedDependencies.includes(pkgName), + ), + }; + } + + const result: WorkspaceSettings = { + currentWorkspace: withoutNodeModulesDeps(settings.currentWorkspace), + workspaces: {}, + }; + for (const [name, workspace] of Object.entries(settings.workspaces)) { + result.workspaces[name] = withoutNodeModulesDeps(workspace); + } + return result; +} + +export function isRootDir(path: string) { + const packageJsonPath = Path.join(path, "package.json"); + if (!existsSync(packageJsonPath)) return false; + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")); + if (packageJson.workspaces) return true; + return false; +} From 366639be9222f4c2c883e6105d060c2f5c9cf10f Mon Sep 17 00:00:00 2001 From: "lz.piotr" Date: Tue, 25 Oct 2022 13:46:04 +0200 Subject: [PATCH 2/2] feat: detect package manager --- README.md | 3 --- src/detectPackageManager.ts | 16 ++++++++++++++++ src/index.ts | 16 +++++++++------- src/pnpmWorkspace.ts | 10 +++++----- src/types.ts | 2 ++ 5 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 src/detectPackageManager.ts diff --git a/README.md b/README.md index 67fc66a..ad1c126 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,3 @@ Moreover, deployment statuses are reported as successful even when builds are sk Then, it proceeds to check whether the given package or any of its dependencies were modified since the last commit with the use of `git diff "HEAD^" "HEAD" --quiet`. Currently, only `pnpm` and `yarn` workspaces are supported but more is on the roadmap. - -### Yarn workspaces -If you are using `yarn` workspaces pass `--yarn` flag \ No newline at end of file diff --git a/src/detectPackageManager.ts b/src/detectPackageManager.ts new file mode 100644 index 0000000..368f665 --- /dev/null +++ b/src/detectPackageManager.ts @@ -0,0 +1,16 @@ +import Path from "node:path"; +import { PackageManager } from "./types.js"; +import { isRootDir as isPnpmRootDir } from "./pnpmWorkspace.js"; +import { isRootDir as isYarnRootDir } from "./yarnWorkspace.js"; + +export function detectPackageManager(cwd: string): PackageManager { + const paths = cwd + .split(Path.sep) + .map((_, idx) => Path.join(cwd, "../".repeat(idx))); + + for (const path of paths) { + if (isPnpmRootDir(path)) return "pnpm"; + if (isYarnRootDir(path)) return "yarn"; + } + throw new Error("Package manager could not be detected"); +} diff --git a/src/index.ts b/src/index.ts index 9271659..a7fbbaa 100755 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,8 @@ import * as yarn from "./yarnWorkspace.js"; import { compare } from "./git.js"; import { debug } from "./debug.js"; import { parseArgs } from "./parseArgs.js"; +import { PackageManager } from "./types.js"; +import { detectPackageManager } from "./detectPackageManager.js"; const cwd = process.cwd(); @@ -21,21 +23,21 @@ const configuration = parseArgs({ type: "boolean", default: false, }, - yarn: { - type: "boolean", - default: false, - }, }, }); +const log = debug(configuration.values.verbose); + +const packageManager: PackageManager = detectPackageManager(cwd); + +log({ packageManager }); + const { readWorkspaceDirs, readWorkspaceSettings, resolveWorkspaceDeps, isRootDir, -} = configuration.values.yarn ? yarn : pnpm; - -const log = debug(configuration.values.verbose); +} = packageManager === "pnpm" ? pnpm : yarn; const [gitFromPointer = "HEAD^", gitToPointer = "HEAD"] = configuration.positionals; diff --git a/src/pnpmWorkspace.ts b/src/pnpmWorkspace.ts index 40a1a81..bf21ff5 100644 --- a/src/pnpmWorkspace.ts +++ b/src/pnpmWorkspace.ts @@ -2,12 +2,12 @@ import { readFile, readdir } from "node:fs/promises"; import Path from "node:path"; import { existsSync } from "node:fs"; import { fileExist, readJson } from "./utils.js"; -import { Ctx, Workspace, PackageJson } from "./types.js"; +import { Ctx, Workspace, PackageJson, WorkspaceSettings } from "./types.js"; -export async function readWorkspaceSettings({ rootDir, cwd }: Ctx): Promise<{ - workspaces: Record; - currentWorkspace: Workspace; -}> { +export async function readWorkspaceSettings({ + rootDir, + cwd, +}: Ctx): Promise { const workspaceDirs = await readWorkspaceDirs({ rootDir, cwd }); const workspaces = (await Promise.all(workspaceDirs.map(findPackagesInDir))) diff --git a/src/types.ts b/src/types.ts index a7b776f..278baf0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,3 +19,5 @@ export type WorkspaceSettings = { workspaces: Record; currentWorkspace: Workspace; }; + +export type PackageManager = "yarn" | "npm" | "pnpm";