@savvy-web/lint-staged 0.1.3 → 0.2.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/376.js +1198 -0
- package/README.md +18 -6
- package/bin/savvy-lint.js +3 -0
- package/index.d.ts +81 -87
- package/index.js +39 -978
- package/package.json +22 -3
- package/tsdoc-metadata.json +11 -0
package/index.js
CHANGED
|
@@ -1,377 +1,5 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
4
|
-
import { cosmiconfigSync, defaultLoaders } from "cosmiconfig";
|
|
5
|
-
import sort_package_json from "sort-package-json";
|
|
6
1
|
import { parse, stringify } from "yaml";
|
|
7
|
-
import
|
|
8
|
-
import { ESLint } from "eslint";
|
|
9
|
-
import eslint_plugin_tsdoc from "eslint-plugin-tsdoc";
|
|
10
|
-
import { getWorkspaces } from "workspace-tools";
|
|
11
|
-
import typescript from "typescript";
|
|
12
|
-
const VALID_COMMAND_PATTERN = /^[\w@/-]+$/;
|
|
13
|
-
function validateCommandName(name) {
|
|
14
|
-
if (!VALID_COMMAND_PATTERN.test(name)) throw new Error(`Invalid command name: "${name}". Only alphanumeric characters, hyphens, underscores, @ and / are allowed.`);
|
|
15
|
-
}
|
|
16
|
-
class Command {
|
|
17
|
-
static cachedPackageManager = null;
|
|
18
|
-
static detectPackageManager(cwd = process.cwd()) {
|
|
19
|
-
if (null !== Command.cachedPackageManager) return Command.cachedPackageManager;
|
|
20
|
-
const packageJsonPath = join(cwd, "package.json");
|
|
21
|
-
if (!existsSync(packageJsonPath)) {
|
|
22
|
-
Command.cachedPackageManager = "npm";
|
|
23
|
-
return "npm";
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
const content = readFileSync(packageJsonPath, "utf-8");
|
|
27
|
-
const pkg = JSON.parse(content);
|
|
28
|
-
if (pkg.packageManager) {
|
|
29
|
-
const match = pkg.packageManager.match(/^(npm|pnpm|yarn|bun)@/);
|
|
30
|
-
if (match) {
|
|
31
|
-
Command.cachedPackageManager = match[1];
|
|
32
|
-
return Command.cachedPackageManager;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
} catch {}
|
|
36
|
-
Command.cachedPackageManager = "npm";
|
|
37
|
-
return "npm";
|
|
38
|
-
}
|
|
39
|
-
static getExecPrefix(packageManager) {
|
|
40
|
-
switch(packageManager){
|
|
41
|
-
case "pnpm":
|
|
42
|
-
return [
|
|
43
|
-
"pnpm",
|
|
44
|
-
"exec"
|
|
45
|
-
];
|
|
46
|
-
case "yarn":
|
|
47
|
-
return [
|
|
48
|
-
"yarn",
|
|
49
|
-
"exec"
|
|
50
|
-
];
|
|
51
|
-
case "bun":
|
|
52
|
-
return [
|
|
53
|
-
"bunx"
|
|
54
|
-
];
|
|
55
|
-
default:
|
|
56
|
-
return [
|
|
57
|
-
"npx",
|
|
58
|
-
"--no"
|
|
59
|
-
];
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
static clearCache() {
|
|
63
|
-
Command.cachedPackageManager = null;
|
|
64
|
-
}
|
|
65
|
-
static isAvailable(command) {
|
|
66
|
-
validateCommandName(command);
|
|
67
|
-
try {
|
|
68
|
-
execSync(`command -v ${command}`, {
|
|
69
|
-
stdio: "ignore"
|
|
70
|
-
});
|
|
71
|
-
return true;
|
|
72
|
-
} catch {
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
static findTool(tool) {
|
|
77
|
-
validateCommandName(tool);
|
|
78
|
-
if (Command.isAvailable(tool)) return {
|
|
79
|
-
available: true,
|
|
80
|
-
command: tool,
|
|
81
|
-
source: "global"
|
|
82
|
-
};
|
|
83
|
-
const pm = Command.detectPackageManager();
|
|
84
|
-
const prefix = Command.getExecPrefix(pm);
|
|
85
|
-
const execCmd = [
|
|
86
|
-
...prefix,
|
|
87
|
-
tool
|
|
88
|
-
].join(" ");
|
|
89
|
-
try {
|
|
90
|
-
execSync(`${execCmd} --version`, {
|
|
91
|
-
stdio: "ignore"
|
|
92
|
-
});
|
|
93
|
-
return {
|
|
94
|
-
available: true,
|
|
95
|
-
command: execCmd,
|
|
96
|
-
source: pm
|
|
97
|
-
};
|
|
98
|
-
} catch {}
|
|
99
|
-
return {
|
|
100
|
-
available: false,
|
|
101
|
-
command: void 0,
|
|
102
|
-
source: void 0
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
static requireTool(tool, errorMessage) {
|
|
106
|
-
const result = Command.findTool(tool);
|
|
107
|
-
if (!result.available || !result.command) throw new Error(errorMessage ?? `Required tool '${tool}' is not available. Install it globally or add it as a dev dependency.`);
|
|
108
|
-
return result.command;
|
|
109
|
-
}
|
|
110
|
-
static exec(command) {
|
|
111
|
-
return execSync(command, {
|
|
112
|
-
encoding: "utf-8"
|
|
113
|
-
}).trim();
|
|
114
|
-
}
|
|
115
|
-
static execSilent(command) {
|
|
116
|
-
try {
|
|
117
|
-
execSync(command, {
|
|
118
|
-
stdio: "ignore"
|
|
119
|
-
});
|
|
120
|
-
return true;
|
|
121
|
-
} catch {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
const TOOL_CONFIGS = {
|
|
127
|
-
markdownlint: {
|
|
128
|
-
moduleName: "markdownlint-cli2",
|
|
129
|
-
libConfigFiles: [
|
|
130
|
-
".markdownlint-cli2.jsonc",
|
|
131
|
-
".markdownlint-cli2.json",
|
|
132
|
-
".markdownlint-cli2.yaml",
|
|
133
|
-
".markdownlint-cli2.cjs",
|
|
134
|
-
".markdownlint.jsonc",
|
|
135
|
-
".markdownlint.json",
|
|
136
|
-
".markdownlint.yaml"
|
|
137
|
-
],
|
|
138
|
-
standardPlaces: [
|
|
139
|
-
".markdownlint-cli2.jsonc",
|
|
140
|
-
".markdownlint-cli2.json",
|
|
141
|
-
".markdownlint-cli2.yaml",
|
|
142
|
-
".markdownlint-cli2.cjs",
|
|
143
|
-
".markdownlint.jsonc",
|
|
144
|
-
".markdownlint.json",
|
|
145
|
-
".markdownlint.yaml"
|
|
146
|
-
]
|
|
147
|
-
},
|
|
148
|
-
biome: {
|
|
149
|
-
moduleName: "biome",
|
|
150
|
-
libConfigFiles: [
|
|
151
|
-
"biome.jsonc",
|
|
152
|
-
"biome.json"
|
|
153
|
-
],
|
|
154
|
-
standardPlaces: [
|
|
155
|
-
"biome.jsonc",
|
|
156
|
-
"biome.json"
|
|
157
|
-
]
|
|
158
|
-
},
|
|
159
|
-
eslint: {
|
|
160
|
-
moduleName: "eslint",
|
|
161
|
-
libConfigFiles: [
|
|
162
|
-
"eslint.config.ts",
|
|
163
|
-
"eslint.config.js",
|
|
164
|
-
"eslint.config.mjs"
|
|
165
|
-
],
|
|
166
|
-
standardPlaces: [
|
|
167
|
-
"eslint.config.ts",
|
|
168
|
-
"eslint.config.js",
|
|
169
|
-
"eslint.config.mjs"
|
|
170
|
-
]
|
|
171
|
-
},
|
|
172
|
-
prettier: {
|
|
173
|
-
moduleName: "prettier",
|
|
174
|
-
libConfigFiles: [
|
|
175
|
-
".prettierrc",
|
|
176
|
-
".prettierrc.json",
|
|
177
|
-
".prettierrc.yaml",
|
|
178
|
-
".prettierrc.js",
|
|
179
|
-
"prettier.config.js"
|
|
180
|
-
],
|
|
181
|
-
standardPlaces: [
|
|
182
|
-
".prettierrc",
|
|
183
|
-
".prettierrc.json",
|
|
184
|
-
".prettierrc.yaml",
|
|
185
|
-
".prettierrc.js",
|
|
186
|
-
"prettier.config.js",
|
|
187
|
-
"package.json"
|
|
188
|
-
]
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
class ConfigSearch {
|
|
192
|
-
static libConfigDir = "lib/configs";
|
|
193
|
-
static find(tool, options = {}) {
|
|
194
|
-
const config = TOOL_CONFIGS[tool];
|
|
195
|
-
if (!config) return {
|
|
196
|
-
filepath: void 0,
|
|
197
|
-
found: false
|
|
198
|
-
};
|
|
199
|
-
return ConfigSearch.findFile(config.moduleName, {
|
|
200
|
-
libConfigFiles: config.libConfigFiles,
|
|
201
|
-
standardPlaces: config.standardPlaces,
|
|
202
|
-
...options
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
static findFile(moduleName, options = {}) {
|
|
206
|
-
const { searchFrom = process.cwd(), stopDir, libConfigFiles = [], standardPlaces = [] } = options;
|
|
207
|
-
const loaders = {
|
|
208
|
-
".jsonc": defaultLoaders[".json"],
|
|
209
|
-
".yaml": defaultLoaders[".yaml"],
|
|
210
|
-
".yml": defaultLoaders[".yaml"]
|
|
211
|
-
};
|
|
212
|
-
const libConfigDir = join(searchFrom, ConfigSearch.libConfigDir);
|
|
213
|
-
for (const file of libConfigFiles){
|
|
214
|
-
const filepath = join(libConfigDir, file);
|
|
215
|
-
if (existsSync(filepath)) return {
|
|
216
|
-
filepath,
|
|
217
|
-
found: true
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
if (0 === standardPlaces.length) return {
|
|
221
|
-
filepath: void 0,
|
|
222
|
-
found: false
|
|
223
|
-
};
|
|
224
|
-
try {
|
|
225
|
-
const explorer = cosmiconfigSync(moduleName, {
|
|
226
|
-
searchPlaces: standardPlaces,
|
|
227
|
-
loaders,
|
|
228
|
-
stopDir
|
|
229
|
-
});
|
|
230
|
-
const result = explorer.search(searchFrom);
|
|
231
|
-
if (result?.filepath) return {
|
|
232
|
-
filepath: result.filepath,
|
|
233
|
-
found: true
|
|
234
|
-
};
|
|
235
|
-
} catch {}
|
|
236
|
-
return {
|
|
237
|
-
filepath: void 0,
|
|
238
|
-
found: false
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
static exists(filepath) {
|
|
242
|
-
return existsSync(filepath);
|
|
243
|
-
}
|
|
244
|
-
static resolve(filename, fallback) {
|
|
245
|
-
const libPath = `${ConfigSearch.libConfigDir}/${filename}`;
|
|
246
|
-
if (ConfigSearch.exists(libPath)) return libPath;
|
|
247
|
-
return fallback;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
class Filter {
|
|
251
|
-
static exclude(filenames, patterns) {
|
|
252
|
-
if (0 === patterns.length) return [
|
|
253
|
-
...filenames
|
|
254
|
-
];
|
|
255
|
-
return filenames.filter((file)=>!patterns.some((pattern)=>file.includes(pattern)));
|
|
256
|
-
}
|
|
257
|
-
static include(filenames, patterns) {
|
|
258
|
-
if (0 === patterns.length) return [];
|
|
259
|
-
return filenames.filter((file)=>patterns.some((pattern)=>file.includes(pattern)));
|
|
260
|
-
}
|
|
261
|
-
static apply(filenames, options) {
|
|
262
|
-
let result = [
|
|
263
|
-
...filenames
|
|
264
|
-
];
|
|
265
|
-
if (options.include && options.include.length > 0) result = Filter.include(result, options.include);
|
|
266
|
-
if (options.exclude && options.exclude.length > 0) result = Filter.exclude(result, options.exclude);
|
|
267
|
-
return result;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
class Biome {
|
|
271
|
-
static glob = "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}";
|
|
272
|
-
static defaultExcludes = [
|
|
273
|
-
"package-lock.json",
|
|
274
|
-
"__fixtures__"
|
|
275
|
-
];
|
|
276
|
-
static handler = Biome.create();
|
|
277
|
-
static findBiome() {
|
|
278
|
-
const result = Command.findTool("biome");
|
|
279
|
-
return result.command;
|
|
280
|
-
}
|
|
281
|
-
static isAvailable() {
|
|
282
|
-
return Command.findTool("biome").available;
|
|
283
|
-
}
|
|
284
|
-
static findConfig() {
|
|
285
|
-
const result = ConfigSearch.find("biome");
|
|
286
|
-
return result.filepath;
|
|
287
|
-
}
|
|
288
|
-
static create(options = {}) {
|
|
289
|
-
const excludes = options.exclude ?? [
|
|
290
|
-
...Biome.defaultExcludes
|
|
291
|
-
];
|
|
292
|
-
const config = options.config ?? Biome.findConfig();
|
|
293
|
-
return (filenames)=>{
|
|
294
|
-
const filtered = Filter.exclude(filenames, excludes);
|
|
295
|
-
if (0 === filtered.length) return [];
|
|
296
|
-
const biomeCmd = Command.requireTool("biome", "Biome is not available. Install it globally (recommended) or add @biomejs/biome as a dev dependency.");
|
|
297
|
-
const files = filtered.join(" ");
|
|
298
|
-
const flags = options.flags ?? [];
|
|
299
|
-
const configFlag = config ? `--config-path=${config}` : "";
|
|
300
|
-
const cmd = [
|
|
301
|
-
`${biomeCmd} check --write --no-errors-on-unmatched`,
|
|
302
|
-
configFlag,
|
|
303
|
-
...flags,
|
|
304
|
-
files
|
|
305
|
-
].filter(Boolean).join(" ");
|
|
306
|
-
return cmd;
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
class DesignDocs {
|
|
311
|
-
static glob = ".claude/design/**/*.md";
|
|
312
|
-
static defaultExcludes = [
|
|
313
|
-
"design.config.json"
|
|
314
|
-
];
|
|
315
|
-
static defaultValidateScript = ".claude/skills/design-validate/scripts/validate-design-doc.sh";
|
|
316
|
-
static defaultTimestampScript = ".claude/skills/design-update/scripts/update-timestamp.sh";
|
|
317
|
-
static handler = DesignDocs.create();
|
|
318
|
-
static create(options = {}) {
|
|
319
|
-
const excludes = options.exclude ?? [
|
|
320
|
-
...DesignDocs.defaultExcludes
|
|
321
|
-
];
|
|
322
|
-
const validateScript = options.validateScript ?? DesignDocs.defaultValidateScript;
|
|
323
|
-
const timestampScript = options.timestampScript ?? DesignDocs.defaultTimestampScript;
|
|
324
|
-
const skipTimestamp = options.skipTimestamp ?? false;
|
|
325
|
-
return (filenames)=>{
|
|
326
|
-
const filtered = Filter.exclude(filenames, excludes);
|
|
327
|
-
if (0 === filtered.length) return [];
|
|
328
|
-
const commands = [];
|
|
329
|
-
for (const file of filtered){
|
|
330
|
-
commands.push(`${validateScript} "${file}"`);
|
|
331
|
-
if (!skipTimestamp) commands.push(`${timestampScript} "${file}"`);
|
|
332
|
-
}
|
|
333
|
-
return commands;
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
class Markdown {
|
|
338
|
-
static glob = "**/*.{md,mdx}";
|
|
339
|
-
static defaultExcludes = [];
|
|
340
|
-
static handler = Markdown.create();
|
|
341
|
-
static findMarkdownlint() {
|
|
342
|
-
const result = Command.findTool("markdownlint-cli2");
|
|
343
|
-
return result.command;
|
|
344
|
-
}
|
|
345
|
-
static isAvailable() {
|
|
346
|
-
return Command.findTool("markdownlint-cli2").available;
|
|
347
|
-
}
|
|
348
|
-
static findConfig() {
|
|
349
|
-
const result = ConfigSearch.find("markdownlint");
|
|
350
|
-
return result.filepath;
|
|
351
|
-
}
|
|
352
|
-
static create(options = {}) {
|
|
353
|
-
const excludes = options.exclude ?? [
|
|
354
|
-
...Markdown.defaultExcludes
|
|
355
|
-
];
|
|
356
|
-
const noFix = options.noFix ?? false;
|
|
357
|
-
const config = options.config ?? Markdown.findConfig();
|
|
358
|
-
return (filenames)=>{
|
|
359
|
-
const filtered = Filter.exclude(filenames, excludes);
|
|
360
|
-
if (0 === filtered.length) return [];
|
|
361
|
-
const mdlintCmd = Command.requireTool("markdownlint-cli2", "markdownlint-cli2 is not available. Install it globally or add it as a dev dependency.");
|
|
362
|
-
const files = filtered.join(" ");
|
|
363
|
-
const fixFlag = noFix ? "" : "--fix";
|
|
364
|
-
const configFlag = config ? `--config '${config}'` : "";
|
|
365
|
-
const cmd = [
|
|
366
|
-
mdlintCmd,
|
|
367
|
-
configFlag,
|
|
368
|
-
fixFlag,
|
|
369
|
-
files
|
|
370
|
-
].filter(Boolean).join(" ");
|
|
371
|
-
return cmd;
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
}
|
|
2
|
+
import { Biome, Command as Command_Command, readFileSync, existsSync, TypeScript, writeFileSync, Markdown, Filter } from "./376.js";
|
|
375
3
|
class PackageJson {
|
|
376
4
|
static glob = "**/package.json";
|
|
377
5
|
static defaultExcludes = [
|
|
@@ -387,14 +15,21 @@ class PackageJson {
|
|
|
387
15
|
return (filenames)=>{
|
|
388
16
|
const filtered = Filter.exclude(filenames, excludes);
|
|
389
17
|
if (0 === filtered.length) return [];
|
|
390
|
-
if (!skipSort) for (const filepath of filtered){
|
|
391
|
-
const content = readFileSync(filepath, "utf-8");
|
|
392
|
-
const sorted = sort_package_json(content);
|
|
393
|
-
writeFileSync(filepath, sorted, "utf-8");
|
|
394
|
-
}
|
|
395
18
|
const files = filtered.join(" ");
|
|
19
|
+
const commands = [];
|
|
20
|
+
if (!skipSort) {
|
|
21
|
+
const pm = Command_Command.detectPackageManager();
|
|
22
|
+
const prefix = Command_Command.getExecPrefix(pm);
|
|
23
|
+
const sortCmd = [
|
|
24
|
+
...prefix,
|
|
25
|
+
"sort-package-json",
|
|
26
|
+
files
|
|
27
|
+
].join(" ");
|
|
28
|
+
commands.push(sortCmd);
|
|
29
|
+
}
|
|
396
30
|
const biomeCmd = options.biomeConfig ? `biome check --write --max-diagnostics=none --config-path=${options.biomeConfig} ${files}` : `biome check --write --max-diagnostics=none ${files}`;
|
|
397
|
-
|
|
31
|
+
commands.push(biomeCmd);
|
|
32
|
+
return commands.join(" && ");
|
|
398
33
|
};
|
|
399
34
|
}
|
|
400
35
|
}
|
|
@@ -407,6 +42,11 @@ class PnpmWorkspace {
|
|
|
407
42
|
static glob = "pnpm-workspace.yaml";
|
|
408
43
|
static defaultExcludes = [];
|
|
409
44
|
static handler = PnpmWorkspace.create();
|
|
45
|
+
static SORTABLE_ARRAY_KEYS = new Set([
|
|
46
|
+
"packages",
|
|
47
|
+
"onlyBuiltDependencies",
|
|
48
|
+
"publicHoistPattern"
|
|
49
|
+
]);
|
|
410
50
|
static sortContent(content) {
|
|
411
51
|
const result = {};
|
|
412
52
|
const keys = Object.keys(content).sort((a, b)=>{
|
|
@@ -416,13 +56,7 @@ class PnpmWorkspace {
|
|
|
416
56
|
});
|
|
417
57
|
for (const key of keys){
|
|
418
58
|
const value = content[key];
|
|
419
|
-
if (
|
|
420
|
-
...value
|
|
421
|
-
].sort();
|
|
422
|
-
else if ("onlyBuiltDependencies" === key && Array.isArray(value)) result[key] = [
|
|
423
|
-
...value
|
|
424
|
-
].sort();
|
|
425
|
-
else if ("publicHoistPattern" === key && Array.isArray(value)) result[key] = [
|
|
59
|
+
if (PnpmWorkspace.SORTABLE_ARRAY_KEYS.has(key) && Array.isArray(value)) result[key] = [
|
|
426
60
|
...value
|
|
427
61
|
].sort();
|
|
428
62
|
else result[key] = value;
|
|
@@ -448,6 +82,7 @@ class PnpmWorkspace {
|
|
|
448
82
|
if (!skipSort || !skipFormat) {
|
|
449
83
|
const formatted = stringify(parsed, DEFAULT_STRINGIFY_OPTIONS);
|
|
450
84
|
writeFileSync(filepath, formatted, "utf-8");
|
|
85
|
+
return `git add ${filepath}`;
|
|
451
86
|
}
|
|
452
87
|
return [];
|
|
453
88
|
};
|
|
@@ -472,578 +107,6 @@ class ShellScripts {
|
|
|
472
107
|
};
|
|
473
108
|
}
|
|
474
109
|
}
|
|
475
|
-
class TsDocLinter {
|
|
476
|
-
eslint;
|
|
477
|
-
constructor(options = {}){
|
|
478
|
-
const ignorePatterns = options.ignorePatterns ?? [];
|
|
479
|
-
const config = [
|
|
480
|
-
{
|
|
481
|
-
ignores: [
|
|
482
|
-
"**/node_modules/**",
|
|
483
|
-
"**/dist/**",
|
|
484
|
-
"**/coverage/**",
|
|
485
|
-
...ignorePatterns
|
|
486
|
-
]
|
|
487
|
-
},
|
|
488
|
-
{
|
|
489
|
-
files: [
|
|
490
|
-
"**/*.ts",
|
|
491
|
-
"**/*.tsx",
|
|
492
|
-
"**/*.mts",
|
|
493
|
-
"**/*.cts"
|
|
494
|
-
],
|
|
495
|
-
languageOptions: {
|
|
496
|
-
parser: parser
|
|
497
|
-
},
|
|
498
|
-
plugins: {
|
|
499
|
-
tsdoc: eslint_plugin_tsdoc
|
|
500
|
-
},
|
|
501
|
-
rules: {
|
|
502
|
-
"tsdoc/syntax": "error"
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
];
|
|
506
|
-
this.eslint = new ESLint({
|
|
507
|
-
overrideConfigFile: true,
|
|
508
|
-
overrideConfig: config
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
async lintFiles(filePaths) {
|
|
512
|
-
if (0 === filePaths.length) return [];
|
|
513
|
-
const results = await this.eslint.lintFiles(filePaths);
|
|
514
|
-
return results.map((result)=>({
|
|
515
|
-
filePath: result.filePath,
|
|
516
|
-
errorCount: result.errorCount,
|
|
517
|
-
warningCount: result.warningCount,
|
|
518
|
-
messages: result.messages.map((msg)=>({
|
|
519
|
-
line: msg.line,
|
|
520
|
-
column: msg.column,
|
|
521
|
-
severity: msg.severity,
|
|
522
|
-
message: msg.message,
|
|
523
|
-
ruleId: msg.ruleId
|
|
524
|
-
}))
|
|
525
|
-
}));
|
|
526
|
-
}
|
|
527
|
-
async lintFilesAndThrow(filePaths) {
|
|
528
|
-
const results = await this.lintFiles(filePaths);
|
|
529
|
-
const errors = [];
|
|
530
|
-
for (const result of results)if (result.errorCount > 0) {
|
|
531
|
-
for (const msg of result.messages)if (2 === msg.severity) errors.push(`${result.filePath}:${msg.line}:${msg.column} - ${msg.message}`);
|
|
532
|
-
}
|
|
533
|
-
if (errors.length > 0) throw new Error(`TSDoc validation failed:\n${errors.join("\n")}`);
|
|
534
|
-
}
|
|
535
|
-
static formatResults(results) {
|
|
536
|
-
const lines = [];
|
|
537
|
-
for (const result of results)if (0 !== result.errorCount || 0 !== result.warningCount) {
|
|
538
|
-
lines.push(`\n${result.filePath}`);
|
|
539
|
-
for (const msg of result.messages){
|
|
540
|
-
const severity = 2 === msg.severity ? "error" : "warning";
|
|
541
|
-
const rule = msg.ruleId ? ` (${msg.ruleId})` : "";
|
|
542
|
-
lines.push(` ${msg.line}:${msg.column} ${severity} ${msg.message}${rule}`);
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
const totalErrors = results.reduce((sum, r)=>sum + r.errorCount, 0);
|
|
546
|
-
const totalWarnings = results.reduce((sum, r)=>sum + r.warningCount, 0);
|
|
547
|
-
if (totalErrors > 0 || totalWarnings > 0) lines.push(`\n✖ ${totalErrors} error(s), ${totalWarnings} warning(s)`);
|
|
548
|
-
return lines.join("\n");
|
|
549
|
-
}
|
|
550
|
-
static hasErrors(results) {
|
|
551
|
-
return results.some((r)=>r.errorCount > 0);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
const TS_EXTENSIONS = [
|
|
555
|
-
".ts",
|
|
556
|
-
".tsx",
|
|
557
|
-
".mts",
|
|
558
|
-
".cts"
|
|
559
|
-
];
|
|
560
|
-
class EntryExtractor {
|
|
561
|
-
extract(packageJson) {
|
|
562
|
-
const entries = {};
|
|
563
|
-
const unresolved = [];
|
|
564
|
-
const { exports } = packageJson;
|
|
565
|
-
if (!exports) {
|
|
566
|
-
const mainEntry = packageJson.module ?? packageJson.main;
|
|
567
|
-
if (mainEntry && this.isTypeScriptFile(mainEntry)) entries["."] = mainEntry;
|
|
568
|
-
else if (mainEntry) unresolved.push(".");
|
|
569
|
-
return {
|
|
570
|
-
entries,
|
|
571
|
-
unresolved
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
if ("string" == typeof exports) {
|
|
575
|
-
if (this.isTypeScriptFile(exports)) entries["."] = exports;
|
|
576
|
-
else unresolved.push(".");
|
|
577
|
-
return {
|
|
578
|
-
entries,
|
|
579
|
-
unresolved
|
|
580
|
-
};
|
|
581
|
-
}
|
|
582
|
-
this.extractFromObject(exports, entries, unresolved, ".");
|
|
583
|
-
return {
|
|
584
|
-
entries,
|
|
585
|
-
unresolved
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
extractFromObject(obj, entries, unresolved, currentPath) {
|
|
589
|
-
for (const [key, value] of Object.entries(obj)){
|
|
590
|
-
const exportPath = key.startsWith(".") ? key : currentPath;
|
|
591
|
-
if ("string" == typeof value) {
|
|
592
|
-
if (this.isTypeScriptFile(value)) entries[exportPath] = value;
|
|
593
|
-
else if (key.startsWith(".")) unresolved.push(exportPath);
|
|
594
|
-
} else if (value && "object" == typeof value && !Array.isArray(value)) {
|
|
595
|
-
const nested = value;
|
|
596
|
-
const tsPath = this.findTypeScriptCondition(nested);
|
|
597
|
-
if (tsPath) entries[exportPath] = tsPath;
|
|
598
|
-
else if (key.startsWith(".")) this.extractFromObject(nested, entries, unresolved, exportPath);
|
|
599
|
-
else {
|
|
600
|
-
const sourcePath = this.findSourceCondition(nested);
|
|
601
|
-
if (sourcePath && this.isTypeScriptFile(sourcePath)) entries[exportPath] = sourcePath;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
findTypeScriptCondition(conditions) {
|
|
607
|
-
const priorityKeys = [
|
|
608
|
-
"source",
|
|
609
|
-
"typescript",
|
|
610
|
-
"development",
|
|
611
|
-
"default"
|
|
612
|
-
];
|
|
613
|
-
for (const key of priorityKeys){
|
|
614
|
-
const value = conditions[key];
|
|
615
|
-
if ("string" == typeof value && this.isTypeScriptFile(value)) return value;
|
|
616
|
-
if (value && "object" == typeof value) {
|
|
617
|
-
const nested = this.findTypeScriptCondition(value);
|
|
618
|
-
if (nested) return nested;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
return null;
|
|
622
|
-
}
|
|
623
|
-
findSourceCondition(conditions) {
|
|
624
|
-
const priorityKeys = [
|
|
625
|
-
"source",
|
|
626
|
-
"import",
|
|
627
|
-
"require",
|
|
628
|
-
"default"
|
|
629
|
-
];
|
|
630
|
-
for (const key of priorityKeys){
|
|
631
|
-
const value = conditions[key];
|
|
632
|
-
if ("string" == typeof value) return value;
|
|
633
|
-
if (value && "object" == typeof value) {
|
|
634
|
-
const nested = this.findSourceCondition(value);
|
|
635
|
-
if (nested) return nested;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
return null;
|
|
639
|
-
}
|
|
640
|
-
isTypeScriptFile(filePath) {
|
|
641
|
-
return TS_EXTENSIONS.some((ext)=>filePath.endsWith(ext));
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
class ImportGraph {
|
|
645
|
-
options;
|
|
646
|
-
program = null;
|
|
647
|
-
compilerOptions = null;
|
|
648
|
-
moduleResolutionCache = null;
|
|
649
|
-
constructor(options){
|
|
650
|
-
this.options = options;
|
|
651
|
-
}
|
|
652
|
-
traceFromEntries(entryPaths) {
|
|
653
|
-
const errors = [];
|
|
654
|
-
const visited = new Set();
|
|
655
|
-
const entries = [];
|
|
656
|
-
const initResult = this.initializeProgram();
|
|
657
|
-
if (!initResult.success) return {
|
|
658
|
-
files: [],
|
|
659
|
-
entries: [],
|
|
660
|
-
errors: [
|
|
661
|
-
initResult.error
|
|
662
|
-
]
|
|
663
|
-
};
|
|
664
|
-
for (const entryPath of entryPaths){
|
|
665
|
-
const absolutePath = this.resolveEntryPath(entryPath);
|
|
666
|
-
if (!existsSync(absolutePath)) {
|
|
667
|
-
errors.push({
|
|
668
|
-
type: "entry_not_found",
|
|
669
|
-
message: `Entry file not found: ${entryPath}`,
|
|
670
|
-
path: absolutePath
|
|
671
|
-
});
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
entries.push(absolutePath);
|
|
675
|
-
this.traceImports(absolutePath, visited, errors);
|
|
676
|
-
}
|
|
677
|
-
const files = Array.from(visited).filter((file)=>this.isSourceFile(file));
|
|
678
|
-
return {
|
|
679
|
-
files: files.sort(),
|
|
680
|
-
entries,
|
|
681
|
-
errors
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
traceFromPackageExports(packageJsonPath) {
|
|
685
|
-
const absolutePath = this.resolveEntryPath(packageJsonPath);
|
|
686
|
-
let packageJson;
|
|
687
|
-
try {
|
|
688
|
-
if (!existsSync(absolutePath)) return {
|
|
689
|
-
files: [],
|
|
690
|
-
entries: [],
|
|
691
|
-
errors: [
|
|
692
|
-
{
|
|
693
|
-
type: "package_json_not_found",
|
|
694
|
-
message: `Failed to read package.json: File not found at ${absolutePath}`,
|
|
695
|
-
path: absolutePath
|
|
696
|
-
}
|
|
697
|
-
]
|
|
698
|
-
};
|
|
699
|
-
const content = readFileSync(absolutePath, "utf-8");
|
|
700
|
-
packageJson = JSON.parse(content);
|
|
701
|
-
} catch (error) {
|
|
702
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
703
|
-
return {
|
|
704
|
-
files: [],
|
|
705
|
-
entries: [],
|
|
706
|
-
errors: [
|
|
707
|
-
{
|
|
708
|
-
type: "package_json_parse_error",
|
|
709
|
-
message: `Failed to parse package.json: ${message}`,
|
|
710
|
-
path: absolutePath
|
|
711
|
-
}
|
|
712
|
-
]
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
const extractor = new EntryExtractor();
|
|
716
|
-
const { entries } = extractor.extract(packageJson);
|
|
717
|
-
const packageDir = dirname(absolutePath);
|
|
718
|
-
const entryPaths = Object.values(entries).map((p)=>resolve(packageDir, p));
|
|
719
|
-
return this.traceFromEntries(entryPaths);
|
|
720
|
-
}
|
|
721
|
-
initializeProgram() {
|
|
722
|
-
if (this.program) return {
|
|
723
|
-
success: true
|
|
724
|
-
};
|
|
725
|
-
const configPath = this.findTsConfig();
|
|
726
|
-
if (!configPath) {
|
|
727
|
-
this.compilerOptions = {
|
|
728
|
-
moduleResolution: typescript.ModuleResolutionKind.NodeNext,
|
|
729
|
-
module: typescript.ModuleKind.NodeNext,
|
|
730
|
-
target: typescript.ScriptTarget.ESNext,
|
|
731
|
-
strict: true
|
|
732
|
-
};
|
|
733
|
-
this.moduleResolutionCache = typescript.createModuleResolutionCache(this.options.rootDir, (fileName)=>fileName.toLowerCase(), this.compilerOptions);
|
|
734
|
-
const host = typescript.createCompilerHost(this.compilerOptions, true);
|
|
735
|
-
host.getCurrentDirectory = ()=>this.options.rootDir;
|
|
736
|
-
this.program = typescript.createProgram([], this.compilerOptions, host);
|
|
737
|
-
return {
|
|
738
|
-
success: true
|
|
739
|
-
};
|
|
740
|
-
}
|
|
741
|
-
const configFile = typescript.readConfigFile(configPath, (path)=>readFileSync(path, "utf-8"));
|
|
742
|
-
if (configFile.error) {
|
|
743
|
-
const message = typescript.flattenDiagnosticMessageText(configFile.error.messageText, "\n");
|
|
744
|
-
return {
|
|
745
|
-
success: false,
|
|
746
|
-
error: {
|
|
747
|
-
type: "tsconfig_read_error",
|
|
748
|
-
message: `Failed to read tsconfig.json: ${message}`,
|
|
749
|
-
path: configPath
|
|
750
|
-
}
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
const parsed = typescript.parseJsonConfigFileContent(configFile.config, typescript.sys, dirname(configPath));
|
|
754
|
-
if (parsed.errors.length > 0) {
|
|
755
|
-
const messages = parsed.errors.map((e)=>typescript.flattenDiagnosticMessageText(e.messageText, "\n")).join("\n");
|
|
756
|
-
return {
|
|
757
|
-
success: false,
|
|
758
|
-
error: {
|
|
759
|
-
type: "tsconfig_parse_error",
|
|
760
|
-
message: `Failed to parse tsconfig.json: ${messages}`,
|
|
761
|
-
path: configPath
|
|
762
|
-
}
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
this.compilerOptions = parsed.options;
|
|
766
|
-
this.moduleResolutionCache = typescript.createModuleResolutionCache(this.options.rootDir, (fileName)=>fileName.toLowerCase(), this.compilerOptions);
|
|
767
|
-
const host = typescript.createCompilerHost(this.compilerOptions, true);
|
|
768
|
-
host.getCurrentDirectory = ()=>this.options.rootDir;
|
|
769
|
-
this.program = typescript.createProgram([], this.compilerOptions, host);
|
|
770
|
-
return {
|
|
771
|
-
success: true
|
|
772
|
-
};
|
|
773
|
-
}
|
|
774
|
-
findTsConfig() {
|
|
775
|
-
if (this.options.tsconfigPath) {
|
|
776
|
-
const customPath = isAbsolute(this.options.tsconfigPath) ? this.options.tsconfigPath : resolve(this.options.rootDir, this.options.tsconfigPath);
|
|
777
|
-
if (existsSync(customPath)) return customPath;
|
|
778
|
-
return null;
|
|
779
|
-
}
|
|
780
|
-
const configPath = typescript.findConfigFile(this.options.rootDir, (path)=>existsSync(path));
|
|
781
|
-
return configPath ?? null;
|
|
782
|
-
}
|
|
783
|
-
resolveEntryPath(entryPath) {
|
|
784
|
-
if (isAbsolute(entryPath)) return normalize(entryPath);
|
|
785
|
-
return normalize(resolve(this.options.rootDir, entryPath));
|
|
786
|
-
}
|
|
787
|
-
traceImports(filePath, visited, errors) {
|
|
788
|
-
const normalizedPath = normalize(filePath);
|
|
789
|
-
if (visited.has(normalizedPath)) return;
|
|
790
|
-
if (this.isExternalModule(normalizedPath)) return;
|
|
791
|
-
visited.add(normalizedPath);
|
|
792
|
-
let content;
|
|
793
|
-
try {
|
|
794
|
-
content = readFileSync(normalizedPath, "utf-8");
|
|
795
|
-
} catch {
|
|
796
|
-
errors.push({
|
|
797
|
-
type: "file_read_error",
|
|
798
|
-
message: `Failed to read file: ${normalizedPath}`,
|
|
799
|
-
path: normalizedPath
|
|
800
|
-
});
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
const sourceFile = typescript.createSourceFile(normalizedPath, content, typescript.ScriptTarget.Latest, true);
|
|
804
|
-
const imports = this.extractImports(sourceFile);
|
|
805
|
-
for (const importPath of imports){
|
|
806
|
-
const resolved = this.resolveImport(importPath, normalizedPath);
|
|
807
|
-
if (resolved) this.traceImports(resolved, visited, errors);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
extractImports(sourceFile) {
|
|
811
|
-
const imports = [];
|
|
812
|
-
const visit = (node)=>{
|
|
813
|
-
if (typescript.isImportDeclaration(node)) {
|
|
814
|
-
const specifier = node.moduleSpecifier;
|
|
815
|
-
if (typescript.isStringLiteral(specifier)) imports.push(specifier.text);
|
|
816
|
-
} else if (typescript.isExportDeclaration(node)) {
|
|
817
|
-
const specifier = node.moduleSpecifier;
|
|
818
|
-
if (specifier && typescript.isStringLiteral(specifier)) imports.push(specifier.text);
|
|
819
|
-
} else if (typescript.isCallExpression(node)) {
|
|
820
|
-
const expression = node.expression;
|
|
821
|
-
if (expression.kind === typescript.SyntaxKind.ImportKeyword && node.arguments.length > 0) {
|
|
822
|
-
const arg = node.arguments[0];
|
|
823
|
-
if (arg && typescript.isStringLiteral(arg)) imports.push(arg.text);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
typescript.forEachChild(node, visit);
|
|
827
|
-
};
|
|
828
|
-
visit(sourceFile);
|
|
829
|
-
return imports;
|
|
830
|
-
}
|
|
831
|
-
resolveImport(specifier, fromFile) {
|
|
832
|
-
if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
|
|
833
|
-
if (!this.compilerOptions?.paths || !Object.keys(this.compilerOptions.paths).length) return null;
|
|
834
|
-
}
|
|
835
|
-
if (!this.compilerOptions || !this.moduleResolutionCache) return null;
|
|
836
|
-
const resolved = typescript.resolveModuleName(specifier, fromFile, this.compilerOptions, typescript.sys, this.moduleResolutionCache);
|
|
837
|
-
if (resolved.resolvedModule) {
|
|
838
|
-
const resolvedPath = resolved.resolvedModule.resolvedFileName;
|
|
839
|
-
if (resolved.resolvedModule.isExternalLibraryImport) return null;
|
|
840
|
-
if (resolvedPath.endsWith(".d.ts")) {
|
|
841
|
-
const sourcePath = resolvedPath.replace(/\.d\.ts$/, ".ts");
|
|
842
|
-
if (existsSync(sourcePath)) return sourcePath;
|
|
843
|
-
return null;
|
|
844
|
-
}
|
|
845
|
-
return resolvedPath;
|
|
846
|
-
}
|
|
847
|
-
return null;
|
|
848
|
-
}
|
|
849
|
-
isExternalModule(filePath) {
|
|
850
|
-
return filePath.includes("/node_modules/") || filePath.includes("\\node_modules\\");
|
|
851
|
-
}
|
|
852
|
-
isSourceFile(filePath) {
|
|
853
|
-
if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx") && !filePath.endsWith(".mts") && !filePath.endsWith(".cts")) return false;
|
|
854
|
-
if (filePath.endsWith(".d.ts") || filePath.endsWith(".d.mts") || filePath.endsWith(".d.cts")) return false;
|
|
855
|
-
if (filePath.includes(".test.") || filePath.includes(".spec.")) return false;
|
|
856
|
-
if (filePath.includes("/__test__/") || filePath.includes("\\__test__\\")) return false;
|
|
857
|
-
if (filePath.includes("/__tests__/") || filePath.includes("\\__tests__\\")) return false;
|
|
858
|
-
const excludePatterns = this.options.excludePatterns ?? [];
|
|
859
|
-
for (const pattern of excludePatterns)if (filePath.includes(pattern)) return false;
|
|
860
|
-
return true;
|
|
861
|
-
}
|
|
862
|
-
static fromEntries(entryPaths, options) {
|
|
863
|
-
const graph = new ImportGraph(options);
|
|
864
|
-
return graph.traceFromEntries(entryPaths);
|
|
865
|
-
}
|
|
866
|
-
static fromPackageExports(packageJsonPath, options) {
|
|
867
|
-
const graph = new ImportGraph(options);
|
|
868
|
-
return graph.traceFromPackageExports(packageJsonPath);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
class TsDocResolver {
|
|
872
|
-
options;
|
|
873
|
-
constructor(options){
|
|
874
|
-
this.options = options;
|
|
875
|
-
}
|
|
876
|
-
resolve() {
|
|
877
|
-
const { rootDir } = this.options;
|
|
878
|
-
const workspaces = [];
|
|
879
|
-
const repoTsdocPath = join(rootDir, "tsdoc.json");
|
|
880
|
-
const repoTsdocConfig = existsSync(repoTsdocPath) ? repoTsdocPath : void 0;
|
|
881
|
-
let workspaceInfos;
|
|
882
|
-
let isMonorepo = false;
|
|
883
|
-
try {
|
|
884
|
-
workspaceInfos = getWorkspaces(rootDir);
|
|
885
|
-
isMonorepo = workspaceInfos.length > 1;
|
|
886
|
-
} catch {
|
|
887
|
-
workspaceInfos = [];
|
|
888
|
-
}
|
|
889
|
-
if (0 === workspaceInfos.length) {
|
|
890
|
-
const result = this.resolveWorkspace(rootDir, repoTsdocConfig);
|
|
891
|
-
if (result) workspaces.push(result);
|
|
892
|
-
} else for (const info of workspaceInfos){
|
|
893
|
-
const workspacePath = info.path;
|
|
894
|
-
const result = this.resolveWorkspace(workspacePath, repoTsdocConfig);
|
|
895
|
-
if (result) workspaces.push(result);
|
|
896
|
-
}
|
|
897
|
-
return {
|
|
898
|
-
workspaces,
|
|
899
|
-
isMonorepo,
|
|
900
|
-
repoTsdocConfig
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
resolveWorkspace(workspacePath, repoTsdocConfig) {
|
|
904
|
-
const packageJsonPath = join(workspacePath, "package.json");
|
|
905
|
-
if (!existsSync(packageJsonPath)) return null;
|
|
906
|
-
let packageJson;
|
|
907
|
-
try {
|
|
908
|
-
const content = readFileSync(packageJsonPath, "utf-8");
|
|
909
|
-
packageJson = JSON.parse(content);
|
|
910
|
-
} catch {
|
|
911
|
-
return null;
|
|
912
|
-
}
|
|
913
|
-
const workspaceTsdocPath = join(workspacePath, "tsdoc.json");
|
|
914
|
-
const workspaceTsdocConfig = existsSync(workspaceTsdocPath) ? workspaceTsdocPath : void 0;
|
|
915
|
-
const tsdocConfigPath = workspaceTsdocConfig ?? repoTsdocConfig;
|
|
916
|
-
if (!tsdocConfigPath) return null;
|
|
917
|
-
if (!packageJson.exports) return null;
|
|
918
|
-
const name = packageJson.name ?? relative(this.options.rootDir, workspacePath);
|
|
919
|
-
const errors = [];
|
|
920
|
-
const graph = new ImportGraph({
|
|
921
|
-
rootDir: workspacePath,
|
|
922
|
-
excludePatterns: this.options.excludePatterns
|
|
923
|
-
});
|
|
924
|
-
const result = graph.traceFromPackageExports(packageJsonPath);
|
|
925
|
-
for (const error of result.errors)errors.push(error.message);
|
|
926
|
-
return {
|
|
927
|
-
name,
|
|
928
|
-
path: workspacePath,
|
|
929
|
-
tsdocConfigPath,
|
|
930
|
-
files: result.files,
|
|
931
|
-
errors
|
|
932
|
-
};
|
|
933
|
-
}
|
|
934
|
-
filterStagedFiles(stagedFiles) {
|
|
935
|
-
const result = this.resolve();
|
|
936
|
-
const output = [];
|
|
937
|
-
for (const workspace of result.workspaces){
|
|
938
|
-
const workspaceFiles = new Set(workspace.files);
|
|
939
|
-
const matchedFiles = stagedFiles.filter((f)=>workspaceFiles.has(f));
|
|
940
|
-
if (matchedFiles.length > 0) output.push({
|
|
941
|
-
files: matchedFiles,
|
|
942
|
-
tsdocConfigPath: workspace.tsdocConfigPath
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
return output;
|
|
946
|
-
}
|
|
947
|
-
needsLinting(filePath) {
|
|
948
|
-
const result = this.resolve();
|
|
949
|
-
for (const workspace of result.workspaces)if (workspace.files.includes(filePath)) return true;
|
|
950
|
-
return false;
|
|
951
|
-
}
|
|
952
|
-
getTsDocConfig(filePath) {
|
|
953
|
-
const result = this.resolve();
|
|
954
|
-
for (const workspace of result.workspaces)if (workspace.files.includes(filePath)) return workspace.tsdocConfigPath;
|
|
955
|
-
}
|
|
956
|
-
findWorkspace(filePath) {
|
|
957
|
-
const result = this.resolve();
|
|
958
|
-
for (const workspace of result.workspaces)if (filePath.startsWith(workspace.path)) return workspace;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
class TypeScript {
|
|
962
|
-
static glob = "*.{ts,cts,mts,tsx}";
|
|
963
|
-
static defaultExcludes = [];
|
|
964
|
-
static defaultTsdocExcludes = [
|
|
965
|
-
".test.",
|
|
966
|
-
".spec.",
|
|
967
|
-
"__test__",
|
|
968
|
-
"__tests__"
|
|
969
|
-
];
|
|
970
|
-
static detectCompiler(cwd = process.cwd()) {
|
|
971
|
-
const packageJsonPath = join(cwd, "package.json");
|
|
972
|
-
if (!existsSync(packageJsonPath)) return;
|
|
973
|
-
try {
|
|
974
|
-
const content = readFileSync(packageJsonPath, "utf-8");
|
|
975
|
-
const pkg = JSON.parse(content);
|
|
976
|
-
const allDeps = {
|
|
977
|
-
...pkg.dependencies,
|
|
978
|
-
...pkg.devDependencies
|
|
979
|
-
};
|
|
980
|
-
if ("@typescript/native-preview" in allDeps) return "tsgo";
|
|
981
|
-
if ("typescript" in allDeps) return "tsc";
|
|
982
|
-
} catch {}
|
|
983
|
-
}
|
|
984
|
-
static isAvailable() {
|
|
985
|
-
return void 0 !== TypeScript.detectCompiler();
|
|
986
|
-
}
|
|
987
|
-
static getDefaultTypecheckCommand() {
|
|
988
|
-
const compiler = TypeScript.detectCompiler();
|
|
989
|
-
if (!compiler) throw new Error("No TypeScript compiler found. Install 'typescript' or '@typescript/native-preview' as a dev dependency.");
|
|
990
|
-
const pm = Command.detectPackageManager();
|
|
991
|
-
const prefix = Command.getExecPrefix(pm);
|
|
992
|
-
return [
|
|
993
|
-
...prefix,
|
|
994
|
-
compiler,
|
|
995
|
-
"--noEmit"
|
|
996
|
-
].join(" ");
|
|
997
|
-
}
|
|
998
|
-
static handler = TypeScript.create();
|
|
999
|
-
static isTsdocAvailable(cwd = process.cwd()) {
|
|
1000
|
-
const tsdocPath = join(cwd, "tsdoc.json");
|
|
1001
|
-
return existsSync(tsdocPath);
|
|
1002
|
-
}
|
|
1003
|
-
static create(options = {}) {
|
|
1004
|
-
const excludes = options.exclude ?? [
|
|
1005
|
-
...TypeScript.defaultExcludes
|
|
1006
|
-
];
|
|
1007
|
-
const tsdocExcludes = options.excludeTsdoc ?? [
|
|
1008
|
-
...TypeScript.defaultTsdocExcludes
|
|
1009
|
-
];
|
|
1010
|
-
const skipTsdoc = options.skipTsdoc ?? false;
|
|
1011
|
-
const skipTypecheck = options.skipTypecheck ?? false;
|
|
1012
|
-
const rootDir = options.rootDir ?? process.cwd();
|
|
1013
|
-
let typecheckCommand;
|
|
1014
|
-
const getTypecheckCommand = ()=>{
|
|
1015
|
-
if (void 0 === typecheckCommand) typecheckCommand = options.typecheckCommand ?? TypeScript.getDefaultTypecheckCommand();
|
|
1016
|
-
return typecheckCommand;
|
|
1017
|
-
};
|
|
1018
|
-
return async (filenames)=>{
|
|
1019
|
-
const filtered = Filter.exclude(filenames, excludes);
|
|
1020
|
-
if (0 === filtered.length) return [];
|
|
1021
|
-
const commands = [];
|
|
1022
|
-
if (!skipTsdoc) {
|
|
1023
|
-
const resolver = new TsDocResolver({
|
|
1024
|
-
rootDir,
|
|
1025
|
-
excludePatterns: [
|
|
1026
|
-
...tsdocExcludes
|
|
1027
|
-
]
|
|
1028
|
-
});
|
|
1029
|
-
const absoluteFiles = filtered.map((f)=>isAbsolute(f) ? f : join(rootDir, f));
|
|
1030
|
-
const tsdocGroups = resolver.filterStagedFiles(absoluteFiles);
|
|
1031
|
-
for (const group of tsdocGroups)if (group.files.length > 0) {
|
|
1032
|
-
const linter = new TsDocLinter({
|
|
1033
|
-
ignorePatterns: tsdocExcludes.map((p)=>`**/*${p}*`)
|
|
1034
|
-
});
|
|
1035
|
-
const results = await linter.lintFiles(group.files);
|
|
1036
|
-
if (TsDocLinter.hasErrors(results)) {
|
|
1037
|
-
const output = TsDocLinter.formatResults(results);
|
|
1038
|
-
throw new Error(`TSDoc validation failed:\n${output}`);
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
if (!skipTypecheck && filtered.length > 0) commands.push(getTypecheckCommand());
|
|
1043
|
-
return commands;
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
110
|
const Yaml_DEFAULT_STRINGIFY_OPTIONS = {
|
|
1048
111
|
indent: 2,
|
|
1049
112
|
lineWidth: 0,
|
|
@@ -1084,6 +147,7 @@ class Yaml {
|
|
|
1084
147
|
} catch (error) {
|
|
1085
148
|
throw new Error(`Invalid YAML in ${filepath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1086
149
|
}
|
|
150
|
+
if (!skipFormat && filtered.length > 0) return `git add ${filtered.join(" ")}`;
|
|
1087
151
|
return [];
|
|
1088
152
|
};
|
|
1089
153
|
}
|
|
@@ -1118,52 +182,48 @@ function createConfig(options = {}) {
|
|
|
1118
182
|
const handlerOptions = "object" == typeof options.typescript ? options.typescript : {};
|
|
1119
183
|
config[TypeScript.glob] = TypeScript.create(handlerOptions);
|
|
1120
184
|
}
|
|
1121
|
-
if (true === options.designDocs || "object" == typeof options.designDocs) {
|
|
1122
|
-
const handlerOptions = "object" == typeof options.designDocs ? options.designDocs : {};
|
|
1123
|
-
config[DesignDocs.glob] = DesignDocs.create(handlerOptions);
|
|
1124
|
-
}
|
|
1125
185
|
if (options.custom) for (const [glob, handler] of Object.entries(options.custom))config[glob] = handler;
|
|
1126
186
|
return config;
|
|
1127
187
|
}
|
|
1128
188
|
class Preset {
|
|
1129
189
|
static minimal(extend = {}) {
|
|
1130
|
-
|
|
190
|
+
const options = {
|
|
1131
191
|
packageJson: extend.packageJson ?? {},
|
|
1132
192
|
biome: extend.biome ?? {},
|
|
1133
193
|
markdown: extend.markdown ?? false,
|
|
1134
194
|
yaml: extend.yaml ?? false,
|
|
1135
195
|
pnpmWorkspace: extend.pnpmWorkspace ?? false,
|
|
1136
196
|
shellScripts: extend.shellScripts ?? false,
|
|
1137
|
-
typescript: extend.typescript ?? false
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
197
|
+
typescript: extend.typescript ?? false
|
|
198
|
+
};
|
|
199
|
+
if (void 0 !== extend.custom) options.custom = extend.custom;
|
|
200
|
+
return createConfig(options);
|
|
1141
201
|
}
|
|
1142
202
|
static standard(extend = {}) {
|
|
1143
|
-
|
|
203
|
+
const options = {
|
|
1144
204
|
packageJson: extend.packageJson ?? {},
|
|
1145
205
|
biome: extend.biome ?? {},
|
|
1146
206
|
markdown: extend.markdown ?? {},
|
|
1147
207
|
yaml: extend.yaml ?? {},
|
|
1148
208
|
pnpmWorkspace: extend.pnpmWorkspace ?? {},
|
|
1149
209
|
shellScripts: extend.shellScripts ?? {},
|
|
1150
|
-
typescript: extend.typescript ?? false
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
210
|
+
typescript: extend.typescript ?? false
|
|
211
|
+
};
|
|
212
|
+
if (void 0 !== extend.custom) options.custom = extend.custom;
|
|
213
|
+
return createConfig(options);
|
|
1154
214
|
}
|
|
1155
215
|
static full(extend = {}) {
|
|
1156
|
-
|
|
216
|
+
const options = {
|
|
1157
217
|
packageJson: extend.packageJson ?? {},
|
|
1158
218
|
biome: extend.biome ?? {},
|
|
1159
219
|
markdown: extend.markdown ?? {},
|
|
1160
220
|
yaml: extend.yaml ?? {},
|
|
1161
221
|
pnpmWorkspace: extend.pnpmWorkspace ?? {},
|
|
1162
222
|
shellScripts: extend.shellScripts ?? {},
|
|
1163
|
-
typescript: extend.typescript ?? {}
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
223
|
+
typescript: extend.typescript ?? {}
|
|
224
|
+
};
|
|
225
|
+
if (void 0 !== extend.custom) options.custom = extend.custom;
|
|
226
|
+
return createConfig(options);
|
|
1167
227
|
}
|
|
1168
228
|
static get(name, extend = {}) {
|
|
1169
229
|
switch(name){
|
|
@@ -1184,4 +244,5 @@ class Handler {
|
|
|
1184
244
|
throw new Error("Handler.create() must be implemented by subclass");
|
|
1185
245
|
}
|
|
1186
246
|
}
|
|
1187
|
-
export { Biome, Command, ConfigSearch,
|
|
247
|
+
export { Biome, Command, ConfigSearch, EntryExtractor, Filter, ImportGraph, Markdown, TsDocLinter, TsDocResolver, TypeScript, checkCommand, initCommand, rootCommand, runCli } from "./376.js";
|
|
248
|
+
export { Handler, PackageJson, PnpmWorkspace, Preset, ShellScripts, Yaml, createConfig };
|