@geoql/doctor-core 0.1.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.d.ts +73 -2
- package/dist/index.js +268 -14
- package/dist/index.js.map +1 -1
- package/dist/{load-BGjurxpY.js → load-yXDi-5ti.js} +155 -2
- package/dist/load-yXDi-5ti.js.map +1 -0
- package/package.json +3 -3
- package/dist/load-BGjurxpY.js.map +0 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ This is the library that does the work. The two CLIs are thin `cac` wrappers; ev
|
|
|
6
6
|
|
|
7
7
|
## What it does
|
|
8
8
|
|
|
9
|
-
`doctor-core` runs a **hybrid two-pass scan** over a Vue 3 or Nuxt 4 project, as locked in [`docs/
|
|
9
|
+
`doctor-core` runs a **hybrid two-pass scan** over a Vue 3 or Nuxt 4 project, as locked in [`docs/SPEC.md`](../../docs/SPEC.md) §10:
|
|
10
10
|
|
|
11
11
|
- **Pass 1 — template AST.** Parses `.vue` SFCs with `@vue/compiler-sfc` and walks the template AST in-process. This is the only pass that can see the template, so template-shaped rules (`v-for` missing `:key`, `v-if`/`v-for` on the same node, inline object props in lists) live here.
|
|
12
12
|
- **Pass 2 — script ESTree.** Spawns `oxlint` as a subprocess against a generated `.oxlintrc.json` that turns on oxlint's native `vue` plugin plus the doctor JS plugins (`@geoql/oxlint-plugin-vue-doctor`, `@geoql/oxlint-plugin-nuxt-doctor`). oxlint only hands JS plugins the extracted `<script>`, so all script-level rules run here.
|
|
@@ -88,7 +88,7 @@ Vue 3 and Nuxt 4 only. Nuxt detection expects the `app/` directory layout with `
|
|
|
88
88
|
|
|
89
89
|
## Architecture
|
|
90
90
|
|
|
91
|
-
See [`docs/
|
|
91
|
+
See [`docs/SPEC.md`](../../docs/SPEC.md) §10 for the full two-pass design, the empirical oxlint findings that shaped it, and the canonical `Diagnostic` shape.
|
|
92
92
|
|
|
93
93
|
## License
|
|
94
94
|
|
package/dist/index.d.ts
CHANGED
|
@@ -100,6 +100,8 @@ interface AuditConfig {
|
|
|
100
100
|
threshold?: number;
|
|
101
101
|
scopeFiles?: string[];
|
|
102
102
|
respectInlineDisables?: boolean;
|
|
103
|
+
fix?: boolean;
|
|
104
|
+
fixExcludes?: string[];
|
|
103
105
|
}
|
|
104
106
|
interface AuditTimings {
|
|
105
107
|
template: number;
|
|
@@ -148,6 +150,19 @@ interface DoctorUserConfig {
|
|
|
148
150
|
preset?: string;
|
|
149
151
|
rules?: Record<string, Severity | 'off'>;
|
|
150
152
|
extends?: string[];
|
|
153
|
+
fixExcludes?: string[];
|
|
154
|
+
}
|
|
155
|
+
interface ResolvedDoctorConfig {
|
|
156
|
+
rootDir: string;
|
|
157
|
+
include: string[];
|
|
158
|
+
exclude: string[];
|
|
159
|
+
failOn: 'error' | 'warn' | 'none';
|
|
160
|
+
threshold: number;
|
|
161
|
+
rules: Record<string, Severity>;
|
|
162
|
+
preset: 'minimal' | 'recommended' | 'strict' | 'all';
|
|
163
|
+
source: ConfigSource;
|
|
164
|
+
configFile?: string;
|
|
165
|
+
fixExcludes?: string[];
|
|
151
166
|
}
|
|
152
167
|
/**
|
|
153
168
|
* Resolved + normalized config that audit consumes.
|
|
@@ -201,6 +216,7 @@ interface CliOverrides {
|
|
|
201
216
|
failOn?: 'error' | 'warn' | 'none';
|
|
202
217
|
threshold?: number;
|
|
203
218
|
rules?: Record<string, Severity | 'off'>;
|
|
219
|
+
fixExcludes?: string[];
|
|
204
220
|
}
|
|
205
221
|
declare function mergeCliOverrides(resolved: ResolvedDoctorConfig, cli: CliOverrides): ResolvedDoctorConfig;
|
|
206
222
|
//#endregion
|
|
@@ -233,6 +249,51 @@ declare function validateConfig(raw: unknown): void;
|
|
|
233
249
|
//#region src/detect-project.d.ts
|
|
234
250
|
declare function detectProject(rootDirectory: string): Promise<ProjectInfo>;
|
|
235
251
|
//#endregion
|
|
252
|
+
//#region src/init/index.d.ts
|
|
253
|
+
type InitConfigFormat = 'ts' | 'json' | 'package-json';
|
|
254
|
+
type InitPreset = 'recommended' | 'strict' | 'minimal';
|
|
255
|
+
interface RawInitAnswers {
|
|
256
|
+
target?: InitConfigFormat;
|
|
257
|
+
preset?: InitPreset;
|
|
258
|
+
threshold?: number;
|
|
259
|
+
exclude?: string;
|
|
260
|
+
}
|
|
261
|
+
interface ResolvedInitAnswers {
|
|
262
|
+
configFormat: InitConfigFormat;
|
|
263
|
+
preset: InitPreset;
|
|
264
|
+
threshold: number | undefined;
|
|
265
|
+
exclude: string[] | undefined;
|
|
266
|
+
}
|
|
267
|
+
interface InitOptions {
|
|
268
|
+
dir: string;
|
|
269
|
+
configFormat: InitConfigFormat;
|
|
270
|
+
preset: InitPreset;
|
|
271
|
+
threshold: number | undefined;
|
|
272
|
+
exclude: string[] | undefined;
|
|
273
|
+
binName: string;
|
|
274
|
+
}
|
|
275
|
+
interface InitFileWrite {
|
|
276
|
+
path: string;
|
|
277
|
+
content: string;
|
|
278
|
+
}
|
|
279
|
+
interface InitPlan {
|
|
280
|
+
writes: InitFileWrite[];
|
|
281
|
+
conflict: boolean;
|
|
282
|
+
conflictPath: string | null;
|
|
283
|
+
}
|
|
284
|
+
declare function parseExcludeList(raw: string | undefined): string[] | undefined;
|
|
285
|
+
declare function normalizeInitAnswers(raw: RawInitAnswers): ResolvedInitAnswers;
|
|
286
|
+
declare function renderDetectSummary(project: ProjectInfo, sfcCount: number): string;
|
|
287
|
+
declare function detectSummary(dir: string): Promise<string>;
|
|
288
|
+
declare function planInit(options: InitOptions): Promise<InitPlan>;
|
|
289
|
+
//#endregion
|
|
290
|
+
//#region src/project-info/find-monorepo-root.d.ts
|
|
291
|
+
interface MonorepoResult {
|
|
292
|
+
root: string;
|
|
293
|
+
kind: MonorepoKind;
|
|
294
|
+
}
|
|
295
|
+
declare function findMonorepoRoot(rootDirectory: string): Promise<MonorepoResult>;
|
|
296
|
+
//#endregion
|
|
236
297
|
//#region src/disables/apply.d.ts
|
|
237
298
|
interface ApplyInlineDisablesOptions {
|
|
238
299
|
respect: boolean;
|
|
@@ -264,6 +325,13 @@ interface GitScopeOptions {
|
|
|
264
325
|
}
|
|
265
326
|
declare function listChangedFiles(options: GitScopeOptions): Promise<string[]>;
|
|
266
327
|
//#endregion
|
|
328
|
+
//#region src/project-info/list-workspace-packages.d.ts
|
|
329
|
+
interface WorkspacePackage {
|
|
330
|
+
name: string;
|
|
331
|
+
dir: string;
|
|
332
|
+
}
|
|
333
|
+
declare function listWorkspacePackages(rootDir: string): Promise<WorkspacePackage[]>;
|
|
334
|
+
//#endregion
|
|
267
335
|
//#region src/nuxt/file-role.d.ts
|
|
268
336
|
/**
|
|
269
337
|
* True when the path points at a Nuxt page component — a `.vue` file under
|
|
@@ -315,7 +383,7 @@ interface VerboseTraceOptions {
|
|
|
315
383
|
declare function renderVerboseTrace(report: AuditReport, options: VerboseTraceOptions): string;
|
|
316
384
|
//#endregion
|
|
317
385
|
//#region src/reporters/index.d.ts
|
|
318
|
-
type ReporterFormat = 'agent' | 'pretty' | 'json' | 'json-compact' | 'sarif' | 'html';
|
|
386
|
+
type ReporterFormat = 'agent' | 'pretty' | 'json' | 'json-compact' | 'sarif' | 'html' | 'pr-comment';
|
|
319
387
|
declare function format(input: ReporterInput, kind?: ReporterFormat, options?: ReporterOptions): string;
|
|
320
388
|
//#endregion
|
|
321
389
|
//#region src/reporters/json.d.ts
|
|
@@ -374,6 +442,9 @@ declare function jsonReport(input: ReporterInput): string;
|
|
|
374
442
|
//#region src/reporters/json-compact.d.ts
|
|
375
443
|
declare function jsonCompactReport(input: ReporterInput): string;
|
|
376
444
|
//#endregion
|
|
445
|
+
//#region src/reporters/pr-comment.d.ts
|
|
446
|
+
declare function prCommentReport(input: ReporterInput): string;
|
|
447
|
+
//#endregion
|
|
377
448
|
//#region src/reporters/pretty.d.ts
|
|
378
449
|
declare function prettyReport(input: ReporterInput, options?: ReporterOptions): string;
|
|
379
450
|
//#endregion
|
|
@@ -414,5 +485,5 @@ interface ListRulesFilter {
|
|
|
414
485
|
}
|
|
415
486
|
declare function listRules(filter?: ListRulesFilter): RegisteredRule[];
|
|
416
487
|
//#endregion
|
|
417
|
-
export { type ApplyInlineDisablesOptions, type AuditConfig, type AuditReport, type AuditTimings, BUILT_IN_RECOMMENDED, type Capability, type CliOverrides, ConfigCycleError, ConfigFileNotFoundError, type ConfigSource, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, type Diagnostic, type DiagnosticSource, type DirectiveLine, type DirectiveRange, type DirectiveSet, type DoctorReport, type DoctorUserConfig, type Framework, type GitScopeMode, InvalidConfigError, type ListRulesFilter, type LoadRuleDocOptions, type MonorepoKind, OxlintOutputTooLarge, OxlintSpawnFailed, type ProjectInfo, type ProjectInfoLite, RULE_REGISTRY, type RegisteredRule, type ReporterFormat, type ReporterInput, type ReporterOptions, type ResolvedDoctorConfig, type RuleCategory, type RuleDoc, type RuleSource, type ScoreBreakdownEntry, type ScoreConfig, type ScoreResult, type Severity, type VerboseTraceOptions, agentReport, applyInlineDisables, audit, buildDoctorReport, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, docsUrl, encodeAnnotation, encodeAnnotations, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, parseDirectives, prettyReport, renderVerboseTrace, runCrossFilePass, sarifReport, scoreDiagnostics, validateConfig };
|
|
488
|
+
export { type ApplyInlineDisablesOptions, type AuditConfig, type AuditReport, type AuditTimings, BUILT_IN_RECOMMENDED, type Capability, type CliOverrides, ConfigCycleError, ConfigFileNotFoundError, type ConfigSource, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, type Diagnostic, type DiagnosticSource, type DirectiveLine, type DirectiveRange, type DirectiveSet, type DoctorReport, type DoctorUserConfig, type Framework, type GitScopeMode, type InitConfigFormat, type InitFileWrite, type InitOptions, type InitPlan, type InitPreset, InvalidConfigError, type ListRulesFilter, type LoadRuleDocOptions, type MonorepoKind, type MonorepoResult, OxlintOutputTooLarge, OxlintSpawnFailed, type ProjectInfo, type ProjectInfoLite, RULE_REGISTRY, type RawInitAnswers, type RegisteredRule, type ReporterFormat, type ReporterInput, type ReporterOptions, type ResolvedDoctorConfig, type ResolvedInitAnswers, type RuleCategory, type RuleDoc, type RuleSource, type ScoreBreakdownEntry, type ScoreConfig, type ScoreResult, type Severity, type VerboseTraceOptions, type WorkspacePackage, agentReport, applyInlineDisables, audit, buildDoctorReport, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, detectSummary, docsUrl, encodeAnnotation, encodeAnnotations, findMonorepoRoot, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, listWorkspacePackages, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, normalizeInitAnswers, parseDirectives, parseExcludeList, planInit, prCommentReport, prettyReport, renderDetectSummary, renderVerboseTrace, runCrossFilePass, sarifReport, scoreDiagnostics, validateConfig };
|
|
418
489
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as listRules, c as InvalidConfigError, i as RULE_REGISTRY, l as BUILT_IN_RECOMMENDED, o as ConfigCycleError, r as validateConfig, s as ConfigFileNotFoundError, t as loadDoctorConfig } from "./load-
|
|
1
|
+
import { a as listRules, c as InvalidConfigError, i as RULE_REGISTRY, l as BUILT_IN_RECOMMENDED, o as ConfigCycleError, r as validateConfig, s as ConfigFileNotFoundError, t as loadDoctorConfig } from "./load-yXDi-5ti.js";
|
|
2
2
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { performance } from "node:perf_hooks";
|
|
4
4
|
import { access, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
@@ -215,6 +215,7 @@ async function checkBuildQuality(projectInfo) {
|
|
|
215
215
|
//#region src/dead-code/build-knip-config.ts
|
|
216
216
|
function buildKnipConfig(projectInfo, doctorConfig) {
|
|
217
217
|
const isNuxt = projectInfo.framework === "nuxt";
|
|
218
|
+
const usesConventionResolution = projectInfo.hasAutoImports || projectInfo.hasComponentsAutoImport || projectInfo.hasVueRouter;
|
|
218
219
|
const entry = isNuxt ? [
|
|
219
220
|
"app/**/*.vue",
|
|
220
221
|
"app/**/*.ts",
|
|
@@ -224,7 +225,14 @@ function buildKnipConfig(projectInfo, doctorConfig) {
|
|
|
224
225
|
"src/main.{ts,js}",
|
|
225
226
|
"src/App.vue",
|
|
226
227
|
"index.html",
|
|
227
|
-
"vite.config.{ts,js}"
|
|
228
|
+
"vite.config.{ts,js}",
|
|
229
|
+
...usesConventionResolution ? [
|
|
230
|
+
"src/pages/**/*.vue",
|
|
231
|
+
"src/layouts/**/*.vue",
|
|
232
|
+
"src/components/**/*.vue",
|
|
233
|
+
"src/composables/**/*.ts",
|
|
234
|
+
"src/stores/**/*.ts"
|
|
235
|
+
] : []
|
|
228
236
|
];
|
|
229
237
|
const config = {
|
|
230
238
|
cwd: projectInfo.rootDirectory,
|
|
@@ -381,6 +389,20 @@ const _knipLoader = { load: async () => {
|
|
|
381
389
|
run: runModule.run
|
|
382
390
|
};
|
|
383
391
|
} };
|
|
392
|
+
async function writeKnipConfig(rootDir, config) {
|
|
393
|
+
const cacheDir = join(rootDir, "node_modules", ".cache", "doctor");
|
|
394
|
+
await mkdir(cacheDir, { recursive: true });
|
|
395
|
+
const configPath = join(cacheDir, "knip.json");
|
|
396
|
+
const knipJson = {
|
|
397
|
+
entry: config.entry,
|
|
398
|
+
project: config.project,
|
|
399
|
+
ignore: config.ignoreFiles,
|
|
400
|
+
ignoreDependencies: config.ignoreDependencies
|
|
401
|
+
};
|
|
402
|
+
if (config.compilers) knipJson.compilers = config.compilers;
|
|
403
|
+
await writeFile(configPath, JSON.stringify(knipJson, null, 2), "utf8");
|
|
404
|
+
return configPath;
|
|
405
|
+
}
|
|
384
406
|
async function checkDeadCode(options) {
|
|
385
407
|
if (!options.enabled) return [];
|
|
386
408
|
const config = buildKnipConfig(options.projectInfo, options.doctorConfig);
|
|
@@ -394,13 +416,10 @@ async function checkDeadCode(options) {
|
|
|
394
416
|
} catch (err) {
|
|
395
417
|
throw new DeadCodeImportFailed(err);
|
|
396
418
|
}
|
|
419
|
+
const configPath = await writeKnipConfig(options.projectInfo.rootDirectory, config);
|
|
397
420
|
const knipOptions = await createOptions({
|
|
398
421
|
cwd: options.projectInfo.rootDirectory,
|
|
399
|
-
|
|
400
|
-
project: config.project,
|
|
401
|
-
ignoreFiles: config.ignoreFiles,
|
|
402
|
-
ignoreDependencies: config.ignoreDependencies,
|
|
403
|
-
...config.compilers ? { compilers: config.compilers } : {}
|
|
422
|
+
args: { config: configPath }
|
|
404
423
|
});
|
|
405
424
|
const diagnostics = flattenKnipIssues((await Promise.race([run(knipOptions), new Promise((_, reject) => setTimeout(() => reject(new DeadCodeTimeoutError(timeoutMs)), timeoutMs))])).results.issues).map((issue) => mapKnipDiagnostic(options.projectInfo.rootDirectory, issue)).filter((d) => d !== null);
|
|
406
425
|
diagnostics.sort((a, b) => {
|
|
@@ -1347,6 +1366,23 @@ function toCanonicalDiagnostics(raws, rootDir) {
|
|
|
1347
1366
|
const VUE_DEFAULT_RULES = {
|
|
1348
1367
|
"vue/no-export-in-script-setup": "error",
|
|
1349
1368
|
"vue/require-typed-ref": "warn",
|
|
1369
|
+
"vue/no-arrow-functions-in-watch": "error",
|
|
1370
|
+
"vue/no-deprecated-data-object-declaration": "error",
|
|
1371
|
+
"vue/no-deprecated-events-api": "error",
|
|
1372
|
+
"vue/no-deprecated-destroyed-lifecycle": "error",
|
|
1373
|
+
"vue/no-deprecated-model-definition": "error",
|
|
1374
|
+
"vue/no-deprecated-delete-set": "error",
|
|
1375
|
+
"vue/no-deprecated-vue-config-keycodes": "error",
|
|
1376
|
+
"vue/no-lifecycle-after-await": "error",
|
|
1377
|
+
"vue/no-this-in-before-route-enter": "error",
|
|
1378
|
+
"vue/return-in-computed-property": "error",
|
|
1379
|
+
"vue/valid-define-emits": "error",
|
|
1380
|
+
"vue/valid-define-props": "error",
|
|
1381
|
+
"vue/no-required-prop-with-default": "warn",
|
|
1382
|
+
"vue/prefer-import-from-vue": "warn",
|
|
1383
|
+
"vue/no-import-compiler-macros": "warn",
|
|
1384
|
+
"vue/no-multiple-slot-args": "warn",
|
|
1385
|
+
"vue/require-default-export": "warn",
|
|
1350
1386
|
"vue-doctor/no-em-dash-in-string": "warn",
|
|
1351
1387
|
"vue-doctor/no-non-null-assertion-on-ref-value": "warn",
|
|
1352
1388
|
"vue-doctor/no-imports-from-vue-when-auto-imported": "warn",
|
|
@@ -1369,6 +1405,27 @@ const NUXT_PLUGIN_RULES = {
|
|
|
1369
1405
|
const VUE_OXLINT_RULE_IDS = new Set([
|
|
1370
1406
|
"vue/no-export-in-script-setup",
|
|
1371
1407
|
"vue/require-typed-ref",
|
|
1408
|
+
"vue/no-arrow-functions-in-watch",
|
|
1409
|
+
"vue/no-deprecated-data-object-declaration",
|
|
1410
|
+
"vue/no-deprecated-events-api",
|
|
1411
|
+
"vue/no-deprecated-destroyed-lifecycle",
|
|
1412
|
+
"vue/no-deprecated-model-definition",
|
|
1413
|
+
"vue/no-deprecated-delete-set",
|
|
1414
|
+
"vue/no-deprecated-vue-config-keycodes",
|
|
1415
|
+
"vue/no-lifecycle-after-await",
|
|
1416
|
+
"vue/no-this-in-before-route-enter",
|
|
1417
|
+
"vue/return-in-computed-property",
|
|
1418
|
+
"vue/valid-define-emits",
|
|
1419
|
+
"vue/valid-define-props",
|
|
1420
|
+
"vue/no-required-prop-with-default",
|
|
1421
|
+
"vue/prefer-import-from-vue",
|
|
1422
|
+
"vue/no-import-compiler-macros",
|
|
1423
|
+
"vue/no-multiple-slot-args",
|
|
1424
|
+
"vue/require-default-export",
|
|
1425
|
+
"vue/define-emits-declaration",
|
|
1426
|
+
"vue/define-props-declaration",
|
|
1427
|
+
"vue/define-props-destructuring",
|
|
1428
|
+
"vue/max-props",
|
|
1372
1429
|
"vue-doctor/no-em-dash-in-string",
|
|
1373
1430
|
"vue-doctor/no-destructure-props-without-to-refs",
|
|
1374
1431
|
"vue-doctor/no-destructure-reactive-without-to-refs",
|
|
@@ -1543,9 +1600,10 @@ async function runOxlint(opts) {
|
|
|
1543
1600
|
"-c",
|
|
1544
1601
|
opts.configPath,
|
|
1545
1602
|
"--format",
|
|
1546
|
-
"json"
|
|
1547
|
-
opts.targetPath
|
|
1603
|
+
"json"
|
|
1548
1604
|
];
|
|
1605
|
+
if (opts.fix) args.push("--fix");
|
|
1606
|
+
args.push(opts.targetPath);
|
|
1549
1607
|
const child = spawn(opts.oxlintBin, args, {
|
|
1550
1608
|
cwd: opts.rootDir,
|
|
1551
1609
|
stdio: [
|
|
@@ -1647,7 +1705,8 @@ async function runScriptPass(opts) {
|
|
|
1647
1705
|
targetPath: opts.targetPath,
|
|
1648
1706
|
configPath,
|
|
1649
1707
|
oxlintBin,
|
|
1650
|
-
timeoutMs: opts.timeoutMs
|
|
1708
|
+
timeoutMs: opts.timeoutMs,
|
|
1709
|
+
fix: opts.fix
|
|
1651
1710
|
});
|
|
1652
1711
|
return {
|
|
1653
1712
|
diagnostics: toCanonicalDiagnostics(raw.diagnostics, opts.rootDir),
|
|
@@ -2618,7 +2677,9 @@ async function audit(config = {}) {
|
|
|
2618
2677
|
rootDir,
|
|
2619
2678
|
targetPath: rootDir,
|
|
2620
2679
|
ruleOverrides: config.rules,
|
|
2621
|
-
framework: project.framework === "nuxt" ? "nuxt" : "vue"
|
|
2680
|
+
framework: project.framework === "nuxt" ? "nuxt" : "vue",
|
|
2681
|
+
fix: config.fix === true && config.scopeFiles === void 0,
|
|
2682
|
+
fixExcludes: config.fixExcludes
|
|
2622
2683
|
});
|
|
2623
2684
|
scriptDiagnostics = result.diagnostics;
|
|
2624
2685
|
oxlintStderr = result.stderr;
|
|
@@ -2632,7 +2693,7 @@ async function audit(config = {}) {
|
|
|
2632
2693
|
if (config.deadCode !== false) {
|
|
2633
2694
|
const deadCodeStart = performance.now();
|
|
2634
2695
|
try {
|
|
2635
|
-
const { loadDoctorConfig } = await import("./load-
|
|
2696
|
+
const { loadDoctorConfig } = await import("./load-yXDi-5ti.js").then((n) => n.n);
|
|
2636
2697
|
deadCodeDiagnostics = dedupeDeadCodeAgainstLint(await checkDeadCode({
|
|
2637
2698
|
projectInfo: project,
|
|
2638
2699
|
doctorConfig: await loadDoctorConfig(rootDir),
|
|
@@ -2751,7 +2812,111 @@ function mergeCliOverrides(resolved, cli) {
|
|
|
2751
2812
|
threshold: cli.threshold ?? resolved.threshold,
|
|
2752
2813
|
rules,
|
|
2753
2814
|
source: resolved.source,
|
|
2754
|
-
configFile: resolved.configFile
|
|
2815
|
+
configFile: resolved.configFile,
|
|
2816
|
+
...cli.fixExcludes ?? resolved.fixExcludes ? { fixExcludes: cli.fixExcludes ?? resolved.fixExcludes } : {}
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
//#endregion
|
|
2820
|
+
//#region src/init/index.ts
|
|
2821
|
+
function parseExcludeList(raw) {
|
|
2822
|
+
if (!raw) return void 0;
|
|
2823
|
+
const parts = raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
2824
|
+
return parts.length > 0 ? parts : void 0;
|
|
2825
|
+
}
|
|
2826
|
+
function normalizeInitAnswers(raw) {
|
|
2827
|
+
return {
|
|
2828
|
+
configFormat: raw.target ?? "ts",
|
|
2829
|
+
preset: raw.preset ?? "recommended",
|
|
2830
|
+
threshold: typeof raw.threshold === "number" ? raw.threshold : void 0,
|
|
2831
|
+
exclude: parseExcludeList(raw.exclude)
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
function frameworkLabel(project) {
|
|
2835
|
+
if (project.framework === "nuxt") return `Nuxt ${project.nuxtVersion ?? "unknown"}`;
|
|
2836
|
+
if (project.framework === "vue") return `Vue ${project.vueVersion ?? "unknown"}`;
|
|
2837
|
+
return "unknown project";
|
|
2838
|
+
}
|
|
2839
|
+
function renderDetectSummary(project, sfcCount) {
|
|
2840
|
+
const ts = project.capabilities.has("typescript") ? " · TS" : "";
|
|
2841
|
+
const noun = sfcCount === 1 ? "SFC" : "SFCs";
|
|
2842
|
+
return `detected: ${frameworkLabel(project)}${ts} · ${sfcCount} ${noun}`;
|
|
2843
|
+
}
|
|
2844
|
+
async function detectSummary(dir) {
|
|
2845
|
+
return renderDetectSummary(await detectProject(dir), (await listSourceFiles({
|
|
2846
|
+
rootDir: dir,
|
|
2847
|
+
include: ["**/*.vue"],
|
|
2848
|
+
exclude: ["**/node_modules/**"]
|
|
2849
|
+
})).length);
|
|
2850
|
+
}
|
|
2851
|
+
function buildUserConfig(options) {
|
|
2852
|
+
const config = { preset: options.preset };
|
|
2853
|
+
if (options.threshold !== void 0) config.threshold = options.threshold;
|
|
2854
|
+
if (options.exclude && options.exclude.length > 0) config.exclude = options.exclude;
|
|
2855
|
+
return config;
|
|
2856
|
+
}
|
|
2857
|
+
function renderTsConfig(options) {
|
|
2858
|
+
const config = buildUserConfig(options);
|
|
2859
|
+
const lines = [
|
|
2860
|
+
"import { defineConfig } from '@geoql/doctor-core';",
|
|
2861
|
+
"",
|
|
2862
|
+
"export default defineConfig({",
|
|
2863
|
+
` preset: '${config.preset}',`
|
|
2864
|
+
];
|
|
2865
|
+
if (config.threshold !== void 0) lines.push(` threshold: ${config.threshold},`);
|
|
2866
|
+
if (config.exclude) {
|
|
2867
|
+
const globs = config.exclude.map((glob) => `'${glob}'`).join(", ");
|
|
2868
|
+
lines.push(` exclude: [${globs}],`);
|
|
2869
|
+
}
|
|
2870
|
+
lines.push("});");
|
|
2871
|
+
return `${lines.join("\n")}\n`;
|
|
2872
|
+
}
|
|
2873
|
+
function renderJsonConfig(options) {
|
|
2874
|
+
return `${JSON.stringify(buildUserConfig(options), null, 2)}\n`;
|
|
2875
|
+
}
|
|
2876
|
+
async function readPackageJsonRaw(path) {
|
|
2877
|
+
try {
|
|
2878
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
2879
|
+
} catch {
|
|
2880
|
+
return null;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
async function planInit(options) {
|
|
2884
|
+
const writes = [];
|
|
2885
|
+
const packageJsonPath = join(options.dir, "package.json");
|
|
2886
|
+
const pkg = { ...await readPackageJsonRaw(packageJsonPath) ?? {} };
|
|
2887
|
+
let conflict = false;
|
|
2888
|
+
let conflictPath = null;
|
|
2889
|
+
if (options.configFormat === "package-json") {
|
|
2890
|
+
if (pkg.doctor !== void 0) {
|
|
2891
|
+
conflict = true;
|
|
2892
|
+
conflictPath = packageJsonPath;
|
|
2893
|
+
}
|
|
2894
|
+
pkg.doctor = buildUserConfig(options);
|
|
2895
|
+
} else {
|
|
2896
|
+
const fileName = options.configFormat === "ts" ? "doctor.config.ts" : "doctor.config.json";
|
|
2897
|
+
const configPath = join(options.dir, fileName);
|
|
2898
|
+
if (await pathExists(configPath)) {
|
|
2899
|
+
conflict = true;
|
|
2900
|
+
conflictPath = configPath;
|
|
2901
|
+
}
|
|
2902
|
+
const content = options.configFormat === "ts" ? renderTsConfig(options) : renderJsonConfig(options);
|
|
2903
|
+
writes.push({
|
|
2904
|
+
path: configPath,
|
|
2905
|
+
content
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
pkg.scripts = {
|
|
2909
|
+
...pkg.scripts ?? {},
|
|
2910
|
+
"doctor:check": options.binName
|
|
2911
|
+
};
|
|
2912
|
+
writes.push({
|
|
2913
|
+
path: packageJsonPath,
|
|
2914
|
+
content: `${JSON.stringify(pkg, null, 2)}\n`
|
|
2915
|
+
});
|
|
2916
|
+
return {
|
|
2917
|
+
writes,
|
|
2918
|
+
conflict,
|
|
2919
|
+
conflictPath
|
|
2755
2920
|
};
|
|
2756
2921
|
}
|
|
2757
2922
|
//#endregion
|
|
@@ -2798,6 +2963,47 @@ async function listChangedFiles(options) {
|
|
|
2798
2963
|
return [...new Set(absolute)].sort();
|
|
2799
2964
|
}
|
|
2800
2965
|
//#endregion
|
|
2966
|
+
//#region src/project-info/list-workspace-packages.ts
|
|
2967
|
+
function parsePackageGlobs(yaml) {
|
|
2968
|
+
const globs = [];
|
|
2969
|
+
let inPackages = false;
|
|
2970
|
+
for (const raw of yaml.split("\n")) {
|
|
2971
|
+
if (!inPackages) {
|
|
2972
|
+
if (/^packages:/.test(raw)) inPackages = true;
|
|
2973
|
+
continue;
|
|
2974
|
+
}
|
|
2975
|
+
const item = raw.match(/^\s*-\s*(.+?)\s*$/);
|
|
2976
|
+
if (item) {
|
|
2977
|
+
globs.push(item[1].replace(/^['"]|['"]$/g, ""));
|
|
2978
|
+
continue;
|
|
2979
|
+
}
|
|
2980
|
+
if (raw.trim() === "") continue;
|
|
2981
|
+
break;
|
|
2982
|
+
}
|
|
2983
|
+
return globs;
|
|
2984
|
+
}
|
|
2985
|
+
async function listWorkspacePackages(rootDir) {
|
|
2986
|
+
const { root, kind } = await findMonorepoRoot(rootDir);
|
|
2987
|
+
if (kind !== "pnpm") return [];
|
|
2988
|
+
const manifests = await glob(parsePackageGlobs(await readFile(join(root, "pnpm-workspace.yaml"), "utf8")).map((g) => `${g}/package.json`), {
|
|
2989
|
+
cwd: root,
|
|
2990
|
+
absolute: true,
|
|
2991
|
+
onlyFiles: true,
|
|
2992
|
+
dot: false
|
|
2993
|
+
});
|
|
2994
|
+
const packages = [];
|
|
2995
|
+
for (const manifest of manifests) {
|
|
2996
|
+
const dir = dirname(manifest);
|
|
2997
|
+
const pkg = await readPackageJson(dir);
|
|
2998
|
+
if (pkg?.name) packages.push({
|
|
2999
|
+
name: pkg.name,
|
|
3000
|
+
dir
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
packages.sort((a, b) => a.name.localeCompare(b.name));
|
|
3004
|
+
return packages;
|
|
3005
|
+
}
|
|
3006
|
+
//#endregion
|
|
2801
3007
|
//#region src/reporters/docs-url.ts
|
|
2802
3008
|
function docsUrl(ruleId) {
|
|
2803
3009
|
return `https://docs.doctor.geoql.in/rules/${ruleId}`;
|
|
@@ -3162,6 +3368,53 @@ function jsonCompactReport(input) {
|
|
|
3162
3368
|
return `${JSON.stringify(buildDoctorReport(input))}\n`;
|
|
3163
3369
|
}
|
|
3164
3370
|
//#endregion
|
|
3371
|
+
//#region src/reporters/pr-comment.ts
|
|
3372
|
+
const DOCS_BASE = "https://docs.doctor.geoql.in/rules/";
|
|
3373
|
+
const MAX_PER_SECTION = 5;
|
|
3374
|
+
function binName(toolName) {
|
|
3375
|
+
return toolName.replace("@geoql/", "");
|
|
3376
|
+
}
|
|
3377
|
+
function relativeLocation(diag, rootDirectory) {
|
|
3378
|
+
return `${relative(rootDirectory, diag.file).replaceAll("\\", "/")}:${diag.line}`;
|
|
3379
|
+
}
|
|
3380
|
+
function findingLine(diag, rootDirectory) {
|
|
3381
|
+
return `- \`${relativeLocation(diag, rootDirectory)}\` ${diag.ruleId} — ${diag.message}`;
|
|
3382
|
+
}
|
|
3383
|
+
function sortBySeverityFilter(diagnostics, severity, rootDirectory) {
|
|
3384
|
+
return diagnostics.filter((d) => d.severity === severity).sort((a, b) => {
|
|
3385
|
+
const byFile = compareStrings(relative(rootDirectory, a.file).replaceAll("\\", "/"), relative(rootDirectory, b.file).replaceAll("\\", "/"));
|
|
3386
|
+
if (byFile !== 0) return byFile;
|
|
3387
|
+
if (a.line !== b.line) return a.line - b.line;
|
|
3388
|
+
if (a.column !== b.column) return a.column - b.column;
|
|
3389
|
+
return compareStrings(a.ruleId, b.ruleId);
|
|
3390
|
+
});
|
|
3391
|
+
}
|
|
3392
|
+
function section(title, diagnostics, rootDirectory) {
|
|
3393
|
+
if (diagnostics.length === 0) return [];
|
|
3394
|
+
const lines = ["", `### ${title}`];
|
|
3395
|
+
for (const d of diagnostics.slice(0, MAX_PER_SECTION)) lines.push(findingLine(d, rootDirectory));
|
|
3396
|
+
return lines;
|
|
3397
|
+
}
|
|
3398
|
+
function prCommentReport(input) {
|
|
3399
|
+
const score = input.score.score;
|
|
3400
|
+
const header = `## 🛡 ${input.toolName} — Score: ${score}`;
|
|
3401
|
+
const errors = sortBySeverityFilter(input.diagnostics, "error", input.rootDirectory);
|
|
3402
|
+
const warnings = sortBySeverityFilter(input.diagnostics, "warn", input.rootDirectory);
|
|
3403
|
+
const actionable = errors.length + warnings.length;
|
|
3404
|
+
if (actionable === 0) return `${header}\n\n✓ No actionable findings. Score: ${score}\n`;
|
|
3405
|
+
const topRule = (errors[0] ?? warnings[0]).ruleId;
|
|
3406
|
+
return `${[
|
|
3407
|
+
header,
|
|
3408
|
+
"",
|
|
3409
|
+
`**${actionable} findings** (${errors.length} errors, ${warnings.length} warnings)`,
|
|
3410
|
+
...section("Errors", errors, input.rootDirectory),
|
|
3411
|
+
...section("Warnings", warnings, input.rootDirectory),
|
|
3412
|
+
"",
|
|
3413
|
+
"---",
|
|
3414
|
+
`[View the docs](${DOCS_BASE}) · Run \`${binName(input.toolName)} explain ${topRule}\` for details.`
|
|
3415
|
+
].join("\n")}\n`;
|
|
3416
|
+
}
|
|
3417
|
+
//#endregion
|
|
3165
3418
|
//#region src/reporters/pretty.ts
|
|
3166
3419
|
const colors = pc.createColors(true);
|
|
3167
3420
|
const colorTheme = {
|
|
@@ -3212,9 +3465,10 @@ function format(input, kind = "agent", options) {
|
|
|
3212
3465
|
if (kind === "json-compact") return jsonCompactReport(input);
|
|
3213
3466
|
if (kind === "sarif") return sarifReport(input);
|
|
3214
3467
|
if (kind === "html") return htmlReport(input);
|
|
3468
|
+
if (kind === "pr-comment") return prCommentReport(input);
|
|
3215
3469
|
return agentReport(input, options);
|
|
3216
3470
|
}
|
|
3217
3471
|
//#endregion
|
|
3218
|
-
export { BUILT_IN_RECOMMENDED, ConfigCycleError, ConfigFileNotFoundError, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, InvalidConfigError, OxlintOutputTooLarge, OxlintSpawnFailed, RULE_REGISTRY, agentReport, applyInlineDisables, audit, buildDoctorReport, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, docsUrl, encodeAnnotation, encodeAnnotations, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, parseDirectives, prettyReport, renderVerboseTrace, runCrossFilePass, sarifReport, scoreDiagnostics, validateConfig };
|
|
3472
|
+
export { BUILT_IN_RECOMMENDED, ConfigCycleError, ConfigFileNotFoundError, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, InvalidConfigError, OxlintOutputTooLarge, OxlintSpawnFailed, RULE_REGISTRY, agentReport, applyInlineDisables, audit, buildDoctorReport, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, detectSummary, docsUrl, encodeAnnotation, encodeAnnotations, findMonorepoRoot, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, listWorkspacePackages, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, normalizeInitAnswers, parseDirectives, parseExcludeList, planInit, prCommentReport, prettyReport, renderDetectSummary, renderVerboseTrace, runCrossFilePass, sarifReport, scoreDiagnostics, validateConfig };
|
|
3219
3473
|
|
|
3220
3474
|
//# sourceMappingURL=index.js.map
|