pacwich
v0.7.0
Published
Monorepo tooling that works with Bun, pnpm, and npm workspaces and orchestrates package.json scripts, with a CLI and TypeScript library API
Downloads
31,856
Maintainers
Readme
pacwich
Latest: 0.7.0
Monorepo tooling that works on top of Bun, npm, and pnpm workspaces. Zero config required. AI-friendly and human-friendly documentation. Has an affected graph and rules for workspace code sharing. Comes with a CLI and TypeScript API.
To get started, all you need is a repo using workspaces for nested JavaScript/TypeScript packages. This adds enhanced features on top of plain workspaces, like getting metadata about the monorepo and running scripts across workspaces.
Start running some CLI commands right away in your repo, or take full advantage of the TypeScript API and its features.
Full Documentation: https://pacwich.dev
Terminology
The definitions of workspace and project differ between package managers and monorepo tooling.
In pacwich, the word workspace is closer to how it's used in npm and Bun, a workspace being a nested package in a monorepo. Not necessarily all packages are workspaces, but all workspaces are packages.
The project is synonymous with your root where workspaces are defined, often the root of your git repository as well.
This means you have a root project that has some number of workspaces. The root package is not considered a workspace by default, but it optionally can be.
Note: bun-workspaces
This is the continuation of the
bun-workspacespackage that only worked with Bun. See the migration guide for more information. Read the blog post about motivations and development strategy.You can also instruct an agent to read
https://pacwich.dev/intro/bun-workspaces-migration/index.mdto assist with migration.Thanks to most core code and tests carrying over from
bun-workspaces,pacwichinherits its maturity to a large degree.
Quick Start
Installation
Global CLI
The global install gives you a convenient pacwich binary, which will still delegate to a local install's CLI, if available.
# Use the package manager of your choice:
bun add -g pacwich
# OR
pnpm add -g pacwich
# OR
npm install -g pacwichCompletions
When using the global install, you can set up shell completions for bash, zsh, or fish.
# Print help for setting up completions
pacwich completion
# Attempt to automatically install completion for your shell
pacwich completion installLocal install
Pin the CLI version for your project, and/or use the TypeScript library.
If you only use a local install, you can still invoke the pacwich command within package.json scripts, potentially using your root package.json scripts for common pacwich operations
you need in your project.
# Use the package manager of your choice:
bun add -d pacwich
# OR
pnpm add -D pacwich
# OR
npm install -D pacwichStale workspace data
Note that you need to run your package manager's install for pacwich to have current workspace data available, e.g. via bun install, pnpm install, or npm install. If you've added/removed/updated any workspace package.json, you'll likely need to run this again.
Calling the CLI
You might optionally alias your most used invocation to pw,
especially if you area bun-workspaces user that had a bw alias.
# Use the global command if installed
# This will use a local install's CLI, if available
pacwich --help
# Or use a one-off/local invocation
npx pacwich --help
bunx pacwich --help
pnpm exec pacwich --helpThe verify Feature
pacwich uses workspaces' package.json files to detect when a workspace depends on another workspace.
pacwich is relatively unopinionated, but it is highly encouraged for workspaces to always declare
other workspaces as dependencies explicitly in their own package.json. The verify feature exists to help catch instances where this can be dodged.
In Bun and pnpm, explicit dependencies are required to import/export from another workspace, by adding "my-workspace": "workspace:*" to package.json dependencies. npm workspaces do not require explicit package.json dependencies at all, so undeclared workspace imports simply work.
Nested workspaces also create a gap in explicit dependencies. A dependency declared by an ancestor workspace, most commonly the root package.json, resolves for every workspace beneath it through recursive parent-directory node_modules lookup, whether or not the importing workspace declares it.
The verify feature performs simple analysis of imports/exports in JS/TS files to catch both cases:
- Implicit workspace dependencies: a workspace imports another workspace that neither it nor any ancestor declares. Generally only possible with npm workspaces.
- Ancestor workspace dependencies: the import resolves only because an ancestor workspace declares it. Possible with any package manager.
Adding pacwich verify to a "prepare" script, pre-commit hook, CI pipeline, or similar can be especially helpful to catch issues.
pacwich verify --strict will fail on implicit workspace dependencies. Ancestor findings warn by default, and the strictDisallowAncestorWorkspaceDeps config option makes strict mode fail on those as well.
CLI Quickstart
##########
# Global #
##########
# Show usage (you can pass --help to any command)
pacwich --help
# Show version
pacwich --version
# Pass --cwd to any command
pacwich --cwd=/path/to/your/project ls
pacwich --cwd=/path/to/your/project run my-script
# Specify package manager, if you have multiple lockfiles
pacwich --pm=pnpm ls
# Pass --log-level to any command (debug, info, warn, error, or silent)
# A default can also be set with the PACWICH_LOG_LEVEL env var (the flag overrides it)
pacwich --log-level=debug ls
# Suppress specific warning messages (can also be set by project config)
# Warning IDs can be seen in warning log prefixes (full list: https://pacwich.dev/config/warnings)
pacwich --suppress-warnings=MultiplePackageManagerLockfiles ls
pacwich --suppress-warnings=MultiplePackageManagerLockfiles,ParallelExceedsAvailableCpus run lint
####################
# Getting metadata #
####################
# List all workspaces in your project
pacwich list-workspaces
# ls is an alias for list-workspaces
pacwich ls --json --pretty # Output as formatted JSON
# Get info about a workspace
pacwich workspace-info my-workspace
pacwich info my-workspace --json --pretty # info is alias for workspace-info
# Get info about a script, such as the workspaces that have it
pacwich script-info my-script
##########
# Verify #
##########
# Check for issues with your project
# Can be useful as your root package.json "prepare" script
# or as a pre-commit hook
pacwich verify
# Fails if workspaces detected that import/export from each other
# without explicit dependency declared in package.json
pacwich verify --strict
###################
# Running scripts #
###################
# Run the lint script for all workspaces in parallel
# that have it in their package.json "scripts" field
pacwich run lint
pacwich run lint my-workspace # Run for a single workspace
pacwich run lint my-workspace-a my-workspace-b # Run for multiple workspaces
pacwich run lint my-alias-a my-alias-b # Run by alias (set by optional config)
# A workspace's script will wait until
# any workspaces it depends on have completed
pacwich run lint --dep-order
pacwich run lint --dep-order --ignore-dep-failure
# Workspace patterns
pacwich run lint "my-workspace-*" # Run for matching workspace names
pacwich run lint "alias:my-alias-*" "path:my-glob/**/*" "tag:my-tag"
pacwich run lint "re:my-name-regex.*" "path:re:my-path-regex.*"
pacwich run lint "*" "not:path:my-path/*" # Run for all workspaces not in my-path/
pacwich run lint --args="--my-appended-args" # Add args to each script call
pacwich run lint --args="--my-arg=<workspaceName>" # Use the workspace name in args
pacwich run "cat package.json" --inline # Run an inline shell command
# Inline scripts can use the Bun shell if Bun is available,
# which is a cross-platform Bash-like shell
# This can be helpful for multi-OS support
pacwich run "cat package.json" --inline --shell=bun
# Scripts run in parallel by default
pacwich run lint --parallel=auto # Default, based on available logical CPUs
pacwich run lint --parallel=false # Run sequentially
pacwich run lint --parallel=2 # 2 max scripts run concurrently
pacwich run lint --parallel=50% # half of available logical CPUs
# Set the max preview lines for script output
# when "grouped" output style is used (the default on TTY)
pacwich run my-script --output-style=grouped --grouped-lines=10
# Use simple script output with workspace prefixes (default when not on a TTY)
pacwich run my-script --output-style=prefixed
# Use the plain output style (no workspace prefixes)
pacwich run my-script --output-style=plain
# Run an interactive script with full stdio, for user input etc.
# Requires a script and one workspace name or alias
# A script only gets a TTY if the caller is a TTY (i.e. not piped or redirected)
pacwich run-interactive my-interactive-script my-workspace-name-or-alias
# ri is an alias for run-interactive
pacwich ri my-interactive-script my-workspace-name-or-alias
# Silence all output
pacwich --log-level=silent run my-script --output-style=none
#####################
# Affected Features #
#####################
# List affected workspaces based on git diff (main vs. HEAD by default)
pacwich affected list
# Set the git base and head for comparison
pacwich affected list --base=my-branch-a --head=my-branch-b
# See detailed reasons for affected workspaces
pacwich affected list --explain --detailed
# Run a script across the workspaces affected by a change
pacwich affected run my-script
pacwich affected run my-script --base=my-branch-a --head=my-branch-b
# Aliases
pacwich affected ls
pacwich af ls
pacwich af run
##########
# Config #
##########
# Commands for pacwich's optional configuration
# Print project and all workspace configs as JSON
pacwich config debug
pacwich config debug --pretty # pretty print JSON
# Print just the project config as JSON
pacwich config debug --project
# Print a single workspace config as JSON
pacwich config debug --workspace=name-or-alias
# Print workspace configs matching a pattern as JSON
pacwich config debug --workspace-patterns="my-pattern-*"TS API Quickstart
import { createFileSystemProject } from "pacwich";
// A Project contains the core functionality of pacwich.
// Below defaults to process.cwd() for the project root directory
// Pass { rootDirectory: "path/to/your/project" } to use a different root directory
const project = createFileSystemProject();
// A Workspace that matches the name or alias "my-workspace"
const myWorkspace = project.findWorkspaceByNameOrAlias("my-workspace");
// Array of workspaces whose names match the wildcard pattern
const wildcardWorkspaces = project.findWorkspacesByPattern("my-workspace-*");
// Array of workspaces that have "my-script" in their package.json "scripts"
const workspacesWithScript = project.listWorkspacesWithScript("my-script");
// Run a script in a workspace
const runSingleScript = async () => {
const { output, exit } = project.runWorkspaceScript({
workspaceNameOrAlias: "my-workspace",
script: "my-script",
// Optional. Arguments to add to the command
// Can be a string or an array of strings
// If string, the argv will be parsed POSIX-style
args: ["--my", "--appended", "--args"],
// Optional (default false). Whether to ignore all output from the script.
// This saves memory when you don't need script output.
ignoreOutput: false,
// Optional (default false). Run the script interactively to accept user input.
// When true, the script will have access to stdin, stdout, and stderr,
// and script output cannot be captured. Note that a script only gets a TTY
// if the caller is a TTY (i.e. not piped or redirected)
interactive: false,
});
// Get a stream of the script subprocess's output
for await (const { chunk, metadata } of output.text()) {
// console.log(chunk); // The output chunk's content (string)
// console.log(metadata.streamName); // "stdout" or "stderr"
// console.log(metadata.workspace); // The target Workspace
}
// Get data about the script execution after it exits
const exitResult = await exit;
// exitResult.exitCode // The exit code (number)
// exitResult.signal // The exit signal (string), or null
// exitResult.success // true if exit code was 0
// exitResult.startTimeISO // Start time (string)
// exitResult.endTimeISO // End time (string)
// exitResult.durationMs // Duration in milliseconds (number)
// exitResult.metadata.workspace // The target workspace (Workspace)
};
// Run a script in all workspaces that have it in their package.json "scripts"
const runManyScripts = async () => {
const { output, summary } = project.runScriptAcrossWorkspaces({
// Optional. This will run in all matching workspaces that have my-script
// Accepts same values as the CLI run-script command's workspace patterns
// When not provided, all workspaces that have the script will be used.
workspacePatterns: ["my-workspace", "my-name-pattern-*"],
// Required. The package.json "scripts" field name to run
script: "my-script",
// Optional. Arguments to add to the command (same as for runWorkspaceScript)
args: ["--my", "--appended", "--args"],
// Optional (default true). Whether to run the scripts in parallel
parallel: true,
// Optional (default false). When true, a workspace's script will wait
// until any workspaces it depends on have completed
dependencyOrder: false,
// Optional. When true and dependencyOrder is true,
// continue running scripts even if a dependency fails
ignoreDependencyFailure: false,
// Optional (default false). Whether to ignore all output from the scripts.
// This saves memory when you don't need script output.
ignoreOutput: false,
// Optional, callback when script starts, skips, or exits
onScriptEvent: (event, { workspace, exitResult }) => {
// event: "start", "skip", "exit"
},
});
// Get a stream of script output
for await (const { chunk, metadata } of output.text()) {
// console.log(chunk); // the output chunk's content (string)
// console.log(metadata.streamName); // "stdout" or "stderr"
// console.log(metadata.workspace); // the Workspace of the output
}
// Get final summary data and script exit details
const summaryResult = await summary;
// summaryResult.totalCount // Total number of scripts
// summaryResult.allSuccess // true if all scripts succeeded
// summaryResult.successCount // Number of scripts that succeeded
// summaryResult.failureCount // Number of scripts that failed
// summaryResult.startTimeISO // Start time (string)
// summaryResult.endTimeISO // End time (string)
// summaryResult.durationMs // Total duration in milliseconds (number)
// The exit details of each workspace script
for (const exitResult of summaryResult.scriptResults) {
// exitResult.exitCode // The exit code (number)
// exitResult.signal // The exit signal (string), or null
// exitResult.success // true if exit code was 0
// exitResult.startTimeISO // Start time (ISO string)
// exitResult.endTimeISO // End time (ISO string)
// exitResult.durationMs // Duration in milliseconds (number)
// exitResult.metadata.workspace // The target workspace (Workspace)
}
};Configuration Overview
pacwich has no required configuration, but there are optional config files.
Workspace Config
Workspace configs can be placed in a workspace's directory at pacwich.workspace.ts.
Workspace configuration documentation here
// pacwich.workspace.ts - placed in a workspace directory
// Also supported: pacwich.workspace.js, pacwich.workspace.json, pacwich.workspace.jsonc,
// or a "pacwich" key in package.json
import { defineWorkspaceConfig } from "pacwich/config";
export default defineWorkspaceConfig({
// Shorthand name. use array for multiple
// Must be unique across workspace names and other aliases
alias: "my-web-app",
tags: ["app", "frontend"],
// Optional, for configuring affected workspace resolution inputs
// Applies to all scripts that don't configure their own inputs
defaultInputs: {
// File paths, directory paths, or globs relative to the workspace's path.
// Default is all git-trackable files in the workspace directory.
files: ["src/**/*.ts", "!src/**/*.test.ts"],
// Workspaces to treat like dependencies that aren't package.json dependencies
workspacePatterns: ["tag:lib"],
// Dependency names (e.g. "react") to treat as dependencies (default: all)
externalDependencies: ["react"],
},
// Configure the verify feature for this workspace
// This is additive with any project-level verify configuration
verify: {
workspaceDependencies: {
// Exclude paths from import/export scanning, relative to the workspace path
// Use a leading / for project-relative paths
ignoreInputFiles: ["**/*.d.ts"],
// Ignore imports from specific workspaces
ignoreImportsFromWorkspacePatterns: ["tag:internal"],
},
},
scripts: {
// lower order runs first in sequenced script execution
build: {
// Optional, for setting the default script execution order
order: 1,
// Optional, for configuring affected workspace resolution inputs
// Applies to the build script only
inputs: { files: ["src/**/*.ts"] },
},
test: { order: 2 },
},
rules: {
workspaceDependencies: {
// Only "my-workspace" or workspaces tagged "lib" are allowed as dependencies
allowPatterns: ["tag:lib", "my-workspace"],
// Workspaces tagged "backend" are forbidden as dependencies
denyPatterns: ["tag:backend"],
},
},
});Project Config
A project-level config can be placed in the project root directory at pacwich.project.ts,
which can also apply workspace configs in bulk by using workspace patterns.
Project configuration documentation here
More on workspace pattern configs here
// pacwich.project.ts - placed in your project root directory
// Also supported: pacwich.project.js, pacwich.project.json, pacwich.project.jsonc,
// or a "pacwich-root" key in package.json
import { defineRootConfig } from "pacwich/config";
export default defineRootConfig({
defaults: {
// default value for --parallel option
parallelMax: 4,
// default value for --shell option
shell: "system",
// default value for global --include-root-workspace option
includeRootWorkspace: false,
},
// Configure the verify feature
verify: {
workspaceDependencies: {
// Exclude paths from import/export scanning across the project
ignoreInputFiles: ["**/*.d.ts"],
// Ignore imports from specific workspaces
ignoreImportsFromWorkspacePatterns: ["tag:internal"],
},
},
// Apply workspace configs in bulk by workspace pattern, in order.
// Each entry merges into matching workspaces' accumulated config.
// Pattern matching reflects aliases and tags added by earlier entries.
workspacePatternConfigs: [
{
patterns: ["path:packages/apps/**/*"],
config: { tags: ["app"] },
},
{
patterns: ["path:packages/libs/**/*"],
config: { tags: ["lib"] },
},
{
// "tag:app" matches because the first entry added it
patterns: ["tag:app"],
config: {
// Inputs always override previous entries instead of deep merging
defaultInputs: { files: ["src/**/*.ts"] },
scripts: {
build: { order: 1, inputs: { files: ["src/**/*.ts"] } },
},
rules: {
workspaceDependencies: {
allowPatterns: ["tag:lib"], // apps may only depend on libs
},
},
},
},
{
patterns: ["tag:app"],
// Factory form: receives static workspace data and accumulated config
config: (workspace, prevConfig) => ({
alias: workspace.name.replace(/^@my-scope\//, ""),
}),
},
],
});