@geoql/doctor-core 1.2.0 → 1.3.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
@@ -1545,9 +1545,14 @@ const VUE_OXLINT_RULE_IDS = new Set([
1545
1545
  "vue-doctor/reactivity/watch-without-cleanup",
1546
1546
  "vue-doctor/reactivity/prefer-shallowRef-for-large-data",
1547
1547
  "vue-doctor/reactivity/prefer-readonly-for-injected",
1548
+ "vue-doctor/reactivity/no-fresh-deps-in-watch",
1548
1549
  "vue-doctor/composition/prefer-script-setup-for-new-files",
1549
1550
  "vue-doctor/composition/defineProps-typed",
1551
+ "vue-doctor/composition/no-pinia-store-in-setup",
1550
1552
  "vue-doctor/performance/prefer-defineAsyncComponent-on-route",
1553
+ "vue-doctor/performance/prefer-module-scope-static-value",
1554
+ "vue-doctor/performance/prefer-module-scope-pure-function",
1555
+ "vue-doctor/performance/prefer-stable-empty-fallback",
1551
1556
  "vue-doctor/security/no-inner-html",
1552
1557
  "vue-doctor/security/no-eval-like",
1553
1558
  "vue-doctor/security/no-auth-token-in-web-storage",
@@ -1832,2226 +1837,2459 @@ async function runScriptPass(opts) {
1832
1837
  }
1833
1838
  }
1834
1839
  //#endregion
1835
- //#region src/score.ts
1836
- const SEVERITY_WEIGHTS = {
1837
- error: 5,
1838
- warn: 2,
1839
- info: .5
1840
- };
1841
- function scoreDiagnostics(diagnostics, config) {
1842
- const threshold = config?.threshold ?? 0;
1843
- let errorCount = 0;
1844
- let warnCount = 0;
1845
- let infoCount = 0;
1846
- const byRule = /* @__PURE__ */ new Map();
1847
- for (const d of diagnostics) {
1848
- if (d.severity === "error") errorCount += 1;
1849
- else if (d.severity === "warn") warnCount += 1;
1850
- else infoCount += 1;
1851
- const list = byRule.get(d.ruleId);
1852
- if (list) list.push(d);
1853
- else byRule.set(d.ruleId, [d]);
1854
- }
1855
- const sortedRuleIds = [...byRule.keys()].sort();
1856
- const breakdown = [];
1857
- let penalty = 0;
1858
- for (const ruleId of sortedRuleIds) {
1859
- const list = byRule.get(ruleId);
1860
- const weight = config?.rules?.[ruleId]?.weight ?? SEVERITY_WEIGHTS[list[0].severity];
1861
- let rulePenalty = 0;
1862
- for (let i = 0; i < list.length; i++) rulePenalty += weight * (i === 0 ? 1 : 1 / Math.sqrt(i + 1));
1863
- penalty += rulePenalty;
1864
- breakdown.push({
1865
- ruleId,
1866
- occurrences: list.length,
1867
- weightPerOccurrence: weight,
1868
- penalty: rulePenalty
1869
- });
1870
- }
1871
- breakdown.sort((a, b) => b.penalty - a.penalty);
1872
- const score = Math.max(0, Math.round(100 - penalty));
1873
- return {
1874
- score,
1875
- passed: score >= threshold,
1876
- threshold,
1877
- totalFindings: diagnostics.length,
1878
- errorCount,
1879
- warnCount,
1880
- infoCount,
1881
- breakdown
1882
- };
1883
- }
1884
- //#endregion
1885
- //#region src/sfc/parse-sfc-descriptor.ts
1886
- const cache$1 = /* @__PURE__ */ new Map();
1887
- async function parseSfcDescriptor(absPath) {
1888
- if (cache$1.has(absPath)) return cache$1.get(absPath) ?? null;
1889
- let source;
1890
- try {
1891
- source = await readFile(absPath, "utf8");
1892
- } catch {
1893
- cache$1.set(absPath, null);
1894
- return null;
1895
- }
1896
- const { descriptor, errors } = parse(source, { filename: absPath });
1897
- if (errors.length > 0) {
1898
- cache$1.set(absPath, null);
1899
- return null;
1900
- }
1901
- cache$1.set(absPath, descriptor);
1902
- return descriptor;
1903
- }
1904
- //#endregion
1905
- //#region src/sfc/rules/no-mixed-options-and-composition-api.ts
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.";
1909
- const DISALLOWED = new Set([
1910
- "data",
1911
- "methods",
1912
- "computed",
1913
- "watch",
1914
- "props",
1915
- "emits",
1916
- "provide",
1917
- "inject",
1918
- "beforeCreate",
1919
- "created",
1920
- "beforeMount",
1921
- "mounted",
1922
- "beforeUpdate",
1923
- "updated",
1924
- "beforeUnmount",
1925
- "unmounted",
1926
- "activated",
1927
- "deactivated",
1928
- "errorCaptured",
1929
- "serverPrefetch"
1930
- ]);
1931
- function resolveDefineComponentCall(call) {
1932
- if (call.callee.type !== "Identifier") return null;
1933
- if (call.callee.name !== "defineComponent") return null;
1934
- const first = call.arguments[0];
1935
- if (!first || first.type !== "ObjectExpression") return null;
1936
- return first;
1937
- }
1938
- function findBindingInit(body, name) {
1939
- for (const stmt of body) {
1940
- if (stmt.type !== "VariableDeclaration") continue;
1941
- for (const declarator of stmt.declarations) {
1942
- if (declarator.id.type !== "Identifier") continue;
1943
- if (declarator.id.name !== name) continue;
1944
- return declarator.init;
1945
- }
1946
- }
1947
- return null;
1948
- }
1949
- function resolveOptionsObject(program) {
1950
- const exported = program.body.find((node) => node.type === "ExportDefaultDeclaration");
1951
- if (!exported) return null;
1952
- const declaration = exported.declaration;
1953
- if (declaration.type === "ObjectExpression") return declaration;
1954
- if (declaration.type === "CallExpression") return resolveDefineComponentCall(declaration);
1955
- if (declaration.type === "Identifier") {
1956
- const init = findBindingInit(program.body, declaration.name);
1957
- if (init && init.type === "CallExpression") return resolveDefineComponentCall(init);
1958
- return null;
1959
- }
1960
- return null;
1961
- }
1962
- function locate(content, offset, startLine, startColumn) {
1963
- let line = startLine;
1964
- let lastNewline = -1;
1965
- for (let i = 0; i < offset; i += 1) if (content.charCodeAt(i) === 10) {
1966
- line += 1;
1967
- lastNewline = i;
1968
- }
1969
- const column = lastNewline === -1 ? startColumn + offset : offset - lastNewline;
1970
- return {
1971
- line,
1972
- column
1973
- };
1974
- }
1975
- function check$19(ctx) {
1976
- const { script, scriptSetup } = ctx.descriptor;
1977
- if (!script || !scriptSetup) return { diagnostics: [] };
1978
- const lang = script.lang === "ts" ? "ts" : "js";
1979
- const { program } = parseSync(`script.${lang}`, script.content, {
1980
- sourceType: "module",
1981
- lang
1982
- });
1983
- const options = resolveOptionsObject(program);
1984
- if (!options) return { diagnostics: [] };
1985
- const diagnostics = [];
1986
- for (const property of options.properties) {
1987
- if (property.type !== "Property") continue;
1988
- if (property.key.type !== "Identifier") continue;
1989
- if (!DISALLOWED.has(property.key.name)) continue;
1990
- const { line, column } = locate(script.content, property.start, script.loc.start.line, script.loc.start.column);
1991
- diagnostics.push({
1992
- file: ctx.file,
1993
- line,
1994
- column,
1995
- ruleId: RULE_ID$5,
1996
- severity: "error",
1997
- message: MESSAGE$5,
1998
- source: "sfc",
1999
- recommendation: RECOMMENDATION$5
2000
- });
2001
- }
2002
- return { diagnostics };
2003
- }
2004
- //#endregion
2005
- //#region src/nuxt/file-role.ts
2006
- const PAGE_DIR = /^(?:app\/)?pages\//;
2007
- const LAYOUT_DIR = /^(?:app\/)?layouts\//;
2008
- function normalize(relPath) {
2009
- return relPath.replace(/\\/g, "/").replace(/^\.\//, "");
2010
- }
2011
- /**
2012
- * True when the path points at a Nuxt page component — a `.vue` file under
2013
- * `pages/` (Nuxt 3 layout) or `app/pages/` (Nuxt 4 layout) at the project root.
2014
- */
2015
- function isNuxtPageFile(relPath) {
2016
- const path = normalize(relPath);
2017
- return PAGE_DIR.test(path) && path.endsWith(".vue");
2018
- }
2019
- /**
2020
- * True when the path points at a Nitro server file — a `.ts` file under the
2021
- * project-root `server/` directory.
2022
- */
2023
- function isNuxtServerFile(relPath) {
2024
- const path = normalize(relPath);
2025
- return path.startsWith("server/") && path.endsWith(".ts");
2026
- }
2027
- /**
2028
- * True when the path points at a Nuxt layout component — a `.vue` file under
2029
- * `layouts/` (Nuxt 3 layout) or `app/layouts/` (Nuxt 4 layout) at the project
2030
- * root.
2031
- */
2032
- function isNuxtLayoutFile(relPath) {
2033
- const path = normalize(relPath);
2034
- return LAYOUT_DIR.test(path) && path.endsWith(".vue");
2035
- }
2036
- //#endregion
2037
- //#region src/template/walk.ts
2038
- const NODE_ELEMENT$2 = 1;
2039
- function isElementNode(node) {
2040
- return node !== null && node !== void 0 && node.type === NODE_ELEMENT$2;
2041
- }
2042
- function walkElements(root, visit) {
2043
- const stack = [...root.children];
2044
- while (stack.length > 0) {
2045
- const node = stack.pop();
2046
- if (!node) continue;
2047
- if (isElementNode(node)) {
2048
- visit(node);
2049
- if (node.children) for (const child of node.children) stack.push(child);
2050
- }
2051
- }
2052
- }
2053
- //#endregion
2054
- //#region src/sfc/rules/nuxt/no-mixed-app-and-root-layout.ts
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) {
2059
- if (!isNuxtLayoutFile(ctx.relativePath)) return { diagnostics: [] };
2060
- const { template } = ctx.descriptor;
2061
- if (!template || !template.ast) return { diagnostics: [] };
2062
- let foundNuxtLayout = false;
2063
- let nuxtLayoutNode;
2064
- walkElements(template.ast, (el) => {
2065
- if (el.tag === "NuxtLayout") {
2066
- foundNuxtLayout = true;
2067
- nuxtLayoutNode = el;
2068
- }
2069
- });
2070
- if (!foundNuxtLayout) return { diagnostics: [] };
2071
- const node = nuxtLayoutNode;
2072
- return { diagnostics: [{
2073
- file: ctx.file,
2074
- line: node.loc.start.line,
2075
- column: node.loc.start.column,
2076
- endLine: node.loc.end.line,
2077
- endColumn: node.loc.end.column,
2078
- ruleId: RULE_ID$4,
1840
+ //#region src/rule-registry.ts
1841
+ const RULE_REGISTRY = [
1842
+ {
1843
+ id: "vue/no-export-in-script-setup",
1844
+ severity: "error",
1845
+ category: "vue-builtin",
1846
+ source: "oxlint-builtin",
1847
+ recommended: true
1848
+ },
1849
+ {
1850
+ id: "vue/require-typed-ref",
2079
1851
  severity: "warn",
2080
- message: MESSAGE$4,
2081
- source: "sfc",
2082
- recommendation: RECOMMENDATION$4
2083
- }] };
2084
- }
2085
- //#endregion
2086
- //#region src/sfc/rules/nuxt/og-image-defined.ts
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.";
2090
- function hasOgImageInCall(program) {
2091
- for (const stmt of program.body) {
2092
- if (stmt.type !== "ExpressionStatement") continue;
2093
- const call = stmt.expression;
2094
- if (call.type !== "CallExpression") continue;
2095
- if (call.callee.type !== "Identifier") continue;
2096
- const name = call.callee.name;
2097
- if (name !== "useSeoMeta" && name !== "useHead") continue;
2098
- const firstArg = call.arguments[0];
2099
- if (!firstArg || firstArg.type !== "ObjectExpression") continue;
2100
- for (const prop of firstArg.properties) {
2101
- if (prop.type !== "Property") continue;
2102
- if (prop.key.type === "Identifier" && prop.key.name === "ogImage") return true;
2103
- if (prop.key.type === "Literal" && prop.key.value === "og:image") return true;
2104
- }
2105
- }
2106
- return false;
2107
- }
2108
- function hasOgImageDep(packageJsonPath) {
2109
- try {
2110
- const raw = readFileSync(packageJsonPath, "utf8");
2111
- const pkg = JSON.parse(raw);
2112
- const deps = {
2113
- ...pkg.dependencies,
2114
- ...pkg.devDependencies
2115
- };
2116
- return "@nuxtjs/og-image" in deps || "nuxt-og-image" in deps;
2117
- } catch {
2118
- return false;
2119
- }
2120
- }
2121
- function check$17(ctx) {
2122
- if (!isNuxtPageFile(ctx.relativePath)) return { diagnostics: [] };
2123
- const { scriptSetup } = ctx.descriptor;
2124
- if (!scriptSetup) return { diagnostics: [] };
2125
- const lang = scriptSetup.lang === "ts" ? "ts" : "js";
2126
- const { program } = parseSync(`script.${lang}`, scriptSetup.content, {
2127
- sourceType: "module",
2128
- lang
2129
- });
2130
- if (hasOgImageInCall(program)) return { diagnostics: [] };
2131
- if (ctx.projectInfo.packageJsonPath === null) return { diagnostics: [] };
2132
- if (hasOgImageDep(ctx.projectInfo.packageJsonPath)) return { diagnostics: [] };
2133
- const { line, column } = scriptSetup.loc.start;
2134
- return { diagnostics: [{
2135
- file: ctx.file,
2136
- line,
2137
- column,
2138
- ruleId: RULE_ID$3,
1852
+ category: "vue-builtin",
1853
+ source: "oxlint-builtin",
1854
+ recommended: true
1855
+ },
1856
+ {
1857
+ id: "vue/no-arrow-functions-in-watch",
1858
+ severity: "error",
1859
+ category: "vue-builtin",
1860
+ source: "oxlint-builtin",
1861
+ recommended: true
1862
+ },
1863
+ {
1864
+ id: "vue/no-deprecated-data-object-declaration",
1865
+ severity: "error",
1866
+ category: "vue-builtin",
1867
+ source: "oxlint-builtin",
1868
+ recommended: true
1869
+ },
1870
+ {
1871
+ id: "vue/no-deprecated-events-api",
1872
+ severity: "error",
1873
+ category: "vue-builtin",
1874
+ source: "oxlint-builtin",
1875
+ recommended: true
1876
+ },
1877
+ {
1878
+ id: "vue/no-deprecated-destroyed-lifecycle",
1879
+ severity: "error",
1880
+ category: "vue-builtin",
1881
+ source: "oxlint-builtin",
1882
+ recommended: true
1883
+ },
1884
+ {
1885
+ id: "vue/no-deprecated-model-definition",
1886
+ severity: "error",
1887
+ category: "vue-builtin",
1888
+ source: "oxlint-builtin",
1889
+ recommended: true
1890
+ },
1891
+ {
1892
+ id: "vue/no-deprecated-delete-set",
1893
+ severity: "error",
1894
+ category: "vue-builtin",
1895
+ source: "oxlint-builtin",
1896
+ recommended: true
1897
+ },
1898
+ {
1899
+ id: "vue/no-deprecated-vue-config-keycodes",
1900
+ severity: "error",
1901
+ category: "vue-builtin",
1902
+ source: "oxlint-builtin",
1903
+ recommended: true
1904
+ },
1905
+ {
1906
+ id: "vue/no-lifecycle-after-await",
1907
+ severity: "error",
1908
+ category: "vue-builtin",
1909
+ source: "oxlint-builtin",
1910
+ recommended: true
1911
+ },
1912
+ {
1913
+ id: "vue/no-this-in-before-route-enter",
1914
+ severity: "error",
1915
+ category: "vue-builtin",
1916
+ source: "oxlint-builtin",
1917
+ recommended: true
1918
+ },
1919
+ {
1920
+ id: "vue/return-in-computed-property",
1921
+ severity: "error",
1922
+ category: "vue-builtin",
1923
+ source: "oxlint-builtin",
1924
+ recommended: true
1925
+ },
1926
+ {
1927
+ id: "vue/valid-define-emits",
1928
+ severity: "error",
1929
+ category: "vue-builtin",
1930
+ source: "oxlint-builtin",
1931
+ recommended: true
1932
+ },
1933
+ {
1934
+ id: "vue/valid-define-props",
1935
+ severity: "error",
1936
+ category: "vue-builtin",
1937
+ source: "oxlint-builtin",
1938
+ recommended: true
1939
+ },
1940
+ {
1941
+ id: "vue/no-required-prop-with-default",
2139
1942
  severity: "warn",
2140
- message: MESSAGE$3,
2141
- source: "sfc",
2142
- recommendation: RECOMMENDATION$3
2143
- }] };
2144
- }
2145
- //#endregion
2146
- //#region src/sfc/rules/nuxt/use-seo-meta-on-public-page.ts
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.";
2150
- function hasTitleInCall(program) {
2151
- for (const stmt of program.body) {
2152
- if (stmt.type !== "ExpressionStatement") continue;
2153
- const call = stmt.expression;
2154
- if (call.type !== "CallExpression") continue;
2155
- if (call.callee.type !== "Identifier") continue;
2156
- const name = call.callee.name;
2157
- if (name !== "useSeoMeta" && name !== "useHead" && name !== "definePageMeta") continue;
2158
- const firstArg = call.arguments[0];
2159
- if (!firstArg || firstArg.type !== "ObjectExpression") continue;
2160
- for (const prop of firstArg.properties) {
2161
- if (prop.type !== "Property") continue;
2162
- if (prop.key.type === "Identifier" && prop.key.name === "title") return true;
2163
- if (prop.key.type === "Literal" && prop.key.value === "title") return true;
2164
- }
2165
- }
2166
- return false;
2167
- }
2168
- function check$16(ctx) {
2169
- if (!isNuxtPageFile(ctx.relativePath)) return { diagnostics: [] };
2170
- const { scriptSetup } = ctx.descriptor;
2171
- if (!scriptSetup) return { diagnostics: [] };
2172
- const lang = scriptSetup.lang === "ts" ? "ts" : "js";
2173
- const { program } = parseSync(`script.${lang}`, scriptSetup.content, {
2174
- sourceType: "module",
2175
- lang
2176
- });
2177
- if (hasTitleInCall(program)) return { diagnostics: [] };
2178
- const { line, column } = scriptSetup.loc.start;
2179
- return { diagnostics: [{
2180
- file: ctx.file,
2181
- line,
2182
- column,
2183
- ruleId: RULE_ID$2,
1943
+ category: "vue-builtin",
1944
+ source: "oxlint-builtin",
1945
+ recommended: true
1946
+ },
1947
+ {
1948
+ id: "vue/prefer-import-from-vue",
2184
1949
  severity: "warn",
2185
- message: MESSAGE$2,
2186
- source: "sfc",
2187
- recommendation: RECOMMENDATION$2
2188
- }] };
2189
- }
2190
- //#endregion
2191
- //#region src/sfc/rules/index.ts
2192
- const SFC_RULES = [
1950
+ category: "vue-builtin",
1951
+ source: "oxlint-builtin",
1952
+ recommended: true
1953
+ },
2193
1954
  {
2194
- id: "vue-doctor/sfc/no-mixed-options-and-composition-api",
2195
- check: check$19
1955
+ id: "vue/no-import-compiler-macros",
1956
+ severity: "warn",
1957
+ category: "vue-builtin",
1958
+ source: "oxlint-builtin",
1959
+ recommended: true
2196
1960
  },
2197
1961
  {
2198
- id: "nuxt-doctor/seo/useSeoMeta-on-public-page",
2199
- check: check$16
1962
+ id: "vue/no-multiple-slot-args",
1963
+ severity: "warn",
1964
+ category: "vue-builtin",
1965
+ source: "oxlint-builtin",
1966
+ recommended: true
2200
1967
  },
2201
1968
  {
2202
- id: "nuxt-doctor/seo/og-image-defined",
2203
- check: check$17
1969
+ id: "vue/require-default-export",
1970
+ severity: "warn",
1971
+ category: "vue-builtin",
1972
+ source: "oxlint-builtin",
1973
+ recommended: true
2204
1974
  },
2205
1975
  {
2206
- id: "nuxt-doctor/ai-slop/no-mixed-app-and-root-layout",
2207
- check: check$18
2208
- }
2209
- ];
2210
- //#endregion
2211
- //#region src/sfc/run.ts
2212
- async function runSfcPass(opts) {
2213
- const all = [];
2214
- const { projectInfo } = opts;
2215
- const rootDir = projectInfo?.rootDirectory ?? process.cwd();
2216
- for (const file of opts.files) {
2217
- if (!file.endsWith(".vue")) continue;
2218
- const descriptor = await parseSfcDescriptor(file);
2219
- if (!descriptor) continue;
2220
- const relativePath = relative(rootDir, file).replace(/\\/g, "/");
2221
- for (const rule of SFC_RULES) {
2222
- const override = opts.ruleOverrides?.[rule.id];
2223
- if (override === "off") continue;
2224
- const { diagnostics } = rule.check({
2225
- file,
2226
- descriptor,
2227
- rootDirectory: rootDir,
2228
- relativePath,
2229
- projectInfo
2230
- });
2231
- for (const d of diagnostics) all.push(override ? {
2232
- ...d,
2233
- severity: override
2234
- } : d);
2235
- }
2236
- }
2237
- return all;
2238
- }
2239
- //#endregion
2240
- //#region src/nuxt/cross-file/run.ts
2241
- function extractDataFetchingKeys(content, lang) {
2242
- const keys = [];
2243
- try {
2244
- const { program } = parseSync(`script.${lang}`, content, {
2245
- sourceType: "module",
2246
- lang
2247
- });
2248
- for (const stmt of program.body) if (stmt.type === "VariableDeclaration") for (const decl of stmt.declarations) {
2249
- const call = unwrapAwait(decl.init);
2250
- if (call?.type !== "CallExpression") continue;
2251
- extractKeyFromCall(call, keys);
2252
- }
2253
- else if (stmt.type === "ExpressionStatement") {
2254
- const expr = unwrapAwait(stmt.expression);
2255
- if (expr?.type === "CallExpression") extractKeyFromCall(expr, keys);
2256
- }
2257
- } catch {}
2258
- return keys;
2259
- }
2260
- function unwrapAwait(node) {
2261
- if (node?.type === "AwaitExpression") return node.argument;
2262
- return node;
2263
- }
2264
- function extractKeyFromCall(call, keys) {
2265
- if (call.callee.type !== "Identifier") return;
2266
- const fnName = call.callee.name;
2267
- if (fnName !== "useAsyncData" && fnName !== "useFetch") return;
2268
- const keyArg = call.arguments[0];
2269
- if (keyArg?.type === "Literal" && typeof keyArg.value === "string") keys.push(keyArg.value);
2270
- else if (keyArg?.type === "TemplateLiteral" && keyArg.quasis.length === 1) {
2271
- const val = keyArg.quasis[0]?.value?.raw;
2272
- if (val) keys.push(val);
2273
- }
2274
- if (fnName === "useFetch" && call.arguments.length >= 2) {
2275
- const optsArg = call.arguments[1];
2276
- if (optsArg?.type === "ObjectExpression") {
2277
- for (const prop of optsArg.properties) if (prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "key" && prop.value.type === "Literal" && typeof prop.value.value === "string") keys.push(prop.value.value);
2278
- }
2279
- }
2280
- }
2281
- async function runCrossFilePass(opts) {
2282
- const { files, projectInfo } = opts;
2283
- if (projectInfo.framework !== "nuxt") return [];
2284
- const rootDir = projectInfo.rootDirectory;
2285
- const pageFiles = files.filter((f) => isNuxtPageFile(relative(rootDir, f).replace(/\\/g, "/")));
2286
- if (pageFiles.length === 0) return [];
2287
- const parsed = [];
2288
- for (const file of pageFiles) {
2289
- const descriptor = await parseSfcDescriptor(file);
2290
- if (!descriptor) continue;
2291
- const { scriptSetup } = descriptor;
2292
- if (!scriptSetup) continue;
2293
- parsed.push({
2294
- file,
2295
- relativePath: relative(rootDir, file).replace(/\\/g, "/"),
2296
- keys: extractDataFetchingKeys(scriptSetup.content, scriptSetup.lang === "ts" ? "ts" : "js"),
2297
- scriptSetupContent: scriptSetup.content,
2298
- scriptSetupLang: scriptSetup.lang === "ts" ? "ts" : "js",
2299
- scriptSetupLine: scriptSetup.loc.start.line
2300
- });
2301
- }
2302
- return runCrossFileRules(parsed);
2303
- }
2304
- function runCrossFileRules(pages) {
2305
- const all = [];
2306
- all.push(...ruleNoSharedKeyAcrossPages(pages));
2307
- all.push(...ruleSsrSafeOnMountedOnlyForClient(pages));
2308
- return all;
2309
- }
2310
- function ruleNoSharedKeyAcrossPages(pages) {
2311
- const keyToFiles = /* @__PURE__ */ new Map();
2312
- for (const page of pages) for (const key of page.keys) {
2313
- const existing = keyToFiles.get(key) ?? [];
2314
- if (!existing.includes(page.file)) existing.push(page.file);
2315
- keyToFiles.set(key, existing);
2316
- }
2317
- const diags = [];
2318
- for (const [key, files] of keyToFiles) {
2319
- if (files.length < 2) continue;
2320
- for (const file of files) diags.push({
2321
- file,
2322
- line: 1,
2323
- column: 1,
2324
- ruleId: "nuxt-doctor/data-fetching/no-shared-key-across-pages",
2325
- severity: "warn",
2326
- message: `Data fetching key "${key}" is shared across ${files.length} different page files. This causes cache collisions in useAsyncData/useFetch. Use a unique key per page.`,
2327
- source: "cross-file",
2328
- recommendation: `Rename the key to something page-specific, e.g. "page-${key}" or "users-${key}".`
2329
- });
2330
- }
2331
- return diags;
2332
- }
2333
- const BROWSER_GLOBALS = new Set([
2334
- "window",
2335
- "document",
2336
- "navigator",
2337
- "localStorage",
2338
- "sessionStorage",
2339
- "location",
2340
- "history",
2341
- "fetch",
2342
- "XMLHttpRequest",
2343
- "matchMedia",
2344
- "IntersectionObserver",
2345
- "MutationObserver",
2346
- "indexedDB",
2347
- "webkit",
2348
- "moz",
2349
- "onmessage"
2350
- ]);
2351
- function ruleSsrSafeOnMountedOnlyForClient(pages) {
2352
- const diags = [];
2353
- for (const page of pages) {
2354
- const { program } = parseSync(`script.${page.scriptSetupLang}`, page.scriptSetupContent, {
2355
- sourceType: "module",
2356
- lang: page.scriptSetupLang
2357
- });
2358
- for (const stmt of program.body) if (stmt.type === "VariableDeclaration") for (const decl of stmt.declarations) {
2359
- const init = decl.init;
2360
- if (!init) continue;
2361
- if (init.type === "CallExpression") {
2362
- const call = init;
2363
- if (isSkippedReactiveCall(call)) continue;
2364
- checkCallForBrowserGlobal(call, page, diags);
2365
- } else if (init.type === "MemberExpression") checkMemberForBrowserGlobal(init, page, diags);
2366
- }
2367
- else if (stmt.type === "ExpressionStatement") {
2368
- const expr = stmt.expression;
2369
- if (expr.type === "CallExpression") {
2370
- if (isSkippedReactiveCall(expr)) continue;
2371
- checkCallForBrowserGlobal(expr, page, diags);
2372
- } else if (expr.type === "MemberExpression") checkMemberForBrowserGlobal(expr, page, diags);
2373
- }
2374
- }
2375
- return diags;
2376
- }
2377
- const CLIENT_SAFE_WRAPPERS = new Set([
2378
- "onMounted",
2379
- "watchEffect",
2380
- "watch"
2381
- ]);
2382
- function isSkippedReactiveCall(call) {
2383
- if (call.callee.type !== "Identifier") return false;
2384
- return CLIENT_SAFE_WRAPPERS.has(call.callee.name);
2385
- }
2386
- function checkCallForBrowserGlobal(call, page, diags) {
2387
- const callee = call.callee;
2388
- if (callee.type !== "MemberExpression") return;
2389
- const obj = callee.object;
2390
- if (obj.type !== "Identifier" || !BROWSER_GLOBALS.has(obj.name)) return;
2391
- diags.push({
2392
- file: page.file,
2393
- line: page.scriptSetupLine,
2394
- column: 1,
2395
- ruleId: "nuxt-doctor/data-fetching/ssr-safe-onMounted-only-for-client",
1976
+ id: "vue/define-emits-declaration",
1977
+ severity: "info",
1978
+ category: "vue-builtin",
1979
+ source: "oxlint-builtin",
1980
+ recommended: false
1981
+ },
1982
+ {
1983
+ id: "vue/define-props-declaration",
1984
+ severity: "info",
1985
+ category: "vue-builtin",
1986
+ source: "oxlint-builtin",
1987
+ recommended: false
1988
+ },
1989
+ {
1990
+ id: "vue/define-props-destructuring",
1991
+ severity: "info",
1992
+ category: "vue-builtin",
1993
+ source: "oxlint-builtin",
1994
+ recommended: false
1995
+ },
1996
+ {
1997
+ id: "vue/max-props",
1998
+ severity: "info",
1999
+ category: "vue-builtin",
2000
+ source: "oxlint-builtin",
2001
+ recommended: false
2002
+ },
2003
+ {
2004
+ id: "vue-doctor/no-em-dash-in-string",
2396
2005
  severity: "warn",
2397
- message: `Browser global "${obj.name}" is accessed at setup top-level outside onMounted. This causes SSR/hydration mismatches. Move browser-only code inside onMounted or a client-only plugin.`,
2398
- source: "cross-file",
2399
- recommendation: `Wrap "${obj.name}" access in onMounted(() => { /* code */ }) or use defineNuxtPlugin to run only on the client.`
2400
- });
2401
- }
2402
- function checkMemberForBrowserGlobal(mem, page, diags) {
2403
- const obj = mem.object;
2404
- if (obj.type !== "Identifier" || !BROWSER_GLOBALS.has(obj.name)) return;
2405
- diags.push({
2406
- file: page.file,
2407
- line: page.scriptSetupLine,
2408
- column: 1,
2409
- ruleId: "nuxt-doctor/data-fetching/ssr-safe-onMounted-only-for-client",
2006
+ category: "ai-slop",
2007
+ source: "doctor",
2008
+ recommended: true
2009
+ },
2010
+ {
2011
+ id: "vue-doctor/no-destructure-props-without-to-refs",
2012
+ severity: "error",
2013
+ category: "ai-slop",
2014
+ source: "doctor",
2015
+ recommended: true
2016
+ },
2017
+ {
2018
+ id: "vue-doctor/no-destructure-reactive-without-to-refs",
2019
+ severity: "error",
2020
+ category: "ai-slop",
2021
+ source: "doctor",
2022
+ recommended: true
2023
+ },
2024
+ {
2025
+ id: "vue-doctor/no-non-null-assertion-on-ref-value",
2410
2026
  severity: "warn",
2411
- message: `Browser global "${obj.name}" is accessed at setup top-level outside onMounted. This causes SSR/hydration mismatches. Move browser-only code inside onMounted or a client-only plugin.`,
2412
- source: "cross-file",
2413
- recommendation: `Wrap "${obj.name}" access in onMounted(() => { /* code */ }) or use defineNuxtPlugin to run only on the client.`
2414
- });
2415
- }
2416
- //#endregion
2417
- //#region src/template/parse-sfc.ts
2418
- const cache = /* @__PURE__ */ new Map();
2419
- async function parseSfc(absPath) {
2420
- if (cache.has(absPath)) return cache.get(absPath) ?? null;
2421
- let source;
2422
- try {
2423
- source = await readFile(absPath, "utf8");
2424
- } catch {
2425
- cache.set(absPath, null);
2426
- return null;
2427
- }
2428
- const { descriptor, errors } = parse(source, { filename: absPath });
2429
- if (errors.length > 0 || !descriptor.template) {
2430
- cache.set(absPath, null);
2431
- return null;
2432
- }
2433
- cache.set(absPath, descriptor);
2434
- return descriptor;
2435
- }
2436
- //#endregion
2437
- //#region src/template/directive-helpers.ts
2438
- const NODE_DIRECTIVE$4 = 7;
2439
- function findDirective(el, name) {
2440
- for (const prop of el.props) if (prop.type === NODE_DIRECTIVE$4 && prop.name === name) return prop;
2441
- }
2442
- function findBindAttr(el, attrName) {
2443
- for (const prop of el.props) {
2444
- if (prop.type !== NODE_DIRECTIVE$4) continue;
2445
- const dir = prop;
2446
- if (dir.name !== "bind") continue;
2447
- if (dir.arg?.content === attrName) return dir;
2448
- }
2449
- }
2450
- function findStaticAttr(el, attrName) {
2451
- for (const prop of el.props) if (prop.type === 6 && prop.name === attrName) return prop;
2452
- }
2453
- //#endregion
2454
- //#region src/template/rules/v-for-has-key.ts
2455
- function check$15(ctx) {
2456
- const diagnostics = [];
2457
- walkElements(ctx.template, (el) => {
2458
- if (!findDirective(el, "for")) return;
2459
- if (findBindAttr(el, "key") ?? findStaticAttr(el, "key")) return;
2460
- diagnostics.push({
2461
- file: ctx.file,
2462
- line: el.loc.start.line,
2463
- column: el.loc.start.column,
2464
- endLine: el.loc.end.line,
2465
- endColumn: el.loc.end.column,
2466
- ruleId: "vue-doctor/template/v-for-has-key",
2467
- severity: "error",
2468
- message: `<${el.tag}> uses v-for without :key. Vue cannot efficiently diff this list across renders.`,
2469
- source: "template",
2470
- recommendation: "Add :key with a stable, unique identifier from the iterated item."
2471
- });
2472
- });
2473
- return { diagnostics };
2474
- }
2475
- //#endregion
2476
- //#region src/template/rules/v-if-v-for-precedence.ts
2477
- function check$14(ctx) {
2478
- const diagnostics = [];
2479
- walkElements(ctx.template, (el) => {
2480
- const vIf = findDirective(el, "if");
2481
- const vFor = findDirective(el, "for");
2482
- if (!vIf || !vFor) return;
2483
- diagnostics.push({
2484
- file: ctx.file,
2485
- line: el.loc.start.line,
2486
- column: el.loc.start.column,
2487
- endLine: el.loc.end.line,
2488
- endColumn: el.loc.end.column,
2489
- ruleId: "vue-doctor/template/v-if-v-for-precedence",
2490
- severity: "error",
2491
- message: `<${el.tag}> uses v-if and v-for on the same element. In Vue 3, v-if binds tighter than v-for, so the condition cannot reference loop variables.`,
2492
- source: "template",
2493
- recommendation: "Move the v-if to a <template v-for> wrapper or to a parent element, or filter the iterated source in a computed."
2494
- });
2495
- });
2496
- return { diagnostics };
2497
- }
2498
- //#endregion
2499
- //#region src/template/rules/v-memo-on-large-list.ts
2500
- const ARRAY_LITERAL_THRESHOLD = 100;
2501
- function findLargeArrayBindings(script) {
2502
- const bindings = /* @__PURE__ */ new Map();
2503
- try {
2504
- const ast = babelParse(script, { sourceType: "module" });
2505
- for (const node of ast.program.body) {
2506
- if (node.type !== "VariableDeclaration") continue;
2507
- if (node.kind !== "const" && node.kind !== "let") continue;
2508
- for (const decl of node.declarations) {
2509
- if (decl.id.type !== "Identifier") continue;
2510
- const init = decl.init;
2511
- if (!init || init.type !== "ArrayExpression") continue;
2512
- if (!init.elements || init.elements.length <= ARRAY_LITERAL_THRESHOLD) continue;
2513
- bindings.set(decl.id.name, init.elements.length);
2514
- }
2515
- }
2516
- } catch {}
2517
- return bindings;
2518
- }
2519
- function check$13(ctx) {
2520
- const diagnostics = [];
2521
- const largeArrays = ctx.script && ctx.script.length > 0 ? findLargeArrayBindings(ctx.script) : /* @__PURE__ */ new Map();
2522
- walkElements(ctx.template, (el) => {
2523
- const vFor = findDirective(el, "for");
2524
- if (!vFor) return;
2525
- if (findDirective(el, "memo")) return;
2526
- const source = vFor.forParseResult?.source;
2527
- if (!source) return;
2528
- const sourceName = source.content;
2529
- const largeArraySize = largeArrays.get(sourceName);
2530
- if (largeArraySize !== void 0 && largeArraySize > ARRAY_LITERAL_THRESHOLD) diagnostics.push({
2531
- file: ctx.file,
2532
- line: el.loc.start.line,
2533
- column: el.loc.start.column,
2534
- endLine: el.loc.end.line,
2535
- endColumn: el.loc.end.column,
2536
- ruleId: "vue-doctor/template/v-memo-on-large-list",
2537
- severity: "warn",
2538
- message: `<${el.tag}> uses v-for over a large dataset (array literal with ${largeArraySize} items) without v-memo. Vue cannot skip diffing this list on every render.`,
2539
- source: "template",
2540
- recommendation: "Add v-memo with a meaningful memoization key so Vue can skip re-rendering unchanged items."
2541
- });
2542
- });
2543
- return { diagnostics };
2544
- }
2545
- //#endregion
2546
- //#region src/template/rules/no-inline-object-prop-in-list.ts
2547
- const NODE_DIRECTIVE$3 = 7;
2548
- function checkElement$2(el, inVForSubtree, ctx, diagnostics) {
2549
- const isVFor = el.props.some((p) => p.type === NODE_DIRECTIVE$3 && p.name === "for");
2550
- const effectiveInVFor = inVForSubtree || isVFor;
2551
- if (effectiveInVFor) for (const prop of el.props) {
2552
- if (prop.type !== NODE_DIRECTIVE$3) continue;
2553
- const dir = prop;
2554
- if (dir.name !== "bind") continue;
2555
- const attrName = dir.arg?.content;
2556
- if (!attrName || attrName === "key") continue;
2557
- const astType = dir.exp?.ast?.type;
2558
- if (astType !== "ObjectExpression" && astType !== "ArrayExpression") continue;
2559
- diagnostics.push({
2560
- file: ctx.file,
2561
- line: dir.loc.start.line,
2562
- column: dir.loc.start.column,
2563
- endLine: dir.loc.end.line,
2564
- endColumn: dir.loc.end.column,
2565
- ruleId: "vue-doctor/template/no-inline-object-prop-in-list",
2566
- severity: "warn",
2567
- message: `<${el.tag}> has an inline ${attrName} prop with an object or array literal inside a v-for subtree. Inline objects and arrays create new references on every render, defeating v-for's ability to reuse DOM nodes.`,
2568
- source: "template",
2569
- recommendation: "Move the object or array to a computed property or a module-level constant so the reference remains stable across renders."
2570
- });
2571
- }
2572
- for (const child of el.children) if (child.type === 1) checkElement$2(child, effectiveInVFor, ctx, diagnostics);
2573
- }
2574
- function check$12(ctx) {
2575
- const diagnostics = [];
2576
- function walk(node, inVForSubtree) {
2577
- if (node.type === 1) checkElement$2(node, inVForSubtree, ctx, diagnostics);
2578
- else if (node.type === 0) {
2579
- const root = node;
2580
- for (const child of root.children) walk(child, inVForSubtree);
2581
- }
2582
- }
2583
- walk(ctx.template, false);
2584
- return { diagnostics };
2585
- }
2586
- //#endregion
2587
- //#region src/template/rules/no-computed-getter-in-template-loop.ts
2588
- const NODE_ELEMENT$1 = 1;
2589
- const NODE_INTERPOLATION = 5;
2590
- const NODE_DIRECTIVE$2 = 7;
2591
- function isValueDeref(node) {
2592
- if (node.type !== "MemberExpression" || node.computed === true) return false;
2593
- const { object, property } = node;
2594
- return object.type === "Identifier" && property.name === "value";
2595
- }
2596
- function containsValueDeref(value) {
2597
- if (value === null || typeof value !== "object") return false;
2598
- if (Array.isArray(value)) return value.some((item) => containsValueDeref(item));
2599
- const node = value;
2600
- return isValueDeref(node) || Object.values(node).some((child) => containsValueDeref(child));
2601
- }
2602
- function pushDiagnostic(el, loc, ctx, diagnostics) {
2603
- diagnostics.push({
2604
- file: ctx.file,
2605
- line: loc.start.line,
2606
- column: loc.start.column,
2607
- endLine: loc.end.line,
2608
- endColumn: loc.end.column,
2609
- ruleId: "vue-doctor/template/no-computed-getter-in-template-loop",
2027
+ category: "ai-slop",
2028
+ source: "doctor",
2029
+ recommended: true
2030
+ },
2031
+ {
2032
+ id: "vue-doctor/no-imports-from-vue-when-auto-imported",
2610
2033
  severity: "warn",
2611
- message: `<${el.tag}> reads a ref or computed via .value inside a v-for subtree. The getter re-runs on every item and every render, which is easy to hoist out of the loop.`,
2612
- source: "template",
2613
- recommendation: "Read .value once into a computed property or a local binding outside the loop so the getter runs a single time per render."
2614
- });
2615
- }
2616
- function checkElement$1(el, inVForSubtree, ctx, diagnostics) {
2617
- const isVFor = el.props.some((p) => p.type === NODE_DIRECTIVE$2 && p.name === "for");
2618
- const effectiveInVFor = inVForSubtree || isVFor;
2619
- if (effectiveInVFor) {
2620
- for (const prop of el.props) {
2621
- if (prop.type !== NODE_DIRECTIVE$2) continue;
2622
- const dir = prop;
2623
- if (dir.name !== "bind") continue;
2624
- if (containsValueDeref(dir.exp?.ast)) pushDiagnostic(el, dir.loc, ctx, diagnostics);
2625
- }
2626
- for (const child of el.children) if (child.type === NODE_INTERPOLATION) {
2627
- const interp = child;
2628
- if (containsValueDeref(interp.content.ast)) pushDiagnostic(el, interp.loc, ctx, diagnostics);
2629
- }
2630
- }
2631
- for (const child of el.children) if (child.type === NODE_ELEMENT$1) checkElement$1(child, effectiveInVFor, ctx, diagnostics);
2632
- }
2633
- function check$11(ctx) {
2634
- const diagnostics = [];
2635
- function walk(node, inVForSubtree) {
2636
- if (node.type === NODE_ELEMENT$1) checkElement$1(node, inVForSubtree, ctx, diagnostics);
2637
- else if (node.type === 0) {
2638
- const root = node;
2639
- for (const child of root.children) walk(child, inVForSubtree);
2640
- }
2641
- }
2642
- walk(ctx.template, false);
2643
- return { diagnostics };
2644
- }
2645
- //#endregion
2646
- //#region src/template/rules/avoid-deep-v-bind-spread-in-list.ts
2647
- const NODE_ELEMENT = 1;
2648
- const NODE_DIRECTIVE$1 = 7;
2649
- function isIdentifierSpread(dir) {
2650
- return dir.name === "bind" && dir.arg === void 0 && dir.exp?.ast === null;
2651
- }
2652
- function checkElement(el, inVForSubtree, ctx, diagnostics) {
2653
- const isVFor = el.props.some((p) => p.type === NODE_DIRECTIVE$1 && p.name === "for");
2654
- const effectiveInVFor = inVForSubtree || isVFor;
2655
- if (effectiveInVFor) for (const prop of el.props) {
2656
- if (prop.type !== NODE_DIRECTIVE$1) continue;
2657
- const dir = prop;
2658
- if (!isIdentifierSpread(dir)) continue;
2659
- diagnostics.push({
2660
- file: ctx.file,
2661
- line: dir.loc.start.line,
2662
- column: dir.loc.start.column,
2663
- endLine: dir.loc.end.line,
2664
- endColumn: dir.loc.end.column,
2665
- ruleId: "vue-doctor/template/avoid-deep-v-bind-spread-in-list",
2666
- severity: "info",
2667
- message: `<${el.tag}> spreads a whole object via v-bind inside a v-for subtree. Spreading a reactive object binds every property per item, which can churn props and defeat patching on large lists.`,
2668
- source: "template",
2669
- recommendation: "Bind only the props each item needs explicitly, or hoist a stable per-item object so Vue can patch instead of re-binding the full spread."
2670
- });
2671
- }
2672
- for (const child of el.children) if (child.type === NODE_ELEMENT) checkElement(child, effectiveInVFor, ctx, diagnostics);
2673
- }
2674
- function check$10(ctx) {
2675
- const diagnostics = [];
2676
- function walk(node, inVForSubtree) {
2677
- if (node.type === NODE_ELEMENT) checkElement(node, inVForSubtree, ctx, diagnostics);
2678
- else if (node.type === 0) {
2679
- const root = node;
2680
- for (const child of root.children) walk(child, inVForSubtree);
2681
- }
2682
- }
2683
- walk(ctx.template, false);
2684
- return { diagnostics };
2685
- }
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
3059
- //#region src/template/rules/index.ts
3060
- const TEMPLATE_RULES = [
3061
- {
3062
- id: "vue-doctor/template/v-for-has-key",
3063
- check: check$15
3064
- },
3065
- {
3066
- id: "vue-doctor/template/v-if-v-for-precedence",
3067
- check: check$14
3068
- },
3069
- {
3070
- id: "vue-doctor/template/v-memo-on-large-list",
3071
- check: check$13
3072
- },
3073
- {
3074
- id: "vue-doctor/template/no-inline-object-prop-in-list",
3075
- check: check$12
3076
- },
3077
- {
3078
- id: "vue-doctor/template/no-computed-getter-in-template-loop",
3079
- check: check$11
3080
- },
3081
- {
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",
3123
- check
3124
- }
3125
- ];
3126
- //#endregion
3127
- //#region src/template/run.ts
3128
- async function runTemplatePass(opts) {
3129
- const all = [];
3130
- for (const file of opts.files) {
3131
- if (!file.endsWith(".vue")) continue;
3132
- const descriptor = await parseSfc(file);
3133
- if (!descriptor?.template?.ast) continue;
3134
- for (const rule of TEMPLATE_RULES) {
3135
- const override = opts.ruleOverrides?.[rule.id];
3136
- if (override === "off") continue;
3137
- const { diagnostics } = rule.check({
3138
- file,
3139
- template: descriptor.template.ast,
3140
- script: descriptor.scriptSetup?.content ?? descriptor.script?.content
3141
- });
3142
- for (const d of diagnostics) all.push(override ? {
3143
- ...d,
3144
- severity: override
3145
- } : d);
3146
- }
3147
- }
3148
- return all;
3149
- }
3150
- //#endregion
3151
- //#region src/audit.ts
3152
- const DEFAULT_INCLUDE = [
3153
- "**/*.vue",
3154
- "**/*.ts",
3155
- "**/*.tsx",
3156
- "**/*.js",
3157
- "**/*.jsx"
3158
- ];
3159
- const DEFAULT_EXCLUDE = [
3160
- "node_modules",
3161
- "dist",
3162
- ".nuxt",
3163
- ".output",
3164
- "coverage"
3165
- ];
3166
- function countRuleCounts(diagnostics) {
3167
- const counts = {};
3168
- for (const d of diagnostics) counts[d.ruleId] = (counts[d.ruleId] ?? 0) + 1;
3169
- return counts;
3170
- }
3171
- async function audit(config = {}) {
3172
- const rootDir = resolve(config.rootDir ?? process.cwd());
3173
- const include = config.include ?? DEFAULT_INCLUDE;
3174
- const exclude = config.exclude ?? DEFAULT_EXCLUDE;
3175
- const failOn = config.failOn ?? "error";
3176
- const threshold = config.threshold ?? 0;
3177
- const lintEnabled = config.lint !== false;
3178
- const files = await listSourceFiles({
3179
- rootDir,
3180
- include,
3181
- exclude
3182
- });
3183
- const overallStart = performance.now();
3184
- const project = await detectProject(rootDir);
3185
- const templateStart = performance.now();
3186
- const templateDiagnostics = lintEnabled ? await runTemplatePass({
3187
- files,
3188
- ruleOverrides: config.rules
3189
- }) : [];
3190
- const templateElapsed = performance.now() - templateStart;
3191
- const sfcStart = performance.now();
3192
- const sfcDiagnostics = lintEnabled ? await runSfcPass({
3193
- files,
3194
- ruleOverrides: config.rules,
3195
- projectInfo: project
3196
- }) : [];
3197
- const sfcElapsed = performance.now() - sfcStart;
3198
- let scriptDiagnostics = [];
3199
- let oxlintStderr = "";
3200
- const scriptStart = performance.now();
3201
- if (lintEnabled) try {
3202
- const result = await runScriptPass({
3203
- rootDir,
3204
- targetPath: rootDir,
3205
- ruleOverrides: config.rules,
3206
- framework: project.framework === "nuxt" ? "nuxt" : "vue",
3207
- fix: config.fix === true && config.scopeFiles === void 0,
3208
- fixExcludes: config.fixExcludes
3209
- });
3210
- scriptDiagnostics = result.diagnostics;
3211
- oxlintStderr = result.stderr;
3212
- } catch (err) {
3213
- oxlintStderr = err instanceof Error ? err.message : String(err);
3214
- if (process.env.DOCTOR_DEBUG) process.stderr.write(`[doctor-core] script pass failed: ${oxlintStderr}\n`);
3215
- }
3216
- const scriptElapsed = performance.now() - scriptStart;
3217
- let deadCodeDiagnostics = [];
3218
- let deadCodeElapsed = 0;
3219
- if (config.deadCode !== false) {
3220
- const deadCodeStart = performance.now();
3221
- try {
3222
- const { loadDoctorConfig } = await Promise.resolve().then(() => load_exports);
3223
- deadCodeDiagnostics = dedupeDeadCodeAgainstLint(await checkDeadCode({
3224
- projectInfo: project,
3225
- doctorConfig: await loadDoctorConfig(rootDir),
3226
- enabled: true
3227
- }), [
3228
- ...templateDiagnostics,
3229
- ...sfcDiagnostics,
3230
- ...scriptDiagnostics
3231
- ]).filter((d) => config.rules?.[d.ruleId] !== "off");
3232
- } catch (err) {
3233
- if (process.env.DOCTOR_DEBUG) process.stderr.write(`[doctor-core] dead-code pass failed: ${String(err)}\n`);
3234
- }
3235
- deadCodeElapsed = performance.now() - deadCodeStart;
3236
- }
3237
- let buildQualityDiagnostics = [];
3238
- try {
3239
- buildQualityDiagnostics = (await checkBuildQuality(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
3240
- const override = config.rules?.[d.ruleId];
3241
- return override ? {
3242
- ...d,
3243
- severity: override
3244
- } : d;
3245
- });
3246
- } catch {}
3247
- let depsDiagnostics = [];
3248
- try {
3249
- depsDiagnostics = (await checkDeps(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
3250
- const override = config.rules?.[d.ruleId];
3251
- return override ? {
3252
- ...d,
3253
- severity: override
3254
- } : d;
3255
- });
3256
- } catch {}
3257
- let nuxtProjectDiagnostics = [];
3258
- if (project.framework === "nuxt") try {
3259
- nuxtProjectDiagnostics = (await checkNuxtProject(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
3260
- const override = config.rules?.[d.ruleId];
3261
- return override ? {
3262
- ...d,
3263
- severity: override
3264
- } : d;
3265
- });
3266
- } catch {}
3267
- let crossFileDiagnostics = [];
3268
- if (project.framework === "nuxt") try {
3269
- crossFileDiagnostics = (await runCrossFilePass({
3270
- files,
3271
- projectInfo: project
3272
- })).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
3273
- const override = config.rules?.[d.ruleId];
3274
- return override ? {
3275
- ...d,
3276
- severity: override
3277
- } : d;
3278
- });
3279
- } catch {}
3280
- const elapsedMs = performance.now() - overallStart;
3281
- const timings = {
3282
- template: templateElapsed,
3283
- sfc: sfcElapsed,
3284
- script: scriptElapsed,
3285
- deadCode: deadCodeElapsed,
3286
- total: elapsedMs
3287
- };
3288
- let afterDisables = applyInlineDisables(mergeDiagnostics(templateDiagnostics, sfcDiagnostics, scriptDiagnostics, deadCodeDiagnostics, buildQualityDiagnostics, depsDiagnostics, nuxtProjectDiagnostics, crossFileDiagnostics), { respect: config.respectInlineDisables !== false });
3289
- if (config.scopeFiles) {
3290
- const scope = new Set(config.scopeFiles.map((f) => resolve(rootDir, f)));
3291
- afterDisables = afterDisables.filter((d) => scope.has(resolve(rootDir, d.file)));
3292
- }
3293
- const diagnostics = await attachCodeSnippets(afterDisables);
3294
- const scored = scoreDiagnostics(diagnostics, { threshold });
3295
- const ruleCounts = countRuleCounts(diagnostics);
3296
- const projectInfo = {
3297
- framework: project.framework,
3298
- vueVersion: project.vueVersion,
3299
- nuxtVersion: project.nuxtVersion,
3300
- capabilities: [...project.capabilities].sort(),
3301
- rootDirectory: project.rootDirectory
3302
- };
3303
- let exitCode = 0;
3304
- if (oxlintStderr && scriptDiagnostics.length === 0 && oxlintStderr.includes("Failed")) exitCode = 2;
3305
- else if (failOn !== "none") {
3306
- if ((failOn === "warn" ? scored.errorCount + scored.warnCount : scored.errorCount) > 0) exitCode = 1;
3307
- }
3308
- return {
3309
- rootDir,
3310
- filesScanned: files.length,
3311
- diagnostics,
3312
- score: scored.score,
3313
- errorCount: scored.errorCount,
3314
- warnCount: scored.warnCount,
3315
- infoCount: scored.infoCount,
3316
- exitCode,
3317
- scoreResult: scored,
3318
- projectInfo,
3319
- elapsedMs,
3320
- timings,
3321
- ruleCounts
3322
- };
3323
- }
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
3368
- //#region src/config/define-config.ts
3369
- function defineConfig(config) {
3370
- return config;
3371
- }
3372
- //#endregion
3373
- //#region src/rule-registry.ts
3374
- const RULE_REGISTRY = [
2034
+ category: "ai-slop",
2035
+ source: "doctor",
2036
+ recommended: true
2037
+ },
3375
2038
  {
3376
- id: "vue/no-export-in-script-setup",
3377
- severity: "error",
3378
- category: "vue-builtin",
3379
- source: "oxlint-builtin",
2039
+ id: "vue-doctor/reactivity/watch-without-cleanup",
2040
+ severity: "warn",
2041
+ category: "reactivity",
2042
+ source: "doctor",
3380
2043
  recommended: true
3381
2044
  },
3382
2045
  {
3383
- id: "vue/require-typed-ref",
2046
+ id: "vue-doctor/reactivity/prefer-shallowRef-for-large-data",
2047
+ severity: "info",
2048
+ category: "reactivity",
2049
+ source: "doctor",
2050
+ recommended: false
2051
+ },
2052
+ {
2053
+ id: "vue-doctor/reactivity/prefer-readonly-for-injected",
2054
+ severity: "info",
2055
+ category: "reactivity",
2056
+ source: "doctor",
2057
+ recommended: false
2058
+ },
2059
+ {
2060
+ id: "vue-doctor/reactivity/no-fresh-deps-in-watch",
3384
2061
  severity: "warn",
3385
- category: "vue-builtin",
3386
- source: "oxlint-builtin",
2062
+ category: "reactivity",
2063
+ source: "doctor",
3387
2064
  recommended: true
3388
2065
  },
3389
2066
  {
3390
- id: "vue/no-arrow-functions-in-watch",
3391
- severity: "error",
3392
- category: "vue-builtin",
3393
- source: "oxlint-builtin",
2067
+ id: "vue-doctor/composition/prefer-script-setup-for-new-files",
2068
+ severity: "warn",
2069
+ category: "composition",
2070
+ source: "doctor",
3394
2071
  recommended: true
3395
2072
  },
3396
2073
  {
3397
- id: "vue/no-deprecated-data-object-declaration",
3398
- severity: "error",
3399
- category: "vue-builtin",
3400
- source: "oxlint-builtin",
2074
+ id: "vue-doctor/composition/defineProps-typed",
2075
+ severity: "warn",
2076
+ category: "composition",
2077
+ source: "doctor",
3401
2078
  recommended: true
3402
2079
  },
3403
2080
  {
3404
- id: "vue/no-deprecated-events-api",
3405
- severity: "error",
3406
- category: "vue-builtin",
3407
- source: "oxlint-builtin",
2081
+ id: "vue-doctor/composition/no-pinia-store-in-setup",
2082
+ severity: "warn",
2083
+ category: "composition",
2084
+ source: "doctor",
3408
2085
  recommended: true
3409
2086
  },
3410
2087
  {
3411
- id: "vue/no-deprecated-destroyed-lifecycle",
3412
- severity: "error",
3413
- category: "vue-builtin",
3414
- source: "oxlint-builtin",
2088
+ id: "vue-doctor/performance/prefer-defineAsyncComponent-on-route",
2089
+ severity: "info",
2090
+ category: "performance",
2091
+ source: "doctor",
2092
+ recommended: false
2093
+ },
2094
+ {
2095
+ id: "vue-doctor/performance/prefer-module-scope-static-value",
2096
+ severity: "info",
2097
+ category: "performance",
2098
+ source: "doctor",
2099
+ recommended: false
2100
+ },
2101
+ {
2102
+ id: "vue-doctor/performance/prefer-module-scope-pure-function",
2103
+ severity: "info",
2104
+ category: "performance",
2105
+ source: "doctor",
2106
+ recommended: false
2107
+ },
2108
+ {
2109
+ id: "vue-doctor/performance/prefer-stable-empty-fallback",
2110
+ severity: "warn",
2111
+ category: "performance",
2112
+ source: "doctor",
3415
2113
  recommended: true
3416
2114
  },
3417
2115
  {
3418
- id: "vue/no-deprecated-model-definition",
2116
+ id: "vue-doctor/template/v-for-has-key",
3419
2117
  severity: "error",
3420
- category: "vue-builtin",
3421
- source: "oxlint-builtin",
2118
+ category: "template",
2119
+ source: "doctor",
3422
2120
  recommended: true
3423
2121
  },
3424
2122
  {
3425
- id: "vue/no-deprecated-delete-set",
2123
+ id: "vue-doctor/template/v-if-v-for-precedence",
3426
2124
  severity: "error",
3427
- category: "vue-builtin",
3428
- source: "oxlint-builtin",
2125
+ category: "template",
2126
+ source: "doctor",
3429
2127
  recommended: true
3430
2128
  },
3431
2129
  {
3432
- id: "vue/no-deprecated-vue-config-keycodes",
3433
- severity: "error",
3434
- category: "vue-builtin",
3435
- source: "oxlint-builtin",
2130
+ id: "vue-doctor/template/v-memo-on-large-list",
2131
+ severity: "warn",
2132
+ category: "performance",
2133
+ source: "doctor",
3436
2134
  recommended: true
3437
2135
  },
3438
2136
  {
3439
- id: "vue/no-lifecycle-after-await",
3440
- severity: "error",
3441
- category: "vue-builtin",
3442
- source: "oxlint-builtin",
2137
+ id: "vue-doctor/template/no-inline-object-prop-in-list",
2138
+ severity: "warn",
2139
+ category: "performance",
2140
+ source: "doctor",
3443
2141
  recommended: true
3444
2142
  },
3445
2143
  {
3446
- id: "vue/no-this-in-before-route-enter",
3447
- severity: "error",
3448
- category: "vue-builtin",
3449
- source: "oxlint-builtin",
2144
+ id: "vue-doctor/template/no-computed-getter-in-template-loop",
2145
+ severity: "warn",
2146
+ category: "template-perf",
2147
+ source: "doctor",
3450
2148
  recommended: true
3451
2149
  },
3452
2150
  {
3453
- id: "vue/return-in-computed-property",
3454
- severity: "error",
3455
- category: "vue-builtin",
3456
- source: "oxlint-builtin",
2151
+ id: "vue-doctor/template/avoid-deep-v-bind-spread-in-list",
2152
+ severity: "info",
2153
+ category: "template-perf",
2154
+ source: "doctor",
3457
2155
  recommended: true
3458
2156
  },
3459
2157
  {
3460
- id: "vue/valid-define-emits",
3461
- severity: "error",
3462
- category: "vue-builtin",
3463
- source: "oxlint-builtin",
2158
+ id: "vue-doctor/template/no-random-key",
2159
+ severity: "warn",
2160
+ category: "performance",
2161
+ source: "doctor",
3464
2162
  recommended: true
3465
2163
  },
3466
2164
  {
3467
- id: "vue/valid-define-props",
2165
+ id: "vue-doctor/template/no-v-memo-in-vapor",
2166
+ severity: "warn",
2167
+ category: "template-perf",
2168
+ source: "doctor",
2169
+ recommended: true
2170
+ },
2171
+ {
2172
+ id: "vue-doctor/sfc/no-mixed-options-and-composition-api",
2173
+ severity: "warn",
2174
+ category: "sfc",
2175
+ source: "doctor",
2176
+ recommended: true
2177
+ },
2178
+ {
2179
+ id: "vue-doctor/build-quality/tsconfig-strict-required",
2180
+ severity: "warn",
2181
+ category: "build-quality",
2182
+ source: "doctor",
2183
+ recommended: true
2184
+ },
2185
+ {
2186
+ id: "vue-doctor/build-quality/vue-tsc-in-devDeps",
2187
+ severity: "warn",
2188
+ category: "build-quality",
2189
+ source: "doctor",
2190
+ recommended: true
2191
+ },
2192
+ {
2193
+ id: "vue-doctor/build-quality/no-vue-cli",
2194
+ severity: "warn",
2195
+ category: "build-quality",
2196
+ source: "doctor",
2197
+ recommended: true
2198
+ },
2199
+ {
2200
+ id: "vue-doctor/build-quality/eslint-plugin-vue-installed",
2201
+ severity: "info",
2202
+ category: "build-quality",
2203
+ source: "doctor",
2204
+ recommended: true
2205
+ },
2206
+ {
2207
+ id: "vue-doctor/deps/duplicate-vue-versions",
3468
2208
  severity: "error",
3469
- category: "vue-builtin",
3470
- source: "oxlint-builtin",
2209
+ category: "deps",
2210
+ source: "doctor",
3471
2211
  recommended: true
3472
2212
  },
3473
2213
  {
3474
- id: "vue/no-required-prop-with-default",
2214
+ id: "vue-doctor/deps/vue-major-current",
2215
+ severity: "info",
2216
+ category: "deps",
2217
+ source: "doctor",
2218
+ recommended: false
2219
+ },
2220
+ {
2221
+ id: "dead-code/unused-file",
3475
2222
  severity: "warn",
3476
- category: "vue-builtin",
3477
- source: "oxlint-builtin",
2223
+ category: "dead-code",
2224
+ source: "doctor",
2225
+ recommended: true
2226
+ },
2227
+ {
2228
+ id: "dead-code/unused-export",
2229
+ severity: "warn",
2230
+ category: "dead-code",
2231
+ source: "doctor",
2232
+ recommended: true
2233
+ },
2234
+ {
2235
+ id: "dead-code/unused-type-export",
2236
+ severity: "info",
2237
+ category: "dead-code",
2238
+ source: "doctor",
2239
+ recommended: true
2240
+ },
2241
+ {
2242
+ id: "dead-code/unused-member",
2243
+ severity: "info",
2244
+ category: "dead-code",
2245
+ source: "doctor",
3478
2246
  recommended: true
3479
2247
  },
3480
2248
  {
3481
- id: "vue/prefer-import-from-vue",
2249
+ id: "dead-code/unused-dependency",
3482
2250
  severity: "warn",
3483
- category: "vue-builtin",
3484
- source: "oxlint-builtin",
2251
+ category: "dead-code",
2252
+ source: "doctor",
3485
2253
  recommended: true
3486
2254
  },
3487
2255
  {
3488
- id: "vue/no-import-compiler-macros",
3489
- severity: "warn",
3490
- category: "vue-builtin",
3491
- source: "oxlint-builtin",
2256
+ id: "dead-code/unlisted-dependency",
2257
+ severity: "error",
2258
+ category: "dead-code",
2259
+ source: "doctor",
3492
2260
  recommended: true
3493
2261
  },
3494
2262
  {
3495
- id: "vue/no-multiple-slot-args",
2263
+ id: "dead-code/duplicate-export",
3496
2264
  severity: "warn",
3497
- category: "vue-builtin",
3498
- source: "oxlint-builtin",
2265
+ category: "dead-code",
2266
+ source: "doctor",
3499
2267
  recommended: true
3500
2268
  },
3501
2269
  {
3502
- id: "vue/require-default-export",
2270
+ id: "nuxt-doctor/structure/uses-app-directory",
3503
2271
  severity: "warn",
3504
- category: "vue-builtin",
3505
- source: "oxlint-builtin",
2272
+ category: "structure",
2273
+ source: "doctor",
3506
2274
  recommended: true
3507
2275
  },
3508
2276
  {
3509
- id: "vue/define-emits-declaration",
2277
+ id: "nuxt-doctor/structure/nuxt-major-current",
3510
2278
  severity: "info",
3511
- category: "vue-builtin",
3512
- source: "oxlint-builtin",
2279
+ category: "structure",
2280
+ source: "doctor",
3513
2281
  recommended: false
3514
2282
  },
3515
2283
  {
3516
- id: "vue/define-props-declaration",
3517
- severity: "info",
3518
- category: "vue-builtin",
3519
- source: "oxlint-builtin",
3520
- recommended: false
2284
+ id: "nuxt-doctor/modules-deps/no-modules-incompatible-with-nuxt-4",
2285
+ severity: "warn",
2286
+ category: "modules-deps",
2287
+ source: "doctor",
2288
+ recommended: true
3521
2289
  },
3522
2290
  {
3523
- id: "vue/define-props-destructuring",
2291
+ id: "nuxt-doctor/modules-deps/recommended-modules-installed",
3524
2292
  severity: "info",
3525
- category: "vue-builtin",
3526
- source: "oxlint-builtin",
2293
+ category: "modules-deps",
2294
+ source: "doctor",
3527
2295
  recommended: false
3528
2296
  },
3529
2297
  {
3530
- id: "vue/max-props",
3531
- severity: "info",
3532
- category: "vue-builtin",
3533
- source: "oxlint-builtin",
3534
- recommended: false
2298
+ id: "nuxt-doctor/nitro/compatibilityDate-set",
2299
+ severity: "error",
2300
+ category: "nitro",
2301
+ source: "doctor",
2302
+ recommended: true
3535
2303
  },
3536
2304
  {
3537
- id: "vue-doctor/no-em-dash-in-string",
2305
+ id: "nuxt-doctor/nitro/preset-defined-for-deploy-target",
3538
2306
  severity: "warn",
3539
- category: "ai-slop",
2307
+ category: "nitro",
3540
2308
  source: "doctor",
3541
2309
  recommended: true
3542
2310
  },
3543
2311
  {
3544
- id: "vue-doctor/no-destructure-props-without-to-refs",
3545
- severity: "error",
3546
- category: "ai-slop",
2312
+ id: "nuxt-doctor/nitro/runtime-config-typed",
2313
+ severity: "info",
2314
+ category: "nitro",
3547
2315
  source: "doctor",
3548
- recommended: true
2316
+ recommended: false
3549
2317
  },
3550
2318
  {
3551
- id: "vue-doctor/no-destructure-reactive-without-to-refs",
3552
- severity: "error",
3553
- category: "ai-slop",
2319
+ id: "nuxt-doctor/seo/lang-on-html",
2320
+ severity: "warn",
2321
+ category: "seo",
3554
2322
  source: "doctor",
3555
2323
  recommended: true
3556
2324
  },
3557
2325
  {
3558
- id: "vue-doctor/no-non-null-assertion-on-ref-value",
2326
+ id: "nuxt-doctor/seo/useSeoMeta-on-public-page",
3559
2327
  severity: "warn",
3560
- category: "ai-slop",
2328
+ category: "seo",
3561
2329
  source: "doctor",
3562
2330
  recommended: true
3563
2331
  },
3564
2332
  {
3565
- id: "vue-doctor/no-imports-from-vue-when-auto-imported",
2333
+ id: "nuxt-doctor/seo/og-image-defined",
3566
2334
  severity: "warn",
3567
- category: "ai-slop",
2335
+ category: "seo",
3568
2336
  source: "doctor",
3569
2337
  recommended: true
3570
2338
  },
3571
2339
  {
3572
- id: "vue-doctor/reactivity/watch-without-cleanup",
2340
+ id: "nuxt-doctor/cloudflare/nitro-cloudflare-preset",
3573
2341
  severity: "warn",
3574
- category: "reactivity",
2342
+ category: "cloudflare",
3575
2343
  source: "doctor",
3576
2344
  recommended: true
3577
2345
  },
3578
2346
  {
3579
- id: "vue-doctor/reactivity/prefer-shallowRef-for-large-data",
2347
+ id: "nuxt-doctor/cloudflare/og-image-via-satori",
3580
2348
  severity: "info",
3581
- category: "reactivity",
2349
+ category: "cloudflare",
3582
2350
  source: "doctor",
3583
2351
  recommended: false
3584
2352
  },
3585
2353
  {
3586
- id: "vue-doctor/reactivity/prefer-readonly-for-injected",
3587
- severity: "info",
3588
- category: "reactivity",
2354
+ id: "nuxt-doctor/cloudflare/no-node-only-modules",
2355
+ severity: "warn",
2356
+ category: "cloudflare",
3589
2357
  source: "doctor",
3590
- recommended: false
2358
+ recommended: true
3591
2359
  },
3592
2360
  {
3593
- id: "vue-doctor/composition/prefer-script-setup-for-new-files",
2361
+ id: "nuxt-doctor/data-fetching/no-shared-key-across-pages",
3594
2362
  severity: "warn",
3595
- category: "composition",
2363
+ category: "data-fetching",
3596
2364
  source: "doctor",
3597
2365
  recommended: true
3598
2366
  },
3599
2367
  {
3600
- id: "vue-doctor/composition/defineProps-typed",
2368
+ id: "nuxt-doctor/data-fetching/ssr-safe-onMounted-only-for-client",
3601
2369
  severity: "warn",
3602
- category: "composition",
2370
+ category: "data-fetching",
3603
2371
  source: "doctor",
3604
2372
  recommended: true
3605
2373
  },
3606
2374
  {
3607
- id: "vue-doctor/performance/prefer-defineAsyncComponent-on-route",
3608
- severity: "info",
3609
- category: "performance",
2375
+ id: "nuxt-doctor/ai-slop/no-mixed-app-and-root-layout",
2376
+ severity: "warn",
2377
+ category: "ai-slop",
3610
2378
  source: "doctor",
3611
- recommended: false
2379
+ recommended: true
3612
2380
  },
3613
2381
  {
3614
- id: "vue-doctor/template/v-for-has-key",
2382
+ id: "nuxt-doctor/ai-slop/no-process-client-server",
3615
2383
  severity: "error",
3616
- category: "template",
2384
+ category: "ai-slop",
3617
2385
  source: "doctor",
3618
2386
  recommended: true
3619
2387
  },
3620
2388
  {
3621
- id: "vue-doctor/template/v-if-v-for-precedence",
3622
- severity: "error",
3623
- category: "template",
2389
+ id: "nuxt-doctor/ai-slop/no-explicit-imports-of-auto-imported",
2390
+ severity: "warn",
2391
+ category: "ai-slop",
3624
2392
  source: "doctor",
3625
2393
  recommended: true
3626
2394
  },
3627
2395
  {
3628
- id: "vue-doctor/template/v-memo-on-large-list",
2396
+ id: "nuxt-doctor/ai-slop/no-useState-for-server-data",
3629
2397
  severity: "warn",
3630
- category: "performance",
2398
+ category: "ai-slop",
3631
2399
  source: "doctor",
3632
2400
  recommended: true
3633
2401
  },
3634
2402
  {
3635
- id: "vue-doctor/template/no-inline-object-prop-in-list",
2403
+ id: "nuxt-doctor/ai-slop/no-fetch-in-setup",
3636
2404
  severity: "warn",
3637
- category: "performance",
2405
+ category: "ai-slop",
3638
2406
  source: "doctor",
3639
2407
  recommended: true
3640
2408
  },
3641
2409
  {
3642
- id: "vue-doctor/template/no-computed-getter-in-template-loop",
3643
- severity: "warn",
3644
- category: "template-perf",
2410
+ id: "nuxt-doctor/data-fetching/useAsyncData-key-required-in-loop",
2411
+ severity: "error",
2412
+ category: "data-fetching",
3645
2413
  source: "doctor",
3646
2414
  recommended: true
3647
2415
  },
3648
2416
  {
3649
- id: "vue-doctor/template/avoid-deep-v-bind-spread-in-list",
3650
- severity: "info",
3651
- category: "template-perf",
2417
+ id: "nuxt-doctor/server-routes/defineEventHandler-typed",
2418
+ severity: "warn",
2419
+ category: "server-routes",
3652
2420
  source: "doctor",
3653
2421
  recommended: true
3654
2422
  },
3655
2423
  {
3656
- id: "vue-doctor/sfc/no-mixed-options-and-composition-api",
2424
+ id: "nuxt-doctor/server-routes/validate-body-with-h3-v2",
3657
2425
  severity: "warn",
3658
- category: "sfc",
2426
+ category: "server-routes",
3659
2427
  source: "doctor",
3660
2428
  recommended: true
3661
2429
  },
3662
2430
  {
3663
- id: "vue-doctor/build-quality/tsconfig-strict-required",
2431
+ id: "nuxt-doctor/server-routes/createError-on-failure",
3664
2432
  severity: "warn",
3665
- category: "build-quality",
2433
+ category: "server-routes",
3666
2434
  source: "doctor",
3667
2435
  recommended: true
3668
2436
  },
3669
2437
  {
3670
- id: "vue-doctor/build-quality/vue-tsc-in-devDeps",
2438
+ id: "nuxt-doctor/hydration/no-document-in-setup",
2439
+ severity: "error",
2440
+ category: "hydration",
2441
+ source: "doctor",
2442
+ recommended: true
2443
+ },
2444
+ {
2445
+ id: "nuxt-doctor/hydration/clientOnly-for-browser-apis",
2446
+ severity: "error",
2447
+ category: "hydration",
2448
+ source: "doctor",
2449
+ recommended: true
2450
+ },
2451
+ {
2452
+ id: "vue-doctor/security/no-v-html",
2453
+ severity: "error",
2454
+ category: "security",
2455
+ source: "doctor",
2456
+ recommended: true
2457
+ },
2458
+ {
2459
+ id: "vue-doctor/security/no-inner-html",
2460
+ severity: "error",
2461
+ category: "security",
2462
+ source: "doctor",
2463
+ recommended: true
2464
+ },
2465
+ {
2466
+ id: "vue-doctor/security/no-eval-like",
2467
+ severity: "error",
2468
+ category: "security",
2469
+ source: "doctor",
2470
+ recommended: true
2471
+ },
2472
+ {
2473
+ id: "vue-doctor/security/no-auth-token-in-web-storage",
3671
2474
  severity: "warn",
3672
- category: "build-quality",
2475
+ category: "security",
3673
2476
  source: "doctor",
3674
2477
  recommended: true
3675
2478
  },
3676
2479
  {
3677
- id: "vue-doctor/build-quality/no-vue-cli",
2480
+ id: "vue-doctor/security/no-secrets-in-source",
3678
2481
  severity: "warn",
3679
- category: "build-quality",
2482
+ category: "security",
3680
2483
  source: "doctor",
3681
2484
  recommended: true
3682
2485
  },
3683
2486
  {
3684
- id: "vue-doctor/build-quality/eslint-plugin-vue-installed",
3685
- severity: "info",
3686
- category: "build-quality",
2487
+ id: "vue-doctor/security/no-target-blank-without-rel",
2488
+ severity: "warn",
2489
+ category: "security",
3687
2490
  source: "doctor",
3688
2491
  recommended: true
3689
2492
  },
3690
2493
  {
3691
- id: "vue-doctor/deps/duplicate-vue-versions",
2494
+ id: "vue-doctor/security/no-javascript-uri",
3692
2495
  severity: "error",
3693
- category: "deps",
2496
+ category: "security",
3694
2497
  source: "doctor",
3695
2498
  recommended: true
3696
2499
  },
3697
2500
  {
3698
- id: "vue-doctor/deps/vue-major-current",
3699
- severity: "info",
3700
- category: "deps",
2501
+ id: "nuxt-doctor/security/no-secret-in-public-runtime-config",
2502
+ severity: "error",
2503
+ category: "security",
3701
2504
  source: "doctor",
3702
- recommended: false
2505
+ recommended: true
3703
2506
  },
3704
2507
  {
3705
- id: "dead-code/unused-file",
2508
+ id: "nuxt-doctor/security/no-user-input-in-fetch-url",
3706
2509
  severity: "warn",
3707
- category: "dead-code",
2510
+ category: "security",
3708
2511
  source: "doctor",
3709
2512
  recommended: true
3710
2513
  },
3711
2514
  {
3712
- id: "dead-code/unused-export",
2515
+ id: "vue-doctor/design/no-arbitrary-tailwind-values",
3713
2516
  severity: "warn",
3714
- category: "dead-code",
2517
+ category: "design",
3715
2518
  source: "doctor",
3716
2519
  recommended: true
3717
2520
  },
3718
2521
  {
3719
- id: "dead-code/unused-type-export",
3720
- severity: "info",
3721
- category: "dead-code",
2522
+ id: "vue-doctor/design/no-raw-hex-color",
2523
+ severity: "warn",
2524
+ category: "design",
3722
2525
  source: "doctor",
3723
2526
  recommended: true
3724
2527
  },
3725
2528
  {
3726
- id: "dead-code/unused-member",
3727
- severity: "info",
3728
- category: "dead-code",
2529
+ id: "vue-doctor/design/no-default-tailwind-palette",
2530
+ severity: "warn",
2531
+ category: "design",
3729
2532
  source: "doctor",
3730
2533
  recommended: true
3731
2534
  },
3732
2535
  {
3733
- id: "dead-code/unused-dependency",
2536
+ id: "vue-doctor/design/no-important-utility",
3734
2537
  severity: "warn",
3735
- category: "dead-code",
2538
+ category: "design",
3736
2539
  source: "doctor",
3737
2540
  recommended: true
3738
2541
  },
3739
2542
  {
3740
- id: "dead-code/unlisted-dependency",
3741
- severity: "error",
3742
- category: "dead-code",
2543
+ id: "vue-doctor/design/no-hardcoded-inline-style",
2544
+ severity: "warn",
2545
+ category: "design",
3743
2546
  source: "doctor",
3744
2547
  recommended: true
3745
2548
  },
3746
2549
  {
3747
- id: "dead-code/duplicate-export",
2550
+ id: "vue-doctor/design/no-missing-alt",
3748
2551
  severity: "warn",
3749
- category: "dead-code",
2552
+ category: "design",
3750
2553
  source: "doctor",
3751
2554
  recommended: true
3752
2555
  },
3753
2556
  {
3754
- id: "nuxt-doctor/structure/uses-app-directory",
2557
+ id: "vue-doctor/design/no-absurd-z-index",
2558
+ severity: "warn",
2559
+ category: "design",
2560
+ source: "doctor",
2561
+ recommended: true
2562
+ }
2563
+ ];
2564
+ function listRules(filter = {}) {
2565
+ let rules = [...RULE_REGISTRY];
2566
+ if (filter.preset === "recommended") rules = rules.filter((r) => r.recommended);
2567
+ if (filter.category) rules = rules.filter((r) => r.category === filter.category);
2568
+ if (filter.source) rules = rules.filter((r) => r.source === filter.source);
2569
+ if (filter.severity) rules = rules.filter((r) => r.severity === filter.severity);
2570
+ return rules.sort((a, b) => a.id.localeCompare(b.id));
2571
+ }
2572
+ //#endregion
2573
+ //#region src/score-dimensions.ts
2574
+ const SCORE_DIMENSIONS = [
2575
+ "performance",
2576
+ "correctness",
2577
+ "security",
2578
+ "maintainability",
2579
+ "nuxt",
2580
+ "design"
2581
+ ];
2582
+ const CATEGORY_TO_DIMENSION = {
2583
+ performance: "performance",
2584
+ "template-perf": "performance",
2585
+ reactivity: "correctness",
2586
+ composition: "correctness",
2587
+ template: "correctness",
2588
+ sfc: "correctness",
2589
+ "vue-builtin": "correctness",
2590
+ hydration: "correctness",
2591
+ security: "security",
2592
+ "ai-slop": "maintainability",
2593
+ "dead-code": "maintainability",
2594
+ "build-quality": "maintainability",
2595
+ deps: "maintainability",
2596
+ structure: "maintainability",
2597
+ "modules-deps": "maintainability",
2598
+ nitro: "nuxt",
2599
+ seo: "nuxt",
2600
+ cloudflare: "nuxt",
2601
+ "server-routes": "nuxt",
2602
+ "data-fetching": "nuxt",
2603
+ design: "design"
2604
+ };
2605
+ function dimensionForCategory(category) {
2606
+ return CATEGORY_TO_DIMENSION[category];
2607
+ }
2608
+ function isScoreDimension(value) {
2609
+ return SCORE_DIMENSIONS.includes(value);
2610
+ }
2611
+ function categoriesForDimension(dimension) {
2612
+ const out = [];
2613
+ for (const [cat, dim] of Object.entries(CATEGORY_TO_DIMENSION)) if (dim === dimension) out.push(cat);
2614
+ return out;
2615
+ }
2616
+ //#endregion
2617
+ //#region src/score.ts
2618
+ const SEVERITY_WEIGHTS = {
2619
+ error: 5,
2620
+ warn: 2,
2621
+ info: .5
2622
+ };
2623
+ const DIMENSION_BY_RULE_ID = new Map(RULE_REGISTRY.map((r) => [r.id, dimensionForCategory(r.category)]));
2624
+ function weightFor(diag, config) {
2625
+ return config?.rules?.[diag.ruleId]?.weight ?? SEVERITY_WEIGHTS[diag.severity];
2626
+ }
2627
+ function penaltyForGroups(byRule, config) {
2628
+ let penalty = 0;
2629
+ for (const list of byRule.values()) {
2630
+ const weight = weightFor(list[0], config);
2631
+ for (let i = 0; i < list.length; i++) penalty += weight * (i === 0 ? 1 : 1 / Math.sqrt(i + 1));
2632
+ }
2633
+ return penalty;
2634
+ }
2635
+ function emptyDimensionScores() {
2636
+ const out = {};
2637
+ for (const dim of SCORE_DIMENSIONS) out[dim] = {
2638
+ score: 100,
2639
+ totalFindings: 0,
2640
+ errorCount: 0,
2641
+ warnCount: 0,
2642
+ infoCount: 0
2643
+ };
2644
+ return out;
2645
+ }
2646
+ function computeByDimension(diagnostics, config) {
2647
+ const grouped = /* @__PURE__ */ new Map();
2648
+ const counts = emptyDimensionScores();
2649
+ for (const d of diagnostics) {
2650
+ const dim = DIMENSION_BY_RULE_ID.get(d.ruleId);
2651
+ if (dim === void 0) continue;
2652
+ const bucket = counts[dim];
2653
+ bucket.totalFindings += 1;
2654
+ if (d.severity === "error") bucket.errorCount += 1;
2655
+ else if (d.severity === "warn") bucket.warnCount += 1;
2656
+ else bucket.infoCount += 1;
2657
+ let byRule = grouped.get(dim);
2658
+ if (!byRule) {
2659
+ byRule = /* @__PURE__ */ new Map();
2660
+ grouped.set(dim, byRule);
2661
+ }
2662
+ const list = byRule.get(d.ruleId);
2663
+ if (list) list.push(d);
2664
+ else byRule.set(d.ruleId, [d]);
2665
+ }
2666
+ for (const [dim, byRule] of grouped) {
2667
+ const penalty = penaltyForGroups(byRule, config);
2668
+ counts[dim].score = Math.max(0, Math.round(100 - penalty));
2669
+ }
2670
+ return counts;
2671
+ }
2672
+ function scoreDiagnostics(diagnostics, config) {
2673
+ const threshold = config?.threshold ?? 0;
2674
+ let errorCount = 0;
2675
+ let warnCount = 0;
2676
+ let infoCount = 0;
2677
+ const byRule = /* @__PURE__ */ new Map();
2678
+ for (const d of diagnostics) {
2679
+ if (d.severity === "error") errorCount += 1;
2680
+ else if (d.severity === "warn") warnCount += 1;
2681
+ else infoCount += 1;
2682
+ const list = byRule.get(d.ruleId);
2683
+ if (list) list.push(d);
2684
+ else byRule.set(d.ruleId, [d]);
2685
+ }
2686
+ const sortedRuleIds = [...byRule.keys()].sort();
2687
+ const breakdown = [];
2688
+ let penalty = 0;
2689
+ for (const ruleId of sortedRuleIds) {
2690
+ const list = byRule.get(ruleId);
2691
+ const weight = config?.rules?.[ruleId]?.weight ?? SEVERITY_WEIGHTS[list[0].severity];
2692
+ let rulePenalty = 0;
2693
+ for (let i = 0; i < list.length; i++) rulePenalty += weight * (i === 0 ? 1 : 1 / Math.sqrt(i + 1));
2694
+ penalty += rulePenalty;
2695
+ breakdown.push({
2696
+ ruleId,
2697
+ occurrences: list.length,
2698
+ weightPerOccurrence: weight,
2699
+ penalty: rulePenalty
2700
+ });
2701
+ }
2702
+ breakdown.sort((a, b) => b.penalty - a.penalty);
2703
+ const score = Math.max(0, Math.round(100 - penalty));
2704
+ return {
2705
+ score,
2706
+ passed: score >= threshold,
2707
+ threshold,
2708
+ totalFindings: diagnostics.length,
2709
+ errorCount,
2710
+ warnCount,
2711
+ infoCount,
2712
+ breakdown,
2713
+ byDimension: computeByDimension(diagnostics, config)
2714
+ };
2715
+ }
2716
+ //#endregion
2717
+ //#region src/sfc/parse-sfc-descriptor.ts
2718
+ const cache$1 = /* @__PURE__ */ new Map();
2719
+ async function parseSfcDescriptor(absPath) {
2720
+ if (cache$1.has(absPath)) return cache$1.get(absPath) ?? null;
2721
+ let source;
2722
+ try {
2723
+ source = await readFile(absPath, "utf8");
2724
+ } catch {
2725
+ cache$1.set(absPath, null);
2726
+ return null;
2727
+ }
2728
+ const { descriptor, errors } = parse(source, { filename: absPath });
2729
+ if (errors.length > 0) {
2730
+ cache$1.set(absPath, null);
2731
+ return null;
2732
+ }
2733
+ cache$1.set(absPath, descriptor);
2734
+ return descriptor;
2735
+ }
2736
+ //#endregion
2737
+ //#region src/sfc/rules/no-mixed-options-and-composition-api.ts
2738
+ const RULE_ID$5 = "vue-doctor/sfc/no-mixed-options-and-composition-api";
2739
+ 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";
2740
+ 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.";
2741
+ const DISALLOWED = new Set([
2742
+ "data",
2743
+ "methods",
2744
+ "computed",
2745
+ "watch",
2746
+ "props",
2747
+ "emits",
2748
+ "provide",
2749
+ "inject",
2750
+ "beforeCreate",
2751
+ "created",
2752
+ "beforeMount",
2753
+ "mounted",
2754
+ "beforeUpdate",
2755
+ "updated",
2756
+ "beforeUnmount",
2757
+ "unmounted",
2758
+ "activated",
2759
+ "deactivated",
2760
+ "errorCaptured",
2761
+ "serverPrefetch"
2762
+ ]);
2763
+ function resolveDefineComponentCall(call) {
2764
+ if (call.callee.type !== "Identifier") return null;
2765
+ if (call.callee.name !== "defineComponent") return null;
2766
+ const first = call.arguments[0];
2767
+ if (!first || first.type !== "ObjectExpression") return null;
2768
+ return first;
2769
+ }
2770
+ function findBindingInit(body, name) {
2771
+ for (const stmt of body) {
2772
+ if (stmt.type !== "VariableDeclaration") continue;
2773
+ for (const declarator of stmt.declarations) {
2774
+ if (declarator.id.type !== "Identifier") continue;
2775
+ if (declarator.id.name !== name) continue;
2776
+ return declarator.init;
2777
+ }
2778
+ }
2779
+ return null;
2780
+ }
2781
+ function resolveOptionsObject(program) {
2782
+ const exported = program.body.find((node) => node.type === "ExportDefaultDeclaration");
2783
+ if (!exported) return null;
2784
+ const declaration = exported.declaration;
2785
+ if (declaration.type === "ObjectExpression") return declaration;
2786
+ if (declaration.type === "CallExpression") return resolveDefineComponentCall(declaration);
2787
+ if (declaration.type === "Identifier") {
2788
+ const init = findBindingInit(program.body, declaration.name);
2789
+ if (init && init.type === "CallExpression") return resolveDefineComponentCall(init);
2790
+ return null;
2791
+ }
2792
+ return null;
2793
+ }
2794
+ function locate(content, offset, startLine, startColumn) {
2795
+ let line = startLine;
2796
+ let lastNewline = -1;
2797
+ for (let i = 0; i < offset; i += 1) if (content.charCodeAt(i) === 10) {
2798
+ line += 1;
2799
+ lastNewline = i;
2800
+ }
2801
+ const column = lastNewline === -1 ? startColumn + offset : offset - lastNewline;
2802
+ return {
2803
+ line,
2804
+ column
2805
+ };
2806
+ }
2807
+ function check$21(ctx) {
2808
+ const { script, scriptSetup } = ctx.descriptor;
2809
+ if (!script || !scriptSetup) return { diagnostics: [] };
2810
+ const lang = script.lang === "ts" ? "ts" : "js";
2811
+ const { program } = parseSync(`script.${lang}`, script.content, {
2812
+ sourceType: "module",
2813
+ lang
2814
+ });
2815
+ const options = resolveOptionsObject(program);
2816
+ if (!options) return { diagnostics: [] };
2817
+ const diagnostics = [];
2818
+ for (const property of options.properties) {
2819
+ if (property.type !== "Property") continue;
2820
+ if (property.key.type !== "Identifier") continue;
2821
+ if (!DISALLOWED.has(property.key.name)) continue;
2822
+ const { line, column } = locate(script.content, property.start, script.loc.start.line, script.loc.start.column);
2823
+ diagnostics.push({
2824
+ file: ctx.file,
2825
+ line,
2826
+ column,
2827
+ ruleId: RULE_ID$5,
2828
+ severity: "error",
2829
+ message: MESSAGE$5,
2830
+ source: "sfc",
2831
+ recommendation: RECOMMENDATION$5
2832
+ });
2833
+ }
2834
+ return { diagnostics };
2835
+ }
2836
+ //#endregion
2837
+ //#region src/nuxt/file-role.ts
2838
+ const PAGE_DIR = /^(?:app\/)?pages\//;
2839
+ const LAYOUT_DIR = /^(?:app\/)?layouts\//;
2840
+ function normalize(relPath) {
2841
+ return relPath.replace(/\\/g, "/").replace(/^\.\//, "");
2842
+ }
2843
+ /**
2844
+ * True when the path points at a Nuxt page component — a `.vue` file under
2845
+ * `pages/` (Nuxt 3 layout) or `app/pages/` (Nuxt 4 layout) at the project root.
2846
+ */
2847
+ function isNuxtPageFile(relPath) {
2848
+ const path = normalize(relPath);
2849
+ return PAGE_DIR.test(path) && path.endsWith(".vue");
2850
+ }
2851
+ /**
2852
+ * True when the path points at a Nitro server file — a `.ts` file under the
2853
+ * project-root `server/` directory.
2854
+ */
2855
+ function isNuxtServerFile(relPath) {
2856
+ const path = normalize(relPath);
2857
+ return path.startsWith("server/") && path.endsWith(".ts");
2858
+ }
2859
+ /**
2860
+ * True when the path points at a Nuxt layout component — a `.vue` file under
2861
+ * `layouts/` (Nuxt 3 layout) or `app/layouts/` (Nuxt 4 layout) at the project
2862
+ * root.
2863
+ */
2864
+ function isNuxtLayoutFile(relPath) {
2865
+ const path = normalize(relPath);
2866
+ return LAYOUT_DIR.test(path) && path.endsWith(".vue");
2867
+ }
2868
+ //#endregion
2869
+ //#region src/template/walk.ts
2870
+ const NODE_ELEMENT$2 = 1;
2871
+ function isElementNode(node) {
2872
+ return node !== null && node !== void 0 && node.type === NODE_ELEMENT$2;
2873
+ }
2874
+ function walkElements(root, visit) {
2875
+ const stack = [...root.children];
2876
+ while (stack.length > 0) {
2877
+ const node = stack.pop();
2878
+ if (!node) continue;
2879
+ if (isElementNode(node)) {
2880
+ visit(node);
2881
+ if (node.children) for (const child of node.children) stack.push(child);
2882
+ }
2883
+ }
2884
+ }
2885
+ //#endregion
2886
+ //#region src/sfc/rules/nuxt/no-mixed-app-and-root-layout.ts
2887
+ const RULE_ID$4 = "nuxt-doctor/ai-slop/no-mixed-app-and-root-layout";
2888
+ 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.";
2889
+ const RECOMMENDATION$4 = "Replace <NuxtLayout> in this layout file with <slot />. The layout slot is already provided by the parent layout wrapper.";
2890
+ function check$20(ctx) {
2891
+ if (!isNuxtLayoutFile(ctx.relativePath)) return { diagnostics: [] };
2892
+ const { template } = ctx.descriptor;
2893
+ if (!template || !template.ast) return { diagnostics: [] };
2894
+ let foundNuxtLayout = false;
2895
+ let nuxtLayoutNode;
2896
+ walkElements(template.ast, (el) => {
2897
+ if (el.tag === "NuxtLayout") {
2898
+ foundNuxtLayout = true;
2899
+ nuxtLayoutNode = el;
2900
+ }
2901
+ });
2902
+ if (!foundNuxtLayout) return { diagnostics: [] };
2903
+ const node = nuxtLayoutNode;
2904
+ return { diagnostics: [{
2905
+ file: ctx.file,
2906
+ line: node.loc.start.line,
2907
+ column: node.loc.start.column,
2908
+ endLine: node.loc.end.line,
2909
+ endColumn: node.loc.end.column,
2910
+ ruleId: RULE_ID$4,
3755
2911
  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",
2912
+ message: MESSAGE$4,
2913
+ source: "sfc",
2914
+ recommendation: RECOMMENDATION$4
2915
+ }] };
2916
+ }
2917
+ //#endregion
2918
+ //#region src/sfc/rules/nuxt/og-image-defined.ts
2919
+ const RULE_ID$3 = "nuxt-doctor/seo/og-image-defined";
2920
+ 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.";
2921
+ const RECOMMENDATION$3 = "Add ogImage: \"/path/to/image.png\" to useSeoMeta / useHead, or install @nuxtjs/og-image.";
2922
+ function hasOgImageInCall(program) {
2923
+ for (const stmt of program.body) {
2924
+ if (stmt.type !== "ExpressionStatement") continue;
2925
+ const call = stmt.expression;
2926
+ if (call.type !== "CallExpression") continue;
2927
+ if (call.callee.type !== "Identifier") continue;
2928
+ const name = call.callee.name;
2929
+ if (name !== "useSeoMeta" && name !== "useHead") continue;
2930
+ const firstArg = call.arguments[0];
2931
+ if (!firstArg || firstArg.type !== "ObjectExpression") continue;
2932
+ for (const prop of firstArg.properties) {
2933
+ if (prop.type !== "Property") continue;
2934
+ if (prop.key.type === "Identifier" && prop.key.name === "ogImage") return true;
2935
+ if (prop.key.type === "Literal" && prop.key.value === "og:image") return true;
2936
+ }
2937
+ }
2938
+ return false;
2939
+ }
2940
+ function hasOgImageDep(packageJsonPath) {
2941
+ try {
2942
+ const raw = readFileSync(packageJsonPath, "utf8");
2943
+ const pkg = JSON.parse(raw);
2944
+ const deps = {
2945
+ ...pkg.dependencies,
2946
+ ...pkg.devDependencies
2947
+ };
2948
+ return "@nuxtjs/og-image" in deps || "nuxt-og-image" in deps;
2949
+ } catch {
2950
+ return false;
2951
+ }
2952
+ }
2953
+ function check$19(ctx) {
2954
+ if (!isNuxtPageFile(ctx.relativePath)) return { diagnostics: [] };
2955
+ const { scriptSetup } = ctx.descriptor;
2956
+ if (!scriptSetup) return { diagnostics: [] };
2957
+ const lang = scriptSetup.lang === "ts" ? "ts" : "js";
2958
+ const { program } = parseSync(`script.${lang}`, scriptSetup.content, {
2959
+ sourceType: "module",
2960
+ lang
2961
+ });
2962
+ if (hasOgImageInCall(program)) return { diagnostics: [] };
2963
+ if (ctx.projectInfo.packageJsonPath === null) return { diagnostics: [] };
2964
+ if (hasOgImageDep(ctx.projectInfo.packageJsonPath)) return { diagnostics: [] };
2965
+ const { line, column } = scriptSetup.loc.start;
2966
+ return { diagnostics: [{
2967
+ file: ctx.file,
2968
+ line,
2969
+ column,
2970
+ ruleId: RULE_ID$3,
3769
2971
  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",
2972
+ message: MESSAGE$3,
2973
+ source: "sfc",
2974
+ recommendation: RECOMMENDATION$3
2975
+ }] };
2976
+ }
2977
+ //#endregion
2978
+ //#region src/sfc/rules/nuxt/use-seo-meta-on-public-page.ts
2979
+ const RULE_ID$2 = "nuxt-doctor/seo/useSeoMeta-on-public-page";
2980
+ 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.";
2981
+ const RECOMMENDATION$2 = "Call useSeoMeta({ title: \"...\" }) in<script setup> to define page title and meta tags for search engines and social previews.";
2982
+ function hasTitleInCall(program) {
2983
+ for (const stmt of program.body) {
2984
+ if (stmt.type !== "ExpressionStatement") continue;
2985
+ const call = stmt.expression;
2986
+ if (call.type !== "CallExpression") continue;
2987
+ if (call.callee.type !== "Identifier") continue;
2988
+ const name = call.callee.name;
2989
+ if (name !== "useSeoMeta" && name !== "useHead" && name !== "definePageMeta") continue;
2990
+ const firstArg = call.arguments[0];
2991
+ if (!firstArg || firstArg.type !== "ObjectExpression") continue;
2992
+ for (const prop of firstArg.properties) {
2993
+ if (prop.type !== "Property") continue;
2994
+ if (prop.key.type === "Identifier" && prop.key.name === "title") return true;
2995
+ if (prop.key.type === "Literal" && prop.key.value === "title") return true;
2996
+ }
2997
+ }
2998
+ return false;
2999
+ }
3000
+ function check$18(ctx) {
3001
+ if (!isNuxtPageFile(ctx.relativePath)) return { diagnostics: [] };
3002
+ const { scriptSetup } = ctx.descriptor;
3003
+ if (!scriptSetup) return { diagnostics: [] };
3004
+ const lang = scriptSetup.lang === "ts" ? "ts" : "js";
3005
+ const { program } = parseSync(`script.${lang}`, scriptSetup.content, {
3006
+ sourceType: "module",
3007
+ lang
3008
+ });
3009
+ if (hasTitleInCall(program)) return { diagnostics: [] };
3010
+ const { line, column } = scriptSetup.loc.start;
3011
+ return { diagnostics: [{
3012
+ file: ctx.file,
3013
+ line,
3014
+ column,
3015
+ ruleId: RULE_ID$2,
3790
3016
  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
- },
3017
+ message: MESSAGE$2,
3018
+ source: "sfc",
3019
+ recommendation: RECOMMENDATION$2
3020
+ }] };
3021
+ }
3022
+ //#endregion
3023
+ //#region src/sfc/rules/index.ts
3024
+ const SFC_RULES = [
3802
3025
  {
3803
- id: "nuxt-doctor/seo/lang-on-html",
3804
- severity: "warn",
3805
- category: "seo",
3806
- source: "doctor",
3807
- recommended: true
3026
+ id: "vue-doctor/sfc/no-mixed-options-and-composition-api",
3027
+ check: check$21
3808
3028
  },
3809
3029
  {
3810
3030
  id: "nuxt-doctor/seo/useSeoMeta-on-public-page",
3811
- severity: "warn",
3812
- category: "seo",
3813
- source: "doctor",
3814
- recommended: true
3031
+ check: check$18
3815
3032
  },
3816
3033
  {
3817
3034
  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
3035
+ check: check$19
3850
3036
  },
3851
3037
  {
3852
- id: "nuxt-doctor/data-fetching/ssr-safe-onMounted-only-for-client",
3038
+ id: "nuxt-doctor/ai-slop/no-mixed-app-and-root-layout",
3039
+ check: check$20
3040
+ }
3041
+ ];
3042
+ //#endregion
3043
+ //#region src/sfc/run.ts
3044
+ async function runSfcPass(opts) {
3045
+ const all = [];
3046
+ const { projectInfo } = opts;
3047
+ const rootDir = projectInfo?.rootDirectory ?? process.cwd();
3048
+ for (const file of opts.files) {
3049
+ if (!file.endsWith(".vue")) continue;
3050
+ const descriptor = await parseSfcDescriptor(file);
3051
+ if (!descriptor) continue;
3052
+ const relativePath = relative(rootDir, file).replace(/\\/g, "/");
3053
+ for (const rule of SFC_RULES) {
3054
+ const override = opts.ruleOverrides?.[rule.id];
3055
+ if (override === "off") continue;
3056
+ const { diagnostics } = rule.check({
3057
+ file,
3058
+ descriptor,
3059
+ rootDirectory: rootDir,
3060
+ relativePath,
3061
+ projectInfo
3062
+ });
3063
+ for (const d of diagnostics) all.push(override ? {
3064
+ ...d,
3065
+ severity: override
3066
+ } : d);
3067
+ }
3068
+ }
3069
+ return all;
3070
+ }
3071
+ //#endregion
3072
+ //#region src/nuxt/cross-file/run.ts
3073
+ function extractDataFetchingKeys(content, lang) {
3074
+ const keys = [];
3075
+ try {
3076
+ const { program } = parseSync(`script.${lang}`, content, {
3077
+ sourceType: "module",
3078
+ lang
3079
+ });
3080
+ for (const stmt of program.body) if (stmt.type === "VariableDeclaration") for (const decl of stmt.declarations) {
3081
+ const call = unwrapAwait(decl.init);
3082
+ if (call?.type !== "CallExpression") continue;
3083
+ extractKeyFromCall(call, keys);
3084
+ }
3085
+ else if (stmt.type === "ExpressionStatement") {
3086
+ const expr = unwrapAwait(stmt.expression);
3087
+ if (expr?.type === "CallExpression") extractKeyFromCall(expr, keys);
3088
+ }
3089
+ } catch {}
3090
+ return keys;
3091
+ }
3092
+ function unwrapAwait(node) {
3093
+ if (node?.type === "AwaitExpression") return node.argument;
3094
+ return node;
3095
+ }
3096
+ function extractKeyFromCall(call, keys) {
3097
+ if (call.callee.type !== "Identifier") return;
3098
+ const fnName = call.callee.name;
3099
+ if (fnName !== "useAsyncData" && fnName !== "useFetch") return;
3100
+ const keyArg = call.arguments[0];
3101
+ if (keyArg?.type === "Literal" && typeof keyArg.value === "string") keys.push(keyArg.value);
3102
+ else if (keyArg?.type === "TemplateLiteral" && keyArg.quasis.length === 1) {
3103
+ const val = keyArg.quasis[0]?.value?.raw;
3104
+ if (val) keys.push(val);
3105
+ }
3106
+ if (fnName === "useFetch" && call.arguments.length >= 2) {
3107
+ const optsArg = call.arguments[1];
3108
+ if (optsArg?.type === "ObjectExpression") {
3109
+ for (const prop of optsArg.properties) if (prop.type === "Property" && prop.key.type === "Identifier" && prop.key.name === "key" && prop.value.type === "Literal" && typeof prop.value.value === "string") keys.push(prop.value.value);
3110
+ }
3111
+ }
3112
+ }
3113
+ async function runCrossFilePass(opts) {
3114
+ const { files, projectInfo } = opts;
3115
+ if (projectInfo.framework !== "nuxt") return [];
3116
+ const rootDir = projectInfo.rootDirectory;
3117
+ const pageFiles = files.filter((f) => isNuxtPageFile(relative(rootDir, f).replace(/\\/g, "/")));
3118
+ if (pageFiles.length === 0) return [];
3119
+ const parsed = [];
3120
+ for (const file of pageFiles) {
3121
+ const descriptor = await parseSfcDescriptor(file);
3122
+ if (!descriptor) continue;
3123
+ const { scriptSetup } = descriptor;
3124
+ if (!scriptSetup) continue;
3125
+ parsed.push({
3126
+ file,
3127
+ relativePath: relative(rootDir, file).replace(/\\/g, "/"),
3128
+ keys: extractDataFetchingKeys(scriptSetup.content, scriptSetup.lang === "ts" ? "ts" : "js"),
3129
+ scriptSetupContent: scriptSetup.content,
3130
+ scriptSetupLang: scriptSetup.lang === "ts" ? "ts" : "js",
3131
+ scriptSetupLine: scriptSetup.loc.start.line
3132
+ });
3133
+ }
3134
+ return runCrossFileRules(parsed);
3135
+ }
3136
+ function runCrossFileRules(pages) {
3137
+ const all = [];
3138
+ all.push(...ruleNoSharedKeyAcrossPages(pages));
3139
+ all.push(...ruleSsrSafeOnMountedOnlyForClient(pages));
3140
+ return all;
3141
+ }
3142
+ function ruleNoSharedKeyAcrossPages(pages) {
3143
+ const keyToFiles = /* @__PURE__ */ new Map();
3144
+ for (const page of pages) for (const key of page.keys) {
3145
+ const existing = keyToFiles.get(key) ?? [];
3146
+ if (!existing.includes(page.file)) existing.push(page.file);
3147
+ keyToFiles.set(key, existing);
3148
+ }
3149
+ const diags = [];
3150
+ for (const [key, files] of keyToFiles) {
3151
+ if (files.length < 2) continue;
3152
+ for (const file of files) diags.push({
3153
+ file,
3154
+ line: 1,
3155
+ column: 1,
3156
+ ruleId: "nuxt-doctor/data-fetching/no-shared-key-across-pages",
3157
+ severity: "warn",
3158
+ message: `Data fetching key "${key}" is shared across ${files.length} different page files. This causes cache collisions in useAsyncData/useFetch. Use a unique key per page.`,
3159
+ source: "cross-file",
3160
+ recommendation: `Rename the key to something page-specific, e.g. "page-${key}" or "users-${key}".`
3161
+ });
3162
+ }
3163
+ return diags;
3164
+ }
3165
+ const BROWSER_GLOBALS = new Set([
3166
+ "window",
3167
+ "document",
3168
+ "navigator",
3169
+ "localStorage",
3170
+ "sessionStorage",
3171
+ "location",
3172
+ "history",
3173
+ "fetch",
3174
+ "XMLHttpRequest",
3175
+ "matchMedia",
3176
+ "IntersectionObserver",
3177
+ "MutationObserver",
3178
+ "indexedDB",
3179
+ "webkit",
3180
+ "moz",
3181
+ "onmessage"
3182
+ ]);
3183
+ function ruleSsrSafeOnMountedOnlyForClient(pages) {
3184
+ const diags = [];
3185
+ for (const page of pages) {
3186
+ const { program } = parseSync(`script.${page.scriptSetupLang}`, page.scriptSetupContent, {
3187
+ sourceType: "module",
3188
+ lang: page.scriptSetupLang
3189
+ });
3190
+ for (const stmt of program.body) if (stmt.type === "VariableDeclaration") for (const decl of stmt.declarations) {
3191
+ const init = decl.init;
3192
+ if (!init) continue;
3193
+ if (init.type === "CallExpression") {
3194
+ const call = init;
3195
+ if (isSkippedReactiveCall(call)) continue;
3196
+ checkCallForBrowserGlobal(call, page, diags);
3197
+ } else if (init.type === "MemberExpression") checkMemberForBrowserGlobal(init, page, diags);
3198
+ }
3199
+ else if (stmt.type === "ExpressionStatement") {
3200
+ const expr = stmt.expression;
3201
+ if (expr.type === "CallExpression") {
3202
+ if (isSkippedReactiveCall(expr)) continue;
3203
+ checkCallForBrowserGlobal(expr, page, diags);
3204
+ } else if (expr.type === "MemberExpression") checkMemberForBrowserGlobal(expr, page, diags);
3205
+ }
3206
+ }
3207
+ return diags;
3208
+ }
3209
+ const CLIENT_SAFE_WRAPPERS = new Set([
3210
+ "onMounted",
3211
+ "watchEffect",
3212
+ "watch"
3213
+ ]);
3214
+ function isSkippedReactiveCall(call) {
3215
+ if (call.callee.type !== "Identifier") return false;
3216
+ return CLIENT_SAFE_WRAPPERS.has(call.callee.name);
3217
+ }
3218
+ function checkCallForBrowserGlobal(call, page, diags) {
3219
+ const callee = call.callee;
3220
+ if (callee.type !== "MemberExpression") return;
3221
+ const obj = callee.object;
3222
+ if (obj.type !== "Identifier" || !BROWSER_GLOBALS.has(obj.name)) return;
3223
+ diags.push({
3224
+ file: page.file,
3225
+ line: page.scriptSetupLine,
3226
+ column: 1,
3227
+ ruleId: "nuxt-doctor/data-fetching/ssr-safe-onMounted-only-for-client",
3853
3228
  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",
3229
+ message: `Browser global "${obj.name}" is accessed at setup top-level outside onMounted. This causes SSR/hydration mismatches. Move browser-only code inside onMounted or a client-only plugin.`,
3230
+ source: "cross-file",
3231
+ recommendation: `Wrap "${obj.name}" access in onMounted(() => { /* code */ }) or use defineNuxtPlugin to run only on the client.`
3232
+ });
3233
+ }
3234
+ function checkMemberForBrowserGlobal(mem, page, diags) {
3235
+ const obj = mem.object;
3236
+ if (obj.type !== "Identifier" || !BROWSER_GLOBALS.has(obj.name)) return;
3237
+ diags.push({
3238
+ file: page.file,
3239
+ line: page.scriptSetupLine,
3240
+ column: 1,
3241
+ ruleId: "nuxt-doctor/data-fetching/ssr-safe-onMounted-only-for-client",
3860
3242
  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",
3243
+ message: `Browser global "${obj.name}" is accessed at setup top-level outside onMounted. This causes SSR/hydration mismatches. Move browser-only code inside onMounted or a client-only plugin.`,
3244
+ source: "cross-file",
3245
+ recommendation: `Wrap "${obj.name}" access in onMounted(() => { /* code */ }) or use defineNuxtPlugin to run only on the client.`
3246
+ });
3247
+ }
3248
+ //#endregion
3249
+ //#region src/template/parse-sfc.ts
3250
+ const cache = /* @__PURE__ */ new Map();
3251
+ async function parseSfc(absPath) {
3252
+ if (cache.has(absPath)) return cache.get(absPath) ?? null;
3253
+ let source;
3254
+ try {
3255
+ source = await readFile(absPath, "utf8");
3256
+ } catch {
3257
+ cache.set(absPath, null);
3258
+ return null;
3259
+ }
3260
+ const { descriptor, errors } = parse(source, { filename: absPath });
3261
+ if (errors.length > 0 || !descriptor.template) {
3262
+ cache.set(absPath, null);
3263
+ return null;
3264
+ }
3265
+ cache.set(absPath, descriptor);
3266
+ return descriptor;
3267
+ }
3268
+ //#endregion
3269
+ //#region src/template/directive-helpers.ts
3270
+ const NODE_DIRECTIVE$4 = 7;
3271
+ function findDirective(el, name) {
3272
+ for (const prop of el.props) if (prop.type === NODE_DIRECTIVE$4 && prop.name === name) return prop;
3273
+ }
3274
+ function findBindAttr(el, attrName) {
3275
+ for (const prop of el.props) {
3276
+ if (prop.type !== NODE_DIRECTIVE$4) continue;
3277
+ const dir = prop;
3278
+ if (dir.name !== "bind") continue;
3279
+ if (dir.arg?.content === attrName) return dir;
3280
+ }
3281
+ }
3282
+ function findStaticAttr(el, attrName) {
3283
+ for (const prop of el.props) if (prop.type === 6 && prop.name === attrName) return prop;
3284
+ }
3285
+ //#endregion
3286
+ //#region src/template/rules/v-for-has-key.ts
3287
+ function check$17(ctx) {
3288
+ const diagnostics = [];
3289
+ walkElements(ctx.template, (el) => {
3290
+ if (!findDirective(el, "for")) return;
3291
+ if (findBindAttr(el, "key") ?? findStaticAttr(el, "key")) return;
3292
+ diagnostics.push({
3293
+ file: ctx.file,
3294
+ line: el.loc.start.line,
3295
+ column: el.loc.start.column,
3296
+ endLine: el.loc.end.line,
3297
+ endColumn: el.loc.end.column,
3298
+ ruleId: "vue-doctor/template/v-for-has-key",
3299
+ severity: "error",
3300
+ message: `<${el.tag}> uses v-for without :key. Vue cannot efficiently diff this list across renders.`,
3301
+ source: "template",
3302
+ recommendation: "Add :key with a stable, unique identifier from the iterated item."
3303
+ });
3304
+ });
3305
+ return { diagnostics };
3306
+ }
3307
+ //#endregion
3308
+ //#region src/template/rules/v-if-v-for-precedence.ts
3309
+ function check$16(ctx) {
3310
+ const diagnostics = [];
3311
+ walkElements(ctx.template, (el) => {
3312
+ const vIf = findDirective(el, "if");
3313
+ const vFor = findDirective(el, "for");
3314
+ if (!vIf || !vFor) return;
3315
+ diagnostics.push({
3316
+ file: ctx.file,
3317
+ line: el.loc.start.line,
3318
+ column: el.loc.start.column,
3319
+ endLine: el.loc.end.line,
3320
+ endColumn: el.loc.end.column,
3321
+ ruleId: "vue-doctor/template/v-if-v-for-precedence",
3322
+ severity: "error",
3323
+ message: `<${el.tag}> uses v-if and v-for on the same element. In Vue 3, v-if binds tighter than v-for, so the condition cannot reference loop variables.`,
3324
+ source: "template",
3325
+ recommendation: "Move the v-if to a <template v-for> wrapper or to a parent element, or filter the iterated source in a computed."
3326
+ });
3327
+ });
3328
+ return { diagnostics };
3329
+ }
3330
+ //#endregion
3331
+ //#region src/template/rules/v-memo-on-large-list.ts
3332
+ const ARRAY_LITERAL_THRESHOLD = 100;
3333
+ function findLargeArrayBindings(script) {
3334
+ const bindings = /* @__PURE__ */ new Map();
3335
+ try {
3336
+ const ast = babelParse(script, { sourceType: "module" });
3337
+ for (const node of ast.program.body) {
3338
+ if (node.type !== "VariableDeclaration") continue;
3339
+ if (node.kind !== "const" && node.kind !== "let") continue;
3340
+ for (const decl of node.declarations) {
3341
+ if (decl.id.type !== "Identifier") continue;
3342
+ const init = decl.init;
3343
+ if (!init || init.type !== "ArrayExpression") continue;
3344
+ if (!init.elements || init.elements.length <= ARRAY_LITERAL_THRESHOLD) continue;
3345
+ bindings.set(decl.id.name, init.elements.length);
3346
+ }
3347
+ }
3348
+ } catch {}
3349
+ return bindings;
3350
+ }
3351
+ function check$15(ctx) {
3352
+ const diagnostics = [];
3353
+ const largeArrays = ctx.script && ctx.script.length > 0 ? findLargeArrayBindings(ctx.script) : /* @__PURE__ */ new Map();
3354
+ walkElements(ctx.template, (el) => {
3355
+ const vFor = findDirective(el, "for");
3356
+ if (!vFor) return;
3357
+ if (findDirective(el, "memo")) return;
3358
+ const source = vFor.forParseResult?.source;
3359
+ if (!source) return;
3360
+ const sourceName = source.content;
3361
+ const largeArraySize = largeArrays.get(sourceName);
3362
+ if (largeArraySize !== void 0 && largeArraySize > ARRAY_LITERAL_THRESHOLD) diagnostics.push({
3363
+ file: ctx.file,
3364
+ line: el.loc.start.line,
3365
+ column: el.loc.start.column,
3366
+ endLine: el.loc.end.line,
3367
+ endColumn: el.loc.end.column,
3368
+ ruleId: "vue-doctor/template/v-memo-on-large-list",
3369
+ severity: "warn",
3370
+ message: `<${el.tag}> uses v-for over a large dataset (array literal with ${largeArraySize} items) without v-memo. Vue cannot skip diffing this list on every render.`,
3371
+ source: "template",
3372
+ recommendation: "Add v-memo with a meaningful memoization key so Vue can skip re-rendering unchanged items."
3373
+ });
3374
+ });
3375
+ return { diagnostics };
3376
+ }
3377
+ //#endregion
3378
+ //#region src/template/rules/no-inline-object-prop-in-list.ts
3379
+ const NODE_DIRECTIVE$3 = 7;
3380
+ function checkElement$2(el, inVForSubtree, ctx, diagnostics) {
3381
+ const isVFor = el.props.some((p) => p.type === NODE_DIRECTIVE$3 && p.name === "for");
3382
+ const effectiveInVFor = inVForSubtree || isVFor;
3383
+ if (effectiveInVFor) for (const prop of el.props) {
3384
+ if (prop.type !== NODE_DIRECTIVE$3) continue;
3385
+ const dir = prop;
3386
+ if (dir.name !== "bind") continue;
3387
+ const attrName = dir.arg?.content;
3388
+ if (!attrName || attrName === "key") continue;
3389
+ const astType = dir.exp?.ast?.type;
3390
+ if (astType !== "ObjectExpression" && astType !== "ArrayExpression") continue;
3391
+ diagnostics.push({
3392
+ file: ctx.file,
3393
+ line: dir.loc.start.line,
3394
+ column: dir.loc.start.column,
3395
+ endLine: dir.loc.end.line,
3396
+ endColumn: dir.loc.end.column,
3397
+ ruleId: "vue-doctor/template/no-inline-object-prop-in-list",
3398
+ severity: "warn",
3399
+ message: `<${el.tag}> has an inline ${attrName} prop with an object or array literal inside a v-for subtree. Inline objects and arrays create new references on every render, defeating v-for's ability to reuse DOM nodes.`,
3400
+ source: "template",
3401
+ recommendation: "Move the object or array to a computed property or a module-level constant so the reference remains stable across renders."
3402
+ });
3403
+ }
3404
+ for (const child of el.children) if (child.type === 1) checkElement$2(child, effectiveInVFor, ctx, diagnostics);
3405
+ }
3406
+ function check$14(ctx) {
3407
+ const diagnostics = [];
3408
+ function walk(node, inVForSubtree) {
3409
+ if (node.type === 1) checkElement$2(node, inVForSubtree, ctx, diagnostics);
3410
+ else if (node.type === 0) {
3411
+ const root = node;
3412
+ for (const child of root.children) walk(child, inVForSubtree);
3413
+ }
3414
+ }
3415
+ walk(ctx.template, false);
3416
+ return { diagnostics };
3417
+ }
3418
+ //#endregion
3419
+ //#region src/template/rules/no-computed-getter-in-template-loop.ts
3420
+ const NODE_ELEMENT$1 = 1;
3421
+ const NODE_INTERPOLATION = 5;
3422
+ const NODE_DIRECTIVE$2 = 7;
3423
+ function isValueDeref(node) {
3424
+ if (node.type !== "MemberExpression" || node.computed === true) return false;
3425
+ const { object, property } = node;
3426
+ return object.type === "Identifier" && property.name === "value";
3427
+ }
3428
+ function containsValueDeref(value) {
3429
+ if (value === null || typeof value !== "object") return false;
3430
+ if (Array.isArray(value)) return value.some((item) => containsValueDeref(item));
3431
+ const node = value;
3432
+ return isValueDeref(node) || Object.values(node).some((child) => containsValueDeref(child));
3433
+ }
3434
+ function pushDiagnostic(el, loc, ctx, diagnostics) {
3435
+ diagnostics.push({
3436
+ file: ctx.file,
3437
+ line: loc.start.line,
3438
+ column: loc.start.column,
3439
+ endLine: loc.end.line,
3440
+ endColumn: loc.end.column,
3441
+ ruleId: "vue-doctor/template/no-computed-getter-in-template-loop",
3874
3442
  severity: "warn",
3875
- category: "ai-slop",
3876
- source: "doctor",
3877
- recommended: true
3878
- },
3443
+ message: `<${el.tag}> reads a ref or computed via .value inside a v-for subtree. The getter re-runs on every item and every render, which is easy to hoist out of the loop.`,
3444
+ source: "template",
3445
+ recommendation: "Read .value once into a computed property or a local binding outside the loop so the getter runs a single time per render."
3446
+ });
3447
+ }
3448
+ function checkElement$1(el, inVForSubtree, ctx, diagnostics) {
3449
+ const isVFor = el.props.some((p) => p.type === NODE_DIRECTIVE$2 && p.name === "for");
3450
+ const effectiveInVFor = inVForSubtree || isVFor;
3451
+ if (effectiveInVFor) {
3452
+ for (const prop of el.props) {
3453
+ if (prop.type !== NODE_DIRECTIVE$2) continue;
3454
+ const dir = prop;
3455
+ if (dir.name !== "bind") continue;
3456
+ if (containsValueDeref(dir.exp?.ast)) pushDiagnostic(el, dir.loc, ctx, diagnostics);
3457
+ }
3458
+ for (const child of el.children) if (child.type === NODE_INTERPOLATION) {
3459
+ const interp = child;
3460
+ if (containsValueDeref(interp.content.ast)) pushDiagnostic(el, interp.loc, ctx, diagnostics);
3461
+ }
3462
+ }
3463
+ for (const child of el.children) if (child.type === NODE_ELEMENT$1) checkElement$1(child, effectiveInVFor, ctx, diagnostics);
3464
+ }
3465
+ function check$13(ctx) {
3466
+ const diagnostics = [];
3467
+ function walk(node, inVForSubtree) {
3468
+ if (node.type === NODE_ELEMENT$1) checkElement$1(node, inVForSubtree, ctx, diagnostics);
3469
+ else if (node.type === 0) {
3470
+ const root = node;
3471
+ for (const child of root.children) walk(child, inVForSubtree);
3472
+ }
3473
+ }
3474
+ walk(ctx.template, false);
3475
+ return { diagnostics };
3476
+ }
3477
+ //#endregion
3478
+ //#region src/template/rules/avoid-deep-v-bind-spread-in-list.ts
3479
+ const NODE_ELEMENT = 1;
3480
+ const NODE_DIRECTIVE$1 = 7;
3481
+ function isIdentifierSpread(dir) {
3482
+ return dir.name === "bind" && dir.arg === void 0 && dir.exp?.ast === null;
3483
+ }
3484
+ function checkElement(el, inVForSubtree, ctx, diagnostics) {
3485
+ const isVFor = el.props.some((p) => p.type === NODE_DIRECTIVE$1 && p.name === "for");
3486
+ const effectiveInVFor = inVForSubtree || isVFor;
3487
+ if (effectiveInVFor) for (const prop of el.props) {
3488
+ if (prop.type !== NODE_DIRECTIVE$1) continue;
3489
+ const dir = prop;
3490
+ if (!isIdentifierSpread(dir)) continue;
3491
+ diagnostics.push({
3492
+ file: ctx.file,
3493
+ line: dir.loc.start.line,
3494
+ column: dir.loc.start.column,
3495
+ endLine: dir.loc.end.line,
3496
+ endColumn: dir.loc.end.column,
3497
+ ruleId: "vue-doctor/template/avoid-deep-v-bind-spread-in-list",
3498
+ severity: "info",
3499
+ message: `<${el.tag}> spreads a whole object via v-bind inside a v-for subtree. Spreading a reactive object binds every property per item, which can churn props and defeat patching on large lists.`,
3500
+ source: "template",
3501
+ recommendation: "Bind only the props each item needs explicitly, or hoist a stable per-item object so Vue can patch instead of re-binding the full spread."
3502
+ });
3503
+ }
3504
+ for (const child of el.children) if (child.type === NODE_ELEMENT) checkElement(child, effectiveInVFor, ctx, diagnostics);
3505
+ }
3506
+ function check$12(ctx) {
3507
+ const diagnostics = [];
3508
+ function walk(node, inVForSubtree) {
3509
+ if (node.type === NODE_ELEMENT) checkElement(node, inVForSubtree, ctx, diagnostics);
3510
+ else if (node.type === 0) {
3511
+ const root = node;
3512
+ for (const child of root.children) walk(child, inVForSubtree);
3513
+ }
3514
+ }
3515
+ walk(ctx.template, false);
3516
+ return { diagnostics };
3517
+ }
3518
+ //#endregion
3519
+ //#region src/template/rules/no-random-key.ts
3520
+ const UNSTABLE_KEY_CALL = /\b(?:Math\.random|Date\.now|performance\.now|crypto\.randomUUID|uuid|uuidv4|nanoid|uniqueId|cuid|ulid)\s*\(/;
3521
+ function keyExpression(el) {
3522
+ const bind = findBindAttr(el, "key");
3523
+ if (!bind) return void 0;
3524
+ return bind;
3525
+ }
3526
+ function unstableReason(keyContent, indexVar) {
3527
+ if (UNSTABLE_KEY_CALL.test(keyContent)) return "a value that changes on every render (e.g. Math.random(), Date.now(), or a fresh uuid)";
3528
+ if (indexVar !== void 0 && keyContent.trim() === indexVar) return "the v-for index, which is unstable when the list is reordered, filtered, or spliced";
3529
+ }
3530
+ function check$11(ctx) {
3531
+ const diagnostics = [];
3532
+ walkElements(ctx.template, (el) => {
3533
+ const vFor = findDirective(el, "for");
3534
+ if (!vFor) return;
3535
+ const bind = keyExpression(el);
3536
+ const keyContent = bind?.exp?.content;
3537
+ if (!bind || keyContent === void 0) return;
3538
+ const reason = unstableReason(keyContent, vFor.forParseResult?.key?.content ?? void 0);
3539
+ if (reason === void 0) return;
3540
+ diagnostics.push({
3541
+ file: ctx.file,
3542
+ line: bind.loc.start.line,
3543
+ column: bind.loc.start.column,
3544
+ endLine: bind.loc.end.line,
3545
+ endColumn: bind.loc.end.column,
3546
+ ruleId: "vue-doctor/template/no-random-key",
3547
+ severity: "warn",
3548
+ message: `<${el.tag}> uses ${reason} as its v-for :key. Unstable keys defeat Vue's DOM reuse and force a full re-mount of each item, losing component state and hurting performance.`,
3549
+ source: "template",
3550
+ recommendation: "Use a stable, unique id from the item itself (e.g. :key=\"item.id\") so Vue can reuse DOM nodes across renders."
3551
+ });
3552
+ });
3553
+ return { diagnostics };
3554
+ }
3555
+ //#endregion
3556
+ //#region src/template/rules/no-v-memo-in-vapor.ts
3557
+ function check$10(ctx) {
3558
+ const diagnostics = [];
3559
+ walkElements(ctx.template, (el) => {
3560
+ const vMemo = findDirective(el, "memo");
3561
+ if (!vMemo) return;
3562
+ diagnostics.push({
3563
+ file: ctx.file,
3564
+ line: vMemo.loc.start.line,
3565
+ column: vMemo.loc.start.column,
3566
+ endLine: vMemo.loc.end.line,
3567
+ endColumn: vMemo.loc.end.column,
3568
+ ruleId: "vue-doctor/template/no-v-memo-in-vapor",
3569
+ severity: "warn",
3570
+ message: `<${el.tag}> uses v-memo in a Vapor-mode component. Vapor compiles to fine-grained DOM updates with no virtual-DOM diff, so v-memo is a no-op here and only adds noise.`,
3571
+ source: "template",
3572
+ recommendation: "Remove v-memo from this Vapor component; fine-grained reactivity already avoids re-rendering unchanged nodes."
3573
+ });
3574
+ });
3575
+ return { diagnostics };
3576
+ }
3577
+ //#endregion
3578
+ //#region src/template/rules/no-v-html.ts
3579
+ function check$9(ctx) {
3580
+ const diagnostics = [];
3581
+ walkElements(ctx.template, (el) => {
3582
+ const vHtml = findDirective(el, "html");
3583
+ if (!vHtml) return;
3584
+ diagnostics.push({
3585
+ file: ctx.file,
3586
+ line: vHtml.loc.start.line,
3587
+ column: vHtml.loc.start.column,
3588
+ endLine: vHtml.loc.end.line,
3589
+ endColumn: vHtml.loc.end.column,
3590
+ ruleId: "vue-doctor/security/no-v-html",
3591
+ severity: "error",
3592
+ message: `<${el.tag}> uses v-html, which renders raw HTML and bypasses Vue's escaping — the primary XSS sink in Vue apps.`,
3593
+ source: "template",
3594
+ recommendation: "Render text with {{ }} interpolation, or sanitize the HTML with DOMPurify in a computed before binding it."
3595
+ });
3596
+ });
3597
+ return { diagnostics };
3598
+ }
3599
+ //#endregion
3600
+ //#region src/template/rules/no-target-blank-without-rel.ts
3601
+ function unquote$1(raw) {
3602
+ const trimmed = raw.trim();
3603
+ const first = trimmed.charAt(0);
3604
+ if ((first === "\"" || first === "'") && trimmed.endsWith(first)) return trimmed.slice(1, -1);
3605
+ return trimmed;
3606
+ }
3607
+ function attrValue(el, name) {
3608
+ const staticAttr = findStaticAttr(el, name);
3609
+ if (staticAttr) return {
3610
+ value: staticAttr.value?.content ?? "",
3611
+ loc: staticAttr.loc
3612
+ };
3613
+ const bound = findBindAttr(el, name);
3614
+ if (bound?.exp && "content" in bound.exp) return {
3615
+ value: unquote$1(bound.exp.content),
3616
+ loc: bound.loc
3617
+ };
3618
+ }
3619
+ function check$8(ctx) {
3620
+ const diagnostics = [];
3621
+ walkElements(ctx.template, (el) => {
3622
+ if (el.tag !== "a") return;
3623
+ const target = attrValue(el, "target");
3624
+ if (!target || target.value !== "_blank") return;
3625
+ const rel = attrValue(el, "rel");
3626
+ if (rel && rel.value.includes("noopener")) return;
3627
+ diagnostics.push({
3628
+ file: ctx.file,
3629
+ line: target.loc.start.line,
3630
+ column: target.loc.start.column,
3631
+ endLine: target.loc.end.line,
3632
+ endColumn: target.loc.end.column,
3633
+ ruleId: "vue-doctor/security/no-target-blank-without-rel",
3634
+ severity: "warn",
3635
+ message: `<a target="_blank"> without rel="noopener noreferrer" exposes the opener window to reverse-tabnabbing.`,
3636
+ source: "template",
3637
+ recommendation: "Add rel=\"noopener noreferrer\" to anchors that open in a new tab."
3638
+ });
3639
+ });
3640
+ return { diagnostics };
3641
+ }
3642
+ //#endregion
3643
+ //#region src/template/rules/no-javascript-uri.ts
3644
+ const NODE_ATTRIBUTE$1 = 6;
3645
+ const URI_SINKS = new Set([
3646
+ "href",
3647
+ "src",
3648
+ "srcset",
3649
+ "action",
3650
+ "xlink:href",
3651
+ "formaction",
3652
+ "poster",
3653
+ "data"
3654
+ ]);
3655
+ const DANGEROUS_SCHEME = /^\s*(?:javascript:|vbscript:|data:text\/html)/i;
3656
+ function unquote(raw) {
3657
+ const trimmed = raw.trim();
3658
+ const first = trimmed.charAt(0);
3659
+ if ((first === "\"" || first === "'" || first === "`") && trimmed.endsWith(first)) return trimmed.slice(1, -1);
3660
+ return trimmed;
3661
+ }
3662
+ function resolveUriProp(prop) {
3663
+ if (prop.type === NODE_ATTRIBUTE$1) {
3664
+ const attr = prop;
3665
+ if (attr.value === void 0) return void 0;
3666
+ return {
3667
+ name: attr.name,
3668
+ value: attr.value.content,
3669
+ loc: attr.loc
3670
+ };
3671
+ }
3672
+ const dir = prop;
3673
+ if (dir.name !== "bind") return void 0;
3674
+ if (!dir.arg || !("content" in dir.arg)) return void 0;
3675
+ if (!dir.exp || !("content" in dir.exp)) return void 0;
3676
+ return {
3677
+ name: dir.arg.content,
3678
+ value: unquote(dir.exp.content),
3679
+ loc: dir.loc
3680
+ };
3681
+ }
3682
+ function check$7(ctx) {
3683
+ const diagnostics = [];
3684
+ walkElements(ctx.template, (el) => {
3685
+ for (const prop of el.props) {
3686
+ const resolved = resolveUriProp(prop);
3687
+ if (!resolved) continue;
3688
+ if (!URI_SINKS.has(resolved.name)) continue;
3689
+ if (!DANGEROUS_SCHEME.test(resolved.value)) continue;
3690
+ diagnostics.push({
3691
+ file: ctx.file,
3692
+ line: resolved.loc.start.line,
3693
+ column: resolved.loc.start.column,
3694
+ endLine: resolved.loc.end.line,
3695
+ endColumn: resolved.loc.end.column,
3696
+ ruleId: "vue-doctor/security/no-javascript-uri",
3697
+ severity: "error",
3698
+ message: `<${el.tag}> binds ${resolved.name} to a dangerous URI scheme (javascript:/vbscript:/data:text/html), which executes arbitrary code.`,
3699
+ source: "template",
3700
+ recommendation: "Use a safe http(s) or relative URL; never put a javascript:, vbscript:, or data:text/html value in a URL attribute."
3701
+ });
3702
+ }
3703
+ });
3704
+ return { diagnostics };
3705
+ }
3706
+ //#endregion
3707
+ //#region src/template/class-attr-helpers.ts
3708
+ const NODE_ATTRIBUTE = 6;
3709
+ const NODE_DIRECTIVE = 7;
3710
+ /**
3711
+ * Collect the scannable text of every `class` source on an element: the static
3712
+ * `class="..."` value and any bound `:class` expression source. Bound
3713
+ * expressions are returned verbatim so a regex can match string-literal class
3714
+ * fragments inside ternaries, arrays, and objects without a full JS parse.
3715
+ */
3716
+ function collectClassSources(el) {
3717
+ const sources = [];
3718
+ for (const prop of el.props) {
3719
+ if (prop.type === NODE_ATTRIBUTE) {
3720
+ const attr = prop;
3721
+ if (attr.name === "class" && attr.value) sources.push({
3722
+ text: attr.value.content,
3723
+ loc: attr.loc
3724
+ });
3725
+ }
3726
+ if (prop.type === NODE_DIRECTIVE) {
3727
+ const dir = prop;
3728
+ if (dir.name === "bind" && dir.arg && "content" in dir.arg && dir.arg.content === "class" && dir.exp && "content" in dir.exp) sources.push({
3729
+ text: dir.exp.content,
3730
+ loc: dir.loc
3731
+ });
3732
+ }
3733
+ }
3734
+ return sources;
3735
+ }
3736
+ /** Find a static `style="..."` attribute on an element, if present. */
3737
+ function findStaticStyle(el) {
3738
+ for (const prop of el.props) if (prop.type === NODE_ATTRIBUTE && prop.name === "style") return prop;
3739
+ }
3740
+ //#endregion
3741
+ //#region src/template/rules/no-arbitrary-tailwind-values.ts
3742
+ const ARBITRARY_VALUE = /\[[^\]]+\]/;
3743
+ function check$6(ctx) {
3744
+ const diagnostics = [];
3745
+ walkElements(ctx.template, (el) => {
3746
+ for (const source of collectClassSources(el)) {
3747
+ if (!ARBITRARY_VALUE.test(source.text)) continue;
3748
+ diagnostics.push({
3749
+ file: ctx.file,
3750
+ line: source.loc.start.line,
3751
+ column: source.loc.start.column,
3752
+ endLine: source.loc.end.line,
3753
+ endColumn: source.loc.end.column,
3754
+ ruleId: "vue-doctor/design/no-arbitrary-tailwind-values",
3755
+ severity: "warn",
3756
+ message: `<${el.tag}> uses a Tailwind arbitrary value (e.g. w-[412px]). Arbitrary values are design-system debt; repeated values should become @theme tokens.`,
3757
+ source: "template",
3758
+ recommendation: "Replace the arbitrary value with a design token defined in your @theme (e.g. w-container-lg)."
3759
+ });
3760
+ }
3761
+ });
3762
+ return { diagnostics };
3763
+ }
3764
+ //#endregion
3765
+ //#region src/template/rules/no-raw-hex-color.ts
3766
+ const HEX_COLOR = /#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/;
3767
+ const RULE_ID$1 = "vue-doctor/design/no-raw-hex-color";
3768
+ const MESSAGE$1 = "Raw hex color bypasses the token system and breaks theming. Use a semantic color token instead.";
3769
+ const RECOMMENDATION$1 = "Replace the hex color with a design token (e.g. bg-danger / text-success) defined in your @theme.";
3770
+ function check$5(ctx) {
3771
+ const diagnostics = [];
3772
+ walkElements(ctx.template, (el) => {
3773
+ for (const source of collectClassSources(el)) {
3774
+ if (!HEX_COLOR.test(source.text)) continue;
3775
+ diagnostics.push({
3776
+ file: ctx.file,
3777
+ line: source.loc.start.line,
3778
+ column: source.loc.start.column,
3779
+ endLine: source.loc.end.line,
3780
+ endColumn: source.loc.end.column,
3781
+ ruleId: RULE_ID$1,
3782
+ severity: "warn",
3783
+ message: `<${el.tag}> embeds a raw hex color in a class. ${MESSAGE$1}`,
3784
+ source: "template",
3785
+ recommendation: RECOMMENDATION$1
3786
+ });
3787
+ }
3788
+ const style = findStaticStyle(el);
3789
+ if (style?.value && HEX_COLOR.test(style.value.content)) diagnostics.push({
3790
+ file: ctx.file,
3791
+ line: style.loc.start.line,
3792
+ column: style.loc.start.column,
3793
+ endLine: style.loc.end.line,
3794
+ endColumn: style.loc.end.column,
3795
+ ruleId: RULE_ID$1,
3796
+ severity: "warn",
3797
+ message: `<${el.tag}> embeds a raw hex color in an inline style. ${MESSAGE$1}`,
3798
+ source: "template",
3799
+ recommendation: RECOMMENDATION$1
3800
+ });
3801
+ });
3802
+ return { diagnostics };
3803
+ }
3804
+ //#endregion
3805
+ //#region src/template/rules/no-default-tailwind-palette.ts
3806
+ 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/;
3807
+ function check$4(ctx) {
3808
+ const diagnostics = [];
3809
+ walkElements(ctx.template, (el) => {
3810
+ for (const source of collectClassSources(el)) {
3811
+ if (!DEFAULT_PALETTE.test(source.text)) continue;
3812
+ diagnostics.push({
3813
+ file: ctx.file,
3814
+ line: source.loc.start.line,
3815
+ column: source.loc.start.column,
3816
+ endLine: source.loc.end.line,
3817
+ endColumn: source.loc.end.column,
3818
+ ruleId: "vue-doctor/design/no-default-tailwind-palette",
3819
+ severity: "warn",
3820
+ 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.`,
3821
+ source: "template",
3822
+ recommendation: "Map the color to a brand token (e.g. bg-primary-600) defined in your @theme instead of the default palette."
3823
+ });
3824
+ }
3825
+ });
3826
+ return { diagnostics };
3827
+ }
3828
+ //#endregion
3829
+ //#region src/template/rules/no-important-utility.ts
3830
+ const IMPORTANT_UTILITY = /(?:^|[\s:'"`])!-?[a-z][a-z0-9-]*/;
3831
+ function check$3(ctx) {
3832
+ const diagnostics = [];
3833
+ walkElements(ctx.template, (el) => {
3834
+ for (const source of collectClassSources(el)) {
3835
+ if (!IMPORTANT_UTILITY.test(source.text)) continue;
3836
+ diagnostics.push({
3837
+ file: ctx.file,
3838
+ line: source.loc.start.line,
3839
+ column: source.loc.start.column,
3840
+ endLine: source.loc.end.line,
3841
+ endColumn: source.loc.end.column,
3842
+ ruleId: "vue-doctor/design/no-important-utility",
3843
+ severity: "warn",
3844
+ message: `<${el.tag}> uses a !important Tailwind utility (e.g. !bg-red-500). !important is a specificity escape hatch that makes maintenance harder.`,
3845
+ source: "template",
3846
+ recommendation: "Remove the ! prefix and resolve the specificity conflict at its source instead of forcing !important."
3847
+ });
3848
+ }
3849
+ });
3850
+ return { diagnostics };
3851
+ }
3852
+ //#endregion
3853
+ //#region src/template/rules/no-hardcoded-inline-style.ts
3854
+ const HARDCODED_VALUE = /\d+px\b|#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\b/;
3855
+ function check$2(ctx) {
3856
+ const diagnostics = [];
3857
+ walkElements(ctx.template, (el) => {
3858
+ const style = findStaticStyle(el);
3859
+ if (!style?.value) return;
3860
+ if (!HARDCODED_VALUE.test(style.value.content)) return;
3861
+ diagnostics.push({
3862
+ file: ctx.file,
3863
+ line: style.loc.start.line,
3864
+ column: style.loc.start.column,
3865
+ endLine: style.loc.end.line,
3866
+ endColumn: style.loc.end.column,
3867
+ ruleId: "vue-doctor/design/no-hardcoded-inline-style",
3868
+ severity: "warn",
3869
+ message: `<${el.tag}> uses an inline style with hardcoded px or hex values. Inline styles bypass tokens, hurt caching, and create drift.`,
3870
+ source: "template",
3871
+ recommendation: "Move the styling to token-based utility classes (e.g. w-60 text-neutral-700) instead of an inline style."
3872
+ });
3873
+ });
3874
+ return { diagnostics };
3875
+ }
3876
+ //#endregion
3877
+ //#region src/template/rules/no-missing-alt.ts
3878
+ const IMAGE_TAGS = new Set([
3879
+ "img",
3880
+ "NuxtImg",
3881
+ "nuxt-img"
3882
+ ]);
3883
+ function check$1(ctx) {
3884
+ const diagnostics = [];
3885
+ walkElements(ctx.template, (el) => {
3886
+ if (!IMAGE_TAGS.has(el.tag)) return;
3887
+ if (findStaticAttr(el, "alt") ?? findBindAttr(el, "alt")) return;
3888
+ diagnostics.push({
3889
+ file: ctx.file,
3890
+ line: el.loc.start.line,
3891
+ column: el.loc.start.column,
3892
+ endLine: el.loc.end.line,
3893
+ endColumn: el.loc.end.column,
3894
+ ruleId: "vue-doctor/design/no-missing-alt",
3895
+ severity: "warn",
3896
+ message: `<${el.tag}> has no alt attribute. Screen readers and SEO depend on descriptive alt text.`,
3897
+ source: "template",
3898
+ recommendation: "Add an alt attribute describing the image, or alt=\"\" for purely decorative images."
3899
+ });
3900
+ });
3901
+ return { diagnostics };
3902
+ }
3903
+ //#endregion
3904
+ //#region src/template/rules/no-absurd-z-index.ts
3905
+ const ABSURD_THRESHOLD = 1e3;
3906
+ const ARBITRARY_Z = /(?:^|[\s:'"`])-?z-\[(\d+)\]/;
3907
+ const INLINE_Z = /z-index\s*:\s*(\d+)/;
3908
+ const RULE_ID = "vue-doctor/design/no-absurd-z-index";
3909
+ const MESSAGE = "huge z-index values usually paper over layering bugs instead of fixing stacking context.";
3910
+ const RECOMMENDATION = "Use a small, tokenized z-index scale (e.g. z-modal) and fix the underlying stacking context.";
3911
+ function check(ctx) {
3912
+ const diagnostics = [];
3913
+ walkElements(ctx.template, (el) => {
3914
+ for (const source of collectClassSources(el)) {
3915
+ const match = ARBITRARY_Z.exec(source.text);
3916
+ if (!match || Number(match[1]) < ABSURD_THRESHOLD) continue;
3917
+ diagnostics.push({
3918
+ file: ctx.file,
3919
+ line: source.loc.start.line,
3920
+ column: source.loc.start.column,
3921
+ endLine: source.loc.end.line,
3922
+ endColumn: source.loc.end.column,
3923
+ ruleId: RULE_ID,
3924
+ severity: "warn",
3925
+ message: `<${el.tag}> uses an absurd z-index class (>= ${ABSURD_THRESHOLD}); ${MESSAGE}`,
3926
+ source: "template",
3927
+ recommendation: RECOMMENDATION
3928
+ });
3929
+ }
3930
+ const style = findStaticStyle(el);
3931
+ if (style?.value) {
3932
+ const match = INLINE_Z.exec(style.value.content);
3933
+ if (match && Number(match[1]) >= ABSURD_THRESHOLD) diagnostics.push({
3934
+ file: ctx.file,
3935
+ line: style.loc.start.line,
3936
+ column: style.loc.start.column,
3937
+ endLine: style.loc.end.line,
3938
+ endColumn: style.loc.end.column,
3939
+ ruleId: RULE_ID,
3940
+ severity: "warn",
3941
+ message: `<${el.tag}> uses an absurd inline z-index (>= ${ABSURD_THRESHOLD}); ${MESSAGE}`,
3942
+ source: "template",
3943
+ recommendation: RECOMMENDATION
3944
+ });
3945
+ }
3946
+ });
3947
+ return { diagnostics };
3948
+ }
3949
+ //#endregion
3950
+ //#region src/template/rules/index.ts
3951
+ const TEMPLATE_RULES = [
3879
3952
  {
3880
- id: "nuxt-doctor/ai-slop/no-useState-for-server-data",
3881
- severity: "warn",
3882
- category: "ai-slop",
3883
- source: "doctor",
3884
- recommended: true
3953
+ id: "vue-doctor/template/v-for-has-key",
3954
+ check: check$17
3885
3955
  },
3886
3956
  {
3887
- id: "nuxt-doctor/ai-slop/no-fetch-in-setup",
3888
- severity: "warn",
3889
- category: "ai-slop",
3890
- source: "doctor",
3891
- recommended: true
3957
+ id: "vue-doctor/template/v-if-v-for-precedence",
3958
+ check: check$16
3892
3959
  },
3893
3960
  {
3894
- id: "nuxt-doctor/data-fetching/useAsyncData-key-required-in-loop",
3895
- severity: "error",
3896
- category: "data-fetching",
3897
- source: "doctor",
3898
- recommended: true
3961
+ id: "vue-doctor/template/v-memo-on-large-list",
3962
+ check: check$15,
3963
+ disabledBy: ["vue:vapor"]
3899
3964
  },
3900
3965
  {
3901
- id: "nuxt-doctor/server-routes/defineEventHandler-typed",
3902
- severity: "warn",
3903
- category: "server-routes",
3904
- source: "doctor",
3905
- recommended: true
3966
+ id: "vue-doctor/template/no-inline-object-prop-in-list",
3967
+ check: check$14
3906
3968
  },
3907
3969
  {
3908
- id: "nuxt-doctor/server-routes/validate-body-with-h3-v2",
3909
- severity: "warn",
3910
- category: "server-routes",
3911
- source: "doctor",
3912
- recommended: true
3970
+ id: "vue-doctor/template/no-computed-getter-in-template-loop",
3971
+ check: check$13
3913
3972
  },
3914
3973
  {
3915
- id: "nuxt-doctor/server-routes/createError-on-failure",
3916
- severity: "warn",
3917
- category: "server-routes",
3918
- source: "doctor",
3919
- recommended: true
3974
+ id: "vue-doctor/template/avoid-deep-v-bind-spread-in-list",
3975
+ check: check$12
3920
3976
  },
3921
3977
  {
3922
- id: "nuxt-doctor/hydration/no-document-in-setup",
3923
- severity: "error",
3924
- category: "hydration",
3925
- source: "doctor",
3926
- recommended: true
3978
+ id: "vue-doctor/template/no-random-key",
3979
+ check: check$11
3927
3980
  },
3928
3981
  {
3929
- id: "nuxt-doctor/hydration/clientOnly-for-browser-apis",
3930
- severity: "error",
3931
- category: "hydration",
3932
- source: "doctor",
3933
- recommended: true
3982
+ id: "vue-doctor/template/no-v-memo-in-vapor",
3983
+ check: check$10,
3984
+ requires: ["vue:vapor"]
3934
3985
  },
3935
3986
  {
3936
3987
  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
3988
+ check: check$9
3969
3989
  },
3970
3990
  {
3971
3991
  id: "vue-doctor/security/no-target-blank-without-rel",
3972
- severity: "warn",
3973
- category: "security",
3974
- source: "doctor",
3975
- recommended: true
3992
+ check: check$8
3976
3993
  },
3977
3994
  {
3978
3995
  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
3996
+ check: check$7
3997
3997
  },
3998
3998
  {
3999
3999
  id: "vue-doctor/design/no-arbitrary-tailwind-values",
4000
- severity: "warn",
4001
- category: "design",
4002
- source: "doctor",
4003
- recommended: true
4000
+ check: check$6
4004
4001
  },
4005
4002
  {
4006
4003
  id: "vue-doctor/design/no-raw-hex-color",
4007
- severity: "warn",
4008
- category: "design",
4009
- source: "doctor",
4010
- recommended: true
4004
+ check: check$5
4011
4005
  },
4012
4006
  {
4013
4007
  id: "vue-doctor/design/no-default-tailwind-palette",
4014
- severity: "warn",
4015
- category: "design",
4016
- source: "doctor",
4017
- recommended: true
4008
+ check: check$4
4018
4009
  },
4019
4010
  {
4020
4011
  id: "vue-doctor/design/no-important-utility",
4021
- severity: "warn",
4022
- category: "design",
4023
- source: "doctor",
4024
- recommended: true
4012
+ check: check$3
4025
4013
  },
4026
4014
  {
4027
4015
  id: "vue-doctor/design/no-hardcoded-inline-style",
4028
- severity: "warn",
4029
- category: "design",
4030
- source: "doctor",
4031
- recommended: true
4016
+ check: check$2
4032
4017
  },
4033
4018
  {
4034
4019
  id: "vue-doctor/design/no-missing-alt",
4035
- severity: "warn",
4036
- category: "design",
4037
- source: "doctor",
4038
- recommended: true
4020
+ check: check$1
4039
4021
  },
4040
4022
  {
4041
4023
  id: "vue-doctor/design/no-absurd-z-index",
4042
- severity: "warn",
4043
- category: "design",
4044
- source: "doctor",
4045
- recommended: true
4024
+ check
4025
+ }
4026
+ ];
4027
+ //#endregion
4028
+ //#region src/template/vapor.ts
4029
+ const VAPOR_CAPABILITY = "vue:vapor";
4030
+ /**
4031
+ * Detect whether an SFC opts into Vue Vapor mode. Vapor is enabled per-block
4032
+ * via a boolean `vapor` attribute on `<script setup vapor>` or `<template vapor>`
4033
+ * (Vue 3.6+). A string value (e.g. `vapor="false"`) does not enable it.
4034
+ */
4035
+ function isVaporSfc(descriptor) {
4036
+ if (descriptor.scriptSetup?.attrs?.["vapor"] === true) return true;
4037
+ if (descriptor.template?.attrs?.["vapor"] === true) return true;
4038
+ return false;
4039
+ }
4040
+ //#endregion
4041
+ //#region src/template/run.ts
4042
+ function capabilityGated(rule, caps) {
4043
+ if (rule.disabledBy?.some((c) => caps.has(c))) return true;
4044
+ if (rule.requires?.some((c) => !caps.has(c))) return true;
4045
+ return false;
4046
+ }
4047
+ async function runTemplatePass(opts) {
4048
+ const all = [];
4049
+ for (const file of opts.files) {
4050
+ if (!file.endsWith(".vue")) continue;
4051
+ const descriptor = await parseSfc(file);
4052
+ if (!descriptor?.template?.ast) continue;
4053
+ const caps = /* @__PURE__ */ new Set();
4054
+ if (isVaporSfc(descriptor)) caps.add(VAPOR_CAPABILITY);
4055
+ for (const rule of TEMPLATE_RULES) {
4056
+ const override = opts.ruleOverrides?.[rule.id];
4057
+ if (override === "off") continue;
4058
+ if (capabilityGated(rule, caps)) continue;
4059
+ const { diagnostics } = rule.check({
4060
+ file,
4061
+ template: descriptor.template.ast,
4062
+ script: descriptor.scriptSetup?.content ?? descriptor.script?.content
4063
+ });
4064
+ for (const d of diagnostics) all.push(override ? {
4065
+ ...d,
4066
+ severity: override
4067
+ } : d);
4068
+ }
4046
4069
  }
4070
+ return all;
4071
+ }
4072
+ //#endregion
4073
+ //#region src/audit.ts
4074
+ const DEFAULT_INCLUDE = [
4075
+ "**/*.vue",
4076
+ "**/*.ts",
4077
+ "**/*.tsx",
4078
+ "**/*.js",
4079
+ "**/*.jsx"
4047
4080
  ];
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));
4081
+ const DEFAULT_EXCLUDE = [
4082
+ "node_modules",
4083
+ "dist",
4084
+ ".nuxt",
4085
+ ".output",
4086
+ "coverage"
4087
+ ];
4088
+ function countRuleCounts(diagnostics) {
4089
+ const counts = {};
4090
+ for (const d of diagnostics) counts[d.ruleId] = (counts[d.ruleId] ?? 0) + 1;
4091
+ return counts;
4092
+ }
4093
+ async function audit(config = {}) {
4094
+ const rootDir = resolve(config.rootDir ?? process.cwd());
4095
+ const include = config.include ?? DEFAULT_INCLUDE;
4096
+ const exclude = config.exclude ?? DEFAULT_EXCLUDE;
4097
+ const failOn = config.failOn ?? "error";
4098
+ const threshold = config.threshold ?? 0;
4099
+ const lintEnabled = config.lint !== false;
4100
+ const files = await listSourceFiles({
4101
+ rootDir,
4102
+ include,
4103
+ exclude
4104
+ });
4105
+ const overallStart = performance.now();
4106
+ const project = await detectProject(rootDir);
4107
+ const templateStart = performance.now();
4108
+ const templateDiagnostics = lintEnabled ? await runTemplatePass({
4109
+ files,
4110
+ ruleOverrides: config.rules
4111
+ }) : [];
4112
+ const templateElapsed = performance.now() - templateStart;
4113
+ const sfcStart = performance.now();
4114
+ const sfcDiagnostics = lintEnabled ? await runSfcPass({
4115
+ files,
4116
+ ruleOverrides: config.rules,
4117
+ projectInfo: project
4118
+ }) : [];
4119
+ const sfcElapsed = performance.now() - sfcStart;
4120
+ let scriptDiagnostics = [];
4121
+ let oxlintStderr = "";
4122
+ const scriptStart = performance.now();
4123
+ if (lintEnabled) try {
4124
+ const result = await runScriptPass({
4125
+ rootDir,
4126
+ targetPath: rootDir,
4127
+ ruleOverrides: config.rules,
4128
+ framework: project.framework === "nuxt" ? "nuxt" : "vue",
4129
+ fix: config.fix === true && config.scopeFiles === void 0,
4130
+ fixExcludes: config.fixExcludes
4131
+ });
4132
+ scriptDiagnostics = result.diagnostics;
4133
+ oxlintStderr = result.stderr;
4134
+ } catch (err) {
4135
+ oxlintStderr = err instanceof Error ? err.message : String(err);
4136
+ if (process.env.DOCTOR_DEBUG) process.stderr.write(`[doctor-core] script pass failed: ${oxlintStderr}\n`);
4137
+ }
4138
+ const scriptElapsed = performance.now() - scriptStart;
4139
+ let deadCodeDiagnostics = [];
4140
+ let deadCodeElapsed = 0;
4141
+ if (config.deadCode !== false) {
4142
+ const deadCodeStart = performance.now();
4143
+ try {
4144
+ const { loadDoctorConfig } = await Promise.resolve().then(() => load_exports);
4145
+ deadCodeDiagnostics = dedupeDeadCodeAgainstLint(await checkDeadCode({
4146
+ projectInfo: project,
4147
+ doctorConfig: await loadDoctorConfig(rootDir),
4148
+ enabled: true
4149
+ }), [
4150
+ ...templateDiagnostics,
4151
+ ...sfcDiagnostics,
4152
+ ...scriptDiagnostics
4153
+ ]).filter((d) => config.rules?.[d.ruleId] !== "off");
4154
+ } catch (err) {
4155
+ if (process.env.DOCTOR_DEBUG) process.stderr.write(`[doctor-core] dead-code pass failed: ${String(err)}\n`);
4156
+ }
4157
+ deadCodeElapsed = performance.now() - deadCodeStart;
4158
+ }
4159
+ let buildQualityDiagnostics = [];
4160
+ try {
4161
+ buildQualityDiagnostics = (await checkBuildQuality(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4162
+ const override = config.rules?.[d.ruleId];
4163
+ return override ? {
4164
+ ...d,
4165
+ severity: override
4166
+ } : d;
4167
+ });
4168
+ } catch {}
4169
+ let depsDiagnostics = [];
4170
+ try {
4171
+ depsDiagnostics = (await checkDeps(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4172
+ const override = config.rules?.[d.ruleId];
4173
+ return override ? {
4174
+ ...d,
4175
+ severity: override
4176
+ } : d;
4177
+ });
4178
+ } catch {}
4179
+ let nuxtProjectDiagnostics = [];
4180
+ if (project.framework === "nuxt") try {
4181
+ nuxtProjectDiagnostics = (await checkNuxtProject(project)).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4182
+ const override = config.rules?.[d.ruleId];
4183
+ return override ? {
4184
+ ...d,
4185
+ severity: override
4186
+ } : d;
4187
+ });
4188
+ } catch {}
4189
+ let crossFileDiagnostics = [];
4190
+ if (project.framework === "nuxt") try {
4191
+ crossFileDiagnostics = (await runCrossFilePass({
4192
+ files,
4193
+ projectInfo: project
4194
+ })).filter((d) => config.rules?.[d.ruleId] !== "off").map((d) => {
4195
+ const override = config.rules?.[d.ruleId];
4196
+ return override ? {
4197
+ ...d,
4198
+ severity: override
4199
+ } : d;
4200
+ });
4201
+ } catch {}
4202
+ const elapsedMs = performance.now() - overallStart;
4203
+ const timings = {
4204
+ template: templateElapsed,
4205
+ sfc: sfcElapsed,
4206
+ script: scriptElapsed,
4207
+ deadCode: deadCodeElapsed,
4208
+ total: elapsedMs
4209
+ };
4210
+ let afterDisables = applyInlineDisables(mergeDiagnostics(templateDiagnostics, sfcDiagnostics, scriptDiagnostics, deadCodeDiagnostics, buildQualityDiagnostics, depsDiagnostics, nuxtProjectDiagnostics, crossFileDiagnostics), { respect: config.respectInlineDisables !== false });
4211
+ if (config.scopeFiles) {
4212
+ const scope = new Set(config.scopeFiles.map((f) => resolve(rootDir, f)));
4213
+ afterDisables = afterDisables.filter((d) => scope.has(resolve(rootDir, d.file)));
4214
+ }
4215
+ const diagnostics = await attachCodeSnippets(afterDisables);
4216
+ const scored = scoreDiagnostics(diagnostics, { threshold });
4217
+ const ruleCounts = countRuleCounts(diagnostics);
4218
+ const projectInfo = {
4219
+ framework: project.framework,
4220
+ vueVersion: project.vueVersion,
4221
+ nuxtVersion: project.nuxtVersion,
4222
+ capabilities: [...project.capabilities].sort(),
4223
+ rootDirectory: project.rootDirectory
4224
+ };
4225
+ let exitCode = 0;
4226
+ if (oxlintStderr && scriptDiagnostics.length === 0 && oxlintStderr.includes("Failed")) exitCode = 2;
4227
+ else if (failOn !== "none") {
4228
+ if ((failOn === "warn" ? scored.errorCount + scored.warnCount : scored.errorCount) > 0) exitCode = 1;
4229
+ }
4230
+ return {
4231
+ rootDir,
4232
+ filesScanned: files.length,
4233
+ diagnostics,
4234
+ score: scored.score,
4235
+ errorCount: scored.errorCount,
4236
+ warnCount: scored.warnCount,
4237
+ infoCount: scored.infoCount,
4238
+ exitCode,
4239
+ scoreResult: scored,
4240
+ projectInfo,
4241
+ elapsedMs,
4242
+ timings,
4243
+ ruleCounts
4244
+ };
4245
+ }
4246
+ //#endregion
4247
+ //#region src/config/built-in.ts
4248
+ const BUILT_IN_RECOMMENDED = {
4249
+ include: [
4250
+ "**/*.vue",
4251
+ "**/*.ts",
4252
+ "**/*.tsx",
4253
+ "**/*.js",
4254
+ "**/*.jsx"
4255
+ ],
4256
+ exclude: [
4257
+ "node_modules",
4258
+ "dist",
4259
+ ".nuxt",
4260
+ ".output",
4261
+ "coverage"
4262
+ ],
4263
+ failOn: "error",
4264
+ threshold: 0,
4265
+ rules: {}
4266
+ };
4267
+ //#endregion
4268
+ //#region src/config/errors.ts
4269
+ var ConfigFileNotFoundError = class extends Error {
4270
+ name = "ConfigFileNotFoundError";
4271
+ constructor(path) {
4272
+ super(`Config file not found: ${path}`);
4273
+ }
4274
+ };
4275
+ var ConfigCycleError = class extends Error {
4276
+ name = "ConfigCycleError";
4277
+ constructor(chain) {
4278
+ super(`Config extends cycle detected: ${chain.join(" -> ")}`);
4279
+ }
4280
+ };
4281
+ var InvalidConfigError = class extends Error {
4282
+ name = "InvalidConfigError";
4283
+ issues;
4284
+ constructor(message, issues = []) {
4285
+ super(message);
4286
+ this.issues = issues;
4287
+ }
4288
+ };
4289
+ //#endregion
4290
+ //#region src/config/define-config.ts
4291
+ function defineConfig(config) {
4292
+ return config;
4055
4293
  }
4056
4294
  //#endregion
4057
4295
  //#region src/config/presets.ts
@@ -4802,7 +5040,14 @@ function buildDoctorReport(input) {
4802
5040
  occurrences: entry.occurrences,
4803
5041
  weightPerOccurrence: entry.weightPerOccurrence,
4804
5042
  penalty: entry.penalty
4805
- }))
5043
+ })),
5044
+ byDimension: Object.fromEntries(Object.entries(input.score.byDimension).map(([dim, d]) => [dim, {
5045
+ score: d.score,
5046
+ totalFindings: d.totalFindings,
5047
+ error: d.errorCount,
5048
+ warn: d.warnCount,
5049
+ info: d.infoCount
5050
+ }]))
4806
5051
  },
4807
5052
  diagnostics: input.diagnostics.map((d) => ({
4808
5053
  file: relative(input.rootDirectory, d.file).replaceAll("\\", "/"),
@@ -5147,6 +5392,44 @@ function filterReportByRules(report, allowedRuleIds, failOn = "error", oxlintStd
5147
5392
  };
5148
5393
  }
5149
5394
  //#endregion
5395
+ //#region src/config/category-filter.ts
5396
+ const VALID_CATEGORIES = new Set(RULE_REGISTRY.map((r) => r.category));
5397
+ /**
5398
+ * Resolve `--category` + `--dimension` inputs to the concrete set of
5399
+ * RuleCategory values they cover. Returns undefined when no scope was
5400
+ * requested (run everything). Throws on an unknown category/dimension so the
5401
+ * CLI can exit 2 rather than silently ignore a typo.
5402
+ */
5403
+ function resolveCategoryScope(input) {
5404
+ const cats = input.categories ?? [];
5405
+ const dims = input.dimensions ?? [];
5406
+ if (cats.length === 0 && dims.length === 0) return void 0;
5407
+ const out = /* @__PURE__ */ new Set();
5408
+ for (const cat of cats) {
5409
+ if (!VALID_CATEGORIES.has(cat)) throw new Error(`unknown --category '${cat}'`);
5410
+ out.add(cat);
5411
+ }
5412
+ for (const dim of dims) {
5413
+ if (!isScoreDimension(dim)) throw new Error(`unknown --dimension '${dim}'`);
5414
+ for (const cat of categoriesForDimension(dim)) out.add(cat);
5415
+ }
5416
+ return out;
5417
+ }
5418
+ /**
5419
+ * Narrow `ruleIds` to those whose registered category is in `scope`. RuleIds
5420
+ * not in the registry are dropped (a category scope is an explicit allowlist).
5421
+ */
5422
+ function filterRuleIdsByCategory(ruleIds, scope) {
5423
+ const categoryById = /* @__PURE__ */ new Map();
5424
+ for (const rule of RULE_REGISTRY) categoryById.set(rule.id, rule.category);
5425
+ const out = /* @__PURE__ */ new Set();
5426
+ for (const id of ruleIds) {
5427
+ const cat = categoryById.get(id);
5428
+ if (cat !== void 0 && scope.has(cat)) out.add(id);
5429
+ }
5430
+ return out;
5431
+ }
5432
+ //#endregion
5150
5433
  //#region src/push.ts
5151
5434
  const PUSH_TIMEOUT_MS = 5e3;
5152
5435
  const categoryByRuleId = new Map(RULE_REGISTRY.map((r) => [r.id, r.category]));
@@ -5262,6 +5545,6 @@ async function pushFindings(opts) {
5262
5545
  }
5263
5546
  }
5264
5547
  //#endregion
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 };
5548
+ export { BUILT_IN_RECOMMENDED, ConfigCycleError, ConfigFileNotFoundError, DOCTOR_REPORT_SCHEMA_VERSION, DeadCodeImportFailed, DeadCodeTimeoutError, DoctorUserConfigSchema, InvalidConfigError, LevelSchema, OxlintOutputTooLarge, OxlintSpawnFailed, RULE_REGISTRY, RuleEntrySchema, SCORE_DIMENSIONS, agentReport, applyInlineDisables, audit, buildDoctorReport, buildJsonSchema, buildPushPayload, categoriesForDimension, checkBuildQuality, checkDeadCode, checkDeps, checkNuxtProject, dedupeDeadCodeAgainstLint, defineConfig, detectProject, detectSummary, dimensionForCategory, docsUrl, encodeAnnotation, encodeAnnotations, filterReportByRules, filterRuleIdsByCategory, findMonorepoRoot, format, isNuxtLayoutFile, isNuxtPageFile, isNuxtServerFile, isScoreDimension, jsonCompactReport, jsonReport, listChangedFiles, listRules, listWorkspacePackages, loadAllRuleDocs, loadDoctorConfig, loadRuleDoc, mergeCliOverrides, normalizeInitAnswers, parseDirectives, parseExcludeList, planInit, prCommentReport, prettyReport, pushFindings, renderAgentPlaybook, renderDetectSummary, renderRulePrompt, renderVerboseTrace, resolveCategoryScope, runCrossFilePass, sarifReport, scoreDiagnostics, stripFindings, validateConfig };
5266
5549
 
5267
5550
  //# sourceMappingURL=index.js.map