dep-fence
v0.6.1
Published
A tool to safeguard package dependencies and TypeScript configuration hygiene in monorepos, based on rich policy examples and explicit reasons
Downloads
32
Maintainers
Readme
dep-fence 🧱✨
A tool to safeguard package dependencies and TypeScript configuration hygiene in monorepos, based on rich policy examples and explicit reasons. Every finding explains why a rule must or should apply, keeping reviews focused and predictable. This helps automate reviews, clarify next steps, and connect findings directly to coding-assistant AI instructions. 🚥
Table of Contents 🧭
- What, Why & How 💡
- Install 📦
- Getting Started 🛣️a
- Basic Usage 🖥️
- Advanced Usage 💪
- Examples 📁
- Programmatic API 🧩
- Troubleshooting 🆘
- FAQ ❓
- Author ✍️
- License 📄
What, Why & How 💡
- What it is: A lightweight, policy‑driven guardrail for repository‑wide dependency boundaries. It detects boundary crossings and can fail CI. Typical use: enforce public‑API‑only imports, keep UI/domain layers separate, align peerDependencies with bundler externals, keep tsconfig sane, and govern skipLibCheck — always with an explicit “Because: …”.
- Problems it solves: deep import leaks, accidental cross‑package coupling in monorepos, type/exports drift that breaks publishing, peer vs bundler external mismatches, JSX option inconsistencies, and more.
- How it compares and when to use which:
- ESLint: great for per-file static analysis and style/bug-catching. Use ESLint for in-file concerns (optionally with rules like
import/no-internal-modulesorno-restricted-imports); use dep-fence for cross-package boundaries and repo-level policies. - Knip: finds unused/missing dependencies and unused files/exports, with first-class monorepo support. Use Knip for dependency inventory & dead-code surfacing; use dep-fence for boundary policies and CI gating — they complement each other.
- dependency-cruiser / madge: visualize and validate dependency graphs. Use them for exploration and complex graph rules; use dep-fence for an opinionated, CI-first policy engine with simple allow/forbid semantics.
- syncpack: keeps versions and workspace ranges consistent across a monorepo. Use syncpack for manifest hygiene; use dep-fence for runtime/build-time import and peer/bundler alignment.
- publint: lints the published package surface for environment compatibility and common mistakes. publint protects consumers; dep-fence keeps sources respecting boundaries before you publish.
- (optional) Are the Types Wrong? (attw): validates TypeScript type resolution/exports of published output; pairs well with publint in release pipelines.
- ESLint: great for per-file static analysis and style/bug-catching. Use ESLint for in-file concerns (optionally with rules like
Why dep‑fence? ✨
- Condition‑driven rules (e.g., apply only to packages that are UI + publishable).
- Prevent double‑bundling by aligning tsup
externalwithpeerDependencies. 📦➕📦❌ - Govern
skipLibCheckwith explicit reasons or an allow‑list. ⚖️ - Keep
tsconfighealthy (baseline inheritance, forbid../srcdirect references, JSX option sanity). - Every message includes “Because: …”, making policy intent visible. 🗣️
It works alongside your existing linters, graph analyzers, dependency inventory tools, and package-publish validators. Together they cover in-file quality, dependency hygiene, and release readiness — while dep-fence enforces cross-package boundaries and provides CI-first policy guardrails.
How it works? 🧭
- Detects repo root via
pnpm-workspace.yamlor nearestpackage.json. - Scans package directories:
packages/*/**/package.jsonapp/package.json(if present)
- Infers attributes:
ui,publishable/private,usesTsup,hasTsx,browser/node,worker,next,storybook,app. - Derives
tsup.externalfrom repotsup.base.config.*and per‑packagetsup.config.*when available.
Install 📦
Local (recommended):
pnpm add -D dep-fence
# or
npm i -D dep-fenceGlobal:
npm i -g dep-fenceRequirement: Node.js >= 18
Getting Started 🛣️
CLI Usage
dep-fence # YAML output (default)
dep-fence --format pretty # human‑readable (pretty)
dep-fence --format yaml # YAML explicitly
dep-fence -f yaml -g severity # group by ERROR/WARN/INFO
dep-fence -f json # JSON output
dep-fence --strict # exit 1 if any ERROR
dep-fence -c path/to/dep-fence.config.ts # explicit policy file (TS/JS supported)
dep-fence -h # help
dep-fence -v # versionOutput Formats
- Default: YAML grouped by package (equivalent to
--format yaml --group-by package). - Pretty:
--format prettyfor human‑readable, package‑grouped output. - JSON:
--format jsonfor machine‑readable output. - YAML grouping:
--group-by <package|rule|severity>only affects YAML.- Examples:
dep-fence --format yaml --group-by ruledep-fence --format yaml --group-by severity
- Examples:
Short flags: -f = --format, -g = --group-by, -c = --config.
Note: the legacy --json flag was removed; use --format json.
Commands and expected output:
pnpm dep-fence
pnpm dep-fence --strict # CI gate (exit 1 on ERROR)Success Output
✔ No violations (0)Violation Output in YAML format (default):
"@your/ui-button":
- type: ui-in-deps
severity: ERROR
message: |
UI libs should be peerDependencies (not dependencies):
- react
reason: UI packages should not bundle React/MUI; rely on host peers.Violation Output in pretty format (with --format pretty option):
=== @your/ui-button ===
ERROR ui-in-deps: UI libs should be peerDependencies (not dependencies):
- react
Because: UI packages should not bundle React/MUI; rely on host peers.Zero‑Config Mode 🚀
Zero‑Config Mode runs the default package policies (package‑level checks) in a typical monorepo:
- Auto‑detects repo root via nearest
pnpm-workspace.yamlorpackage.json. - Scans packages under
packages/*/**/package.jsonandapp/package.json(if present). - Applies sensible default policies that cover UI/peer/tsup alignment,
tsconfighygiene, andskipLibCheckgovernance. - No
dep-fence.config.*required to get value on day one.
Why this matters (concrete benefits):
- Instant adoption: drop into CI and get actionable findings without setup.
- Predictable baselines: the same defaults across repos reduce bikeshedding.
- Safe by default: read‑only checks; use
--strictto fail on ERRORs when ready. - Incremental rollout: start as a linter, then codify exceptions as you learn.
- Reviewer‑friendly: every finding says “Because: …” to explain the policy.
Basic Usage 🖥️
Save a report for review (grouped by severity)
dep-fence -f yaml -g severity > dep-fence.report.yamlThis helps leads/owners scan ERRORs vs WARNs separately and share the file in reviews.
Extract error summaries in CI logs (JSON → jq)
dep-fence -f json | jq -r '.findings[] | select(.severity=="ERROR") | "[\(.severity)] \(.packageName) :: \(.rule)"'This prints concise error lines like: [ERROR] @your/ui-button :: ui-in-deps.
Advanced Usage 💪
Policy Configuration 🛠️
Zero‑config works out of the box. When you need more control, provide an explicit policy file (dep-fence.config.ts or dep-fence.config.mjs) at the repo root to replace the defaults.
Policy file (TypeScript example):
// dep-fence.config.ts
import { all, isUI, isPublishable } from 'dep-fence/conditions';
import type { Policy } from 'dep-fence/types';
export const policies: Policy[] = [
{
id: 'ui-externals-and-peers',
when: all(isUI(), isPublishable()),
because: 'UI packages should not bundle React/MUI; rely on peers.',
rules: ['ui-in-deps', 'ui-missing-peer', 'peer-in-external']
}
];You can also tweak severity per rule via severityOverride and compose complex conditions with all(...), any(...), and not(...). Helpers like isUI(), isPublishable(), and usesTsup() are available from dep-fence/conditions.
Repo‑wide operational settings (JSON) can be declared in dep-fence.config.json:
{
"allowSkipLibCheck": [
"@your/legacy-chart" // temporary exception
]
}Per‑package justification for skipLibCheck can live in each tsconfig.json:
{
"compilerOptions": { "skipLibCheck": true },
"checkDeps": {
"allowSkipLibCheck": true,
"reason": "3rd‑party types temporary mismatch; scheduled fix"
}
}Policies by Example
Representative Policy Examples (Purpose / Snippet / Outcome)
Public API only (ban deep imports across packages)
- Purpose:
@org/foois OK;@org/foo/src/xis not. - Snippet:
{ id: 'public-api-only', when: () => true, because: 'Only use package public entrypoints.', rules: [ { rule: 'import-path-ban', options: { forbid: ['^@[^/]+/[^/]+/src/'] }, severity: 'ERROR' }, ]} - Outcome: Detects cross‑package references into
src/.
- Purpose:
Peers × tsup externals alignment
- Purpose: ensure peers are treated as externals by the bundler.
- Snippet:
{ id: 'tsup-peer-hygiene', when: isPublishable(), because: "Don't bundle peers.", rules: ['peer-in-external','external-in-deps'] } - Outcome: Flags peers missing in
tsup.externalor duplicated independencies.
Enforce types from dist
- Purpose: published types should come from
dist/*.d.ts. - Snippet:
{ id: 'package-types-dist', when: isPublishable(), because: 'Expose types from dist/*.d.ts.', rules: ['package-types-dist'] } - Outcome: Violates when
typesorexports[entry].typesdo not point todist/*.d.ts.
- Purpose: published types should come from
Select Pre-Defined Config Files
You can import pre-defined config files in the examples directory with --config option as needed.
Please consult examples/README.md for stacks/recipes oriented examples (React, Router v7, TypeScript v5, Bundlers). Legacy examples/policies/ paths have been retired; use the stacks/recipes paths.
Create Your Original Config Files
You can create your own config files to customize the rules and settings.
Simple example 1:
import type { Policy } from 'dep-fence/types';
import { defaultPolicies } from 'dep-fence';
import { pkgUiPeersRule, pkgExportsExistRule, tsconfigHygieneRule } from 'dep-fence/guards';
const custon: Policy[] = [
pkgUiPeersRule({ exclude: ['@your/app'] }),
pkgExportsExistRule({ roots: ['packages', 'app'] }),
tsconfigHygieneRule({
skipLibCheck: { allowedPackages: ['@your/temp-exception'], requireReasonField: true, action: 'warn' },
}),
];
const policies: Policy[] = [...defaultPolicies, ...custom];
export default policies;Simple example 2:
import type { Policy } from 'dep-fence/types';
export const policies: Policy[] = [
{
id: 'my-custom-checks',
when: isPublishable(),
because: 'repo‑specific validation',
rules: [{
rule: 'custom',
id: 'check-pkg-field',
run: (ctx) => {
const f = [] as any[];
if (!ctx.pkgJson.customField) {
f.push({ packageName: ctx.pkgName, packageDir: ctx.pkgDir, rule: 'check-pkg-field', severity: 'WARN', message: 'missing customField', because: ctx.because });
}
return f;
}
}]
}
];Create Your Own Policies from Scratch with TS (typed, recommended) or MJS (zero‑setup)
Config format choice (TS or MJS)
- Supported:
dep-fence.config.ts,dep-fence.config.mjs(ESM.mjspreferred over.js). - Recommendation: prefer
.mjsfor zero‑setup CI/offline; prefer.tsfor editor type‑safety. - Running
.tsconfigs:- Built‑in fallback (no extra deps): strips type‑only syntax and evaluates at runtime.
- Loader (e.g.,
tsx):NODE_OPTIONS="--loader tsx" pnpm dep-fence. - Prebuild (e.g.,
tsup):tsup dep-fence.config.ts --format esm --dts false --out-dir ..
- Pitfalls: require Node ≥ 18 with ESM, avoid mixing CJS/ESM, minimize runtime deps in air‑gapped CI.
TS: dep-fence.config.ts
import { defaultPolicies } from 'dep-fence';
import type { Policy } from 'dep-fence/types';
const custom: Policy[] = [
{ id: 'ban-deep-imports', when: () => true, because: 'Use public API only; avoid cross‑package internals.', rules: [
{ rule: 'import-path-ban', options: { forbid: ['^@[^/]+/[^/]+/src/'] }, severity: 'ERROR' },
]},
];
export const policies: Policy[] = [...defaultPolicies, ...custom];MJS: dep-fence.config.mjs
import { defaultPolicies } from 'dep-fence';
/** @type {import('dep-fence/types').Policy[]} */
export const policies = [
...defaultPolicies,
{ id: 'ban-deep-imports', when: () => true, because: 'Use public API only; avoid cross‑package internals.', rules: [
{ rule: 'import-path-ban', options: { forbid: ['^@[^/]+/[^/]+/src/'] }, severity: 'ERROR' },
]},
];Environment variables:
DEP_FENCE_CONFIG— absolute/relative path to a policies module (overrides file discovery).DEP_FENCE_REPO_CONFIG— path to a JSON file with repo‑wide settings (overridesdep-fence.config.json).
Workspace/subtree overrides
- Typical setup is a single root config; split into modules or swap via
DEP_FENCE_CONFIGfor subtrees/teams if needed.
Performance and caching
- Prefer fewer, broader rules to many tiny ones.
- In CI, scope checks to changed packages where possible.
Git Integration: pre‑commit / pre‑push 🐙
Alongside package policies (see Zero‑Config Mode), dep‑fence ships lightweight repository‑level guards under the dep-fence/guards entry. They are designed for Git hooks (predictable, no hidden state):
allowed-dirs— Commit scope guard: staged files must be under allowed globs.mtime-compare— Advisory: detect files newer than your rules/SSOT baseline.upstream-conflict— Optimistic conflict detection: fail if upstream has other‑author changes touching protected paths since your base.
New guards for monorepo publishing/build hygiene:
pkg-exports-exist— Verify package.json main/module/exports paths point to existing files (prevents publish/bundler breakage).pkg-ui-peers— Enforce UI singletons as peers and align bundler externals (flags: ui-in-deps, ui-missing-peer, peer-in-external, external-in-deps).tsconfig-hygiene— Keep tsconfig healthy (extends repo base, jsx option sanity, skipLibCheck governance with allowlist/justification).
Try the examples:
pnpm dlx tsx examples/guards/run.ts --mode pre-commit
pnpm dlx tsx examples/guards/run.ts --mode pre-push
pnpm dlx tsx examples/guards/run.ts --mode pre-commit \
--config examples/guards/guards.ui-peers.config.ts
pnpm dlx tsx examples/guards/run.ts --mode pre-commit \
--config examples/guards/guards.pkg-exports.config.ts
pnpm dlx tsx examples/guards/run.ts --mode pre-commit \
--config examples/guards/guards.tsconfig-hygiene.config.ts
# Additional guard presets (pair with recipes)
pnpm dlx tsx examples/guards/run.ts --mode pre-commit \
--config examples/guards/guards.ui-peer-policy.config.ts
pnpm dlx tsx examples/guards/run.ts --mode pre-commit \
--config examples/guards/guards.publishable-tsconfig-hygiene.config.ts
pnpm dlx tsx examples/guards/run.ts --mode pre-commit \
--config examples/guards/guards.jsx-option-for-tsx.config.ts
pnpm dlx tsx examples/guards/run.ts --mode pre-commit \
--config examples/guards/guards.tsconfig-paths.config.ts
pnpm dlx tsx examples/guards/run.ts --mode pre-commit \
--config examples/guards/guards.maplibre-allowlist.config.ts
pnpm dlx tsx examples/guards/run.ts --mode pre-commit \
--config examples/guards/guards.package-types-dist.config.ts
pnpm dlx tsx examples/guards/run.ts --mode pre-commit \
--config examples/guards/guards.strict-ui.config.ts
pnpm dlx tsx examples/guards/run.ts --mode pre-commit \
--config examples/guards/guards.source-import-ban.config.tsCI Integration 🛡️
# GitHub Actions example
jobs:
dep-fence:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: pnpm i --frozen-lockfile
- run: pnpm dep-fence # refer to your package.json scriptConcepts & Terminology 📚
- Rule
- Smallest, atomic check that produces findings (e.g.,
peer-in-external,source-import-ban). - Takes per‑rule
optionsand a computed severity; may be built‑in or a plugin (viacreate(options)=>check(ctx)).
- Smallest, atomic check that produces findings (e.g.,
- Policy
- A composable unit that targets a package set and explains intent.
- Shape:
{ id, when, because, rules, options?, severityOverride? }— evaluated per package. - Example: “UI externals and peers” bundles
ui-peer-policyrule +peer-in-externalrule for publishable UI packages.
- Recipe
- A ready‑to‑run policy module focused on one goal; copy‑pasteable.
- Lives under
examples/recipes/…/dep-fence.config.tsand is referenced viaDEP_FENCE_CONFIG=….
- Guard
- A repository‑level check for Git hooks/CI (pre‑commit/pre‑push). Runs via
examples/guards/run.ts. - Guards read workspace state (staged files, tsconfig, package.json) and fail fast outside policy evaluation.
- A repository‑level check for Git hooks/CI (pre‑commit/pre‑push). Runs via
Examples 📁
See examples/README.md for a concise, up‑to‑date index of stacks and recipes. Typical invocations:
# Minimal policy set
DEP_FENCE_CONFIG=examples/recipes/minimal/dep-fence.config.ts pnpm dep-fence
# Focused examples (stacks/recipes paths)
DEP_FENCE_CONFIG=examples/recipes/tsconfig-paths/dep-fence.config.ts pnpm dep-fence
DEP_FENCE_CONFIG=examples/recipes/package-exports-guard/dep-fence.config.ts pnpm dep-fence
DEP_FENCE_CONFIG=examples/stacks/bundlers/vite/recipes/multi-entry-workers/dep-fence.config.ts pnpm dep-fence
# With repo‑wide JSON settings
DEP_FENCE_REPO_CONFIG=examples/repo-config/dep-fence.config.json pnpm dep-fenceUI policies — quick guide:
ui-peer-policy(recipe): package.json‑only check; fast to adopt; good first step.ui-peers-light(recipe): gentle variant; WARN by default; no bundler checks.minimal(stack): adds bundler externals alignment (tsup) on top of peers.strict-ui(recipe): strict peers + bundler alignment; suitable for CI gating in libraries.
What It Checks 🔍
- Peers × tsup externals
peer-in-external(peer missing fromtsup.external)external-in-deps(external also listed independencies)
- UI package hygiene
ui-in-deps(React/MUI/Emotion independencies)ui-missing-peer(UI libs used but missing frompeerDependencies)
- TypeScript hygiene
tsconfig-no-base(not extending repo base; hint)paths-direct-src(direct../srcreferences)jsx-mismatch(.tsx present butjsxnotreact-jsx)
skipLibCheckgovernanceskipLibCheck-not-allowed(enabled without permission)skipLibCheck-no-reason(permitted but missing rationale)
- Encapsulation rules
maplibre-direct-dep(direct MapLibre deps outside the wrapper package)
All findings include “Because: …” to surface rationale.
Default Policies (catalog) 📚
Each default policy explains why it applies (Because) and targets specific attributes:
ui-externals-and-peers— UI packages must not bundle React/MUI; rely on peers. Rules:ui-in-deps,ui-missing-peer,peer-in-external.tsup-peer-hygiene— bundlers must externalize peers. Rules:peer-in-external,external-in-deps.publishable-tsconfig-hygiene— keep publishedtsconfigclean. Rules:tsconfig-no-base,paths-direct-src.publishable-local-shims— avoid long‑lived local*.d.ts. Rule:local-shims(default WARN; can be raised).jsx-option-for-tsx— TSX requiresjsx: react-jsx. Rule:jsx-mismatch.skipLibCheck-governance— permissioned toggle with reasons. Rules:skipLibCheck-*.non-ui-paths-hygiene— discourage cross‑src refs broadly. Rule:paths-direct-src.maplibre-encapsulation— only wrapper package may depend on MapLibre. Rule:maplibre-direct-dep.
Severity override example:
export const policies = [
{
id: 'ui-externals-and-peers',
when: all(isUI(), isPublishable()),
because: '…',
rules: ['ui-in-deps', 'ui-missing-peer', 'peer-in-external'],
severityOverride: { 'ui-missing-peer': 'ERROR' }
}
];Opt‑in Extensions (examples) 🧪
Additional rules and helpers can be enabled via a policy file:
source-import-ban— ban specific named imports from a module. Seeexamples/recipes/source-import-ban/dep-fence.config.ts.tsconfig-paths— enforcepathsto point todist/*.d.tsand/or forbid patterns.package-exports-guard— guard subpaths (e.g., forbidtypesfor./workers/*).package-types-dist— ensure packagetypesandexports[entry].typespoint todist/*.d.ts.
Programmatic API 🧩
import { runWithPolicies, defaultPolicies } from 'dep-fence';
import { any, isPublishable } from 'dep-fence/conditions';
const findings = runWithPolicies(defaultPolicies);
const hasError = findings.some((f) => f.severity === 'ERROR');Types are available from dep-fence/types (Finding, Policy, Condition, Severity, ...).
Troubleshooting 🆘
- False positives with path aliases: align dep‑fence’s resolver with your tsconfig
paths/bundler aliases. - Type‑only imports flagged: adjust rule targets/conditions or use rules that account for types‑only edges.
- Dynamic imports: dynamic/computed paths are treated conservatively; model critical boundaries with static paths.
FAQ ❓
ESLint or dep‑fence?
- Both. ESLint covers in‑file quality; dep‑fence enforces cross‑file/package boundaries.
Why not just dependency‑cruiser?
- It’s great for exploration/visualization. dep‑fence focuses on CI‑first, opinionated defaults for monorepos with a small set of high‑signal rules.
What are the best practices for using dep-fence?
- Start with simple, high-impact rules such as banning deep imports and aligning peers with bundler externals.
- Keep all exceptions justified with clear “Because:” statements. Use
severityOverride(e.g., WARN → ERROR) to roll out gradually without blocking early adoption. - In CI, run dep-fence with
--strictto fail on ERRORs; locally, run without--strictto discover and review issues incrementally. - Document exceptions and policies transparently so reviews stay predictable and team members understand why rules apply.
How to allow a temporary exception?
- Use a narrowly scoped policy/condition or
severityOverride, and record a Because statement.
- Use a narrowly scoped policy/condition or
How to protect publish quality?
- Pair dep‑fence (boundaries/types path/peer×bundler) with publint (package export surface) in CI.
Where does “unused dependency warning” fit?
- Not shipped by default today. It’s best implemented as a Guard preset (repo‑level, may be slower) or as a plugin Rule if you prefer policy evaluation.
- Guard approach (recommended): scan import graph vs package.json to flag unused deps (pair it with pre‑commit). See
depcheck/build graph tools, or author a custom guard similar toguards.package-types-dist. - Policy approach: add a plugin rule
unused-depsand include it in a policy for publishable packages. This repo already documents plugin rules indocs/dep-fence-upstream-guide.md.
Author ✍️
Hiroya Kubo [email protected]
License 📄
MIT
Happy fencing! 🧱✨ Add reasons to rules and keep dependencies sane.
