perf(cli): compile ignore patterns once

Long-running CLI processes rebuilt the same minimatch ASTs for every checked path.

Compile patterns while loading ignore rules and reuse them until the rules are reloaded.

Refs #1006
This commit is contained in:
Ouyang Xingyuan
2026-07-13 10:47:35 +08:00
parent 9c375bd6fd
commit f28d886603
2 changed files with 44 additions and 5 deletions
@@ -1,7 +1,28 @@
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const minimatchStats = vi.hoisted(() => ({ constructions: 0 }));
vi.mock("minimatch", async (importOriginal) => {
const actual = await importOriginal<typeof import("minimatch")>();
class CountingMinimatch extends actual.Minimatch {
constructor(pattern: string, options?: import("minimatch").MinimatchOptions) {
super(pattern, options);
minimatchStats.constructions++;
}
}
return {
...actual,
Minimatch: CountingMinimatch,
minimatch: (path: string, pattern: string, options?: import("minimatch").MinimatchOptions) =>
new CountingMinimatch(pattern, options).match(path),
};
});
import { IgnoreRules } from "./IgnoreRules";
describe("IgnoreRules", () => {
@@ -19,6 +40,10 @@ describe("IgnoreRules", () => {
await fs.writeFile(path.join(ignoreDir, "ignore"), content, "utf-8");
}
beforeEach(() => {
minimatchStats.constructions = 0;
});
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
});
@@ -55,6 +80,20 @@ describe("IgnoreRules", () => {
});
describe("shouldIgnore", () => {
it("compiles loaded patterns once", async () => {
const vaultPath = await createVault();
await writeIgnoreFile(vaultPath, "*.tmp\nbuild/\n");
const rules = new IgnoreRules(vaultPath);
await rules.load();
expect(rules.shouldIgnore("notes/readme.md")).toBe(false);
expect(rules.shouldIgnore("notes/scratch.tmp")).toBe(true);
expect(rules.shouldIgnore("build/output.js")).toBe(true);
expect(rules.shouldIgnore("other.md")).toBe(false);
expect(minimatchStats.constructions).toBe(2);
});
it("matches **/*.tmp against notes/scratch.tmp", async () => {
const vaultPath = await createVault();
await writeIgnoreFile(vaultPath, "*.tmp\n");