@gotgenes/pi-autoformat 0.1.0 → 4.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +1 -3
- package/.github/workflows/release-please.yml +29 -0
- package/.markdownlint-cli2.yaml +14 -2
- package/.pi/extensions/pi-autoformat/config.json +3 -6
- package/.pi/prompts/README.md +59 -0
- package/.pi/prompts/plan-issue.md +64 -0
- package/.pi/prompts/retro.md +144 -0
- package/.pi/prompts/ship-issue.md +77 -0
- package/.pi/prompts/tdd-plan.md +67 -0
- package/.pi/skills/pi-extension-lifecycle/SKILL.md +256 -0
- package/.release-please-manifest.json +1 -1
- package/AGENTS.md +39 -0
- package/CHANGELOG.md +365 -0
- package/README.md +42 -109
- package/biome.json +1 -1
- package/docs/assets/logo.png +0 -0
- package/docs/assets/logo.svg +533 -0
- package/docs/configuration.md +358 -38
- package/docs/plans/0001-initial-implementation-plan.md +17 -9
- package/docs/plans/0002-richer-tui-formatter-summaries.md +220 -0
- package/docs/plans/0003-additional-pi-mutation-tools.md +273 -0
- package/docs/plans/0004-shell-driven-mutation-coverage.md +296 -0
- package/docs/plans/0010-acceptance-test-coverage.md +240 -0
- package/docs/plans/0012-remove-unused-formatter-extensions-field.md +152 -0
- package/docs/plans/0013-fallback-chain-step-type.md +280 -0
- package/docs/plans/0014-batch-by-default-formatter-dispatch.md +195 -0
- package/docs/plans/0015-builtin-treefmt-and-treefmt-nix-support.md +290 -0
- package/docs/plans/0016-detailed-formatter-output-on-failure.md +245 -0
- package/docs/plans/0022-pi-coding-agent-types.md +201 -0
- package/docs/plans/0027-format-before-agent-exit-follow-up-turn.md +355 -0
- package/docs/plans/0031-turn-end-flush-with-change-detection.md +365 -0
- package/docs/retro/0002-richer-tui-formatter-summaries.md +47 -0
- package/docs/retro/0013-fallback-chain-step-type.md +67 -0
- package/docs/retro/0015-builtin-treefmt-and-treefmt-nix-support.md +56 -0
- package/docs/retro/0016-detailed-formatter-output-on-failure.md +60 -0
- package/docs/retro/0022-pi-coding-agent-types.md +62 -0
- package/docs/testing.md +95 -0
- package/package.json +30 -11
- package/prek.toml +2 -2
- package/schemas/pi-autoformat.schema.json +145 -21
- package/src/builtin-formatters.ts +205 -0
- package/src/command-probe.ts +66 -0
- package/src/config-loader.ts +829 -90
- package/src/custom-mutation-tools.ts +125 -0
- package/src/extension.ts +469 -82
- package/src/format-scope.ts +118 -0
- package/src/formatter-config.ts +73 -36
- package/src/formatter-executor.ts +230 -34
- package/src/formatter-output-report.ts +149 -0
- package/src/formatter-registry.ts +139 -30
- package/src/index.ts +26 -5
- package/src/prompt-autoformatter.ts +148 -23
- package/src/shell-mutation-detector.ts +572 -0
- package/src/touched-files-queue.ts +72 -11
- package/test/acceptance-event-bus.test.ts +138 -0
- package/test/acceptance.test.ts +69 -0
- package/test/builtin-formatters.test.ts +382 -0
- package/test/command-probe.test.ts +79 -0
- package/test/config-loader.test.ts +640 -21
- package/test/custom-mutation-tools.test.ts +190 -0
- package/test/extension.test.ts +1535 -158
- package/test/fallback-acceptance.test.ts +98 -0
- package/test/fixtures/event-bus-emitter.ts +26 -0
- package/test/fixtures/formatter-recorder.mjs +25 -0
- package/test/format-scope.test.ts +139 -0
- package/test/formatter-config.test.ts +56 -5
- package/test/formatter-executor.test.ts +555 -35
- package/test/formatter-output-report.test.ts +178 -0
- package/test/formatter-registry.test.ts +330 -37
- package/test/helpers/rpc.ts +146 -0
- package/test/prompt-autoformatter.test.ts +315 -22
- package/test/schema.test.ts +149 -0
- package/test/shell-mutation-detector.test.ts +221 -0
- package/test/touched-files-queue.test.ts +40 -1
- package/test/types/theme-stub.test-d.ts +42 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { FormatterOutputReportingConfig } from "../src/formatter-config.js";
|
|
3
|
+
import type { BatchRun } from "../src/formatter-executor.js";
|
|
4
|
+
import { formatRunOutputBlock } from "../src/formatter-output-report.js";
|
|
5
|
+
|
|
6
|
+
const ENABLED_STDERR: FormatterOutputReportingConfig = {
|
|
7
|
+
onFailure: "stderr",
|
|
8
|
+
maxBytes: 4096,
|
|
9
|
+
maxLines: 40,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const ENABLED_BOTH: FormatterOutputReportingConfig = {
|
|
13
|
+
onFailure: "both",
|
|
14
|
+
maxBytes: 4096,
|
|
15
|
+
maxLines: 40,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const DISABLED: FormatterOutputReportingConfig = {
|
|
19
|
+
onFailure: "none",
|
|
20
|
+
maxBytes: 4096,
|
|
21
|
+
maxLines: 40,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function failedRun(overrides: Partial<BatchRun> = {}): BatchRun {
|
|
25
|
+
return {
|
|
26
|
+
formatterName: "prettier",
|
|
27
|
+
command: ["prettier", "--write", "src/foo.ts"],
|
|
28
|
+
files: ["src/foo.ts"],
|
|
29
|
+
success: false,
|
|
30
|
+
exitCode: 2,
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("formatRunOutputBlock", () => {
|
|
36
|
+
it("returns undefined when onFailure is none", () => {
|
|
37
|
+
const block = formatRunOutputBlock(
|
|
38
|
+
failedRun({ stderr: "boom\n" }),
|
|
39
|
+
DISABLED,
|
|
40
|
+
);
|
|
41
|
+
expect(block).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns undefined for successful runs even when enabled", () => {
|
|
45
|
+
const block = formatRunOutputBlock(
|
|
46
|
+
failedRun({ success: true, exitCode: 0, stderr: "noisy success" }),
|
|
47
|
+
ENABLED_BOTH,
|
|
48
|
+
);
|
|
49
|
+
expect(block).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns undefined when both streams are empty / whitespace", () => {
|
|
53
|
+
const block = formatRunOutputBlock(
|
|
54
|
+
failedRun({ stdout: " \n ", stderr: "" }),
|
|
55
|
+
ENABLED_BOTH,
|
|
56
|
+
);
|
|
57
|
+
expect(block).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns undefined when stderr-only mode has no stderr", () => {
|
|
61
|
+
const block = formatRunOutputBlock(
|
|
62
|
+
failedRun({ stdout: "lots of stdout" }),
|
|
63
|
+
ENABLED_STDERR,
|
|
64
|
+
);
|
|
65
|
+
expect(block).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("renders only stderr under onFailure: stderr", () => {
|
|
69
|
+
const block = formatRunOutputBlock(
|
|
70
|
+
failedRun({ stdout: "ignored", stderr: "line one\nline two" }),
|
|
71
|
+
ENABLED_STDERR,
|
|
72
|
+
);
|
|
73
|
+
expect(block).toBe(
|
|
74
|
+
[" stderr:", " line one", " line two"].join("\n"),
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("renders stdout above stderr under onFailure: both", () => {
|
|
79
|
+
const block = formatRunOutputBlock(
|
|
80
|
+
failedRun({ stdout: "out one\nout two", stderr: "err one" }),
|
|
81
|
+
ENABLED_BOTH,
|
|
82
|
+
);
|
|
83
|
+
expect(block).toBe(
|
|
84
|
+
[
|
|
85
|
+
" stdout:",
|
|
86
|
+
" out one",
|
|
87
|
+
" out two",
|
|
88
|
+
" stderr:",
|
|
89
|
+
" err one",
|
|
90
|
+
].join("\n"),
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("omits the stdout block when stdout is empty under onFailure: both", () => {
|
|
95
|
+
const block = formatRunOutputBlock(
|
|
96
|
+
failedRun({ stdout: "", stderr: "err only" }),
|
|
97
|
+
ENABLED_BOTH,
|
|
98
|
+
);
|
|
99
|
+
expect(block).toBe([" stderr:", " err only"].join("\n"));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("strips trailing whitespace before rendering", () => {
|
|
103
|
+
const block = formatRunOutputBlock(
|
|
104
|
+
failedRun({ stderr: "useful line\n\n \n" }),
|
|
105
|
+
ENABLED_STDERR,
|
|
106
|
+
);
|
|
107
|
+
expect(block).toBe([" stderr:", " useful line"].join("\n"));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("truncates by line count and reports how many earlier lines were dropped", () => {
|
|
111
|
+
const stderr = Array.from({ length: 50 }, (_, i) => `line${i + 1}`).join(
|
|
112
|
+
"\n",
|
|
113
|
+
);
|
|
114
|
+
const block = formatRunOutputBlock(failedRun({ stderr }), {
|
|
115
|
+
onFailure: "stderr",
|
|
116
|
+
maxBytes: 65536,
|
|
117
|
+
maxLines: 5,
|
|
118
|
+
});
|
|
119
|
+
expect(block).toBeDefined();
|
|
120
|
+
const lines = (block ?? "").split("\n");
|
|
121
|
+
expect(lines[0]).toBe(" stderr:");
|
|
122
|
+
expect(lines[1]).toBe(" ... (truncated, 45 earlier lines)");
|
|
123
|
+
expect(lines.slice(2)).toEqual([
|
|
124
|
+
" line46",
|
|
125
|
+
" line47",
|
|
126
|
+
" line48",
|
|
127
|
+
" line49",
|
|
128
|
+
" line50",
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("truncates by byte cap and snaps forward to a newline boundary", () => {
|
|
133
|
+
// 10 lines, each 100 bytes -> ~1000 bytes total; cap at 250 bytes.
|
|
134
|
+
const stderr = Array.from(
|
|
135
|
+
{ length: 10 },
|
|
136
|
+
(_, i) => `${String(i).padStart(2, "0")}-${"x".repeat(96)}`,
|
|
137
|
+
).join("\n");
|
|
138
|
+
const block = formatRunOutputBlock(failedRun({ stderr }), {
|
|
139
|
+
onFailure: "stderr",
|
|
140
|
+
maxBytes: 250,
|
|
141
|
+
maxLines: 100,
|
|
142
|
+
});
|
|
143
|
+
expect(block).toBeDefined();
|
|
144
|
+
const lines = (block ?? "").split("\n");
|
|
145
|
+
expect(lines[0]).toBe(" stderr:");
|
|
146
|
+
expect(lines[1]).toMatch(/^ {4}\.\.\. \(truncated, \d+ earlier bytes\)$/);
|
|
147
|
+
// Only the tail lines survive; the first line ("00-…") must be gone.
|
|
148
|
+
expect(block).not.toContain("00-x");
|
|
149
|
+
expect(block).toContain("09-x");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("does not bisect a multibyte UTF-8 character at the byte boundary", () => {
|
|
153
|
+
// Each emoji is 4 bytes in UTF-8. Keep last ~10 bytes after a long prefix.
|
|
154
|
+
const head = "x".repeat(100);
|
|
155
|
+
const tail = "🌟🌟🌟"; // 12 bytes
|
|
156
|
+
const stderr = `${head}\n${tail}`;
|
|
157
|
+
const block = formatRunOutputBlock(failedRun({ stderr }), {
|
|
158
|
+
onFailure: "stderr",
|
|
159
|
+
maxBytes: 14,
|
|
160
|
+
maxLines: 100,
|
|
161
|
+
});
|
|
162
|
+
expect(block).toBeDefined();
|
|
163
|
+
// Whatever survived must be valid UTF-8 (round-tripping through Buffer
|
|
164
|
+
// preserves bytes; a bisected sequence would render as U+FFFD).
|
|
165
|
+
expect(block).not.toContain("\uFFFD");
|
|
166
|
+
expect(block).toContain("🌟");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("leaves output unchanged when both caps are well above the content size", () => {
|
|
170
|
+
const stderr = "short and tidy";
|
|
171
|
+
const block = formatRunOutputBlock(failedRun({ stderr }), {
|
|
172
|
+
onFailure: "stderr",
|
|
173
|
+
maxBytes: 10_000,
|
|
174
|
+
maxLines: 100,
|
|
175
|
+
});
|
|
176
|
+
expect(block).toBe([" stderr:", " short and tidy"].join("\n"));
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -2,74 +2,367 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
type FormatterConfig,
|
|
5
|
-
|
|
5
|
+
groupFilesByChain,
|
|
6
|
+
resolveChain,
|
|
7
|
+
resolveChainSteps,
|
|
6
8
|
} from "../src/formatter-registry.js";
|
|
7
9
|
|
|
8
|
-
describe("
|
|
10
|
+
describe("groupFilesByChain", () => {
|
|
11
|
+
const config: FormatterConfig = {
|
|
12
|
+
formatters: {
|
|
13
|
+
prettier: { command: ["prettier", "--write"] },
|
|
14
|
+
markdownlint: { command: ["markdownlint-cli2", "--fix"] },
|
|
15
|
+
biome: { command: ["biome", "format", "--write"] },
|
|
16
|
+
},
|
|
17
|
+
chains: {
|
|
18
|
+
".md": ["prettier", "markdownlint"],
|
|
19
|
+
".markdown": ["prettier", "markdownlint"],
|
|
20
|
+
".ts": ["prettier"],
|
|
21
|
+
".js": ["prettier"],
|
|
22
|
+
".rs": ["biome"],
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
it("groups files that share a chain into one group", () => {
|
|
27
|
+
const groups = groupFilesByChain(
|
|
28
|
+
["/repo/a.md", "/repo/b.md", "/repo/c.md"],
|
|
29
|
+
config,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(groups).toEqual([
|
|
33
|
+
{
|
|
34
|
+
chain: ["prettier", "markdownlint"],
|
|
35
|
+
files: ["/repo/a.md", "/repo/b.md", "/repo/c.md"],
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("creates separate groups for distinct chains", () => {
|
|
41
|
+
const groups = groupFilesByChain(
|
|
42
|
+
["/repo/a.md", "/repo/b.ts", "/repo/c.rs"],
|
|
43
|
+
config,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(groups).toEqual([
|
|
47
|
+
{ chain: ["prettier", "markdownlint"], files: ["/repo/a.md"] },
|
|
48
|
+
{ chain: ["prettier"], files: ["/repo/b.ts"] },
|
|
49
|
+
{ chain: ["biome"], files: ["/repo/c.rs"] },
|
|
50
|
+
]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("merges different extensions that resolve to the same chain", () => {
|
|
54
|
+
const groups = groupFilesByChain(
|
|
55
|
+
["/repo/a.md", "/repo/b.markdown", "/repo/c.ts", "/repo/d.js"],
|
|
56
|
+
config,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(groups).toEqual([
|
|
60
|
+
{
|
|
61
|
+
chain: ["prettier", "markdownlint"],
|
|
62
|
+
files: ["/repo/a.md", "/repo/b.markdown"],
|
|
63
|
+
},
|
|
64
|
+
{ chain: ["prettier"], files: ["/repo/c.ts", "/repo/d.js"] },
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("drops files with no chain", () => {
|
|
69
|
+
const groups = groupFilesByChain(
|
|
70
|
+
["/repo/a.md", "/repo/logo.png", "/repo/notes.txt"],
|
|
71
|
+
config,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(groups).toEqual([
|
|
75
|
+
{ chain: ["prettier", "markdownlint"], files: ["/repo/a.md"] },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("preserves first-seen group order and within-group file order", () => {
|
|
80
|
+
const groups = groupFilesByChain(
|
|
81
|
+
["/repo/x.ts", "/repo/a.md", "/repo/y.ts", "/repo/b.md"],
|
|
82
|
+
config,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(groups).toEqual([
|
|
86
|
+
{ chain: ["prettier"], files: ["/repo/x.ts", "/repo/y.ts"] },
|
|
87
|
+
{
|
|
88
|
+
chain: ["prettier", "markdownlint"],
|
|
89
|
+
files: ["/repo/a.md", "/repo/b.md"],
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns no groups when given no files", () => {
|
|
95
|
+
expect(groupFilesByChain([], config)).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("with a wildcard chain", () => {
|
|
99
|
+
const wildcardConfig: FormatterConfig = {
|
|
100
|
+
formatters: {
|
|
101
|
+
prettier: { command: ["prettier", "--write"] },
|
|
102
|
+
markdownlint: { command: ["markdownlint-cli2", "--fix"] },
|
|
103
|
+
},
|
|
104
|
+
chains: {
|
|
105
|
+
"*": ["treefmt"],
|
|
106
|
+
".md": ["prettier", "markdownlint"],
|
|
107
|
+
".ts": ["prettier"],
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
it("emits the wildcard group first with every touched file", () => {
|
|
112
|
+
const groups = groupFilesByChain(
|
|
113
|
+
["/repo/a.md", "/repo/b.ts", "/repo/Makefile"],
|
|
114
|
+
wildcardConfig,
|
|
115
|
+
);
|
|
116
|
+
expect(groups[0]).toEqual({
|
|
117
|
+
chain: ["treefmt"],
|
|
118
|
+
files: ["/repo/a.md", "/repo/b.ts", "/repo/Makefile"],
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("keeps per-extension groups after the wildcard group", () => {
|
|
123
|
+
const groups = groupFilesByChain(
|
|
124
|
+
["/repo/a.md", "/repo/b.ts"],
|
|
125
|
+
wildcardConfig,
|
|
126
|
+
);
|
|
127
|
+
expect(groups).toEqual([
|
|
128
|
+
{
|
|
129
|
+
chain: ["treefmt"],
|
|
130
|
+
files: ["/repo/a.md", "/repo/b.ts"],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
chain: ["prettier", "markdownlint"],
|
|
134
|
+
files: ["/repo/a.md"],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
chain: ["prettier"],
|
|
138
|
+
files: ["/repo/b.ts"],
|
|
139
|
+
},
|
|
140
|
+
]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("includes extensionless files in the wildcard group only", () => {
|
|
144
|
+
const groups = groupFilesByChain(
|
|
145
|
+
["/repo/Makefile", "/repo/notes"],
|
|
146
|
+
wildcardConfig,
|
|
147
|
+
);
|
|
148
|
+
expect(groups).toEqual([
|
|
149
|
+
{
|
|
150
|
+
chain: ["treefmt"],
|
|
151
|
+
files: ["/repo/Makefile", "/repo/notes"],
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('emits no wildcard group when chains["*"] is absent', () => {
|
|
157
|
+
const groups = groupFilesByChain(["/repo/a.md"], {
|
|
158
|
+
formatters: wildcardConfig.formatters,
|
|
159
|
+
chains: {
|
|
160
|
+
".md": ["prettier"],
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
expect(groups).toEqual([{ chain: ["prettier"], files: ["/repo/a.md"] }]);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("with fallback steps", () => {
|
|
168
|
+
const fallbackConfig: FormatterConfig = {
|
|
169
|
+
formatters: {
|
|
170
|
+
biome: { command: ["biome", "format", "--write"] },
|
|
171
|
+
prettier: { command: ["prettier", "--write"] },
|
|
172
|
+
"markdownlint-cli2": {
|
|
173
|
+
command: ["markdownlint-cli2", "--fix"],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
chains: {
|
|
177
|
+
".ts": [{ fallback: ["biome", "prettier"] }],
|
|
178
|
+
".tsx": [{ fallback: ["biome", "prettier"] }],
|
|
179
|
+
".md": [{ fallback: ["biome", "prettier"] }, "markdownlint-cli2"],
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
it("keeps the original chain step shape in the returned group", () => {
|
|
184
|
+
const groups = groupFilesByChain(["/repo/a.ts"], fallbackConfig);
|
|
185
|
+
expect(groups).toEqual([
|
|
186
|
+
{
|
|
187
|
+
chain: [{ fallback: ["biome", "prettier"] }],
|
|
188
|
+
files: ["/repo/a.ts"],
|
|
189
|
+
},
|
|
190
|
+
]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("groups identical fallback chains across extensions", () => {
|
|
194
|
+
const groups = groupFilesByChain(
|
|
195
|
+
["/repo/a.ts", "/repo/b.tsx"],
|
|
196
|
+
fallbackConfig,
|
|
197
|
+
);
|
|
198
|
+
expect(groups).toEqual([
|
|
199
|
+
{
|
|
200
|
+
chain: [{ fallback: ["biome", "prettier"] }],
|
|
201
|
+
files: ["/repo/a.ts", "/repo/b.tsx"],
|
|
202
|
+
},
|
|
203
|
+
]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("groups mixed string + fallback chains", () => {
|
|
207
|
+
const groups = groupFilesByChain(
|
|
208
|
+
["/repo/a.ts", "/repo/b.md", "/repo/c.md"],
|
|
209
|
+
fallbackConfig,
|
|
210
|
+
);
|
|
211
|
+
expect(groups).toHaveLength(2);
|
|
212
|
+
const md = groups.find((g) => g.files.includes("/repo/b.md"));
|
|
213
|
+
expect(md?.chain).toEqual([
|
|
214
|
+
{ fallback: ["biome", "prettier"] },
|
|
215
|
+
"markdownlint-cli2",
|
|
216
|
+
]);
|
|
217
|
+
expect(md?.files).toEqual(["/repo/b.md", "/repo/c.md"]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("separates groups whose fallback ordering differs", () => {
|
|
221
|
+
const reorderedConfig: FormatterConfig = {
|
|
222
|
+
formatters: fallbackConfig.formatters,
|
|
223
|
+
chains: {
|
|
224
|
+
".ts": [{ fallback: ["biome", "prettier"] }],
|
|
225
|
+
".js": [{ fallback: ["prettier", "biome"] }],
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
const groups = groupFilesByChain(
|
|
229
|
+
["/repo/a.ts", "/repo/b.js"],
|
|
230
|
+
reorderedConfig,
|
|
231
|
+
);
|
|
232
|
+
expect(groups).toHaveLength(2);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("resolveChain", () => {
|
|
9
238
|
const config: FormatterConfig = {
|
|
10
239
|
formatters: {
|
|
11
240
|
prettier: {
|
|
12
|
-
command: ["prettier", "--write"
|
|
13
|
-
|
|
241
|
+
command: ["prettier", "--write"],
|
|
242
|
+
environment: { PRETTIERD_DEFAULT_CONFIG: "./.prettierrc" },
|
|
14
243
|
},
|
|
15
244
|
markdownlint: {
|
|
16
|
-
command: ["markdownlint-cli2", "--fix"
|
|
17
|
-
|
|
245
|
+
command: ["markdownlint-cli2", "--fix"],
|
|
246
|
+
},
|
|
247
|
+
disabled: {
|
|
248
|
+
command: ["never"],
|
|
249
|
+
disabled: true,
|
|
18
250
|
},
|
|
19
251
|
},
|
|
20
|
-
chains: {
|
|
21
|
-
".md": ["prettier", "markdownlint"],
|
|
22
|
-
},
|
|
252
|
+
chains: {},
|
|
23
253
|
};
|
|
24
254
|
|
|
25
|
-
it("resolves
|
|
26
|
-
const
|
|
255
|
+
it("resolves formatters in declared order", () => {
|
|
256
|
+
const resolved = resolveChain(["prettier", "markdownlint"], config);
|
|
27
257
|
|
|
28
|
-
expect(
|
|
258
|
+
expect(resolved.map((entry) => entry.name)).toEqual([
|
|
29
259
|
"prettier",
|
|
30
260
|
"markdownlint",
|
|
31
261
|
]);
|
|
32
262
|
});
|
|
33
263
|
|
|
34
|
-
it("returns
|
|
35
|
-
const
|
|
264
|
+
it("returns the configured command verbatim (no $FILE substitution)", () => {
|
|
265
|
+
const resolved = resolveChain(["prettier"], config);
|
|
266
|
+
|
|
267
|
+
expect(resolved[0]?.command).toEqual(["prettier", "--write"]);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("propagates the formatter environment", () => {
|
|
271
|
+
const resolved = resolveChain(["prettier"], config);
|
|
36
272
|
|
|
37
|
-
expect(
|
|
273
|
+
expect(resolved[0]?.environment).toEqual({
|
|
274
|
+
PRETTIERD_DEFAULT_CONFIG: "./.prettierrc",
|
|
275
|
+
});
|
|
38
276
|
});
|
|
39
277
|
|
|
40
|
-
it("
|
|
41
|
-
const
|
|
278
|
+
it("skips disabled formatters", () => {
|
|
279
|
+
const resolved = resolveChain(
|
|
280
|
+
["prettier", "disabled", "markdownlint"],
|
|
281
|
+
config,
|
|
282
|
+
);
|
|
42
283
|
|
|
43
|
-
expect(
|
|
284
|
+
expect(resolved.map((entry) => entry.name)).toEqual([
|
|
44
285
|
"prettier",
|
|
45
|
-
"
|
|
46
|
-
"/repo/docs/readme.md",
|
|
286
|
+
"markdownlint",
|
|
47
287
|
]);
|
|
48
288
|
});
|
|
49
289
|
|
|
50
|
-
it("skips
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
290
|
+
it("skips unknown formatter names", () => {
|
|
291
|
+
const resolved = resolveChain(["prettier", "nonexistent"], config);
|
|
292
|
+
|
|
293
|
+
expect(resolved.map((entry) => entry.name)).toEqual(["prettier"]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("returns an empty array for an empty chain", () => {
|
|
297
|
+
expect(resolveChain([], config)).toEqual([]);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe("resolveChainSteps", () => {
|
|
302
|
+
const config: FormatterConfig = {
|
|
303
|
+
formatters: {
|
|
304
|
+
prettier: { command: ["prettier", "--write"] },
|
|
305
|
+
biome: { command: ["biome", "format", "--write"] },
|
|
306
|
+
"markdownlint-cli2": {
|
|
307
|
+
command: ["markdownlint-cli2", "--fix"],
|
|
59
308
|
},
|
|
60
|
-
|
|
309
|
+
off: { command: ["never"], disabled: true },
|
|
310
|
+
},
|
|
311
|
+
chains: {},
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
it("resolves a single string step to kind 'single'", () => {
|
|
315
|
+
const resolved = resolveChainSteps(["prettier"], config);
|
|
316
|
+
expect(resolved).toHaveLength(1);
|
|
317
|
+
expect(resolved[0]?.kind).toBe("single");
|
|
318
|
+
if (resolved[0]?.kind === "single") {
|
|
319
|
+
expect(resolved[0].formatter.name).toBe("prettier");
|
|
320
|
+
expect(resolved[0].formatter.command).toEqual(["prettier", "--write"]);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
61
323
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
324
|
+
it("drops a single step that names an unknown or disabled formatter", () => {
|
|
325
|
+
expect(resolveChainSteps(["nope"], config)).toEqual([]);
|
|
326
|
+
expect(resolveChainSteps(["off"], config)).toEqual([]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("resolves a fallback step to kind 'fallback' with alternatives in order", () => {
|
|
330
|
+
const resolved = resolveChainSteps(
|
|
331
|
+
[{ fallback: ["biome", "prettier"] }],
|
|
332
|
+
config,
|
|
65
333
|
);
|
|
334
|
+
expect(resolved).toHaveLength(1);
|
|
335
|
+
expect(resolved[0]?.kind).toBe("fallback");
|
|
336
|
+
if (resolved[0]?.kind === "fallback") {
|
|
337
|
+
expect(resolved[0].alternatives.map((a) => a.name)).toEqual([
|
|
338
|
+
"biome",
|
|
339
|
+
"prettier",
|
|
340
|
+
]);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
66
343
|
|
|
67
|
-
|
|
344
|
+
it("drops disabled and unknown alternatives within a fallback step", () => {
|
|
345
|
+
const resolved = resolveChainSteps(
|
|
346
|
+
[{ fallback: ["off", "unknown", "prettier"] }],
|
|
347
|
+
config,
|
|
348
|
+
);
|
|
349
|
+
expect(resolved).toHaveLength(1);
|
|
350
|
+
if (resolved[0]?.kind === "fallback") {
|
|
351
|
+
expect(resolved[0].alternatives.map((a) => a.name)).toEqual(["prettier"]);
|
|
352
|
+
}
|
|
68
353
|
});
|
|
69
354
|
|
|
70
|
-
it("
|
|
71
|
-
|
|
355
|
+
it("drops a fallback step whose alternatives all reduce away", () => {
|
|
356
|
+
expect(
|
|
357
|
+
resolveChainSteps([{ fallback: ["off", "unknown"] }], config),
|
|
358
|
+
).toEqual([]);
|
|
359
|
+
});
|
|
72
360
|
|
|
73
|
-
|
|
361
|
+
it("resolves mixed string + fallback chains preserving order", () => {
|
|
362
|
+
const resolved = resolveChainSteps(
|
|
363
|
+
[{ fallback: ["biome", "prettier"] }, "markdownlint-cli2"],
|
|
364
|
+
config,
|
|
365
|
+
);
|
|
366
|
+
expect(resolved.map((s) => s.kind)).toEqual(["fallback", "single"]);
|
|
74
367
|
});
|
|
75
368
|
});
|