@org-press/lint 0.9.12
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/LICENSE +29 -0
- package/dist/index.js +344 -0
- package/package.json +51 -0
- package/src/command.ts +166 -0
- package/src/index.ts +33 -0
- package/src/plugin.ts +30 -0
- package/src/types.ts +80 -0
- package/src/utils.ts +319 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
GNU GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 2, June 1991
|
|
3
|
+
|
|
4
|
+
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
|
5
|
+
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
6
|
+
|
|
7
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
8
|
+
of this license document, but changing it is not allowed.
|
|
9
|
+
|
|
10
|
+
For the full license text, see: https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Org-Press - Static site generator for org-mode files
|
|
15
|
+
Copyright (C) 2024-2026
|
|
16
|
+
|
|
17
|
+
This program is free software; you can redistribute it and/or modify
|
|
18
|
+
it under the terms of the GNU General Public License as published by
|
|
19
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
20
|
+
(at your option) any later version.
|
|
21
|
+
|
|
22
|
+
This program is distributed in the hope that it will be useful,
|
|
23
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
24
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
25
|
+
GNU General Public License for more details.
|
|
26
|
+
|
|
27
|
+
You should have received a copy of the GNU General Public License
|
|
28
|
+
along with this program; if not, write to the Free Software
|
|
29
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// src/plugin.ts
|
|
2
|
+
import { CreateCommand } from "org-press";
|
|
3
|
+
|
|
4
|
+
// src/utils.ts
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { parse } from "uniorg-parse/lib/parser.js";
|
|
8
|
+
function findOrgFiles(dir) {
|
|
9
|
+
const files = [];
|
|
10
|
+
if (!fs.existsSync(dir)) {
|
|
11
|
+
return files;
|
|
12
|
+
}
|
|
13
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const fullPath = path.join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) {
|
|
17
|
+
if (entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
18
|
+
files.push(...findOrgFiles(fullPath));
|
|
19
|
+
}
|
|
20
|
+
} else if (entry.name.endsWith(".org")) {
|
|
21
|
+
files.push(fullPath);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return files;
|
|
25
|
+
}
|
|
26
|
+
function extractBlocksFromFile(orgFilePath, projectRoot, options) {
|
|
27
|
+
const absolutePath = path.isAbsolute(orgFilePath) ? orgFilePath : path.join(projectRoot, orgFilePath);
|
|
28
|
+
const relativePath = path.relative(projectRoot, absolutePath);
|
|
29
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
30
|
+
const lines = content.split("\n");
|
|
31
|
+
const ast = parse(content);
|
|
32
|
+
const blocks = [];
|
|
33
|
+
let blockIndex = 0;
|
|
34
|
+
const blockPositions = [];
|
|
35
|
+
for (let i = 0; i < lines.length; i++) {
|
|
36
|
+
const line = lines[i];
|
|
37
|
+
const beginMatch = line.match(/^\s*#\+begin_src\s+(\w+)(.*)$/i);
|
|
38
|
+
if (beginMatch) {
|
|
39
|
+
const language = beginMatch[1].toLowerCase();
|
|
40
|
+
let name;
|
|
41
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
42
|
+
const prevLine = lines[j].trim();
|
|
43
|
+
const nameMatch = prevLine.match(/^#\+name:\s*(.+)$/i);
|
|
44
|
+
if (nameMatch) {
|
|
45
|
+
name = nameMatch[1].trim();
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
if (prevLine && !prevLine.startsWith("#")) break;
|
|
49
|
+
}
|
|
50
|
+
let endLine = i;
|
|
51
|
+
for (let k = i + 1; k < lines.length; k++) {
|
|
52
|
+
if (lines[k].match(/^\s*#\+end_src\s*$/i)) {
|
|
53
|
+
endLine = k;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
blockPositions.push({
|
|
58
|
+
startLine: i + 1,
|
|
59
|
+
// 1-based
|
|
60
|
+
endLine: endLine + 1,
|
|
61
|
+
// 1-based
|
|
62
|
+
name,
|
|
63
|
+
language
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function walk(node) {
|
|
68
|
+
if (!node || typeof node !== "object") return;
|
|
69
|
+
const nodeObj = node;
|
|
70
|
+
if (nodeObj.type === "src-block") {
|
|
71
|
+
const position = blockPositions[blockIndex];
|
|
72
|
+
if (!position) {
|
|
73
|
+
blockIndex++;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const language = nodeObj.language?.toLowerCase() || "";
|
|
77
|
+
if (options?.languages && options.languages.length > 0 && !options.languages.includes(language)) {
|
|
78
|
+
blockIndex++;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
blocks.push({
|
|
82
|
+
orgFilePath: relativePath,
|
|
83
|
+
blockIndex,
|
|
84
|
+
blockName: position.name,
|
|
85
|
+
code: nodeObj.value || "",
|
|
86
|
+
language,
|
|
87
|
+
startLine: position.startLine,
|
|
88
|
+
endLine: position.endLine
|
|
89
|
+
});
|
|
90
|
+
blockIndex++;
|
|
91
|
+
}
|
|
92
|
+
if (nodeObj.children && Array.isArray(nodeObj.children)) {
|
|
93
|
+
for (const child of nodeObj.children) {
|
|
94
|
+
walk(child);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
walk(ast);
|
|
99
|
+
return blocks;
|
|
100
|
+
}
|
|
101
|
+
async function collectCodeBlocks(contentDir, projectRoot = process.cwd(), options) {
|
|
102
|
+
const absoluteContentDir = path.isAbsolute(contentDir) ? contentDir : path.join(projectRoot, contentDir);
|
|
103
|
+
let orgFiles = findOrgFiles(absoluteContentDir);
|
|
104
|
+
if (options?.files && options.files.length > 0) {
|
|
105
|
+
const patterns = options.files.map(
|
|
106
|
+
(f) => path.isAbsolute(f) ? f : path.join(projectRoot, f)
|
|
107
|
+
);
|
|
108
|
+
orgFiles = orgFiles.filter(
|
|
109
|
+
(file) => patterns.some((pattern) => {
|
|
110
|
+
if (pattern.endsWith(".org")) {
|
|
111
|
+
return file === pattern || file.endsWith(pattern);
|
|
112
|
+
}
|
|
113
|
+
return file.includes(pattern);
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const allBlocks = [];
|
|
118
|
+
for (const orgFile of orgFiles) {
|
|
119
|
+
try {
|
|
120
|
+
const blocks = extractBlocksFromFile(orgFile, projectRoot, options);
|
|
121
|
+
allBlocks.push(...blocks);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.warn(
|
|
124
|
+
`[lint] Warning: Failed to parse ${orgFile}: ${error instanceof Error ? error.message : String(error)}`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return allBlocks;
|
|
129
|
+
}
|
|
130
|
+
function writeBlockContentBatch(updates, projectRoot = process.cwd()) {
|
|
131
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
132
|
+
for (const update of updates) {
|
|
133
|
+
const filePath = update.block.orgFilePath;
|
|
134
|
+
if (!byFile.has(filePath)) {
|
|
135
|
+
byFile.set(filePath, []);
|
|
136
|
+
}
|
|
137
|
+
byFile.get(filePath).push(update);
|
|
138
|
+
}
|
|
139
|
+
for (const [filePath, fileUpdates] of byFile) {
|
|
140
|
+
fileUpdates.sort((a, b) => b.block.blockIndex - a.block.blockIndex);
|
|
141
|
+
const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectRoot, filePath);
|
|
142
|
+
let fileContent = fs.readFileSync(absolutePath, "utf-8");
|
|
143
|
+
let lines = fileContent.split("\n");
|
|
144
|
+
for (const { block, newContent } of fileUpdates) {
|
|
145
|
+
const beginLineIndex = block.startLine - 1;
|
|
146
|
+
const endLineIndex = block.endLine - 1;
|
|
147
|
+
if (beginLineIndex < 0 || endLineIndex >= lines.length || beginLineIndex >= endLineIndex) {
|
|
148
|
+
console.warn(
|
|
149
|
+
`[lint] Warning: Invalid block position in ${filePath}: lines ${block.startLine}-${block.endLine}`
|
|
150
|
+
);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const beginLine = lines[beginLineIndex];
|
|
154
|
+
const endLine = lines[endLineIndex];
|
|
155
|
+
if (!beginLine.match(/^\s*#\+begin_src\s+\w+/i)) {
|
|
156
|
+
console.warn(
|
|
157
|
+
`[lint] Warning: Expected #+begin_src at line ${block.startLine} in ${filePath}`
|
|
158
|
+
);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (!endLine.match(/^\s*#\+end_src\s*$/i)) {
|
|
162
|
+
console.warn(
|
|
163
|
+
`[lint] Warning: Expected #+end_src at line ${block.endLine} in ${filePath}`
|
|
164
|
+
);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const trimmedContent = newContent.replace(/\n$/, "");
|
|
168
|
+
const beforeBlock = lines.slice(0, beginLineIndex + 1);
|
|
169
|
+
const afterBlock = lines.slice(endLineIndex);
|
|
170
|
+
lines = [...beforeBlock, trimmedContent, ...afterBlock];
|
|
171
|
+
}
|
|
172
|
+
fs.writeFileSync(absolutePath, lines.join("\n"), "utf-8");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function findEslintConfig(projectRoot) {
|
|
176
|
+
const configFiles = [
|
|
177
|
+
"eslint.config.js",
|
|
178
|
+
"eslint.config.mjs",
|
|
179
|
+
"eslint.config.cjs",
|
|
180
|
+
// Legacy configs
|
|
181
|
+
".eslintrc.js",
|
|
182
|
+
".eslintrc.cjs",
|
|
183
|
+
".eslintrc.json",
|
|
184
|
+
".eslintrc"
|
|
185
|
+
];
|
|
186
|
+
for (const configFile of configFiles) {
|
|
187
|
+
const configPath = path.join(projectRoot, configFile);
|
|
188
|
+
if (fs.existsSync(configPath)) {
|
|
189
|
+
return configPath;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/types.ts
|
|
196
|
+
var LANGUAGE_EXTENSIONS = {
|
|
197
|
+
typescript: "ts",
|
|
198
|
+
ts: "ts",
|
|
199
|
+
tsx: "tsx",
|
|
200
|
+
javascript: "js",
|
|
201
|
+
js: "js",
|
|
202
|
+
jsx: "jsx",
|
|
203
|
+
json: "json",
|
|
204
|
+
css: "css",
|
|
205
|
+
scss: "scss",
|
|
206
|
+
less: "less",
|
|
207
|
+
html: "html",
|
|
208
|
+
yaml: "yaml",
|
|
209
|
+
yml: "yml",
|
|
210
|
+
markdown: "md",
|
|
211
|
+
md: "md",
|
|
212
|
+
graphql: "graphql",
|
|
213
|
+
gql: "graphql"
|
|
214
|
+
};
|
|
215
|
+
var LINT_LANGUAGES = [
|
|
216
|
+
"typescript",
|
|
217
|
+
"ts",
|
|
218
|
+
"tsx",
|
|
219
|
+
"javascript",
|
|
220
|
+
"js",
|
|
221
|
+
"jsx"
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
// src/command.ts
|
|
225
|
+
function parseLintArgs(args) {
|
|
226
|
+
const options = {
|
|
227
|
+
fix: false,
|
|
228
|
+
languages: [],
|
|
229
|
+
files: []
|
|
230
|
+
};
|
|
231
|
+
for (let i = 0; i < args.length; i++) {
|
|
232
|
+
const arg = args[i];
|
|
233
|
+
if (arg === "--fix" || arg === "-f") {
|
|
234
|
+
options.fix = true;
|
|
235
|
+
} else if (arg === "--languages" || arg === "-l") {
|
|
236
|
+
const next = args[++i];
|
|
237
|
+
if (next) {
|
|
238
|
+
options.languages = next.split(",").map((l) => l.trim().toLowerCase());
|
|
239
|
+
}
|
|
240
|
+
} else if (!arg.startsWith("-")) {
|
|
241
|
+
options.files.push(arg);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return options;
|
|
245
|
+
}
|
|
246
|
+
function getExtension(language) {
|
|
247
|
+
return LANGUAGE_EXTENSIONS[language.toLowerCase()] || "js";
|
|
248
|
+
}
|
|
249
|
+
async function runLint(args, ctx) {
|
|
250
|
+
const options = parseLintArgs(args);
|
|
251
|
+
let ESLint;
|
|
252
|
+
try {
|
|
253
|
+
const eslintModule = await import("eslint");
|
|
254
|
+
ESLint = eslintModule.ESLint;
|
|
255
|
+
} catch {
|
|
256
|
+
console.error("[lint] ESLint is not installed. Please install eslint:");
|
|
257
|
+
console.error(" npm install -D eslint");
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
260
|
+
const configPath = findEslintConfig(ctx.projectRoot);
|
|
261
|
+
if (!configPath) {
|
|
262
|
+
console.warn("[lint] No ESLint configuration found. Using default settings.");
|
|
263
|
+
}
|
|
264
|
+
const languages = options.languages?.length ? options.languages : LINT_LANGUAGES;
|
|
265
|
+
const blocks = await collectCodeBlocks(ctx.contentDir, ctx.projectRoot, {
|
|
266
|
+
files: options.files?.length ? options.files : void 0,
|
|
267
|
+
languages
|
|
268
|
+
});
|
|
269
|
+
if (blocks.length === 0) {
|
|
270
|
+
console.log("[lint] No code blocks found to lint");
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
const eslint = new ESLint({
|
|
274
|
+
cwd: ctx.projectRoot,
|
|
275
|
+
fix: options.fix
|
|
276
|
+
});
|
|
277
|
+
let errorCount = 0;
|
|
278
|
+
let warningCount = 0;
|
|
279
|
+
const updates = [];
|
|
280
|
+
for (const block of blocks) {
|
|
281
|
+
const ext = getExtension(block.language);
|
|
282
|
+
const virtualPath = block.blockName ? `${block.orgFilePath}#${block.blockName}.${ext}` : `${block.orgFilePath}#block-${block.blockIndex}.${ext}`;
|
|
283
|
+
try {
|
|
284
|
+
const results = await eslint.lintText(block.code, {
|
|
285
|
+
filePath: virtualPath
|
|
286
|
+
});
|
|
287
|
+
for (const result of results) {
|
|
288
|
+
for (const msg of result.messages) {
|
|
289
|
+
const orgLine = block.startLine + msg.line;
|
|
290
|
+
const severity = msg.severity === 2 ? "error" : "warning";
|
|
291
|
+
const ruleId = msg.ruleId ? ` (${msg.ruleId})` : "";
|
|
292
|
+
console.log(
|
|
293
|
+
`${block.orgFilePath}:${orgLine}:${msg.column} ${severity}: ${msg.message}${ruleId}`
|
|
294
|
+
);
|
|
295
|
+
if (msg.severity === 2) {
|
|
296
|
+
errorCount++;
|
|
297
|
+
} else {
|
|
298
|
+
warningCount++;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (options.fix && result.output && result.output !== block.code) {
|
|
302
|
+
updates.push({ block, newContent: result.output.replace(/\n$/, "") });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
const location = block.blockName ? `${block.orgFilePath}:${block.startLine} (${block.blockName})` : `${block.orgFilePath}:${block.startLine}`;
|
|
307
|
+
console.warn(
|
|
308
|
+
`[lint] Warning: Failed to lint ${location}: ${error instanceof Error ? error.message : String(error)}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (updates.length > 0) {
|
|
313
|
+
writeBlockContentBatch(updates, ctx.projectRoot);
|
|
314
|
+
console.log(`[lint] Fixed ${updates.length} block(s)`);
|
|
315
|
+
}
|
|
316
|
+
if (errorCount > 0 || warningCount > 0) {
|
|
317
|
+
console.log(
|
|
318
|
+
`[lint] Found ${errorCount} error(s) and ${warningCount} warning(s) in ${blocks.length} block(s)`
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
console.log(`[lint] No issues found in ${blocks.length} block(s)`);
|
|
322
|
+
}
|
|
323
|
+
return errorCount > 0 ? 1 : 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/plugin.ts
|
|
327
|
+
var lintPlugin = CreateCommand("lint", {
|
|
328
|
+
description: "Lint code blocks in org files using ESLint",
|
|
329
|
+
args: [
|
|
330
|
+
{ name: "fix", type: "boolean", description: "Auto-fix problems" },
|
|
331
|
+
{ name: "languages", type: "string", description: "Comma-separated list of languages" }
|
|
332
|
+
],
|
|
333
|
+
execute: (args, ctx) => runLint(args._, ctx)
|
|
334
|
+
});
|
|
335
|
+
export {
|
|
336
|
+
LANGUAGE_EXTENSIONS,
|
|
337
|
+
LINT_LANGUAGES,
|
|
338
|
+
collectCodeBlocks,
|
|
339
|
+
findEslintConfig,
|
|
340
|
+
lintPlugin,
|
|
341
|
+
parseLintArgs,
|
|
342
|
+
runLint,
|
|
343
|
+
writeBlockContentBatch
|
|
344
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@org-press/lint",
|
|
3
|
+
"version": "0.9.12",
|
|
4
|
+
"description": "Lint code blocks in org files using ESLint",
|
|
5
|
+
"license": "GPL-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"org-press": ">=0.9.0"
|
|
22
|
+
},
|
|
23
|
+
"optionalDependencies": {
|
|
24
|
+
"eslint": "^8.0.0 || ^9.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"tsup": "^8.0.0",
|
|
28
|
+
"typescript": "~5.7.3",
|
|
29
|
+
"uniorg": "^1.3.0",
|
|
30
|
+
"uniorg-parse": "^3.2.0",
|
|
31
|
+
"eslint": "^9.0.0",
|
|
32
|
+
"@types/node": "^20.0.0",
|
|
33
|
+
"org-press": "0.9.13"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://orgp.dev",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "https://github.com/org-press/org-press.git"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/org-press/org-press/issues"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsup",
|
|
48
|
+
"dev": "tsup --watch",
|
|
49
|
+
"clean": "rm -rf dist"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lint Command Implementation
|
|
3
|
+
*
|
|
4
|
+
* Lints code blocks in org files using ESLint.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CommandContext } from "org-press";
|
|
8
|
+
import { collectCodeBlocks, writeBlockContentBatch, findEslintConfig } from "./utils.js";
|
|
9
|
+
import { LINT_LANGUAGES, LANGUAGE_EXTENSIONS, type LintOptions } from "./types.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse command line arguments for lint command
|
|
13
|
+
*/
|
|
14
|
+
export function parseLintArgs(args: string[]): LintOptions {
|
|
15
|
+
const options: LintOptions = {
|
|
16
|
+
fix: false,
|
|
17
|
+
languages: [],
|
|
18
|
+
files: [],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < args.length; i++) {
|
|
22
|
+
const arg = args[i];
|
|
23
|
+
|
|
24
|
+
if (arg === "--fix" || arg === "-f") {
|
|
25
|
+
options.fix = true;
|
|
26
|
+
} else if (arg === "--languages" || arg === "-l") {
|
|
27
|
+
const next = args[++i];
|
|
28
|
+
if (next) {
|
|
29
|
+
options.languages = next.split(",").map((l) => l.trim().toLowerCase());
|
|
30
|
+
}
|
|
31
|
+
} else if (!arg.startsWith("-")) {
|
|
32
|
+
// Positional argument - file pattern
|
|
33
|
+
options.files!.push(arg);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return options;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get file extension for a language
|
|
42
|
+
*/
|
|
43
|
+
function getExtension(language: string): string {
|
|
44
|
+
return LANGUAGE_EXTENSIONS[language.toLowerCase()] || "js";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run the lint command
|
|
49
|
+
*
|
|
50
|
+
* @param args - Command line arguments
|
|
51
|
+
* @param ctx - CLI context
|
|
52
|
+
* @returns Exit code (0 for success, 1 if errors found)
|
|
53
|
+
*/
|
|
54
|
+
export async function runLint(
|
|
55
|
+
args: string[],
|
|
56
|
+
ctx: CommandContext
|
|
57
|
+
): Promise<number> {
|
|
58
|
+
const options = parseLintArgs(args);
|
|
59
|
+
|
|
60
|
+
// Check if ESLint is available
|
|
61
|
+
let ESLint: typeof import("eslint").ESLint;
|
|
62
|
+
try {
|
|
63
|
+
const eslintModule = await import("eslint");
|
|
64
|
+
ESLint = eslintModule.ESLint;
|
|
65
|
+
} catch {
|
|
66
|
+
console.error("[lint] ESLint is not installed. Please install eslint:");
|
|
67
|
+
console.error(" npm install -D eslint");
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check for ESLint config
|
|
72
|
+
const configPath = findEslintConfig(ctx.projectRoot);
|
|
73
|
+
if (!configPath) {
|
|
74
|
+
console.warn("[lint] No ESLint configuration found. Using default settings.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Determine which languages to lint
|
|
78
|
+
const languages =
|
|
79
|
+
options.languages?.length ? options.languages : LINT_LANGUAGES;
|
|
80
|
+
|
|
81
|
+
// Collect blocks
|
|
82
|
+
const blocks = await collectCodeBlocks(ctx.contentDir, ctx.projectRoot, {
|
|
83
|
+
files: options.files?.length ? options.files : undefined,
|
|
84
|
+
languages,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (blocks.length === 0) {
|
|
88
|
+
console.log("[lint] No code blocks found to lint");
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Create ESLint instance
|
|
93
|
+
const eslint = new ESLint({
|
|
94
|
+
cwd: ctx.projectRoot,
|
|
95
|
+
fix: options.fix,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
let errorCount = 0;
|
|
99
|
+
let warningCount = 0;
|
|
100
|
+
const updates: Array<{ block: (typeof blocks)[0]; newContent: string }> = [];
|
|
101
|
+
|
|
102
|
+
for (const block of blocks) {
|
|
103
|
+
const ext = getExtension(block.language);
|
|
104
|
+
// Use virtual filename for ESLint to apply correct rules
|
|
105
|
+
const virtualPath = block.blockName
|
|
106
|
+
? `${block.orgFilePath}#${block.blockName}.${ext}`
|
|
107
|
+
: `${block.orgFilePath}#block-${block.blockIndex}.${ext}`;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const results = await eslint.lintText(block.code, {
|
|
111
|
+
filePath: virtualPath,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
for (const result of results) {
|
|
115
|
+
// Print messages
|
|
116
|
+
for (const msg of result.messages) {
|
|
117
|
+
// Map line number back to org file
|
|
118
|
+
const orgLine = block.startLine + msg.line;
|
|
119
|
+
const severity = msg.severity === 2 ? "error" : "warning";
|
|
120
|
+
const ruleId = msg.ruleId ? ` (${msg.ruleId})` : "";
|
|
121
|
+
|
|
122
|
+
console.log(
|
|
123
|
+
`${block.orgFilePath}:${orgLine}:${msg.column} ${severity}: ${msg.message}${ruleId}`
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (msg.severity === 2) {
|
|
127
|
+
errorCount++;
|
|
128
|
+
} else {
|
|
129
|
+
warningCount++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Collect fixes if --fix and there's output
|
|
134
|
+
if (options.fix && result.output && result.output !== block.code) {
|
|
135
|
+
updates.push({ block, newContent: result.output.replace(/\n$/, "") });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const location = block.blockName
|
|
140
|
+
? `${block.orgFilePath}:${block.startLine} (${block.blockName})`
|
|
141
|
+
: `${block.orgFilePath}:${block.startLine}`;
|
|
142
|
+
console.warn(
|
|
143
|
+
`[lint] Warning: Failed to lint ${location}: ${
|
|
144
|
+
error instanceof Error ? error.message : String(error)
|
|
145
|
+
}`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Write fixes
|
|
151
|
+
if (updates.length > 0) {
|
|
152
|
+
writeBlockContentBatch(updates, ctx.projectRoot);
|
|
153
|
+
console.log(`[lint] Fixed ${updates.length} block(s)`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Summary
|
|
157
|
+
if (errorCount > 0 || warningCount > 0) {
|
|
158
|
+
console.log(
|
|
159
|
+
`[lint] Found ${errorCount} error(s) and ${warningCount} warning(s) in ${blocks.length} block(s)`
|
|
160
|
+
);
|
|
161
|
+
} else {
|
|
162
|
+
console.log(`[lint] No issues found in ${blocks.length} block(s)`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return errorCount > 0 ? 1 : 0;
|
|
166
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @org-press/lint
|
|
3
|
+
*
|
|
4
|
+
* Lint code blocks in org files using ESLint.
|
|
5
|
+
*
|
|
6
|
+
* This package provides the `orgp lint` CLI command plugin.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // .org-press/config.ts
|
|
11
|
+
* import { lintPlugin } from '@org-press/lint';
|
|
12
|
+
*
|
|
13
|
+
* export default {
|
|
14
|
+
* contentDir: 'content',
|
|
15
|
+
* plugins: [lintPlugin],
|
|
16
|
+
* };
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Plugin export
|
|
21
|
+
export { lintPlugin } from "./plugin.js";
|
|
22
|
+
|
|
23
|
+
// Command export (for programmatic use)
|
|
24
|
+
export { runLint, parseLintArgs } from "./command.js";
|
|
25
|
+
|
|
26
|
+
// Type exports
|
|
27
|
+
export type { CollectedBlock, CollectOptions, LintOptions } from "./types.js";
|
|
28
|
+
|
|
29
|
+
// Constant exports
|
|
30
|
+
export { LANGUAGE_EXTENSIONS, LINT_LANGUAGES } from "./types.js";
|
|
31
|
+
|
|
32
|
+
// Utility exports (for advanced use cases)
|
|
33
|
+
export { collectCodeBlocks, writeBlockContentBatch, findEslintConfig } from "./utils.js";
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lint Plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides the `orgp lint` command for linting code blocks with ESLint.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CreateCommand } from "org-press";
|
|
8
|
+
import type { ParsedArgs, CommandContext } from "org-press";
|
|
9
|
+
import { runLint } from "./command.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Lint plugin
|
|
13
|
+
*
|
|
14
|
+
* Registers the `lint` CLI command for linting JavaScript/TypeScript
|
|
15
|
+
* code blocks in org files using ESLint.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* orgp lint # Lint all JS/TS blocks
|
|
19
|
+
* orgp lint --fix # Auto-fix problems
|
|
20
|
+
* orgp lint --languages ts,tsx # Lint only TypeScript blocks
|
|
21
|
+
* orgp lint content/utils.org # Lint specific file
|
|
22
|
+
*/
|
|
23
|
+
export const lintPlugin = CreateCommand("lint", {
|
|
24
|
+
description: "Lint code blocks in org files using ESLint",
|
|
25
|
+
args: [
|
|
26
|
+
{ name: "fix", type: "boolean", description: "Auto-fix problems" },
|
|
27
|
+
{ name: "languages", type: "string", description: "Comma-separated list of languages" },
|
|
28
|
+
],
|
|
29
|
+
execute: (args: ParsedArgs, ctx: CommandContext) => runLint(args._, ctx),
|
|
30
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for @org-press/lint
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A collected code block from an org file
|
|
7
|
+
*/
|
|
8
|
+
export interface CollectedBlock {
|
|
9
|
+
/** Relative path to the org file */
|
|
10
|
+
orgFilePath: string;
|
|
11
|
+
/** 0-based index of the block in the file */
|
|
12
|
+
blockIndex: number;
|
|
13
|
+
/** Block name from #+NAME: directive */
|
|
14
|
+
blockName?: string;
|
|
15
|
+
/** The source code content */
|
|
16
|
+
code: string;
|
|
17
|
+
/** Block language (e.g., "typescript", "javascript") */
|
|
18
|
+
language: string;
|
|
19
|
+
/** 1-based line number where the block starts (#+begin_src line) */
|
|
20
|
+
startLine: number;
|
|
21
|
+
/** 1-based line number where the block ends (#+end_src line) */
|
|
22
|
+
endLine: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for collecting code blocks
|
|
27
|
+
*/
|
|
28
|
+
export interface CollectOptions {
|
|
29
|
+
/** Filter by file path patterns */
|
|
30
|
+
files?: string[];
|
|
31
|
+
/** Filter by languages */
|
|
32
|
+
languages?: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options for the lint command
|
|
37
|
+
*/
|
|
38
|
+
export interface LintOptions {
|
|
39
|
+
/** Auto-fix problems */
|
|
40
|
+
fix?: boolean;
|
|
41
|
+
/** Filter by languages */
|
|
42
|
+
languages?: string[];
|
|
43
|
+
/** Filter by file patterns */
|
|
44
|
+
files?: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Language to file extension mapping
|
|
49
|
+
*/
|
|
50
|
+
export const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
|
51
|
+
typescript: "ts",
|
|
52
|
+
ts: "ts",
|
|
53
|
+
tsx: "tsx",
|
|
54
|
+
javascript: "js",
|
|
55
|
+
js: "js",
|
|
56
|
+
jsx: "jsx",
|
|
57
|
+
json: "json",
|
|
58
|
+
css: "css",
|
|
59
|
+
scss: "scss",
|
|
60
|
+
less: "less",
|
|
61
|
+
html: "html",
|
|
62
|
+
yaml: "yaml",
|
|
63
|
+
yml: "yml",
|
|
64
|
+
markdown: "md",
|
|
65
|
+
md: "md",
|
|
66
|
+
graphql: "graphql",
|
|
67
|
+
gql: "graphql",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Languages that can be linted with ESLint
|
|
72
|
+
*/
|
|
73
|
+
export const LINT_LANGUAGES = [
|
|
74
|
+
"typescript",
|
|
75
|
+
"ts",
|
|
76
|
+
"tsx",
|
|
77
|
+
"javascript",
|
|
78
|
+
"js",
|
|
79
|
+
"jsx",
|
|
80
|
+
];
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities for @org-press/lint
|
|
3
|
+
*
|
|
4
|
+
* Block collection, writing, and config loading functions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import { parse } from "uniorg-parse/lib/parser.js";
|
|
10
|
+
import type { OrgData } from "uniorg";
|
|
11
|
+
import type { CollectedBlock, CollectOptions } from "./types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find all org files recursively in a directory
|
|
15
|
+
*/
|
|
16
|
+
function findOrgFiles(dir: string): string[] {
|
|
17
|
+
const files: string[] = [];
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(dir)) {
|
|
20
|
+
return files;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
24
|
+
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const fullPath = path.join(dir, entry.name);
|
|
27
|
+
|
|
28
|
+
if (entry.isDirectory()) {
|
|
29
|
+
// Skip node_modules and hidden directories
|
|
30
|
+
if (entry.name !== "node_modules" && !entry.name.startsWith(".")) {
|
|
31
|
+
files.push(...findOrgFiles(fullPath));
|
|
32
|
+
}
|
|
33
|
+
} else if (entry.name.endsWith(".org")) {
|
|
34
|
+
files.push(fullPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract code blocks from a single org file
|
|
43
|
+
*/
|
|
44
|
+
function extractBlocksFromFile(
|
|
45
|
+
orgFilePath: string,
|
|
46
|
+
projectRoot: string,
|
|
47
|
+
options?: CollectOptions
|
|
48
|
+
): CollectedBlock[] {
|
|
49
|
+
const absolutePath = path.isAbsolute(orgFilePath)
|
|
50
|
+
? orgFilePath
|
|
51
|
+
: path.join(projectRoot, orgFilePath);
|
|
52
|
+
const relativePath = path.relative(projectRoot, absolutePath);
|
|
53
|
+
const content = fs.readFileSync(absolutePath, "utf-8");
|
|
54
|
+
const lines = content.split("\n");
|
|
55
|
+
const ast = parse(content) as OrgData;
|
|
56
|
+
|
|
57
|
+
const blocks: CollectedBlock[] = [];
|
|
58
|
+
let blockIndex = 0;
|
|
59
|
+
|
|
60
|
+
// Find block positions and names by scanning raw content
|
|
61
|
+
const blockPositions: Array<{
|
|
62
|
+
startLine: number;
|
|
63
|
+
endLine: number;
|
|
64
|
+
name?: string;
|
|
65
|
+
language: string;
|
|
66
|
+
}> = [];
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
const line = lines[i];
|
|
70
|
+
const beginMatch = line.match(/^\s*#\+begin_src\s+(\w+)(.*)$/i);
|
|
71
|
+
|
|
72
|
+
if (beginMatch) {
|
|
73
|
+
const language = beginMatch[1].toLowerCase();
|
|
74
|
+
|
|
75
|
+
// Look backwards for #+NAME: directive
|
|
76
|
+
let name: string | undefined;
|
|
77
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
78
|
+
const prevLine = lines[j].trim();
|
|
79
|
+
const nameMatch = prevLine.match(/^#\+name:\s*(.+)$/i);
|
|
80
|
+
if (nameMatch) {
|
|
81
|
+
name = nameMatch[1].trim();
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
// Stop looking if we hit a non-comment, non-empty line
|
|
85
|
+
if (prevLine && !prevLine.startsWith("#")) break;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Find matching #+end_src
|
|
89
|
+
let endLine = i;
|
|
90
|
+
for (let k = i + 1; k < lines.length; k++) {
|
|
91
|
+
if (lines[k].match(/^\s*#\+end_src\s*$/i)) {
|
|
92
|
+
endLine = k;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
blockPositions.push({
|
|
98
|
+
startLine: i + 1, // 1-based
|
|
99
|
+
endLine: endLine + 1, // 1-based
|
|
100
|
+
name,
|
|
101
|
+
language,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Walk AST and match blocks with their positions
|
|
107
|
+
function walk(node: unknown): void {
|
|
108
|
+
if (!node || typeof node !== "object") return;
|
|
109
|
+
|
|
110
|
+
const nodeObj = node as Record<string, unknown>;
|
|
111
|
+
|
|
112
|
+
if (nodeObj.type === "src-block") {
|
|
113
|
+
const position = blockPositions[blockIndex];
|
|
114
|
+
if (!position) {
|
|
115
|
+
blockIndex++;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const language = (nodeObj.language as string)?.toLowerCase() || "";
|
|
120
|
+
|
|
121
|
+
// Filter by language if specified
|
|
122
|
+
if (
|
|
123
|
+
options?.languages &&
|
|
124
|
+
options.languages.length > 0 &&
|
|
125
|
+
!options.languages.includes(language)
|
|
126
|
+
) {
|
|
127
|
+
blockIndex++;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
blocks.push({
|
|
132
|
+
orgFilePath: relativePath,
|
|
133
|
+
blockIndex,
|
|
134
|
+
blockName: position.name,
|
|
135
|
+
code: (nodeObj.value as string) || "",
|
|
136
|
+
language,
|
|
137
|
+
startLine: position.startLine,
|
|
138
|
+
endLine: position.endLine,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
blockIndex++;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Recurse into children
|
|
145
|
+
if (nodeObj.children && Array.isArray(nodeObj.children)) {
|
|
146
|
+
for (const child of nodeObj.children) {
|
|
147
|
+
walk(child);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
walk(ast);
|
|
153
|
+
return blocks;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Collect all code blocks from a content directory
|
|
158
|
+
*
|
|
159
|
+
* @param contentDir - Content directory to scan
|
|
160
|
+
* @param projectRoot - Project root directory
|
|
161
|
+
* @param options - Collection options
|
|
162
|
+
* @returns Array of collected code blocks
|
|
163
|
+
*/
|
|
164
|
+
export async function collectCodeBlocks(
|
|
165
|
+
contentDir: string,
|
|
166
|
+
projectRoot: string = process.cwd(),
|
|
167
|
+
options?: CollectOptions
|
|
168
|
+
): Promise<CollectedBlock[]> {
|
|
169
|
+
const absoluteContentDir = path.isAbsolute(contentDir)
|
|
170
|
+
? contentDir
|
|
171
|
+
: path.join(projectRoot, contentDir);
|
|
172
|
+
|
|
173
|
+
// Find all org files
|
|
174
|
+
let orgFiles = findOrgFiles(absoluteContentDir);
|
|
175
|
+
|
|
176
|
+
// Filter by file patterns if specified
|
|
177
|
+
if (options?.files && options.files.length > 0) {
|
|
178
|
+
const patterns = options.files.map((f) =>
|
|
179
|
+
path.isAbsolute(f) ? f : path.join(projectRoot, f)
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
orgFiles = orgFiles.filter((file) =>
|
|
183
|
+
patterns.some((pattern) => {
|
|
184
|
+
// Support both exact match and contains match
|
|
185
|
+
if (pattern.endsWith(".org")) {
|
|
186
|
+
return file === pattern || file.endsWith(pattern);
|
|
187
|
+
}
|
|
188
|
+
return file.includes(pattern);
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const allBlocks: CollectedBlock[] = [];
|
|
194
|
+
|
|
195
|
+
// Process each file
|
|
196
|
+
for (const orgFile of orgFiles) {
|
|
197
|
+
try {
|
|
198
|
+
const blocks = extractBlocksFromFile(orgFile, projectRoot, options);
|
|
199
|
+
allBlocks.push(...blocks);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.warn(
|
|
202
|
+
`[lint] Warning: Failed to parse ${orgFile}: ${
|
|
203
|
+
error instanceof Error ? error.message : String(error)
|
|
204
|
+
}`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return allBlocks;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Batch write multiple block updates to minimize file I/O
|
|
214
|
+
*
|
|
215
|
+
* Updates are grouped by file and applied in reverse order
|
|
216
|
+
* (to preserve line numbers for earlier blocks in the same file)
|
|
217
|
+
*
|
|
218
|
+
* @param updates - Array of {block, newContent} pairs
|
|
219
|
+
* @param projectRoot - Project root directory
|
|
220
|
+
*/
|
|
221
|
+
export function writeBlockContentBatch(
|
|
222
|
+
updates: Array<{ block: CollectedBlock; newContent: string }>,
|
|
223
|
+
projectRoot: string = process.cwd()
|
|
224
|
+
): void {
|
|
225
|
+
// Group updates by file
|
|
226
|
+
const byFile = new Map<string, Array<{ block: CollectedBlock; newContent: string }>>();
|
|
227
|
+
|
|
228
|
+
for (const update of updates) {
|
|
229
|
+
const filePath = update.block.orgFilePath;
|
|
230
|
+
if (!byFile.has(filePath)) {
|
|
231
|
+
byFile.set(filePath, []);
|
|
232
|
+
}
|
|
233
|
+
byFile.get(filePath)!.push(update);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Process each file
|
|
237
|
+
for (const [filePath, fileUpdates] of byFile) {
|
|
238
|
+
// Sort by blockIndex in reverse order so we can apply changes
|
|
239
|
+
// from the end of the file backwards (preserving line numbers)
|
|
240
|
+
fileUpdates.sort((a, b) => b.block.blockIndex - a.block.blockIndex);
|
|
241
|
+
|
|
242
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
243
|
+
? filePath
|
|
244
|
+
: path.join(projectRoot, filePath);
|
|
245
|
+
|
|
246
|
+
let fileContent = fs.readFileSync(absolutePath, "utf-8");
|
|
247
|
+
let lines = fileContent.split("\n");
|
|
248
|
+
|
|
249
|
+
for (const { block, newContent } of fileUpdates) {
|
|
250
|
+
const beginLineIndex = block.startLine - 1;
|
|
251
|
+
const endLineIndex = block.endLine - 1;
|
|
252
|
+
|
|
253
|
+
// Validate
|
|
254
|
+
if (
|
|
255
|
+
beginLineIndex < 0 ||
|
|
256
|
+
endLineIndex >= lines.length ||
|
|
257
|
+
beginLineIndex >= endLineIndex
|
|
258
|
+
) {
|
|
259
|
+
console.warn(
|
|
260
|
+
`[lint] Warning: Invalid block position in ${filePath}: lines ${block.startLine}-${block.endLine}`
|
|
261
|
+
);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const beginLine = lines[beginLineIndex];
|
|
266
|
+
const endLine = lines[endLineIndex];
|
|
267
|
+
|
|
268
|
+
if (!beginLine.match(/^\s*#\+begin_src\s+\w+/i)) {
|
|
269
|
+
console.warn(
|
|
270
|
+
`[lint] Warning: Expected #+begin_src at line ${block.startLine} in ${filePath}`
|
|
271
|
+
);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!endLine.match(/^\s*#\+end_src\s*$/i)) {
|
|
276
|
+
console.warn(
|
|
277
|
+
`[lint] Warning: Expected #+end_src at line ${block.endLine} in ${filePath}`
|
|
278
|
+
);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Replace content
|
|
283
|
+
const trimmedContent = newContent.replace(/\n$/, "");
|
|
284
|
+
const beforeBlock = lines.slice(0, beginLineIndex + 1);
|
|
285
|
+
const afterBlock = lines.slice(endLineIndex);
|
|
286
|
+
lines = [...beforeBlock, trimmedContent, ...afterBlock];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
fs.writeFileSync(absolutePath, lines.join("\n"), "utf-8");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check if ESLint flat config exists
|
|
295
|
+
*
|
|
296
|
+
* @param projectRoot - Project root directory
|
|
297
|
+
* @returns Path to ESLint config if found, null otherwise
|
|
298
|
+
*/
|
|
299
|
+
export function findEslintConfig(projectRoot: string): string | null {
|
|
300
|
+
const configFiles = [
|
|
301
|
+
"eslint.config.js",
|
|
302
|
+
"eslint.config.mjs",
|
|
303
|
+
"eslint.config.cjs",
|
|
304
|
+
// Legacy configs
|
|
305
|
+
".eslintrc.js",
|
|
306
|
+
".eslintrc.cjs",
|
|
307
|
+
".eslintrc.json",
|
|
308
|
+
".eslintrc",
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
for (const configFile of configFiles) {
|
|
312
|
+
const configPath = path.join(projectRoot, configFile);
|
|
313
|
+
if (fs.existsSync(configPath)) {
|
|
314
|
+
return configPath;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return null;
|
|
319
|
+
}
|