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
JavaScript
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 };