UNPKG

workspaces-filter

Version:

A companion for filtering monorepo workspaces, by package name or package dir. Because all package manager's are weird. Useful for running scripts on a subset of workspaces. The primary reason is because Bun's "--filter" feature is buggy, but it's also us

150 lines (148 loc) 4.82 kB
import { shell } from "@tunnckocore/execa"; import fastGlob from "fast-glob"; import fs from "node:fs/promises"; import path from "node:path"; import proc from "node:process"; import picomatch from "picomatch"; //#region src/index.ts /** * Filters workspace packages based on provided glob patterns and search patterns. * * @example * import { filter } from 'workspaces-filter'; * * // Filter workspaces matching 'pkg-*' pattern * const graph = await filter(['packages/*'], 'pkg-*'); * * // Filter multiple patterns * const graph = await filter(['packages/*'], ['pkg-1', 'pkg-2']); * * // Filter with package dirs * const graph = await filter(['packages/*'], ['packages/foo']); * * // Filter with custom working directory * const graph = await filter(['packages/*'], '*', '/path/to/project'); * * * @param {string|string[]} wsGlobs - Array of workspace glob patterns to search for package.json files. * @param {string|string[]} pattern - String or array of strings to filter workspaces by name or directory. * @param {string} [cwd] - Optional current working directory (defaults to `process.cwd()`). * * @throws {Error} When no workspace globs are provided. * @throws {Error} When no pattern is provided. * * @returns {Promise<Graph>} Resolving to a Graph object containing filtered workspace metadata. * @public */ async function filter(wsGlobs, pattern, cwd) { if (!wsGlobs || wsGlobs && Array.isArray(wsGlobs) && wsGlobs.length === 0) throw new Error("No workspace globs provided."); pattern = Array.isArray(pattern) ? pattern : [pattern]; pattern = pattern.filter(Boolean); if (pattern.length === 0) throw new Error("No pattern provided."); cwd = cwd || proc.cwd(); const stream = fastGlob.stream(wsGlobs.map((workspaceGlob) => `${workspaceGlob}/package.json`), { absolute: true, cwd }); const workspaces = {}; for await (const pkgJsonPath of stream) { const pkgRoot = path.dirname(pkgJsonPath); const pkgJsonStr = await fs.readFile(pkgJsonPath, "utf8"); const { dependencies = {}, exports = {}, license = "", name, scripts = {}, version = "0.0.0" } = JSON.parse(pkgJsonStr); const pkg = { dependencies, exports, license, name, scripts, version }; workspaces[pkg.name] = { dir: path.relative(cwd, pkgRoot), ...pkg }; } if (pattern.find((p) => p.trim() === ".") || pattern.find((p) => p.trim() === "*")) return workspaces; const isMatch = picomatch(pattern.map((p) => p.replaceAll("/", "."))); const graph = Object.fromEntries(Object.entries(workspaces).filter(([name, meta]) => { const hasMatch = pattern.some((p) => name.includes(p)) || pattern.some((p) => meta.dir.includes(p)) || isMatch(name.replaceAll("/", ".")) || isMatch(meta.dir.replaceAll("/", ".")); return hasMatch; })); return graph; } /** * Executes a shell command or a package script in the context of each package in the graph. * * @example * import { filter, runCommandOn } from 'workspaces-filter'; * * const graph = await filter(['packages/*'], ['@scope/*']); * console.log(graph); * * type RunCommandOnOptions = { * cwd?: string; * isShell?: boolean; * packageManager?: string; * onTestCallback?: (_err: any, _ok: any) => void | Promise<void>; * }; * * // Run a shell command in each package * await runCommandOn(['echo', 'Hello, World!'], graph, { isShell: true } as RunCommandOnOptions); * * // Run a package script in each package * await runCommandOn(['build'], graph); * * @param {string[]} args - Arguments to pass to the command. * @param {Graph} graph - Graph object containing package metadata. * @param {RunCommandOnOptions} options - Optional configuration for running the command. * @returns {Promise<Graph>} Resolving to the input graph object. * @public */ async function runCommandOn(args, graph, options) { const opts = { cwd: proc.cwd(), isShell: false, onTestCallback: () => {}, onTestCallbackcwd: proc.cwd(), packageManager: "bun", ...options }; await Promise.all(Object.values(graph).map(async (x) => { const meta = x; const pkgDir = path.join(opts.cwd, meta.dir); if (opts.isShell) { try { await shell(args.join(" "), { cwd: pkgDir, stdout: "inherit" }); await opts.onTestCallback(null, true); } catch (err) { console.error(err.stack); await opts.onTestCallback(err, false); } return; } const [script, ...argz] = args; const isScript = Boolean(meta.scripts[script || "_____$$$__"]); const cmd = [ opts.packageManager, isScript ? "run" : "", script, ...argz ].filter(Boolean); try { await shell(cmd.join(" "), { cwd: pkgDir, stdout: "inherit" }); await opts.onTestCallback(null, true); } catch (err) { console.error(err.stack); await opts.onTestCallback(err, false); } })); return graph; } //#endregion export { filter, runCommandOn };