eslint-plugin-harlanzw 0.5.21 → 0.6.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/README.md +2 -0
- package/dist/index.mjs +235 -54
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -60,6 +60,8 @@ The rules are organized into the following categories:
|
|
|
60
60
|
| [`vue-no-reactive-destructuring`](./src/rules/vue-no-reactive-destructuring.md) | avoid destructuring reactive objects |
|
|
61
61
|
| [`vue-no-ref-access-in-templates`](./src/rules/vue-no-ref-access-in-templates.md) | don't use `.value` in Vue templates |
|
|
62
62
|
| [`vue-no-torefs-on-props`](./src/rules/vue-no-torefs-on-props.md) | don't use `toRefs()` on the props object |
|
|
63
|
+
| [`vue-no-reactivity-after-await`](./src/rules/vue-no-reactivity-after-await.md) | disallow subscription APIs (`watch`, `computed`, etc.) after `await` in async functions |
|
|
64
|
+
| [`vue-no-async-lifecycle-hook`](./src/rules/vue-no-async-lifecycle-hook.md) | disallow async callbacks in Vue lifecycle hooks |
|
|
63
65
|
| [`vue-no-resolve-component-in-composables`](./src/rules/vue-no-resolve-component-in-composables.ts) | disallow `resolveComponent()`/`resolveDirective()` outside top-level `<script setup>` |
|
|
64
66
|
| [`vue-require-composable-prefix`](./src/rules/vue-require-composable-prefix.ts) | enforce `use*` prefix for functions using Vue reactivity |
|
|
65
67
|
| **AI Deslop** | |
|
package/dist/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import process from 'node:process';
|
|
|
4
4
|
import { TextSourceCodeBase, ConfigCommentParser, Directive, VisitNodeStep } from '@eslint/plugin-kit';
|
|
5
5
|
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
6
6
|
|
|
7
|
-
const version = "0.
|
|
7
|
+
const version = "0.6.0";
|
|
8
8
|
|
|
9
9
|
const STRENGTH_PATTERNS = {
|
|
10
10
|
strong: ["never", "must", "always", "under no circumstances", "absolutely", "required", "mandatory", "forbidden", "prohibited"],
|
|
@@ -1531,6 +1531,8 @@ const aiDeslopCodeLang = {
|
|
|
1531
1531
|
continue;
|
|
1532
1532
|
const code = match[1];
|
|
1533
1533
|
const lastChar = code.at(-1);
|
|
1534
|
+
if (!lastChar)
|
|
1535
|
+
continue;
|
|
1534
1536
|
const lang = LANG_MAP[lastChar];
|
|
1535
1537
|
if (!lang)
|
|
1536
1538
|
continue;
|
|
@@ -1628,7 +1630,8 @@ const aiDeslopFiller = {
|
|
|
1628
1630
|
const REGEX_4$2 = /[.*+?^${}()|[\]\\]/g;
|
|
1629
1631
|
const REGEX_3$4 = /(?:\bnot|n't|no longer|more than|rather than)\s+$/;
|
|
1630
1632
|
const REGEX_2$b = /^[-*>\s#\d.]+$/;
|
|
1631
|
-
const REGEX_1$q =
|
|
1633
|
+
const REGEX_1$q = /[.:]\s+$/;
|
|
1634
|
+
const NEGATION_AFTER = /^(?:never|no|nothing|none|nobody|nowhere)\b/;
|
|
1632
1635
|
const COMPILED$2 = HEDGING_WORDS.map((word) => {
|
|
1633
1636
|
const escaped = word.replace(REGEX_4$2, "\\$&");
|
|
1634
1637
|
return { regex: new RegExp(`\\b${escaped}\\s+`, "gi"), word };
|
|
@@ -1668,6 +1671,8 @@ const aiDeslopHedging = {
|
|
|
1668
1671
|
continue;
|
|
1669
1672
|
if (word === "just" && REGEX_3$4.test(line.slice(Math.max(0, match.index - 20), match.index)))
|
|
1670
1673
|
continue;
|
|
1674
|
+
if (NEGATION_AFTER.test(line.slice(match.index + match[0].length)))
|
|
1675
|
+
continue;
|
|
1671
1676
|
const startOffset = lineNode.position.start.offset + match.index;
|
|
1672
1677
|
const endOffset = startOffset + match[0].length;
|
|
1673
1678
|
const textBefore = line.slice(0, match.index);
|
|
@@ -1800,6 +1805,8 @@ function report(context, lineIdx, col, messageId, fixStart, fixEnd, replacement)
|
|
|
1800
1805
|
|
|
1801
1806
|
const REGEX_2$a = /!(?!\[)/g;
|
|
1802
1807
|
const REGEX_1$o = /\w/;
|
|
1808
|
+
const REGEX_WORD_END = /(\w+)$/;
|
|
1809
|
+
const REGEX_UPPER_START = /^[A-Z]/;
|
|
1803
1810
|
const aiDeslopNoExclamation = {
|
|
1804
1811
|
meta: {
|
|
1805
1812
|
type: "suggestion",
|
|
@@ -1830,6 +1837,10 @@ const aiDeslopNoExclamation = {
|
|
|
1830
1837
|
continue;
|
|
1831
1838
|
if (match.index === 0 || !REGEX_1$o.test(line[match.index - 1]))
|
|
1832
1839
|
continue;
|
|
1840
|
+
const beforeBang = line.slice(0, match.index);
|
|
1841
|
+
const wordMatch = beforeBang.match(REGEX_WORD_END);
|
|
1842
|
+
if (wordMatch && REGEX_UPPER_START.test(wordMatch[1]))
|
|
1843
|
+
continue;
|
|
1833
1844
|
const startOffset = lineNode.position.start.offset + match.index;
|
|
1834
1845
|
const endOffset = startOffset + 1;
|
|
1835
1846
|
context.report({
|
|
@@ -2841,11 +2852,12 @@ const promptUnclosedCodeFence = {
|
|
|
2841
2852
|
|
|
2842
2853
|
const REGEX_2$5 = /<([a-z_][\w-]*)>/gi;
|
|
2843
2854
|
const REGEX_1$b = /<\/([a-z_][\w-]*)>/gi;
|
|
2855
|
+
const REGEX_INLINE_CODE = /`[^`]+`/g;
|
|
2844
2856
|
const promptUnclosedTag = {
|
|
2845
2857
|
meta: {
|
|
2846
2858
|
type: "problem",
|
|
2847
2859
|
docs: { description: "Detect mismatched XML-style tags" },
|
|
2848
|
-
fixable:
|
|
2860
|
+
fixable: void 0,
|
|
2849
2861
|
schema: [],
|
|
2850
2862
|
messages: {
|
|
2851
2863
|
unclosed: "Mismatched XML tag: <{{tag}}> appears {{openCount}} time(s) but </{{tag}}> appears {{closeCount}} time(s)."
|
|
@@ -2858,7 +2870,7 @@ const promptUnclosedTag = {
|
|
|
2858
2870
|
const lines = sourceCode.lines;
|
|
2859
2871
|
const codeBlockLines = getCodeBlockLines(lines);
|
|
2860
2872
|
const frontmatterEnd = getFrontmatterEnd(lines);
|
|
2861
|
-
const filteredText = lines.filter((_, i) => !shouldSkipLine(i, codeBlockLines, frontmatterEnd)).join("\n");
|
|
2873
|
+
const filteredText = lines.filter((_, i) => !shouldSkipLine(i, codeBlockLines, frontmatterEnd)).map((line) => line.replace(REGEX_INLINE_CODE, "")).join("\n");
|
|
2862
2874
|
const openTags = /* @__PURE__ */ new Map();
|
|
2863
2875
|
const closeTags = /* @__PURE__ */ new Map();
|
|
2864
2876
|
let match;
|
|
@@ -2872,22 +2884,13 @@ const promptUnclosedTag = {
|
|
|
2872
2884
|
const tag = match[1].toLowerCase();
|
|
2873
2885
|
closeTags.set(tag, (closeTags.get(tag) ?? 0) + 1);
|
|
2874
2886
|
}
|
|
2875
|
-
const docEnd = node.position.end.offset;
|
|
2876
2887
|
for (const [tag, count] of openTags) {
|
|
2877
2888
|
const closeCount = closeTags.get(tag) ?? 0;
|
|
2878
2889
|
if (count !== closeCount) {
|
|
2879
|
-
const missing = count - closeCount;
|
|
2880
2890
|
context.report({
|
|
2881
2891
|
node,
|
|
2882
2892
|
messageId: "unclosed",
|
|
2883
|
-
data: { tag, openCount: String(count), closeCount: String(closeCount) }
|
|
2884
|
-
...missing > 0 && {
|
|
2885
|
-
fix(fixer) {
|
|
2886
|
-
const closingTags = Array.from({ length: missing }).fill(`</${tag}>`).join("\n");
|
|
2887
|
-
return fixer.insertTextAfterRange([docEnd, docEnd], `
|
|
2888
|
-
${closingTags}`);
|
|
2889
|
-
}
|
|
2890
|
-
}
|
|
2893
|
+
data: { tag, openCount: String(count), closeCount: String(closeCount) }
|
|
2891
2894
|
});
|
|
2892
2895
|
}
|
|
2893
2896
|
}
|
|
@@ -3211,9 +3214,11 @@ const hasDocs = [
|
|
|
3211
3214
|
"nuxt-prefer-navigate-to-over-router-push-replace",
|
|
3212
3215
|
"nuxt-prefer-nuxt-link-over-router-link",
|
|
3213
3216
|
"use-composables-must-use-reactivity",
|
|
3217
|
+
"vue-no-async-lifecycle-hook",
|
|
3214
3218
|
"vue-no-nested-reactivity",
|
|
3215
3219
|
"vue-no-passing-refs-as-props",
|
|
3216
3220
|
"vue-no-reactive-destructuring",
|
|
3221
|
+
"vue-no-reactivity-after-await",
|
|
3217
3222
|
"vue-no-ref-access-in-templates",
|
|
3218
3223
|
"vue-no-torefs-on-props"
|
|
3219
3224
|
];
|
|
@@ -3636,9 +3641,9 @@ function createReactivityChecker(vueImports, nonVueImports) {
|
|
|
3636
3641
|
|
|
3637
3642
|
const REGEX_2$4 = /[^\u0020-\u007F]/;
|
|
3638
3643
|
const REGEX_1$6 = /[^\u0020-\u007F]/;
|
|
3639
|
-
const RULE_NAME$
|
|
3644
|
+
const RULE_NAME$p = "link-ascii-only";
|
|
3640
3645
|
const linkAsciiOnly = createEslintRule({
|
|
3641
|
-
name: RULE_NAME$
|
|
3646
|
+
name: RULE_NAME$p,
|
|
3642
3647
|
meta: {
|
|
3643
3648
|
type: "suggestion",
|
|
3644
3649
|
docs: {
|
|
@@ -3714,9 +3719,9 @@ const REGEX_4$1 = /[A-Z]/;
|
|
|
3714
3719
|
const REGEX_3$2 = /%[0-9A-F]{2}/i;
|
|
3715
3720
|
const REGEX_2$3 = /[A-Z]/;
|
|
3716
3721
|
const REGEX_1$5 = /%[0-9A-F]{2}/i;
|
|
3717
|
-
const RULE_NAME$
|
|
3722
|
+
const RULE_NAME$o = "link-lowercase";
|
|
3718
3723
|
const linkLowercase = createEslintRule({
|
|
3719
|
-
name: RULE_NAME$
|
|
3724
|
+
name: RULE_NAME$o,
|
|
3720
3725
|
meta: {
|
|
3721
3726
|
type: "suggestion",
|
|
3722
3727
|
docs: {
|
|
@@ -3795,7 +3800,7 @@ const linkLowercase = createEslintRule({
|
|
|
3795
3800
|
const REGEX_3$1 = /\/+/g;
|
|
3796
3801
|
const REGEX_2$2 = /\/{2,}/;
|
|
3797
3802
|
const REGEX_1$4 = /\/{2,}/;
|
|
3798
|
-
const RULE_NAME$
|
|
3803
|
+
const RULE_NAME$n = "link-no-double-slashes";
|
|
3799
3804
|
function fixDoubleSlashesInUrl(url) {
|
|
3800
3805
|
if (url.startsWith("//") || url.includes("://"))
|
|
3801
3806
|
return url;
|
|
@@ -3815,7 +3820,7 @@ function fixDoubleSlashesInUrl(url) {
|
|
|
3815
3820
|
return `${path.replace(REGEX_3$1, "/")}${search}${hash}`;
|
|
3816
3821
|
}
|
|
3817
3822
|
const linkNoDoubleSlashes = createEslintRule({
|
|
3818
|
-
name: RULE_NAME$
|
|
3823
|
+
name: RULE_NAME$n,
|
|
3819
3824
|
meta: {
|
|
3820
3825
|
type: "problem",
|
|
3821
3826
|
docs: {
|
|
@@ -3891,9 +3896,9 @@ const linkNoDoubleSlashes = createEslintRule({
|
|
|
3891
3896
|
}
|
|
3892
3897
|
});
|
|
3893
3898
|
|
|
3894
|
-
const RULE_NAME$
|
|
3899
|
+
const RULE_NAME$m = "link-no-underscores";
|
|
3895
3900
|
const linkNoUnderscores = createEslintRule({
|
|
3896
|
-
name: RULE_NAME$
|
|
3901
|
+
name: RULE_NAME$m,
|
|
3897
3902
|
meta: {
|
|
3898
3903
|
type: "suggestion",
|
|
3899
3904
|
docs: {
|
|
@@ -3971,9 +3976,9 @@ const REGEX_4 = /\s/;
|
|
|
3971
3976
|
const REGEX_3 = /\s/g;
|
|
3972
3977
|
const REGEX_2$1 = /\s/;
|
|
3973
3978
|
const REGEX_1$3 = /\s/g;
|
|
3974
|
-
const RULE_NAME$
|
|
3979
|
+
const RULE_NAME$l = "link-no-whitespace";
|
|
3975
3980
|
const linkNoWhitespace = createEslintRule({
|
|
3976
|
-
name: RULE_NAME$
|
|
3981
|
+
name: RULE_NAME$l,
|
|
3977
3982
|
meta: {
|
|
3978
3983
|
type: "suggestion",
|
|
3979
3984
|
docs: {
|
|
@@ -4047,7 +4052,7 @@ const linkNoWhitespace = createEslintRule({
|
|
|
4047
4052
|
}
|
|
4048
4053
|
});
|
|
4049
4054
|
|
|
4050
|
-
const RULE_NAME$
|
|
4055
|
+
const RULE_NAME$k = "link-require-descriptive-text";
|
|
4051
4056
|
const BAD_LINK_TEXTS = /* @__PURE__ */ new Set([
|
|
4052
4057
|
"click here",
|
|
4053
4058
|
"click this",
|
|
@@ -4093,7 +4098,7 @@ function getVueLinkUrl(node) {
|
|
|
4093
4098
|
return null;
|
|
4094
4099
|
}
|
|
4095
4100
|
const linkRequireDescriptiveText = createEslintRule({
|
|
4096
|
-
name: RULE_NAME$
|
|
4101
|
+
name: RULE_NAME$k,
|
|
4097
4102
|
meta: {
|
|
4098
4103
|
type: "suggestion",
|
|
4099
4104
|
docs: {
|
|
@@ -4169,9 +4174,9 @@ const linkRequireDescriptiveText = createEslintRule({
|
|
|
4169
4174
|
}
|
|
4170
4175
|
});
|
|
4171
4176
|
|
|
4172
|
-
const RULE_NAME$
|
|
4177
|
+
const RULE_NAME$j = "link-require-href";
|
|
4173
4178
|
const linkRequireHref = createEslintRule({
|
|
4174
|
-
name: RULE_NAME$
|
|
4179
|
+
name: RULE_NAME$j,
|
|
4175
4180
|
meta: {
|
|
4176
4181
|
type: "problem",
|
|
4177
4182
|
docs: {
|
|
@@ -4239,12 +4244,12 @@ const linkRequireHref = createEslintRule({
|
|
|
4239
4244
|
}
|
|
4240
4245
|
});
|
|
4241
4246
|
|
|
4242
|
-
const RULE_NAME$
|
|
4247
|
+
const RULE_NAME$i = "link-trailing-slash";
|
|
4243
4248
|
function shouldSkipUrl(url) {
|
|
4244
4249
|
return url.startsWith("#") || url.includes(":") || url === "/" || url === "";
|
|
4245
4250
|
}
|
|
4246
4251
|
const linkTrailingSlash = createEslintRule({
|
|
4247
|
-
name: RULE_NAME$
|
|
4252
|
+
name: RULE_NAME$i,
|
|
4248
4253
|
meta: {
|
|
4249
4254
|
type: "suggestion",
|
|
4250
4255
|
docs: {
|
|
@@ -4359,9 +4364,9 @@ const linkTrailingSlash = createEslintRule({
|
|
|
4359
4364
|
}
|
|
4360
4365
|
});
|
|
4361
4366
|
|
|
4362
|
-
const RULE_NAME$
|
|
4367
|
+
const RULE_NAME$h = "nuxt-await-navigate-to";
|
|
4363
4368
|
const nuxtAwaitNavigateTo = createEslintRule({
|
|
4364
|
-
name: RULE_NAME$
|
|
4369
|
+
name: RULE_NAME$h,
|
|
4365
4370
|
meta: {
|
|
4366
4371
|
type: "problem",
|
|
4367
4372
|
docs: {
|
|
@@ -4517,7 +4522,7 @@ function isInConsequentBranch(node, ternary) {
|
|
|
4517
4522
|
return false;
|
|
4518
4523
|
}
|
|
4519
4524
|
|
|
4520
|
-
const RULE_NAME$
|
|
4525
|
+
const RULE_NAME$g = "nuxt-no-random";
|
|
4521
4526
|
function isMathRandomCall(node) {
|
|
4522
4527
|
return node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Math" && node.callee.property.type === "Identifier" && node.callee.property.name === "random";
|
|
4523
4528
|
}
|
|
@@ -4548,7 +4553,7 @@ function reportRandom(context, node, template) {
|
|
|
4548
4553
|
}
|
|
4549
4554
|
}
|
|
4550
4555
|
const nuxtNoRandom = createEslintRule({
|
|
4551
|
-
name: RULE_NAME$
|
|
4556
|
+
name: RULE_NAME$g,
|
|
4552
4557
|
meta: {
|
|
4553
4558
|
type: "problem",
|
|
4554
4559
|
docs: {
|
|
@@ -4587,9 +4592,9 @@ const nuxtNoRandom = createEslintRule({
|
|
|
4587
4592
|
}
|
|
4588
4593
|
});
|
|
4589
4594
|
|
|
4590
|
-
const RULE_NAME$
|
|
4595
|
+
const RULE_NAME$f = "nuxt-no-redundant-import-meta";
|
|
4591
4596
|
const nuxtNoRedundantImportMeta = createEslintRule({
|
|
4592
|
-
name: RULE_NAME$
|
|
4597
|
+
name: RULE_NAME$f,
|
|
4593
4598
|
meta: {
|
|
4594
4599
|
type: "problem",
|
|
4595
4600
|
docs: {
|
|
@@ -4627,7 +4632,7 @@ const nuxtNoRedundantImportMeta = createEslintRule({
|
|
|
4627
4632
|
});
|
|
4628
4633
|
|
|
4629
4634
|
const REGEX_1$2 = /\n\s*\n\s*\n/g;
|
|
4630
|
-
const RULE_NAME$
|
|
4635
|
+
const RULE_NAME$e = "nuxt-no-side-effects-in-async-data-handler";
|
|
4631
4636
|
const SIDE_EFFECT_PATTERNS = /* @__PURE__ */ new Set([
|
|
4632
4637
|
// Store/State mutations ($ prefix makes these unambiguous)
|
|
4633
4638
|
"$patch",
|
|
@@ -4737,7 +4742,7 @@ function findSideEffectsInFunction(functionNode) {
|
|
|
4737
4742
|
return sideEffects;
|
|
4738
4743
|
}
|
|
4739
4744
|
const nuxtNoSideEffectsInAsyncDataHandler = createEslintRule({
|
|
4740
|
-
name: RULE_NAME$
|
|
4745
|
+
name: RULE_NAME$e,
|
|
4741
4746
|
meta: {
|
|
4742
4747
|
type: "problem",
|
|
4743
4748
|
docs: {
|
|
@@ -4828,9 +4833,9 @@ ${indent}${callOnceBlock}`)
|
|
|
4828
4833
|
}
|
|
4829
4834
|
});
|
|
4830
4835
|
|
|
4831
|
-
const RULE_NAME$
|
|
4836
|
+
const RULE_NAME$d = "nuxt-no-side-effects-in-setup";
|
|
4832
4837
|
const nuxtNoSideEffectsInSetup = createEslintRule({
|
|
4833
|
-
name: RULE_NAME$
|
|
4838
|
+
name: RULE_NAME$d,
|
|
4834
4839
|
meta: {
|
|
4835
4840
|
type: "problem",
|
|
4836
4841
|
docs: {
|
|
@@ -4921,7 +4926,7 @@ ${indent}})`;
|
|
|
4921
4926
|
}
|
|
4922
4927
|
});
|
|
4923
4928
|
|
|
4924
|
-
const RULE_NAME$
|
|
4929
|
+
const RULE_NAME$c = "nuxt-no-unsafe-date";
|
|
4925
4930
|
function isDateNowCall(node) {
|
|
4926
4931
|
return node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Date" && node.callee.property.type === "Identifier" && node.callee.property.name === "now";
|
|
4927
4932
|
}
|
|
@@ -4952,7 +4957,7 @@ function isChainedWithStableMethod(node) {
|
|
|
4952
4957
|
return grandparent?.type === "CallExpression" && grandparent.callee === parent;
|
|
4953
4958
|
}
|
|
4954
4959
|
const nuxtNoUnsafeDate = createEslintRule({
|
|
4955
|
-
name: RULE_NAME$
|
|
4960
|
+
name: RULE_NAME$c,
|
|
4956
4961
|
meta: {
|
|
4957
4962
|
type: "problem",
|
|
4958
4963
|
docs: {
|
|
@@ -5016,9 +5021,9 @@ const nuxtNoUnsafeDate = createEslintRule({
|
|
|
5016
5021
|
}
|
|
5017
5022
|
});
|
|
5018
5023
|
|
|
5019
|
-
const RULE_NAME$
|
|
5024
|
+
const RULE_NAME$b = "nuxt-prefer-navigate-to-over-router-push-replace";
|
|
5020
5025
|
const nuxtPreferNavigateToOverRouterPushReplace = createEslintRule({
|
|
5021
|
-
name: RULE_NAME$
|
|
5026
|
+
name: RULE_NAME$b,
|
|
5022
5027
|
meta: {
|
|
5023
5028
|
type: "suggestion",
|
|
5024
5029
|
docs: {
|
|
@@ -5083,9 +5088,9 @@ const nuxtPreferNavigateToOverRouterPushReplace = createEslintRule({
|
|
|
5083
5088
|
|
|
5084
5089
|
const REGEX_2 = /router-link|RouterLink/gi;
|
|
5085
5090
|
const REGEX_1$1 = /router-link|RouterLink/gi;
|
|
5086
|
-
const RULE_NAME$
|
|
5091
|
+
const RULE_NAME$a = "nuxt-prefer-nuxt-link-over-router-link";
|
|
5087
5092
|
const nuxtPreferNuxtLinkOverRouterLink = createEslintRule({
|
|
5088
|
-
name: RULE_NAME$
|
|
5093
|
+
name: RULE_NAME$a,
|
|
5089
5094
|
meta: {
|
|
5090
5095
|
type: "suggestion",
|
|
5091
5096
|
docs: {
|
|
@@ -5156,7 +5161,7 @@ const nuxtPreferNuxtLinkOverRouterLink = createEslintRule({
|
|
|
5156
5161
|
}
|
|
5157
5162
|
});
|
|
5158
5163
|
|
|
5159
|
-
const RULE_NAME$
|
|
5164
|
+
const RULE_NAME$9 = "nuxt-ui-prefer-shorthand-css";
|
|
5160
5165
|
const REPLACEMENTS = {
|
|
5161
5166
|
// text
|
|
5162
5167
|
"text-[var(--ui-text)]": "text-default",
|
|
@@ -5213,7 +5218,7 @@ function findVerboseClasses(value) {
|
|
|
5213
5218
|
return results;
|
|
5214
5219
|
}
|
|
5215
5220
|
const nuxtUiPreferShorthandCss = createEslintRule({
|
|
5216
|
-
name: RULE_NAME$
|
|
5221
|
+
name: RULE_NAME$9,
|
|
5217
5222
|
meta: {
|
|
5218
5223
|
type: "suggestion",
|
|
5219
5224
|
docs: {
|
|
@@ -5362,9 +5367,60 @@ const nuxtUiPreferShorthandCss = createEslintRule({
|
|
|
5362
5367
|
}
|
|
5363
5368
|
});
|
|
5364
5369
|
|
|
5365
|
-
const RULE_NAME$
|
|
5370
|
+
const RULE_NAME$8 = "vue-no-async-lifecycle-hook";
|
|
5371
|
+
const LIFECYCLE_HOOKS = /* @__PURE__ */ new Set([
|
|
5372
|
+
"onBeforeMount",
|
|
5373
|
+
"onMounted",
|
|
5374
|
+
"onBeforeUnmount",
|
|
5375
|
+
"onUnmounted",
|
|
5376
|
+
"onBeforeUpdate",
|
|
5377
|
+
"onUpdated",
|
|
5378
|
+
"onActivated",
|
|
5379
|
+
"onDeactivated",
|
|
5380
|
+
"onErrorCaptured",
|
|
5381
|
+
"onRenderTracked",
|
|
5382
|
+
"onRenderTriggered"
|
|
5383
|
+
// Note: onServerPrefetch is intentionally excluded — Vue awaits it
|
|
5384
|
+
]);
|
|
5385
|
+
const vueNoAsyncLifecycleHook = createEslintRule({
|
|
5386
|
+
name: RULE_NAME$8,
|
|
5387
|
+
meta: {
|
|
5388
|
+
type: "problem",
|
|
5389
|
+
docs: {
|
|
5390
|
+
description: "disallow async callbacks in Vue lifecycle hooks \u2014 code after await runs as a detached microtask that Vue does not await"
|
|
5391
|
+
},
|
|
5392
|
+
schema: [],
|
|
5393
|
+
messages: {
|
|
5394
|
+
noAsyncLifecycleHook: "{{hook}}() with an async callback is unsafe \u2014 Vue does not await lifecycle callbacks, so code after await runs as a detached microtask that may not execute during the lifecycle phase."
|
|
5395
|
+
}
|
|
5396
|
+
},
|
|
5397
|
+
defaultOptions: [],
|
|
5398
|
+
create: (context) => {
|
|
5399
|
+
return {
|
|
5400
|
+
CallExpression(node) {
|
|
5401
|
+
if (node.callee.type !== "Identifier")
|
|
5402
|
+
return;
|
|
5403
|
+
const hook = node.callee.name;
|
|
5404
|
+
if (!LIFECYCLE_HOOKS.has(hook))
|
|
5405
|
+
return;
|
|
5406
|
+
const callback = node.arguments[0];
|
|
5407
|
+
if (!callback)
|
|
5408
|
+
return;
|
|
5409
|
+
if ((callback.type === "ArrowFunctionExpression" || callback.type === "FunctionExpression") && callback.async) {
|
|
5410
|
+
context.report({
|
|
5411
|
+
node,
|
|
5412
|
+
messageId: "noAsyncLifecycleHook",
|
|
5413
|
+
data: { hook }
|
|
5414
|
+
});
|
|
5415
|
+
}
|
|
5416
|
+
}
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
5419
|
+
});
|
|
5420
|
+
|
|
5421
|
+
const RULE_NAME$7 = "vue-no-faux-composables";
|
|
5366
5422
|
const vueNoFauxComposables = createEslintRule({
|
|
5367
|
-
name: RULE_NAME$
|
|
5423
|
+
name: RULE_NAME$7,
|
|
5368
5424
|
meta: {
|
|
5369
5425
|
type: "problem",
|
|
5370
5426
|
docs: {
|
|
@@ -5433,9 +5489,9 @@ const vueNoFauxComposables = createEslintRule({
|
|
|
5433
5489
|
}
|
|
5434
5490
|
});
|
|
5435
5491
|
|
|
5436
|
-
const RULE_NAME$
|
|
5492
|
+
const RULE_NAME$6 = "vue-no-nested-reactivity";
|
|
5437
5493
|
const vueNoNestedReactivity = createEslintRule({
|
|
5438
|
-
name: RULE_NAME$
|
|
5494
|
+
name: RULE_NAME$6,
|
|
5439
5495
|
meta: {
|
|
5440
5496
|
type: "problem",
|
|
5441
5497
|
docs: {
|
|
@@ -5680,9 +5736,9 @@ const vueNoNestedReactivity = createEslintRule({
|
|
|
5680
5736
|
}
|
|
5681
5737
|
});
|
|
5682
5738
|
|
|
5683
|
-
const RULE_NAME$
|
|
5739
|
+
const RULE_NAME$5 = "vue-no-passing-refs-as-props";
|
|
5684
5740
|
const vueNoPassingRefsAsProps = createEslintRule({
|
|
5685
|
-
name: RULE_NAME$
|
|
5741
|
+
name: RULE_NAME$5,
|
|
5686
5742
|
meta: {
|
|
5687
5743
|
type: "problem",
|
|
5688
5744
|
docs: {
|
|
@@ -5789,6 +5845,127 @@ const vueNoReactiveDestructuring = createEslintRule({
|
|
|
5789
5845
|
}
|
|
5790
5846
|
});
|
|
5791
5847
|
|
|
5848
|
+
const RULE_NAME$4 = "vue-no-reactivity-after-await";
|
|
5849
|
+
const SUBSCRIPTION_APIS = /* @__PURE__ */ new Set([
|
|
5850
|
+
// Vue watchers
|
|
5851
|
+
"watch",
|
|
5852
|
+
"watchEffect",
|
|
5853
|
+
"watchPostEffect",
|
|
5854
|
+
"watchSyncEffect",
|
|
5855
|
+
// Vue computed
|
|
5856
|
+
"computed",
|
|
5857
|
+
// VueUse watchers
|
|
5858
|
+
"whenever",
|
|
5859
|
+
"watchArray",
|
|
5860
|
+
"watchAtMost",
|
|
5861
|
+
"watchDebounced",
|
|
5862
|
+
"watchDeep",
|
|
5863
|
+
"watchIgnorable",
|
|
5864
|
+
"watchImmediate",
|
|
5865
|
+
"watchOnce",
|
|
5866
|
+
"watchPausable",
|
|
5867
|
+
"watchThrottled",
|
|
5868
|
+
"watchTriggerable",
|
|
5869
|
+
"watchWithFilter",
|
|
5870
|
+
"debouncedWatch",
|
|
5871
|
+
"throttledWatch",
|
|
5872
|
+
// VueUse computed
|
|
5873
|
+
"computedAsync",
|
|
5874
|
+
"computedEager",
|
|
5875
|
+
"computedInject",
|
|
5876
|
+
"computedWithControl",
|
|
5877
|
+
"reactiveComputed"
|
|
5878
|
+
]);
|
|
5879
|
+
function getEnclosingAsyncFunction(node) {
|
|
5880
|
+
let parent = node.parent;
|
|
5881
|
+
while (parent) {
|
|
5882
|
+
if (parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression") {
|
|
5883
|
+
return parent.async ? parent : null;
|
|
5884
|
+
}
|
|
5885
|
+
parent = parent.parent;
|
|
5886
|
+
}
|
|
5887
|
+
return null;
|
|
5888
|
+
}
|
|
5889
|
+
function hasAwaitBefore(callNode, asyncFn) {
|
|
5890
|
+
const body = asyncFn.body;
|
|
5891
|
+
if (body.type !== "BlockStatement")
|
|
5892
|
+
return false;
|
|
5893
|
+
const callStart = callNode.range[0];
|
|
5894
|
+
let found = false;
|
|
5895
|
+
function walkStatements(stmts) {
|
|
5896
|
+
for (const stmt of stmts) {
|
|
5897
|
+
if (found)
|
|
5898
|
+
return;
|
|
5899
|
+
walkNode(stmt);
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
5902
|
+
function walkNode(node) {
|
|
5903
|
+
if (found)
|
|
5904
|
+
return;
|
|
5905
|
+
if (node.range[0] >= callStart)
|
|
5906
|
+
return;
|
|
5907
|
+
if (node.type === "AwaitExpression") {
|
|
5908
|
+
found = true;
|
|
5909
|
+
return;
|
|
5910
|
+
}
|
|
5911
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
5912
|
+
return;
|
|
5913
|
+
}
|
|
5914
|
+
for (const key of Object.keys(node)) {
|
|
5915
|
+
if (key === "parent")
|
|
5916
|
+
continue;
|
|
5917
|
+
const child = node[key];
|
|
5918
|
+
if (child && typeof child === "object") {
|
|
5919
|
+
if (Array.isArray(child)) {
|
|
5920
|
+
for (const item of child) {
|
|
5921
|
+
if (item && typeof item.type === "string")
|
|
5922
|
+
walkNode(item);
|
|
5923
|
+
}
|
|
5924
|
+
} else if (typeof child.type === "string") {
|
|
5925
|
+
walkNode(child);
|
|
5926
|
+
}
|
|
5927
|
+
}
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5930
|
+
walkStatements(body.body);
|
|
5931
|
+
return found;
|
|
5932
|
+
}
|
|
5933
|
+
const vueNoReactivityAfterAwait = createEslintRule({
|
|
5934
|
+
name: RULE_NAME$4,
|
|
5935
|
+
meta: {
|
|
5936
|
+
type: "problem",
|
|
5937
|
+
docs: {
|
|
5938
|
+
description: "disallow Vue subscription APIs (watch, computed, etc.) after await in async functions \u2014 they lose component instance context and become orphaned"
|
|
5939
|
+
},
|
|
5940
|
+
schema: [],
|
|
5941
|
+
messages: {
|
|
5942
|
+
noReactivityAfterAwait: "{{name}}() called after await loses Vue component instance context and will never be auto-stopped on unmount. Move it before any await or wrap in effectScope()."
|
|
5943
|
+
}
|
|
5944
|
+
},
|
|
5945
|
+
defaultOptions: [],
|
|
5946
|
+
create: (context) => {
|
|
5947
|
+
return {
|
|
5948
|
+
CallExpression(node) {
|
|
5949
|
+
if (node.callee.type !== "Identifier")
|
|
5950
|
+
return;
|
|
5951
|
+
const name = node.callee.name;
|
|
5952
|
+
if (!SUBSCRIPTION_APIS.has(name))
|
|
5953
|
+
return;
|
|
5954
|
+
const asyncFn = getEnclosingAsyncFunction(node);
|
|
5955
|
+
if (!asyncFn)
|
|
5956
|
+
return;
|
|
5957
|
+
if (hasAwaitBefore(node, asyncFn)) {
|
|
5958
|
+
context.report({
|
|
5959
|
+
node,
|
|
5960
|
+
messageId: "noReactivityAfterAwait",
|
|
5961
|
+
data: { name }
|
|
5962
|
+
});
|
|
5963
|
+
}
|
|
5964
|
+
}
|
|
5965
|
+
};
|
|
5966
|
+
}
|
|
5967
|
+
});
|
|
5968
|
+
|
|
5792
5969
|
const RULE_NAME$3 = "vue-no-ref-access-in-templates";
|
|
5793
5970
|
const vueNoRefAccessInTemplates = createEslintRule({
|
|
5794
5971
|
name: RULE_NAME$3,
|
|
@@ -6162,10 +6339,12 @@ const plugin = {
|
|
|
6162
6339
|
"prompt-unresolved-reference": promptUnresolvedReference,
|
|
6163
6340
|
"prompt-vague-term": promptVagueTerm,
|
|
6164
6341
|
"prompt-weak-instruction": promptWeakInstruction,
|
|
6342
|
+
"vue-no-async-lifecycle-hook": vueNoAsyncLifecycleHook,
|
|
6165
6343
|
"vue-no-faux-composables": vueNoFauxComposables,
|
|
6166
6344
|
"vue-no-nested-reactivity": vueNoNestedReactivity,
|
|
6167
6345
|
"vue-no-passing-refs-as-props": vueNoPassingRefsAsProps,
|
|
6168
6346
|
"vue-no-reactive-destructuring": vueNoReactiveDestructuring,
|
|
6347
|
+
"vue-no-reactivity-after-await": vueNoReactivityAfterAwait,
|
|
6169
6348
|
"vue-no-ref-access-in-templates": vueNoRefAccessInTemplates,
|
|
6170
6349
|
"vue-no-resolve-component-in-composables": vueNoResolveComponentInComposables,
|
|
6171
6350
|
"vue-no-torefs-on-props": vueNoTorefsOnProps,
|
|
@@ -6310,10 +6489,12 @@ plugin.configs.vue = [
|
|
|
6310
6489
|
ignores: CODE_IGNORES,
|
|
6311
6490
|
plugins: { harlanzw: plugin },
|
|
6312
6491
|
rules: {
|
|
6492
|
+
"harlanzw/vue-no-async-lifecycle-hook": "error",
|
|
6313
6493
|
"harlanzw/vue-no-faux-composables": "error",
|
|
6314
6494
|
"harlanzw/vue-no-nested-reactivity": "error",
|
|
6315
6495
|
"harlanzw/vue-no-passing-refs-as-props": "error",
|
|
6316
6496
|
"harlanzw/vue-no-reactive-destructuring": "error",
|
|
6497
|
+
"harlanzw/vue-no-reactivity-after-await": "error",
|
|
6317
6498
|
"harlanzw/vue-no-ref-access-in-templates": "warn",
|
|
6318
6499
|
"harlanzw/vue-no-resolve-component-in-composables": "error",
|
|
6319
6500
|
"harlanzw/vue-no-torefs-on-props": "warn",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-harlanzw",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"description": "Harlan's opinionated ESLint rules",
|
|
6
6
|
"author": "Harlan Wilton <harlan@harlanzw.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"@eslint/plugin-kit": "^0.6.1"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
-
"@antfu/eslint-config": "^7.7.
|
|
35
|
+
"@antfu/eslint-config": "^7.7.3",
|
|
36
36
|
"@antfu/ni": "^29.0.0",
|
|
37
37
|
"@antfu/utils": "^9.3.0",
|
|
38
38
|
"@types/eslint": "^9.6.1",
|
|
39
39
|
"@types/node": "^25.5.0",
|
|
40
|
-
"@typescript-eslint/typescript-estree": "^8.57.
|
|
41
|
-
"@typescript-eslint/utils": "^8.57.
|
|
42
|
-
"bumpp": "^11.0.
|
|
40
|
+
"@typescript-eslint/typescript-estree": "^8.57.1",
|
|
41
|
+
"@typescript-eslint/utils": "^8.57.1",
|
|
42
|
+
"bumpp": "^11.0.1",
|
|
43
43
|
"eslint": "^10.0.3",
|
|
44
44
|
"eslint-vitest-rule-tester": "^3.1.0",
|
|
45
45
|
"jsonc-eslint-parser": "^3.1.0",
|