Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ yours. `owned` also takes a single key of a shared file, such as `.github/workfl
`package.json#devDependencies.lefthook`; repokeeper then leaves that key, and everything under it,
alone. Every write command accepts `--dry-run`.

Keep your formatter off the files repokeeper writes: a reformatted copy no longer matches, and
`check` reports it as edited. With Biome (2.6 also formats Markdown and YAML) or Prettier in a Node
repository, `init` and `update` name the new files to exclude in `biome.json` or `.prettierignore`.

`init` applies the whole standard unless you say otherwise:

- `repokeeper init --preset essential` writes only the editor settings, `.gitignore` and the CI
Expand Down
25 changes: 25 additions & 0 deletions src/commands/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,29 @@ export function printResult(io: Io, result: SyncResult): void {

const WROTE: string[] = ["create", "write", "adopt"];

/** Files Biome (2.6 and later) or Prettier would format: a formatted copy no longer matches what check expects. */
const FORMATTED = /\.(m?js|cjs|json|jsonc|md|ya?ml)$/;

/** Said when such a file is new: the formatter has to leave repokeeper's files, and its lock, alone. */
function formatterStep(ctx: ModuleContext, result: SyncResult): string | null {
const jobs = ctx.stacks.filter((stack) => stack.id === "node" && !stack.directory).flatMap((stack) => stack.staged);
const tool = jobs.some((job) => job.name === "node:biome")
? "Biome"
: jobs.some((job) => job.name === "node:prettier")
? "Prettier"
: null;
if (!tool) return null;
const paths = result.decisions
.filter((d) => (d.output.kind === "file" || d.output.kind === "seed") && d.action === "create")
.map((d) => d.output.path)
.filter((path) => FORMATTED.test(path));
if (paths.length === 0) return null;
const why = `${tool} would reformat files repokeeper owns, and \`repokeeper check\` would then report them as edited`;
return tool === "Biome"
? `${why}; add ${[...paths, ".repokeeper"].map((p) => `"!${p}"`).join(", ")} to files.includes in biome.json`
: `${why}; add ${[...paths, ".repokeeper/"].join(", ")} to .prettierignore`;
}

/** What the user still has to do after a write: install the hooks or the dependencies, renormalize line endings. */
export async function nextSteps(root: string, ctx: ModuleContext, result: SyncResult): Promise<string[]> {
const steps: string[] = [];
Expand Down Expand Up @@ -127,6 +150,8 @@ export async function nextSteps(root: string, ctx: ModuleContext, result: SyncRe
const plan = publishPlan(ctx);
if (!("problem" in plan)) steps.push(trustedPublisherStep(ctx, plan.registry));
}
const formatter = formatterStep(ctx, result);
if (formatter) steps.push(formatter);
if (wrote(".gitattributes")) {
const crlf = await crlfTrackedFiles(root);
if (crlf.length > 0) {
Expand Down
38 changes: 36 additions & 2 deletions test/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { nextSteps } from "../src/commands/report.js";
import type { Output } from "../src/model.js";
import type { SyncResult } from "../src/sync/sync.js";
import { TOOL_VERSIONS } from "../src/version.js";
import { makeContext } from "./helpers.js";
import { makeContext, nodeResolved } from "./helpers.js";

const hooksFile: Output = { kind: "file", module: "hooks", path: "lefthook.yml", content: "" };
const lefthookRange = (path = "package.json"): Output => ({
Expand Down Expand Up @@ -40,7 +40,7 @@ describe("what to do after a write", () => {
it("says it once when the hooks are new: installing them is the same step", async () => {
const steps = await nextSteps(
".",
makeContext(),
makeContext({ stacks: [nodeResolved({ staged: [] })] }),
result({ output: hooksFile, action: "create" }, { output: lefthookRange(), action: "create" }),
);
expect(steps).toEqual(["install dependencies; this installs the git hooks"]);
Expand All @@ -55,3 +55,37 @@ describe("what to do after a write", () => {
expect(steps).toEqual([]);
});
});

describe("telling a formatter to leave repokeeper's files alone", () => {
const file = (path: string): Output => ({ kind: "file", module: "health", path, content: "" });
const biome = nodeResolved({
staged: [{ name: "node:biome", glob: "*.{js,json}", run: "npx biome check --write {staged_files}" }],
});
const created = result(
{ output: file("CODE_OF_CONDUCT.md"), action: "create" },
{ output: file("commitlint.config.mjs"), action: "create" },
{ output: file("LICENSE"), action: "create" },
{ output: file("SECURITY.md"), action: "unchanged" },
);

// Biome 2.6 formats Markdown and YAML too; a formatted copy no longer matches, and check reports it as edited.
it("names the new files to exclude in biome.json", async () => {
const steps = await nextSteps(".", makeContext({ stacks: [biome] }), created);
expect(steps).toEqual([
'Biome would reformat files repokeeper owns, and `repokeeper check` would then report them as edited; add "!CODE_OF_CONDUCT.md", "!commitlint.config.mjs", "!.repokeeper" to files.includes in biome.json',
]);
});

it("names them for .prettierignore with Prettier", async () => {
const steps = await nextSteps(".", makeContext(), created);
expect(steps).toEqual([
"Prettier would reformat files repokeeper owns, and `repokeeper check` would then report them as edited; add CODE_OF_CONDUCT.md, commitlint.config.mjs, .repokeeper/ to .prettierignore",
]);
});

it("says nothing without a formatter, or when no such file is new", async () => {
expect(await nextSteps(".", makeContext({ stacks: [nodeResolved({ staged: [] })] }), created)).toEqual([]);
const unchanged = result({ output: file("CODE_OF_CONDUCT.md"), action: "unchanged" });
expect(await nextSteps(".", makeContext({ stacks: [biome] }), unchanged)).toEqual([]);
});
});
Loading