@geoql/doctor-rule-core 1.3.0 → 1.4.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 +135 -59
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -133,7 +133,7 @@ const noExplicitImportsOfAutoImported = defineRule({ create(context) {
|
|
|
133
133
|
} });
|
|
134
134
|
//#endregion
|
|
135
135
|
//#region src/rules/nuxt/ai-slop/no-fetch-in-setup.ts
|
|
136
|
-
const MESSAGE$
|
|
136
|
+
const MESSAGE$32 = `Bare $fetch/fetch at the top level of <script setup> runs on both server and client without SSR deduplication. Use useFetch for automatic request deduplication and SSR hydration. See https://nuxt.com/docs/4.x/guide/data-fetching`;
|
|
137
137
|
function isFetchCall(node) {
|
|
138
138
|
if (!node || node.type !== "CallExpression") return false;
|
|
139
139
|
const callee = node.callee;
|
|
@@ -170,7 +170,7 @@ const noFetchInSetup = defineRule({ create(context) {
|
|
|
170
170
|
if (node["parent"]?.type === "AwaitExpression") return;
|
|
171
171
|
context.report({
|
|
172
172
|
node,
|
|
173
|
-
message: MESSAGE$
|
|
173
|
+
message: MESSAGE$32
|
|
174
174
|
});
|
|
175
175
|
},
|
|
176
176
|
AwaitExpression(node) {
|
|
@@ -178,7 +178,7 @@ const noFetchInSetup = defineRule({ create(context) {
|
|
|
178
178
|
if (!isFetchCall(node.argument)) return;
|
|
179
179
|
context.report({
|
|
180
180
|
node,
|
|
181
|
-
message: MESSAGE$
|
|
181
|
+
message: MESSAGE$32
|
|
182
182
|
});
|
|
183
183
|
}
|
|
184
184
|
};
|
|
@@ -190,7 +190,7 @@ const LEGACY_PROPS = /* @__PURE__ */ new Set([
|
|
|
190
190
|
"server",
|
|
191
191
|
"browser"
|
|
192
192
|
]);
|
|
193
|
-
const MESSAGE$
|
|
193
|
+
const MESSAGE$31 = `Use import.meta.client / import.meta.server / import.meta.browser instead of process.client / process.server / process.browser (Nuxt 4). See https://nuxt.com/docs/4.x/guide/concepts/auto-imports`;
|
|
194
194
|
const noProcessClientServer = defineRule({
|
|
195
195
|
create(context) {
|
|
196
196
|
return { MemberExpression(node) {
|
|
@@ -202,7 +202,7 @@ const noProcessClientServer = defineRule({
|
|
|
202
202
|
if (!LEGACY_PROPS.has(property.name)) return;
|
|
203
203
|
context.report({
|
|
204
204
|
node,
|
|
205
|
-
message: MESSAGE$
|
|
205
|
+
message: MESSAGE$31
|
|
206
206
|
});
|
|
207
207
|
} };
|
|
208
208
|
},
|
|
@@ -219,7 +219,7 @@ const noProcessClientServer = defineRule({
|
|
|
219
219
|
});
|
|
220
220
|
//#endregion
|
|
221
221
|
//#region src/rules/nuxt/ai-slop/no-useState-for-server-data.ts
|
|
222
|
-
const MESSAGE$
|
|
222
|
+
const MESSAGE$30 = `useState with a fetch/await initializer suggests useFetch or useAsyncData for automatic SSR hydration and request deduplication. See https://nuxt.com/docs/4.x/guide/data-fetching`;
|
|
223
223
|
function containsFetchOrAwait(node, visited) {
|
|
224
224
|
if (!node || visited.has(node)) return false;
|
|
225
225
|
visited.add(node);
|
|
@@ -256,13 +256,13 @@ const noUseStateForServerData = defineRule({ create(context) {
|
|
|
256
256
|
if (!containsFetchOrAwait(initFn.body, /* @__PURE__ */ new Set())) return;
|
|
257
257
|
context.report({
|
|
258
258
|
node,
|
|
259
|
-
message: MESSAGE$
|
|
259
|
+
message: MESSAGE$30
|
|
260
260
|
});
|
|
261
261
|
} };
|
|
262
262
|
} });
|
|
263
263
|
//#endregion
|
|
264
264
|
//#region src/rules/nuxt/data-fetching/useAsyncData-key-required-in-loop.ts
|
|
265
|
-
const MESSAGE$
|
|
265
|
+
const MESSAGE$29 = `useAsyncData/useFetch without an explicit string key inside a loop causes duplicate requests and cache fragmentation. Pass a unique string key as the first argument. See https://nuxt.com/docs/4.x/guide/data-fetching`;
|
|
266
266
|
const DATA_FETCHERS = /* @__PURE__ */ new Set(["useAsyncData", "useFetch"]);
|
|
267
267
|
function isMapCall(node) {
|
|
268
268
|
const callee = node.callee;
|
|
@@ -315,7 +315,7 @@ const useAsyncDataKeyRequiredInLoop = defineRule({ create(context) {
|
|
|
315
315
|
if (firstArg.type === "Literal" && typeof firstArg.value === "string") return;
|
|
316
316
|
context.report({
|
|
317
317
|
node,
|
|
318
|
-
message: MESSAGE$
|
|
318
|
+
message: MESSAGE$29
|
|
319
319
|
});
|
|
320
320
|
},
|
|
321
321
|
"CallExpression:exit"(node) {
|
|
@@ -325,7 +325,7 @@ const useAsyncDataKeyRequiredInLoop = defineRule({ create(context) {
|
|
|
325
325
|
} });
|
|
326
326
|
//#endregion
|
|
327
327
|
//#region src/rules/nuxt/security/no-user-input-in-fetch-url.ts
|
|
328
|
-
const MESSAGE$
|
|
328
|
+
const MESSAGE$28 = `Building a useFetch/$fetch URL from user input (route.query/route.params) lets an attacker control the request target — an SSRF risk during SSR. Use a fixed path and pass user input via the query/body options instead. See https://owasp.org/www-community/attacks/Server_Side_Request_Forgery`;
|
|
329
329
|
const FETCH_CALLEES = /* @__PURE__ */ new Set([
|
|
330
330
|
"useFetch",
|
|
331
331
|
"useLazyFetch",
|
|
@@ -363,13 +363,13 @@ const noUserInputInFetchUrl = defineRule({ create(context) {
|
|
|
363
363
|
const urlArg = node.arguments[0];
|
|
364
364
|
if (urlArgIsTainted(urlArg)) context.report({
|
|
365
365
|
node,
|
|
366
|
-
message: MESSAGE$
|
|
366
|
+
message: MESSAGE$28
|
|
367
367
|
});
|
|
368
368
|
} };
|
|
369
369
|
} });
|
|
370
370
|
//#endregion
|
|
371
371
|
//#region src/rules/nuxt/hydration/clientOnly-for-browser-apis.ts
|
|
372
|
-
const MESSAGE$
|
|
372
|
+
const MESSAGE$27 = `Accessing window./document./navigator./localStorage./sessionStorage. without an import.meta.client or process.client guard causes SSR failures. Wrap with if (import.meta.client) { ... } or move to onMounted. See https://nuxt.com/docs/4.x/guide/components`;
|
|
373
373
|
const BROWSER_GLOBALS$1 = /* @__PURE__ */ new Set([
|
|
374
374
|
"window",
|
|
375
375
|
"document",
|
|
@@ -440,14 +440,14 @@ const clientOnlyForBrowserApis = defineRule({ create(context) {
|
|
|
440
440
|
if (!BROWSER_GLOBALS$1.has(obj.name)) return;
|
|
441
441
|
context.report({
|
|
442
442
|
node,
|
|
443
|
-
message: MESSAGE$
|
|
443
|
+
message: MESSAGE$27
|
|
444
444
|
});
|
|
445
445
|
}
|
|
446
446
|
};
|
|
447
447
|
} });
|
|
448
448
|
//#endregion
|
|
449
449
|
//#region src/rules/nuxt/hydration/no-browser-global-in-computed.ts
|
|
450
|
-
const MESSAGE$
|
|
450
|
+
const MESSAGE$26 = `Reading a browser global inside a computed getter runs during SSR (computeds evaluate on the server render) and throws or hydration-mismatches. Guard with import.meta.client or derive the value in onMounted. See https://nuxt.com/docs/4.x/getting-started/data-fetching`;
|
|
451
451
|
const BROWSER_GLOBALS = /* @__PURE__ */ new Set([
|
|
452
452
|
"window",
|
|
453
453
|
"document",
|
|
@@ -501,14 +501,14 @@ const noBrowserGlobalInComputed = defineRule({ create(context) {
|
|
|
501
501
|
if (!BROWSER_GLOBALS.has(object.name)) return;
|
|
502
502
|
context.report({
|
|
503
503
|
node,
|
|
504
|
-
message: MESSAGE$
|
|
504
|
+
message: MESSAGE$26
|
|
505
505
|
});
|
|
506
506
|
}
|
|
507
507
|
};
|
|
508
508
|
} });
|
|
509
509
|
//#endregion
|
|
510
510
|
//#region src/rules/nuxt/hydration/no-document-in-setup.ts
|
|
511
|
-
const MESSAGE$
|
|
511
|
+
const MESSAGE$25 = `Accessing document/window/navigator/localStorage at the top level of <script setup> causes a server-side crash (SSR). These browser globals are undefined on the server. Move access inside onMounted or guard with import.meta.client. See https://nuxt.com/docs/4.x/guide/components`;
|
|
512
512
|
const SSR_UNSAFE_GLOBALS = /* @__PURE__ */ new Set([
|
|
513
513
|
"document",
|
|
514
514
|
"window",
|
|
@@ -588,14 +588,14 @@ const noDocumentInSetup = defineRule({ create(context) {
|
|
|
588
588
|
if (isGuardedByImportMetaClient(node)) return;
|
|
589
589
|
context.report({
|
|
590
590
|
node,
|
|
591
|
-
message: MESSAGE$
|
|
591
|
+
message: MESSAGE$25
|
|
592
592
|
});
|
|
593
593
|
}
|
|
594
594
|
};
|
|
595
595
|
} });
|
|
596
596
|
//#endregion
|
|
597
597
|
//#region src/rules/nuxt/server-routes/createError-on-failure.ts
|
|
598
|
-
const MESSAGE$
|
|
598
|
+
const MESSAGE$24 = `throw new Error() leaks the call stack and exposes internal details. Use throw createError() from h3 for proper HTTP errors with no stack leak. See https://h3.unjs.io/guide/throw`;
|
|
599
599
|
const createErrorOnFailure = defineRule({ create(context) {
|
|
600
600
|
let handlerDepth = 0;
|
|
601
601
|
return {
|
|
@@ -620,14 +620,14 @@ const createErrorOnFailure = defineRule({ create(context) {
|
|
|
620
620
|
if (ctor.name !== "Error") return;
|
|
621
621
|
context.report({
|
|
622
622
|
node,
|
|
623
|
-
message: MESSAGE$
|
|
623
|
+
message: MESSAGE$24
|
|
624
624
|
});
|
|
625
625
|
}
|
|
626
626
|
};
|
|
627
627
|
} });
|
|
628
628
|
//#endregion
|
|
629
629
|
//#region src/rules/nuxt/server-routes/defineEventHandler-typed.ts
|
|
630
|
-
const MESSAGE$
|
|
630
|
+
const MESSAGE$23 = `defineEventHandler handler param event lacks a type annotation. Add a generic: defineEventHandler<H3Event>(...) or type the event parameter explicitly. See https://h3.unjs.io/guide/typed`;
|
|
631
631
|
function isFunction$2(node) {
|
|
632
632
|
return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
|
|
633
633
|
}
|
|
@@ -646,13 +646,13 @@ const defineEventHandlerTyped = defineRule({ create(context) {
|
|
|
646
646
|
if (params[0].typeAnnotation) return;
|
|
647
647
|
context.report({
|
|
648
648
|
node,
|
|
649
|
-
message: MESSAGE$
|
|
649
|
+
message: MESSAGE$23
|
|
650
650
|
});
|
|
651
651
|
} };
|
|
652
652
|
} });
|
|
653
653
|
//#endregion
|
|
654
654
|
//#region src/rules/nuxt/server-routes/validate-body-with-h3-v2.ts
|
|
655
|
-
const MESSAGE$
|
|
655
|
+
const MESSAGE$22 = `readBody() is the legacy H3 reader. In Nuxt 4 with h3 v2, use readValidatedBody to parse and validate the request body in one step. See https://nuxt.com/docs/4.x/guide/data-fetching`;
|
|
656
656
|
const validateBodyWithH3V2 = defineRule({ create(context) {
|
|
657
657
|
return { CallExpression(node) {
|
|
658
658
|
const callee = node.callee;
|
|
@@ -660,7 +660,7 @@ const validateBodyWithH3V2 = defineRule({ create(context) {
|
|
|
660
660
|
if (callee.name !== "readBody") return;
|
|
661
661
|
context.report({
|
|
662
662
|
node,
|
|
663
|
-
message: MESSAGE$
|
|
663
|
+
message: MESSAGE$22
|
|
664
664
|
});
|
|
665
665
|
} };
|
|
666
666
|
} });
|
|
@@ -691,7 +691,7 @@ const NUXT_RULES = [
|
|
|
691
691
|
];
|
|
692
692
|
//#endregion
|
|
693
693
|
//#region src/rules/vue/ai-slop/no-destructure-props-without-toRefs.ts
|
|
694
|
-
const MESSAGE$
|
|
694
|
+
const MESSAGE$21 = `Destructuring props loses reactivity. Wrap with toRefs(...) to keep refs. See https://vuejs.org/guide/components/props.html#reactive-props-destructure`;
|
|
695
695
|
function calleeName$6(node) {
|
|
696
696
|
const callee = node.callee;
|
|
697
697
|
if (callee?.type === "Identifier") return callee.name;
|
|
@@ -747,7 +747,7 @@ const noDestructurePropsWithoutToRefs = defineRule({ create(context) {
|
|
|
747
747
|
if (isToRefsCall$1(init)) return;
|
|
748
748
|
if ((isDefinePropsCall(init) || init.type === "Identifier" && propsSources.has(init.name)) && isOneTimeContext()) context.report({
|
|
749
749
|
node,
|
|
750
|
-
message: MESSAGE$
|
|
750
|
+
message: MESSAGE$21
|
|
751
751
|
});
|
|
752
752
|
},
|
|
753
753
|
Property(node) {
|
|
@@ -762,7 +762,7 @@ const noDestructurePropsWithoutToRefs = defineRule({ create(context) {
|
|
|
762
762
|
} });
|
|
763
763
|
//#endregion
|
|
764
764
|
//#region src/rules/vue/ai-slop/no-destructure-reactive-without-toRefs.ts
|
|
765
|
-
const MESSAGE$
|
|
765
|
+
const MESSAGE$20 = `Destructuring reactive state loses reactivity. Wrap with toRefs(...) or read single keys via toRef(state, 'key'). See https://vuejs.org/guide/essentials/reactivity-fundamentals.html#destructuring-reactive-state`;
|
|
766
766
|
const REACTIVE_FACTORIES = /* @__PURE__ */ new Set(["reactive", "shallowReactive"]);
|
|
767
767
|
function calleeName$5(node) {
|
|
768
768
|
const callee = node.callee;
|
|
@@ -794,7 +794,7 @@ const noDestructureReactiveWithoutToRefs = defineRule({ create(context) {
|
|
|
794
794
|
if (isToRefsCall(init)) return;
|
|
795
795
|
if (isReactiveCall(init) || init.type === "Identifier" && reactiveSources.has(init.name)) context.report({
|
|
796
796
|
node,
|
|
797
|
-
message: MESSAGE$
|
|
797
|
+
message: MESSAGE$20
|
|
798
798
|
});
|
|
799
799
|
} };
|
|
800
800
|
} });
|
|
@@ -863,7 +863,7 @@ const noImportsFromVueWhenAutoImported = defineRule({ create(context) {
|
|
|
863
863
|
} });
|
|
864
864
|
//#endregion
|
|
865
865
|
//#region src/rules/vue/ai-slop/no-non-null-assertion-on-ref-value.ts
|
|
866
|
-
const MESSAGE$
|
|
866
|
+
const MESSAGE$19 = `Non-null assertion on a ref's .value hides nullability. Add an explicit guard ('if (!r.value) return') or use optional chaining ('r.value?.x'). See https://vuejs.org/guide/typescript/composition-api.html#typing-ref`;
|
|
867
867
|
const REF_FACTORIES = /* @__PURE__ */ new Set([
|
|
868
868
|
"ref",
|
|
869
869
|
"shallowRef",
|
|
@@ -900,14 +900,14 @@ const noNonNullAssertionOnRefValue = defineRule({ create(context) {
|
|
|
900
900
|
if (!refSources.has(object.name)) return;
|
|
901
901
|
context.report({
|
|
902
902
|
node,
|
|
903
|
-
message: MESSAGE$
|
|
903
|
+
message: MESSAGE$19
|
|
904
904
|
});
|
|
905
905
|
}
|
|
906
906
|
};
|
|
907
907
|
} });
|
|
908
908
|
//#endregion
|
|
909
909
|
//#region src/rules/vue/composition/defineProps-typed.ts
|
|
910
|
-
const MESSAGE$
|
|
910
|
+
const MESSAGE$18 = `This defineProps() call declares props with a runtime object, which discards static type information. Use the type-only generic form (defineProps<{ ... }>()) so props are fully typed. See https://vuejs.org/guide/typescript/composition-api.html#typing-component-props`;
|
|
911
911
|
function calleeName$3(node) {
|
|
912
912
|
const callee = node.callee;
|
|
913
913
|
if (callee?.type === "Identifier") return callee.name;
|
|
@@ -917,13 +917,13 @@ const definePropsTyped = defineRule({ create(context) {
|
|
|
917
917
|
if (calleeName$3(node) !== "defineProps") return;
|
|
918
918
|
if (node.arguments[0]?.type === "ObjectExpression") context.report({
|
|
919
919
|
node,
|
|
920
|
-
message: MESSAGE$
|
|
920
|
+
message: MESSAGE$18
|
|
921
921
|
});
|
|
922
922
|
} };
|
|
923
923
|
} });
|
|
924
924
|
//#endregion
|
|
925
925
|
//#region src/rules/vue/composition/no-pinia-store-in-setup.ts
|
|
926
|
-
const MESSAGE$
|
|
926
|
+
const MESSAGE$17 = `This calls defineStore() inside a function (setup, a composable, or a component body), so a brand-new store definition is created on every call instead of one shared singleton. Call defineStore() once at module scope and call the returned useXxxStore() inside setup. See https://pinia.vuejs.org/core-concepts/#defining-a-store`;
|
|
927
927
|
const noPiniaStoreInSetup = defineRule({ create(context) {
|
|
928
928
|
let functionDepth = 0;
|
|
929
929
|
const enter = () => {
|
|
@@ -938,7 +938,7 @@ const noPiniaStoreInSetup = defineRule({ create(context) {
|
|
|
938
938
|
if (callee?.type !== "Identifier" || callee.name !== "defineStore") return;
|
|
939
939
|
if (functionDepth > 0) context.report({
|
|
940
940
|
node,
|
|
941
|
-
message: MESSAGE$
|
|
941
|
+
message: MESSAGE$17
|
|
942
942
|
});
|
|
943
943
|
},
|
|
944
944
|
FunctionDeclaration: enter,
|
|
@@ -951,7 +951,7 @@ const noPiniaStoreInSetup = defineRule({ create(context) {
|
|
|
951
951
|
} });
|
|
952
952
|
//#endregion
|
|
953
953
|
//#region src/rules/vue/composition/no-prop-callback-in-setup.ts
|
|
954
|
-
const MESSAGE$
|
|
954
|
+
const MESSAGE$16 = `Calling a parent callback prop during setup or inside a computed getter runs a side effect on the render path — it fires on every re-evaluation and during SSR. Move the call into an event handler, watcher, or lifecycle hook. See https://vuejs.org/guide/components/events.html`;
|
|
955
955
|
const CALLBACK_NAME = /^on[A-Z]/;
|
|
956
956
|
function calleeIdentifierName$2(node) {
|
|
957
957
|
const callee = node.callee;
|
|
@@ -1004,14 +1004,14 @@ const noPropCallbackInSetup = defineRule({ create(context) {
|
|
|
1004
1004
|
const enclosing = functionStack[functionStack.length - 1];
|
|
1005
1005
|
if (enclosing === void 0 || enclosing.isComputedGetter) context.report({
|
|
1006
1006
|
node,
|
|
1007
|
-
message: MESSAGE$
|
|
1007
|
+
message: MESSAGE$16
|
|
1008
1008
|
});
|
|
1009
1009
|
}
|
|
1010
1010
|
};
|
|
1011
1011
|
} });
|
|
1012
1012
|
//#endregion
|
|
1013
1013
|
//#region src/rules/vue/composition/prefer-script-setup-for-new-files.ts
|
|
1014
|
-
const MESSAGE$
|
|
1014
|
+
const MESSAGE$15 = `This component nests composition-API logic inside an options-object setup(). Use a <script setup> block instead — it removes the setup() boilerplate and exposes bindings to the template directly. See https://vuejs.org/api/sfc-script-setup.html`;
|
|
1015
1015
|
function isFunctionValue(node) {
|
|
1016
1016
|
return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
|
|
1017
1017
|
}
|
|
@@ -1028,13 +1028,13 @@ const preferScriptSetupForNewFiles = defineRule({ create(context) {
|
|
|
1028
1028
|
if (declaration?.type !== "ObjectExpression") return;
|
|
1029
1029
|
if (declaration.properties.some(isSetupProperty)) context.report({
|
|
1030
1030
|
node,
|
|
1031
|
-
message: MESSAGE$
|
|
1031
|
+
message: MESSAGE$15
|
|
1032
1032
|
});
|
|
1033
1033
|
} };
|
|
1034
1034
|
} });
|
|
1035
1035
|
//#endregion
|
|
1036
1036
|
//#region src/rules/vue/performance/prefer-defineAsyncComponent-on-route.ts
|
|
1037
|
-
const MESSAGE$
|
|
1037
|
+
const MESSAGE$14 = `This route record's \`component:\` field references the imported component directly, which forces a synchronous import of the route bundle. Use \`() => import('./Foo.vue')\` or \`defineAsyncComponent(() => import('./Foo.vue'))\` to lazy-load the route. See https://vuejs.org/guide/components/async.html`;
|
|
1038
1038
|
function keyName(key) {
|
|
1039
1039
|
return key.type === "Identifier" ? key.name : key.value;
|
|
1040
1040
|
}
|
|
@@ -1044,13 +1044,13 @@ const preferDefineAsyncComponentOnRoute = defineRule({ create(context) {
|
|
|
1044
1044
|
if (keyName(node.key) !== "component") return;
|
|
1045
1045
|
if (node.value.type === "Identifier") context.report({
|
|
1046
1046
|
node,
|
|
1047
|
-
message: MESSAGE$
|
|
1047
|
+
message: MESSAGE$14
|
|
1048
1048
|
});
|
|
1049
1049
|
} };
|
|
1050
1050
|
} });
|
|
1051
1051
|
//#endregion
|
|
1052
1052
|
//#region src/rules/vue/performance/prefer-module-scope-pure-function.ts
|
|
1053
|
-
const MESSAGE$
|
|
1053
|
+
const MESSAGE$13 = `This helper closes over nothing reactive (only its parameters, imports, and globals), yet it is re-created on every call of the enclosing function. Hoist it to module scope so a single function reference is shared across all component instances. See https://vuejs.org/guide/best-practices/performance.html`;
|
|
1054
1054
|
const GLOBALS = /* @__PURE__ */ new Set([
|
|
1055
1055
|
"console",
|
|
1056
1056
|
"Math",
|
|
@@ -1178,7 +1178,7 @@ const preferModuleScopePureFunction = defineRule({ create(context) {
|
|
|
1178
1178
|
if (functionDepth < 1) return;
|
|
1179
1179
|
if (isHoistable(fn, imports)) context.report({
|
|
1180
1180
|
node: fn,
|
|
1181
|
-
message: MESSAGE$
|
|
1181
|
+
message: MESSAGE$13
|
|
1182
1182
|
});
|
|
1183
1183
|
};
|
|
1184
1184
|
return {
|
|
@@ -1196,7 +1196,7 @@ const preferModuleScopePureFunction = defineRule({ create(context) {
|
|
|
1196
1196
|
if (init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression") {
|
|
1197
1197
|
if (isHoistable(init, imports)) context.report({
|
|
1198
1198
|
node: init,
|
|
1199
|
-
message: MESSAGE$
|
|
1199
|
+
message: MESSAGE$13
|
|
1200
1200
|
});
|
|
1201
1201
|
}
|
|
1202
1202
|
}
|
|
@@ -1216,7 +1216,7 @@ const preferModuleScopePureFunction = defineRule({ create(context) {
|
|
|
1216
1216
|
} });
|
|
1217
1217
|
//#endregion
|
|
1218
1218
|
//#region src/rules/vue/performance/prefer-module-scope-static-value.ts
|
|
1219
|
-
const MESSAGE$
|
|
1219
|
+
const MESSAGE$12 = `This static array or object literal is declared inside a function, so a fresh copy is allocated on every call (including every component instantiation). Hoist it to module scope so the single frozen reference is shared. See https://vuejs.org/guide/best-practices/performance.html`;
|
|
1220
1220
|
const LITERAL_TYPES = /* @__PURE__ */ new Set([
|
|
1221
1221
|
"Literal",
|
|
1222
1222
|
"StringLiteral",
|
|
@@ -1281,14 +1281,14 @@ const preferModuleScopeStaticValue = defineRule({ create(context) {
|
|
|
1281
1281
|
if (!isStaticLiteral(init)) return;
|
|
1282
1282
|
context.report({
|
|
1283
1283
|
node: init,
|
|
1284
|
-
message: MESSAGE$
|
|
1284
|
+
message: MESSAGE$12
|
|
1285
1285
|
});
|
|
1286
1286
|
}
|
|
1287
1287
|
};
|
|
1288
1288
|
} });
|
|
1289
1289
|
//#endregion
|
|
1290
1290
|
//#region src/rules/vue/performance/prefer-stable-empty-fallback.ts
|
|
1291
|
-
const MESSAGE$
|
|
1291
|
+
const MESSAGE$11 = `This computed falls back to a fresh empty array or object literal, so it returns a brand-new reference whenever the source is nullish. Downstream watchers, child props, and v-memo see an identity change every recompute. Hoist a single module-scope EMPTY constant and fall back to that. See https://vuejs.org/guide/best-practices/performance.html`;
|
|
1292
1292
|
function isEmptyLiteral(node) {
|
|
1293
1293
|
if (node?.type === "ArrayExpression") return node.elements.length === 0;
|
|
1294
1294
|
if (node?.type === "ObjectExpression") return node.properties.length === 0;
|
|
@@ -1318,13 +1318,13 @@ const preferStableEmptyFallback = defineRule({ create(context) {
|
|
|
1318
1318
|
const returned = getterBody(node);
|
|
1319
1319
|
if (returned && fallsBackToEmptyLiteral(returned)) context.report({
|
|
1320
1320
|
node: returned,
|
|
1321
|
-
message: MESSAGE$
|
|
1321
|
+
message: MESSAGE$11
|
|
1322
1322
|
});
|
|
1323
1323
|
} };
|
|
1324
1324
|
} });
|
|
1325
1325
|
//#endregion
|
|
1326
1326
|
//#region src/rules/vue/reactivity/effect-listener-cleanup-mismatch.ts
|
|
1327
|
-
const MESSAGE$
|
|
1327
|
+
const MESSAGE$10 = `This cleanup cannot detach the listener it pairs with: removeEventListener only removes when the handler reference AND capture flag match addEventListener exactly. Use the same named handler (never an inline arrow) and the same capture flag. See https://vuejs.org/guide/essentials/watchers.html#side-effect-cleanup`;
|
|
1328
1328
|
const EFFECT_CALLS = /* @__PURE__ */ new Set([
|
|
1329
1329
|
"watch",
|
|
1330
1330
|
"watchEffect",
|
|
@@ -1381,7 +1381,7 @@ function reportMismatches(context, frame) {
|
|
|
1381
1381
|
if (pairable.length === 0) continue;
|
|
1382
1382
|
if (!pairable.some((a) => a.handlerKey !== void 0 && a.handlerKey === removed.handlerKey && a.capture === removed.capture)) context.report({
|
|
1383
1383
|
node: removed.node,
|
|
1384
|
-
message: MESSAGE$
|
|
1384
|
+
message: MESSAGE$10
|
|
1385
1385
|
});
|
|
1386
1386
|
}
|
|
1387
1387
|
}
|
|
@@ -1425,7 +1425,7 @@ const effectListenerCleanupMismatch = defineRule({ create(context) {
|
|
|
1425
1425
|
} });
|
|
1426
1426
|
//#endregion
|
|
1427
1427
|
//#region src/rules/vue/reactivity/no-fresh-deps-in-watch.ts
|
|
1428
|
-
const MESSAGE$
|
|
1428
|
+
const MESSAGE$9 = `This watch getter returns a freshly-constructed array or object literal, so Vue compares it by reference (Object.is) and the watcher re-fires on every reactive tick — even when nothing changed. Use the multi-source array form watch([a, b], cb) for several sources, or watch a primitive/ref directly. See https://vuejs.org/guide/essentials/watchers.html#watch-source-types`;
|
|
1429
1429
|
function isFunction$1(node) {
|
|
1430
1430
|
return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
|
|
1431
1431
|
}
|
|
@@ -1452,13 +1452,13 @@ const noFreshDepsInWatch = defineRule({ create(context) {
|
|
|
1452
1452
|
if (!source || !isFunction$1(source)) return;
|
|
1453
1453
|
if (getterReturnsFreshLiteral(source)) context.report({
|
|
1454
1454
|
node: source,
|
|
1455
|
-
message: MESSAGE$
|
|
1455
|
+
message: MESSAGE$9
|
|
1456
1456
|
});
|
|
1457
1457
|
} };
|
|
1458
1458
|
} });
|
|
1459
1459
|
//#endregion
|
|
1460
1460
|
//#region src/rules/vue/reactivity/no-stale-timer-ref.ts
|
|
1461
|
-
const MESSAGE$
|
|
1461
|
+
const MESSAGE$8 = `clearTimeout/clearInterval on a ref leaves the stale timer id in .value, so any "timer pending" check on that ref keeps passing after the timer is gone. Reset the ref (e.g. timer.value = null) after clearing. See https://vuejs.org/guide/essentials/reactivity-fundamentals`;
|
|
1462
1462
|
const CLEAR_CALLS = /* @__PURE__ */ new Set(["clearTimeout", "clearInterval"]);
|
|
1463
1463
|
/** Matches `<identifier>.value` and returns the identifier name. */
|
|
1464
1464
|
function refValueTarget(node) {
|
|
@@ -1472,7 +1472,7 @@ function refValueTarget(node) {
|
|
|
1472
1472
|
function flush(context, frame) {
|
|
1473
1473
|
for (const item of frame.pending) context.report({
|
|
1474
1474
|
node: item.node,
|
|
1475
|
-
message: MESSAGE$
|
|
1475
|
+
message: MESSAGE$8
|
|
1476
1476
|
});
|
|
1477
1477
|
}
|
|
1478
1478
|
const noStaleTimerRef = defineRule({ create(context) {
|
|
@@ -1521,7 +1521,7 @@ const noStaleTimerRef = defineRule({ create(context) {
|
|
|
1521
1521
|
} });
|
|
1522
1522
|
//#endregion
|
|
1523
1523
|
//#region src/rules/vue/reactivity/prefer-readonly-for-injected.ts
|
|
1524
|
-
const MESSAGE$
|
|
1524
|
+
const MESSAGE$7 = `Mutating an injected value from a consumer breaks one-way data flow and makes the source of changes hard to trace. Wrap the provided value in readonly() at the provide site and expose a dedicated mutator instead. See https://vuejs.org/guide/components/provide-inject.html#working-with-reactivity`;
|
|
1525
1525
|
const MUTATING_METHODS = /* @__PURE__ */ new Set([
|
|
1526
1526
|
"push",
|
|
1527
1527
|
"pop",
|
|
@@ -1553,7 +1553,7 @@ const preferReadonlyForInjected = defineRule({ create(context) {
|
|
|
1553
1553
|
const name = memberObjectName(node.left);
|
|
1554
1554
|
if (name !== void 0 && injectedSources.has(name)) context.report({
|
|
1555
1555
|
node,
|
|
1556
|
-
message: MESSAGE$
|
|
1556
|
+
message: MESSAGE$7
|
|
1557
1557
|
});
|
|
1558
1558
|
},
|
|
1559
1559
|
CallExpression(node) {
|
|
@@ -1564,14 +1564,14 @@ const preferReadonlyForInjected = defineRule({ create(context) {
|
|
|
1564
1564
|
const property = callee.property;
|
|
1565
1565
|
if (MUTATING_METHODS.has(property.name)) context.report({
|
|
1566
1566
|
node,
|
|
1567
|
-
message: MESSAGE$
|
|
1567
|
+
message: MESSAGE$7
|
|
1568
1568
|
});
|
|
1569
1569
|
}
|
|
1570
1570
|
};
|
|
1571
1571
|
} });
|
|
1572
1572
|
//#endregion
|
|
1573
1573
|
//#region src/rules/vue/reactivity/prefer-shallowRef-for-large-data.ts
|
|
1574
|
-
const MESSAGE$
|
|
1574
|
+
const MESSAGE$6 = `This ref holds large or fetched data, so deep reactivity tracks every nested property and wastes memory and CPU. Use shallowRef() (and triggerRef on replacement) for large arrays, big objects, or server payloads. See https://vuejs.org/api/reactivity-advanced.html#shallowref`;
|
|
1575
1575
|
const ARRAY_LIMIT = 100;
|
|
1576
1576
|
const OBJECT_LIMIT = 50;
|
|
1577
1577
|
const FETCH_IDENTIFIERS = /* @__PURE__ */ new Set(["$fetch", "useFetch"]);
|
|
@@ -1605,13 +1605,13 @@ const preferShallowRefForLargeData = defineRule({ create(context) {
|
|
|
1605
1605
|
const init = node.arguments[0];
|
|
1606
1606
|
if (isLargeData(init)) context.report({
|
|
1607
1607
|
node,
|
|
1608
|
-
message: MESSAGE$
|
|
1608
|
+
message: MESSAGE$6
|
|
1609
1609
|
});
|
|
1610
1610
|
} };
|
|
1611
1611
|
} });
|
|
1612
1612
|
//#endregion
|
|
1613
1613
|
//#region src/rules/vue/reactivity/watch-without-cleanup.ts
|
|
1614
|
-
const MESSAGE$
|
|
1614
|
+
const MESSAGE$5 = `This watcher registers a listener, timer, or observer but never cleans it up, so it leaks across re-runs and hot reloads. Return a cleanup function (or call onWatcherCleanup) inside the watcher. See https://vuejs.org/guide/essentials/watchers.html#side-effect-cleanup`;
|
|
1615
1615
|
const REGISTER_CALLS = /* @__PURE__ */ new Set([
|
|
1616
1616
|
"addEventListener",
|
|
1617
1617
|
"setInterval",
|
|
@@ -1679,6 +1679,81 @@ const watchWithoutCleanup = defineRule({ create(context) {
|
|
|
1679
1679
|
watchStack.pop();
|
|
1680
1680
|
if (top.hasRegistration && !top.hasCleanup) context.report({
|
|
1681
1681
|
node,
|
|
1682
|
+
message: MESSAGE$5
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
} });
|
|
1687
|
+
//#endregion
|
|
1688
|
+
//#region src/rules/vue/security/markdown-it-unsanitized-html.ts
|
|
1689
|
+
const MESSAGE$4 = `markdown-it is configured with \`html: true\` but no sanitizer is connected. Raw HTML in user-authored markdown is an XSS vector. Pipe md.render output through DOMPurify/sanitize-html, or add a markdown-it sanitizer plugin. See https://github.com/markdown-it/markdown-it/blob/master/docs/security.md`;
|
|
1690
|
+
const SANITIZER_MODULES = /* @__PURE__ */ new Set([
|
|
1691
|
+
"dompurify",
|
|
1692
|
+
"isomorphic-dompurify",
|
|
1693
|
+
"sanitize-html",
|
|
1694
|
+
"xss",
|
|
1695
|
+
"markdown-it-sanitizer",
|
|
1696
|
+
"insane"
|
|
1697
|
+
]);
|
|
1698
|
+
function importSource(node) {
|
|
1699
|
+
return String(node.source.value);
|
|
1700
|
+
}
|
|
1701
|
+
function optsEnableHtml(node) {
|
|
1702
|
+
if (!node || node.type !== "ObjectExpression") return false;
|
|
1703
|
+
for (const prop of node.properties) {
|
|
1704
|
+
if (prop.type !== "Property") continue;
|
|
1705
|
+
const key = prop.key;
|
|
1706
|
+
if ((key.type === "Identifier" ? key.name : key.type === "Literal" ? String(key.value) : void 0) !== "html") continue;
|
|
1707
|
+
const value = prop.value;
|
|
1708
|
+
return value.type === "Literal" && value.value === true;
|
|
1709
|
+
}
|
|
1710
|
+
return false;
|
|
1711
|
+
}
|
|
1712
|
+
function isMarkdownItCallee(callee, local) {
|
|
1713
|
+
return callee?.type === "Identifier" && callee.name === local;
|
|
1714
|
+
}
|
|
1715
|
+
const markdownItUnsanitizedHtml = defineRule({ create(context) {
|
|
1716
|
+
let markdownItLocal;
|
|
1717
|
+
let sanitized = false;
|
|
1718
|
+
const unsafeSites = [];
|
|
1719
|
+
const markdownInstances = /* @__PURE__ */ new Set();
|
|
1720
|
+
const recordConstruction = (node, calleeArgs, assignedTo) => {
|
|
1721
|
+
if (assignedTo) markdownInstances.add(assignedTo);
|
|
1722
|
+
if (optsEnableHtml(calleeArgs?.[0])) unsafeSites.push(node);
|
|
1723
|
+
};
|
|
1724
|
+
return {
|
|
1725
|
+
ImportDeclaration(node) {
|
|
1726
|
+
const source = importSource(node);
|
|
1727
|
+
if (SANITIZER_MODULES.has(source)) sanitized = true;
|
|
1728
|
+
if (source === "markdown-it") {
|
|
1729
|
+
for (const spec of node.specifiers) if (spec.type === "ImportDefaultSpecifier") markdownItLocal = spec.local.name;
|
|
1730
|
+
}
|
|
1731
|
+
},
|
|
1732
|
+
VariableDeclarator(node) {
|
|
1733
|
+
if (markdownItLocal === void 0) return;
|
|
1734
|
+
const init = node.init;
|
|
1735
|
+
const id = node.id;
|
|
1736
|
+
const assignedTo = id.type === "Identifier" ? id.name : void 0;
|
|
1737
|
+
if (init?.type === "NewExpression" && isMarkdownItCallee(init.callee, markdownItLocal)) recordConstruction(init, init.arguments, assignedTo);
|
|
1738
|
+
else if (init?.type === "CallExpression" && isMarkdownItCallee(init.callee, markdownItLocal)) recordConstruction(init, init.arguments, assignedTo);
|
|
1739
|
+
},
|
|
1740
|
+
CallExpression(node) {
|
|
1741
|
+
const callee = node.callee;
|
|
1742
|
+
if (callee?.type !== "MemberExpression") return;
|
|
1743
|
+
const prop = callee.property;
|
|
1744
|
+
const propName = prop.type === "Identifier" ? prop.name : void 0;
|
|
1745
|
+
if (propName === "use") {
|
|
1746
|
+
sanitized = true;
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
if (propName !== "set") return;
|
|
1750
|
+
const object = callee.object;
|
|
1751
|
+
if (object.type === "Identifier" && markdownInstances.has(object.name) && optsEnableHtml(node.arguments?.[0])) unsafeSites.push(node);
|
|
1752
|
+
},
|
|
1753
|
+
"Program:exit"() {
|
|
1754
|
+
if (sanitized) return;
|
|
1755
|
+
for (const site of unsafeSites) context.report({
|
|
1756
|
+
node: site,
|
|
1682
1757
|
message: MESSAGE$4
|
|
1683
1758
|
});
|
|
1684
1759
|
}
|
|
@@ -1873,7 +1948,8 @@ const VUE_RULES = [
|
|
|
1873
1948
|
coreRule("security/no-inner-html", "security", "error", true, defineRule(noInnerHtml)),
|
|
1874
1949
|
coreRule("security/no-eval-like", "security", "error", true, defineRule(noEvalLike)),
|
|
1875
1950
|
coreRule("security/no-auth-token-in-web-storage", "security", "warn", true, defineRule(noAuthTokenInWebStorage)),
|
|
1876
|
-
coreRule("security/no-secrets-in-source", "security", "warn", true, defineRule(noSecretsInSource))
|
|
1951
|
+
coreRule("security/no-secrets-in-source", "security", "warn", true, defineRule(noSecretsInSource)),
|
|
1952
|
+
coreRule("security/markdown-it-unsanitized-html", "security", "warn", true, defineRule(markdownItUnsanitizedHtml))
|
|
1877
1953
|
];
|
|
1878
1954
|
//#endregion
|
|
1879
1955
|
export { NUXT_AUTO_IMPORTED, NUXT_RULES, VUE_AUTO_IMPORTED, VUE_RULES, clientOnlyForBrowserApis, createErrorOnFailure, defineEventHandlerTyped, definePropsTyped, defineRule, noAuthTokenInWebStorage, noDestructurePropsWithoutToRefs, noDestructureReactiveWithoutToRefs, noDocumentInSetup, noEmDashInString, noEvalLike, noExplicitImportsOfAutoImported, noFetchInSetup, noImportsFromVueWhenAutoImported, noInnerHtml, noNonNullAssertionOnRefValue, noProcessClientServer, noSecretsInSource, noUseStateForServerData, noUserInputInFetchUrl, preferDefineAsyncComponentOnRoute, preferReadonlyForInjected, preferScriptSetupForNewFiles, preferShallowRefForLargeData, useAsyncDataKeyRequiredInLoop, validateBodyWithH3V2, watchWithoutCleanup };
|