@savvy-web/lint-staged 0.6.6 → 0.7.1
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/878.js +516 -1344
- package/README.md +22 -8
- package/biome/silk.jsonc +2 -1
- package/index.d.ts +19 -130
- package/index.js +713 -4
- package/package.json +6 -5
package/878.js
CHANGED
|
@@ -1,1005 +1,88 @@
|
|
|
1
1
|
import { Args, Command, Options } from "@effect/cli";
|
|
2
2
|
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
3
|
-
import {
|
|
3
|
+
import { BiomeSchemaSync, BiomeSchemaSyncLive, CheckResult, ConfigDiscovery, ConfigDiscoveryLive, ManagedSection, ManagedSectionLive, SectionDefinition, ShellSectionDefinition, SyncResult, ToolDefinition, ToolDiscovery, ToolDiscoveryLive } from "@savvy-web/silk-effects";
|
|
4
|
+
import { Effect, Layer } from "effect";
|
|
5
|
+
import { PackageManagerDetectorLive, WorkspaceRootLive } from "workspaces-effect";
|
|
4
6
|
import { isDeepStrictEqual } from "node:util";
|
|
5
7
|
import { FileSystem } from "@effect/platform";
|
|
6
8
|
import { applyEdits, modify, parse } from "jsonc-effect";
|
|
7
|
-
import { execSync } from "node:child_process";
|
|
8
9
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { dirname, isAbsolute, join, normalize, relative, resolve } from "node:path";
|
|
10
|
-
import { findProjectRoot, getWorkspaceInfos } from "workspace-tools";
|
|
11
|
-
import { cosmiconfigSync, defaultLoaders } from "cosmiconfig";
|
|
12
|
-
import parser from "@typescript-eslint/parser";
|
|
13
|
-
import { ESLint } from "eslint";
|
|
14
|
-
import eslint_plugin_tsdoc from "eslint-plugin-tsdoc";
|
|
15
|
-
import typescript from "typescript";
|
|
16
10
|
import sort_package_json from "sort-package-json";
|
|
17
11
|
import { parse as external_yaml_parse, stringify } from "yaml";
|
|
12
|
+
import { execSync } from "node:child_process";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
18
14
|
import { format, resolveConfig } from "prettier";
|
|
19
15
|
import { lint } from "yaml-lint";
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
static getExecPrefix(packageManager) {
|
|
61
|
-
switch(packageManager){
|
|
62
|
-
case "pnpm":
|
|
63
|
-
return [
|
|
64
|
-
"pnpm",
|
|
65
|
-
"exec"
|
|
66
|
-
];
|
|
67
|
-
case "yarn":
|
|
68
|
-
return [
|
|
69
|
-
"yarn",
|
|
70
|
-
"exec"
|
|
71
|
-
];
|
|
72
|
-
case "bun":
|
|
73
|
-
return [
|
|
74
|
-
"bun",
|
|
75
|
-
"x",
|
|
76
|
-
"--no-install"
|
|
77
|
-
];
|
|
78
|
-
default:
|
|
79
|
-
return [
|
|
80
|
-
"npx",
|
|
81
|
-
"--no"
|
|
82
|
-
];
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
static clearCache() {
|
|
86
|
-
Command_Command.cachedPackageManager = null;
|
|
87
|
-
Command_Command.cachedRoot = null;
|
|
88
|
-
}
|
|
89
|
-
static isAvailable(command) {
|
|
90
|
-
validateCommandName(command);
|
|
91
|
-
try {
|
|
92
|
-
execSync(`command -v ${command}`, {
|
|
93
|
-
stdio: "ignore"
|
|
94
|
-
});
|
|
95
|
-
return true;
|
|
96
|
-
} catch {
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
static findTool(tool) {
|
|
101
|
-
validateCommandName(tool);
|
|
102
|
-
if (Command_Command.isAvailable(tool)) return {
|
|
103
|
-
available: true,
|
|
104
|
-
command: tool,
|
|
105
|
-
source: "global"
|
|
106
|
-
};
|
|
107
|
-
const pm = Command_Command.detectPackageManager();
|
|
108
|
-
const prefix = Command_Command.getExecPrefix(pm);
|
|
109
|
-
const execCmd = [
|
|
110
|
-
...prefix,
|
|
111
|
-
tool
|
|
112
|
-
].join(" ");
|
|
113
|
-
try {
|
|
114
|
-
execSync(`${execCmd} --version`, {
|
|
115
|
-
stdio: "ignore"
|
|
116
|
-
});
|
|
117
|
-
return {
|
|
118
|
-
available: true,
|
|
119
|
-
command: execCmd,
|
|
120
|
-
source: pm
|
|
121
|
-
};
|
|
122
|
-
} catch {}
|
|
123
|
-
return {
|
|
124
|
-
available: false,
|
|
125
|
-
command: void 0,
|
|
126
|
-
source: void 0
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
static requireTool(tool, errorMessage) {
|
|
130
|
-
const result = Command_Command.findTool(tool);
|
|
131
|
-
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.`);
|
|
132
|
-
return result.command;
|
|
133
|
-
}
|
|
134
|
-
static findSavvyLint() {
|
|
135
|
-
const result = Command_Command.findTool("savvy-lint");
|
|
136
|
-
if (result.available && result.command) return result.command;
|
|
137
|
-
const root = Command_Command.findRoot();
|
|
138
|
-
return `node ${root}/dist/dev/bin/savvy-lint.js`;
|
|
139
|
-
}
|
|
140
|
-
static exec(command) {
|
|
141
|
-
return execSync(command, {
|
|
142
|
-
encoding: "utf-8"
|
|
143
|
-
}).trim();
|
|
144
|
-
}
|
|
145
|
-
static execSilent(command) {
|
|
146
|
-
try {
|
|
147
|
-
execSync(command, {
|
|
148
|
-
stdio: "ignore"
|
|
149
|
-
});
|
|
150
|
-
return true;
|
|
151
|
-
} catch {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
const TOOL_CONFIGS = {
|
|
157
|
-
markdownlint: {
|
|
158
|
-
moduleName: "markdownlint-cli2",
|
|
159
|
-
libConfigFiles: [
|
|
160
|
-
".markdownlint-cli2.jsonc",
|
|
161
|
-
".markdownlint-cli2.json",
|
|
162
|
-
".markdownlint-cli2.yaml",
|
|
163
|
-
".markdownlint-cli2.cjs",
|
|
164
|
-
".markdownlint.jsonc",
|
|
165
|
-
".markdownlint.json",
|
|
166
|
-
".markdownlint.yaml"
|
|
167
|
-
],
|
|
168
|
-
standardPlaces: [
|
|
169
|
-
".markdownlint-cli2.jsonc",
|
|
170
|
-
".markdownlint-cli2.json",
|
|
171
|
-
".markdownlint-cli2.yaml",
|
|
172
|
-
".markdownlint-cli2.cjs",
|
|
173
|
-
".markdownlint.jsonc",
|
|
174
|
-
".markdownlint.json",
|
|
175
|
-
".markdownlint.yaml"
|
|
176
|
-
]
|
|
177
|
-
},
|
|
178
|
-
biome: {
|
|
179
|
-
moduleName: "biome",
|
|
180
|
-
libConfigFiles: [
|
|
181
|
-
"biome.jsonc",
|
|
182
|
-
"biome.json"
|
|
183
|
-
],
|
|
184
|
-
standardPlaces: [
|
|
185
|
-
"biome.jsonc",
|
|
186
|
-
"biome.json"
|
|
187
|
-
]
|
|
188
|
-
},
|
|
189
|
-
eslint: {
|
|
190
|
-
moduleName: "eslint",
|
|
191
|
-
libConfigFiles: [
|
|
192
|
-
"eslint.config.ts",
|
|
193
|
-
"eslint.config.js",
|
|
194
|
-
"eslint.config.mjs"
|
|
195
|
-
],
|
|
196
|
-
standardPlaces: [
|
|
197
|
-
"eslint.config.ts",
|
|
198
|
-
"eslint.config.js",
|
|
199
|
-
"eslint.config.mjs"
|
|
200
|
-
]
|
|
201
|
-
},
|
|
202
|
-
prettier: {
|
|
203
|
-
moduleName: "prettier",
|
|
204
|
-
libConfigFiles: [
|
|
205
|
-
".prettierrc",
|
|
206
|
-
".prettierrc.json",
|
|
207
|
-
".prettierrc.yaml",
|
|
208
|
-
".prettierrc.js",
|
|
209
|
-
"prettier.config.js"
|
|
210
|
-
],
|
|
211
|
-
standardPlaces: [
|
|
212
|
-
".prettierrc",
|
|
213
|
-
".prettierrc.json",
|
|
214
|
-
".prettierrc.yaml",
|
|
215
|
-
".prettierrc.js",
|
|
216
|
-
"prettier.config.js",
|
|
217
|
-
"package.json"
|
|
218
|
-
]
|
|
219
|
-
},
|
|
220
|
-
yamllint: {
|
|
221
|
-
moduleName: "yaml-lint",
|
|
222
|
-
libConfigFiles: [
|
|
223
|
-
".yaml-lint.json"
|
|
224
|
-
],
|
|
225
|
-
standardPlaces: [
|
|
226
|
-
".yaml-lint.json"
|
|
227
|
-
]
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
class ConfigSearch {
|
|
231
|
-
static libConfigDir = "lib/configs";
|
|
232
|
-
static find(tool, options = {}) {
|
|
233
|
-
const config = TOOL_CONFIGS[tool];
|
|
234
|
-
if (!config) return {
|
|
235
|
-
filepath: void 0,
|
|
236
|
-
found: false
|
|
237
|
-
};
|
|
238
|
-
return ConfigSearch.findFile(config.moduleName, {
|
|
239
|
-
libConfigFiles: config.libConfigFiles,
|
|
240
|
-
standardPlaces: config.standardPlaces,
|
|
241
|
-
...options
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
static findFile(moduleName, options = {}) {
|
|
245
|
-
const { searchFrom = process.cwd(), stopDir, libConfigFiles = [], standardPlaces = [] } = options;
|
|
246
|
-
const loaders = {
|
|
247
|
-
".jsonc": defaultLoaders[".json"],
|
|
248
|
-
".yaml": defaultLoaders[".yaml"],
|
|
249
|
-
".yml": defaultLoaders[".yaml"]
|
|
250
|
-
};
|
|
251
|
-
const libConfigDir = join(searchFrom, ConfigSearch.libConfigDir);
|
|
252
|
-
for (const file of libConfigFiles){
|
|
253
|
-
const filepath = join(libConfigDir, file);
|
|
254
|
-
if (existsSync(filepath)) return {
|
|
255
|
-
filepath,
|
|
256
|
-
found: true
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
if (0 === standardPlaces.length) return {
|
|
260
|
-
filepath: void 0,
|
|
261
|
-
found: false
|
|
262
|
-
};
|
|
263
|
-
try {
|
|
264
|
-
const explorer = cosmiconfigSync(moduleName, {
|
|
265
|
-
searchPlaces: standardPlaces,
|
|
266
|
-
loaders,
|
|
267
|
-
...void 0 !== stopDir && {
|
|
268
|
-
stopDir
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
const result = explorer.search(searchFrom);
|
|
272
|
-
if (result?.filepath) return {
|
|
273
|
-
filepath: result.filepath,
|
|
274
|
-
found: true
|
|
275
|
-
};
|
|
276
|
-
} catch {}
|
|
277
|
-
return {
|
|
278
|
-
filepath: void 0,
|
|
279
|
-
found: false
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
static exists(filepath) {
|
|
283
|
-
return existsSync(filepath);
|
|
284
|
-
}
|
|
285
|
-
static resolve(filename, fallback) {
|
|
286
|
-
const libPath = `${ConfigSearch.libConfigDir}/${filename}`;
|
|
287
|
-
if (ConfigSearch.exists(libPath)) return libPath;
|
|
288
|
-
return fallback;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
class Filter {
|
|
292
|
-
static exclude(filenames, patterns) {
|
|
293
|
-
if (0 === patterns.length) return [
|
|
294
|
-
...filenames
|
|
295
|
-
];
|
|
296
|
-
return filenames.filter((file)=>!patterns.some((pattern)=>file.includes(pattern)));
|
|
297
|
-
}
|
|
298
|
-
static include(filenames, patterns) {
|
|
299
|
-
if (0 === patterns.length) return [];
|
|
300
|
-
return filenames.filter((file)=>patterns.some((pattern)=>file.includes(pattern)));
|
|
301
|
-
}
|
|
302
|
-
static apply(filenames, options) {
|
|
303
|
-
let result = [
|
|
304
|
-
...filenames
|
|
305
|
-
];
|
|
306
|
-
if (options.include && options.include.length > 0) result = Filter.include(result, options.include);
|
|
307
|
-
if (options.exclude && options.exclude.length > 0) result = Filter.exclude(result, options.exclude);
|
|
308
|
-
return result;
|
|
309
|
-
}
|
|
310
|
-
static shellEscape(filenames) {
|
|
311
|
-
return filenames.map((f)=>`'${f.replace(/'/g, "'\\''")}'`).join(" ");
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
class Biome {
|
|
315
|
-
static glob = "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}";
|
|
316
|
-
static defaultExcludes = [
|
|
317
|
-
"package.json",
|
|
318
|
-
"package-lock.json",
|
|
319
|
-
"__fixtures__",
|
|
320
|
-
"__test__/fixtures"
|
|
321
|
-
];
|
|
322
|
-
static handler = Biome.create();
|
|
323
|
-
static findBiome() {
|
|
324
|
-
const result = Command_Command.findTool("biome");
|
|
325
|
-
return result.command;
|
|
326
|
-
}
|
|
327
|
-
static isAvailable() {
|
|
328
|
-
return Command_Command.findTool("biome").available;
|
|
329
|
-
}
|
|
330
|
-
static findConfig() {
|
|
331
|
-
const result = ConfigSearch.find("biome");
|
|
332
|
-
return result.filepath;
|
|
333
|
-
}
|
|
334
|
-
static create(options = {}) {
|
|
335
|
-
const excludes = options.exclude ?? [
|
|
336
|
-
...Biome.defaultExcludes
|
|
337
|
-
];
|
|
338
|
-
const config = options.config ?? Biome.findConfig();
|
|
339
|
-
return (filenames)=>{
|
|
340
|
-
const filtered = Filter.exclude(filenames, excludes);
|
|
341
|
-
if (0 === filtered.length) return [];
|
|
342
|
-
const biomeCmd = Command_Command.requireTool("biome", "Biome is not available. Install it globally (recommended) or add @biomejs/biome as a dev dependency.");
|
|
343
|
-
const files = Filter.shellEscape(filtered);
|
|
344
|
-
const flags = options.flags ?? [];
|
|
345
|
-
const configFlag = config ? `--config-path=${config}` : "";
|
|
346
|
-
const cmd = [
|
|
347
|
-
`${biomeCmd} check --write --no-errors-on-unmatched`,
|
|
348
|
-
configFlag,
|
|
349
|
-
...flags,
|
|
350
|
-
files
|
|
351
|
-
].filter(Boolean).join(" ");
|
|
352
|
-
return cmd;
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
class Markdown {
|
|
357
|
-
static glob = "**/*.{md,mdx}";
|
|
358
|
-
static defaultExcludes = [];
|
|
359
|
-
static handler = Markdown.create();
|
|
360
|
-
static findMarkdownlint() {
|
|
361
|
-
const result = Command_Command.findTool("markdownlint-cli2");
|
|
362
|
-
return result.command;
|
|
363
|
-
}
|
|
364
|
-
static isAvailable() {
|
|
365
|
-
return Command_Command.findTool("markdownlint-cli2").available;
|
|
366
|
-
}
|
|
367
|
-
static findConfig() {
|
|
368
|
-
const result = ConfigSearch.find("markdownlint");
|
|
369
|
-
return result.filepath;
|
|
370
|
-
}
|
|
371
|
-
static create(options = {}) {
|
|
372
|
-
const excludes = options.exclude ?? [
|
|
373
|
-
...Markdown.defaultExcludes
|
|
374
|
-
];
|
|
375
|
-
const noFix = options.noFix ?? false;
|
|
376
|
-
const config = options.config ?? Markdown.findConfig();
|
|
377
|
-
return (filenames)=>{
|
|
378
|
-
const filtered = Filter.exclude(filenames, excludes);
|
|
379
|
-
if (0 === filtered.length) return [];
|
|
380
|
-
const mdlintCmd = Command_Command.requireTool("markdownlint-cli2", "markdownlint-cli2 is not available. Install it globally or add it as a dev dependency.");
|
|
381
|
-
const files = Filter.shellEscape(filtered);
|
|
382
|
-
const fixFlag = noFix ? "" : "--fix";
|
|
383
|
-
const configFlag = config ? `--config '${config}'` : "";
|
|
384
|
-
const cmd = [
|
|
385
|
-
mdlintCmd,
|
|
386
|
-
configFlag,
|
|
387
|
-
fixFlag,
|
|
388
|
-
files
|
|
389
|
-
].filter(Boolean).join(" ");
|
|
390
|
-
return cmd;
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
class TsDocLinter {
|
|
395
|
-
eslint;
|
|
396
|
-
constructor(options = {}){
|
|
397
|
-
const ignorePatterns = options.ignorePatterns ?? [];
|
|
398
|
-
const config = [
|
|
399
|
-
{
|
|
400
|
-
ignores: [
|
|
401
|
-
"**/node_modules/**",
|
|
402
|
-
"**/dist/**",
|
|
403
|
-
"**/coverage/**",
|
|
404
|
-
...ignorePatterns
|
|
405
|
-
]
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
files: [
|
|
409
|
-
"**/*.ts",
|
|
410
|
-
"**/*.tsx",
|
|
411
|
-
"**/*.mts",
|
|
412
|
-
"**/*.cts"
|
|
413
|
-
],
|
|
414
|
-
languageOptions: {
|
|
415
|
-
parser: parser
|
|
416
|
-
},
|
|
417
|
-
plugins: {
|
|
418
|
-
tsdoc: eslint_plugin_tsdoc
|
|
419
|
-
},
|
|
420
|
-
rules: {
|
|
421
|
-
"tsdoc/syntax": "error"
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
];
|
|
425
|
-
this.eslint = new ESLint({
|
|
426
|
-
overrideConfigFile: true,
|
|
427
|
-
overrideConfig: config
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
async lintFiles(filePaths) {
|
|
431
|
-
if (0 === filePaths.length) return [];
|
|
432
|
-
const results = await this.eslint.lintFiles(filePaths);
|
|
433
|
-
return results.map((result)=>({
|
|
434
|
-
filePath: result.filePath,
|
|
435
|
-
errorCount: result.errorCount,
|
|
436
|
-
warningCount: result.warningCount,
|
|
437
|
-
messages: result.messages.map((msg)=>({
|
|
438
|
-
line: msg.line,
|
|
439
|
-
column: msg.column,
|
|
440
|
-
severity: msg.severity,
|
|
441
|
-
message: msg.message,
|
|
442
|
-
ruleId: msg.ruleId
|
|
443
|
-
}))
|
|
444
|
-
}));
|
|
445
|
-
}
|
|
446
|
-
async lintFilesAndThrow(filePaths) {
|
|
447
|
-
const results = await this.lintFiles(filePaths);
|
|
448
|
-
const errors = [];
|
|
449
|
-
for (const result of results)if (result.errorCount > 0) {
|
|
450
|
-
for (const msg of result.messages)if (2 === msg.severity) errors.push(`${result.filePath}:${msg.line}:${msg.column} - ${msg.message}`);
|
|
451
|
-
}
|
|
452
|
-
if (errors.length > 0) throw new Error(`TSDoc validation failed:\n${errors.join("\n")}`);
|
|
453
|
-
}
|
|
454
|
-
static formatResults(results) {
|
|
455
|
-
const lines = [];
|
|
456
|
-
for (const result of results)if (0 !== result.errorCount || 0 !== result.warningCount) {
|
|
457
|
-
lines.push(`\n${result.filePath}`);
|
|
458
|
-
for (const msg of result.messages){
|
|
459
|
-
const severity = 2 === msg.severity ? "error" : "warning";
|
|
460
|
-
const rule = msg.ruleId ? ` (${msg.ruleId})` : "";
|
|
461
|
-
lines.push(` ${msg.line}:${msg.column} ${severity} ${msg.message}${rule}`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
const totalErrors = results.reduce((sum, r)=>sum + r.errorCount, 0);
|
|
465
|
-
const totalWarnings = results.reduce((sum, r)=>sum + r.warningCount, 0);
|
|
466
|
-
if (totalErrors > 0 || totalWarnings > 0) lines.push(`\n✖ ${totalErrors} error(s), ${totalWarnings} warning(s)`);
|
|
467
|
-
return lines.join("\n");
|
|
468
|
-
}
|
|
469
|
-
static hasErrors(results) {
|
|
470
|
-
return results.some((r)=>r.errorCount > 0);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
const TS_EXTENSIONS = [
|
|
474
|
-
".ts",
|
|
475
|
-
".tsx",
|
|
476
|
-
".mts",
|
|
477
|
-
".cts"
|
|
478
|
-
];
|
|
479
|
-
class EntryExtractor {
|
|
480
|
-
extract(packageJson) {
|
|
481
|
-
const entries = {};
|
|
482
|
-
const unresolved = [];
|
|
483
|
-
const { exports } = packageJson;
|
|
484
|
-
if (!exports) {
|
|
485
|
-
const mainEntry = packageJson.module ?? packageJson.main;
|
|
486
|
-
if (mainEntry && this.isTypeScriptFile(mainEntry)) entries["."] = mainEntry;
|
|
487
|
-
else if (mainEntry) unresolved.push(".");
|
|
488
|
-
return {
|
|
489
|
-
entries,
|
|
490
|
-
unresolved
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
if ("string" == typeof exports) {
|
|
494
|
-
if (this.isTypeScriptFile(exports)) entries["."] = exports;
|
|
495
|
-
else unresolved.push(".");
|
|
496
|
-
return {
|
|
497
|
-
entries,
|
|
498
|
-
unresolved
|
|
499
|
-
};
|
|
500
|
-
}
|
|
501
|
-
this.extractFromObject(exports, entries, unresolved, ".");
|
|
502
|
-
return {
|
|
503
|
-
entries,
|
|
504
|
-
unresolved
|
|
505
|
-
};
|
|
506
|
-
}
|
|
507
|
-
extractFromObject(obj, entries, unresolved, currentPath) {
|
|
508
|
-
for (const [key, value] of Object.entries(obj)){
|
|
509
|
-
const exportPath = key.startsWith(".") ? key : currentPath;
|
|
510
|
-
if ("string" == typeof value) {
|
|
511
|
-
if (this.isTypeScriptFile(value)) entries[exportPath] = value;
|
|
512
|
-
else if (key.startsWith(".")) unresolved.push(exportPath);
|
|
513
|
-
} else if (value && "object" == typeof value && !Array.isArray(value)) {
|
|
514
|
-
const nested = value;
|
|
515
|
-
const tsPath = this.findTypeScriptCondition(nested);
|
|
516
|
-
if (tsPath) entries[exportPath] = tsPath;
|
|
517
|
-
else if (key.startsWith(".")) this.extractFromObject(nested, entries, unresolved, exportPath);
|
|
518
|
-
else {
|
|
519
|
-
const sourcePath = this.findSourceCondition(nested);
|
|
520
|
-
if (sourcePath && this.isTypeScriptFile(sourcePath)) entries[exportPath] = sourcePath;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
findTypeScriptCondition(conditions) {
|
|
526
|
-
const priorityKeys = [
|
|
527
|
-
"source",
|
|
528
|
-
"typescript",
|
|
529
|
-
"development",
|
|
530
|
-
"default"
|
|
531
|
-
];
|
|
532
|
-
for (const key of priorityKeys){
|
|
533
|
-
const value = conditions[key];
|
|
534
|
-
if ("string" == typeof value && this.isTypeScriptFile(value)) return value;
|
|
535
|
-
if (value && "object" == typeof value) {
|
|
536
|
-
const nested = this.findTypeScriptCondition(value);
|
|
537
|
-
if (nested) return nested;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
return null;
|
|
541
|
-
}
|
|
542
|
-
findSourceCondition(conditions) {
|
|
543
|
-
const priorityKeys = [
|
|
544
|
-
"source",
|
|
545
|
-
"import",
|
|
546
|
-
"require",
|
|
547
|
-
"default"
|
|
548
|
-
];
|
|
549
|
-
for (const key of priorityKeys){
|
|
550
|
-
const value = conditions[key];
|
|
551
|
-
if ("string" == typeof value) return value;
|
|
552
|
-
if (value && "object" == typeof value) {
|
|
553
|
-
const nested = this.findSourceCondition(value);
|
|
554
|
-
if (nested) return nested;
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
return null;
|
|
558
|
-
}
|
|
559
|
-
isTypeScriptFile(filePath) {
|
|
560
|
-
return TS_EXTENSIONS.some((ext)=>filePath.endsWith(ext));
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
class ImportGraph {
|
|
564
|
-
options;
|
|
565
|
-
program = null;
|
|
566
|
-
compilerOptions = null;
|
|
567
|
-
moduleResolutionCache = null;
|
|
568
|
-
constructor(options){
|
|
569
|
-
this.options = options;
|
|
570
|
-
}
|
|
571
|
-
traceFromEntries(entryPaths) {
|
|
572
|
-
const errors = [];
|
|
573
|
-
const visited = new Set();
|
|
574
|
-
const entries = [];
|
|
575
|
-
const initResult = this.initializeProgram();
|
|
576
|
-
if (!initResult.success) return {
|
|
577
|
-
files: [],
|
|
578
|
-
entries: [],
|
|
579
|
-
errors: [
|
|
580
|
-
initResult.error
|
|
581
|
-
]
|
|
582
|
-
};
|
|
583
|
-
for (const entryPath of entryPaths){
|
|
584
|
-
const absolutePath = this.resolveEntryPath(entryPath);
|
|
585
|
-
if (!existsSync(absolutePath)) {
|
|
586
|
-
errors.push({
|
|
587
|
-
type: "entry_not_found",
|
|
588
|
-
message: `Entry file not found: ${entryPath}`,
|
|
589
|
-
path: absolutePath
|
|
590
|
-
});
|
|
591
|
-
continue;
|
|
592
|
-
}
|
|
593
|
-
entries.push(absolutePath);
|
|
594
|
-
this.traceImports(absolutePath, visited, errors);
|
|
595
|
-
}
|
|
596
|
-
const files = Array.from(visited).filter((file)=>this.isSourceFile(file));
|
|
597
|
-
return {
|
|
598
|
-
files: files.sort(),
|
|
599
|
-
entries,
|
|
600
|
-
errors
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
traceFromPackageExports(packageJsonPath) {
|
|
604
|
-
const absolutePath = this.resolveEntryPath(packageJsonPath);
|
|
605
|
-
let packageJson;
|
|
606
|
-
try {
|
|
607
|
-
if (!existsSync(absolutePath)) return {
|
|
608
|
-
files: [],
|
|
609
|
-
entries: [],
|
|
610
|
-
errors: [
|
|
611
|
-
{
|
|
612
|
-
type: "package_json_not_found",
|
|
613
|
-
message: `Failed to read package.json: File not found at ${absolutePath}`,
|
|
614
|
-
path: absolutePath
|
|
615
|
-
}
|
|
616
|
-
]
|
|
617
|
-
};
|
|
618
|
-
const content = readFileSync(absolutePath, "utf-8");
|
|
619
|
-
packageJson = JSON.parse(content);
|
|
620
|
-
} catch (error) {
|
|
621
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
622
|
-
return {
|
|
623
|
-
files: [],
|
|
624
|
-
entries: [],
|
|
625
|
-
errors: [
|
|
626
|
-
{
|
|
627
|
-
type: "package_json_parse_error",
|
|
628
|
-
message: `Failed to parse package.json: ${message}`,
|
|
629
|
-
path: absolutePath
|
|
630
|
-
}
|
|
631
|
-
]
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
const extractor = new EntryExtractor();
|
|
635
|
-
const { entries } = extractor.extract(packageJson);
|
|
636
|
-
const packageDir = dirname(absolutePath);
|
|
637
|
-
const entryPaths = Object.values(entries).map((p)=>resolve(packageDir, p));
|
|
638
|
-
return this.traceFromEntries(entryPaths);
|
|
639
|
-
}
|
|
640
|
-
initializeProgram() {
|
|
641
|
-
if (this.program) return {
|
|
642
|
-
success: true
|
|
643
|
-
};
|
|
644
|
-
const configPath = this.findTsConfig();
|
|
645
|
-
if (!configPath) {
|
|
646
|
-
this.compilerOptions = {
|
|
647
|
-
moduleResolution: typescript.ModuleResolutionKind.NodeNext,
|
|
648
|
-
module: typescript.ModuleKind.NodeNext,
|
|
649
|
-
target: typescript.ScriptTarget.ESNext,
|
|
650
|
-
strict: true
|
|
651
|
-
};
|
|
652
|
-
this.moduleResolutionCache = typescript.createModuleResolutionCache(this.options.rootDir, (fileName)=>fileName.toLowerCase(), this.compilerOptions);
|
|
653
|
-
const host = typescript.createCompilerHost(this.compilerOptions, true);
|
|
654
|
-
host.getCurrentDirectory = ()=>this.options.rootDir;
|
|
655
|
-
this.program = typescript.createProgram([], this.compilerOptions, host);
|
|
656
|
-
return {
|
|
657
|
-
success: true
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
const configFile = typescript.readConfigFile(configPath, (path)=>readFileSync(path, "utf-8"));
|
|
661
|
-
if (configFile.error) {
|
|
662
|
-
const message = typescript.flattenDiagnosticMessageText(configFile.error.messageText, "\n");
|
|
663
|
-
return {
|
|
664
|
-
success: false,
|
|
665
|
-
error: {
|
|
666
|
-
type: "tsconfig_read_error",
|
|
667
|
-
message: `Failed to read tsconfig.json: ${message}`,
|
|
668
|
-
path: configPath
|
|
669
|
-
}
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
const parsed = typescript.parseJsonConfigFileContent(configFile.config, typescript.sys, dirname(configPath));
|
|
673
|
-
if (parsed.errors.length > 0) {
|
|
674
|
-
const messages = parsed.errors.map((e)=>typescript.flattenDiagnosticMessageText(e.messageText, "\n")).join("\n");
|
|
675
|
-
return {
|
|
676
|
-
success: false,
|
|
677
|
-
error: {
|
|
678
|
-
type: "tsconfig_parse_error",
|
|
679
|
-
message: `Failed to parse tsconfig.json: ${messages}`,
|
|
680
|
-
path: configPath
|
|
681
|
-
}
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
this.compilerOptions = parsed.options;
|
|
685
|
-
this.moduleResolutionCache = typescript.createModuleResolutionCache(this.options.rootDir, (fileName)=>fileName.toLowerCase(), this.compilerOptions);
|
|
686
|
-
const host = typescript.createCompilerHost(this.compilerOptions, true);
|
|
687
|
-
host.getCurrentDirectory = ()=>this.options.rootDir;
|
|
688
|
-
this.program = typescript.createProgram([], this.compilerOptions, host);
|
|
689
|
-
return {
|
|
690
|
-
success: true
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
findTsConfig() {
|
|
694
|
-
if (this.options.tsconfigPath) {
|
|
695
|
-
const customPath = isAbsolute(this.options.tsconfigPath) ? this.options.tsconfigPath : resolve(this.options.rootDir, this.options.tsconfigPath);
|
|
696
|
-
if (existsSync(customPath)) return customPath;
|
|
697
|
-
return null;
|
|
698
|
-
}
|
|
699
|
-
const configPath = typescript.findConfigFile(this.options.rootDir, (path)=>existsSync(path));
|
|
700
|
-
return configPath ?? null;
|
|
701
|
-
}
|
|
702
|
-
resolveEntryPath(entryPath) {
|
|
703
|
-
if (isAbsolute(entryPath)) return normalize(entryPath);
|
|
704
|
-
return normalize(resolve(this.options.rootDir, entryPath));
|
|
705
|
-
}
|
|
706
|
-
traceImports(filePath, visited, errors) {
|
|
707
|
-
const normalizedPath = normalize(filePath);
|
|
708
|
-
if (visited.has(normalizedPath)) return;
|
|
709
|
-
if (this.isExternalModule(normalizedPath)) return;
|
|
710
|
-
visited.add(normalizedPath);
|
|
711
|
-
let content;
|
|
712
|
-
try {
|
|
713
|
-
content = readFileSync(normalizedPath, "utf-8");
|
|
714
|
-
} catch {
|
|
715
|
-
errors.push({
|
|
716
|
-
type: "file_read_error",
|
|
717
|
-
message: `Failed to read file: ${normalizedPath}`,
|
|
718
|
-
path: normalizedPath
|
|
719
|
-
});
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
const sourceFile = typescript.createSourceFile(normalizedPath, content, typescript.ScriptTarget.Latest, true);
|
|
723
|
-
const imports = this.extractImports(sourceFile);
|
|
724
|
-
for (const importPath of imports){
|
|
725
|
-
const resolved = this.resolveImport(importPath, normalizedPath);
|
|
726
|
-
if (resolved) this.traceImports(resolved, visited, errors);
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
extractImports(sourceFile) {
|
|
730
|
-
const imports = [];
|
|
731
|
-
const visit = (node)=>{
|
|
732
|
-
if (typescript.isImportDeclaration(node)) {
|
|
733
|
-
const specifier = node.moduleSpecifier;
|
|
734
|
-
if (typescript.isStringLiteral(specifier)) imports.push(specifier.text);
|
|
735
|
-
} else if (typescript.isExportDeclaration(node)) {
|
|
736
|
-
const specifier = node.moduleSpecifier;
|
|
737
|
-
if (specifier && typescript.isStringLiteral(specifier)) imports.push(specifier.text);
|
|
738
|
-
} else if (typescript.isCallExpression(node)) {
|
|
739
|
-
const expression = node.expression;
|
|
740
|
-
if (expression.kind === typescript.SyntaxKind.ImportKeyword && node.arguments.length > 0) {
|
|
741
|
-
const arg = node.arguments[0];
|
|
742
|
-
if (arg && typescript.isStringLiteral(arg)) imports.push(arg.text);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
typescript.forEachChild(node, visit);
|
|
746
|
-
};
|
|
747
|
-
visit(sourceFile);
|
|
748
|
-
return imports;
|
|
749
|
-
}
|
|
750
|
-
resolveImport(specifier, fromFile) {
|
|
751
|
-
if (!specifier.startsWith(".") && !specifier.startsWith("/")) {
|
|
752
|
-
if (!this.compilerOptions?.paths || !Object.keys(this.compilerOptions.paths).length) return null;
|
|
753
|
-
}
|
|
754
|
-
if (!this.compilerOptions || !this.moduleResolutionCache) return null;
|
|
755
|
-
const resolved = typescript.resolveModuleName(specifier, fromFile, this.compilerOptions, typescript.sys, this.moduleResolutionCache);
|
|
756
|
-
if (resolved.resolvedModule) {
|
|
757
|
-
const resolvedPath = resolved.resolvedModule.resolvedFileName;
|
|
758
|
-
if (resolved.resolvedModule.isExternalLibraryImport) return null;
|
|
759
|
-
if (resolvedPath.endsWith(".d.ts")) {
|
|
760
|
-
const sourcePath = resolvedPath.replace(/\.d\.ts$/, ".ts");
|
|
761
|
-
if (existsSync(sourcePath)) return sourcePath;
|
|
762
|
-
return null;
|
|
763
|
-
}
|
|
764
|
-
return resolvedPath;
|
|
765
|
-
}
|
|
766
|
-
return null;
|
|
767
|
-
}
|
|
768
|
-
isExternalModule(filePath) {
|
|
769
|
-
return filePath.includes("/node_modules/") || filePath.includes("\\node_modules\\");
|
|
770
|
-
}
|
|
771
|
-
isSourceFile(filePath) {
|
|
772
|
-
if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx") && !filePath.endsWith(".mts") && !filePath.endsWith(".cts")) return false;
|
|
773
|
-
if (filePath.endsWith(".d.ts") || filePath.endsWith(".d.mts") || filePath.endsWith(".d.cts")) return false;
|
|
774
|
-
if (filePath.includes(".test.") || filePath.includes(".spec.")) return false;
|
|
775
|
-
if (filePath.includes("/__test__/") || filePath.includes("\\__test__\\")) return false;
|
|
776
|
-
if (filePath.includes("/__tests__/") || filePath.includes("\\__tests__\\")) return false;
|
|
777
|
-
const excludePatterns = this.options.excludePatterns ?? [];
|
|
778
|
-
for (const pattern of excludePatterns)if (filePath.includes(pattern)) return false;
|
|
779
|
-
return true;
|
|
780
|
-
}
|
|
781
|
-
static fromEntries(entryPaths, options) {
|
|
782
|
-
const graph = new ImportGraph(options);
|
|
783
|
-
return graph.traceFromEntries(entryPaths);
|
|
784
|
-
}
|
|
785
|
-
static fromPackageExports(packageJsonPath, options) {
|
|
786
|
-
const graph = new ImportGraph(options);
|
|
787
|
-
return graph.traceFromPackageExports(packageJsonPath);
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
class TsDocResolver {
|
|
791
|
-
options;
|
|
792
|
-
constructor(options){
|
|
793
|
-
this.options = options;
|
|
794
|
-
}
|
|
795
|
-
resolve() {
|
|
796
|
-
const { rootDir } = this.options;
|
|
797
|
-
const workspaces = [];
|
|
798
|
-
const repoTsdocPath = join(rootDir, "tsdoc.json");
|
|
799
|
-
const repoTsdocConfig = existsSync(repoTsdocPath) ? repoTsdocPath : void 0;
|
|
800
|
-
const workspaceInfos = getWorkspaceInfos(rootDir);
|
|
801
|
-
const isMonorepo = void 0 !== workspaceInfos && workspaceInfos.length > 1;
|
|
802
|
-
if (void 0 === workspaceInfos || 0 === workspaceInfos.length) {
|
|
803
|
-
const result = this.resolveWorkspace(rootDir, repoTsdocConfig);
|
|
804
|
-
if (result) workspaces.push(result);
|
|
805
|
-
} else for (const info of workspaceInfos){
|
|
806
|
-
const workspacePath = info.path;
|
|
807
|
-
const result = this.resolveWorkspace(workspacePath, repoTsdocConfig);
|
|
808
|
-
if (result) workspaces.push(result);
|
|
809
|
-
}
|
|
810
|
-
const result = {
|
|
811
|
-
workspaces,
|
|
812
|
-
isMonorepo
|
|
813
|
-
};
|
|
814
|
-
if (void 0 !== repoTsdocConfig) result.repoTsdocConfig = repoTsdocConfig;
|
|
815
|
-
return result;
|
|
816
|
-
}
|
|
817
|
-
resolveWorkspace(workspacePath, repoTsdocConfig) {
|
|
818
|
-
const packageJsonPath = join(workspacePath, "package.json");
|
|
819
|
-
if (!existsSync(packageJsonPath)) return null;
|
|
820
|
-
let packageJson;
|
|
821
|
-
try {
|
|
822
|
-
const content = readFileSync(packageJsonPath, "utf-8");
|
|
823
|
-
packageJson = JSON.parse(content);
|
|
824
|
-
} catch {
|
|
825
|
-
return null;
|
|
826
|
-
}
|
|
827
|
-
const workspaceTsdocPath = join(workspacePath, "tsdoc.json");
|
|
828
|
-
const workspaceTsdocConfig = existsSync(workspaceTsdocPath) ? workspaceTsdocPath : void 0;
|
|
829
|
-
const tsdocConfigPath = workspaceTsdocConfig ?? repoTsdocConfig;
|
|
830
|
-
if (!tsdocConfigPath) return null;
|
|
831
|
-
if (!packageJson.exports) return null;
|
|
832
|
-
const name = packageJson.name ?? relative(this.options.rootDir, workspacePath);
|
|
833
|
-
const errors = [];
|
|
834
|
-
const graphOptions = {
|
|
835
|
-
rootDir: workspacePath
|
|
836
|
-
};
|
|
837
|
-
if (void 0 !== this.options.excludePatterns) graphOptions.excludePatterns = this.options.excludePatterns;
|
|
838
|
-
const graph = new ImportGraph(graphOptions);
|
|
839
|
-
const result = graph.traceFromPackageExports(packageJsonPath);
|
|
840
|
-
for (const error of result.errors)errors.push(error.message);
|
|
841
|
-
return {
|
|
842
|
-
name,
|
|
843
|
-
path: workspacePath,
|
|
844
|
-
tsdocConfigPath,
|
|
845
|
-
files: result.files,
|
|
846
|
-
errors
|
|
847
|
-
};
|
|
848
|
-
}
|
|
849
|
-
filterStagedFiles(stagedFiles) {
|
|
850
|
-
const result = this.resolve();
|
|
851
|
-
const output = [];
|
|
852
|
-
for (const workspace of result.workspaces){
|
|
853
|
-
const workspaceFiles = new Set(workspace.files);
|
|
854
|
-
const matchedFiles = stagedFiles.filter((f)=>workspaceFiles.has(f));
|
|
855
|
-
if (matchedFiles.length > 0) output.push({
|
|
856
|
-
files: matchedFiles,
|
|
857
|
-
tsdocConfigPath: workspace.tsdocConfigPath
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
return output;
|
|
861
|
-
}
|
|
862
|
-
needsLinting(filePath) {
|
|
863
|
-
const result = this.resolve();
|
|
864
|
-
for (const workspace of result.workspaces)if (workspace.files.includes(filePath)) return true;
|
|
865
|
-
return false;
|
|
866
|
-
}
|
|
867
|
-
getTsDocConfig(filePath) {
|
|
868
|
-
const result = this.resolve();
|
|
869
|
-
for (const workspace of result.workspaces)if (workspace.files.includes(filePath)) return workspace.tsdocConfigPath;
|
|
870
|
-
}
|
|
871
|
-
findWorkspace(filePath) {
|
|
872
|
-
const result = this.resolve();
|
|
873
|
-
for (const workspace of result.workspaces)if (filePath.startsWith(workspace.path)) return workspace;
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
class TypeScript {
|
|
877
|
-
static glob = "*.{ts,cts,mts,tsx}";
|
|
878
|
-
static defaultExcludes = [];
|
|
879
|
-
static defaultTsdocExcludes = [
|
|
880
|
-
".test.",
|
|
881
|
-
".spec.",
|
|
882
|
-
"__test__",
|
|
883
|
-
"__tests__"
|
|
884
|
-
];
|
|
885
|
-
static cachedCompilerResult = null;
|
|
886
|
-
static detectCompiler(_cwd) {
|
|
887
|
-
if (null !== TypeScript.cachedCompilerResult) return TypeScript.cachedCompilerResult.compiler;
|
|
888
|
-
const tsgo = Command_Command.findTool("tsgo");
|
|
889
|
-
if (tsgo.available) {
|
|
890
|
-
TypeScript.cachedCompilerResult = {
|
|
891
|
-
compiler: "tsgo",
|
|
892
|
-
tool: tsgo
|
|
893
|
-
};
|
|
894
|
-
return "tsgo";
|
|
895
|
-
}
|
|
896
|
-
const tsc = Command_Command.findTool("tsc");
|
|
897
|
-
if (tsc.available) {
|
|
898
|
-
TypeScript.cachedCompilerResult = {
|
|
899
|
-
compiler: "tsc",
|
|
900
|
-
tool: tsc
|
|
901
|
-
};
|
|
902
|
-
return "tsc";
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
static isAvailable() {
|
|
906
|
-
return void 0 !== TypeScript.detectCompiler();
|
|
907
|
-
}
|
|
908
|
-
static getDefaultTypecheckCommand() {
|
|
909
|
-
const compiler = TypeScript.detectCompiler();
|
|
910
|
-
if (!compiler || !TypeScript.cachedCompilerResult) throw new Error("No TypeScript compiler found. Install 'typescript' or '@typescript/native-preview' as a dev dependency.");
|
|
911
|
-
return `${TypeScript.cachedCompilerResult.tool.command} --noEmit`;
|
|
912
|
-
}
|
|
913
|
-
static clearCache() {
|
|
914
|
-
TypeScript.cachedCompilerResult = null;
|
|
915
|
-
}
|
|
916
|
-
static handler = TypeScript.create();
|
|
917
|
-
static isTsdocAvailable(cwd = Command_Command.findRoot()) {
|
|
918
|
-
const tsdocPath = join(cwd, "tsdoc.json");
|
|
919
|
-
return existsSync(tsdocPath);
|
|
920
|
-
}
|
|
921
|
-
static create(options = {}) {
|
|
922
|
-
const excludes = options.exclude ?? [
|
|
923
|
-
...TypeScript.defaultExcludes
|
|
924
|
-
];
|
|
925
|
-
const tsdocExcludes = options.excludeTsdoc ?? [
|
|
926
|
-
...TypeScript.defaultTsdocExcludes
|
|
927
|
-
];
|
|
928
|
-
const skipTsdoc = options.skipTsdoc ?? false;
|
|
929
|
-
const skipTypecheck = options.skipTypecheck ?? false;
|
|
930
|
-
const rootDir = options.rootDir ?? Command_Command.findRoot();
|
|
931
|
-
let typecheckCommand;
|
|
932
|
-
const getTypecheckCommand = ()=>{
|
|
933
|
-
if (void 0 === typecheckCommand) typecheckCommand = options.typecheckCommand ?? TypeScript.getDefaultTypecheckCommand();
|
|
934
|
-
return typecheckCommand;
|
|
935
|
-
};
|
|
936
|
-
return async (filenames)=>{
|
|
937
|
-
const filtered = Filter.exclude(filenames, excludes);
|
|
938
|
-
if (0 === filtered.length) return [];
|
|
939
|
-
const commands = [];
|
|
940
|
-
if (!skipTsdoc) {
|
|
941
|
-
const resolver = new TsDocResolver({
|
|
942
|
-
rootDir,
|
|
943
|
-
excludePatterns: [
|
|
944
|
-
...tsdocExcludes
|
|
945
|
-
]
|
|
946
|
-
});
|
|
947
|
-
const absoluteFiles = filtered.map((f)=>isAbsolute(f) ? f : join(rootDir, f));
|
|
948
|
-
const tsdocGroups = resolver.filterStagedFiles(absoluteFiles);
|
|
949
|
-
for (const group of tsdocGroups)if (group.files.length > 0) {
|
|
950
|
-
const linter = new TsDocLinter({
|
|
951
|
-
ignorePatterns: tsdocExcludes.map((p)=>`**/*${p}*`)
|
|
952
|
-
});
|
|
953
|
-
const results = await linter.lintFiles(group.files);
|
|
954
|
-
if (TsDocLinter.hasErrors(results)) {
|
|
955
|
-
const output = TsDocLinter.formatResults(results);
|
|
956
|
-
throw new Error(`TSDoc validation failed:\n${output}`);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
if (!skipTypecheck && filtered.length > 0) commands.push(getTypecheckCommand());
|
|
961
|
-
return commands;
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
const SCHEMA_URL_PREFIX = "https://biomejs.dev/schemas/";
|
|
966
|
-
const SCHEMA_URL_SUFFIX = "/schema.json";
|
|
967
|
-
const BIOME_GLOB_PATTERN = "**/biome.{json,jsonc}";
|
|
968
|
-
const BIOME_EXCLUDE_DIRS = [
|
|
969
|
-
"node_modules",
|
|
970
|
-
"dist",
|
|
971
|
-
".turbo",
|
|
972
|
-
".git",
|
|
973
|
-
".rslib"
|
|
974
|
-
];
|
|
975
|
-
function extractSemver(versionRange) {
|
|
976
|
-
return versionRange.replace(/^[^\d]*/, "");
|
|
977
|
-
}
|
|
978
|
-
function buildSchemaUrl(version) {
|
|
979
|
-
return `${SCHEMA_URL_PREFIX}${version}${SCHEMA_URL_SUFFIX}`;
|
|
980
|
-
}
|
|
981
|
-
function getBiomePeerVersion() {
|
|
982
|
-
const raw = "catalog:silk";
|
|
983
|
-
if (!raw) return;
|
|
984
|
-
return extractSemver(raw);
|
|
16
|
+
const HUSKY_HOOK_PATH = ".husky/pre-commit";
|
|
17
|
+
const POST_CHECKOUT_HOOK_PATH = ".husky/post-checkout";
|
|
18
|
+
const POST_MERGE_HOOK_PATH = ".husky/post-merge";
|
|
19
|
+
const DEFAULT_CONFIG_PATH = "lib/configs/lint-staged.config.ts";
|
|
20
|
+
const MARKDOWNLINT_CONFIG_PATH = "lib/configs/.markdownlint-cli2.jsonc";
|
|
21
|
+
const SavvyLintSection = ShellSectionDefinition.make({
|
|
22
|
+
toolName: "SAVVY-LINT"
|
|
23
|
+
});
|
|
24
|
+
const SavvyLintSectionDef = SectionDefinition.make({
|
|
25
|
+
toolName: "SAVVY-LINT"
|
|
26
|
+
});
|
|
27
|
+
function generateManagedContent(configPath) {
|
|
28
|
+
return `# DO NOT EDIT between these markers - managed by savvy-lint
|
|
29
|
+
# Skip in CI environment
|
|
30
|
+
if ! { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; }; then
|
|
31
|
+
|
|
32
|
+
# Get repo root directory
|
|
33
|
+
ROOT=$(git rev-parse --show-toplevel)
|
|
34
|
+
|
|
35
|
+
# Detect package manager from package.json or lockfiles
|
|
36
|
+
detect_pm() {
|
|
37
|
+
# Check packageManager field in package.json (e.g., "pnpm@9.0.0")
|
|
38
|
+
if [ -f "$ROOT/package.json" ]; then
|
|
39
|
+
pm=$(jq -r '.packageManager // empty' "$ROOT/package.json" 2>/dev/null | cut -d'@' -f1)
|
|
40
|
+
if [ -n "$pm" ]; then
|
|
41
|
+
echo "$pm"
|
|
42
|
+
return
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Fallback to lockfile detection
|
|
47
|
+
if [ -f "$ROOT/pnpm-lock.yaml" ]; then
|
|
48
|
+
echo "pnpm"
|
|
49
|
+
elif [ -f "$ROOT/yarn.lock" ]; then
|
|
50
|
+
echo "yarn"
|
|
51
|
+
elif [ -f "$ROOT/bun.lock" ]; then
|
|
52
|
+
echo "bun"
|
|
53
|
+
else
|
|
54
|
+
echo "npm"
|
|
55
|
+
fi
|
|
985
56
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
57
|
+
|
|
58
|
+
# Run lint-staged with the detected package manager
|
|
59
|
+
PM=$(detect_pm)
|
|
60
|
+
case "$PM" in
|
|
61
|
+
pnpm) pnpm exec lint-staged --config "$ROOT/${configPath}" ;;
|
|
62
|
+
yarn) yarn exec lint-staged --config "$ROOT/${configPath}" ;;
|
|
63
|
+
bun) bunx lint-staged --config "$ROOT/${configPath}" ;;
|
|
64
|
+
*) npx --no -- lint-staged --config "$ROOT/${configPath}" ;;
|
|
65
|
+
esac
|
|
66
|
+
|
|
67
|
+
fi`;
|
|
990
68
|
}
|
|
991
|
-
function
|
|
992
|
-
return
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
69
|
+
function generateShellScriptsManagedContent() {
|
|
70
|
+
return `# DO NOT EDIT between these markers - managed by savvy-lint
|
|
71
|
+
if ! { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; }; then
|
|
72
|
+
|
|
73
|
+
# Configure git to ignore executable bit changes
|
|
74
|
+
# This ensures hook scripts can be made executable locally without git tracking the change
|
|
75
|
+
git config core.fileMode false
|
|
76
|
+
|
|
77
|
+
# Ensure all shell scripts tracked by git are executable
|
|
78
|
+
git ls-files -z '*.sh' | xargs -0 -r chmod +x 2>/dev/null || true
|
|
79
|
+
|
|
80
|
+
fi`;
|
|
1000
81
|
}
|
|
82
|
+
const preCommitBlock = SavvyLintSection.generate(generateManagedContent);
|
|
83
|
+
const shellScriptsBlock = ()=>SavvyLintSection.block(generateShellScriptsManagedContent());
|
|
1001
84
|
const MARKDOWNLINT_TEMPLATE = {
|
|
1002
|
-
$schema: "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.
|
|
85
|
+
$schema: "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.22.0/schema/markdownlint-cli2-config-schema.json",
|
|
1003
86
|
globs: [
|
|
1004
87
|
"**/*.{md,mdx}"
|
|
1005
88
|
],
|
|
@@ -1101,7 +184,7 @@ const MARKDOWNLINT_TEMPLATE = {
|
|
|
1101
184
|
"changeset-dependency-table-format": false
|
|
1102
185
|
}
|
|
1103
186
|
};
|
|
1104
|
-
const MARKDOWNLINT_SCHEMA = "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.
|
|
187
|
+
const MARKDOWNLINT_SCHEMA = "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/v0.22.0/schema/markdownlint-cli2-config-schema.json";
|
|
1105
188
|
const MARKDOWNLINT_CONFIG = {
|
|
1106
189
|
default: true,
|
|
1107
190
|
MD001: true,
|
|
@@ -1184,263 +267,9 @@ const MARKDOWNLINT_CONFIG = {
|
|
|
1184
267
|
"changeset-dependency-table-format": false
|
|
1185
268
|
};
|
|
1186
269
|
const CHECK_MARK = "\u2713";
|
|
1187
|
-
const WARNING = "\u26A0";
|
|
1188
|
-
const EXECUTABLE_MODE = 493;
|
|
1189
|
-
const HUSKY_HOOK_PATH = ".husky/pre-commit";
|
|
1190
|
-
const POST_CHECKOUT_HOOK_PATH = ".husky/post-checkout";
|
|
1191
|
-
const POST_MERGE_HOOK_PATH = ".husky/post-merge";
|
|
1192
|
-
const DEFAULT_CONFIG_PATH = "lib/configs/lint-staged.config.ts";
|
|
1193
|
-
const MARKDOWNLINT_CONFIG_PATH = "lib/configs/.markdownlint-cli2.jsonc";
|
|
1194
|
-
const JSONC_FORMAT = {
|
|
1195
|
-
tabSize: 1,
|
|
1196
|
-
insertSpaces: false
|
|
1197
|
-
};
|
|
1198
|
-
const BEGIN_MARKER = "# --- BEGIN SAVVY-LINT MANAGED SECTION ---";
|
|
1199
|
-
const END_MARKER = "# --- END SAVVY-LINT MANAGED SECTION ---";
|
|
1200
|
-
function presetIncludesShellScripts(preset) {
|
|
1201
|
-
return "minimal" !== preset;
|
|
1202
|
-
}
|
|
1203
|
-
function presetIncludesMarkdown(preset) {
|
|
1204
|
-
return "minimal" !== preset;
|
|
1205
|
-
}
|
|
1206
|
-
function generateManagedContent(configPath) {
|
|
1207
|
-
return `# DO NOT EDIT between these markers - managed by savvy-lint
|
|
1208
|
-
# Skip in CI environment
|
|
1209
|
-
if ! { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; }; then
|
|
1210
|
-
|
|
1211
|
-
# Get repo root directory
|
|
1212
|
-
ROOT=$(git rev-parse --show-toplevel)
|
|
1213
|
-
|
|
1214
|
-
# Detect package manager from package.json or lockfiles
|
|
1215
|
-
detect_pm() {
|
|
1216
|
-
# Check packageManager field in package.json (e.g., "pnpm@9.0.0")
|
|
1217
|
-
if [ -f "$ROOT/package.json" ]; then
|
|
1218
|
-
pm=$(jq -r '.packageManager // empty' "$ROOT/package.json" 2>/dev/null | cut -d'@' -f1)
|
|
1219
|
-
if [ -n "$pm" ]; then
|
|
1220
|
-
echo "$pm"
|
|
1221
|
-
return
|
|
1222
|
-
fi
|
|
1223
|
-
fi
|
|
1224
|
-
|
|
1225
|
-
# Fallback to lockfile detection
|
|
1226
|
-
if [ -f "$ROOT/pnpm-lock.yaml" ]; then
|
|
1227
|
-
echo "pnpm"
|
|
1228
|
-
elif [ -f "$ROOT/yarn.lock" ]; then
|
|
1229
|
-
echo "yarn"
|
|
1230
|
-
elif [ -f "$ROOT/bun.lock" ]; then
|
|
1231
|
-
echo "bun"
|
|
1232
|
-
else
|
|
1233
|
-
echo "npm"
|
|
1234
|
-
fi
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
# Run lint-staged with the detected package manager
|
|
1238
|
-
PM=$(detect_pm)
|
|
1239
|
-
case "$PM" in
|
|
1240
|
-
pnpm) pnpm exec lint-staged --config "$ROOT/${configPath}" ;;
|
|
1241
|
-
yarn) yarn exec lint-staged --config "$ROOT/${configPath}" ;;
|
|
1242
|
-
bun) bunx lint-staged --config "$ROOT/${configPath}" ;;
|
|
1243
|
-
*) npx --no -- lint-staged --config "$ROOT/${configPath}" ;;
|
|
1244
|
-
esac
|
|
1245
|
-
|
|
1246
|
-
fi`;
|
|
1247
|
-
}
|
|
1248
|
-
function generateShellScriptsManagedContent() {
|
|
1249
|
-
return `# DO NOT EDIT between these markers - managed by savvy-lint
|
|
1250
|
-
if ! { [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; }; then
|
|
1251
|
-
|
|
1252
|
-
# Configure git to ignore executable bit changes
|
|
1253
|
-
# This ensures hook scripts can be made executable locally without git tracking the change
|
|
1254
|
-
git config core.fileMode false
|
|
1255
|
-
|
|
1256
|
-
# Ensure all shell scripts tracked by git are executable
|
|
1257
|
-
git ls-files -z '*.sh' | xargs -0 -r chmod +x 2>/dev/null || true
|
|
1258
|
-
|
|
1259
|
-
fi`;
|
|
1260
|
-
}
|
|
1261
|
-
function updateManagedSectionWithContent(existingContent, managedContent) {
|
|
1262
|
-
const { beforeSection, afterSection, found } = extractManagedSection(existingContent);
|
|
1263
|
-
const newManagedSection = `${BEGIN_MARKER}\n${managedContent}\n${END_MARKER}`;
|
|
1264
|
-
if (found) return `${beforeSection}${newManagedSection}${afterSection}`;
|
|
1265
|
-
const trimmedContent = existingContent.trimEnd();
|
|
1266
|
-
return `${trimmedContent}\n\n${newManagedSection}\n`;
|
|
1267
|
-
}
|
|
1268
|
-
function extractManagedSection(content) {
|
|
1269
|
-
const beginIndex = content.indexOf(BEGIN_MARKER);
|
|
1270
|
-
const endIndex = content.indexOf(END_MARKER);
|
|
1271
|
-
if (-1 === beginIndex || -1 === endIndex || endIndex <= beginIndex) return {
|
|
1272
|
-
beforeSection: content,
|
|
1273
|
-
managedSection: "",
|
|
1274
|
-
afterSection: "",
|
|
1275
|
-
found: false
|
|
1276
|
-
};
|
|
1277
|
-
return {
|
|
1278
|
-
beforeSection: content.slice(0, beginIndex),
|
|
1279
|
-
managedSection: content.slice(beginIndex, endIndex + END_MARKER.length),
|
|
1280
|
-
afterSection: content.slice(endIndex + END_MARKER.length),
|
|
1281
|
-
found: true
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
|
-
function generateFullHookContentFromManaged(comment, managedContent) {
|
|
1285
|
-
return `#!/usr/bin/env sh
|
|
1286
|
-
# ${comment}
|
|
1287
|
-
# Custom hooks can go above or below the managed section
|
|
1288
|
-
|
|
1289
|
-
${BEGIN_MARKER}
|
|
1290
|
-
${managedContent}
|
|
1291
|
-
${END_MARKER}
|
|
1292
|
-
`;
|
|
1293
|
-
}
|
|
1294
|
-
function generateConfigContent(preset) {
|
|
1295
|
-
return `/**
|
|
1296
|
-
* lint-staged configuration
|
|
1297
|
-
* Generated by savvy-lint init
|
|
1298
|
-
*/
|
|
1299
|
-
import { Preset } from "@savvy-web/lint-staged";
|
|
1300
|
-
|
|
1301
|
-
export default Preset.${preset}();
|
|
1302
|
-
`;
|
|
1303
|
-
}
|
|
1304
|
-
function writeMarkdownlintConfig(fs, preset, force) {
|
|
1305
|
-
return Effect.gen(function*() {
|
|
1306
|
-
const configExists = yield* fs.exists(MARKDOWNLINT_CONFIG_PATH);
|
|
1307
|
-
const fullTemplate = JSON.stringify(MARKDOWNLINT_TEMPLATE, null, "\t");
|
|
1308
|
-
if (!configExists) {
|
|
1309
|
-
yield* fs.makeDirectory("lib/configs", {
|
|
1310
|
-
recursive: true
|
|
1311
|
-
});
|
|
1312
|
-
yield* fs.writeFileString(MARKDOWNLINT_CONFIG_PATH, `${fullTemplate}\n`);
|
|
1313
|
-
yield* Effect.log(`${CHECK_MARK} Created ${MARKDOWNLINT_CONFIG_PATH}`);
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1316
|
-
if ("silk" !== preset) return void (yield* Effect.log(`${CHECK_MARK} ${MARKDOWNLINT_CONFIG_PATH}: exists (not managed by ${preset} preset)`));
|
|
1317
|
-
if (force) {
|
|
1318
|
-
yield* fs.writeFileString(MARKDOWNLINT_CONFIG_PATH, `${fullTemplate}\n`);
|
|
1319
|
-
yield* Effect.log(`${CHECK_MARK} Replaced ${MARKDOWNLINT_CONFIG_PATH} (--force)`);
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
const existingText = yield* fs.readFileString(MARKDOWNLINT_CONFIG_PATH);
|
|
1323
|
-
const existingParsed = yield* parse(existingText);
|
|
1324
|
-
let updatedText = existingText;
|
|
1325
|
-
let schemaUpdated = false;
|
|
1326
|
-
if (existingParsed.$schema !== MARKDOWNLINT_SCHEMA) {
|
|
1327
|
-
const edits = yield* modify(updatedText, [
|
|
1328
|
-
"$schema"
|
|
1329
|
-
], MARKDOWNLINT_SCHEMA, {
|
|
1330
|
-
formattingOptions: JSONC_FORMAT
|
|
1331
|
-
});
|
|
1332
|
-
updatedText = yield* applyEdits(updatedText, edits);
|
|
1333
|
-
schemaUpdated = true;
|
|
1334
|
-
}
|
|
1335
|
-
const existingConfig = existingParsed.config;
|
|
1336
|
-
const configMatches = void 0 !== existingConfig && isDeepStrictEqual(existingConfig, MARKDOWNLINT_CONFIG);
|
|
1337
|
-
if (!configMatches) {
|
|
1338
|
-
yield* Effect.log(`${WARNING} ${MARKDOWNLINT_CONFIG_PATH}: config rules differ from template (use --force to overwrite)`);
|
|
1339
|
-
if (schemaUpdated) {
|
|
1340
|
-
yield* fs.writeFileString(MARKDOWNLINT_CONFIG_PATH, updatedText);
|
|
1341
|
-
yield* Effect.log(`${CHECK_MARK} Updated $schema in ${MARKDOWNLINT_CONFIG_PATH}`);
|
|
1342
|
-
}
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
if (schemaUpdated) {
|
|
1346
|
-
yield* fs.writeFileString(MARKDOWNLINT_CONFIG_PATH, updatedText);
|
|
1347
|
-
yield* Effect.log(`${CHECK_MARK} Updated $schema in ${MARKDOWNLINT_CONFIG_PATH}`);
|
|
1348
|
-
} else yield* Effect.log(`${CHECK_MARK} ${MARKDOWNLINT_CONFIG_PATH}: up-to-date`);
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1351
|
-
function syncBiomeSchemas(fs) {
|
|
1352
|
-
return Effect.gen(function*() {
|
|
1353
|
-
const expectedUrl = getExpectedSchemaUrl();
|
|
1354
|
-
if (!expectedUrl) return;
|
|
1355
|
-
const configs = yield* findBiomeConfigs();
|
|
1356
|
-
for (const configPath of configs){
|
|
1357
|
-
const content = yield* fs.readFileString(configPath);
|
|
1358
|
-
const parsed = yield* parse(content);
|
|
1359
|
-
if ("string" != typeof parsed.$schema) continue;
|
|
1360
|
-
if (!parsed.$schema.startsWith(SCHEMA_URL_PREFIX)) continue;
|
|
1361
|
-
if (parsed.$schema === expectedUrl) {
|
|
1362
|
-
yield* Effect.log(`${CHECK_MARK} ${configPath}: biome $schema up-to-date`);
|
|
1363
|
-
continue;
|
|
1364
|
-
}
|
|
1365
|
-
const edits = yield* modify(content, [
|
|
1366
|
-
"$schema"
|
|
1367
|
-
], expectedUrl, {
|
|
1368
|
-
formattingOptions: JSONC_FORMAT
|
|
1369
|
-
});
|
|
1370
|
-
const updated = yield* applyEdits(content, edits);
|
|
1371
|
-
yield* fs.writeFileString(configPath, updated);
|
|
1372
|
-
yield* Effect.log(`${CHECK_MARK} Updated $schema in ${configPath}`);
|
|
1373
|
-
}
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite entire hook file (not just managed section)"), Options.withDefault(false));
|
|
1377
|
-
const configOption = Options.text("config").pipe(Options.withAlias("c"), Options.withDescription("Relative path for the lint-staged config file (from repo root)"), Options.withDefault(DEFAULT_CONFIG_PATH));
|
|
1378
|
-
const presetOption = Options.choice("preset", [
|
|
1379
|
-
"minimal",
|
|
1380
|
-
"standard",
|
|
1381
|
-
"silk"
|
|
1382
|
-
]).pipe(Options.withAlias("p"), Options.withDescription("Preset to use: minimal, standard, or silk"), Options.withDefault("silk"));
|
|
1383
|
-
function makeExecutable(path) {
|
|
1384
|
-
return Effect.tryPromise(()=>import("node:fs/promises").then((fs)=>fs.chmod(path, EXECUTABLE_MODE)));
|
|
1385
|
-
}
|
|
1386
|
-
function writeHook(fs, hookPath, managedContent, comment, force) {
|
|
1387
|
-
return Effect.gen(function*() {
|
|
1388
|
-
const hookExists = yield* fs.exists(hookPath);
|
|
1389
|
-
if (hookExists && !force) {
|
|
1390
|
-
const existingContent = yield* fs.readFileString(hookPath);
|
|
1391
|
-
const { found } = extractManagedSection(existingContent);
|
|
1392
|
-
const updatedContent = updateManagedSectionWithContent(existingContent, managedContent);
|
|
1393
|
-
yield* fs.writeFileString(hookPath, updatedContent);
|
|
1394
|
-
yield* makeExecutable(hookPath);
|
|
1395
|
-
if (found) yield* Effect.log(`${CHECK_MARK} Updated managed section in ${hookPath}`);
|
|
1396
|
-
else yield* Effect.log(`${CHECK_MARK} Added managed section to ${hookPath}`);
|
|
1397
|
-
} else if (hookExists && force) {
|
|
1398
|
-
yield* fs.writeFileString(hookPath, generateFullHookContentFromManaged(comment, managedContent));
|
|
1399
|
-
yield* makeExecutable(hookPath);
|
|
1400
|
-
yield* Effect.log(`${CHECK_MARK} Replaced ${hookPath} (--force)`);
|
|
1401
|
-
} else {
|
|
1402
|
-
yield* fs.makeDirectory(".husky", {
|
|
1403
|
-
recursive: true
|
|
1404
|
-
});
|
|
1405
|
-
yield* fs.writeFileString(hookPath, generateFullHookContentFromManaged(comment, managedContent));
|
|
1406
|
-
yield* makeExecutable(hookPath);
|
|
1407
|
-
yield* Effect.log(`${CHECK_MARK} Created ${hookPath}`);
|
|
1408
|
-
}
|
|
1409
|
-
});
|
|
1410
|
-
}
|
|
1411
|
-
const initCommand = Command.make("init", {
|
|
1412
|
-
force: forceOption,
|
|
1413
|
-
config: configOption,
|
|
1414
|
-
preset: presetOption
|
|
1415
|
-
}, ({ force, config, preset })=>Effect.gen(function*() {
|
|
1416
|
-
const fs = yield* FileSystem.FileSystem;
|
|
1417
|
-
if (config.startsWith("/")) yield* Effect.fail(new Error("Config path must be relative to repository root, not absolute"));
|
|
1418
|
-
yield* Effect.log("Initializing lint-staged configuration...\n");
|
|
1419
|
-
yield* writeHook(fs, HUSKY_HOOK_PATH, generateManagedContent(config), "Pre-commit hook with savvy-lint managed section", force);
|
|
1420
|
-
if (presetIncludesShellScripts(preset)) {
|
|
1421
|
-
const shellContent = generateShellScriptsManagedContent();
|
|
1422
|
-
yield* writeHook(fs, POST_CHECKOUT_HOOK_PATH, shellContent, "Post-checkout hook with savvy-lint managed section", force);
|
|
1423
|
-
yield* writeHook(fs, POST_MERGE_HOOK_PATH, shellContent, "Post-merge hook with savvy-lint managed section", force);
|
|
1424
|
-
}
|
|
1425
|
-
if (presetIncludesMarkdown(preset)) yield* writeMarkdownlintConfig(fs, preset, force);
|
|
1426
|
-
yield* syncBiomeSchemas(fs);
|
|
1427
|
-
const configExists = yield* fs.exists(config);
|
|
1428
|
-
if (configExists && !force) yield* Effect.log(`${WARNING} ${config} already exists (use --force to overwrite)`);
|
|
1429
|
-
else {
|
|
1430
|
-
const configDir = dirname(config);
|
|
1431
|
-
if (configDir && "." !== configDir) yield* fs.makeDirectory(configDir, {
|
|
1432
|
-
recursive: true
|
|
1433
|
-
});
|
|
1434
|
-
yield* fs.writeFileString(config, generateConfigContent(preset));
|
|
1435
|
-
yield* Effect.log(`${CHECK_MARK} Created ${config} (preset: ${preset})`);
|
|
1436
|
-
}
|
|
1437
|
-
yield* Effect.log("\nDone! Lint-staged is ready to use.");
|
|
1438
|
-
})).pipe(Command.withDescription("Initialize lint-staged configuration and husky hooks"));
|
|
1439
|
-
const check_CHECK_MARK = "\u2713";
|
|
1440
270
|
const CROSS_MARK = "\u2717";
|
|
1441
|
-
const
|
|
271
|
+
const WARNING = "\u26A0";
|
|
1442
272
|
const BULLET = "\u2022";
|
|
1443
|
-
const check_HUSKY_HOOK_PATH = ".husky/pre-commit";
|
|
1444
273
|
const CONFIG_FILES = [
|
|
1445
274
|
"lint-staged.config.ts",
|
|
1446
275
|
"lint-staged.config.js",
|
|
@@ -1465,43 +294,68 @@ function findConfigFile(fs) {
|
|
|
1465
294
|
return null;
|
|
1466
295
|
});
|
|
1467
296
|
}
|
|
297
|
+
function findConfig(discovery, names) {
|
|
298
|
+
return Effect.gen(function*() {
|
|
299
|
+
for (const name of names){
|
|
300
|
+
const result = yield* discovery.find(name);
|
|
301
|
+
if (result) return result.path;
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
1468
306
|
function extractConfigPathFromManaged(managedContent) {
|
|
1469
307
|
const match = managedContent.match(/lint-staged --config "\$ROOT\/([^"]+)"/);
|
|
1470
308
|
return match ? match[1] : null;
|
|
1471
309
|
}
|
|
1472
|
-
function checkHookManagedSection(
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
};
|
|
310
|
+
function checkHookManagedSection(section, hookPath, block) {
|
|
311
|
+
return Effect.gen(function*() {
|
|
312
|
+
const result = yield* section.check(hookPath, block);
|
|
313
|
+
return CheckResult.$match(result, {
|
|
314
|
+
Found: ({ isUpToDate })=>({
|
|
315
|
+
found: true,
|
|
316
|
+
isUpToDate,
|
|
317
|
+
needsUpdate: !isUpToDate
|
|
318
|
+
}),
|
|
319
|
+
NotFound: ()=>({
|
|
320
|
+
found: false,
|
|
321
|
+
isUpToDate: false,
|
|
322
|
+
needsUpdate: false
|
|
323
|
+
})
|
|
324
|
+
});
|
|
325
|
+
});
|
|
1488
326
|
}
|
|
1489
|
-
function checkManagedSectionStatus(
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
327
|
+
function checkManagedSectionStatus(section, hookPath) {
|
|
328
|
+
return Effect.gen(function*() {
|
|
329
|
+
const existing = yield* section.read(hookPath, SavvyLintSectionDef);
|
|
330
|
+
if (null === existing) return {
|
|
331
|
+
isUpToDate: false,
|
|
332
|
+
configPath: null,
|
|
333
|
+
needsUpdate: false,
|
|
334
|
+
found: false
|
|
335
|
+
};
|
|
336
|
+
const configPath = extractConfigPathFromManaged(existing.text);
|
|
337
|
+
if (!configPath) return {
|
|
338
|
+
isUpToDate: false,
|
|
339
|
+
configPath: null,
|
|
340
|
+
needsUpdate: true,
|
|
341
|
+
found: true
|
|
342
|
+
};
|
|
343
|
+
const result = yield* section.check(hookPath, preCommitBlock(configPath));
|
|
344
|
+
return CheckResult.$match(result, {
|
|
345
|
+
Found: ({ isUpToDate })=>({
|
|
346
|
+
isUpToDate,
|
|
347
|
+
configPath: configPath,
|
|
348
|
+
needsUpdate: !isUpToDate,
|
|
349
|
+
found: true
|
|
350
|
+
}),
|
|
351
|
+
NotFound: ()=>({
|
|
352
|
+
isUpToDate: false,
|
|
353
|
+
configPath: null,
|
|
354
|
+
needsUpdate: false,
|
|
355
|
+
found: false
|
|
356
|
+
})
|
|
357
|
+
});
|
|
358
|
+
});
|
|
1505
359
|
}
|
|
1506
360
|
function checkMarkdownlintConfig(content) {
|
|
1507
361
|
return Effect.gen(function*() {
|
|
@@ -1517,26 +371,27 @@ function checkMarkdownlintConfig(content) {
|
|
|
1517
371
|
};
|
|
1518
372
|
});
|
|
1519
373
|
}
|
|
1520
|
-
function checkBiomeSchemas(
|
|
374
|
+
function checkBiomeSchemas() {
|
|
1521
375
|
return Effect.gen(function*() {
|
|
1522
|
-
const
|
|
376
|
+
const version = "catalog:silk";
|
|
1523
377
|
const statuses = [];
|
|
1524
|
-
if (!
|
|
378
|
+
if (!version) return {
|
|
1525
379
|
statuses,
|
|
1526
380
|
warnings: []
|
|
1527
381
|
};
|
|
1528
|
-
const
|
|
382
|
+
const syncer = yield* BiomeSchemaSync;
|
|
383
|
+
const result = yield* syncer.check(version);
|
|
1529
384
|
const warnings = [];
|
|
1530
|
-
for (const configPath of
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
385
|
+
for (const configPath of result.current)statuses.push({
|
|
386
|
+
path: configPath,
|
|
387
|
+
matches: true
|
|
388
|
+
});
|
|
389
|
+
for (const configPath of result.updated){
|
|
1535
390
|
statuses.push({
|
|
1536
391
|
path: configPath,
|
|
1537
|
-
matches
|
|
392
|
+
matches: false
|
|
1538
393
|
});
|
|
1539
|
-
|
|
394
|
+
warnings.push(`${WARNING} ${configPath}: biome $schema is outdated.\n Run 'savvy-lint init' to update it.`);
|
|
1540
395
|
}
|
|
1541
396
|
return {
|
|
1542
397
|
statuses,
|
|
@@ -1549,9 +404,12 @@ const checkCommand = Command.make("check", {
|
|
|
1549
404
|
quiet: quietOption
|
|
1550
405
|
}, ({ quiet })=>Effect.gen(function*() {
|
|
1551
406
|
const fs = yield* FileSystem.FileSystem;
|
|
407
|
+
const section = yield* ManagedSection;
|
|
408
|
+
const td = yield* ToolDiscovery;
|
|
409
|
+
const discovery = yield* ConfigDiscovery;
|
|
1552
410
|
const warnings = [];
|
|
1553
411
|
const foundConfig = yield* findConfigFile(fs);
|
|
1554
|
-
const hasHuskyHook = yield* fs.exists(
|
|
412
|
+
const hasHuskyHook = yield* fs.exists(HUSKY_HOOK_PATH);
|
|
1555
413
|
let managedStatus = {
|
|
1556
414
|
isUpToDate: false,
|
|
1557
415
|
configPath: null,
|
|
@@ -1559,26 +417,11 @@ const checkCommand = Command.make("check", {
|
|
|
1559
417
|
found: false
|
|
1560
418
|
};
|
|
1561
419
|
if (hasHuskyHook) {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
if (found) {
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
...status,
|
|
1568
|
-
found: true
|
|
1569
|
-
};
|
|
1570
|
-
if (status.needsUpdate) warnings.push(`${check_WARNING} Your ${check_HUSKY_HOOK_PATH} managed section is outdated.\n Run 'savvy-lint init' to update it (preserves your custom hooks).`);
|
|
1571
|
-
} else {
|
|
1572
|
-
managedStatus = {
|
|
1573
|
-
isUpToDate: false,
|
|
1574
|
-
configPath: null,
|
|
1575
|
-
needsUpdate: false,
|
|
1576
|
-
found: false
|
|
1577
|
-
};
|
|
1578
|
-
warnings.push(`${check_WARNING} Your ${check_HUSKY_HOOK_PATH} does not have a savvy-lint managed section.\n Run 'savvy-lint init' to add it.`);
|
|
1579
|
-
}
|
|
1580
|
-
} else warnings.push(`${check_WARNING} No husky pre-commit hook found.\n Run 'savvy-lint init' to create it.`);
|
|
1581
|
-
if (!foundConfig) warnings.push(`${check_WARNING} No lint-staged config file found.\n Run 'savvy-lint init' to create one.`);
|
|
420
|
+
managedStatus = yield* checkManagedSectionStatus(section, HUSKY_HOOK_PATH);
|
|
421
|
+
if (managedStatus.found && managedStatus.needsUpdate) warnings.push(`${WARNING} Your ${HUSKY_HOOK_PATH} managed section is outdated.\n Run 'savvy-lint init' to update it (preserves your custom hooks).`);
|
|
422
|
+
else if (!managedStatus.found) warnings.push(`${WARNING} Your ${HUSKY_HOOK_PATH} does not have a savvy-lint managed section.\n Run 'savvy-lint init' to add it.`);
|
|
423
|
+
} else warnings.push(`${WARNING} No husky pre-commit hook found.\n Run 'savvy-lint init' to create it.`);
|
|
424
|
+
if (!foundConfig) warnings.push(`${WARNING} No lint-staged config file found.\n Run 'savvy-lint init' to create one.`);
|
|
1582
425
|
const shellHookPaths = [
|
|
1583
426
|
POST_CHECKOUT_HOOK_PATH,
|
|
1584
427
|
POST_MERGE_HOOK_PATH
|
|
@@ -1587,16 +430,20 @@ const checkCommand = Command.make("check", {
|
|
|
1587
430
|
for (const hookPath of shellHookPaths){
|
|
1588
431
|
const hookExists = yield* fs.exists(hookPath);
|
|
1589
432
|
if (hookExists) {
|
|
1590
|
-
const
|
|
1591
|
-
const status = checkHookManagedSection(hookContent, generateShellScriptsManagedContent());
|
|
433
|
+
const status = yield* checkHookManagedSection(section, hookPath, shellScriptsBlock());
|
|
1592
434
|
shellHookStatuses.push({
|
|
1593
435
|
path: hookPath,
|
|
1594
436
|
...status
|
|
1595
437
|
});
|
|
1596
|
-
if (status.found && status.needsUpdate) warnings.push(`${
|
|
438
|
+
if (status.found && status.needsUpdate) warnings.push(`${WARNING} Your ${hookPath} managed section is outdated.\n Run 'savvy-lint init' to update it (preserves your custom hooks).`);
|
|
1597
439
|
}
|
|
1598
440
|
}
|
|
1599
|
-
const biomeSchemaStatus = yield* checkBiomeSchemas(
|
|
441
|
+
const biomeSchemaStatus = yield* checkBiomeSchemas().pipe(Effect.catchTag("BiomeSyncError", ()=>Effect.succeed({
|
|
442
|
+
statuses: [],
|
|
443
|
+
warnings: [
|
|
444
|
+
`${WARNING} Could not check biome $schema URLs.`
|
|
445
|
+
]
|
|
446
|
+
})));
|
|
1600
447
|
warnings.push(...biomeSchemaStatus.warnings);
|
|
1601
448
|
const hasMarkdownlintConfig = yield* fs.exists(MARKDOWNLINT_CONFIG_PATH);
|
|
1602
449
|
let markdownlintStatus = {
|
|
@@ -1608,62 +455,220 @@ const checkCommand = Command.make("check", {
|
|
|
1608
455
|
if (hasMarkdownlintConfig) {
|
|
1609
456
|
const mdContent = yield* fs.readFileString(MARKDOWNLINT_CONFIG_PATH);
|
|
1610
457
|
markdownlintStatus = yield* checkMarkdownlintConfig(mdContent);
|
|
1611
|
-
if (!markdownlintStatus.schemaMatches) warnings.push(`${
|
|
1612
|
-
if (!markdownlintStatus.configMatches) warnings.push(`${
|
|
458
|
+
if (!markdownlintStatus.schemaMatches) warnings.push(`${WARNING} ${MARKDOWNLINT_CONFIG_PATH}: $schema differs from template.\n Run 'savvy-lint init' to update it.`);
|
|
459
|
+
if (!markdownlintStatus.configMatches) warnings.push(`${WARNING} ${MARKDOWNLINT_CONFIG_PATH}: config rules differ from template.\n Run 'savvy-lint init --force' to overwrite.`);
|
|
1613
460
|
}
|
|
1614
461
|
if (quiet) {
|
|
1615
462
|
if (warnings.length > 0) for (const warning of warnings)yield* Effect.log(warning);
|
|
1616
463
|
return;
|
|
1617
464
|
}
|
|
1618
465
|
yield* Effect.log("Checking lint-staged configuration...\n");
|
|
1619
|
-
if (foundConfig) yield* Effect.log(`${
|
|
466
|
+
if (foundConfig) yield* Effect.log(`${CHECK_MARK} Config file: ${foundConfig}`);
|
|
1620
467
|
else yield* Effect.log(`${CROSS_MARK} No lint-staged config file found`);
|
|
1621
|
-
if (hasHuskyHook) yield* Effect.log(`${
|
|
468
|
+
if (hasHuskyHook) yield* Effect.log(`${CHECK_MARK} Husky hook: ${HUSKY_HOOK_PATH}`);
|
|
1622
469
|
else yield* Effect.log(`${CROSS_MARK} No husky pre-commit hook found`);
|
|
1623
|
-
if (hasHuskyHook) if (managedStatus.found) if (managedStatus.isUpToDate) yield* Effect.log(`${
|
|
1624
|
-
else yield* Effect.log(`${
|
|
470
|
+
if (hasHuskyHook) if (managedStatus.found) if (managedStatus.isUpToDate) yield* Effect.log(`${CHECK_MARK} Managed section: up-to-date`);
|
|
471
|
+
else yield* Effect.log(`${WARNING} Managed section: outdated (run 'savvy-lint init' to update)`);
|
|
1625
472
|
else yield* Effect.log(`${BULLET} Managed section: not found (run 'savvy-lint init' to add)`);
|
|
1626
|
-
for (const status of shellHookStatuses)if (status.found) if (status.isUpToDate) yield* Effect.log(`${
|
|
1627
|
-
else yield* Effect.log(`${
|
|
473
|
+
for (const status of shellHookStatuses)if (status.found) if (status.isUpToDate) yield* Effect.log(`${CHECK_MARK} ${status.path}: up-to-date`);
|
|
474
|
+
else yield* Effect.log(`${WARNING} ${status.path}: outdated (run 'savvy-lint init' to update)`);
|
|
1628
475
|
yield* Effect.log("\nTool availability:");
|
|
1629
|
-
const biomeAvailable =
|
|
1630
|
-
|
|
476
|
+
const biomeAvailable = yield* td.isAvailable(ToolDefinition.make({
|
|
477
|
+
name: "biome"
|
|
478
|
+
}));
|
|
479
|
+
const biomeConfig = yield* findConfig(discovery, [
|
|
480
|
+
"biome.jsonc",
|
|
481
|
+
"biome.json"
|
|
482
|
+
]);
|
|
1631
483
|
if (biomeAvailable) {
|
|
1632
484
|
const configInfo = biomeConfig ? ` (config: ${biomeConfig})` : "";
|
|
1633
|
-
yield* Effect.log(` ${
|
|
485
|
+
yield* Effect.log(` ${CHECK_MARK} Biome${configInfo}`);
|
|
1634
486
|
} else yield* Effect.log(` ${BULLET} Biome: not installed`);
|
|
1635
|
-
const markdownAvailable =
|
|
1636
|
-
|
|
487
|
+
const markdownAvailable = yield* td.isAvailable(ToolDefinition.make({
|
|
488
|
+
name: "markdownlint-cli2"
|
|
489
|
+
}));
|
|
490
|
+
const markdownConfig = yield* findConfig(discovery, [
|
|
491
|
+
".markdownlint-cli2.jsonc",
|
|
492
|
+
".markdownlint-cli2.json",
|
|
493
|
+
".markdownlint-cli2.yaml",
|
|
494
|
+
".markdownlint-cli2.cjs",
|
|
495
|
+
".markdownlint.jsonc",
|
|
496
|
+
".markdownlint.json",
|
|
497
|
+
".markdownlint.yaml"
|
|
498
|
+
]);
|
|
1637
499
|
if (markdownAvailable) {
|
|
1638
500
|
const configInfo = markdownConfig ? ` (config: ${markdownConfig})` : "";
|
|
1639
|
-
yield* Effect.log(` ${
|
|
501
|
+
yield* Effect.log(` ${CHECK_MARK} markdownlint-cli2${configInfo}`);
|
|
1640
502
|
} else yield* Effect.log(` ${BULLET} markdownlint-cli2: not installed`);
|
|
1641
|
-
const
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
if (
|
|
503
|
+
const tsgoAvailable = yield* td.isAvailable(ToolDefinition.make({
|
|
504
|
+
name: "tsgo"
|
|
505
|
+
}));
|
|
506
|
+
const tscAvailable = yield* td.isAvailable(ToolDefinition.make({
|
|
507
|
+
name: "tsc"
|
|
508
|
+
}));
|
|
509
|
+
if (tsgoAvailable) yield* Effect.log(` ${CHECK_MARK} TypeScript (tsgo)`);
|
|
510
|
+
else if (tscAvailable) yield* Effect.log(` ${CHECK_MARK} TypeScript (tsc)`);
|
|
511
|
+
else yield* Effect.log(` ${BULLET} TypeScript: not installed`);
|
|
512
|
+
const tsdocConfig = yield* discovery.find("tsdoc.json");
|
|
513
|
+
if (tsdocConfig) yield* Effect.log(` ${CHECK_MARK} TSDoc (tsdoc.json found)`);
|
|
1648
514
|
else yield* Effect.log(` ${BULLET} TSDoc: no tsdoc.json found`);
|
|
1649
|
-
if (hasMarkdownlintConfig) if (markdownlintStatus.isUpToDate) yield* Effect.log(` ${
|
|
515
|
+
if (hasMarkdownlintConfig) if (markdownlintStatus.isUpToDate) yield* Effect.log(` ${CHECK_MARK} ${MARKDOWNLINT_CONFIG_PATH}: up-to-date`);
|
|
1650
516
|
else {
|
|
1651
517
|
const issues = [];
|
|
1652
518
|
if (!markdownlintStatus.schemaMatches) issues.push("$schema");
|
|
1653
519
|
if (!markdownlintStatus.configMatches) issues.push("config");
|
|
1654
|
-
yield* Effect.log(` ${
|
|
520
|
+
yield* Effect.log(` ${WARNING} ${MARKDOWNLINT_CONFIG_PATH}: ${issues.join(", ")} differ from template`);
|
|
1655
521
|
}
|
|
1656
522
|
else yield* Effect.log(` ${BULLET} ${MARKDOWNLINT_CONFIG_PATH}: not found`);
|
|
1657
|
-
for (const status of biomeSchemaStatus.statuses)if (status.matches) yield* Effect.log(` ${
|
|
1658
|
-
else yield* Effect.log(` ${
|
|
523
|
+
for (const status of biomeSchemaStatus.statuses)if (status.matches) yield* Effect.log(` ${CHECK_MARK} ${status.path}: biome $schema up-to-date`);
|
|
524
|
+
else yield* Effect.log(` ${WARNING} ${status.path}: biome $schema outdated (run 'savvy-lint init' to update)`);
|
|
1659
525
|
yield* Effect.log("");
|
|
1660
526
|
const hasShellHookIssues = shellHookStatuses.some((s)=>s.found && s.needsUpdate);
|
|
1661
527
|
const hasMarkdownlintIssues = hasMarkdownlintConfig && !markdownlintStatus.isUpToDate;
|
|
1662
528
|
const hasBiomeSchemaIssues = biomeSchemaStatus.statuses.some((s)=>!s.matches);
|
|
1663
529
|
const hasIssues = !foundConfig || !hasHuskyHook || !managedStatus.found || managedStatus.needsUpdate || hasShellHookIssues || hasMarkdownlintIssues || hasBiomeSchemaIssues;
|
|
1664
|
-
if (hasIssues) yield* Effect.log(`${
|
|
1665
|
-
else yield* Effect.log(`${
|
|
530
|
+
if (hasIssues) yield* Effect.log(`${WARNING} Some issues found. Run 'savvy-lint init' to fix.`);
|
|
531
|
+
else yield* Effect.log(`${CHECK_MARK} Lint-staged is configured correctly.`);
|
|
1666
532
|
})).pipe(Command.withDescription("Check current lint-staged configuration and tool availability"));
|
|
533
|
+
const VALID_COMMAND_PATTERN = /^[\w@/-]+$/;
|
|
534
|
+
function validateCommandName(name) {
|
|
535
|
+
if (!VALID_COMMAND_PATTERN.test(name)) throw new Error(`Invalid command name: "${name}". Only alphanumeric characters, hyphens, underscores, @ and / are allowed.`);
|
|
536
|
+
}
|
|
537
|
+
class Command_Command {
|
|
538
|
+
static cachedPackageManager = null;
|
|
539
|
+
static cachedRoot = null;
|
|
540
|
+
static findRoot(cwd = process.cwd()) {
|
|
541
|
+
if (null !== Command_Command.cachedRoot) return Command_Command.cachedRoot;
|
|
542
|
+
let dir = resolve(cwd);
|
|
543
|
+
while(true){
|
|
544
|
+
if (existsSync(join(dir, "package.json"))) {
|
|
545
|
+
Command_Command.cachedRoot = dir;
|
|
546
|
+
return dir;
|
|
547
|
+
}
|
|
548
|
+
const parent = dirname(dir);
|
|
549
|
+
if (parent === dir) break;
|
|
550
|
+
dir = parent;
|
|
551
|
+
}
|
|
552
|
+
Command_Command.cachedRoot = cwd;
|
|
553
|
+
return cwd;
|
|
554
|
+
}
|
|
555
|
+
static detectPackageManager(cwd = Command_Command.findRoot()) {
|
|
556
|
+
if (null !== Command_Command.cachedPackageManager) return Command_Command.cachedPackageManager;
|
|
557
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
558
|
+
if (!existsSync(packageJsonPath)) {
|
|
559
|
+
Command_Command.cachedPackageManager = "npm";
|
|
560
|
+
return "npm";
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
const content = readFileSync(packageJsonPath, "utf-8");
|
|
564
|
+
const pkg = JSON.parse(content);
|
|
565
|
+
if (pkg.packageManager) {
|
|
566
|
+
const match = pkg.packageManager.match(/^(npm|pnpm|yarn|bun)@/);
|
|
567
|
+
if (match) {
|
|
568
|
+
Command_Command.cachedPackageManager = match[1];
|
|
569
|
+
return Command_Command.cachedPackageManager;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
} catch {}
|
|
573
|
+
Command_Command.cachedPackageManager = "npm";
|
|
574
|
+
return "npm";
|
|
575
|
+
}
|
|
576
|
+
static getExecPrefix(packageManager) {
|
|
577
|
+
switch(packageManager){
|
|
578
|
+
case "pnpm":
|
|
579
|
+
return [
|
|
580
|
+
"pnpm",
|
|
581
|
+
"exec"
|
|
582
|
+
];
|
|
583
|
+
case "yarn":
|
|
584
|
+
return [
|
|
585
|
+
"yarn",
|
|
586
|
+
"exec"
|
|
587
|
+
];
|
|
588
|
+
case "bun":
|
|
589
|
+
return [
|
|
590
|
+
"bun",
|
|
591
|
+
"x",
|
|
592
|
+
"--no-install"
|
|
593
|
+
];
|
|
594
|
+
default:
|
|
595
|
+
return [
|
|
596
|
+
"npx",
|
|
597
|
+
"--no"
|
|
598
|
+
];
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
static clearCache() {
|
|
602
|
+
Command_Command.cachedPackageManager = null;
|
|
603
|
+
Command_Command.cachedRoot = null;
|
|
604
|
+
}
|
|
605
|
+
static isAvailable(command) {
|
|
606
|
+
validateCommandName(command);
|
|
607
|
+
try {
|
|
608
|
+
execSync(`command -v ${command}`, {
|
|
609
|
+
stdio: "ignore"
|
|
610
|
+
});
|
|
611
|
+
return true;
|
|
612
|
+
} catch {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
static findTool(tool) {
|
|
617
|
+
validateCommandName(tool);
|
|
618
|
+
if (Command_Command.isAvailable(tool)) return {
|
|
619
|
+
available: true,
|
|
620
|
+
command: tool,
|
|
621
|
+
source: "global"
|
|
622
|
+
};
|
|
623
|
+
const pm = Command_Command.detectPackageManager();
|
|
624
|
+
const prefix = Command_Command.getExecPrefix(pm);
|
|
625
|
+
const execCmd = [
|
|
626
|
+
...prefix,
|
|
627
|
+
tool
|
|
628
|
+
].join(" ");
|
|
629
|
+
try {
|
|
630
|
+
execSync(`${execCmd} --version`, {
|
|
631
|
+
stdio: "ignore"
|
|
632
|
+
});
|
|
633
|
+
return {
|
|
634
|
+
available: true,
|
|
635
|
+
command: execCmd,
|
|
636
|
+
source: pm
|
|
637
|
+
};
|
|
638
|
+
} catch {}
|
|
639
|
+
return {
|
|
640
|
+
available: false,
|
|
641
|
+
command: void 0,
|
|
642
|
+
source: void 0
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
static requireTool(tool, errorMessage) {
|
|
646
|
+
const result = Command_Command.findTool(tool);
|
|
647
|
+
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.`);
|
|
648
|
+
return result.command;
|
|
649
|
+
}
|
|
650
|
+
static findSavvyLint() {
|
|
651
|
+
const result = Command_Command.findTool("savvy-lint");
|
|
652
|
+
if (result.available && result.command) return result.command;
|
|
653
|
+
const root = Command_Command.findRoot();
|
|
654
|
+
return `node ${root}/dist/dev/bin/savvy-lint.js`;
|
|
655
|
+
}
|
|
656
|
+
static exec(command) {
|
|
657
|
+
return execSync(command, {
|
|
658
|
+
encoding: "utf-8"
|
|
659
|
+
}).trim();
|
|
660
|
+
}
|
|
661
|
+
static execSilent(command) {
|
|
662
|
+
try {
|
|
663
|
+
execSync(command, {
|
|
664
|
+
stdio: "ignore"
|
|
665
|
+
});
|
|
666
|
+
return true;
|
|
667
|
+
} catch {
|
|
668
|
+
return false;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
1667
672
|
const DEFAULT_STRINGIFY_OPTIONS = {
|
|
1668
673
|
indent: 2,
|
|
1669
674
|
lineWidth: 0,
|
|
@@ -1725,6 +730,29 @@ class PnpmWorkspace {
|
|
|
1725
730
|
};
|
|
1726
731
|
}
|
|
1727
732
|
}
|
|
733
|
+
class Filter {
|
|
734
|
+
static exclude(filenames, patterns) {
|
|
735
|
+
if (0 === patterns.length) return [
|
|
736
|
+
...filenames
|
|
737
|
+
];
|
|
738
|
+
return filenames.filter((file)=>!patterns.some((pattern)=>file.includes(pattern)));
|
|
739
|
+
}
|
|
740
|
+
static include(filenames, patterns) {
|
|
741
|
+
if (0 === patterns.length) return [];
|
|
742
|
+
return filenames.filter((file)=>patterns.some((pattern)=>file.includes(pattern)));
|
|
743
|
+
}
|
|
744
|
+
static apply(filenames, options) {
|
|
745
|
+
let result = [
|
|
746
|
+
...filenames
|
|
747
|
+
];
|
|
748
|
+
if (options.include && options.include.length > 0) result = Filter.include(result, options.include);
|
|
749
|
+
if (options.exclude && options.exclude.length > 0) result = Filter.exclude(result, options.exclude);
|
|
750
|
+
return result;
|
|
751
|
+
}
|
|
752
|
+
static shellEscape(filenames) {
|
|
753
|
+
return filenames.map((f)=>`'${f.replace(/'/g, "'\\''")}'`).join(" ");
|
|
754
|
+
}
|
|
755
|
+
}
|
|
1728
756
|
class Yaml {
|
|
1729
757
|
static glob = "**/*.{yml,yaml}";
|
|
1730
758
|
static defaultExcludes = [
|
|
@@ -1734,8 +762,9 @@ class Yaml {
|
|
|
1734
762
|
];
|
|
1735
763
|
static handler = Yaml.create();
|
|
1736
764
|
static findConfig() {
|
|
1737
|
-
const
|
|
1738
|
-
return
|
|
765
|
+
const libPath = "lib/configs/.yaml-lint.json";
|
|
766
|
+
if (existsSync(libPath)) return libPath;
|
|
767
|
+
if (existsSync(".yaml-lint.json")) return ".yaml-lint.json";
|
|
1739
768
|
}
|
|
1740
769
|
static loadConfig(filepath) {
|
|
1741
770
|
try {
|
|
@@ -1834,6 +863,149 @@ const fmtCommand = Command.make("fmt").pipe(Command.withSubcommands([
|
|
|
1834
863
|
pnpmWorkspaceCommand,
|
|
1835
864
|
yamlCommand
|
|
1836
865
|
]));
|
|
866
|
+
const init_CHECK_MARK = "\u2713";
|
|
867
|
+
const init_WARNING = "\u26A0";
|
|
868
|
+
const EXECUTABLE_MODE = 493;
|
|
869
|
+
const JSONC_FORMAT = {
|
|
870
|
+
tabSize: 1,
|
|
871
|
+
insertSpaces: false
|
|
872
|
+
};
|
|
873
|
+
function presetIncludesShellScripts(preset) {
|
|
874
|
+
return "minimal" !== preset;
|
|
875
|
+
}
|
|
876
|
+
function presetIncludesMarkdown(preset) {
|
|
877
|
+
return "minimal" !== preset;
|
|
878
|
+
}
|
|
879
|
+
function generateConfigContent(preset) {
|
|
880
|
+
return `/**
|
|
881
|
+
* lint-staged configuration
|
|
882
|
+
* Generated by savvy-lint init
|
|
883
|
+
*/
|
|
884
|
+
import { Preset } from "@savvy-web/lint-staged";
|
|
885
|
+
|
|
886
|
+
export default Preset.${preset}();
|
|
887
|
+
`;
|
|
888
|
+
}
|
|
889
|
+
function writeMarkdownlintConfig(fs, preset, force) {
|
|
890
|
+
return Effect.gen(function*() {
|
|
891
|
+
const configExists = yield* fs.exists(MARKDOWNLINT_CONFIG_PATH);
|
|
892
|
+
const fullTemplate = JSON.stringify(MARKDOWNLINT_TEMPLATE, null, "\t");
|
|
893
|
+
if (!configExists) {
|
|
894
|
+
yield* fs.makeDirectory("lib/configs", {
|
|
895
|
+
recursive: true
|
|
896
|
+
});
|
|
897
|
+
yield* fs.writeFileString(MARKDOWNLINT_CONFIG_PATH, `${fullTemplate}\n`);
|
|
898
|
+
yield* Effect.log(`${init_CHECK_MARK} Created ${MARKDOWNLINT_CONFIG_PATH}`);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if ("silk" !== preset) return void (yield* Effect.log(`${init_CHECK_MARK} ${MARKDOWNLINT_CONFIG_PATH}: exists (not managed by ${preset} preset)`));
|
|
902
|
+
if (force) {
|
|
903
|
+
yield* fs.writeFileString(MARKDOWNLINT_CONFIG_PATH, `${fullTemplate}\n`);
|
|
904
|
+
yield* Effect.log(`${init_CHECK_MARK} Replaced ${MARKDOWNLINT_CONFIG_PATH} (--force)`);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const existingText = yield* fs.readFileString(MARKDOWNLINT_CONFIG_PATH);
|
|
908
|
+
const existingParsed = yield* parse(existingText);
|
|
909
|
+
let updatedText = existingText;
|
|
910
|
+
let schemaUpdated = false;
|
|
911
|
+
if (existingParsed.$schema !== MARKDOWNLINT_SCHEMA) {
|
|
912
|
+
const edits = yield* modify(updatedText, [
|
|
913
|
+
"$schema"
|
|
914
|
+
], MARKDOWNLINT_SCHEMA, {
|
|
915
|
+
formattingOptions: JSONC_FORMAT
|
|
916
|
+
});
|
|
917
|
+
updatedText = yield* applyEdits(updatedText, edits);
|
|
918
|
+
schemaUpdated = true;
|
|
919
|
+
}
|
|
920
|
+
const existingConfig = existingParsed.config;
|
|
921
|
+
const configMatches = void 0 !== existingConfig && isDeepStrictEqual(existingConfig, MARKDOWNLINT_CONFIG);
|
|
922
|
+
if (!configMatches) {
|
|
923
|
+
yield* Effect.log(`${init_WARNING} ${MARKDOWNLINT_CONFIG_PATH}: config rules differ from template (use --force to overwrite)`);
|
|
924
|
+
if (schemaUpdated) {
|
|
925
|
+
yield* fs.writeFileString(MARKDOWNLINT_CONFIG_PATH, updatedText);
|
|
926
|
+
yield* Effect.log(`${init_CHECK_MARK} Updated $schema in ${MARKDOWNLINT_CONFIG_PATH}`);
|
|
927
|
+
}
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
if (schemaUpdated) {
|
|
931
|
+
yield* fs.writeFileString(MARKDOWNLINT_CONFIG_PATH, updatedText);
|
|
932
|
+
yield* Effect.log(`${init_CHECK_MARK} Updated $schema in ${MARKDOWNLINT_CONFIG_PATH}`);
|
|
933
|
+
} else yield* Effect.log(`${init_CHECK_MARK} ${MARKDOWNLINT_CONFIG_PATH}: up-to-date`);
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
function syncBiomeSchemas() {
|
|
937
|
+
return Effect.gen(function*() {
|
|
938
|
+
const version = "catalog:silk";
|
|
939
|
+
if (!version) return;
|
|
940
|
+
const syncer = yield* BiomeSchemaSync;
|
|
941
|
+
const result = yield* syncer.sync(version);
|
|
942
|
+
for (const configPath of result.current)yield* Effect.log(`${init_CHECK_MARK} ${configPath}: biome $schema up-to-date`);
|
|
943
|
+
for (const configPath of result.updated)yield* Effect.log(`${init_CHECK_MARK} Updated $schema in ${configPath}`);
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
const forceOption = Options.boolean("force").pipe(Options.withAlias("f"), Options.withDescription("Overwrite entire hook file (not just managed section)"), Options.withDefault(false));
|
|
947
|
+
const configOption = Options.text("config").pipe(Options.withAlias("c"), Options.withDescription("Relative path for the lint-staged config file (from repo root)"), Options.withDefault(DEFAULT_CONFIG_PATH));
|
|
948
|
+
const presetOption = Options.choice("preset", [
|
|
949
|
+
"minimal",
|
|
950
|
+
"standard",
|
|
951
|
+
"silk"
|
|
952
|
+
]).pipe(Options.withAlias("p"), Options.withDescription("Preset to use: minimal, standard, or silk"), Options.withDefault("silk"));
|
|
953
|
+
function makeExecutable(path) {
|
|
954
|
+
return Effect.tryPromise(()=>import("node:fs/promises").then((fs)=>fs.chmod(path, EXECUTABLE_MODE)));
|
|
955
|
+
}
|
|
956
|
+
function writeHook(fs, section, hookPath, block, comment, force) {
|
|
957
|
+
return Effect.gen(function*() {
|
|
958
|
+
const hookExists = yield* fs.exists(hookPath);
|
|
959
|
+
const header = `#!/usr/bin/env sh\n# ${comment}\n# Custom hooks can go above or below the managed section\n`;
|
|
960
|
+
if (!hookExists || force) {
|
|
961
|
+
if (!hookExists) yield* fs.makeDirectory(".husky", {
|
|
962
|
+
recursive: true
|
|
963
|
+
});
|
|
964
|
+
yield* fs.writeFileString(hookPath, header);
|
|
965
|
+
}
|
|
966
|
+
const result = yield* section.sync(hookPath, block);
|
|
967
|
+
yield* makeExecutable(hookPath);
|
|
968
|
+
if (hookExists) if (force) yield* Effect.log(`${init_CHECK_MARK} Replaced ${hookPath} (--force)`);
|
|
969
|
+
else yield* SyncResult.$match(result, {
|
|
970
|
+
Created: ()=>Effect.log(`${init_CHECK_MARK} Added managed section to ${hookPath}`),
|
|
971
|
+
Updated: ()=>Effect.log(`${init_CHECK_MARK} Updated managed section in ${hookPath}`),
|
|
972
|
+
Unchanged: ()=>Effect.log(`${init_CHECK_MARK} ${hookPath}: up-to-date`)
|
|
973
|
+
});
|
|
974
|
+
else yield* Effect.log(`${init_CHECK_MARK} Created ${hookPath}`);
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
const initCommand = Command.make("init", {
|
|
978
|
+
force: forceOption,
|
|
979
|
+
config: configOption,
|
|
980
|
+
preset: presetOption
|
|
981
|
+
}, ({ force, config, preset })=>Effect.gen(function*() {
|
|
982
|
+
const fs = yield* FileSystem.FileSystem;
|
|
983
|
+
const section = yield* ManagedSection;
|
|
984
|
+
if (config.startsWith("/")) yield* Effect.fail(new Error("Config path must be relative to repository root, not absolute"));
|
|
985
|
+
yield* Effect.log("Initializing lint-staged configuration...\n");
|
|
986
|
+
yield* writeHook(fs, section, HUSKY_HOOK_PATH, preCommitBlock(config), "Pre-commit hook with savvy-lint managed section", force);
|
|
987
|
+
if (presetIncludesShellScripts(preset)) {
|
|
988
|
+
const shellBlock = shellScriptsBlock();
|
|
989
|
+
yield* writeHook(fs, section, POST_CHECKOUT_HOOK_PATH, shellBlock, "Post-checkout hook with savvy-lint managed section", force);
|
|
990
|
+
yield* writeHook(fs, section, POST_MERGE_HOOK_PATH, shellBlock, "Post-merge hook with savvy-lint managed section", force);
|
|
991
|
+
}
|
|
992
|
+
if (presetIncludesMarkdown(preset)) yield* writeMarkdownlintConfig(fs, preset, force);
|
|
993
|
+
yield* syncBiomeSchemas().pipe(Effect.catchTag("BiomeSyncError", (e)=>Effect.log(`${init_WARNING} Could not sync biome $schema: ${e.message}`)));
|
|
994
|
+
const configExists = yield* fs.exists(config);
|
|
995
|
+
if (configExists && !force) yield* Effect.log(`${init_WARNING} ${config} already exists (use --force to overwrite)`);
|
|
996
|
+
else {
|
|
997
|
+
const configDir = dirname(config);
|
|
998
|
+
if (configDir && "." !== configDir) yield* fs.makeDirectory(configDir, {
|
|
999
|
+
recursive: true
|
|
1000
|
+
});
|
|
1001
|
+
yield* fs.writeFileString(config, generateConfigContent(preset));
|
|
1002
|
+
yield* Effect.log(`${init_CHECK_MARK} Created ${config} (preset: ${preset})`);
|
|
1003
|
+
}
|
|
1004
|
+
yield* Effect.log("\nDone! Lint-staged is ready to use.");
|
|
1005
|
+
})).pipe(Command.withDescription("Initialize lint-staged configuration and husky hooks"));
|
|
1006
|
+
const WorkspaceLive = Layer.mergeAll(PackageManagerDetectorLive, WorkspaceRootLive);
|
|
1007
|
+
const SilkLive = Layer.mergeAll(ManagedSectionLive, BiomeSchemaSyncLive, ConfigDiscoveryLive, ToolDiscoveryLive);
|
|
1008
|
+
const AppLayer = SilkLive.pipe(Layer.provideMerge(WorkspaceLive), Layer.provideMerge(NodeContext.layer));
|
|
1837
1009
|
const rootCommand = Command.make("savvy-lint").pipe(Command.withSubcommands([
|
|
1838
1010
|
initCommand,
|
|
1839
1011
|
checkCommand,
|
|
@@ -1841,10 +1013,10 @@ const rootCommand = Command.make("savvy-lint").pipe(Command.withSubcommands([
|
|
|
1841
1013
|
]));
|
|
1842
1014
|
const cli = Command.run(rootCommand, {
|
|
1843
1015
|
name: "savvy-lint",
|
|
1844
|
-
version: "0.
|
|
1016
|
+
version: "0.7.1"
|
|
1845
1017
|
});
|
|
1846
1018
|
function runCli() {
|
|
1847
|
-
const main = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(
|
|
1019
|
+
const main = Effect.suspend(()=>cli(process.argv)).pipe(Effect.provide(AppLayer));
|
|
1848
1020
|
NodeRuntime.runMain(main);
|
|
1849
1021
|
}
|
|
1850
|
-
export {
|
|
1022
|
+
export { Command_Command as Command, Filter, PnpmWorkspace, Yaml, checkCommand, fmtCommand, initCommand, rootCommand, runCli };
|