block-in-file 1.0.0
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/- +3 -0
- package/.beads/README.md +85 -0
- package/.beads/config.yaml +67 -0
- package/.beads/interactions.jsonl +0 -0
- package/.beads/issues.jsonl +23 -0
- package/.beads/metadata.json +4 -0
- package/.git-blame-ignore-revs +2 -0
- package/.gitattributes +3 -0
- package/.prettierrc.json +5 -0
- package/AGENTS.md +40 -0
- package/README.md +122 -0
- package/block-in-file.ts +150 -0
- package/content +10 -0
- package/deno.json +14 -0
- package/deno.lock +1084 -0
- package/doc/PLAN-envsubst.md +200 -0
- package/doc/PLAN-restructure.md +114 -0
- package/package.json +44 -0
- package/src/attributes.ts +161 -0
- package/src/backup.ts +180 -0
- package/src/block-parser.ts +170 -0
- package/src/block-remover.ts +128 -0
- package/src/conflict-detection.ts +179 -0
- package/src/defaults.ts +23 -0
- package/src/envsubst.ts +59 -0
- package/src/file-processor.ts +378 -0
- package/src/index.ts +5 -0
- package/src/input.ts +69 -0
- package/src/mode-handler.ts +39 -0
- package/src/output.ts +107 -0
- package/src/plugins/.beads/.local_version +1 -0
- package/src/plugins/.beads/issues.jsonl +21 -0
- package/src/plugins/.beads/metadata.json +4 -0
- package/src/plugins/config.ts +282 -0
- package/src/plugins/diff.ts +109 -0
- package/src/plugins/io.ts +72 -0
- package/src/plugins/logger.ts +41 -0
- package/src/tags/tag-merger.ts +31 -0
- package/src/tags/tag-mode.ts +1 -0
- package/src/tags/tag.ts +36 -0
- package/src/tags/tags.ts +4 -0
- package/src/tags/types.ts +4 -0
- package/src/timestamp.ts +39 -0
- package/src/types.ts +32 -0
- package/src/validation.ts +11 -0
- package/test/additive-cli.test.ts +109 -0
- package/test/additive.test.ts +233 -0
- package/test/attributes-integration.test.ts +161 -0
- package/test/attributes.test.ts +100 -0
- package/test/backup.test.ts +386 -0
- package/test/block-in-file.test.ts +235 -0
- package/test/block-parser.test.ts +221 -0
- package/test/block-remover.test.ts +209 -0
- package/test/cli.test.ts +254 -0
- package/test/defaults.test.ts +38 -0
- package/test/envsubst-edge-cases.test.ts +116 -0
- package/test/envsubst-integration.test.ts +78 -0
- package/test/envsubst.test.ts +184 -0
- package/test/input.test.ts +86 -0
- package/test/mode.test.ts +193 -0
- package/test/output.test.ts +44 -0
- package/test/tag-merger.test.ts +176 -0
- package/test/tags.test.ts +116 -0
- package/test/timestamp-integration.test.ts +209 -0
- package/test/timestamp.test.ts +76 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
describe("BlockInFile integration", () => {
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "blockinfile-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function runBlockInFile(args: string, input?: string): string {
|
|
19
|
+
const cwd = path.resolve(import.meta.dirname!, "..");
|
|
20
|
+
const cmd = `npx tsx block-in-file.ts ${args}`;
|
|
21
|
+
return execSync(cmd, {
|
|
22
|
+
cwd,
|
|
23
|
+
input,
|
|
24
|
+
encoding: "utf-8",
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("basic operations", () => {
|
|
29
|
+
it("inserts block into file", async () => {
|
|
30
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
31
|
+
await fs.writeFile(targetFile, "line1\nline2\n");
|
|
32
|
+
|
|
33
|
+
runBlockInFile(targetFile, "INSERTED CONTENT");
|
|
34
|
+
|
|
35
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
36
|
+
expect(result).toContain("# blockinfile start");
|
|
37
|
+
expect(result).toContain("INSERTED CONTENT");
|
|
38
|
+
expect(result).toContain("# blockinfile end");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("replaces existing block", async () => {
|
|
42
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
43
|
+
await fs.writeFile(targetFile, "line1\n# blockinfile start\nOLD\n# blockinfile end\nline2\n");
|
|
44
|
+
|
|
45
|
+
runBlockInFile(targetFile, "NEW");
|
|
46
|
+
|
|
47
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
48
|
+
expect(result).toContain("NEW");
|
|
49
|
+
expect(result).not.toContain("OLD");
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("options", () => {
|
|
54
|
+
it("uses custom comment character", async () => {
|
|
55
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
56
|
+
await fs.writeFile(targetFile, "line1\n");
|
|
57
|
+
|
|
58
|
+
runBlockInFile(`-c "//" ${targetFile}`, "CONTENT");
|
|
59
|
+
|
|
60
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
61
|
+
expect(result).toContain("// blockinfile start");
|
|
62
|
+
expect(result).toContain("// blockinfile end");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("uses custom block name", async () => {
|
|
66
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
67
|
+
await fs.writeFile(targetFile, "line1\n");
|
|
68
|
+
|
|
69
|
+
runBlockInFile(`-n myblock ${targetFile}`, "CONTENT");
|
|
70
|
+
|
|
71
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
72
|
+
expect(result).toContain("# myblock start");
|
|
73
|
+
expect(result).toContain("# myblock end");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("uses custom markers", async () => {
|
|
77
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
78
|
+
await fs.writeFile(targetFile, "line1\n");
|
|
79
|
+
|
|
80
|
+
runBlockInFile(`--marker-start BEGIN --marker-end END ${targetFile}`, "CONTENT");
|
|
81
|
+
|
|
82
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
83
|
+
expect(result).toContain("# blockinfile BEGIN");
|
|
84
|
+
expect(result).toContain("# blockinfile END");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("uses dos line endings", async () => {
|
|
88
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
89
|
+
await fs.writeFile(targetFile, "line1\nline2\n");
|
|
90
|
+
|
|
91
|
+
runBlockInFile(`--dos ${targetFile}`, "CONTENT");
|
|
92
|
+
|
|
93
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
94
|
+
expect(result).toContain("\r\n");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("before/after positioning", () => {
|
|
99
|
+
it("inserts before matching pattern", async () => {
|
|
100
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
101
|
+
await fs.writeFile(targetFile, "line1\nMARKER\nline3\n");
|
|
102
|
+
|
|
103
|
+
runBlockInFile(`-b MARKER ${targetFile}`, "INSERTED");
|
|
104
|
+
|
|
105
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
106
|
+
const lines = result.split("\n");
|
|
107
|
+
const markerIndex = lines.indexOf("MARKER");
|
|
108
|
+
const startIndex = lines.indexOf("# blockinfile start");
|
|
109
|
+
expect(startIndex).toBeLessThan(markerIndex);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("inserts after matching pattern", async () => {
|
|
113
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
114
|
+
await fs.writeFile(targetFile, "line1\nMARKER\nline3\n");
|
|
115
|
+
|
|
116
|
+
runBlockInFile(`-a MARKER ${targetFile}`, "INSERTED");
|
|
117
|
+
|
|
118
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
119
|
+
const lines = result.split("\n");
|
|
120
|
+
const markerIndex = lines.indexOf("MARKER");
|
|
121
|
+
const startIndex = lines.indexOf("# blockinfile start");
|
|
122
|
+
expect(startIndex).toBeGreaterThan(markerIndex);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("output modes", () => {
|
|
127
|
+
it("writes to different output file", async () => {
|
|
128
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
129
|
+
const outputFile = path.join(tempDir, "output.txt");
|
|
130
|
+
await fs.writeFile(targetFile, "original\n");
|
|
131
|
+
|
|
132
|
+
runBlockInFile(`-o ${outputFile} ${targetFile}`, "CONTENT");
|
|
133
|
+
|
|
134
|
+
const originalContent = await fs.readFile(targetFile, "utf-8");
|
|
135
|
+
expect(originalContent).toBe("original\n");
|
|
136
|
+
|
|
137
|
+
const outputContent = await fs.readFile(outputFile, "utf-8");
|
|
138
|
+
expect(outputContent).toContain("CONTENT");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("outputs to stdout with -o -", async () => {
|
|
142
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
143
|
+
await fs.writeFile(targetFile, "original\n");
|
|
144
|
+
|
|
145
|
+
const output = runBlockInFile(`-o - ${targetFile}`, "CONTENT");
|
|
146
|
+
expect(output).toContain("CONTENT");
|
|
147
|
+
|
|
148
|
+
const fileContent = await fs.readFile(targetFile, "utf-8");
|
|
149
|
+
expect(fileContent).toBe("original\n");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("input sources", () => {
|
|
154
|
+
it("reads input from file", async () => {
|
|
155
|
+
const inputFile = path.join(tempDir, "input.txt");
|
|
156
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
157
|
+
await fs.writeFile(inputFile, "FROM FILE");
|
|
158
|
+
await fs.writeFile(targetFile, "original\n");
|
|
159
|
+
|
|
160
|
+
runBlockInFile(`-i ${inputFile} ${targetFile}`);
|
|
161
|
+
|
|
162
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
163
|
+
expect(result).toContain("FROM FILE");
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("append-newline option", () => {
|
|
168
|
+
it("inserts block without blank line after (default)", async () => {
|
|
169
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
170
|
+
await fs.writeFile(targetFile, "line1\nline2\n");
|
|
171
|
+
|
|
172
|
+
runBlockInFile(`${targetFile}`, "CONTENT");
|
|
173
|
+
|
|
174
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
175
|
+
const lines = result.split("\n");
|
|
176
|
+
const endIdx = lines.indexOf("# blockinfile end");
|
|
177
|
+
expect(lines[endIdx + 1]).toBe("");
|
|
178
|
+
expect(lines[endIdx + 2]).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("inserts block with blank line after when --append-newline is set", async () => {
|
|
182
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
183
|
+
await fs.writeFile(targetFile, "line1\nline2\n");
|
|
184
|
+
|
|
185
|
+
runBlockInFile(`--append-newline ${targetFile}`, "CONTENT");
|
|
186
|
+
|
|
187
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
188
|
+
const lines = result.split("\n");
|
|
189
|
+
const endIdx = lines.indexOf("# blockinfile end");
|
|
190
|
+
expect(lines[endIdx + 1]).toBe("");
|
|
191
|
+
expect(lines[endIdx + 2]).toBe("");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("replaces existing block with blank line after when --append-newline is set", async () => {
|
|
195
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
196
|
+
await fs.writeFile(targetFile, "line1\n# blockinfile start\nOLD\n# blockinfile end\nline2\n");
|
|
197
|
+
|
|
198
|
+
runBlockInFile(`--append-newline ${targetFile}`, "NEW");
|
|
199
|
+
|
|
200
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
201
|
+
const lines = result.split("\n");
|
|
202
|
+
const endIdx = lines.indexOf("# blockinfile end");
|
|
203
|
+
expect(lines[endIdx + 1]).toBe("");
|
|
204
|
+
expect(lines[endIdx + 2]).toBe("line2");
|
|
205
|
+
expect(result).toContain("NEW");
|
|
206
|
+
expect(result).not.toContain("OLD");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("inserts before pattern with blank line after", async () => {
|
|
210
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
211
|
+
await fs.writeFile(targetFile, "line1\nMARKER\nline3\n");
|
|
212
|
+
|
|
213
|
+
runBlockInFile(`--append-newline -b MARKER ${targetFile}`, "INSERTED");
|
|
214
|
+
|
|
215
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
216
|
+
const lines = result.split("\n");
|
|
217
|
+
const endIdx = lines.indexOf("# blockinfile end");
|
|
218
|
+
expect(lines[endIdx + 1]).toBe("");
|
|
219
|
+
expect(lines[endIdx + 2]).toBe("MARKER");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("inserts after pattern with blank line after", async () => {
|
|
223
|
+
const targetFile = path.join(tempDir, "target.txt");
|
|
224
|
+
await fs.writeFile(targetFile, "line1\nMARKER\nline3\n");
|
|
225
|
+
|
|
226
|
+
runBlockInFile(`--append-newline -a MARKER ${targetFile}`, "INSERTED");
|
|
227
|
+
|
|
228
|
+
const result = await fs.readFile(targetFile, "utf-8");
|
|
229
|
+
const lines = result.split("\n");
|
|
230
|
+
const endIdx = lines.indexOf("# blockinfile end");
|
|
231
|
+
expect(lines[endIdx + 1]).toBe("");
|
|
232
|
+
expect(lines[endIdx + 2]).toBe("line3");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
|
|
3
|
+
interface ParseOptions {
|
|
4
|
+
opener: string;
|
|
5
|
+
closer: string;
|
|
6
|
+
inputBlock: string;
|
|
7
|
+
before?: RegExp | boolean;
|
|
8
|
+
after?: RegExp | boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ParseResult {
|
|
12
|
+
outputs: string[];
|
|
13
|
+
matched: number;
|
|
14
|
+
opened?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseLines(lines: string[], opts: ParseOptions): ParseResult {
|
|
18
|
+
const { opener, closer, inputBlock, before, after } = opts;
|
|
19
|
+
const match = before || after;
|
|
20
|
+
const outputs: string[] = [];
|
|
21
|
+
|
|
22
|
+
let done = false;
|
|
23
|
+
let opened: number | undefined;
|
|
24
|
+
let matched = -1;
|
|
25
|
+
let i = -1;
|
|
26
|
+
|
|
27
|
+
if (before === true) {
|
|
28
|
+
outputs.push(opener, inputBlock, closer);
|
|
29
|
+
done = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const isOpen = opened !== undefined;
|
|
34
|
+
i++;
|
|
35
|
+
|
|
36
|
+
if (!isOpen && line === opener) {
|
|
37
|
+
opened = outputs.length;
|
|
38
|
+
} else if (isOpen) {
|
|
39
|
+
if (line !== closer) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
opened = undefined;
|
|
44
|
+
|
|
45
|
+
if (done) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
outputs.push(opener, inputBlock, closer);
|
|
50
|
+
done = true;
|
|
51
|
+
} else {
|
|
52
|
+
outputs.push(line);
|
|
53
|
+
|
|
54
|
+
if (!done && matched === -1 && typeof match === "object" && match?.test?.(line)) {
|
|
55
|
+
matched = i;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (opened !== undefined) {
|
|
61
|
+
outputs.push(opener, inputBlock, closer);
|
|
62
|
+
done = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!done) {
|
|
66
|
+
if (matched === -1) {
|
|
67
|
+
matched = i;
|
|
68
|
+
}
|
|
69
|
+
outputs.splice(matched + (after ? 1 : 0), 0, opener, inputBlock, closer);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { outputs, matched, opened };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("block-parser", () => {
|
|
76
|
+
const defaultOpts: ParseOptions = {
|
|
77
|
+
opener: "# blockinfile start",
|
|
78
|
+
closer: "# blockinfile end",
|
|
79
|
+
inputBlock: "NEW CONTENT",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
describe("insertion into empty or simple files", () => {
|
|
83
|
+
it.each([
|
|
84
|
+
{ lines: [], expected: ["# blockinfile start", "NEW CONTENT", "# blockinfile end"] },
|
|
85
|
+
{ lines: ["line1", "line2", "line3"], expectedContains: ["# blockinfile start", "NEW CONTENT", "# blockinfile end", "line1", "line2", "line3"] },
|
|
86
|
+
])("inserts block: lines=$lines", ({ lines, expected, expectedContains }) => {
|
|
87
|
+
const result = parseLines(lines, defaultOpts);
|
|
88
|
+
if (expected) {
|
|
89
|
+
expect(result.outputs).toEqual(expected);
|
|
90
|
+
}
|
|
91
|
+
if (expectedContains) {
|
|
92
|
+
expectedContains.forEach(str => expect(result.outputs).toContain(str));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("replacing existing blocks", () => {
|
|
98
|
+
it.each([
|
|
99
|
+
{
|
|
100
|
+
lines: ["line1", "# blockinfile start", "OLD CONTENT", "# blockinfile end", "line2"],
|
|
101
|
+
opts: undefined as any,
|
|
102
|
+
expected: ["line1", "# blockinfile start", "NEW CONTENT", "# blockinfile end", "line2"],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
lines: ["line1", "line2"],
|
|
106
|
+
opts: { ...defaultOpts, after: true },
|
|
107
|
+
expected: ["line1", "line2", "# blockinfile start", "NEW CONTENT", "# blockinfile end"],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
lines: ["line1", "line2"],
|
|
111
|
+
opts: { ...defaultOpts, before: true },
|
|
112
|
+
expected: ["# blockinfile start", "NEW CONTENT", "# blockinfile end", "line1", "line2"],
|
|
113
|
+
},
|
|
114
|
+
])("handles block replacement", ({ lines, opts, expected }) => {
|
|
115
|
+
const result = parseLines(lines, opts ?? defaultOpts);
|
|
116
|
+
expect(result.outputs).toEqual(expected);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("replaces multi-line existing block", () => {
|
|
120
|
+
const lines = [
|
|
121
|
+
"header",
|
|
122
|
+
"# blockinfile start",
|
|
123
|
+
"old line 1",
|
|
124
|
+
"old line 2",
|
|
125
|
+
"old line 3",
|
|
126
|
+
"# blockinfile end",
|
|
127
|
+
"footer",
|
|
128
|
+
];
|
|
129
|
+
const result = parseLines(lines, defaultOpts);
|
|
130
|
+
expect(result.outputs).toEqual([
|
|
131
|
+
"header",
|
|
132
|
+
"# blockinfile start",
|
|
133
|
+
"NEW CONTENT",
|
|
134
|
+
"# blockinfile end",
|
|
135
|
+
"footer",
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("removes duplicate blocks", () => {
|
|
140
|
+
const lines = [
|
|
141
|
+
"# blockinfile start",
|
|
142
|
+
"first block",
|
|
143
|
+
"# blockinfile end",
|
|
144
|
+
"middle",
|
|
145
|
+
"# blockinfile start",
|
|
146
|
+
"second block",
|
|
147
|
+
"# blockinfile end",
|
|
148
|
+
];
|
|
149
|
+
const result = parseLines(lines, defaultOpts);
|
|
150
|
+
expect(result.outputs).toEqual([
|
|
151
|
+
"# blockinfile start",
|
|
152
|
+
"NEW CONTENT",
|
|
153
|
+
"# blockinfile end",
|
|
154
|
+
"middle",
|
|
155
|
+
]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("before/after matching", () => {
|
|
160
|
+
it.each([
|
|
161
|
+
{
|
|
162
|
+
description: "at beginning when before=true",
|
|
163
|
+
lines: ["line1", "line2"],
|
|
164
|
+
opts: { ...defaultOpts, before: true },
|
|
165
|
+
expected: ["# blockinfile start", "NEW CONTENT", "# blockinfile end", "line1", "line2"],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
description: "before matching line",
|
|
169
|
+
lines: ["line1", "TARGET", "line3"],
|
|
170
|
+
opts: { ...defaultOpts, before: /TARGET/ },
|
|
171
|
+
expected: ["line1", "# blockinfile start", "NEW CONTENT", "# blockinfile end", "TARGET", "line3"],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
description: "after matching line",
|
|
175
|
+
lines: ["line1", "TARGET", "line3"],
|
|
176
|
+
opts: { ...defaultOpts, after: /TARGET/ },
|
|
177
|
+
expected: ["line1", "TARGET", "# blockinfile start", "NEW CONTENT", "# blockinfile end", "line3"],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
description: "falls back to end when no match",
|
|
181
|
+
lines: ["line1", "line2"],
|
|
182
|
+
opts: { ...defaultOpts, after: /NOMATCH/ },
|
|
183
|
+
expected: ["line1", "line2", "# blockinfile start", "NEW CONTENT", "# blockinfile end"],
|
|
184
|
+
},
|
|
185
|
+
])("inserts $description", ({ lines, opts, expected }) => {
|
|
186
|
+
const result = parseLines(lines, opts);
|
|
187
|
+
expect(result.outputs).toEqual(expected);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("matches first occurrence only", () => {
|
|
191
|
+
const result = parseLines(["TARGET", "middle", "TARGET"], {
|
|
192
|
+
...defaultOpts,
|
|
193
|
+
after: /TARGET/,
|
|
194
|
+
});
|
|
195
|
+
expect(result.outputs[0]).toBe("TARGET");
|
|
196
|
+
expect(result.outputs[1]).toBe("# blockinfile start");
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe("unclosed blocks", () => {
|
|
201
|
+
it("handles unclosed block by closing it", () => {
|
|
202
|
+
const lines = ["line1", "# blockinfile start", "unclosed content"];
|
|
203
|
+
const result = parseLines(lines, defaultOpts);
|
|
204
|
+
expect(result.outputs).toContain("# blockinfile start");
|
|
205
|
+
expect(result.outputs).toContain("NEW CONTENT");
|
|
206
|
+
expect(result.outputs).toContain("# blockinfile end");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("custom markers", () => {
|
|
211
|
+
it("uses custom opener and closer", () => {
|
|
212
|
+
const lines = ["line1", "// BEGIN", "old", "// END", "line2"];
|
|
213
|
+
const result = parseLines(lines, {
|
|
214
|
+
opener: "// BEGIN",
|
|
215
|
+
closer: "// END",
|
|
216
|
+
inputBlock: "REPLACED",
|
|
217
|
+
});
|
|
218
|
+
expect(result.outputs).toEqual(["line1", "// BEGIN", "REPLACED", "// END", "line2"]);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { removeBlocks } from "../src/block-remover.ts";
|
|
3
|
+
|
|
4
|
+
describe("block-remover", () => {
|
|
5
|
+
describe("removeBlocks", () => {
|
|
6
|
+
it("removes single block by name", () => {
|
|
7
|
+
const fileContent = "line1\n# blockinfile start\ncontent\n# blockinfile end\nline2\n";
|
|
8
|
+
const { content, stats } = removeBlocks({
|
|
9
|
+
fileContent,
|
|
10
|
+
blockNames: ["blockinfile"],
|
|
11
|
+
comment: "#",
|
|
12
|
+
markerStart: "start",
|
|
13
|
+
markerEnd: "end",
|
|
14
|
+
removeOrphans: false,
|
|
15
|
+
debug: false,
|
|
16
|
+
logger: { debug: () => {}, log: () => {}, warn: () => {} },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(content).toBe("line1\nline2\n");
|
|
20
|
+
expect(stats.removed).toBe(1);
|
|
21
|
+
expect(stats.orphans).toBe(0);
|
|
22
|
+
expect(stats.blocks.length).toBe(1);
|
|
23
|
+
expect(stats.blocks[0].blockName).toBe("blockinfile");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("removes multiple blocks with same name", () => {
|
|
27
|
+
const fileContent =
|
|
28
|
+
"line1\n# blockinfile start\ncontent1\n# blockinfile end\nline2\n# blockinfile start\ncontent2\n# blockinfile end\nline3\n";
|
|
29
|
+
const { content, stats } = removeBlocks({
|
|
30
|
+
fileContent,
|
|
31
|
+
blockNames: ["blockinfile"],
|
|
32
|
+
comment: "#",
|
|
33
|
+
markerStart: "start",
|
|
34
|
+
markerEnd: "end",
|
|
35
|
+
removeOrphans: false,
|
|
36
|
+
debug: false,
|
|
37
|
+
logger: { debug: () => {}, log: () => {}, warn: () => {} },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(content).toBe("line1\nline2\nline3\n");
|
|
41
|
+
expect(stats.removed).toBe(2);
|
|
42
|
+
expect(stats.blocks.length).toBe(2);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("removes orphan blocks when removeOrphans is true", () => {
|
|
46
|
+
const fileContent =
|
|
47
|
+
"line1\n# blockinfile start\n\n# blockinfile end\nline2\n# blockinfile start \n# blockinfile end\nline3\n";
|
|
48
|
+
const { content, stats } = removeBlocks({
|
|
49
|
+
fileContent,
|
|
50
|
+
blockNames: [],
|
|
51
|
+
comment: "#",
|
|
52
|
+
markerStart: "start",
|
|
53
|
+
markerEnd: "end",
|
|
54
|
+
removeOrphans: true,
|
|
55
|
+
debug: false,
|
|
56
|
+
logger: { debug: () => {}, log: () => {}, warn: () => {} },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(content).toBe("line1\nline2\nline3\n");
|
|
60
|
+
expect(stats.removed).toBe(2);
|
|
61
|
+
expect(stats.orphans).toBe(2);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("keeps orphan blocks when removeOrphans is false", () => {
|
|
65
|
+
const fileContent = "line1\n# blockinfile start\n\n# blockinfile end\nline2\n";
|
|
66
|
+
const { content, stats } = removeBlocks({
|
|
67
|
+
fileContent,
|
|
68
|
+
blockNames: ["blockinfile"],
|
|
69
|
+
comment: "#",
|
|
70
|
+
markerStart: "start",
|
|
71
|
+
markerEnd: "end",
|
|
72
|
+
removeOrphans: false,
|
|
73
|
+
debug: false,
|
|
74
|
+
logger: { debug: () => {}, log: () => {}, warn: () => {} },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(content).toBe("line1\nline2\n");
|
|
78
|
+
expect(stats.removed).toBe(1);
|
|
79
|
+
expect(stats.orphans).toBe(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("preserves content outside blocks", () => {
|
|
83
|
+
const fileContent = "before\n# blockinfile start\ncontent\n# blockinfile end\nafter\n";
|
|
84
|
+
const { content, stats } = removeBlocks({
|
|
85
|
+
fileContent,
|
|
86
|
+
blockNames: ["blockinfile"],
|
|
87
|
+
comment: "#",
|
|
88
|
+
markerStart: "start",
|
|
89
|
+
markerEnd: "end",
|
|
90
|
+
removeOrphans: false,
|
|
91
|
+
debug: false,
|
|
92
|
+
logger: { debug: () => {}, log: () => {}, warn: () => {} },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(content).toBe("before\nafter\n");
|
|
96
|
+
expect(stats.removed).toBe(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("handles empty file content", () => {
|
|
100
|
+
const { content, stats } = removeBlocks({
|
|
101
|
+
fileContent: "",
|
|
102
|
+
blockNames: ["blockinfile"],
|
|
103
|
+
comment: "#",
|
|
104
|
+
markerStart: "start",
|
|
105
|
+
markerEnd: "end",
|
|
106
|
+
removeOrphans: false,
|
|
107
|
+
debug: false,
|
|
108
|
+
logger: { debug: () => {}, log: () => {}, warn: () => {} },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
expect(content).toBe("");
|
|
112
|
+
expect(stats.removed).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("handles file with no matching blocks", () => {
|
|
116
|
+
const fileContent = "line1\nline2\nline3\n";
|
|
117
|
+
const { content, stats } = removeBlocks({
|
|
118
|
+
fileContent,
|
|
119
|
+
blockNames: ["blockinfile"],
|
|
120
|
+
comment: "#",
|
|
121
|
+
markerStart: "start",
|
|
122
|
+
markerEnd: "end",
|
|
123
|
+
removeOrphans: false,
|
|
124
|
+
debug: false,
|
|
125
|
+
logger: { debug: () => {}, log: () => {}, warn: () => {} },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(content).toBe(fileContent);
|
|
129
|
+
expect(stats.removed).toBe(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("handles unclosed blocks gracefully", () => {
|
|
133
|
+
const fileContent = "line1\n# blockinfile start\ncontent\n";
|
|
134
|
+
const { content, stats } = removeBlocks({
|
|
135
|
+
fileContent,
|
|
136
|
+
blockNames: ["blockinfile"],
|
|
137
|
+
comment: "#",
|
|
138
|
+
markerStart: "start",
|
|
139
|
+
markerEnd: "end",
|
|
140
|
+
removeOrphans: false,
|
|
141
|
+
debug: true,
|
|
142
|
+
logger: {
|
|
143
|
+
debug: () => {},
|
|
144
|
+
log: () => {},
|
|
145
|
+
warn: (msg: string) => expect(msg).toContain("Unclosed block"),
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(content).toContain("content");
|
|
150
|
+
expect(stats.removed).toBe(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("correctly tracks line numbers", () => {
|
|
154
|
+
const fileContent = "line1\nline2\n# blockinfile start\ncontent\n# blockinfile end\nline5\n";
|
|
155
|
+
const { stats } = removeBlocks({
|
|
156
|
+
fileContent,
|
|
157
|
+
blockNames: ["blockinfile"],
|
|
158
|
+
comment: "#",
|
|
159
|
+
markerStart: "start",
|
|
160
|
+
markerEnd: "end",
|
|
161
|
+
removeOrphans: false,
|
|
162
|
+
debug: false,
|
|
163
|
+
logger: { debug: () => {}, log: () => {}, warn: () => {} },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
expect(stats.blocks[0].startLine).toBe(3);
|
|
167
|
+
expect(stats.blocks[0].endLine).toBe(5);
|
|
168
|
+
expect(stats.blocks[0].content).toBe("content");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("handles blocks with multi-line content", () => {
|
|
172
|
+
const fileContent =
|
|
173
|
+
"line1\n# blockinfile start\nlineA\nlineB\nlineC\n# blockinfile end\nline2\n";
|
|
174
|
+
const { content, stats } = removeBlocks({
|
|
175
|
+
fileContent,
|
|
176
|
+
blockNames: ["blockinfile"],
|
|
177
|
+
comment: "#",
|
|
178
|
+
markerStart: "start",
|
|
179
|
+
markerEnd: "end",
|
|
180
|
+
removeOrphans: false,
|
|
181
|
+
debug: false,
|
|
182
|
+
logger: { debug: () => {}, log: () => {}, warn: () => {} },
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(content).toBe("line1\nline2\n");
|
|
186
|
+
expect(stats.removed).toBe(1);
|
|
187
|
+
expect(stats.blocks[0].content).toBe("lineA\nlineB\nlineC");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("handles mixed orphan and named blocks", () => {
|
|
191
|
+
const fileContent =
|
|
192
|
+
"line1\n# blockinfile start\n\n# blockinfile end\nline2\n# other start\ncontent\n# other end\nline3\n";
|
|
193
|
+
const { content, stats } = removeBlocks({
|
|
194
|
+
fileContent,
|
|
195
|
+
blockNames: ["other"],
|
|
196
|
+
comment: "#",
|
|
197
|
+
markerStart: "start",
|
|
198
|
+
markerEnd: "end",
|
|
199
|
+
removeOrphans: true,
|
|
200
|
+
debug: false,
|
|
201
|
+
logger: { debug: () => {}, log: () => {}, warn: () => {} },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(content).toBe("line1\nline2\nline3\n");
|
|
205
|
+
expect(stats.removed).toBe(2);
|
|
206
|
+
expect(stats.orphans).toBe(1);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
});
|