@geoql/doctor-core 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +111 -31
- package/dist/index.js +1743 -114
- package/dist/index.js.map +1 -1
- package/package.json +17 -12
- package/schema.json +443 -0
- package/dist/load-yXDi-5ti.js +0 -782
- package/dist/load-yXDi-5ti.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
1
|
+
import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
2
|
import { performance } from "node:perf_hooks";
|
|
4
3
|
import { access, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
5
4
|
import { createRequire } from "node:module";
|
|
@@ -11,9 +10,23 @@ import { glob } from "tinyglobby";
|
|
|
11
10
|
import { tmpdir } from "node:os";
|
|
12
11
|
import { fileURLToPath } from "node:url";
|
|
13
12
|
import { babelParse, parse } from "@vue/compiler-sfc";
|
|
13
|
+
import { loadConfig } from "c12";
|
|
14
|
+
import { z } from "zod";
|
|
14
15
|
import { promisify } from "node:util";
|
|
15
16
|
import process$1 from "node:process";
|
|
16
17
|
import pc from "picocolors";
|
|
18
|
+
//#region \0rolldown/runtime.js
|
|
19
|
+
var __defProp = Object.defineProperty;
|
|
20
|
+
var __exportAll = (all, no_symbols) => {
|
|
21
|
+
let target = {};
|
|
22
|
+
for (var name in all) __defProp(target, name, {
|
|
23
|
+
get: all[name],
|
|
24
|
+
enumerable: true
|
|
25
|
+
});
|
|
26
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
27
|
+
return target;
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
17
30
|
//#region src/annotations.ts
|
|
18
31
|
function escapeProperty(value) {
|
|
19
32
|
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A").replace(/,/g, "%2C");
|
|
@@ -54,7 +67,7 @@ async function readDeps(packageJsonPath) {
|
|
|
54
67
|
}
|
|
55
68
|
//#endregion
|
|
56
69
|
//#region src/build-quality/check-eslint-plugin-vue.ts
|
|
57
|
-
const RULE_ID$
|
|
70
|
+
const RULE_ID$23 = "vue-doctor/build-quality/eslint-plugin-vue-installed";
|
|
58
71
|
const DOCS$3 = "https://eslint.vuejs.org/";
|
|
59
72
|
async function checkEslintPluginVue(projectInfo) {
|
|
60
73
|
if (projectInfo.packageJsonPath === null) return [];
|
|
@@ -63,7 +76,7 @@ async function checkEslintPluginVue(projectInfo) {
|
|
|
63
76
|
if (deps === null) return [];
|
|
64
77
|
if (deps.devDependencies["eslint-plugin-vue"]) return [];
|
|
65
78
|
return [{
|
|
66
|
-
ruleId: RULE_ID$
|
|
79
|
+
ruleId: RULE_ID$23,
|
|
67
80
|
file: projectInfo.packageJsonPath,
|
|
68
81
|
line: 1,
|
|
69
82
|
column: 1,
|
|
@@ -74,7 +87,7 @@ async function checkEslintPluginVue(projectInfo) {
|
|
|
74
87
|
}
|
|
75
88
|
//#endregion
|
|
76
89
|
//#region src/build-quality/check-no-vue-cli.ts
|
|
77
|
-
const RULE_ID$
|
|
90
|
+
const RULE_ID$22 = "vue-doctor/build-quality/no-vue-cli";
|
|
78
91
|
const DOCS$2 = "https://cli.vuejs.org/migrations/migrate-from-v4.html";
|
|
79
92
|
function isOffendingKey(key) {
|
|
80
93
|
return key === "@vue/cli-service" || key.startsWith("vue-cli-plugin-");
|
|
@@ -86,7 +99,7 @@ async function checkNoVueCli(projectInfo) {
|
|
|
86
99
|
const offenders = [];
|
|
87
100
|
for (const key of [...Object.keys(deps.dependencies), ...Object.keys(deps.devDependencies)]) if (isOffendingKey(key) && !offenders.includes(key)) offenders.push(key);
|
|
88
101
|
return offenders.map((key, index) => ({
|
|
89
|
-
ruleId: RULE_ID$
|
|
102
|
+
ruleId: RULE_ID$22,
|
|
90
103
|
file: projectInfo.packageJsonPath,
|
|
91
104
|
line: index + 1,
|
|
92
105
|
column: 1,
|
|
@@ -148,7 +161,7 @@ function stripJsonComments(input) {
|
|
|
148
161
|
}
|
|
149
162
|
//#endregion
|
|
150
163
|
//#region src/build-quality/check-tsconfig-strict.ts
|
|
151
|
-
const RULE_ID$
|
|
164
|
+
const RULE_ID$21 = "vue-doctor/build-quality/tsconfig-strict-required";
|
|
152
165
|
const DOCS$1 = "https://www.typescriptlang.org/tsconfig#strict";
|
|
153
166
|
async function checkTsconfigStrict(projectInfo) {
|
|
154
167
|
if (projectInfo.packageJsonPath === null) return [];
|
|
@@ -162,7 +175,7 @@ async function checkTsconfigStrict(projectInfo) {
|
|
|
162
175
|
}
|
|
163
176
|
if (parsed.compilerOptions?.strict === true) return [];
|
|
164
177
|
return [{
|
|
165
|
-
ruleId: RULE_ID$
|
|
178
|
+
ruleId: RULE_ID$21,
|
|
166
179
|
file: tsconfigPath,
|
|
167
180
|
line: 1,
|
|
168
181
|
column: 1,
|
|
@@ -173,7 +186,7 @@ async function checkTsconfigStrict(projectInfo) {
|
|
|
173
186
|
}
|
|
174
187
|
//#endregion
|
|
175
188
|
//#region src/build-quality/check-vue-tsc.ts
|
|
176
|
-
const RULE_ID$
|
|
189
|
+
const RULE_ID$20 = "vue-doctor/build-quality/vue-tsc-in-devDeps";
|
|
177
190
|
const DOCS = "https://github.com/vuejs/language-tools/tree/master/packages/tsc";
|
|
178
191
|
async function checkVueTsc(projectInfo) {
|
|
179
192
|
if (projectInfo.packageJsonPath === null) return [];
|
|
@@ -182,7 +195,7 @@ async function checkVueTsc(projectInfo) {
|
|
|
182
195
|
if (deps === null) return [];
|
|
183
196
|
if (deps.devDependencies["vue-tsc"]) return [];
|
|
184
197
|
return [{
|
|
185
|
-
ruleId: RULE_ID$
|
|
198
|
+
ruleId: RULE_ID$20,
|
|
186
199
|
file: projectInfo.packageJsonPath,
|
|
187
200
|
line: 1,
|
|
188
201
|
column: 1,
|
|
@@ -542,14 +555,14 @@ async function listVueResolutions(packageJsonPath) {
|
|
|
542
555
|
}
|
|
543
556
|
//#endregion
|
|
544
557
|
//#region src/deps/check-duplicate-vue.ts
|
|
545
|
-
const RULE_ID$
|
|
558
|
+
const RULE_ID$19 = "vue-doctor/deps/duplicate-vue-versions";
|
|
546
559
|
async function checkDuplicateVue(projectInfo) {
|
|
547
560
|
if (projectInfo.packageJsonPath === null) return [];
|
|
548
561
|
const versions = await listVueResolutions(projectInfo.packageJsonPath);
|
|
549
562
|
const uniqueVersions = [...new Set(versions)];
|
|
550
563
|
if (uniqueVersions.length <= 1) return [];
|
|
551
564
|
return [{
|
|
552
|
-
ruleId: RULE_ID$
|
|
565
|
+
ruleId: RULE_ID$19,
|
|
553
566
|
file: projectInfo.packageJsonPath,
|
|
554
567
|
line: 1,
|
|
555
568
|
column: 1,
|
|
@@ -561,7 +574,7 @@ async function checkDuplicateVue(projectInfo) {
|
|
|
561
574
|
}
|
|
562
575
|
//#endregion
|
|
563
576
|
//#region src/deps/check-vue-major-current.ts
|
|
564
|
-
const RULE_ID$
|
|
577
|
+
const RULE_ID$18 = "vue-doctor/deps/vue-major-current";
|
|
565
578
|
const DOCTOR_BUNDLED_VUE_FLOOR_MAJOR = 3;
|
|
566
579
|
const DOCTOR_BUNDLED_VUE_FLOOR_MINOR = 5;
|
|
567
580
|
function parseMajorMinor$1(version) {
|
|
@@ -580,7 +593,7 @@ function checkVueMajorCurrent(projectInfo) {
|
|
|
580
593
|
if (parsed.major !== DOCTOR_BUNDLED_VUE_FLOOR_MAJOR) return [];
|
|
581
594
|
if (parsed.minor >= DOCTOR_BUNDLED_VUE_FLOOR_MINOR) return [];
|
|
582
595
|
return [{
|
|
583
|
-
ruleId: RULE_ID$
|
|
596
|
+
ruleId: RULE_ID$18,
|
|
584
597
|
file: projectInfo.packageJsonPath,
|
|
585
598
|
line: 1,
|
|
586
599
|
column: 1,
|
|
@@ -606,20 +619,20 @@ async function checkDeps(projectInfo) {
|
|
|
606
619
|
}
|
|
607
620
|
//#endregion
|
|
608
621
|
//#region src/project-info/parse-nuxt-config.ts
|
|
609
|
-
const CONFIG_FILES = [
|
|
622
|
+
const CONFIG_FILES$1 = [
|
|
610
623
|
"nuxt.config.ts",
|
|
611
624
|
"nuxt.config.js",
|
|
612
625
|
"nuxt.config.mjs"
|
|
613
626
|
];
|
|
614
|
-
async function readConfigSource(dir) {
|
|
615
|
-
for (const name of CONFIG_FILES) try {
|
|
627
|
+
async function readConfigSource$1(dir) {
|
|
628
|
+
for (const name of CONFIG_FILES$1) try {
|
|
616
629
|
return await readFile(join(dir, name), "utf8");
|
|
617
630
|
} catch {
|
|
618
631
|
continue;
|
|
619
632
|
}
|
|
620
633
|
return null;
|
|
621
634
|
}
|
|
622
|
-
function* objectEntries(obj) {
|
|
635
|
+
function* objectEntries$1(obj) {
|
|
623
636
|
for (const prop of obj.properties) {
|
|
624
637
|
if (prop.type !== "Property") continue;
|
|
625
638
|
const key = prop.key;
|
|
@@ -627,8 +640,8 @@ function* objectEntries(obj) {
|
|
|
627
640
|
yield [key.name, prop.value];
|
|
628
641
|
}
|
|
629
642
|
}
|
|
630
|
-
function findEntry(obj, name) {
|
|
631
|
-
for (const [key, value] of objectEntries(obj)) if (key === name) return value;
|
|
643
|
+
function findEntry$1(obj, name) {
|
|
644
|
+
for (const [key, value] of objectEntries$1(obj)) if (key === name) return value;
|
|
632
645
|
}
|
|
633
646
|
function stringLiteral(node) {
|
|
634
647
|
return node.type === "Literal" && typeof node.value === "string" ? node.value : void 0;
|
|
@@ -640,7 +653,7 @@ function unwrapDefault(decl) {
|
|
|
640
653
|
if (callee.name !== "defineNuxtConfig") return null;
|
|
641
654
|
return decl.arguments[0] ?? null;
|
|
642
655
|
}
|
|
643
|
-
function extractConfigObject(source) {
|
|
656
|
+
function extractConfigObject$1(source) {
|
|
644
657
|
const exported = parseSync("nuxt.config.ts", source, {
|
|
645
658
|
sourceType: "module",
|
|
646
659
|
lang: "ts"
|
|
@@ -652,12 +665,12 @@ function extractConfigObject(source) {
|
|
|
652
665
|
}
|
|
653
666
|
function readNitroPreset(value) {
|
|
654
667
|
if (value.type !== "ObjectExpression") return void 0;
|
|
655
|
-
const preset = findEntry(value, "preset");
|
|
668
|
+
const preset = findEntry$1(value, "preset");
|
|
656
669
|
return preset ? stringLiteral(preset) : void 0;
|
|
657
670
|
}
|
|
658
671
|
function readImportsAutoImport(value) {
|
|
659
672
|
if (value.type !== "ObjectExpression") return void 0;
|
|
660
|
-
const node = findEntry(value, "autoImport");
|
|
673
|
+
const node = findEntry$1(value, "autoImport");
|
|
661
674
|
if (node?.type === "Literal" && typeof node.value === "boolean") return node.value;
|
|
662
675
|
}
|
|
663
676
|
function readModules(value) {
|
|
@@ -675,20 +688,20 @@ function readCompatibility(value) {
|
|
|
675
688
|
}
|
|
676
689
|
function readHtmlLang(value) {
|
|
677
690
|
if (value.type !== "ObjectExpression") return void 0;
|
|
678
|
-
const head = findEntry(value, "head");
|
|
691
|
+
const head = findEntry$1(value, "head");
|
|
679
692
|
if (head?.type !== "ObjectExpression") return void 0;
|
|
680
|
-
const htmlAttrs = findEntry(head, "htmlAttrs");
|
|
693
|
+
const htmlAttrs = findEntry$1(head, "htmlAttrs");
|
|
681
694
|
if (htmlAttrs?.type !== "ObjectExpression") return void 0;
|
|
682
|
-
const lang = findEntry(htmlAttrs, "lang");
|
|
695
|
+
const lang = findEntry$1(htmlAttrs, "lang");
|
|
683
696
|
return lang ? stringLiteral(lang) : void 0;
|
|
684
697
|
}
|
|
685
698
|
async function parseNuxtConfig(dir) {
|
|
686
|
-
const source = await readConfigSource(dir);
|
|
699
|
+
const source = await readConfigSource$1(dir);
|
|
687
700
|
if (source === null) return null;
|
|
688
701
|
const info = {};
|
|
689
|
-
const config = extractConfigObject(source);
|
|
702
|
+
const config = extractConfigObject$1(source);
|
|
690
703
|
if (!config) return info;
|
|
691
|
-
for (const [key, value] of objectEntries(config)) if (key === "compatibilityVersion") {
|
|
704
|
+
for (const [key, value] of objectEntries$1(config)) if (key === "compatibilityVersion") {
|
|
692
705
|
const compat = readCompatibility(value);
|
|
693
706
|
if (compat !== void 0) info.compatibilityVersion = compat;
|
|
694
707
|
} else if (key === "compatibilityDate") {
|
|
@@ -712,12 +725,12 @@ async function parseNuxtConfig(dir) {
|
|
|
712
725
|
}
|
|
713
726
|
//#endregion
|
|
714
727
|
//#region src/nuxt/post-checks/compatibility-date-set.ts
|
|
715
|
-
const RULE_ID$
|
|
728
|
+
const RULE_ID$17 = "nuxt-doctor/nitro/compatibilityDate-set";
|
|
716
729
|
function checkCompatibilityDateSet(projectInfo, nuxtConfig) {
|
|
717
730
|
if (projectInfo.packageJsonPath === null) return [];
|
|
718
731
|
if (nuxtConfig?.compatibilityDate !== void 0) return [];
|
|
719
732
|
return [{
|
|
720
|
-
ruleId: RULE_ID$
|
|
733
|
+
ruleId: RULE_ID$17,
|
|
721
734
|
file: projectInfo.nuxtConfigPath ?? projectInfo.packageJsonPath,
|
|
722
735
|
line: 1,
|
|
723
736
|
column: 1,
|
|
@@ -728,12 +741,12 @@ function checkCompatibilityDateSet(projectInfo, nuxtConfig) {
|
|
|
728
741
|
}
|
|
729
742
|
//#endregion
|
|
730
743
|
//#region src/nuxt/post-checks/lang-on-html.ts
|
|
731
|
-
const RULE_ID$
|
|
744
|
+
const RULE_ID$16 = "nuxt-doctor/seo/lang-on-html";
|
|
732
745
|
function checkLangOnHtml(projectInfo, nuxtConfig) {
|
|
733
746
|
if (projectInfo.packageJsonPath === null) return [];
|
|
734
747
|
if (nuxtConfig?.htmlLang !== void 0) return [];
|
|
735
748
|
return [{
|
|
736
|
-
ruleId: RULE_ID$
|
|
749
|
+
ruleId: RULE_ID$16,
|
|
737
750
|
file: projectInfo.nuxtConfigPath ?? projectInfo.packageJsonPath,
|
|
738
751
|
line: 1,
|
|
739
752
|
column: 1,
|
|
@@ -744,7 +757,7 @@ function checkLangOnHtml(projectInfo, nuxtConfig) {
|
|
|
744
757
|
}
|
|
745
758
|
//#endregion
|
|
746
759
|
//#region src/nuxt/post-checks/nitro-cloudflare-preset.ts
|
|
747
|
-
const RULE_ID$
|
|
760
|
+
const RULE_ID$15 = "nuxt-doctor/cloudflare/nitro-cloudflare-preset";
|
|
748
761
|
const CLOUDFLARE_PRESETS = new Set([
|
|
749
762
|
"cloudflare-pages",
|
|
750
763
|
"cloudflare-module",
|
|
@@ -756,7 +769,7 @@ function checkNitroCloudflarePreset(projectInfo) {
|
|
|
756
769
|
if (projectInfo.nitroPreset === null) return [];
|
|
757
770
|
if (CLOUDFLARE_PRESETS.has(projectInfo.nitroPreset)) return [];
|
|
758
771
|
return [{
|
|
759
|
-
ruleId: RULE_ID$
|
|
772
|
+
ruleId: RULE_ID$15,
|
|
760
773
|
file: projectInfo.nuxtConfigPath ?? projectInfo.packageJsonPath,
|
|
761
774
|
line: 1,
|
|
762
775
|
column: 1,
|
|
@@ -767,7 +780,7 @@ function checkNitroCloudflarePreset(projectInfo) {
|
|
|
767
780
|
}
|
|
768
781
|
//#endregion
|
|
769
782
|
//#region src/nuxt/post-checks/no-modules-incompatible-with-nuxt-4.ts
|
|
770
|
-
const RULE_ID$
|
|
783
|
+
const RULE_ID$14 = "nuxt-doctor/modules-deps/no-modules-incompatible-with-nuxt-4";
|
|
771
784
|
const INCOMPATIBLE_MODULES = new Set([
|
|
772
785
|
"@nuxtjs/composition-api",
|
|
773
786
|
"@nuxt/bridge",
|
|
@@ -780,7 +793,7 @@ async function checkNoModulesIncompatibleWithNuxt4(projectInfo) {
|
|
|
780
793
|
const offenders = [];
|
|
781
794
|
for (const key of [...Object.keys(deps.dependencies), ...Object.keys(deps.devDependencies)]) if (INCOMPATIBLE_MODULES.has(key) && !offenders.includes(key)) offenders.push(key);
|
|
782
795
|
return offenders.map((name, index) => ({
|
|
783
|
-
ruleId: RULE_ID$
|
|
796
|
+
ruleId: RULE_ID$14,
|
|
784
797
|
file: projectInfo.packageJsonPath,
|
|
785
798
|
line: index + 1,
|
|
786
799
|
column: 1,
|
|
@@ -791,7 +804,7 @@ async function checkNoModulesIncompatibleWithNuxt4(projectInfo) {
|
|
|
791
804
|
}
|
|
792
805
|
//#endregion
|
|
793
806
|
//#region src/nuxt/post-checks/no-node-only-modules.ts
|
|
794
|
-
const RULE_ID$
|
|
807
|
+
const RULE_ID$13 = "nuxt-doctor/cloudflare/no-node-only-modules";
|
|
795
808
|
const NODE_ONLY_MODULES = new Set(["fs-extra", "sharp"]);
|
|
796
809
|
async function checkNoNodeOnlyModules(projectInfo) {
|
|
797
810
|
if (projectInfo.packageJsonPath === null) return [];
|
|
@@ -801,7 +814,7 @@ async function checkNoNodeOnlyModules(projectInfo) {
|
|
|
801
814
|
const offenders = [];
|
|
802
815
|
for (const key of [...Object.keys(deps.dependencies), ...Object.keys(deps.devDependencies)]) if (NODE_ONLY_MODULES.has(key) && !offenders.includes(key)) offenders.push(key);
|
|
803
816
|
return offenders.map((name, index) => ({
|
|
804
|
-
ruleId: RULE_ID$
|
|
817
|
+
ruleId: RULE_ID$13,
|
|
805
818
|
file: projectInfo.packageJsonPath,
|
|
806
819
|
line: index + 1,
|
|
807
820
|
column: 1,
|
|
@@ -811,8 +824,97 @@ async function checkNoNodeOnlyModules(projectInfo) {
|
|
|
811
824
|
}));
|
|
812
825
|
}
|
|
813
826
|
//#endregion
|
|
827
|
+
//#region src/nuxt/post-checks/no-secret-in-public-runtime-config.ts
|
|
828
|
+
const RULE_ID$12 = "nuxt-doctor/security/no-secret-in-public-runtime-config";
|
|
829
|
+
const SECRET_KEY = /secret|password|passwd|token|jwt|api[-_]?key|private[-_]?key|bearer|credential/i;
|
|
830
|
+
const CONFIG_FILES = [
|
|
831
|
+
"nuxt.config.ts",
|
|
832
|
+
"nuxt.config.js",
|
|
833
|
+
"nuxt.config.mjs"
|
|
834
|
+
];
|
|
835
|
+
function offsetToPosition(source, offset) {
|
|
836
|
+
let line = 1;
|
|
837
|
+
let lineStart = 0;
|
|
838
|
+
for (let i = 0; i < offset; i++) if (source.charCodeAt(i) === 10) {
|
|
839
|
+
line++;
|
|
840
|
+
lineStart = i + 1;
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
line,
|
|
844
|
+
column: offset - lineStart + 1
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
async function readConfigSource(dir) {
|
|
848
|
+
for (const name of CONFIG_FILES) try {
|
|
849
|
+
const file = join(dir, name);
|
|
850
|
+
return {
|
|
851
|
+
source: await readFile(file, "utf8"),
|
|
852
|
+
file
|
|
853
|
+
};
|
|
854
|
+
} catch {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
function* objectEntries(obj) {
|
|
860
|
+
for (const prop of obj.properties) {
|
|
861
|
+
if (prop.type !== "Property") continue;
|
|
862
|
+
const key = prop.key;
|
|
863
|
+
if (key.type !== "Identifier") continue;
|
|
864
|
+
yield [
|
|
865
|
+
key.name,
|
|
866
|
+
prop.value,
|
|
867
|
+
key
|
|
868
|
+
];
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function findEntry(obj, name) {
|
|
872
|
+
for (const [key, value] of objectEntries(obj)) if (key === name) return value;
|
|
873
|
+
}
|
|
874
|
+
function extractConfigObject(source) {
|
|
875
|
+
const exported = parseSync("nuxt.config.ts", source, {
|
|
876
|
+
sourceType: "module",
|
|
877
|
+
lang: "ts"
|
|
878
|
+
}).program.body.find((n) => n.type === "ExportDefaultDeclaration");
|
|
879
|
+
if (!exported) return null;
|
|
880
|
+
const decl = exported.declaration;
|
|
881
|
+
if (decl.type === "ObjectExpression") return decl;
|
|
882
|
+
if (decl.type !== "CallExpression") return null;
|
|
883
|
+
const callee = decl.callee;
|
|
884
|
+
if (callee.type !== "Identifier" || callee.name !== "defineNuxtConfig") return null;
|
|
885
|
+
const arg = decl.arguments[0];
|
|
886
|
+
return arg?.type === "ObjectExpression" ? arg : null;
|
|
887
|
+
}
|
|
888
|
+
async function checkNoSecretInPublicRuntimeConfig(projectInfo) {
|
|
889
|
+
if (projectInfo.packageJsonPath === null) return [];
|
|
890
|
+
const config = await readConfigSource(projectInfo.rootDirectory);
|
|
891
|
+
if (config === null) return [];
|
|
892
|
+
const root = extractConfigObject(config.source);
|
|
893
|
+
if (!root) return [];
|
|
894
|
+
const runtimeConfig = findEntry(root, "runtimeConfig");
|
|
895
|
+
if (runtimeConfig?.type !== "ObjectExpression") return [];
|
|
896
|
+
const publicBlock = findEntry(runtimeConfig, "public");
|
|
897
|
+
if (publicBlock?.type !== "ObjectExpression") return [];
|
|
898
|
+
const issues = [];
|
|
899
|
+
for (const [name, , keyNode] of objectEntries(publicBlock)) {
|
|
900
|
+
if (!SECRET_KEY.test(name)) continue;
|
|
901
|
+
const offset = keyNode.start;
|
|
902
|
+
const position = offsetToPosition(config.source, offset);
|
|
903
|
+
issues.push({
|
|
904
|
+
ruleId: RULE_ID$12,
|
|
905
|
+
file: config.file,
|
|
906
|
+
line: position.line,
|
|
907
|
+
column: position.column,
|
|
908
|
+
severity: "error",
|
|
909
|
+
message: `runtimeConfig.public.${name} is serialized to the client bundle, exposing the secret. Move it to the private root of runtimeConfig.`,
|
|
910
|
+
recommendation: `Move ${name} out of runtimeConfig.public to the top level of runtimeConfig so it stays server-only.`
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
return issues;
|
|
914
|
+
}
|
|
915
|
+
//#endregion
|
|
814
916
|
//#region src/nuxt/post-checks/nuxt-major-current.ts
|
|
815
|
-
const RULE_ID$
|
|
917
|
+
const RULE_ID$11 = "nuxt-doctor/structure/nuxt-major-current";
|
|
816
918
|
const DOCTOR_BUNDLED_NUXT_FLOOR_MAJOR = 4;
|
|
817
919
|
const DOCTOR_BUNDLED_NUXT_FLOOR_MINOR = 4;
|
|
818
920
|
function parseMajorMinor(version) {
|
|
@@ -831,7 +933,7 @@ function checkNuxtMajorCurrent(projectInfo) {
|
|
|
831
933
|
if (parsed.major !== DOCTOR_BUNDLED_NUXT_FLOOR_MAJOR) return [];
|
|
832
934
|
if (parsed.minor >= DOCTOR_BUNDLED_NUXT_FLOOR_MINOR) return [];
|
|
833
935
|
return [{
|
|
834
|
-
ruleId: RULE_ID$
|
|
936
|
+
ruleId: RULE_ID$11,
|
|
835
937
|
file: projectInfo.packageJsonPath,
|
|
836
938
|
line: 1,
|
|
837
939
|
column: 1,
|
|
@@ -842,7 +944,7 @@ function checkNuxtMajorCurrent(projectInfo) {
|
|
|
842
944
|
}
|
|
843
945
|
//#endregion
|
|
844
946
|
//#region src/nuxt/post-checks/og-image-via-satori.ts
|
|
845
|
-
const RULE_ID$
|
|
947
|
+
const RULE_ID$10 = "nuxt-doctor/cloudflare/og-image-via-satori";
|
|
846
948
|
const OG_IMAGE_MODULES = ["@nuxtjs/og-image", "nuxt-og-image"];
|
|
847
949
|
async function checkOgImageViaSatori(projectInfo) {
|
|
848
950
|
if (projectInfo.packageJsonPath === null) return [];
|
|
@@ -852,7 +954,7 @@ async function checkOgImageViaSatori(projectInfo) {
|
|
|
852
954
|
const installed = new Set([...Object.keys(deps.dependencies), ...Object.keys(deps.devDependencies)]);
|
|
853
955
|
if (OG_IMAGE_MODULES.some((name) => installed.has(name))) return [];
|
|
854
956
|
return [{
|
|
855
|
-
ruleId: RULE_ID$
|
|
957
|
+
ruleId: RULE_ID$10,
|
|
856
958
|
file: projectInfo.packageJsonPath,
|
|
857
959
|
line: 1,
|
|
858
960
|
column: 1,
|
|
@@ -863,13 +965,13 @@ async function checkOgImageViaSatori(projectInfo) {
|
|
|
863
965
|
}
|
|
864
966
|
//#endregion
|
|
865
967
|
//#region src/nuxt/post-checks/preset-defined-for-deploy-target.ts
|
|
866
|
-
const RULE_ID$
|
|
968
|
+
const RULE_ID$9 = "nuxt-doctor/nitro/preset-defined-for-deploy-target";
|
|
867
969
|
function checkPresetDefinedForDeployTarget(projectInfo) {
|
|
868
970
|
if (projectInfo.packageJsonPath === null) return [];
|
|
869
971
|
if (!projectInfo.hasWranglerConfig) return [];
|
|
870
972
|
if (projectInfo.nitroPreset !== null) return [];
|
|
871
973
|
return [{
|
|
872
|
-
ruleId: RULE_ID$
|
|
974
|
+
ruleId: RULE_ID$9,
|
|
873
975
|
file: projectInfo.nuxtConfigPath ?? projectInfo.packageJsonPath,
|
|
874
976
|
line: 1,
|
|
875
977
|
column: 1,
|
|
@@ -880,7 +982,7 @@ function checkPresetDefinedForDeployTarget(projectInfo) {
|
|
|
880
982
|
}
|
|
881
983
|
//#endregion
|
|
882
984
|
//#region src/nuxt/post-checks/recommended-modules-installed.ts
|
|
883
|
-
const RULE_ID$
|
|
985
|
+
const RULE_ID$8 = "nuxt-doctor/modules-deps/recommended-modules-installed";
|
|
884
986
|
const RECOMMENDED_GROUPS = [
|
|
885
987
|
{
|
|
886
988
|
label: "@nuxt/image",
|
|
@@ -901,7 +1003,7 @@ async function checkRecommendedModulesInstalled(projectInfo) {
|
|
|
901
1003
|
if (deps === null) return [];
|
|
902
1004
|
const installed = new Set([...Object.keys(deps.dependencies), ...Object.keys(deps.devDependencies)]);
|
|
903
1005
|
return RECOMMENDED_GROUPS.filter((group) => !group.candidates.some((name) => installed.has(name))).map((group, index) => ({
|
|
904
|
-
ruleId: RULE_ID$
|
|
1006
|
+
ruleId: RULE_ID$8,
|
|
905
1007
|
file: projectInfo.packageJsonPath,
|
|
906
1008
|
line: index + 1,
|
|
907
1009
|
column: 1,
|
|
@@ -912,7 +1014,7 @@ async function checkRecommendedModulesInstalled(projectInfo) {
|
|
|
912
1014
|
}
|
|
913
1015
|
//#endregion
|
|
914
1016
|
//#region src/nuxt/post-checks/runtime-config-typed.ts
|
|
915
|
-
const RULE_ID$
|
|
1017
|
+
const RULE_ID$7 = "nuxt-doctor/nitro/runtime-config-typed";
|
|
916
1018
|
async function hasRuntimeConfigAugmentation(dir) {
|
|
917
1019
|
const entries = await readdir(dir);
|
|
918
1020
|
for (const name of entries) {
|
|
@@ -926,7 +1028,7 @@ async function checkRuntimeConfigTyped(projectInfo, nuxtConfig) {
|
|
|
926
1028
|
if (nuxtConfig?.hasRuntimeConfig !== true) return [];
|
|
927
1029
|
if (await hasRuntimeConfigAugmentation(projectInfo.rootDirectory)) return [];
|
|
928
1030
|
return [{
|
|
929
|
-
ruleId: RULE_ID$
|
|
1031
|
+
ruleId: RULE_ID$7,
|
|
930
1032
|
file: projectInfo.packageJsonPath,
|
|
931
1033
|
line: 1,
|
|
932
1034
|
column: 1,
|
|
@@ -937,12 +1039,12 @@ async function checkRuntimeConfigTyped(projectInfo, nuxtConfig) {
|
|
|
937
1039
|
}
|
|
938
1040
|
//#endregion
|
|
939
1041
|
//#region src/nuxt/post-checks/uses-app-directory.ts
|
|
940
|
-
const RULE_ID$
|
|
1042
|
+
const RULE_ID$6 = "nuxt-doctor/structure/uses-app-directory";
|
|
941
1043
|
function checkUsesAppDirectory(projectInfo) {
|
|
942
1044
|
if (projectInfo.packageJsonPath === null) return [];
|
|
943
1045
|
if (projectInfo.hasAppDir) return [];
|
|
944
1046
|
return [{
|
|
945
|
-
ruleId: RULE_ID$
|
|
1047
|
+
ruleId: RULE_ID$6,
|
|
946
1048
|
file: projectInfo.packageJsonPath,
|
|
947
1049
|
line: 1,
|
|
948
1050
|
column: 1,
|
|
@@ -968,7 +1070,8 @@ async function checkNuxtProject(projectInfo) {
|
|
|
968
1070
|
Promise.resolve(checkLangOnHtml(projectInfo, nuxtConfig)),
|
|
969
1071
|
Promise.resolve(checkNitroCloudflarePreset(projectInfo)),
|
|
970
1072
|
checkOgImageViaSatori(projectInfo),
|
|
971
|
-
checkNoNodeOnlyModules(projectInfo)
|
|
1073
|
+
checkNoNodeOnlyModules(projectInfo),
|
|
1074
|
+
checkNoSecretInPublicRuntimeConfig(projectInfo)
|
|
972
1075
|
])).flat().map((issue) => ({
|
|
973
1076
|
file: issue.file,
|
|
974
1077
|
line: issue.line,
|
|
@@ -1391,7 +1494,11 @@ const VUE_DEFAULT_RULES = {
|
|
|
1391
1494
|
"vue-doctor/no-imports-from-vue-when-auto-imported": "warn",
|
|
1392
1495
|
"vue-doctor/reactivity/watch-without-cleanup": "warn",
|
|
1393
1496
|
"vue-doctor/composition/prefer-script-setup-for-new-files": "warn",
|
|
1394
|
-
"vue-doctor/composition/defineProps-typed": "warn"
|
|
1497
|
+
"vue-doctor/composition/defineProps-typed": "warn",
|
|
1498
|
+
"vue-doctor/security/no-inner-html": "error",
|
|
1499
|
+
"vue-doctor/security/no-eval-like": "error",
|
|
1500
|
+
"vue-doctor/security/no-auth-token-in-web-storage": "warn",
|
|
1501
|
+
"vue-doctor/security/no-secrets-in-source": "warn"
|
|
1395
1502
|
};
|
|
1396
1503
|
const NUXT_PLUGIN_RULES = {
|
|
1397
1504
|
"nuxt-doctor/ai-slop/no-process-client-server": "error",
|
|
@@ -1403,7 +1510,8 @@ const NUXT_PLUGIN_RULES = {
|
|
|
1403
1510
|
"nuxt-doctor/server-routes/validate-body-with-h3-v2": "warn",
|
|
1404
1511
|
"nuxt-doctor/server-routes/createError-on-failure": "warn",
|
|
1405
1512
|
"nuxt-doctor/hydration/no-document-in-setup": "error",
|
|
1406
|
-
"nuxt-doctor/hydration/clientOnly-for-browser-apis": "error"
|
|
1513
|
+
"nuxt-doctor/hydration/clientOnly-for-browser-apis": "error",
|
|
1514
|
+
"nuxt-doctor/security/no-user-input-in-fetch-url": "warn"
|
|
1407
1515
|
};
|
|
1408
1516
|
const VUE_OXLINT_RULE_IDS = new Set([
|
|
1409
1517
|
"vue/no-export-in-script-setup",
|
|
@@ -1439,7 +1547,11 @@ const VUE_OXLINT_RULE_IDS = new Set([
|
|
|
1439
1547
|
"vue-doctor/reactivity/prefer-readonly-for-injected",
|
|
1440
1548
|
"vue-doctor/composition/prefer-script-setup-for-new-files",
|
|
1441
1549
|
"vue-doctor/composition/defineProps-typed",
|
|
1442
|
-
"vue-doctor/performance/prefer-defineAsyncComponent-on-route"
|
|
1550
|
+
"vue-doctor/performance/prefer-defineAsyncComponent-on-route",
|
|
1551
|
+
"vue-doctor/security/no-inner-html",
|
|
1552
|
+
"vue-doctor/security/no-eval-like",
|
|
1553
|
+
"vue-doctor/security/no-auth-token-in-web-storage",
|
|
1554
|
+
"vue-doctor/security/no-secrets-in-source"
|
|
1443
1555
|
]);
|
|
1444
1556
|
const NUXT_OXLINT_RULE_IDS = new Set(Object.keys(NUXT_PLUGIN_RULES));
|
|
1445
1557
|
function oxlintRuleAllowlist(framework) {
|
|
@@ -1577,11 +1689,10 @@ function resolveOxlintBin(fromDir) {
|
|
|
1577
1689
|
}
|
|
1578
1690
|
//#endregion
|
|
1579
1691
|
//#region src/oxlint/errors.ts
|
|
1580
|
-
const STDERR_TAIL_LIMIT = 2e3;
|
|
1581
1692
|
var OxlintSpawnFailed = class extends Error {
|
|
1582
1693
|
name = "OxlintSpawnFailed";
|
|
1583
1694
|
constructor(exitCode, stderr) {
|
|
1584
|
-
const tail = stderr.slice(-
|
|
1695
|
+
const tail = stderr.slice(-2e3);
|
|
1585
1696
|
super(`oxlint subprocess exited with code ${exitCode}: ${tail}`);
|
|
1586
1697
|
}
|
|
1587
1698
|
};
|
|
@@ -1792,9 +1903,9 @@ async function parseSfcDescriptor(absPath) {
|
|
|
1792
1903
|
}
|
|
1793
1904
|
//#endregion
|
|
1794
1905
|
//#region src/sfc/rules/no-mixed-options-and-composition-api.ts
|
|
1795
|
-
const RULE_ID$
|
|
1796
|
-
const MESSAGE$
|
|
1797
|
-
const RECOMMENDATION$
|
|
1906
|
+
const RULE_ID$5 = "vue-doctor/sfc/no-mixed-options-and-composition-api";
|
|
1907
|
+
const MESSAGE$5 = "Mixed Options API in a <script setup> SFC. Move data/methods/computed/watch/lifecycle into <script setup> Composition API; keep <script> only for options like name/inheritAttrs. See https://vuejs.org/api/sfc-script-setup.html#usage-alongside-normal-script";
|
|
1908
|
+
const RECOMMENDATION$5 = "Move this option into the <script setup> block using the Composition API, or keep <script> only for options-only config such as name or inheritAttrs.";
|
|
1798
1909
|
const DISALLOWED = new Set([
|
|
1799
1910
|
"data",
|
|
1800
1911
|
"methods",
|
|
@@ -1861,7 +1972,7 @@ function locate(content, offset, startLine, startColumn) {
|
|
|
1861
1972
|
column
|
|
1862
1973
|
};
|
|
1863
1974
|
}
|
|
1864
|
-
function check$
|
|
1975
|
+
function check$19(ctx) {
|
|
1865
1976
|
const { script, scriptSetup } = ctx.descriptor;
|
|
1866
1977
|
if (!script || !scriptSetup) return { diagnostics: [] };
|
|
1867
1978
|
const lang = script.lang === "ts" ? "ts" : "js";
|
|
@@ -1881,11 +1992,11 @@ function check$9(ctx) {
|
|
|
1881
1992
|
file: ctx.file,
|
|
1882
1993
|
line,
|
|
1883
1994
|
column,
|
|
1884
|
-
ruleId: RULE_ID$
|
|
1995
|
+
ruleId: RULE_ID$5,
|
|
1885
1996
|
severity: "error",
|
|
1886
|
-
message: MESSAGE$
|
|
1997
|
+
message: MESSAGE$5,
|
|
1887
1998
|
source: "sfc",
|
|
1888
|
-
recommendation: RECOMMENDATION$
|
|
1999
|
+
recommendation: RECOMMENDATION$5
|
|
1889
2000
|
});
|
|
1890
2001
|
}
|
|
1891
2002
|
return { diagnostics };
|
|
@@ -1941,10 +2052,10 @@ function walkElements(root, visit) {
|
|
|
1941
2052
|
}
|
|
1942
2053
|
//#endregion
|
|
1943
2054
|
//#region src/sfc/rules/nuxt/no-mixed-app-and-root-layout.ts
|
|
1944
|
-
const RULE_ID$
|
|
1945
|
-
const MESSAGE$
|
|
1946
|
-
const RECOMMENDATION$
|
|
1947
|
-
function check$
|
|
2055
|
+
const RULE_ID$4 = "nuxt-doctor/ai-slop/no-mixed-app-and-root-layout";
|
|
2056
|
+
const MESSAGE$4 = "Layout file renders<NuxtLayout> inside itself. This creates a nested layout chain where the root layout renders itself as a slot. Use<slot /> directly and let the page content flow through.";
|
|
2057
|
+
const RECOMMENDATION$4 = "Replace <NuxtLayout> in this layout file with <slot />. The layout slot is already provided by the parent layout wrapper.";
|
|
2058
|
+
function check$18(ctx) {
|
|
1948
2059
|
if (!isNuxtLayoutFile(ctx.relativePath)) return { diagnostics: [] };
|
|
1949
2060
|
const { template } = ctx.descriptor;
|
|
1950
2061
|
if (!template || !template.ast) return { diagnostics: [] };
|
|
@@ -1964,18 +2075,18 @@ function check$8(ctx) {
|
|
|
1964
2075
|
column: node.loc.start.column,
|
|
1965
2076
|
endLine: node.loc.end.line,
|
|
1966
2077
|
endColumn: node.loc.end.column,
|
|
1967
|
-
ruleId: RULE_ID$
|
|
2078
|
+
ruleId: RULE_ID$4,
|
|
1968
2079
|
severity: "warn",
|
|
1969
|
-
message: MESSAGE$
|
|
2080
|
+
message: MESSAGE$4,
|
|
1970
2081
|
source: "sfc",
|
|
1971
|
-
recommendation: RECOMMENDATION$
|
|
2082
|
+
recommendation: RECOMMENDATION$4
|
|
1972
2083
|
}] };
|
|
1973
2084
|
}
|
|
1974
2085
|
//#endregion
|
|
1975
2086
|
//#region src/sfc/rules/nuxt/og-image-defined.ts
|
|
1976
|
-
const RULE_ID$
|
|
1977
|
-
const MESSAGE$
|
|
1978
|
-
const RECOMMENDATION$
|
|
2087
|
+
const RULE_ID$3 = "nuxt-doctor/seo/og-image-defined";
|
|
2088
|
+
const MESSAGE$3 = "Page uses SEO meta but has no og:image property. Open Graph images improve social sharing previews. Add an og:image value or install @nuxtjs/og-image for automatic OG images.";
|
|
2089
|
+
const RECOMMENDATION$3 = "Add ogImage: \"/path/to/image.png\" to useSeoMeta / useHead, or install @nuxtjs/og-image.";
|
|
1979
2090
|
function hasOgImageInCall(program) {
|
|
1980
2091
|
for (const stmt of program.body) {
|
|
1981
2092
|
if (stmt.type !== "ExpressionStatement") continue;
|
|
@@ -2007,7 +2118,7 @@ function hasOgImageDep(packageJsonPath) {
|
|
|
2007
2118
|
return false;
|
|
2008
2119
|
}
|
|
2009
2120
|
}
|
|
2010
|
-
function check$
|
|
2121
|
+
function check$17(ctx) {
|
|
2011
2122
|
if (!isNuxtPageFile(ctx.relativePath)) return { diagnostics: [] };
|
|
2012
2123
|
const { scriptSetup } = ctx.descriptor;
|
|
2013
2124
|
if (!scriptSetup) return { diagnostics: [] };
|
|
@@ -2024,18 +2135,18 @@ function check$7(ctx) {
|
|
|
2024
2135
|
file: ctx.file,
|
|
2025
2136
|
line,
|
|
2026
2137
|
column,
|
|
2027
|
-
ruleId: RULE_ID$
|
|
2138
|
+
ruleId: RULE_ID$3,
|
|
2028
2139
|
severity: "warn",
|
|
2029
|
-
message: MESSAGE$
|
|
2140
|
+
message: MESSAGE$3,
|
|
2030
2141
|
source: "sfc",
|
|
2031
|
-
recommendation: RECOMMENDATION$
|
|
2142
|
+
recommendation: RECOMMENDATION$3
|
|
2032
2143
|
}] };
|
|
2033
2144
|
}
|
|
2034
2145
|
//#endregion
|
|
2035
2146
|
//#region src/sfc/rules/nuxt/use-seo-meta-on-public-page.ts
|
|
2036
|
-
const RULE_ID = "nuxt-doctor/seo/useSeoMeta-on-public-page";
|
|
2037
|
-
const MESSAGE = "Public page component is missing SEO metadata. Add useSeoMeta, useHead, or definePageMeta with a title so search engines can index it properly.";
|
|
2038
|
-
const RECOMMENDATION = "Call useSeoMeta({ title: \"...\" }) in<script setup> to define page title and meta tags for search engines and social previews.";
|
|
2147
|
+
const RULE_ID$2 = "nuxt-doctor/seo/useSeoMeta-on-public-page";
|
|
2148
|
+
const MESSAGE$2 = "Public page component is missing SEO metadata. Add useSeoMeta, useHead, or definePageMeta with a title so search engines can index it properly.";
|
|
2149
|
+
const RECOMMENDATION$2 = "Call useSeoMeta({ title: \"...\" }) in<script setup> to define page title and meta tags for search engines and social previews.";
|
|
2039
2150
|
function hasTitleInCall(program) {
|
|
2040
2151
|
for (const stmt of program.body) {
|
|
2041
2152
|
if (stmt.type !== "ExpressionStatement") continue;
|
|
@@ -2054,7 +2165,7 @@ function hasTitleInCall(program) {
|
|
|
2054
2165
|
}
|
|
2055
2166
|
return false;
|
|
2056
2167
|
}
|
|
2057
|
-
function check$
|
|
2168
|
+
function check$16(ctx) {
|
|
2058
2169
|
if (!isNuxtPageFile(ctx.relativePath)) return { diagnostics: [] };
|
|
2059
2170
|
const { scriptSetup } = ctx.descriptor;
|
|
2060
2171
|
if (!scriptSetup) return { diagnostics: [] };
|
|
@@ -2069,11 +2180,11 @@ function check$6(ctx) {
|
|
|
2069
2180
|
file: ctx.file,
|
|
2070
2181
|
line,
|
|
2071
2182
|
column,
|
|
2072
|
-
ruleId: RULE_ID,
|
|
2183
|
+
ruleId: RULE_ID$2,
|
|
2073
2184
|
severity: "warn",
|
|
2074
|
-
message: MESSAGE,
|
|
2185
|
+
message: MESSAGE$2,
|
|
2075
2186
|
source: "sfc",
|
|
2076
|
-
recommendation: RECOMMENDATION
|
|
2187
|
+
recommendation: RECOMMENDATION$2
|
|
2077
2188
|
}] };
|
|
2078
2189
|
}
|
|
2079
2190
|
//#endregion
|
|
@@ -2081,19 +2192,19 @@ function check$6(ctx) {
|
|
|
2081
2192
|
const SFC_RULES = [
|
|
2082
2193
|
{
|
|
2083
2194
|
id: "vue-doctor/sfc/no-mixed-options-and-composition-api",
|
|
2084
|
-
check: check$
|
|
2195
|
+
check: check$19
|
|
2085
2196
|
},
|
|
2086
2197
|
{
|
|
2087
2198
|
id: "nuxt-doctor/seo/useSeoMeta-on-public-page",
|
|
2088
|
-
check: check$
|
|
2199
|
+
check: check$16
|
|
2089
2200
|
},
|
|
2090
2201
|
{
|
|
2091
2202
|
id: "nuxt-doctor/seo/og-image-defined",
|
|
2092
|
-
check: check$
|
|
2203
|
+
check: check$17
|
|
2093
2204
|
},
|
|
2094
2205
|
{
|
|
2095
2206
|
id: "nuxt-doctor/ai-slop/no-mixed-app-and-root-layout",
|
|
2096
|
-
check: check$
|
|
2207
|
+
check: check$18
|
|
2097
2208
|
}
|
|
2098
2209
|
];
|
|
2099
2210
|
//#endregion
|
|
@@ -2324,13 +2435,13 @@ async function parseSfc(absPath) {
|
|
|
2324
2435
|
}
|
|
2325
2436
|
//#endregion
|
|
2326
2437
|
//#region src/template/directive-helpers.ts
|
|
2327
|
-
const NODE_DIRECTIVE$
|
|
2438
|
+
const NODE_DIRECTIVE$4 = 7;
|
|
2328
2439
|
function findDirective(el, name) {
|
|
2329
|
-
for (const prop of el.props) if (prop.type === NODE_DIRECTIVE$
|
|
2440
|
+
for (const prop of el.props) if (prop.type === NODE_DIRECTIVE$4 && prop.name === name) return prop;
|
|
2330
2441
|
}
|
|
2331
2442
|
function findBindAttr(el, attrName) {
|
|
2332
2443
|
for (const prop of el.props) {
|
|
2333
|
-
if (prop.type !== NODE_DIRECTIVE$
|
|
2444
|
+
if (prop.type !== NODE_DIRECTIVE$4) continue;
|
|
2334
2445
|
const dir = prop;
|
|
2335
2446
|
if (dir.name !== "bind") continue;
|
|
2336
2447
|
if (dir.arg?.content === attrName) return dir;
|
|
@@ -2341,7 +2452,7 @@ function findStaticAttr(el, attrName) {
|
|
|
2341
2452
|
}
|
|
2342
2453
|
//#endregion
|
|
2343
2454
|
//#region src/template/rules/v-for-has-key.ts
|
|
2344
|
-
function check$
|
|
2455
|
+
function check$15(ctx) {
|
|
2345
2456
|
const diagnostics = [];
|
|
2346
2457
|
walkElements(ctx.template, (el) => {
|
|
2347
2458
|
if (!findDirective(el, "for")) return;
|
|
@@ -2363,7 +2474,7 @@ function check$5(ctx) {
|
|
|
2363
2474
|
}
|
|
2364
2475
|
//#endregion
|
|
2365
2476
|
//#region src/template/rules/v-if-v-for-precedence.ts
|
|
2366
|
-
function check$
|
|
2477
|
+
function check$14(ctx) {
|
|
2367
2478
|
const diagnostics = [];
|
|
2368
2479
|
walkElements(ctx.template, (el) => {
|
|
2369
2480
|
const vIf = findDirective(el, "if");
|
|
@@ -2405,7 +2516,7 @@ function findLargeArrayBindings(script) {
|
|
|
2405
2516
|
} catch {}
|
|
2406
2517
|
return bindings;
|
|
2407
2518
|
}
|
|
2408
|
-
function check$
|
|
2519
|
+
function check$13(ctx) {
|
|
2409
2520
|
const diagnostics = [];
|
|
2410
2521
|
const largeArrays = ctx.script && ctx.script.length > 0 ? findLargeArrayBindings(ctx.script) : /* @__PURE__ */ new Map();
|
|
2411
2522
|
walkElements(ctx.template, (el) => {
|
|
@@ -2433,12 +2544,12 @@ function check$3(ctx) {
|
|
|
2433
2544
|
}
|
|
2434
2545
|
//#endregion
|
|
2435
2546
|
//#region src/template/rules/no-inline-object-prop-in-list.ts
|
|
2436
|
-
const NODE_DIRECTIVE$
|
|
2547
|
+
const NODE_DIRECTIVE$3 = 7;
|
|
2437
2548
|
function checkElement$2(el, inVForSubtree, ctx, diagnostics) {
|
|
2438
|
-
const isVFor = el.props.some((p) => p.type === NODE_DIRECTIVE$
|
|
2549
|
+
const isVFor = el.props.some((p) => p.type === NODE_DIRECTIVE$3 && p.name === "for");
|
|
2439
2550
|
const effectiveInVFor = inVForSubtree || isVFor;
|
|
2440
2551
|
if (effectiveInVFor) for (const prop of el.props) {
|
|
2441
|
-
if (prop.type !== NODE_DIRECTIVE$
|
|
2552
|
+
if (prop.type !== NODE_DIRECTIVE$3) continue;
|
|
2442
2553
|
const dir = prop;
|
|
2443
2554
|
if (dir.name !== "bind") continue;
|
|
2444
2555
|
const attrName = dir.arg?.content;
|
|
@@ -2460,7 +2571,7 @@ function checkElement$2(el, inVForSubtree, ctx, diagnostics) {
|
|
|
2460
2571
|
}
|
|
2461
2572
|
for (const child of el.children) if (child.type === 1) checkElement$2(child, effectiveInVFor, ctx, diagnostics);
|
|
2462
2573
|
}
|
|
2463
|
-
function check$
|
|
2574
|
+
function check$12(ctx) {
|
|
2464
2575
|
const diagnostics = [];
|
|
2465
2576
|
function walk(node, inVForSubtree) {
|
|
2466
2577
|
if (node.type === 1) checkElement$2(node, inVForSubtree, ctx, diagnostics);
|
|
@@ -2476,7 +2587,7 @@ function check$2(ctx) {
|
|
|
2476
2587
|
//#region src/template/rules/no-computed-getter-in-template-loop.ts
|
|
2477
2588
|
const NODE_ELEMENT$1 = 1;
|
|
2478
2589
|
const NODE_INTERPOLATION = 5;
|
|
2479
|
-
const NODE_DIRECTIVE$
|
|
2590
|
+
const NODE_DIRECTIVE$2 = 7;
|
|
2480
2591
|
function isValueDeref(node) {
|
|
2481
2592
|
if (node.type !== "MemberExpression" || node.computed === true) return false;
|
|
2482
2593
|
const { object, property } = node;
|
|
@@ -2503,11 +2614,11 @@ function pushDiagnostic(el, loc, ctx, diagnostics) {
|
|
|
2503
2614
|
});
|
|
2504
2615
|
}
|
|
2505
2616
|
function checkElement$1(el, inVForSubtree, ctx, diagnostics) {
|
|
2506
|
-
const isVFor = el.props.some((p) => p.type === NODE_DIRECTIVE$
|
|
2617
|
+
const isVFor = el.props.some((p) => p.type === NODE_DIRECTIVE$2 && p.name === "for");
|
|
2507
2618
|
const effectiveInVFor = inVForSubtree || isVFor;
|
|
2508
2619
|
if (effectiveInVFor) {
|
|
2509
2620
|
for (const prop of el.props) {
|
|
2510
|
-
if (prop.type !== NODE_DIRECTIVE$
|
|
2621
|
+
if (prop.type !== NODE_DIRECTIVE$2) continue;
|
|
2511
2622
|
const dir = prop;
|
|
2512
2623
|
if (dir.name !== "bind") continue;
|
|
2513
2624
|
if (containsValueDeref(dir.exp?.ast)) pushDiagnostic(el, dir.loc, ctx, diagnostics);
|
|
@@ -2519,7 +2630,7 @@ function checkElement$1(el, inVForSubtree, ctx, diagnostics) {
|
|
|
2519
2630
|
}
|
|
2520
2631
|
for (const child of el.children) if (child.type === NODE_ELEMENT$1) checkElement$1(child, effectiveInVFor, ctx, diagnostics);
|
|
2521
2632
|
}
|
|
2522
|
-
function check$
|
|
2633
|
+
function check$11(ctx) {
|
|
2523
2634
|
const diagnostics = [];
|
|
2524
2635
|
function walk(node, inVForSubtree) {
|
|
2525
2636
|
if (node.type === NODE_ELEMENT$1) checkElement$1(node, inVForSubtree, ctx, diagnostics);
|
|
@@ -2534,15 +2645,15 @@ function check$1(ctx) {
|
|
|
2534
2645
|
//#endregion
|
|
2535
2646
|
//#region src/template/rules/avoid-deep-v-bind-spread-in-list.ts
|
|
2536
2647
|
const NODE_ELEMENT = 1;
|
|
2537
|
-
const NODE_DIRECTIVE = 7;
|
|
2648
|
+
const NODE_DIRECTIVE$1 = 7;
|
|
2538
2649
|
function isIdentifierSpread(dir) {
|
|
2539
2650
|
return dir.name === "bind" && dir.arg === void 0 && dir.exp?.ast === null;
|
|
2540
2651
|
}
|
|
2541
2652
|
function checkElement(el, inVForSubtree, ctx, diagnostics) {
|
|
2542
|
-
const isVFor = el.props.some((p) => p.type === NODE_DIRECTIVE && p.name === "for");
|
|
2653
|
+
const isVFor = el.props.some((p) => p.type === NODE_DIRECTIVE$1 && p.name === "for");
|
|
2543
2654
|
const effectiveInVFor = inVForSubtree || isVFor;
|
|
2544
2655
|
if (effectiveInVFor) for (const prop of el.props) {
|
|
2545
|
-
if (prop.type !== NODE_DIRECTIVE) continue;
|
|
2656
|
+
if (prop.type !== NODE_DIRECTIVE$1) continue;
|
|
2546
2657
|
const dir = prop;
|
|
2547
2658
|
if (!isIdentifierSpread(dir)) continue;
|
|
2548
2659
|
diagnostics.push({
|
|
@@ -2560,7 +2671,7 @@ function checkElement(el, inVForSubtree, ctx, diagnostics) {
|
|
|
2560
2671
|
}
|
|
2561
2672
|
for (const child of el.children) if (child.type === NODE_ELEMENT) checkElement(child, effectiveInVFor, ctx, diagnostics);
|
|
2562
2673
|
}
|
|
2563
|
-
function check(ctx) {
|
|
2674
|
+
function check$10(ctx) {
|
|
2564
2675
|
const diagnostics = [];
|
|
2565
2676
|
function walk(node, inVForSubtree) {
|
|
2566
2677
|
if (node.type === NODE_ELEMENT) checkElement(node, inVForSubtree, ctx, diagnostics);
|
|
@@ -2573,30 +2684,442 @@ function check(ctx) {
|
|
|
2573
2684
|
return { diagnostics };
|
|
2574
2685
|
}
|
|
2575
2686
|
//#endregion
|
|
2687
|
+
//#region src/template/rules/no-v-html.ts
|
|
2688
|
+
function check$9(ctx) {
|
|
2689
|
+
const diagnostics = [];
|
|
2690
|
+
walkElements(ctx.template, (el) => {
|
|
2691
|
+
const vHtml = findDirective(el, "html");
|
|
2692
|
+
if (!vHtml) return;
|
|
2693
|
+
diagnostics.push({
|
|
2694
|
+
file: ctx.file,
|
|
2695
|
+
line: vHtml.loc.start.line,
|
|
2696
|
+
column: vHtml.loc.start.column,
|
|
2697
|
+
endLine: vHtml.loc.end.line,
|
|
2698
|
+
endColumn: vHtml.loc.end.column,
|
|
2699
|
+
ruleId: "vue-doctor/security/no-v-html",
|
|
2700
|
+
severity: "error",
|
|
2701
|
+
message: `<${el.tag}> uses v-html, which renders raw HTML and bypasses Vue's escaping — the primary XSS sink in Vue apps.`,
|
|
2702
|
+
source: "template",
|
|
2703
|
+
recommendation: "Render text with {{ }} interpolation, or sanitize the HTML with DOMPurify in a computed before binding it."
|
|
2704
|
+
});
|
|
2705
|
+
});
|
|
2706
|
+
return { diagnostics };
|
|
2707
|
+
}
|
|
2708
|
+
//#endregion
|
|
2709
|
+
//#region src/template/rules/no-target-blank-without-rel.ts
|
|
2710
|
+
function unquote$1(raw) {
|
|
2711
|
+
const trimmed = raw.trim();
|
|
2712
|
+
const first = trimmed.charAt(0);
|
|
2713
|
+
if ((first === "\"" || first === "'") && trimmed.endsWith(first)) return trimmed.slice(1, -1);
|
|
2714
|
+
return trimmed;
|
|
2715
|
+
}
|
|
2716
|
+
function attrValue(el, name) {
|
|
2717
|
+
const staticAttr = findStaticAttr(el, name);
|
|
2718
|
+
if (staticAttr) return {
|
|
2719
|
+
value: staticAttr.value?.content ?? "",
|
|
2720
|
+
loc: staticAttr.loc
|
|
2721
|
+
};
|
|
2722
|
+
const bound = findBindAttr(el, name);
|
|
2723
|
+
if (bound?.exp && "content" in bound.exp) return {
|
|
2724
|
+
value: unquote$1(bound.exp.content),
|
|
2725
|
+
loc: bound.loc
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
function check$8(ctx) {
|
|
2729
|
+
const diagnostics = [];
|
|
2730
|
+
walkElements(ctx.template, (el) => {
|
|
2731
|
+
if (el.tag !== "a") return;
|
|
2732
|
+
const target = attrValue(el, "target");
|
|
2733
|
+
if (!target || target.value !== "_blank") return;
|
|
2734
|
+
const rel = attrValue(el, "rel");
|
|
2735
|
+
if (rel && rel.value.includes("noopener")) return;
|
|
2736
|
+
diagnostics.push({
|
|
2737
|
+
file: ctx.file,
|
|
2738
|
+
line: target.loc.start.line,
|
|
2739
|
+
column: target.loc.start.column,
|
|
2740
|
+
endLine: target.loc.end.line,
|
|
2741
|
+
endColumn: target.loc.end.column,
|
|
2742
|
+
ruleId: "vue-doctor/security/no-target-blank-without-rel",
|
|
2743
|
+
severity: "warn",
|
|
2744
|
+
message: `<a target="_blank"> without rel="noopener noreferrer" exposes the opener window to reverse-tabnabbing.`,
|
|
2745
|
+
source: "template",
|
|
2746
|
+
recommendation: "Add rel=\"noopener noreferrer\" to anchors that open in a new tab."
|
|
2747
|
+
});
|
|
2748
|
+
});
|
|
2749
|
+
return { diagnostics };
|
|
2750
|
+
}
|
|
2751
|
+
//#endregion
|
|
2752
|
+
//#region src/template/rules/no-javascript-uri.ts
|
|
2753
|
+
const NODE_ATTRIBUTE$1 = 6;
|
|
2754
|
+
const URI_SINKS = new Set([
|
|
2755
|
+
"href",
|
|
2756
|
+
"src",
|
|
2757
|
+
"srcset",
|
|
2758
|
+
"action",
|
|
2759
|
+
"xlink:href",
|
|
2760
|
+
"formaction",
|
|
2761
|
+
"poster",
|
|
2762
|
+
"data"
|
|
2763
|
+
]);
|
|
2764
|
+
const DANGEROUS_SCHEME = /^\s*(?:javascript:|vbscript:|data:text\/html)/i;
|
|
2765
|
+
function unquote(raw) {
|
|
2766
|
+
const trimmed = raw.trim();
|
|
2767
|
+
const first = trimmed.charAt(0);
|
|
2768
|
+
if ((first === "\"" || first === "'" || first === "`") && trimmed.endsWith(first)) return trimmed.slice(1, -1);
|
|
2769
|
+
return trimmed;
|
|
2770
|
+
}
|
|
2771
|
+
function resolveUriProp(prop) {
|
|
2772
|
+
if (prop.type === NODE_ATTRIBUTE$1) {
|
|
2773
|
+
const attr = prop;
|
|
2774
|
+
if (attr.value === void 0) return void 0;
|
|
2775
|
+
return {
|
|
2776
|
+
name: attr.name,
|
|
2777
|
+
value: attr.value.content,
|
|
2778
|
+
loc: attr.loc
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2781
|
+
const dir = prop;
|
|
2782
|
+
if (dir.name !== "bind") return void 0;
|
|
2783
|
+
if (!dir.arg || !("content" in dir.arg)) return void 0;
|
|
2784
|
+
if (!dir.exp || !("content" in dir.exp)) return void 0;
|
|
2785
|
+
return {
|
|
2786
|
+
name: dir.arg.content,
|
|
2787
|
+
value: unquote(dir.exp.content),
|
|
2788
|
+
loc: dir.loc
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
function check$7(ctx) {
|
|
2792
|
+
const diagnostics = [];
|
|
2793
|
+
walkElements(ctx.template, (el) => {
|
|
2794
|
+
for (const prop of el.props) {
|
|
2795
|
+
const resolved = resolveUriProp(prop);
|
|
2796
|
+
if (!resolved) continue;
|
|
2797
|
+
if (!URI_SINKS.has(resolved.name)) continue;
|
|
2798
|
+
if (!DANGEROUS_SCHEME.test(resolved.value)) continue;
|
|
2799
|
+
diagnostics.push({
|
|
2800
|
+
file: ctx.file,
|
|
2801
|
+
line: resolved.loc.start.line,
|
|
2802
|
+
column: resolved.loc.start.column,
|
|
2803
|
+
endLine: resolved.loc.end.line,
|
|
2804
|
+
endColumn: resolved.loc.end.column,
|
|
2805
|
+
ruleId: "vue-doctor/security/no-javascript-uri",
|
|
2806
|
+
severity: "error",
|
|
2807
|
+
message: `<${el.tag}> binds ${resolved.name} to a dangerous URI scheme (javascript:/vbscript:/data:text/html), which executes arbitrary code.`,
|
|
2808
|
+
source: "template",
|
|
2809
|
+
recommendation: "Use a safe http(s) or relative URL; never put a javascript:, vbscript:, or data:text/html value in a URL attribute."
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
});
|
|
2813
|
+
return { diagnostics };
|
|
2814
|
+
}
|
|
2815
|
+
//#endregion
|
|
2816
|
+
//#region src/template/class-attr-helpers.ts
|
|
2817
|
+
const NODE_ATTRIBUTE = 6;
|
|
2818
|
+
const NODE_DIRECTIVE = 7;
|
|
2819
|
+
/**
|
|
2820
|
+
* Collect the scannable text of every `class` source on an element: the static
|
|
2821
|
+
* `class="..."` value and any bound `:class` expression source. Bound
|
|
2822
|
+
* expressions are returned verbatim so a regex can match string-literal class
|
|
2823
|
+
* fragments inside ternaries, arrays, and objects without a full JS parse.
|
|
2824
|
+
*/
|
|
2825
|
+
function collectClassSources(el) {
|
|
2826
|
+
const sources = [];
|
|
2827
|
+
for (const prop of el.props) {
|
|
2828
|
+
if (prop.type === NODE_ATTRIBUTE) {
|
|
2829
|
+
const attr = prop;
|
|
2830
|
+
if (attr.name === "class" && attr.value) sources.push({
|
|
2831
|
+
text: attr.value.content,
|
|
2832
|
+
loc: attr.loc
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
if (prop.type === NODE_DIRECTIVE) {
|
|
2836
|
+
const dir = prop;
|
|
2837
|
+
if (dir.name === "bind" && dir.arg && "content" in dir.arg && dir.arg.content === "class" && dir.exp && "content" in dir.exp) sources.push({
|
|
2838
|
+
text: dir.exp.content,
|
|
2839
|
+
loc: dir.loc
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
return sources;
|
|
2844
|
+
}
|
|
2845
|
+
/** Find a static `style="..."` attribute on an element, if present. */
|
|
2846
|
+
function findStaticStyle(el) {
|
|
2847
|
+
for (const prop of el.props) if (prop.type === NODE_ATTRIBUTE && prop.name === "style") return prop;
|
|
2848
|
+
}
|
|
2849
|
+
//#endregion
|
|
2850
|
+
//#region src/template/rules/no-arbitrary-tailwind-values.ts
|
|
2851
|
+
const ARBITRARY_VALUE = /\[[^\]]+\]/;
|
|
2852
|
+
function check$6(ctx) {
|
|
2853
|
+
const diagnostics = [];
|
|
2854
|
+
walkElements(ctx.template, (el) => {
|
|
2855
|
+
for (const source of collectClassSources(el)) {
|
|
2856
|
+
if (!ARBITRARY_VALUE.test(source.text)) continue;
|
|
2857
|
+
diagnostics.push({
|
|
2858
|
+
file: ctx.file,
|
|
2859
|
+
line: source.loc.start.line,
|
|
2860
|
+
column: source.loc.start.column,
|
|
2861
|
+
endLine: source.loc.end.line,
|
|
2862
|
+
endColumn: source.loc.end.column,
|
|
2863
|
+
ruleId: "vue-doctor/design/no-arbitrary-tailwind-values",
|
|
2864
|
+
severity: "warn",
|
|
2865
|
+
message: `<${el.tag}> uses a Tailwind arbitrary value (e.g. w-[412px]). Arbitrary values are design-system debt; repeated values should become @theme tokens.`,
|
|
2866
|
+
source: "template",
|
|
2867
|
+
recommendation: "Replace the arbitrary value with a design token defined in your @theme (e.g. w-container-lg)."
|
|
2868
|
+
});
|
|
2869
|
+
}
|
|
2870
|
+
});
|
|
2871
|
+
return { diagnostics };
|
|
2872
|
+
}
|
|
2873
|
+
//#endregion
|
|
2874
|
+
//#region src/template/rules/no-raw-hex-color.ts
|
|
2875
|
+
const HEX_COLOR = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/;
|
|
2876
|
+
const RULE_ID$1 = "vue-doctor/design/no-raw-hex-color";
|
|
2877
|
+
const MESSAGE$1 = "Raw hex color bypasses the token system and breaks theming. Use a semantic color token instead.";
|
|
2878
|
+
const RECOMMENDATION$1 = "Replace the hex color with a design token (e.g. bg-danger / text-success) defined in your @theme.";
|
|
2879
|
+
function check$5(ctx) {
|
|
2880
|
+
const diagnostics = [];
|
|
2881
|
+
walkElements(ctx.template, (el) => {
|
|
2882
|
+
for (const source of collectClassSources(el)) {
|
|
2883
|
+
if (!HEX_COLOR.test(source.text)) continue;
|
|
2884
|
+
diagnostics.push({
|
|
2885
|
+
file: ctx.file,
|
|
2886
|
+
line: source.loc.start.line,
|
|
2887
|
+
column: source.loc.start.column,
|
|
2888
|
+
endLine: source.loc.end.line,
|
|
2889
|
+
endColumn: source.loc.end.column,
|
|
2890
|
+
ruleId: RULE_ID$1,
|
|
2891
|
+
severity: "warn",
|
|
2892
|
+
message: `<${el.tag}> embeds a raw hex color in a class. ${MESSAGE$1}`,
|
|
2893
|
+
source: "template",
|
|
2894
|
+
recommendation: RECOMMENDATION$1
|
|
2895
|
+
});
|
|
2896
|
+
}
|
|
2897
|
+
const style = findStaticStyle(el);
|
|
2898
|
+
if (style?.value && HEX_COLOR.test(style.value.content)) diagnostics.push({
|
|
2899
|
+
file: ctx.file,
|
|
2900
|
+
line: style.loc.start.line,
|
|
2901
|
+
column: style.loc.start.column,
|
|
2902
|
+
endLine: style.loc.end.line,
|
|
2903
|
+
endColumn: style.loc.end.column,
|
|
2904
|
+
ruleId: RULE_ID$1,
|
|
2905
|
+
severity: "warn",
|
|
2906
|
+
message: `<${el.tag}> embeds a raw hex color in an inline style. ${MESSAGE$1}`,
|
|
2907
|
+
source: "template",
|
|
2908
|
+
recommendation: RECOMMENDATION$1
|
|
2909
|
+
});
|
|
2910
|
+
});
|
|
2911
|
+
return { diagnostics };
|
|
2912
|
+
}
|
|
2913
|
+
//#endregion
|
|
2914
|
+
//#region src/template/rules/no-default-tailwind-palette.ts
|
|
2915
|
+
const DEFAULT_PALETTE = /(?:^|[\s:'"`])(?:bg|text|border|ring|shadow|from|via|to|accent|caret|fill|stroke|outline|decoration|divide|ring-offset|placeholder)-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d{2,3}\b/;
|
|
2916
|
+
function check$4(ctx) {
|
|
2917
|
+
const diagnostics = [];
|
|
2918
|
+
walkElements(ctx.template, (el) => {
|
|
2919
|
+
for (const source of collectClassSources(el)) {
|
|
2920
|
+
if (!DEFAULT_PALETTE.test(source.text)) continue;
|
|
2921
|
+
diagnostics.push({
|
|
2922
|
+
file: ctx.file,
|
|
2923
|
+
line: source.loc.start.line,
|
|
2924
|
+
column: source.loc.start.column,
|
|
2925
|
+
endLine: source.loc.end.line,
|
|
2926
|
+
endColumn: source.loc.end.column,
|
|
2927
|
+
ruleId: "vue-doctor/design/no-default-tailwind-palette",
|
|
2928
|
+
severity: "warn",
|
|
2929
|
+
message: `<${el.tag}> uses a default Tailwind palette utility (e.g. bg-blue-600). The default palette produces generic UI; map colors to brand tokens.`,
|
|
2930
|
+
source: "template",
|
|
2931
|
+
recommendation: "Map the color to a brand token (e.g. bg-primary-600) defined in your @theme instead of the default palette."
|
|
2932
|
+
});
|
|
2933
|
+
}
|
|
2934
|
+
});
|
|
2935
|
+
return { diagnostics };
|
|
2936
|
+
}
|
|
2937
|
+
//#endregion
|
|
2938
|
+
//#region src/template/rules/no-important-utility.ts
|
|
2939
|
+
const IMPORTANT_UTILITY = /(?:^|[\s:'"`])!-?[a-z][a-z0-9-]*/;
|
|
2940
|
+
function check$3(ctx) {
|
|
2941
|
+
const diagnostics = [];
|
|
2942
|
+
walkElements(ctx.template, (el) => {
|
|
2943
|
+
for (const source of collectClassSources(el)) {
|
|
2944
|
+
if (!IMPORTANT_UTILITY.test(source.text)) continue;
|
|
2945
|
+
diagnostics.push({
|
|
2946
|
+
file: ctx.file,
|
|
2947
|
+
line: source.loc.start.line,
|
|
2948
|
+
column: source.loc.start.column,
|
|
2949
|
+
endLine: source.loc.end.line,
|
|
2950
|
+
endColumn: source.loc.end.column,
|
|
2951
|
+
ruleId: "vue-doctor/design/no-important-utility",
|
|
2952
|
+
severity: "warn",
|
|
2953
|
+
message: `<${el.tag}> uses a !important Tailwind utility (e.g. !bg-red-500). !important is a specificity escape hatch that makes maintenance harder.`,
|
|
2954
|
+
source: "template",
|
|
2955
|
+
recommendation: "Remove the ! prefix and resolve the specificity conflict at its source instead of forcing !important."
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
return { diagnostics };
|
|
2960
|
+
}
|
|
2961
|
+
//#endregion
|
|
2962
|
+
//#region src/template/rules/no-hardcoded-inline-style.ts
|
|
2963
|
+
const HARDCODED_VALUE = /\d+px\b|#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/;
|
|
2964
|
+
function check$2(ctx) {
|
|
2965
|
+
const diagnostics = [];
|
|
2966
|
+
walkElements(ctx.template, (el) => {
|
|
2967
|
+
const style = findStaticStyle(el);
|
|
2968
|
+
if (!style?.value) return;
|
|
2969
|
+
if (!HARDCODED_VALUE.test(style.value.content)) return;
|
|
2970
|
+
diagnostics.push({
|
|
2971
|
+
file: ctx.file,
|
|
2972
|
+
line: style.loc.start.line,
|
|
2973
|
+
column: style.loc.start.column,
|
|
2974
|
+
endLine: style.loc.end.line,
|
|
2975
|
+
endColumn: style.loc.end.column,
|
|
2976
|
+
ruleId: "vue-doctor/design/no-hardcoded-inline-style",
|
|
2977
|
+
severity: "warn",
|
|
2978
|
+
message: `<${el.tag}> uses an inline style with hardcoded px or hex values. Inline styles bypass tokens, hurt caching, and create drift.`,
|
|
2979
|
+
source: "template",
|
|
2980
|
+
recommendation: "Move the styling to token-based utility classes (e.g. w-60 text-neutral-700) instead of an inline style."
|
|
2981
|
+
});
|
|
2982
|
+
});
|
|
2983
|
+
return { diagnostics };
|
|
2984
|
+
}
|
|
2985
|
+
//#endregion
|
|
2986
|
+
//#region src/template/rules/no-missing-alt.ts
|
|
2987
|
+
const IMAGE_TAGS = new Set([
|
|
2988
|
+
"img",
|
|
2989
|
+
"NuxtImg",
|
|
2990
|
+
"nuxt-img"
|
|
2991
|
+
]);
|
|
2992
|
+
function check$1(ctx) {
|
|
2993
|
+
const diagnostics = [];
|
|
2994
|
+
walkElements(ctx.template, (el) => {
|
|
2995
|
+
if (!IMAGE_TAGS.has(el.tag)) return;
|
|
2996
|
+
if (findStaticAttr(el, "alt") ?? findBindAttr(el, "alt")) return;
|
|
2997
|
+
diagnostics.push({
|
|
2998
|
+
file: ctx.file,
|
|
2999
|
+
line: el.loc.start.line,
|
|
3000
|
+
column: el.loc.start.column,
|
|
3001
|
+
endLine: el.loc.end.line,
|
|
3002
|
+
endColumn: el.loc.end.column,
|
|
3003
|
+
ruleId: "vue-doctor/design/no-missing-alt",
|
|
3004
|
+
severity: "warn",
|
|
3005
|
+
message: `<${el.tag}> has no alt attribute. Screen readers and SEO depend on descriptive alt text.`,
|
|
3006
|
+
source: "template",
|
|
3007
|
+
recommendation: "Add an alt attribute describing the image, or alt=\"\" for purely decorative images."
|
|
3008
|
+
});
|
|
3009
|
+
});
|
|
3010
|
+
return { diagnostics };
|
|
3011
|
+
}
|
|
3012
|
+
//#endregion
|
|
3013
|
+
//#region src/template/rules/no-absurd-z-index.ts
|
|
3014
|
+
const ABSURD_THRESHOLD = 1e3;
|
|
3015
|
+
const ARBITRARY_Z = /(?:^|[\s:'"`])-?z-\[(\d+)\]/;
|
|
3016
|
+
const INLINE_Z = /z-index\s*:\s*(\d+)/;
|
|
3017
|
+
const RULE_ID = "vue-doctor/design/no-absurd-z-index";
|
|
3018
|
+
const MESSAGE = "huge z-index values usually paper over layering bugs instead of fixing stacking context.";
|
|
3019
|
+
const RECOMMENDATION = "Use a small, tokenized z-index scale (e.g. z-modal) and fix the underlying stacking context.";
|
|
3020
|
+
function check(ctx) {
|
|
3021
|
+
const diagnostics = [];
|
|
3022
|
+
walkElements(ctx.template, (el) => {
|
|
3023
|
+
for (const source of collectClassSources(el)) {
|
|
3024
|
+
const match = ARBITRARY_Z.exec(source.text);
|
|
3025
|
+
if (!match || Number(match[1]) < ABSURD_THRESHOLD) continue;
|
|
3026
|
+
diagnostics.push({
|
|
3027
|
+
file: ctx.file,
|
|
3028
|
+
line: source.loc.start.line,
|
|
3029
|
+
column: source.loc.start.column,
|
|
3030
|
+
endLine: source.loc.end.line,
|
|
3031
|
+
endColumn: source.loc.end.column,
|
|
3032
|
+
ruleId: RULE_ID,
|
|
3033
|
+
severity: "warn",
|
|
3034
|
+
message: `<${el.tag}> uses an absurd z-index class (>= ${ABSURD_THRESHOLD}); ${MESSAGE}`,
|
|
3035
|
+
source: "template",
|
|
3036
|
+
recommendation: RECOMMENDATION
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
const style = findStaticStyle(el);
|
|
3040
|
+
if (style?.value) {
|
|
3041
|
+
const match = INLINE_Z.exec(style.value.content);
|
|
3042
|
+
if (match && Number(match[1]) >= ABSURD_THRESHOLD) diagnostics.push({
|
|
3043
|
+
file: ctx.file,
|
|
3044
|
+
line: style.loc.start.line,
|
|
3045
|
+
column: style.loc.start.column,
|
|
3046
|
+
endLine: style.loc.end.line,
|
|
3047
|
+
endColumn: style.loc.end.column,
|
|
3048
|
+
ruleId: RULE_ID,
|
|
3049
|
+
severity: "warn",
|
|
3050
|
+
message: `<${el.tag}> uses an absurd inline z-index (>= ${ABSURD_THRESHOLD}); ${MESSAGE}`,
|
|
3051
|
+
source: "template",
|
|
3052
|
+
recommendation: RECOMMENDATION
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
});
|
|
3056
|
+
return { diagnostics };
|
|
3057
|
+
}
|
|
3058
|
+
//#endregion
|
|
2576
3059
|
//#region src/template/rules/index.ts
|
|
2577
3060
|
const TEMPLATE_RULES = [
|
|
2578
3061
|
{
|
|
2579
3062
|
id: "vue-doctor/template/v-for-has-key",
|
|
2580
|
-
check: check$
|
|
3063
|
+
check: check$15
|
|
2581
3064
|
},
|
|
2582
3065
|
{
|
|
2583
3066
|
id: "vue-doctor/template/v-if-v-for-precedence",
|
|
2584
|
-
check: check$
|
|
3067
|
+
check: check$14
|
|
2585
3068
|
},
|
|
2586
3069
|
{
|
|
2587
3070
|
id: "vue-doctor/template/v-memo-on-large-list",
|
|
2588
|
-
check: check$
|
|
3071
|
+
check: check$13
|
|
2589
3072
|
},
|
|
2590
3073
|
{
|
|
2591
3074
|
id: "vue-doctor/template/no-inline-object-prop-in-list",
|
|
2592
|
-
check: check$
|
|
3075
|
+
check: check$12
|
|
2593
3076
|
},
|
|
2594
3077
|
{
|
|
2595
3078
|
id: "vue-doctor/template/no-computed-getter-in-template-loop",
|
|
2596
|
-
check: check$
|
|
3079
|
+
check: check$11
|
|
2597
3080
|
},
|
|
2598
3081
|
{
|
|
2599
3082
|
id: "vue-doctor/template/avoid-deep-v-bind-spread-in-list",
|
|
3083
|
+
check: check$10
|
|
3084
|
+
},
|
|
3085
|
+
{
|
|
3086
|
+
id: "vue-doctor/security/no-v-html",
|
|
3087
|
+
check: check$9
|
|
3088
|
+
},
|
|
3089
|
+
{
|
|
3090
|
+
id: "vue-doctor/security/no-target-blank-without-rel",
|
|
3091
|
+
check: check$8
|
|
3092
|
+
},
|
|
3093
|
+
{
|
|
3094
|
+
id: "vue-doctor/security/no-javascript-uri",
|
|
3095
|
+
check: check$7
|
|
3096
|
+
},
|
|
3097
|
+
{
|
|
3098
|
+
id: "vue-doctor/design/no-arbitrary-tailwind-values",
|
|
3099
|
+
check: check$6
|
|
3100
|
+
},
|
|
3101
|
+
{
|
|
3102
|
+
id: "vue-doctor/design/no-raw-hex-color",
|
|
3103
|
+
check: check$5
|
|
3104
|
+
},
|
|
3105
|
+
{
|
|
3106
|
+
id: "vue-doctor/design/no-default-tailwind-palette",
|
|
3107
|
+
check: check$4
|
|
3108
|
+
},
|
|
3109
|
+
{
|
|
3110
|
+
id: "vue-doctor/design/no-important-utility",
|
|
3111
|
+
check: check$3
|
|
3112
|
+
},
|
|
3113
|
+
{
|
|
3114
|
+
id: "vue-doctor/design/no-hardcoded-inline-style",
|
|
3115
|
+
check: check$2
|
|
3116
|
+
},
|
|
3117
|
+
{
|
|
3118
|
+
id: "vue-doctor/design/no-missing-alt",
|
|
3119
|
+
check: check$1
|
|
3120
|
+
},
|
|
3121
|
+
{
|
|
3122
|
+
id: "vue-doctor/design/no-absurd-z-index",
|
|
2600
3123
|
check
|
|
2601
3124
|
}
|
|
2602
3125
|
];
|
|
@@ -2696,7 +3219,7 @@ async function audit(config = {}) {
|
|
|
2696
3219
|
if (config.deadCode !== false) {
|
|
2697
3220
|
const deadCodeStart = performance.now();
|
|
2698
3221
|
try {
|
|
2699
|
-
const { loadDoctorConfig } = await
|
|
3222
|
+
const { loadDoctorConfig } = await Promise.resolve().then(() => load_exports);
|
|
2700
3223
|
deadCodeDiagnostics = dedupeDeadCodeAgainstLint(await checkDeadCode({
|
|
2701
3224
|
projectInfo: project,
|
|
2702
3225
|
doctorConfig: await loadDoctorConfig(rootDir),
|
|
@@ -2799,11 +3322,946 @@ async function audit(config = {}) {
|
|
|
2799
3322
|
};
|
|
2800
3323
|
}
|
|
2801
3324
|
//#endregion
|
|
3325
|
+
//#region src/config/built-in.ts
|
|
3326
|
+
const BUILT_IN_RECOMMENDED = {
|
|
3327
|
+
include: [
|
|
3328
|
+
"**/*.vue",
|
|
3329
|
+
"**/*.ts",
|
|
3330
|
+
"**/*.tsx",
|
|
3331
|
+
"**/*.js",
|
|
3332
|
+
"**/*.jsx"
|
|
3333
|
+
],
|
|
3334
|
+
exclude: [
|
|
3335
|
+
"node_modules",
|
|
3336
|
+
"dist",
|
|
3337
|
+
".nuxt",
|
|
3338
|
+
".output",
|
|
3339
|
+
"coverage"
|
|
3340
|
+
],
|
|
3341
|
+
failOn: "error",
|
|
3342
|
+
threshold: 0,
|
|
3343
|
+
rules: {}
|
|
3344
|
+
};
|
|
3345
|
+
//#endregion
|
|
3346
|
+
//#region src/config/errors.ts
|
|
3347
|
+
var ConfigFileNotFoundError = class extends Error {
|
|
3348
|
+
name = "ConfigFileNotFoundError";
|
|
3349
|
+
constructor(path) {
|
|
3350
|
+
super(`Config file not found: ${path}`);
|
|
3351
|
+
}
|
|
3352
|
+
};
|
|
3353
|
+
var ConfigCycleError = class extends Error {
|
|
3354
|
+
name = "ConfigCycleError";
|
|
3355
|
+
constructor(chain) {
|
|
3356
|
+
super(`Config extends cycle detected: ${chain.join(" -> ")}`);
|
|
3357
|
+
}
|
|
3358
|
+
};
|
|
3359
|
+
var InvalidConfigError = class extends Error {
|
|
3360
|
+
name = "InvalidConfigError";
|
|
3361
|
+
issues;
|
|
3362
|
+
constructor(message, issues = []) {
|
|
3363
|
+
super(message);
|
|
3364
|
+
this.issues = issues;
|
|
3365
|
+
}
|
|
3366
|
+
};
|
|
3367
|
+
//#endregion
|
|
2802
3368
|
//#region src/config/define-config.ts
|
|
2803
3369
|
function defineConfig(config) {
|
|
2804
3370
|
return config;
|
|
2805
3371
|
}
|
|
2806
3372
|
//#endregion
|
|
3373
|
+
//#region src/rule-registry.ts
|
|
3374
|
+
const RULE_REGISTRY = [
|
|
3375
|
+
{
|
|
3376
|
+
id: "vue/no-export-in-script-setup",
|
|
3377
|
+
severity: "error",
|
|
3378
|
+
category: "vue-builtin",
|
|
3379
|
+
source: "oxlint-builtin",
|
|
3380
|
+
recommended: true
|
|
3381
|
+
},
|
|
3382
|
+
{
|
|
3383
|
+
id: "vue/require-typed-ref",
|
|
3384
|
+
severity: "warn",
|
|
3385
|
+
category: "vue-builtin",
|
|
3386
|
+
source: "oxlint-builtin",
|
|
3387
|
+
recommended: true
|
|
3388
|
+
},
|
|
3389
|
+
{
|
|
3390
|
+
id: "vue/no-arrow-functions-in-watch",
|
|
3391
|
+
severity: "error",
|
|
3392
|
+
category: "vue-builtin",
|
|
3393
|
+
source: "oxlint-builtin",
|
|
3394
|
+
recommended: true
|
|
3395
|
+
},
|
|
3396
|
+
{
|
|
3397
|
+
id: "vue/no-deprecated-data-object-declaration",
|
|
3398
|
+
severity: "error",
|
|
3399
|
+
category: "vue-builtin",
|
|
3400
|
+
source: "oxlint-builtin",
|
|
3401
|
+
recommended: true
|
|
3402
|
+
},
|
|
3403
|
+
{
|
|
3404
|
+
id: "vue/no-deprecated-events-api",
|
|
3405
|
+
severity: "error",
|
|
3406
|
+
category: "vue-builtin",
|
|
3407
|
+
source: "oxlint-builtin",
|
|
3408
|
+
recommended: true
|
|
3409
|
+
},
|
|
3410
|
+
{
|
|
3411
|
+
id: "vue/no-deprecated-destroyed-lifecycle",
|
|
3412
|
+
severity: "error",
|
|
3413
|
+
category: "vue-builtin",
|
|
3414
|
+
source: "oxlint-builtin",
|
|
3415
|
+
recommended: true
|
|
3416
|
+
},
|
|
3417
|
+
{
|
|
3418
|
+
id: "vue/no-deprecated-model-definition",
|
|
3419
|
+
severity: "error",
|
|
3420
|
+
category: "vue-builtin",
|
|
3421
|
+
source: "oxlint-builtin",
|
|
3422
|
+
recommended: true
|
|
3423
|
+
},
|
|
3424
|
+
{
|
|
3425
|
+
id: "vue/no-deprecated-delete-set",
|
|
3426
|
+
severity: "error",
|
|
3427
|
+
category: "vue-builtin",
|
|
3428
|
+
source: "oxlint-builtin",
|
|
3429
|
+
recommended: true
|
|
3430
|
+
},
|
|
3431
|
+
{
|
|
3432
|
+
id: "vue/no-deprecated-vue-config-keycodes",
|
|
3433
|
+
severity: "error",
|
|
3434
|
+
category: "vue-builtin",
|
|
3435
|
+
source: "oxlint-builtin",
|
|
3436
|
+
recommended: true
|
|
3437
|
+
},
|
|
3438
|
+
{
|
|
3439
|
+
id: "vue/no-lifecycle-after-await",
|
|
3440
|
+
severity: "error",
|
|
3441
|
+
category: "vue-builtin",
|
|
3442
|
+
source: "oxlint-builtin",
|
|
3443
|
+
recommended: true
|
|
3444
|
+
},
|
|
3445
|
+
{
|
|
3446
|
+
id: "vue/no-this-in-before-route-enter",
|
|
3447
|
+
severity: "error",
|
|
3448
|
+
category: "vue-builtin",
|
|
3449
|
+
source: "oxlint-builtin",
|
|
3450
|
+
recommended: true
|
|
3451
|
+
},
|
|
3452
|
+
{
|
|
3453
|
+
id: "vue/return-in-computed-property",
|
|
3454
|
+
severity: "error",
|
|
3455
|
+
category: "vue-builtin",
|
|
3456
|
+
source: "oxlint-builtin",
|
|
3457
|
+
recommended: true
|
|
3458
|
+
},
|
|
3459
|
+
{
|
|
3460
|
+
id: "vue/valid-define-emits",
|
|
3461
|
+
severity: "error",
|
|
3462
|
+
category: "vue-builtin",
|
|
3463
|
+
source: "oxlint-builtin",
|
|
3464
|
+
recommended: true
|
|
3465
|
+
},
|
|
3466
|
+
{
|
|
3467
|
+
id: "vue/valid-define-props",
|
|
3468
|
+
severity: "error",
|
|
3469
|
+
category: "vue-builtin",
|
|
3470
|
+
source: "oxlint-builtin",
|
|
3471
|
+
recommended: true
|
|
3472
|
+
},
|
|
3473
|
+
{
|
|
3474
|
+
id: "vue/no-required-prop-with-default",
|
|
3475
|
+
severity: "warn",
|
|
3476
|
+
category: "vue-builtin",
|
|
3477
|
+
source: "oxlint-builtin",
|
|
3478
|
+
recommended: true
|
|
3479
|
+
},
|
|
3480
|
+
{
|
|
3481
|
+
id: "vue/prefer-import-from-vue",
|
|
3482
|
+
severity: "warn",
|
|
3483
|
+
category: "vue-builtin",
|
|
3484
|
+
source: "oxlint-builtin",
|
|
3485
|
+
recommended: true
|
|
3486
|
+
},
|
|
3487
|
+
{
|
|
3488
|
+
id: "vue/no-import-compiler-macros",
|
|
3489
|
+
severity: "warn",
|
|
3490
|
+
category: "vue-builtin",
|
|
3491
|
+
source: "oxlint-builtin",
|
|
3492
|
+
recommended: true
|
|
3493
|
+
},
|
|
3494
|
+
{
|
|
3495
|
+
id: "vue/no-multiple-slot-args",
|
|
3496
|
+
severity: "warn",
|
|
3497
|
+
category: "vue-builtin",
|
|
3498
|
+
source: "oxlint-builtin",
|
|
3499
|
+
recommended: true
|
|
3500
|
+
},
|
|
3501
|
+
{
|
|
3502
|
+
id: "vue/require-default-export",
|
|
3503
|
+
severity: "warn",
|
|
3504
|
+
category: "vue-builtin",
|
|
3505
|
+
source: "oxlint-builtin",
|
|
3506
|
+
recommended: true
|
|
3507
|
+
},
|
|
3508
|
+
{
|
|
3509
|
+
id: "vue/define-emits-declaration",
|
|
3510
|
+
severity: "info",
|
|
3511
|
+
category: "vue-builtin",
|
|
3512
|
+
source: "oxlint-builtin",
|
|
3513
|
+
recommended: false
|
|
3514
|
+
},
|
|
3515
|
+
{
|
|
3516
|
+
id: "vue/define-props-declaration",
|
|
3517
|
+
severity: "info",
|
|
3518
|
+
category: "vue-builtin",
|
|
3519
|
+
source: "oxlint-builtin",
|
|
3520
|
+
recommended: false
|
|
3521
|
+
},
|
|
3522
|
+
{
|
|
3523
|
+
id: "vue/define-props-destructuring",
|
|
3524
|
+
severity: "info",
|
|
3525
|
+
category: "vue-builtin",
|
|
3526
|
+
source: "oxlint-builtin",
|
|
3527
|
+
recommended: false
|
|
3528
|
+
},
|
|
3529
|
+
{
|
|
3530
|
+
id: "vue/max-props",
|
|
3531
|
+
severity: "info",
|
|
3532
|
+
category: "vue-builtin",
|
|
3533
|
+
source: "oxlint-builtin",
|
|
3534
|
+
recommended: false
|
|
3535
|
+
},
|
|
3536
|
+
{
|
|
3537
|
+
id: "vue-doctor/no-em-dash-in-string",
|
|
3538
|
+
severity: "warn",
|
|
3539
|
+
category: "ai-slop",
|
|
3540
|
+
source: "doctor",
|
|
3541
|
+
recommended: true
|
|
3542
|
+
},
|
|
3543
|
+
{
|
|
3544
|
+
id: "vue-doctor/no-destructure-props-without-to-refs",
|
|
3545
|
+
severity: "error",
|
|
3546
|
+
category: "ai-slop",
|
|
3547
|
+
source: "doctor",
|
|
3548
|
+
recommended: true
|
|
3549
|
+
},
|
|
3550
|
+
{
|
|
3551
|
+
id: "vue-doctor/no-destructure-reactive-without-to-refs",
|
|
3552
|
+
severity: "error",
|
|
3553
|
+
category: "ai-slop",
|
|
3554
|
+
source: "doctor",
|
|
3555
|
+
recommended: true
|
|
3556
|
+
},
|
|
3557
|
+
{
|
|
3558
|
+
id: "vue-doctor/no-non-null-assertion-on-ref-value",
|
|
3559
|
+
severity: "warn",
|
|
3560
|
+
category: "ai-slop",
|
|
3561
|
+
source: "doctor",
|
|
3562
|
+
recommended: true
|
|
3563
|
+
},
|
|
3564
|
+
{
|
|
3565
|
+
id: "vue-doctor/no-imports-from-vue-when-auto-imported",
|
|
3566
|
+
severity: "warn",
|
|
3567
|
+
category: "ai-slop",
|
|
3568
|
+
source: "doctor",
|
|
3569
|
+
recommended: true
|
|
3570
|
+
},
|
|
3571
|
+
{
|
|
3572
|
+
id: "vue-doctor/reactivity/watch-without-cleanup",
|
|
3573
|
+
severity: "warn",
|
|
3574
|
+
category: "reactivity",
|
|
3575
|
+
source: "doctor",
|
|
3576
|
+
recommended: true
|
|
3577
|
+
},
|
|
3578
|
+
{
|
|
3579
|
+
id: "vue-doctor/reactivity/prefer-shallowRef-for-large-data",
|
|
3580
|
+
severity: "info",
|
|
3581
|
+
category: "reactivity",
|
|
3582
|
+
source: "doctor",
|
|
3583
|
+
recommended: false
|
|
3584
|
+
},
|
|
3585
|
+
{
|
|
3586
|
+
id: "vue-doctor/reactivity/prefer-readonly-for-injected",
|
|
3587
|
+
severity: "info",
|
|
3588
|
+
category: "reactivity",
|
|
3589
|
+
source: "doctor",
|
|
3590
|
+
recommended: false
|
|
3591
|
+
},
|
|
3592
|
+
{
|
|
3593
|
+
id: "vue-doctor/composition/prefer-script-setup-for-new-files",
|
|
3594
|
+
severity: "warn",
|
|
3595
|
+
category: "composition",
|
|
3596
|
+
source: "doctor",
|
|
3597
|
+
recommended: true
|
|
3598
|
+
},
|
|
3599
|
+
{
|
|
3600
|
+
id: "vue-doctor/composition/defineProps-typed",
|
|
3601
|
+
severity: "warn",
|
|
3602
|
+
category: "composition",
|
|
3603
|
+
source: "doctor",
|
|
3604
|
+
recommended: true
|
|
3605
|
+
},
|
|
3606
|
+
{
|
|
3607
|
+
id: "vue-doctor/performance/prefer-defineAsyncComponent-on-route",
|
|
3608
|
+
severity: "info",
|
|
3609
|
+
category: "performance",
|
|
3610
|
+
source: "doctor",
|
|
3611
|
+
recommended: false
|
|
3612
|
+
},
|
|
3613
|
+
{
|
|
3614
|
+
id: "vue-doctor/template/v-for-has-key",
|
|
3615
|
+
severity: "error",
|
|
3616
|
+
category: "template",
|
|
3617
|
+
source: "doctor",
|
|
3618
|
+
recommended: true
|
|
3619
|
+
},
|
|
3620
|
+
{
|
|
3621
|
+
id: "vue-doctor/template/v-if-v-for-precedence",
|
|
3622
|
+
severity: "error",
|
|
3623
|
+
category: "template",
|
|
3624
|
+
source: "doctor",
|
|
3625
|
+
recommended: true
|
|
3626
|
+
},
|
|
3627
|
+
{
|
|
3628
|
+
id: "vue-doctor/template/v-memo-on-large-list",
|
|
3629
|
+
severity: "warn",
|
|
3630
|
+
category: "performance",
|
|
3631
|
+
source: "doctor",
|
|
3632
|
+
recommended: true
|
|
3633
|
+
},
|
|
3634
|
+
{
|
|
3635
|
+
id: "vue-doctor/template/no-inline-object-prop-in-list",
|
|
3636
|
+
severity: "warn",
|
|
3637
|
+
category: "performance",
|
|
3638
|
+
source: "doctor",
|
|
3639
|
+
recommended: true
|
|
3640
|
+
},
|
|
3641
|
+
{
|
|
3642
|
+
id: "vue-doctor/template/no-computed-getter-in-template-loop",
|
|
3643
|
+
severity: "warn",
|
|
3644
|
+
category: "template-perf",
|
|
3645
|
+
source: "doctor",
|
|
3646
|
+
recommended: true
|
|
3647
|
+
},
|
|
3648
|
+
{
|
|
3649
|
+
id: "vue-doctor/template/avoid-deep-v-bind-spread-in-list",
|
|
3650
|
+
severity: "info",
|
|
3651
|
+
category: "template-perf",
|
|
3652
|
+
source: "doctor",
|
|
3653
|
+
recommended: true
|
|
3654
|
+
},
|
|
3655
|
+
{
|
|
3656
|
+
id: "vue-doctor/sfc/no-mixed-options-and-composition-api",
|
|
3657
|
+
severity: "warn",
|
|
3658
|
+
category: "sfc",
|
|
3659
|
+
source: "doctor",
|
|
3660
|
+
recommended: true
|
|
3661
|
+
},
|
|
3662
|
+
{
|
|
3663
|
+
id: "vue-doctor/build-quality/tsconfig-strict-required",
|
|
3664
|
+
severity: "warn",
|
|
3665
|
+
category: "build-quality",
|
|
3666
|
+
source: "doctor",
|
|
3667
|
+
recommended: true
|
|
3668
|
+
},
|
|
3669
|
+
{
|
|
3670
|
+
id: "vue-doctor/build-quality/vue-tsc-in-devDeps",
|
|
3671
|
+
severity: "warn",
|
|
3672
|
+
category: "build-quality",
|
|
3673
|
+
source: "doctor",
|
|
3674
|
+
recommended: true
|
|
3675
|
+
},
|
|
3676
|
+
{
|
|
3677
|
+
id: "vue-doctor/build-quality/no-vue-cli",
|
|
3678
|
+
severity: "warn",
|
|
3679
|
+
category: "build-quality",
|
|
3680
|
+
source: "doctor",
|
|
3681
|
+
recommended: true
|
|
3682
|
+
},
|
|
3683
|
+
{
|
|
3684
|
+
id: "vue-doctor/build-quality/eslint-plugin-vue-installed",
|
|
3685
|
+
severity: "info",
|
|
3686
|
+
category: "build-quality",
|
|
3687
|
+
source: "doctor",
|
|
3688
|
+
recommended: true
|
|
3689
|
+
},
|
|
3690
|
+
{
|
|
3691
|
+
id: "vue-doctor/deps/duplicate-vue-versions",
|
|
3692
|
+
severity: "error",
|
|
3693
|
+
category: "deps",
|
|
3694
|
+
source: "doctor",
|
|
3695
|
+
recommended: true
|
|
3696
|
+
},
|
|
3697
|
+
{
|
|
3698
|
+
id: "vue-doctor/deps/vue-major-current",
|
|
3699
|
+
severity: "info",
|
|
3700
|
+
category: "deps",
|
|
3701
|
+
source: "doctor",
|
|
3702
|
+
recommended: false
|
|
3703
|
+
},
|
|
3704
|
+
{
|
|
3705
|
+
id: "dead-code/unused-file",
|
|
3706
|
+
severity: "warn",
|
|
3707
|
+
category: "dead-code",
|
|
3708
|
+
source: "doctor",
|
|
3709
|
+
recommended: true
|
|
3710
|
+
},
|
|
3711
|
+
{
|
|
3712
|
+
id: "dead-code/unused-export",
|
|
3713
|
+
severity: "warn",
|
|
3714
|
+
category: "dead-code",
|
|
3715
|
+
source: "doctor",
|
|
3716
|
+
recommended: true
|
|
3717
|
+
},
|
|
3718
|
+
{
|
|
3719
|
+
id: "dead-code/unused-type-export",
|
|
3720
|
+
severity: "info",
|
|
3721
|
+
category: "dead-code",
|
|
3722
|
+
source: "doctor",
|
|
3723
|
+
recommended: true
|
|
3724
|
+
},
|
|
3725
|
+
{
|
|
3726
|
+
id: "dead-code/unused-member",
|
|
3727
|
+
severity: "info",
|
|
3728
|
+
category: "dead-code",
|
|
3729
|
+
source: "doctor",
|
|
3730
|
+
recommended: true
|
|
3731
|
+
},
|
|
3732
|
+
{
|
|
3733
|
+
id: "dead-code/unused-dependency",
|
|
3734
|
+
severity: "warn",
|
|
3735
|
+
category: "dead-code",
|
|
3736
|
+
source: "doctor",
|
|
3737
|
+
recommended: true
|
|
3738
|
+
},
|
|
3739
|
+
{
|
|
3740
|
+
id: "dead-code/unlisted-dependency",
|
|
3741
|
+
severity: "error",
|
|
3742
|
+
category: "dead-code",
|
|
3743
|
+
source: "doctor",
|
|
3744
|
+
recommended: true
|
|
3745
|
+
},
|
|
3746
|
+
{
|
|
3747
|
+
id: "dead-code/duplicate-export",
|
|
3748
|
+
severity: "warn",
|
|
3749
|
+
category: "dead-code",
|
|
3750
|
+
source: "doctor",
|
|
3751
|
+
recommended: true
|
|
3752
|
+
},
|
|
3753
|
+
{
|
|
3754
|
+
id: "nuxt-doctor/structure/uses-app-directory",
|
|
3755
|
+
severity: "warn",
|
|
3756
|
+
category: "structure",
|
|
3757
|
+
source: "doctor",
|
|
3758
|
+
recommended: true
|
|
3759
|
+
},
|
|
3760
|
+
{
|
|
3761
|
+
id: "nuxt-doctor/structure/nuxt-major-current",
|
|
3762
|
+
severity: "info",
|
|
3763
|
+
category: "structure",
|
|
3764
|
+
source: "doctor",
|
|
3765
|
+
recommended: false
|
|
3766
|
+
},
|
|
3767
|
+
{
|
|
3768
|
+
id: "nuxt-doctor/modules-deps/no-modules-incompatible-with-nuxt-4",
|
|
3769
|
+
severity: "warn",
|
|
3770
|
+
category: "modules-deps",
|
|
3771
|
+
source: "doctor",
|
|
3772
|
+
recommended: true
|
|
3773
|
+
},
|
|
3774
|
+
{
|
|
3775
|
+
id: "nuxt-doctor/modules-deps/recommended-modules-installed",
|
|
3776
|
+
severity: "info",
|
|
3777
|
+
category: "modules-deps",
|
|
3778
|
+
source: "doctor",
|
|
3779
|
+
recommended: false
|
|
3780
|
+
},
|
|
3781
|
+
{
|
|
3782
|
+
id: "nuxt-doctor/nitro/compatibilityDate-set",
|
|
3783
|
+
severity: "error",
|
|
3784
|
+
category: "nitro",
|
|
3785
|
+
source: "doctor",
|
|
3786
|
+
recommended: true
|
|
3787
|
+
},
|
|
3788
|
+
{
|
|
3789
|
+
id: "nuxt-doctor/nitro/preset-defined-for-deploy-target",
|
|
3790
|
+
severity: "warn",
|
|
3791
|
+
category: "nitro",
|
|
3792
|
+
source: "doctor",
|
|
3793
|
+
recommended: true
|
|
3794
|
+
},
|
|
3795
|
+
{
|
|
3796
|
+
id: "nuxt-doctor/nitro/runtime-config-typed",
|
|
3797
|
+
severity: "info",
|
|
3798
|
+
category: "nitro",
|
|
3799
|
+
source: "doctor",
|
|
3800
|
+
recommended: false
|
|
3801
|
+
},
|
|
3802
|
+
{
|
|
3803
|
+
id: "nuxt-doctor/seo/lang-on-html",
|
|
3804
|
+
severity: "warn",
|
|
3805
|
+
category: "seo",
|
|
3806
|
+
source: "doctor",
|
|
3807
|
+
recommended: true
|
|
3808
|
+
},
|
|
3809
|
+
{
|
|
3810
|
+
id: "nuxt-doctor/seo/useSeoMeta-on-public-page",
|
|
3811
|
+
severity: "warn",
|
|
3812
|
+
category: "seo",
|
|
3813
|
+
source: "doctor",
|
|
3814
|
+
recommended: true
|
|
3815
|
+
},
|
|
3816
|
+
{
|
|
3817
|
+
id: "nuxt-doctor/seo/og-image-defined",
|
|
3818
|
+
severity: "warn",
|
|
3819
|
+
category: "seo",
|
|
3820
|
+
source: "doctor",
|
|
3821
|
+
recommended: true
|
|
3822
|
+
},
|
|
3823
|
+
{
|
|
3824
|
+
id: "nuxt-doctor/cloudflare/nitro-cloudflare-preset",
|
|
3825
|
+
severity: "warn",
|
|
3826
|
+
category: "cloudflare",
|
|
3827
|
+
source: "doctor",
|
|
3828
|
+
recommended: true
|
|
3829
|
+
},
|
|
3830
|
+
{
|
|
3831
|
+
id: "nuxt-doctor/cloudflare/og-image-via-satori",
|
|
3832
|
+
severity: "info",
|
|
3833
|
+
category: "cloudflare",
|
|
3834
|
+
source: "doctor",
|
|
3835
|
+
recommended: false
|
|
3836
|
+
},
|
|
3837
|
+
{
|
|
3838
|
+
id: "nuxt-doctor/cloudflare/no-node-only-modules",
|
|
3839
|
+
severity: "warn",
|
|
3840
|
+
category: "cloudflare",
|
|
3841
|
+
source: "doctor",
|
|
3842
|
+
recommended: true
|
|
3843
|
+
},
|
|
3844
|
+
{
|
|
3845
|
+
id: "nuxt-doctor/data-fetching/no-shared-key-across-pages",
|
|
3846
|
+
severity: "warn",
|
|
3847
|
+
category: "data-fetching",
|
|
3848
|
+
source: "doctor",
|
|
3849
|
+
recommended: true
|
|
3850
|
+
},
|
|
3851
|
+
{
|
|
3852
|
+
id: "nuxt-doctor/data-fetching/ssr-safe-onMounted-only-for-client",
|
|
3853
|
+
severity: "warn",
|
|
3854
|
+
category: "data-fetching",
|
|
3855
|
+
source: "doctor",
|
|
3856
|
+
recommended: true
|
|
3857
|
+
},
|
|
3858
|
+
{
|
|
3859
|
+
id: "nuxt-doctor/ai-slop/no-mixed-app-and-root-layout",
|
|
3860
|
+
severity: "warn",
|
|
3861
|
+
category: "ai-slop",
|
|
3862
|
+
source: "doctor",
|
|
3863
|
+
recommended: true
|
|
3864
|
+
},
|
|
3865
|
+
{
|
|
3866
|
+
id: "nuxt-doctor/ai-slop/no-process-client-server",
|
|
3867
|
+
severity: "error",
|
|
3868
|
+
category: "ai-slop",
|
|
3869
|
+
source: "doctor",
|
|
3870
|
+
recommended: true
|
|
3871
|
+
},
|
|
3872
|
+
{
|
|
3873
|
+
id: "nuxt-doctor/ai-slop/no-explicit-imports-of-auto-imported",
|
|
3874
|
+
severity: "warn",
|
|
3875
|
+
category: "ai-slop",
|
|
3876
|
+
source: "doctor",
|
|
3877
|
+
recommended: true
|
|
3878
|
+
},
|
|
3879
|
+
{
|
|
3880
|
+
id: "nuxt-doctor/ai-slop/no-useState-for-server-data",
|
|
3881
|
+
severity: "warn",
|
|
3882
|
+
category: "ai-slop",
|
|
3883
|
+
source: "doctor",
|
|
3884
|
+
recommended: true
|
|
3885
|
+
},
|
|
3886
|
+
{
|
|
3887
|
+
id: "nuxt-doctor/ai-slop/no-fetch-in-setup",
|
|
3888
|
+
severity: "warn",
|
|
3889
|
+
category: "ai-slop",
|
|
3890
|
+
source: "doctor",
|
|
3891
|
+
recommended: true
|
|
3892
|
+
},
|
|
3893
|
+
{
|
|
3894
|
+
id: "nuxt-doctor/data-fetching/useAsyncData-key-required-in-loop",
|
|
3895
|
+
severity: "error",
|
|
3896
|
+
category: "data-fetching",
|
|
3897
|
+
source: "doctor",
|
|
3898
|
+
recommended: true
|
|
3899
|
+
},
|
|
3900
|
+
{
|
|
3901
|
+
id: "nuxt-doctor/server-routes/defineEventHandler-typed",
|
|
3902
|
+
severity: "warn",
|
|
3903
|
+
category: "server-routes",
|
|
3904
|
+
source: "doctor",
|
|
3905
|
+
recommended: true
|
|
3906
|
+
},
|
|
3907
|
+
{
|
|
3908
|
+
id: "nuxt-doctor/server-routes/validate-body-with-h3-v2",
|
|
3909
|
+
severity: "warn",
|
|
3910
|
+
category: "server-routes",
|
|
3911
|
+
source: "doctor",
|
|
3912
|
+
recommended: true
|
|
3913
|
+
},
|
|
3914
|
+
{
|
|
3915
|
+
id: "nuxt-doctor/server-routes/createError-on-failure",
|
|
3916
|
+
severity: "warn",
|
|
3917
|
+
category: "server-routes",
|
|
3918
|
+
source: "doctor",
|
|
3919
|
+
recommended: true
|
|
3920
|
+
},
|
|
3921
|
+
{
|
|
3922
|
+
id: "nuxt-doctor/hydration/no-document-in-setup",
|
|
3923
|
+
severity: "error",
|
|
3924
|
+
category: "hydration",
|
|
3925
|
+
source: "doctor",
|
|
3926
|
+
recommended: true
|
|
3927
|
+
},
|
|
3928
|
+
{
|
|
3929
|
+
id: "nuxt-doctor/hydration/clientOnly-for-browser-apis",
|
|
3930
|
+
severity: "error",
|
|
3931
|
+
category: "hydration",
|
|
3932
|
+
source: "doctor",
|
|
3933
|
+
recommended: true
|
|
3934
|
+
},
|
|
3935
|
+
{
|
|
3936
|
+
id: "vue-doctor/security/no-v-html",
|
|
3937
|
+
severity: "error",
|
|
3938
|
+
category: "security",
|
|
3939
|
+
source: "doctor",
|
|
3940
|
+
recommended: true
|
|
3941
|
+
},
|
|
3942
|
+
{
|
|
3943
|
+
id: "vue-doctor/security/no-inner-html",
|
|
3944
|
+
severity: "error",
|
|
3945
|
+
category: "security",
|
|
3946
|
+
source: "doctor",
|
|
3947
|
+
recommended: true
|
|
3948
|
+
},
|
|
3949
|
+
{
|
|
3950
|
+
id: "vue-doctor/security/no-eval-like",
|
|
3951
|
+
severity: "error",
|
|
3952
|
+
category: "security",
|
|
3953
|
+
source: "doctor",
|
|
3954
|
+
recommended: true
|
|
3955
|
+
},
|
|
3956
|
+
{
|
|
3957
|
+
id: "vue-doctor/security/no-auth-token-in-web-storage",
|
|
3958
|
+
severity: "warn",
|
|
3959
|
+
category: "security",
|
|
3960
|
+
source: "doctor",
|
|
3961
|
+
recommended: true
|
|
3962
|
+
},
|
|
3963
|
+
{
|
|
3964
|
+
id: "vue-doctor/security/no-secrets-in-source",
|
|
3965
|
+
severity: "warn",
|
|
3966
|
+
category: "security",
|
|
3967
|
+
source: "doctor",
|
|
3968
|
+
recommended: true
|
|
3969
|
+
},
|
|
3970
|
+
{
|
|
3971
|
+
id: "vue-doctor/security/no-target-blank-without-rel",
|
|
3972
|
+
severity: "warn",
|
|
3973
|
+
category: "security",
|
|
3974
|
+
source: "doctor",
|
|
3975
|
+
recommended: true
|
|
3976
|
+
},
|
|
3977
|
+
{
|
|
3978
|
+
id: "vue-doctor/security/no-javascript-uri",
|
|
3979
|
+
severity: "error",
|
|
3980
|
+
category: "security",
|
|
3981
|
+
source: "doctor",
|
|
3982
|
+
recommended: true
|
|
3983
|
+
},
|
|
3984
|
+
{
|
|
3985
|
+
id: "nuxt-doctor/security/no-secret-in-public-runtime-config",
|
|
3986
|
+
severity: "error",
|
|
3987
|
+
category: "security",
|
|
3988
|
+
source: "doctor",
|
|
3989
|
+
recommended: true
|
|
3990
|
+
},
|
|
3991
|
+
{
|
|
3992
|
+
id: "nuxt-doctor/security/no-user-input-in-fetch-url",
|
|
3993
|
+
severity: "warn",
|
|
3994
|
+
category: "security",
|
|
3995
|
+
source: "doctor",
|
|
3996
|
+
recommended: true
|
|
3997
|
+
},
|
|
3998
|
+
{
|
|
3999
|
+
id: "vue-doctor/design/no-arbitrary-tailwind-values",
|
|
4000
|
+
severity: "warn",
|
|
4001
|
+
category: "design",
|
|
4002
|
+
source: "doctor",
|
|
4003
|
+
recommended: true
|
|
4004
|
+
},
|
|
4005
|
+
{
|
|
4006
|
+
id: "vue-doctor/design/no-raw-hex-color",
|
|
4007
|
+
severity: "warn",
|
|
4008
|
+
category: "design",
|
|
4009
|
+
source: "doctor",
|
|
4010
|
+
recommended: true
|
|
4011
|
+
},
|
|
4012
|
+
{
|
|
4013
|
+
id: "vue-doctor/design/no-default-tailwind-palette",
|
|
4014
|
+
severity: "warn",
|
|
4015
|
+
category: "design",
|
|
4016
|
+
source: "doctor",
|
|
4017
|
+
recommended: true
|
|
4018
|
+
},
|
|
4019
|
+
{
|
|
4020
|
+
id: "vue-doctor/design/no-important-utility",
|
|
4021
|
+
severity: "warn",
|
|
4022
|
+
category: "design",
|
|
4023
|
+
source: "doctor",
|
|
4024
|
+
recommended: true
|
|
4025
|
+
},
|
|
4026
|
+
{
|
|
4027
|
+
id: "vue-doctor/design/no-hardcoded-inline-style",
|
|
4028
|
+
severity: "warn",
|
|
4029
|
+
category: "design",
|
|
4030
|
+
source: "doctor",
|
|
4031
|
+
recommended: true
|
|
4032
|
+
},
|
|
4033
|
+
{
|
|
4034
|
+
id: "vue-doctor/design/no-missing-alt",
|
|
4035
|
+
severity: "warn",
|
|
4036
|
+
category: "design",
|
|
4037
|
+
source: "doctor",
|
|
4038
|
+
recommended: true
|
|
4039
|
+
},
|
|
4040
|
+
{
|
|
4041
|
+
id: "vue-doctor/design/no-absurd-z-index",
|
|
4042
|
+
severity: "warn",
|
|
4043
|
+
category: "design",
|
|
4044
|
+
source: "doctor",
|
|
4045
|
+
recommended: true
|
|
4046
|
+
}
|
|
4047
|
+
];
|
|
4048
|
+
function listRules(filter = {}) {
|
|
4049
|
+
let rules = [...RULE_REGISTRY];
|
|
4050
|
+
if (filter.preset === "recommended") rules = rules.filter((r) => r.recommended);
|
|
4051
|
+
if (filter.category) rules = rules.filter((r) => r.category === filter.category);
|
|
4052
|
+
if (filter.source) rules = rules.filter((r) => r.source === filter.source);
|
|
4053
|
+
if (filter.severity) rules = rules.filter((r) => r.severity === filter.severity);
|
|
4054
|
+
return rules.sort((a, b) => a.id.localeCompare(b.id));
|
|
4055
|
+
}
|
|
4056
|
+
//#endregion
|
|
4057
|
+
//#region src/config/presets.ts
|
|
4058
|
+
const PRESET_NAMES = [
|
|
4059
|
+
"minimal",
|
|
4060
|
+
"recommended",
|
|
4061
|
+
"strict",
|
|
4062
|
+
"all"
|
|
4063
|
+
];
|
|
4064
|
+
function isPresetName(value) {
|
|
4065
|
+
return PRESET_NAMES.includes(value);
|
|
4066
|
+
}
|
|
4067
|
+
/**
|
|
4068
|
+
* Resolve a preset name to its base ruleId -> Severity map.
|
|
4069
|
+
*
|
|
4070
|
+
* - `minimal` : errors only (warn/info turned off)
|
|
4071
|
+
* - `recommended` : errors + warns (info off) — same as today's default
|
|
4072
|
+
* - `strict` : errors + warns + infos all on at registered severity
|
|
4073
|
+
* - `all` : alias of `strict` for now; reserved to surface every
|
|
4074
|
+
* known ruleId regardless of preset opt-in policy
|
|
4075
|
+
*
|
|
4076
|
+
* The returned map is the BASE; downstream code merges user config
|
|
4077
|
+
* `rules:` on top, then CLI `--rule` overrides on top of that.
|
|
4078
|
+
*/
|
|
4079
|
+
function resolvePreset(name) {
|
|
4080
|
+
const rules = {};
|
|
4081
|
+
for (const rule of RULE_REGISTRY) if (name === "minimal") {
|
|
4082
|
+
if (rule.severity === "error") rules[rule.id] = "error";
|
|
4083
|
+
} else if (name === "recommended") {
|
|
4084
|
+
if (rule.severity === "error" || rule.severity === "warn") rules[rule.id] = rule.severity;
|
|
4085
|
+
} else rules[rule.id] = rule.severity;
|
|
4086
|
+
return rules;
|
|
4087
|
+
}
|
|
4088
|
+
//#endregion
|
|
4089
|
+
//#region src/config/schema.ts
|
|
4090
|
+
/**
|
|
4091
|
+
* A rule severity level as accepted in user config. `off` disables a rule
|
|
4092
|
+
* (removing it from the resolved preset base); the other three mirror the
|
|
4093
|
+
* internal {@link import('../types.js').Severity} vocabulary.
|
|
4094
|
+
*/
|
|
4095
|
+
const LevelSchema = z.enum([
|
|
4096
|
+
"error",
|
|
4097
|
+
"warn",
|
|
4098
|
+
"info",
|
|
4099
|
+
"off"
|
|
4100
|
+
]);
|
|
4101
|
+
/**
|
|
4102
|
+
* The accepted shape of a single `rules` entry. Doctor config only ever takes
|
|
4103
|
+
* a bare severity string (no `[level, options]` tuple, no `{ level }` object) —
|
|
4104
|
+
* this matches the historical hand-rolled validator exactly.
|
|
4105
|
+
*/
|
|
4106
|
+
const RuleEntrySchema = LevelSchema;
|
|
4107
|
+
const ruleIds = RULE_REGISTRY.map((rule) => rule.id);
|
|
4108
|
+
/**
|
|
4109
|
+
* Dynamic `rules` object: every registered rule id is a known optional key, and
|
|
4110
|
+
* a `catchall` keeps unknown keys parseable so {@link DoctorUserConfigSchema}'s
|
|
4111
|
+
* `superRefine` can report them with a precise path. Building the object from
|
|
4112
|
+
* RULE_REGISTRY is what lets `z.toJSONSchema()` enumerate the known ids.
|
|
4113
|
+
*/
|
|
4114
|
+
const RulesSchema = z.object(Object.fromEntries(ruleIds.map((id) => [id, RuleEntrySchema.optional()]))).catchall(RuleEntrySchema.optional());
|
|
4115
|
+
/**
|
|
4116
|
+
* Scalar fields of the user config. `.loose()` preserves unknown top-level keys
|
|
4117
|
+
* (forward-compatibility, matching the old validator which ignored them) and is
|
|
4118
|
+
* required so a `$schema` editor hint round-trips untouched.
|
|
4119
|
+
*/
|
|
4120
|
+
const BaseUserConfigSchema = z.object({
|
|
4121
|
+
$schema: z.string().optional(),
|
|
4122
|
+
rootDir: z.string().optional(),
|
|
4123
|
+
include: z.array(z.string()).optional(),
|
|
4124
|
+
exclude: z.array(z.string()).optional(),
|
|
4125
|
+
failOn: z.enum([
|
|
4126
|
+
"error",
|
|
4127
|
+
"warn",
|
|
4128
|
+
"none"
|
|
4129
|
+
]).optional(),
|
|
4130
|
+
threshold: z.int().min(0).max(100).optional(),
|
|
4131
|
+
preset: z.enum([
|
|
4132
|
+
"minimal",
|
|
4133
|
+
"recommended",
|
|
4134
|
+
"strict",
|
|
4135
|
+
"all"
|
|
4136
|
+
], { error: "must be one of 'minimal', 'recommended', 'strict', 'all'" }).optional(),
|
|
4137
|
+
extends: z.array(z.string()).optional(),
|
|
4138
|
+
fixExcludes: z.array(z.string()).optional(),
|
|
4139
|
+
rules: RulesSchema.optional()
|
|
4140
|
+
}).loose();
|
|
4141
|
+
/**
|
|
4142
|
+
* The single source of truth for doctor config validation. Wraps the base
|
|
4143
|
+
* object in a `superRefine` that rejects any unknown rule id with a
|
|
4144
|
+
* `rules.<id>` path, so a typo'd rule surfaces as an `InvalidConfigError`
|
|
4145
|
+
* rather than silently doing nothing.
|
|
4146
|
+
*/
|
|
4147
|
+
const DoctorUserConfigSchema = BaseUserConfigSchema.superRefine((value, ctx) => {
|
|
4148
|
+
const rules = value.rules;
|
|
4149
|
+
if (!rules) return;
|
|
4150
|
+
const known = new Set(ruleIds);
|
|
4151
|
+
for (const key of Object.keys(rules)) if (!known.has(key)) ctx.addIssue({
|
|
4152
|
+
code: "custom",
|
|
4153
|
+
path: ["rules", key],
|
|
4154
|
+
message: `unknown rule id ${JSON.stringify(key)}`
|
|
4155
|
+
});
|
|
4156
|
+
});
|
|
4157
|
+
/**
|
|
4158
|
+
* Generate the JSON Schema for the user config. Built from the base object
|
|
4159
|
+
* (not the refined wrapper) so `z.toJSONSchema` emits the full property set,
|
|
4160
|
+
* including every enumerated rule id. The result is what gets written to
|
|
4161
|
+
* `schema.json` and served from the docs site.
|
|
4162
|
+
*/
|
|
4163
|
+
function buildJsonSchema() {
|
|
4164
|
+
return z.toJSONSchema(BaseUserConfigSchema);
|
|
4165
|
+
}
|
|
4166
|
+
//#endregion
|
|
4167
|
+
//#region src/config/validate.ts
|
|
4168
|
+
/**
|
|
4169
|
+
* Render a single zod issue into the historical hand-rolled message format
|
|
4170
|
+
* (`<path>: <message>`), so InvalidConfigError.message stays backward-compatible
|
|
4171
|
+
* with callers that assert on substrings like `threshold`, `failOn`, or
|
|
4172
|
+
* `rules.foo/bar`. A root-level issue (empty path) maps to the legacy
|
|
4173
|
+
* `config: must be an object` wording.
|
|
4174
|
+
*/
|
|
4175
|
+
function formatIssue(issue) {
|
|
4176
|
+
if (issue.path.length === 0) return "config: must be an object";
|
|
4177
|
+
return `${issue.path.map((segment) => String(segment)).join(".")}: ${issue.message}`;
|
|
4178
|
+
}
|
|
4179
|
+
/**
|
|
4180
|
+
* Validate a raw, untrusted config object against the zod schema — the single
|
|
4181
|
+
* source of truth. Throws {@link InvalidConfigError} (carrying the full zod
|
|
4182
|
+
* issue list) on the first failure, mirroring the legacy validator's
|
|
4183
|
+
* throw-on-first-error contract.
|
|
4184
|
+
*/
|
|
4185
|
+
function validateConfig(raw) {
|
|
4186
|
+
const result = DoctorUserConfigSchema.safeParse(raw);
|
|
4187
|
+
if (result.success) return;
|
|
4188
|
+
const { issues } = result.error;
|
|
4189
|
+
throw new InvalidConfigError(formatIssue(issues[0]), issues);
|
|
4190
|
+
}
|
|
4191
|
+
//#endregion
|
|
4192
|
+
//#region src/config/load.ts
|
|
4193
|
+
var load_exports = /* @__PURE__ */ __exportAll({ loadDoctorConfig: () => loadDoctorConfig });
|
|
4194
|
+
const SOURCE_MAP = {
|
|
4195
|
+
ts: "ts",
|
|
4196
|
+
mjs: "mjs",
|
|
4197
|
+
js: "js",
|
|
4198
|
+
json: "json",
|
|
4199
|
+
jsonc: "jsonc"
|
|
4200
|
+
};
|
|
4201
|
+
async function loadDoctorConfig(rootDir, explicitPathOrOptions) {
|
|
4202
|
+
const opts = typeof explicitPathOrOptions === "string" ? { explicitPath: explicitPathOrOptions } : explicitPathOrOptions ?? {};
|
|
4203
|
+
const explicitPath = opts.explicitPath;
|
|
4204
|
+
if (opts.presetOverride !== void 0 && !isPresetName(opts.presetOverride)) throw new InvalidConfigError(`preset: must be one of 'minimal', 'recommended', 'strict', 'all', got ${JSON.stringify(opts.presetOverride)}`);
|
|
4205
|
+
if (explicitPath) {
|
|
4206
|
+
if (!existsSync(resolve(rootDir, explicitPath))) throw new ConfigFileNotFoundError(explicitPath);
|
|
4207
|
+
}
|
|
4208
|
+
const chain = [];
|
|
4209
|
+
const result = await loadConfig({
|
|
4210
|
+
cwd: rootDir,
|
|
4211
|
+
name: "doctor",
|
|
4212
|
+
packageJson: "doctor",
|
|
4213
|
+
rcFile: false,
|
|
4214
|
+
globalRc: false,
|
|
4215
|
+
...explicitPath ? { configFile: resolve(rootDir, explicitPath) } : {},
|
|
4216
|
+
resolve(source, options) {
|
|
4217
|
+
const base = options.cwd;
|
|
4218
|
+
const key = resolve(base, source);
|
|
4219
|
+
if (chain.includes(key)) throw new ConfigCycleError([...chain, key]);
|
|
4220
|
+
chain.push(key);
|
|
4221
|
+
}
|
|
4222
|
+
});
|
|
4223
|
+
const raw = result.config;
|
|
4224
|
+
validateConfig(raw);
|
|
4225
|
+
let source;
|
|
4226
|
+
let configFile;
|
|
4227
|
+
if (explicitPath) {
|
|
4228
|
+
source = "flag";
|
|
4229
|
+
configFile = resolve(rootDir, explicitPath);
|
|
4230
|
+
} else if (result._configFile) {
|
|
4231
|
+
source = SOURCE_MAP[extname(result._configFile).slice(1)];
|
|
4232
|
+
configFile = result._configFile;
|
|
4233
|
+
} else if (Object.keys(raw).length > 0) {
|
|
4234
|
+
source = "package.json";
|
|
4235
|
+
configFile = resolve(rootDir, "package.json");
|
|
4236
|
+
} else {
|
|
4237
|
+
source = "built-in";
|
|
4238
|
+
configFile = void 0;
|
|
4239
|
+
}
|
|
4240
|
+
const presetName = opts.presetOverride ?? raw.preset ?? "recommended";
|
|
4241
|
+
const presetRules = resolvePreset(presetName);
|
|
4242
|
+
const userRules = {};
|
|
4243
|
+
const userOff = /* @__PURE__ */ new Set();
|
|
4244
|
+
if (raw.rules && typeof raw.rules === "object") for (const [key, value] of Object.entries(raw.rules)) if (value === "off") userOff.add(key);
|
|
4245
|
+
else userRules[key] = value;
|
|
4246
|
+
const mergedRules = {
|
|
4247
|
+
...presetRules,
|
|
4248
|
+
...userRules
|
|
4249
|
+
};
|
|
4250
|
+
for (const key of userOff) delete mergedRules[key];
|
|
4251
|
+
return {
|
|
4252
|
+
rootDir,
|
|
4253
|
+
include: raw.include ?? BUILT_IN_RECOMMENDED.include,
|
|
4254
|
+
exclude: raw.exclude ?? BUILT_IN_RECOMMENDED.exclude,
|
|
4255
|
+
failOn: raw.failOn ?? BUILT_IN_RECOMMENDED.failOn,
|
|
4256
|
+
threshold: raw.threshold ?? BUILT_IN_RECOMMENDED.threshold,
|
|
4257
|
+
rules: mergedRules,
|
|
4258
|
+
preset: presetName,
|
|
4259
|
+
source,
|
|
4260
|
+
configFile,
|
|
4261
|
+
...raw.fixExcludes ? { fixExcludes: raw.fixExcludes } : {}
|
|
4262
|
+
};
|
|
4263
|
+
}
|
|
4264
|
+
//#endregion
|
|
2807
4265
|
//#region src/config/merge-cli-overrides.ts
|
|
2808
4266
|
function mergeCliOverrides(resolved, cli) {
|
|
2809
4267
|
const rules = { ...resolved.rules };
|
|
@@ -3474,6 +4932,177 @@ function format(input, kind = "agent", options) {
|
|
|
3474
4932
|
return agentReport(input, options);
|
|
3475
4933
|
}
|
|
3476
4934
|
//#endregion
|
|
4935
|
+
//#region src/rule-prompt.ts
|
|
4936
|
+
function cliForRule(ruleId) {
|
|
4937
|
+
return ruleId.startsWith("nuxt-doctor/") ? "@geoql/nuxt-doctor" : "@geoql/vue-doctor";
|
|
4938
|
+
}
|
|
4939
|
+
function presetLine(doc) {
|
|
4940
|
+
return doc.recommended ? "This rule is part of the `recommended` preset, so it runs by default." : `This rule is off by default in \`recommended\`; enable it with \`--rule ${doc.id}:warn\` or in \`doctor.config.ts\`.`;
|
|
4941
|
+
}
|
|
4942
|
+
function article(word) {
|
|
4943
|
+
return /^[aeiou]/i.test(word) ? "an" : "a";
|
|
4944
|
+
}
|
|
4945
|
+
function whyThisMatters(doc) {
|
|
4946
|
+
const severityNote = doc.severity === "error" ? "It is an `error`-severity rule: a finding is a correctness, security, or hard-correctness defect that should block a clean audit." : doc.severity === "warn" ? "It is a `warn`-severity rule: a finding is a likely bug, performance trap, or AI-slop pattern worth fixing before shipping." : "It is an `info`-severity rule: a finding is an opt-in suggestion that nudges the code toward the idiomatic Vue 3 / Nuxt 4 pattern.";
|
|
4947
|
+
return `\`${doc.id}\` is ${article(doc.severity)} ${doc.severity}-severity ${doc.category} rule from ${doc.source}. ${severityNote}\n\n${presetLine(doc)}`;
|
|
4948
|
+
}
|
|
4949
|
+
function howToFix(doc, cli) {
|
|
4950
|
+
return [
|
|
4951
|
+
`Open each file the audit flags for \`${doc.id}\` and edit the offending code in the working tree directly.`,
|
|
4952
|
+
"",
|
|
4953
|
+
`1. Read the canonical rule reference at ${doc.helpUri} for the full rationale and the idiomatic ${doc.category} pattern.`,
|
|
4954
|
+
`2. Apply the smallest change that resolves the ${doc.category} concern without altering unrelated behavior — do not suppress the finding with an inline disable unless the rule is a genuine false positive for that line.`,
|
|
4955
|
+
`3. Run \`npx ${cli} explain ${doc.id}\` for an inline summary of the severity, category, and help link while you work.`,
|
|
4956
|
+
"",
|
|
4957
|
+
"Fix by severity: resolve `error` findings first, then `warn`, then `info`."
|
|
4958
|
+
].join("\n");
|
|
4959
|
+
}
|
|
4960
|
+
function howToValidate(doc, cli) {
|
|
4961
|
+
return [
|
|
4962
|
+
"Re-run the audit scoped to just this rule and confirm it reports zero findings:",
|
|
4963
|
+
"",
|
|
4964
|
+
"```bash",
|
|
4965
|
+
`npx ${cli} --rule ${doc.id}:error --full`,
|
|
4966
|
+
"```",
|
|
4967
|
+
"",
|
|
4968
|
+
`A clean run (no diagnostics for \`${doc.id}\`) means the fix is complete. If findings remain, repeat the "How to fix" steps on the still-flagged files.`
|
|
4969
|
+
].join("\n");
|
|
4970
|
+
}
|
|
4971
|
+
/**
|
|
4972
|
+
* Render a self-contained "fix + validate" markdown recipe for a single rule.
|
|
4973
|
+
* Returns `null` for an unknown rule id. Pure: derives everything from the
|
|
4974
|
+
* rule's `RuleDoc` (id, severity, category, source, recommended, helpUri).
|
|
4975
|
+
*/
|
|
4976
|
+
function renderRulePrompt(ruleId) {
|
|
4977
|
+
const doc = loadRuleDoc(ruleId);
|
|
4978
|
+
if (!doc) return null;
|
|
4979
|
+
const cli = cliForRule(doc.id);
|
|
4980
|
+
return [
|
|
4981
|
+
`# Fix: \`${doc.id}\``,
|
|
4982
|
+
"",
|
|
4983
|
+
`> **Rule** \`${doc.id}\` · **Severity** \`${doc.severity}\` · **Category** \`${doc.category}\` · **Source** \`${doc.source}\``,
|
|
4984
|
+
"",
|
|
4985
|
+
"## Why this matters",
|
|
4986
|
+
"",
|
|
4987
|
+
whyThisMatters(doc),
|
|
4988
|
+
"",
|
|
4989
|
+
"## How to fix",
|
|
4990
|
+
"",
|
|
4991
|
+
howToFix(doc, cli),
|
|
4992
|
+
"",
|
|
4993
|
+
"## How to validate",
|
|
4994
|
+
"",
|
|
4995
|
+
howToValidate(doc, cli),
|
|
4996
|
+
"",
|
|
4997
|
+
"## Reference",
|
|
4998
|
+
"",
|
|
4999
|
+
`Canonical rule documentation: ${doc.helpUri}`,
|
|
5000
|
+
""
|
|
5001
|
+
].join("\n");
|
|
5002
|
+
}
|
|
5003
|
+
/**
|
|
5004
|
+
* Render the canonical agent playbook: the scan → filter → triage → fix →
|
|
5005
|
+
* validate loop an AI agent follows when cleaning up doctor diagnostics. Pure
|
|
5006
|
+
* markdown; fetched on demand by the agent skill at runtime.
|
|
5007
|
+
*/
|
|
5008
|
+
function renderAgentPlaybook() {
|
|
5009
|
+
return [
|
|
5010
|
+
"# Doctor Agent Playbook",
|
|
5011
|
+
"",
|
|
5012
|
+
"You are an AI agent cleaning up `@geoql/doctor` findings in a Vue 3 / Nuxt 4",
|
|
5013
|
+
"codebase. Doctor does not generate code — it critiques the code you just",
|
|
5014
|
+
"wrote. Follow this loop exactly.",
|
|
5015
|
+
"",
|
|
5016
|
+
"## Guardrails",
|
|
5017
|
+
"",
|
|
5018
|
+
"- Edit the working tree directly to fix findings.",
|
|
5019
|
+
"- **NEVER commit.** Leave staging and committing to the human.",
|
|
5020
|
+
"- **NEVER open a pull request**, push a branch, or run any git write command.",
|
|
5021
|
+
"- Never weaken a rule to make a finding disappear; fix the underlying code.",
|
|
5022
|
+
"",
|
|
5023
|
+
"## Pick the right CLI",
|
|
5024
|
+
"",
|
|
5025
|
+
"Use `npx @geoql/nuxt-doctor` for a Nuxt 4 project (it runs the Vue passes",
|
|
5026
|
+
"too) and `npx @geoql/vue-doctor` for a standalone Vue 3 project. The flags",
|
|
5027
|
+
`below are identical for both. Doctor ships ~${loadAllRuleDocs().length} rules across the vue,`,
|
|
5028
|
+
"nuxt, and oxlint-builtin plugins.",
|
|
5029
|
+
"",
|
|
5030
|
+
"## The loop",
|
|
5031
|
+
"",
|
|
5032
|
+
"### 1. Scan",
|
|
5033
|
+
"",
|
|
5034
|
+
"Audit only what changed so you stay focused on your own edits:",
|
|
5035
|
+
"",
|
|
5036
|
+
"```bash",
|
|
5037
|
+
"npx @geoql/vue-doctor --diff # findings in files changed vs HEAD",
|
|
5038
|
+
"npx @geoql/vue-doctor --staged # findings in staged files only",
|
|
5039
|
+
"npx @geoql/vue-doctor --full # complete scan (overrides --diff/--staged)",
|
|
5040
|
+
"```",
|
|
5041
|
+
"",
|
|
5042
|
+
"For a quick health gate, read just the score:",
|
|
5043
|
+
"",
|
|
5044
|
+
"```bash",
|
|
5045
|
+
"npx @geoql/vue-doctor --score # prints a single 0-100 integer",
|
|
5046
|
+
"```",
|
|
5047
|
+
"",
|
|
5048
|
+
"### 2. Filter",
|
|
5049
|
+
"",
|
|
5050
|
+
"Get the machine-readable report and work from it:",
|
|
5051
|
+
"",
|
|
5052
|
+
"```bash",
|
|
5053
|
+
"npx @geoql/vue-doctor --diff --json",
|
|
5054
|
+
"```",
|
|
5055
|
+
"",
|
|
5056
|
+
"Group the diagnostics by `ruleId`. Drop nothing — every finding is in scope",
|
|
5057
|
+
"unless the human told you otherwise.",
|
|
5058
|
+
"",
|
|
5059
|
+
"### 3. Triage",
|
|
5060
|
+
"",
|
|
5061
|
+
"Order the work by severity: `error` first, then `warn`, then `info`. To",
|
|
5062
|
+
"understand a single rule before touching code:",
|
|
5063
|
+
"",
|
|
5064
|
+
"```bash",
|
|
5065
|
+
"npx @geoql/vue-doctor explain <rule> # severity, category, help link",
|
|
5066
|
+
"npx @geoql/vue-doctor list-rules # every registered rule (alias: rules)",
|
|
5067
|
+
"```",
|
|
5068
|
+
"",
|
|
5069
|
+
"### 4. Fix",
|
|
5070
|
+
"",
|
|
5071
|
+
"For each rule with findings, fetch the canonical per-rule recipe and apply",
|
|
5072
|
+
"it to every flagged file:",
|
|
5073
|
+
"",
|
|
5074
|
+
"```bash",
|
|
5075
|
+
"curl --fail --silent --show-error \\",
|
|
5076
|
+
" https://docs.the-doctor.report/prompts/rules/<plugin>/<rule>.md",
|
|
5077
|
+
"```",
|
|
5078
|
+
"",
|
|
5079
|
+
"For example, `vue-doctor/template/v-for-has-key` lives at",
|
|
5080
|
+
"`/prompts/rules/vue-doctor/template/v-for-has-key.md`. Each recipe explains",
|
|
5081
|
+
"why the rule matters, how to fix it, and how to validate the fix. Edit the",
|
|
5082
|
+
"working tree directly — never an inline disable unless it is a true false",
|
|
5083
|
+
"positive.",
|
|
5084
|
+
"",
|
|
5085
|
+
"### 5. Validate",
|
|
5086
|
+
"",
|
|
5087
|
+
"After fixing a rule, re-run the audit scoped to just that rule and confirm",
|
|
5088
|
+
"zero findings:",
|
|
5089
|
+
"",
|
|
5090
|
+
"```bash",
|
|
5091
|
+
"npx @geoql/vue-doctor --rule <rule>:error --full",
|
|
5092
|
+
"```",
|
|
5093
|
+
"",
|
|
5094
|
+
"Then re-run the full scan and confirm the score did not regress:",
|
|
5095
|
+
"",
|
|
5096
|
+
"```bash",
|
|
5097
|
+
"npx @geoql/vue-doctor --full --score",
|
|
5098
|
+
"```",
|
|
5099
|
+
"",
|
|
5100
|
+
"Repeat the loop until the score is clean. Hand the working tree back to the",
|
|
5101
|
+
"human to review and commit.",
|
|
5102
|
+
""
|
|
5103
|
+
].join("\n");
|
|
5104
|
+
}
|
|
5105
|
+
//#endregion
|
|
3477
5106
|
//#region src/audit-filter.ts
|
|
3478
5107
|
/**
|
|
3479
5108
|
* Filter `report.diagnostics` to a known set of allowed rule ids, then
|
|
@@ -3633,6 +5262,6 @@ async function pushFindings(opts) {
|
|
|
3633
5262
|
}
|
|
3634
5263
|
}
|
|
3635
5264
|
//#endregion
|
|
3636
|
-
export { BUILT_IN_RECOMMENDED, ConfigCycleError, ConfigFileNotFoundError, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, InvalidConfigError, OxlintOutputTooLarge, OxlintSpawnFailed, RULE_REGISTRY, agentReport, applyInlineDisables, audit, buildDoctorReport, buildPushPayload, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, detectSummary, docsUrl, encodeAnnotation, encodeAnnotations, filterReportByRules, findMonorepoRoot, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, listWorkspacePackages, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, normalizeInitAnswers, parseDirectives, parseExcludeList, planInit, prCommentReport, prettyReport, pushFindings, renderDetectSummary, renderVerboseTrace, runCrossFilePass, sarifReport, scoreDiagnostics, stripFindings, validateConfig };
|
|
5265
|
+
export { BUILT_IN_RECOMMENDED, ConfigCycleError, ConfigFileNotFoundError, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, DoctorUserConfigSchema, InvalidConfigError, LevelSchema, OxlintOutputTooLarge, OxlintSpawnFailed, RULE_REGISTRY, RuleEntrySchema, agentReport, applyInlineDisables, audit, buildDoctorReport, buildJsonSchema, buildPushPayload, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, detectSummary, docsUrl, encodeAnnotation, encodeAnnotations, filterReportByRules, findMonorepoRoot, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, jsonCompactReport, jsonReport, listChangedFiles, listRules, listWorkspacePackages, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, normalizeInitAnswers, parseDirectives, parseExcludeList, planInit, prCommentReport, prettyReport, pushFindings, renderAgentPlaybook, renderDetectSummary, renderRulePrompt, renderVerboseTrace, runCrossFilePass, sarifReport, scoreDiagnostics, stripFindings, validateConfig };
|
|
3637
5266
|
|
|
3638
5267
|
//# sourceMappingURL=index.js.map
|