tools-monorepo CLAUDE.md
Package-specific guidance for the repository policy enforcement tool.
CLAUDE.md - repopo
Package-specific guidance for the repository policy enforcement tool.
Package Overview
Extensible policy enforcement tool that validates and auto-fixes files in git repositories. Think of it as a lint tool for any file type, with straightforward custom policy creation.
Binary: repopo
Dev Mode: ./bin/dev.js
Config: repopo.config.ts (or .cjs, .mjs) in repo root
Core Architecture
Policy System
Policies are objects that implement the PolicyDefinition<C> interface:
interface PolicyDefinition<C = undefined> {
name: PolicyName; // Display name
description: string; // Detailed description (required)
match: RegExp; // File path regex
handler: PolicyHandler<C>; // Check function
resolver?: PolicyStandaloneResolver<C>; // Optional auto-fix
defaultConfig?: C; // Default configuration
}
Policy Execution Flow:
- Enumerate all files in git repo
- For each file, test against all policy
matchregexes - If match, call
handler(file, root, resolve, config) - Handler returns
trueorPolicyFailure - If
resolve=trueand policy hasresolver, auto-fix
Handler Function
type PolicyHandler<C> = (args: {
file: string; // Repo-relative path
root: string; // Absolute repo root
resolve: boolean; // If true, apply auto-fixes
config?: C; // Policy configuration
}) => Promise<PolicyHandlerResult>;
type PolicyHandlerResult = true | PolicyFailure | PolicyFixResult;
Return values:
true- File passes policyPolicyFailure- File fails (includesautoFixableflag)PolicyFixResult- Failure withresolved: booleanfield
Configuration System
Policies are configured in repopo.config.ts:
import { makePolicy, type RepopoConfig } from "repopo";
import { PackageJsonProperties } from "repopo/policies";
const config: RepopoConfig = {
policies: [
makePolicy(PackageJsonProperties, {
verbatim: {
license: "MIT",
author: "Tyler Butler <tyler@tylerbutler.com>",
},
}),
],
};
export default config;
File Exclusion:
- Global:
RepopoConfig.excludeFiles(excludes from all policies) - Per-policy:
makePolicy(Policy, config, { excludeFiles: [...] })
Built-in Policies
File Headers:
HtmlFileHeaders- Enforce headers in HTML filesJsTsFileHeaders- Enforce headers in JS/TS files
Package.json:
PackageJsonProperties- Enforce specific fields/valuesPackageJsonRepoDirectoryProperty- Validaterepository.directoryPackageJsonSorted- Enforce sorted keys (usessort-package-json)PackageScripts- Validate npm scripts
Code Standards:
NoJsFileExtensions- Prevent ambiguous.jsfiles (require.mjs/.cjs)
All built-in policies are enabled by default via DefaultPolicies array.
Policy Generators
Helper functions to reduce boilerplate for common file types:
generatePackagePolicy
Creates policies for package.json files:
import { generatePackagePolicy, makePolicy } from "repopo";
const MyPackagePolicy = generatePackagePolicy(
"MyPackagePolicy",
async ({ content, resolve, config }) => {
// content: parsed package.json
// Return true or PolicyFailure
if (content.version === "0.0.0") {
return {
name: "MyPackagePolicy",
file: "package.json",
errorMessage: "Version must not be 0.0.0",
autoFixable: false,
};
}
return true;
}
);
// Use in config
const config: RepopoConfig = {
policies: [makePolicy(MyPackagePolicy)],
};
Creating Custom Policies
Simple Policy
import { Policy, type PolicyHandler } from "repopo";
const handler: PolicyHandler = async ({ file, root, resolve, config }) => {
const absolutePath = path.join(root, file);
const content = await fs.readFile(absolutePath, "utf-8");
if (content.includes("TODO")) {
return {
name: "NoTodoComments",
file,
errorMessage: "File contains TODO comments",
autoFixable: true,
};
}
return true;
};
export const NoTodoComments = new Policy({
name: "NoTodoComments",
description: "Prevents TODO comments in code",
match: /\.(ts|js)$/, // Match TypeScript/JavaScript files
handler,
});
Policy with Auto-Fix
const handlerWithFix: PolicyHandler = async ({ file, root, resolve }) => {
const absolutePath = path.join(root, file);
let content = await fs.readFile(absolutePath, "utf-8");
if (content.includes("TODO")) {
if (resolve) {
// Auto-fix: remove TODO lines
content = content.split("\n")
.filter(line => !line.includes("TODO"))
.join("\n");
await fs.writeFile(absolutePath, content);
return {
name: "NoTodoComments",
file,
resolved: true,
errorMessage: "Removed TODO comments",
};
}
return {
name: "NoTodoComments",
file,
autoFixable: true,
errorMessage: "File contains TODO comments",
};
}
return true;
};
Policy with Configuration
interface TodoPolicyConfig {
allowedKeywords: string[];
}
const handler: PolicyHandler<TodoPolicyConfig> = async ({
file, root, resolve, config
}) => {
const allowed = config?.allowedKeywords ?? ["TODO"];
// Use config.allowedKeywords in validation logic
};
export const ConfigurableTodoPolicy = new Policy<TodoPolicyConfig>({
name: "ConfigurableTodoPolicy",
description: "Configurable TODO validation",
match: /\.(ts|js)$/,
handler,
defaultConfig: { allowedKeywords: ["TODO", "FIXME"] },
});
CLI Commands
# Check all files (read-only)
repopo check
# Check and auto-fix
repopo check --fix
# Check specific files via stdin
git diff --name-only | repopo check --stdin
# List configured policies
repopo list
# Dev mode
./bin/dev.js check --fix
Typical CI Usage:
# Fail CI if policies don't pass
repopo check
# Pre-commit hook (auto-fix)
repopo check --fix --stdin
Development Commands
# Run checks on this repo
./bin/dev.js check
# Fix policy violations
./bin/dev.js check --fix
# Test policy on specific files
echo "packages/cli/package.json" | ./bin/dev.js check --stdin
# Build and test
pnpm build
pnpm test
Integration Patterns
Monorepo Usage
Place repopo.config.ts at monorepo root:
import { makePolicy, type RepopoConfig } from "repopo";
import {
NoJsFileExtensions,
PackageJsonProperties,
PackageJsonSorted,
} from "repopo/policies";
import { SortTsconfigsPolicy } from "sort-tsconfig";
const config: RepopoConfig = {
policies: [
makePolicy(NoJsFileExtensions, undefined, {
excludeFiles: [".*/bin/.*js"], // Exclude bin scripts
}),
makePolicy(PackageJsonProperties, {
verbatim: {
license: "MIT",
author: "Tyler Butler <tyler@tylerbutler.com>",
},
}),
makePolicy(PackageJsonSorted),
makePolicy(SortTsconfigsPolicy), // External policy from another package
],
};
export default config;
External Policies
Other packages can export policies (e.g., sort-tsconfig exports SortTsconfigsPolicy):
// In sort-tsconfig package
export const SortTsconfigsPolicy: PolicyDefinition = { /* ... */ };
// In repopo.config.ts
import { SortTsconfigsPolicy } from "sort-tsconfig";
const config: RepopoConfig = {
policies: [makePolicy(SortTsconfigsPolicy)],
};
API Exports
// Core exports
import { makePolicy, generatePackagePolicy } from "repopo";
import type {
RepopoConfig,
PolicyDefinition,
PolicyHandler,
PolicyFailure,
} from "repopo";
// Built-in policies
import {
HtmlFileHeaders,
JsTsFileHeaders,
NoJsFileExtensions,
PackageJsonProperties,
PackageJsonRepoDirectoryProperty,
PackageJsonSorted,
PackageScripts,
} from "repopo/policies";
// API utilities
import type { /* advanced types */ } from "repopo/api";
Testing Policies
// Example test structure
import { test, expect } from "vitest";
import { MyPolicy } from "./myPolicy.js";
import tmp from "tmp-promise";
import { writeFile } from "node:fs/promises";
test("MyPolicy fails on invalid file", async () => {
const { path: tmpDir } = await tmp.dir();
const testFile = "test.ts";
await writeFile(`${tmpDir}/${testFile}`, "invalid content");
const result = await MyPolicy.handler({
file: testFile,
root: tmpDir,
resolve: false,
config: undefined,
});
expect(result).not.toBe(true);
expect(result).toHaveProperty("errorMessage");
});
Key Constraints
- Policies must return
trueorPolicyFailure/PolicyFixResult - File paths in
matchregex are relative to repo root resolve=truemeans auto-fix should be applied (if supported)- All built-in policies are enabled by default
- Config file must have default export of type
RepopoConfig - Policies run on all files in git repo (respecting
.gitignore)