@geoql/doctor-core 0.1.0-alpha.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vinayak Kulkarni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # `@geoql/doctor-core`
2
+
3
+ > Audit engine shared by [`@geoql/vue-doctor`](../vue-doctor) and (future) `@geoql/nuxt-doctor`.
4
+
5
+ Implements the **hybrid two-pass design** locked in [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md):
6
+
7
+ 1. **Template pass** — parses `.vue` SFCs with `@vue/compiler-sfc` and walks the template AST for rules that need template-level context (e.g. `v-for` missing `:key`).
8
+ 2. **Script pass** — spawns `oxlint` as a subprocess with a generated `.oxlintrc.json` that activates oxlint's built-in `vue` plugin AND a custom `jsPlugins` entry (`@geoql/oxlint-plugin-vue-doctor`).
9
+
10
+ Diagnostics from both passes are merged, deduped, and fed into a deterministic 0–100 score.
11
+
12
+ ## Usage (programmatic)
13
+
14
+ ```ts
15
+ import { audit } from '@geoql/doctor-core';
16
+
17
+ const report = await audit({
18
+ rootDir: process.cwd(),
19
+ include: ['**/*.vue', '**/*.ts'],
20
+ failOn: 'error',
21
+ });
22
+
23
+ for (const diag of report.diagnostics) {
24
+ console.log(
25
+ `${diag.file}:${diag.line}:${diag.column} ${diag.severity} ${diag.ruleId}: ${diag.message}`,
26
+ );
27
+ }
28
+ console.log(`Score: ${report.score}/100`);
29
+ ```
30
+
31
+ Most users should depend on `@geoql/vue-doctor` (the CLI) rather than this package directly.
32
+
33
+ ## License
34
+
35
+ MIT © Vinayak Kulkarni
@@ -0,0 +1,48 @@
1
+ //#region src/types.d.ts
2
+ type Severity = 'error' | 'warning';
3
+ type DiagnosticSource = 'template' | 'oxlint';
4
+ interface Diagnostic {
5
+ file: string;
6
+ line: number;
7
+ column: number;
8
+ endLine?: number;
9
+ endColumn?: number;
10
+ ruleId: string;
11
+ severity: Severity;
12
+ message: string;
13
+ source: DiagnosticSource;
14
+ recommendation?: string;
15
+ }
16
+ interface AuditConfig {
17
+ rootDir?: string;
18
+ include?: string[];
19
+ exclude?: string[];
20
+ rules?: Record<string, Severity | 'off'>;
21
+ failOn?: Severity;
22
+ }
23
+ interface AuditReport {
24
+ rootDir: string;
25
+ filesScanned: number;
26
+ diagnostics: Diagnostic[];
27
+ score: number;
28
+ errorCount: number;
29
+ warningCount: number;
30
+ exitCode: 0 | 1 | 2;
31
+ }
32
+ //#endregion
33
+ //#region src/audit.d.ts
34
+ declare function audit(config?: AuditConfig): Promise<AuditReport>;
35
+ //#endregion
36
+ //#region src/config.d.ts
37
+ interface LoadedConfig {
38
+ config: AuditConfig;
39
+ configFile?: string;
40
+ }
41
+ declare function loadAuditConfig(rootDir: string, explicitPath?: string): Promise<LoadedConfig>;
42
+ //#endregion
43
+ //#region src/reporters/index.d.ts
44
+ type ReporterFormat = 'text' | 'json';
45
+ declare function format(report: AuditReport, kind: ReporterFormat): string;
46
+ //#endregion
47
+ export { type AuditConfig, type AuditReport, type Diagnostic, type DiagnosticSource, type ReporterFormat, type Severity, audit, format, loadAuditConfig };
48
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,523 @@
1
+ import { dirname, join, relative, resolve } from "node:path";
2
+ import { glob } from "tinyglobby";
3
+ import { mkdtemp, readFile, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { spawn } from "node:child_process";
8
+ import { parse } from "@vue/compiler-sfc";
9
+ import { loadConfig } from "c12";
10
+ import * as c from "kolorist";
11
+ //#region src/file-scan.ts
12
+ async function listSourceFiles(opts) {
13
+ return (await glob(opts.include, {
14
+ cwd: opts.rootDir,
15
+ absolute: true,
16
+ ignore: opts.exclude,
17
+ onlyFiles: true,
18
+ dot: false
19
+ })).map((f) => resolve(f)).sort();
20
+ }
21
+ //#endregion
22
+ //#region src/merge-diagnostics.ts
23
+ function mergeDiagnostics(...batches) {
24
+ const seen = /* @__PURE__ */ new Set();
25
+ const out = [];
26
+ for (const batch of batches) for (const d of batch) {
27
+ const key = `${d.file}|${d.line}|${d.column}|${d.ruleId}`;
28
+ if (seen.has(key)) continue;
29
+ seen.add(key);
30
+ out.push(d);
31
+ }
32
+ out.sort((a, b) => {
33
+ if (a.file !== b.file) return a.file < b.file ? -1 : 1;
34
+ if (a.line !== b.line) return a.line - b.line;
35
+ if (a.column !== b.column) return a.column - b.column;
36
+ return a.ruleId < b.ruleId ? -1 : 1;
37
+ });
38
+ return out;
39
+ }
40
+ //#endregion
41
+ //#region src/oxlint/diagnostic.ts
42
+ const OXLINT_CODE_PATTERN = /^([a-z0-9_-]+)\(([a-z0-9_-]+)\)$/i;
43
+ function normalizeOxlintRuleId(raw) {
44
+ if (raw.code) {
45
+ const match = OXLINT_CODE_PATTERN.exec(raw.code);
46
+ if (match) return `${match[1]}/${match[2]}`;
47
+ return raw.code;
48
+ }
49
+ if (raw.rule) return raw.rule;
50
+ return "oxlint/unknown";
51
+ }
52
+ function toCanonicalDiagnostic(raw, rootDir) {
53
+ const ruleId = normalizeOxlintRuleId(raw);
54
+ const severity = raw.severity === "warning" ? "warning" : "error";
55
+ const primary = raw.labels?.[0]?.span;
56
+ const line = primary?.line ?? raw.start_line ?? 1;
57
+ const column = primary?.column ?? raw.start_column ?? 1;
58
+ return {
59
+ file: resolve(rootDir, raw.filename),
60
+ line,
61
+ column,
62
+ endLine: raw.end_line,
63
+ endColumn: raw.end_column,
64
+ ruleId,
65
+ severity,
66
+ message: raw.message,
67
+ source: "oxlint"
68
+ };
69
+ }
70
+ function toCanonicalDiagnostics(raws, rootDir) {
71
+ return raws.map((r) => toCanonicalDiagnostic(r, rootDir));
72
+ }
73
+ //#endregion
74
+ //#region src/oxlint/generate-config.ts
75
+ const DEFAULT_RULES = {
76
+ "vue/no-export-in-script-setup": "error",
77
+ "vue/require-typed-ref": "warning",
78
+ "vue-doctor/no-em-dash-in-string": "warning"
79
+ };
80
+ function toOxlintSeverity(s) {
81
+ if (s === "warning") return "warn";
82
+ return s;
83
+ }
84
+ async function generateOxlintConfig(input) {
85
+ const dir = await mkdtemp(join(tmpdir(), "geoql-doctor-"));
86
+ const merged = { ...DEFAULT_RULES };
87
+ if (input.ruleOverrides) for (const [id, sev] of Object.entries(input.ruleOverrides)) if (sev === "off") delete merged[id];
88
+ else merged[id] = sev;
89
+ const rules = {};
90
+ for (const [id, sev] of Object.entries(merged)) rules[id] = toOxlintSeverity(sev);
91
+ const config = {
92
+ $schema: "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
93
+ plugins: ["vue"],
94
+ jsPlugins: [input.pluginPath],
95
+ rules
96
+ };
97
+ const configPath = join(dir, ".oxlintrc.json");
98
+ await writeFile(configPath, JSON.stringify(config, null, 2));
99
+ return configPath;
100
+ }
101
+ //#endregion
102
+ //#region src/oxlint/resolve-plugin.ts
103
+ function readPkgMain(pkgJsonPath) {
104
+ try {
105
+ const raw = readFileSync(pkgJsonPath, "utf8");
106
+ const pkg = JSON.parse(raw);
107
+ const exp = pkg.exports?.["."];
108
+ if (exp?.import) return exp.import;
109
+ if (exp?.default) return exp.default;
110
+ if (pkg.module) return pkg.module;
111
+ if (pkg.main) return pkg.main;
112
+ return;
113
+ } catch {
114
+ return;
115
+ }
116
+ }
117
+ function lookUpwards(fromDir, relPath) {
118
+ let current = resolve(fromDir);
119
+ while (true) {
120
+ const candidate = resolve(current, relPath);
121
+ if (existsSync(candidate)) return candidate;
122
+ const parent = dirname(current);
123
+ if (parent === current) return void 0;
124
+ current = parent;
125
+ }
126
+ }
127
+ function resolveVueDoctorPluginPath(fromDir) {
128
+ const pkgJson = lookUpwards(fromDir, "node_modules/@geoql/oxlint-plugin-vue-doctor/package.json");
129
+ if (pkgJson) {
130
+ const main = readPkgMain(pkgJson) ?? "./dist/index.js";
131
+ return resolve(dirname(pkgJson), main);
132
+ }
133
+ if (typeof import.meta.resolve === "function") try {
134
+ const baseUrl = pathToFileURL(resolve(fromDir, "package.json")).href;
135
+ return fileURLToPath(import.meta.resolve("@geoql/oxlint-plugin-vue-doctor", baseUrl));
136
+ } catch {
137
+ return throwResolveError(fromDir);
138
+ }
139
+ return throwResolveError(fromDir);
140
+ }
141
+ function throwResolveError(fromDir) {
142
+ throw new Error(`Failed to resolve @geoql/oxlint-plugin-vue-doctor from ${fromDir}. Install it as a dependency of your project or use the bundled @geoql/vue-doctor CLI.`);
143
+ }
144
+ function resolveOxlintBin(fromDir) {
145
+ const pkgJson = lookUpwards(fromDir, "node_modules/oxlint/package.json");
146
+ if (pkgJson) return resolve(dirname(pkgJson), "bin/oxlint");
147
+ throw new Error(`Failed to resolve oxlint from ${fromDir}. Install oxlint as a dependency of your project.`);
148
+ }
149
+ //#endregion
150
+ //#region src/oxlint/spawn.ts
151
+ const DEFAULT_TIMEOUT_MS = 6e4;
152
+ async function runOxlint(opts) {
153
+ return new Promise((resolve, reject) => {
154
+ const args = [
155
+ "-c",
156
+ opts.configPath,
157
+ "--format",
158
+ "json",
159
+ opts.targetPath
160
+ ];
161
+ const child = spawn(opts.oxlintBin, args, {
162
+ cwd: opts.rootDir,
163
+ stdio: [
164
+ "ignore",
165
+ "pipe",
166
+ "pipe"
167
+ ]
168
+ });
169
+ const stdoutChunks = [];
170
+ const stderrChunks = [];
171
+ let timer;
172
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
173
+ if (timeoutMs > 0) timer = setTimeout(() => {
174
+ child.kill("SIGKILL");
175
+ reject(/* @__PURE__ */ new Error(`oxlint subprocess timed out after ${timeoutMs}ms`));
176
+ }, timeoutMs);
177
+ child.stdout.on("data", (chunk) => stdoutChunks.push(chunk));
178
+ child.stderr.on("data", (chunk) => stderrChunks.push(chunk));
179
+ child.on("error", (err) => {
180
+ if (timer) clearTimeout(timer);
181
+ reject(err);
182
+ });
183
+ child.on("close", (exitCode) => {
184
+ if (timer) clearTimeout(timer);
185
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
186
+ const stderr = Buffer.concat(stderrChunks).toString("utf8");
187
+ resolve({
188
+ diagnostics: parseOxlintJsonStream(stdout),
189
+ stderr,
190
+ exitCode
191
+ });
192
+ });
193
+ });
194
+ }
195
+ function parseOxlintJsonStream(stdout) {
196
+ const trimmed = stdout.trim();
197
+ if (!trimmed) return [];
198
+ try {
199
+ const parsed = JSON.parse(trimmed);
200
+ if (Array.isArray(parsed)) return parsed;
201
+ if (parsed.diagnostics) return parsed.diagnostics;
202
+ } catch {
203
+ return parseNdjson(trimmed);
204
+ }
205
+ return [];
206
+ }
207
+ function parseNdjson(text) {
208
+ const out = [];
209
+ for (const line of text.split("\n")) {
210
+ const t = line.trim();
211
+ if (!t) continue;
212
+ try {
213
+ const obj = JSON.parse(t);
214
+ if (obj && typeof obj === "object" && "message" in obj) out.push(obj);
215
+ } catch {}
216
+ }
217
+ return out;
218
+ }
219
+ //#endregion
220
+ //#region src/oxlint/run.ts
221
+ async function runScriptPass(opts) {
222
+ const pluginPath = resolveVueDoctorPluginPath(opts.rootDir);
223
+ const oxlintBin = resolveOxlintBin(opts.rootDir);
224
+ const configPath = await generateOxlintConfig({
225
+ pluginPath,
226
+ ruleOverrides: opts.ruleOverrides
227
+ });
228
+ const raw = await runOxlint({
229
+ rootDir: opts.rootDir,
230
+ targetPath: opts.targetPath,
231
+ configPath,
232
+ oxlintBin,
233
+ timeoutMs: opts.timeoutMs
234
+ });
235
+ return {
236
+ diagnostics: toCanonicalDiagnostics(raw.diagnostics, opts.rootDir),
237
+ stderr: raw.stderr,
238
+ exitCode: raw.exitCode
239
+ };
240
+ }
241
+ //#endregion
242
+ //#region src/score.ts
243
+ const ERROR_PENALTY = 10;
244
+ const WARNING_PENALTY = 2;
245
+ function scoreDiagnostics(diagnostics) {
246
+ let errorCount = 0;
247
+ let warningCount = 0;
248
+ for (const d of diagnostics) if (d.severity === "error") errorCount += 1;
249
+ else warningCount += 1;
250
+ const penalty = errorCount * ERROR_PENALTY + warningCount * WARNING_PENALTY;
251
+ return {
252
+ score: Math.max(0, 100 - penalty),
253
+ errorCount,
254
+ warningCount
255
+ };
256
+ }
257
+ //#endregion
258
+ //#region src/template/parse-sfc.ts
259
+ const cache = /* @__PURE__ */ new Map();
260
+ async function parseSfc(absPath) {
261
+ if (cache.has(absPath)) return cache.get(absPath) ?? null;
262
+ let source;
263
+ try {
264
+ source = await readFile(absPath, "utf8");
265
+ } catch {
266
+ cache.set(absPath, null);
267
+ return null;
268
+ }
269
+ const { descriptor, errors } = parse(source, { filename: absPath });
270
+ if (errors.length > 0 || !descriptor.template) {
271
+ cache.set(absPath, null);
272
+ return null;
273
+ }
274
+ cache.set(absPath, descriptor);
275
+ return descriptor;
276
+ }
277
+ //#endregion
278
+ //#region src/template/directive-helpers.ts
279
+ const NODE_DIRECTIVE = 7;
280
+ function findDirective(el, name) {
281
+ for (const prop of el.props) if (prop.type === NODE_DIRECTIVE && prop.name === name) return prop;
282
+ }
283
+ function findBindAttr(el, attrName) {
284
+ for (const prop of el.props) {
285
+ if (prop.type !== NODE_DIRECTIVE) continue;
286
+ const dir = prop;
287
+ if (dir.name !== "bind") continue;
288
+ if (dir.arg?.content === attrName) return dir;
289
+ }
290
+ }
291
+ function findStaticAttr(el, attrName) {
292
+ for (const prop of el.props) if (prop.type === 6 && prop.name === attrName) return prop;
293
+ }
294
+ //#endregion
295
+ //#region src/template/walk.ts
296
+ const NODE_ELEMENT = 1;
297
+ function isElementNode(node) {
298
+ return node !== null && node !== void 0 && node.type === NODE_ELEMENT;
299
+ }
300
+ function walkElements(root, visit) {
301
+ const stack = [...root.children];
302
+ while (stack.length > 0) {
303
+ const node = stack.pop();
304
+ if (!node) continue;
305
+ if (isElementNode(node)) {
306
+ visit(node);
307
+ if (node.children) for (const child of node.children) stack.push(child);
308
+ }
309
+ }
310
+ }
311
+ //#endregion
312
+ //#region src/template/rules/v-for-has-key.ts
313
+ function check$1(ctx) {
314
+ const diagnostics = [];
315
+ walkElements(ctx.template, (el) => {
316
+ if (!findDirective(el, "for")) return;
317
+ if (findBindAttr(el, "key") ?? findStaticAttr(el, "key")) return;
318
+ diagnostics.push({
319
+ file: ctx.file,
320
+ line: el.loc.start.line,
321
+ column: el.loc.start.column,
322
+ endLine: el.loc.end.line,
323
+ endColumn: el.loc.end.column,
324
+ ruleId: "vue-doctor/template/v-for-has-key",
325
+ severity: "error",
326
+ message: `<${el.tag}> uses v-for without :key. Vue cannot efficiently diff this list across renders.`,
327
+ source: "template",
328
+ recommendation: "Add :key with a stable, unique identifier from the iterated item."
329
+ });
330
+ });
331
+ return { diagnostics };
332
+ }
333
+ //#endregion
334
+ //#region src/template/rules/v-if-v-for-precedence.ts
335
+ function check(ctx) {
336
+ const diagnostics = [];
337
+ walkElements(ctx.template, (el) => {
338
+ const vIf = findDirective(el, "if");
339
+ const vFor = findDirective(el, "for");
340
+ if (!vIf || !vFor) return;
341
+ diagnostics.push({
342
+ file: ctx.file,
343
+ line: el.loc.start.line,
344
+ column: el.loc.start.column,
345
+ endLine: el.loc.end.line,
346
+ endColumn: el.loc.end.column,
347
+ ruleId: "vue-doctor/template/v-if-v-for-precedence",
348
+ severity: "error",
349
+ message: `<${el.tag}> uses v-if and v-for on the same element. In Vue 3, v-if binds tighter than v-for, so the condition cannot reference loop variables.`,
350
+ source: "template",
351
+ recommendation: "Move the v-if to a <template v-for> wrapper or to a parent element, or filter the iterated source in a computed."
352
+ });
353
+ });
354
+ return { diagnostics };
355
+ }
356
+ //#endregion
357
+ //#region src/template/rules/index.ts
358
+ const TEMPLATE_RULES = [{
359
+ id: "vue-doctor/template/v-for-has-key",
360
+ check: check$1
361
+ }, {
362
+ id: "vue-doctor/template/v-if-v-for-precedence",
363
+ check
364
+ }];
365
+ //#endregion
366
+ //#region src/template/run.ts
367
+ async function runTemplatePass(opts) {
368
+ const all = [];
369
+ for (const file of opts.files) {
370
+ if (!file.endsWith(".vue")) continue;
371
+ const descriptor = await parseSfc(file);
372
+ if (!descriptor?.template?.ast) continue;
373
+ for (const rule of TEMPLATE_RULES) {
374
+ const override = opts.ruleOverrides?.[rule.id];
375
+ if (override === "off") continue;
376
+ const { diagnostics } = rule.check({
377
+ file,
378
+ template: descriptor.template.ast
379
+ });
380
+ for (const d of diagnostics) all.push(override ? {
381
+ ...d,
382
+ severity: override
383
+ } : d);
384
+ }
385
+ }
386
+ return all;
387
+ }
388
+ //#endregion
389
+ //#region src/audit.ts
390
+ const DEFAULT_INCLUDE = [
391
+ "**/*.vue",
392
+ "**/*.ts",
393
+ "**/*.tsx",
394
+ "**/*.js",
395
+ "**/*.jsx"
396
+ ];
397
+ const DEFAULT_EXCLUDE = [
398
+ "node_modules",
399
+ "dist",
400
+ ".nuxt",
401
+ ".output",
402
+ "coverage"
403
+ ];
404
+ async function audit(config = {}) {
405
+ const rootDir = resolve(config.rootDir ?? process.cwd());
406
+ const include = config.include ?? DEFAULT_INCLUDE;
407
+ const exclude = config.exclude ?? DEFAULT_EXCLUDE;
408
+ const failOn = config.failOn ?? "error";
409
+ const files = await listSourceFiles({
410
+ rootDir,
411
+ include,
412
+ exclude
413
+ });
414
+ const templateDiagnostics = await runTemplatePass({
415
+ files,
416
+ ruleOverrides: config.rules
417
+ });
418
+ let scriptDiagnostics = [];
419
+ let oxlintStderr = "";
420
+ try {
421
+ const result = await runScriptPass({
422
+ rootDir,
423
+ targetPath: rootDir,
424
+ ruleOverrides: config.rules
425
+ });
426
+ scriptDiagnostics = result.diagnostics;
427
+ oxlintStderr = result.stderr;
428
+ } catch (err) {
429
+ oxlintStderr = err instanceof Error ? err.message : String(err);
430
+ if (process.env.DOCTOR_DEBUG) process.stderr.write(`[doctor-core] script pass failed: ${oxlintStderr}\n`);
431
+ }
432
+ const merged = mergeDiagnostics(templateDiagnostics, scriptDiagnostics);
433
+ const { score, errorCount, warningCount } = scoreDiagnostics(merged);
434
+ let exitCode = 0;
435
+ if (oxlintStderr && scriptDiagnostics.length === 0 && oxlintStderr.includes("Failed")) exitCode = 2;
436
+ else if ((failOn === "warning" ? errorCount + warningCount : errorCount) > 0) exitCode = 1;
437
+ return {
438
+ rootDir,
439
+ filesScanned: files.length,
440
+ diagnostics: merged,
441
+ score,
442
+ errorCount,
443
+ warningCount,
444
+ exitCode
445
+ };
446
+ }
447
+ //#endregion
448
+ //#region src/config.ts
449
+ const DEFAULTS = {
450
+ include: [
451
+ "**/*.vue",
452
+ "**/*.ts",
453
+ "**/*.tsx",
454
+ "**/*.js",
455
+ "**/*.jsx"
456
+ ],
457
+ exclude: [
458
+ "node_modules",
459
+ "dist",
460
+ ".nuxt",
461
+ ".output",
462
+ "coverage"
463
+ ],
464
+ failOn: "error"
465
+ };
466
+ async function loadAuditConfig(rootDir, explicitPath) {
467
+ const { config, configFile } = await loadConfig({
468
+ cwd: rootDir,
469
+ name: "doctor",
470
+ configFile: explicitPath,
471
+ defaults: DEFAULTS
472
+ });
473
+ return {
474
+ config: {
475
+ rootDir,
476
+ ...config
477
+ },
478
+ configFile
479
+ };
480
+ }
481
+ //#endregion
482
+ //#region src/reporters/json.ts
483
+ function formatJson(report) {
484
+ return JSON.stringify(report, null, 2);
485
+ }
486
+ //#endregion
487
+ //#region src/reporters/text.ts
488
+ function formatText(report) {
489
+ const lines = [];
490
+ const groups = /* @__PURE__ */ new Map();
491
+ for (const d of report.diagnostics) {
492
+ const arr = groups.get(d.file) ?? [];
493
+ arr.push(d);
494
+ groups.set(d.file, arr);
495
+ }
496
+ for (const [file, diags] of groups) {
497
+ const rel = relative(report.rootDir, file) || file;
498
+ lines.push(c.underline(c.bold(rel)));
499
+ for (const d of diags) {
500
+ const sev = d.severity === "error" ? c.red("error") : c.yellow("warning");
501
+ const loc = c.dim(`${d.line}:${d.column}`);
502
+ const rule = c.dim(`(${d.ruleId})`);
503
+ lines.push(` ${loc} ${sev} ${d.message} ${rule}`);
504
+ if (d.recommendation) lines.push(` ${c.dim("hint:")} ${d.recommendation}`);
505
+ }
506
+ lines.push("");
507
+ }
508
+ const summary = `${report.errorCount} error${report.errorCount === 1 ? "" : "s"}, ${report.warningCount} warning${report.warningCount === 1 ? "" : "s"} in ${report.filesScanned} file${report.filesScanned === 1 ? "" : "s"}`;
509
+ const scoreLine = `Score: ${c.bold(String(report.score))}/100`;
510
+ lines.push(summary);
511
+ lines.push(scoreLine);
512
+ return lines.join("\n");
513
+ }
514
+ //#endregion
515
+ //#region src/reporters/index.ts
516
+ function format(report, kind) {
517
+ if (kind === "json") return formatJson(report);
518
+ return formatText(report);
519
+ }
520
+ //#endregion
521
+ export { audit, format, loadAuditConfig };
522
+
523
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["resolvePath","check","vForHasKey","vIfVForPrecedence"],"sources":["../src/file-scan.ts","../src/merge-diagnostics.ts","../src/oxlint/diagnostic.ts","../src/oxlint/generate-config.ts","../src/oxlint/resolve-plugin.ts","../src/oxlint/spawn.ts","../src/oxlint/run.ts","../src/score.ts","../src/template/parse-sfc.ts","../src/template/directive-helpers.ts","../src/template/walk.ts","../src/template/rules/v-for-has-key.ts","../src/template/rules/v-if-v-for-precedence.ts","../src/template/rules/index.ts","../src/template/run.ts","../src/audit.ts","../src/config.ts","../src/reporters/json.ts","../src/reporters/text.ts","../src/reporters/index.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { glob } from 'tinyglobby';\n\nexport interface ScanOptions {\n rootDir: string;\n include: string[];\n exclude: string[];\n}\n\nexport async function listSourceFiles(opts: ScanOptions): Promise<string[]> {\n const files = await glob(opts.include, {\n cwd: opts.rootDir,\n absolute: true,\n ignore: opts.exclude,\n onlyFiles: true,\n dot: false,\n });\n return files.map((f) => resolve(f)).sort();\n}\n","import type { Diagnostic } from './types.js';\n\nexport function mergeDiagnostics(...batches: Diagnostic[][]): Diagnostic[] {\n const seen = new Set<string>();\n const out: Diagnostic[] = [];\n for (const batch of batches) {\n for (const d of batch) {\n const key = `${d.file}|${d.line}|${d.column}|${d.ruleId}`;\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(d);\n }\n }\n out.sort((a, b) => {\n if (a.file !== b.file) return a.file < b.file ? -1 : 1;\n if (a.line !== b.line) return a.line - b.line;\n if (a.column !== b.column) return a.column - b.column;\n return a.ruleId < b.ruleId ? -1 : 1;\n });\n return out;\n}\n","import { resolve } from 'node:path';\nimport type { Diagnostic } from '../types.js';\nimport type { OxlintRawDiagnostic } from './types.js';\n\nconst OXLINT_CODE_PATTERN = /^([a-z0-9_-]+)\\(([a-z0-9_-]+)\\)$/i;\n\nexport function normalizeOxlintRuleId(raw: OxlintRawDiagnostic): string {\n if (raw.code) {\n const match = OXLINT_CODE_PATTERN.exec(raw.code);\n if (match) return `${match[1]}/${match[2]}`;\n return raw.code;\n }\n if (raw.rule) return raw.rule;\n return 'oxlint/unknown';\n}\n\nexport function toCanonicalDiagnostic(\n raw: OxlintRawDiagnostic,\n rootDir: string,\n): Diagnostic {\n const ruleId = normalizeOxlintRuleId(raw);\n const severity = raw.severity === 'warning' ? 'warning' : 'error';\n const primary = raw.labels?.[0]?.span;\n const line = primary?.line ?? raw.start_line ?? 1;\n const column = primary?.column ?? raw.start_column ?? 1;\n return {\n file: resolve(rootDir, raw.filename),\n line,\n column,\n endLine: raw.end_line,\n endColumn: raw.end_column,\n ruleId,\n severity,\n message: raw.message,\n source: 'oxlint',\n };\n}\n\nexport function toCanonicalDiagnostics(\n raws: OxlintRawDiagnostic[],\n rootDir: string,\n): Diagnostic[] {\n return raws.map((r) => toCanonicalDiagnostic(r, rootDir));\n}\n","import { writeFile, mkdtemp } from 'node:fs/promises';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport type { Severity } from '../types.js';\n\nexport interface GenerateConfigInput {\n pluginPath: string;\n ruleOverrides?: Record<string, Severity | 'off'>;\n}\n\nconst DEFAULT_RULES: Record<string, Severity> = {\n 'vue/no-export-in-script-setup': 'error',\n 'vue/require-typed-ref': 'warning',\n 'vue-doctor/no-em-dash-in-string': 'warning',\n};\n\nfunction toOxlintSeverity(s: Severity | 'off'): 'error' | 'warn' | 'off' {\n if (s === 'warning') return 'warn';\n return s;\n}\n\nexport async function generateOxlintConfig(\n input: GenerateConfigInput,\n): Promise<string> {\n const dir = await mkdtemp(join(tmpdir(), 'geoql-doctor-'));\n const merged: Record<string, Severity> = { ...DEFAULT_RULES };\n if (input.ruleOverrides) {\n for (const [id, sev] of Object.entries(input.ruleOverrides)) {\n if (sev === 'off') delete merged[id];\n else merged[id] = sev;\n }\n }\n const rules: Record<string, 'error' | 'warn'> = {};\n for (const [id, sev] of Object.entries(merged)) {\n rules[id] = toOxlintSeverity(sev) as 'error' | 'warn';\n }\n const config = {\n $schema:\n 'https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json',\n plugins: ['vue'],\n jsPlugins: [input.pluginPath],\n rules,\n };\n const configPath = join(dir, '.oxlintrc.json');\n await writeFile(configPath, JSON.stringify(config, null, 2));\n return configPath;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { dirname, resolve as resolvePath } from 'node:path';\nimport { fileURLToPath, pathToFileURL } from 'node:url';\n\nfunction readPkgMain(pkgJsonPath: string): string | undefined {\n try {\n const raw = readFileSync(pkgJsonPath, 'utf8');\n const pkg = JSON.parse(raw) as {\n exports?: {\n '.'?: { import?: string; default?: string };\n default?: string;\n };\n module?: string;\n main?: string;\n };\n const exp = pkg.exports?.['.'];\n if (exp?.import) return exp.import;\n if (exp?.default) return exp.default;\n if (pkg.module) return pkg.module;\n if (pkg.main) return pkg.main;\n return undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction lookUpwards(fromDir: string, relPath: string): string | undefined {\n let current = resolvePath(fromDir);\n // Bounded ascent: stop when dirname() returns the same value (filesystem root).\n while (true) {\n const candidate = resolvePath(current, relPath);\n if (existsSync(candidate)) return candidate;\n const parent = dirname(current);\n if (parent === current) return undefined;\n current = parent;\n }\n}\n\nexport function resolveVueDoctorPluginPath(fromDir: string): string {\n const pkgJson = lookUpwards(\n fromDir,\n 'node_modules/@geoql/oxlint-plugin-vue-doctor/package.json',\n );\n if (pkgJson) {\n const main = readPkgMain(pkgJson) ?? './dist/index.js';\n return resolvePath(dirname(pkgJson), main);\n }\n if (typeof import.meta.resolve === 'function') {\n try {\n const baseUrl = pathToFileURL(resolvePath(fromDir, 'package.json')).href;\n const resolved = import.meta.resolve(\n '@geoql/oxlint-plugin-vue-doctor',\n baseUrl,\n );\n return fileURLToPath(resolved);\n } catch {\n return throwResolveError(fromDir);\n }\n }\n return throwResolveError(fromDir);\n}\n\nfunction throwResolveError(fromDir: string): never {\n throw new Error(\n `Failed to resolve @geoql/oxlint-plugin-vue-doctor from ${fromDir}. Install it as a dependency of your project or use the bundled @geoql/vue-doctor CLI.`,\n );\n}\n\nexport function resolveOxlintBin(fromDir: string): string {\n const pkgJson = lookUpwards(fromDir, 'node_modules/oxlint/package.json');\n if (pkgJson) {\n return resolvePath(dirname(pkgJson), 'bin/oxlint');\n }\n throw new Error(\n `Failed to resolve oxlint from ${fromDir}. Install oxlint as a dependency of your project.`,\n );\n}\n","import { spawn } from 'node:child_process';\nimport type {\n OxlintRawDiagnostic,\n OxlintRunOptions,\n OxlintRunResult,\n} from './types.js';\n\nconst DEFAULT_TIMEOUT_MS = 60_000;\n\ninterface OxlintJsonReport {\n diagnostics?: OxlintRawDiagnostic[];\n}\n\nexport async function runOxlint(\n opts: OxlintRunOptions,\n): Promise<OxlintRunResult> {\n return new Promise<OxlintRunResult>((resolve, reject) => {\n const args = ['-c', opts.configPath, '--format', 'json', opts.targetPath];\n const child = spawn(opts.oxlintBin, args, {\n cwd: opts.rootDir,\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n const stdoutChunks: Buffer[] = [];\n const stderrChunks: Buffer[] = [];\n let timer: NodeJS.Timeout | undefined;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n if (timeoutMs > 0) {\n timer = setTimeout(() => {\n child.kill('SIGKILL');\n reject(new Error(`oxlint subprocess timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n }\n child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));\n child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));\n child.on('error', (err) => {\n if (timer) clearTimeout(timer);\n reject(err);\n });\n child.on('close', (exitCode) => {\n if (timer) clearTimeout(timer);\n const stdout = Buffer.concat(stdoutChunks).toString('utf8');\n const stderr = Buffer.concat(stderrChunks).toString('utf8');\n const diagnostics = parseOxlintJsonStream(stdout);\n resolve({ diagnostics, stderr, exitCode });\n });\n });\n}\n\nfunction parseOxlintJsonStream(stdout: string): OxlintRawDiagnostic[] {\n const trimmed = stdout.trim();\n if (!trimmed) return [];\n try {\n const parsed = JSON.parse(trimmed) as\n | OxlintJsonReport\n | OxlintRawDiagnostic[];\n if (Array.isArray(parsed)) return parsed;\n if (parsed.diagnostics) return parsed.diagnostics;\n } catch {\n return parseNdjson(trimmed);\n }\n return [];\n}\n\nfunction parseNdjson(text: string): OxlintRawDiagnostic[] {\n const out: OxlintRawDiagnostic[] = [];\n for (const line of text.split('\\n')) {\n const t = line.trim();\n if (!t) continue;\n try {\n const obj = JSON.parse(t) as OxlintRawDiagnostic;\n if (obj && typeof obj === 'object' && 'message' in obj) out.push(obj);\n } catch {\n // skip non-json line\n }\n }\n return out;\n}\n","import type { Diagnostic, Severity } from '../types.js';\nimport { toCanonicalDiagnostics } from './diagnostic.js';\nimport { generateOxlintConfig } from './generate-config.js';\nimport {\n resolveOxlintBin,\n resolveVueDoctorPluginPath,\n} from './resolve-plugin.js';\nimport { runOxlint } from './spawn.js';\n\nexport interface ScriptPassOptions {\n rootDir: string;\n targetPath: string;\n ruleOverrides?: Record<string, Severity | 'off'>;\n timeoutMs?: number;\n}\n\nexport interface ScriptPassResult {\n diagnostics: Diagnostic[];\n stderr: string;\n exitCode: number | null;\n}\n\nexport async function runScriptPass(\n opts: ScriptPassOptions,\n): Promise<ScriptPassResult> {\n const pluginPath = resolveVueDoctorPluginPath(opts.rootDir);\n const oxlintBin = resolveOxlintBin(opts.rootDir);\n const configPath = await generateOxlintConfig({\n pluginPath,\n ruleOverrides: opts.ruleOverrides,\n });\n const raw = await runOxlint({\n rootDir: opts.rootDir,\n targetPath: opts.targetPath,\n configPath,\n oxlintBin,\n timeoutMs: opts.timeoutMs,\n });\n return {\n diagnostics: toCanonicalDiagnostics(raw.diagnostics, opts.rootDir),\n stderr: raw.stderr,\n exitCode: raw.exitCode,\n };\n}\n","import type { Diagnostic } from './types.js';\n\nconst ERROR_PENALTY = 10;\nconst WARNING_PENALTY = 2;\n\nexport interface ScoreBreakdown {\n score: number;\n errorCount: number;\n warningCount: number;\n}\n\nexport function scoreDiagnostics(diagnostics: Diagnostic[]): ScoreBreakdown {\n let errorCount = 0;\n let warningCount = 0;\n for (const d of diagnostics) {\n if (d.severity === 'error') errorCount += 1;\n else warningCount += 1;\n }\n const penalty = errorCount * ERROR_PENALTY + warningCount * WARNING_PENALTY;\n const score = Math.max(0, 100 - penalty);\n return { score, errorCount, warningCount };\n}\n","import { readFile } from 'node:fs/promises';\nimport { parse, type SFCDescriptor } from '@vue/compiler-sfc';\n\nconst cache = new Map<string, SFCDescriptor | null>();\n\nexport async function parseSfc(absPath: string): Promise<SFCDescriptor | null> {\n if (cache.has(absPath)) return cache.get(absPath) ?? null;\n let source: string;\n try {\n source = await readFile(absPath, 'utf8');\n } catch {\n cache.set(absPath, null);\n return null;\n }\n const { descriptor, errors } = parse(source, { filename: absPath });\n if (errors.length > 0 || !descriptor.template) {\n cache.set(absPath, null);\n return null;\n }\n cache.set(absPath, descriptor);\n return descriptor;\n}\n\nexport function clearSfcCache(): void {\n cache.clear();\n}\n","import type {\n AttributeNode,\n DirectiveNode,\n ElementNode,\n} from '@vue/compiler-core';\n\nconst NODE_DIRECTIVE = 7;\n\nexport function findDirective(\n el: ElementNode,\n name: string,\n): DirectiveNode | undefined {\n for (const prop of el.props) {\n if (prop.type === NODE_DIRECTIVE && prop.name === name)\n return prop as DirectiveNode;\n }\n return undefined;\n}\n\nexport function findBindAttr(\n el: ElementNode,\n attrName: string,\n): DirectiveNode | undefined {\n for (const prop of el.props) {\n if (prop.type !== NODE_DIRECTIVE) continue;\n const dir = prop as DirectiveNode;\n if (dir.name !== 'bind') continue;\n const arg = dir.arg as { content?: string } | undefined;\n if (arg?.content === attrName) return dir;\n }\n return undefined;\n}\n\nexport function findStaticAttr(\n el: ElementNode,\n attrName: string,\n): AttributeNode | undefined {\n for (const prop of el.props) {\n if (prop.type === 6 && (prop as AttributeNode).name === attrName) {\n return prop as AttributeNode;\n }\n }\n return undefined;\n}\n","import type {\n ElementNode,\n RootNode,\n TemplateChildNode,\n} from '@vue/compiler-core';\n\nconst NODE_ELEMENT = 1;\n\nexport function isElementNode(\n node: { type: number } | undefined | null,\n): node is ElementNode {\n return node !== null && node !== undefined && node.type === NODE_ELEMENT;\n}\n\nexport type ElementVisitor = (node: ElementNode) => void;\n\nexport function walkElements(root: RootNode, visit: ElementVisitor): void {\n const stack: TemplateChildNode[] = [...root.children];\n while (stack.length > 0) {\n const node = stack.pop();\n if (!node) continue;\n if (isElementNode(node)) {\n visit(node);\n if (node.children) {\n for (const child of node.children) stack.push(child);\n }\n }\n }\n}\n","import type { ElementNode } from '@vue/compiler-core';\nimport type { Diagnostic } from '../../types.js';\nimport {\n findBindAttr,\n findDirective,\n findStaticAttr,\n} from '../directive-helpers.js';\nimport { walkElements } from '../walk.js';\nimport type { TemplateRuleContext, TemplateRuleResult } from './types.js';\n\nexport function check(ctx: TemplateRuleContext): TemplateRuleResult {\n const diagnostics: Diagnostic[] = [];\n walkElements(ctx.template, (el: ElementNode) => {\n const vFor = findDirective(el, 'for');\n if (!vFor) return;\n const hasKey = findBindAttr(el, 'key') ?? findStaticAttr(el, 'key');\n if (hasKey) return;\n diagnostics.push({\n file: ctx.file,\n line: el.loc.start.line,\n column: el.loc.start.column,\n endLine: el.loc.end.line,\n endColumn: el.loc.end.column,\n ruleId: 'vue-doctor/template/v-for-has-key',\n severity: 'error',\n message: `<${el.tag}> uses v-for without :key. Vue cannot efficiently diff this list across renders.`,\n source: 'template',\n recommendation:\n 'Add :key with a stable, unique identifier from the iterated item.',\n });\n });\n return { diagnostics };\n}\n","import type { ElementNode } from '@vue/compiler-core';\nimport type { Diagnostic } from '../../types.js';\nimport { findDirective } from '../directive-helpers.js';\nimport { walkElements } from '../walk.js';\nimport type { TemplateRuleContext, TemplateRuleResult } from './types.js';\n\nexport function check(ctx: TemplateRuleContext): TemplateRuleResult {\n const diagnostics: Diagnostic[] = [];\n walkElements(ctx.template, (el: ElementNode) => {\n const vIf = findDirective(el, 'if');\n const vFor = findDirective(el, 'for');\n if (!vIf || !vFor) return;\n diagnostics.push({\n file: ctx.file,\n line: el.loc.start.line,\n column: el.loc.start.column,\n endLine: el.loc.end.line,\n endColumn: el.loc.end.column,\n ruleId: 'vue-doctor/template/v-if-v-for-precedence',\n severity: 'error',\n message: `<${el.tag}> uses v-if and v-for on the same element. In Vue 3, v-if binds tighter than v-for, so the condition cannot reference loop variables.`,\n source: 'template',\n recommendation:\n 'Move the v-if to a <template v-for> wrapper or to a parent element, or filter the iterated source in a computed.',\n });\n });\n return { diagnostics };\n}\n","import { check as vForHasKey } from './v-for-has-key.js';\nimport { check as vIfVForPrecedence } from './v-if-v-for-precedence.js';\nimport type { TemplateRule } from './types.js';\n\nexport const TEMPLATE_RULES: TemplateRule[] = [\n { id: 'vue-doctor/template/v-for-has-key', check: vForHasKey },\n { id: 'vue-doctor/template/v-if-v-for-precedence', check: vIfVForPrecedence },\n];\n","import type { Diagnostic, Severity } from '../types.js';\nimport { parseSfc } from './parse-sfc.js';\nimport { TEMPLATE_RULES } from './rules/index.js';\n\nexport interface TemplatePassOptions {\n files: string[];\n ruleOverrides?: Record<string, Severity | 'off'>;\n}\n\nexport async function runTemplatePass(\n opts: TemplatePassOptions,\n): Promise<Diagnostic[]> {\n const all: Diagnostic[] = [];\n for (const file of opts.files) {\n if (!file.endsWith('.vue')) continue;\n const descriptor = await parseSfc(file);\n if (!descriptor?.template?.ast) continue;\n for (const rule of TEMPLATE_RULES) {\n const override = opts.ruleOverrides?.[rule.id];\n if (override === 'off') continue;\n const { diagnostics } = rule.check({\n file,\n template: descriptor.template.ast,\n });\n for (const d of diagnostics) {\n all.push(override ? { ...d, severity: override } : d);\n }\n }\n }\n return all;\n}\n","import { resolve } from 'node:path';\nimport { listSourceFiles } from './file-scan.js';\nimport { mergeDiagnostics } from './merge-diagnostics.js';\nimport { runScriptPass } from './oxlint/run.js';\nimport { scoreDiagnostics } from './score.js';\nimport { runTemplatePass } from './template/run.js';\nimport type { AuditConfig, AuditReport, Diagnostic } from './types.js';\n\nconst DEFAULT_INCLUDE = [\n '**/*.vue',\n '**/*.ts',\n '**/*.tsx',\n '**/*.js',\n '**/*.jsx',\n];\nconst DEFAULT_EXCLUDE = [\n 'node_modules',\n 'dist',\n '.nuxt',\n '.output',\n 'coverage',\n];\n\nexport async function audit(config: AuditConfig = {}): Promise<AuditReport> {\n const rootDir = resolve(config.rootDir ?? process.cwd());\n const include = config.include ?? DEFAULT_INCLUDE;\n const exclude = config.exclude ?? DEFAULT_EXCLUDE;\n const failOn = config.failOn ?? 'error';\n\n const files = await listSourceFiles({ rootDir, include, exclude });\n\n const templateDiagnostics = await runTemplatePass({\n files,\n ruleOverrides: config.rules,\n });\n\n let scriptDiagnostics: Diagnostic[] = [];\n let oxlintStderr = '';\n try {\n const result = await runScriptPass({\n rootDir,\n targetPath: rootDir,\n ruleOverrides: config.rules,\n });\n scriptDiagnostics = result.diagnostics;\n oxlintStderr = result.stderr;\n } catch (err) {\n oxlintStderr = err instanceof Error ? err.message : String(err);\n if (process.env.DOCTOR_DEBUG) {\n process.stderr.write(\n `[doctor-core] script pass failed: ${oxlintStderr}\\n`,\n );\n }\n }\n\n const merged = mergeDiagnostics(templateDiagnostics, scriptDiagnostics);\n const { score, errorCount, warningCount } = scoreDiagnostics(merged);\n\n let exitCode: 0 | 1 | 2 = 0;\n if (\n oxlintStderr &&\n scriptDiagnostics.length === 0 &&\n oxlintStderr.includes('Failed')\n ) {\n exitCode = 2;\n } else {\n const tripping =\n failOn === 'warning' ? errorCount + warningCount : errorCount;\n if (tripping > 0) exitCode = 1;\n }\n\n return {\n rootDir,\n filesScanned: files.length,\n diagnostics: merged,\n score,\n errorCount,\n warningCount,\n exitCode,\n };\n}\n","import { loadConfig } from 'c12';\nimport type { AuditConfig } from './types.js';\n\nconst DEFAULTS: Required<Pick<AuditConfig, 'include' | 'exclude' | 'failOn'>> =\n {\n include: ['**/*.vue', '**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],\n exclude: ['node_modules', 'dist', '.nuxt', '.output', 'coverage'],\n failOn: 'error',\n };\n\nexport interface LoadedConfig {\n config: AuditConfig;\n configFile?: string;\n}\n\nexport async function loadAuditConfig(\n rootDir: string,\n explicitPath?: string,\n): Promise<LoadedConfig> {\n const { config, configFile } = await loadConfig<AuditConfig>({\n cwd: rootDir,\n name: 'doctor',\n configFile: explicitPath,\n defaults: DEFAULTS as AuditConfig,\n });\n return { config: { rootDir, ...config }, configFile };\n}\n","import type { AuditReport } from '../types.js';\n\nexport function formatJson(report: AuditReport): string {\n return JSON.stringify(report, null, 2);\n}\n","import { relative } from 'node:path';\nimport * as c from 'kolorist';\nimport type { AuditReport } from '../types.js';\n\nexport function formatText(report: AuditReport): string {\n const lines: string[] = [];\n const groups = new Map<string, AuditReport['diagnostics']>();\n for (const d of report.diagnostics) {\n const arr = groups.get(d.file) ?? [];\n arr.push(d);\n groups.set(d.file, arr);\n }\n for (const [file, diags] of groups) {\n const rel = relative(report.rootDir, file) || file;\n lines.push(c.underline(c.bold(rel)));\n for (const d of diags) {\n const sev = d.severity === 'error' ? c.red('error') : c.yellow('warning');\n const loc = c.dim(`${d.line}:${d.column}`);\n const rule = c.dim(`(${d.ruleId})`);\n lines.push(` ${loc} ${sev} ${d.message} ${rule}`);\n if (d.recommendation)\n lines.push(` ${c.dim('hint:')} ${d.recommendation}`);\n }\n lines.push('');\n }\n const summary = `${report.errorCount} error${report.errorCount === 1 ? '' : 's'}, ${report.warningCount} warning${report.warningCount === 1 ? '' : 's'} in ${report.filesScanned} file${report.filesScanned === 1 ? '' : 's'}`;\n const scoreLine = `Score: ${c.bold(String(report.score))}/100`;\n lines.push(summary);\n lines.push(scoreLine);\n return lines.join('\\n');\n}\n","import type { AuditReport } from '../types.js';\nimport { formatJson } from './json.js';\nimport { formatText } from './text.js';\n\nexport type ReporterFormat = 'text' | 'json';\n\nexport function format(report: AuditReport, kind: ReporterFormat): string {\n if (kind === 'json') return formatJson(report);\n return formatText(report);\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@geoql/doctor-core",
3
+ "version": "0.1.0-alpha.0",
4
+ "private": false,
5
+ "description": "Audit engine for @geoql/vue-doctor and @geoql/nuxt-doctor. Hybrid two-pass: @vue/compiler-sfc template AST + oxlint subprocess. TypeScript, ESM.",
6
+ "keywords": [
7
+ "audit",
8
+ "doctor",
9
+ "geoql",
10
+ "lint",
11
+ "nuxt",
12
+ "oxlint",
13
+ "vue",
14
+ "vue3"
15
+ ],
16
+ "homepage": "https://github.com/geoql/doctor#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/geoql/doctor/issues"
19
+ },
20
+ "license": "MIT",
21
+ "author": {
22
+ "name": "Vinayak Kulkarni",
23
+ "email": "inbox.vinayak@gmail.com",
24
+ "url": "https://vinayakkulkarni.dev"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/geoql/doctor.git",
29
+ "directory": "packages/doctor-core"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "type": "module",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "import": "./dist/index.js",
39
+ "types": "./dist/index.d.ts"
40
+ }
41
+ },
42
+ "dependencies": {
43
+ "@vue/compiler-sfc": "^3.5.35",
44
+ "c12": "^4.0.0-beta.5",
45
+ "kolorist": "^1.8.0",
46
+ "tinyglobby": "^0.2.16"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^25.9.1",
50
+ "@vue/compiler-core": "^3.5.35",
51
+ "typescript": "^6.0.3",
52
+ "vitest": "^4.1.7"
53
+ },
54
+ "peerDependencies": {
55
+ "oxlint": "^1.66.0"
56
+ },
57
+ "publishConfig": {
58
+ "access": "public"
59
+ },
60
+ "scripts": {
61
+ "lint": "vp lint src",
62
+ "lint:fix": "vp lint src --fix",
63
+ "format": "vp fmt",
64
+ "format:check": "vp fmt --check",
65
+ "build": "vp pack",
66
+ "test": "vitest run"
67
+ }
68
+ }