@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.js CHANGED
@@ -1,5 +1,4 @@
1
- import { a as listRules, c as InvalidConfigError, i as RULE_REGISTRY, l as BUILT_IN_RECOMMENDED, o as ConfigCycleError, r as validateConfig, s as ConfigFileNotFoundError, t as loadDoctorConfig } from "./load-yXDi-5ti.js";
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$20 = "vue-doctor/build-quality/eslint-plugin-vue-installed";
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$20,
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$19 = "vue-doctor/build-quality/no-vue-cli";
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$19,
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$18 = "vue-doctor/build-quality/tsconfig-strict-required";
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$18,
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$17 = "vue-doctor/build-quality/vue-tsc-in-devDeps";
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$17,
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$16 = "vue-doctor/deps/duplicate-vue-versions";
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$16,
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$15 = "vue-doctor/deps/vue-major-current";
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$15,
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$14 = "nuxt-doctor/nitro/compatibilityDate-set";
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$14,
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$13 = "nuxt-doctor/seo/lang-on-html";
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$13,
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$12 = "nuxt-doctor/cloudflare/nitro-cloudflare-preset";
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$12,
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$11 = "nuxt-doctor/modules-deps/no-modules-incompatible-with-nuxt-4";
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$11,
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$10 = "nuxt-doctor/cloudflare/no-node-only-modules";
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$10,
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$9 = "nuxt-doctor/structure/nuxt-major-current";
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$9,
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$8 = "nuxt-doctor/cloudflare/og-image-via-satori";
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$8,
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$7 = "nuxt-doctor/nitro/preset-defined-for-deploy-target";
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$7,
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$6 = "nuxt-doctor/modules-deps/recommended-modules-installed";
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$6,
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$5 = "nuxt-doctor/nitro/runtime-config-typed";
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$5,
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$4 = "nuxt-doctor/structure/uses-app-directory";
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$4,
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(-STDERR_TAIL_LIMIT);
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$3 = "vue-doctor/sfc/no-mixed-options-and-composition-api";
1796
- const MESSAGE$3 = "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";
1797
- const RECOMMENDATION$3 = "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.";
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$9(ctx) {
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$3,
1995
+ ruleId: RULE_ID$5,
1885
1996
  severity: "error",
1886
- message: MESSAGE$3,
1997
+ message: MESSAGE$5,
1887
1998
  source: "sfc",
1888
- recommendation: RECOMMENDATION$3
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$2 = "nuxt-doctor/ai-slop/no-mixed-app-and-root-layout";
1945
- const MESSAGE$2 = "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.";
1946
- const RECOMMENDATION$2 = "Replace <NuxtLayout> in this layout file with <slot />. The layout slot is already provided by the parent layout wrapper.";
1947
- function check$8(ctx) {
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$2,
2078
+ ruleId: RULE_ID$4,
1968
2079
  severity: "warn",
1969
- message: MESSAGE$2,
2080
+ message: MESSAGE$4,
1970
2081
  source: "sfc",
1971
- recommendation: RECOMMENDATION$2
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$1 = "nuxt-doctor/seo/og-image-defined";
1977
- const MESSAGE$1 = "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.";
1978
- const RECOMMENDATION$1 = "Add ogImage: \"/path/to/image.png\" to useSeoMeta / useHead, or install @nuxtjs/og-image.";
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$7(ctx) {
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$1,
2138
+ ruleId: RULE_ID$3,
2028
2139
  severity: "warn",
2029
- message: MESSAGE$1,
2140
+ message: MESSAGE$3,
2030
2141
  source: "sfc",
2031
- recommendation: RECOMMENDATION$1
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$6(ctx) {
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$9
2195
+ check: check$19
2085
2196
  },
2086
2197
  {
2087
2198
  id: "nuxt-doctor/seo/useSeoMeta-on-public-page",
2088
- check: check$6
2199
+ check: check$16
2089
2200
  },
2090
2201
  {
2091
2202
  id: "nuxt-doctor/seo/og-image-defined",
2092
- check: check$7
2203
+ check: check$17
2093
2204
  },
2094
2205
  {
2095
2206
  id: "nuxt-doctor/ai-slop/no-mixed-app-and-root-layout",
2096
- check: check$8
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$3 = 7;
2438
+ const NODE_DIRECTIVE$4 = 7;
2328
2439
  function findDirective(el, name) {
2329
- for (const prop of el.props) if (prop.type === NODE_DIRECTIVE$3 && prop.name === name) return prop;
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$3) continue;
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$5(ctx) {
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$4(ctx) {
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$3(ctx) {
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$2 = 7;
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$2 && p.name === "for");
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$2) continue;
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$2(ctx) {
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$1 = 7;
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$1 && p.name === "for");
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$1) continue;
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$1(ctx) {
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$5
3063
+ check: check$15
2581
3064
  },
2582
3065
  {
2583
3066
  id: "vue-doctor/template/v-if-v-for-precedence",
2584
- check: check$4
3067
+ check: check$14
2585
3068
  },
2586
3069
  {
2587
3070
  id: "vue-doctor/template/v-memo-on-large-list",
2588
- check: check$3
3071
+ check: check$13
2589
3072
  },
2590
3073
  {
2591
3074
  id: "vue-doctor/template/no-inline-object-prop-in-list",
2592
- check: check$2
3075
+ check: check$12
2593
3076
  },
2594
3077
  {
2595
3078
  id: "vue-doctor/template/no-computed-getter-in-template-loop",
2596
- check: check$1
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 import("./load-yXDi-5ti.js").then((n) => n.n);
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