eslint-plugin-harlanzw 0.2.6 → 0.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.d.mts +5 -0
- package/dist/index.mjs +637 -113
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -39,6 +39,11 @@ interface HarlanzwOptions {
|
|
|
39
39
|
declare function harlanzw(options?: HarlanzwOptions, ...extraConfigs: Linter.Config[]): Linter.Config[];
|
|
40
40
|
declare namespace harlanzw {
|
|
41
41
|
var plugin;
|
|
42
|
+
var detectFramework: () => {
|
|
43
|
+
nuxt: boolean;
|
|
44
|
+
vue: boolean;
|
|
45
|
+
prompt: boolean;
|
|
46
|
+
};
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
type RuleDefinitions = typeof plugin['rules'];
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
1
4
|
import { TextSourceCodeBase, ConfigCommentParser, Directive, VisitNodeStep } from '@eslint/plugin-kit';
|
|
2
5
|
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
3
6
|
|
|
4
|
-
const version = "0.
|
|
7
|
+
const version = "0.4.0";
|
|
5
8
|
|
|
6
9
|
const STRENGTH_PATTERNS = {
|
|
7
10
|
strong: ["never", "must", "always", "under no circumstances", "absolutely", "required", "mandatory", "forbidden", "prohibited"],
|
|
@@ -70,18 +73,32 @@ const COMMON_CONTEXT_VARIABLES = /* @__PURE__ */ new Set([
|
|
|
70
73
|
"branch"
|
|
71
74
|
]);
|
|
72
75
|
const PROMPT_FILES = [
|
|
76
|
+
// Claude Code — root + nested CLAUDE.md are auto-loaded
|
|
73
77
|
"**/CLAUDE.md",
|
|
74
|
-
|
|
75
|
-
"**/
|
|
78
|
+
// Claude Code — skills
|
|
79
|
+
"**/SKILL.md",
|
|
80
|
+
// Claude Code — commands (loaded on invoke)
|
|
81
|
+
"**/.claude/commands/**/*.md",
|
|
82
|
+
// Gemini
|
|
83
|
+
"**/.gemini/style_guide.md",
|
|
84
|
+
"**/GEMINI.md",
|
|
85
|
+
// GitHub Copilot
|
|
76
86
|
"**/.github/copilot-instructions.md",
|
|
87
|
+
// Cursor
|
|
77
88
|
"**/.cursorrules",
|
|
78
89
|
"**/.cursor/rules/**/*.md",
|
|
79
90
|
"**/.cursor/rules/**/*.mdc",
|
|
80
|
-
|
|
91
|
+
// Windsurf
|
|
81
92
|
"**/.windsurfrules",
|
|
93
|
+
// Cline
|
|
82
94
|
"**/.clinerules",
|
|
95
|
+
// Goose
|
|
83
96
|
"**/.goose/instructions.md",
|
|
97
|
+
// Amp
|
|
84
98
|
"**/.amp/RULES.md",
|
|
99
|
+
// Codex
|
|
100
|
+
"**/AGENTS.md",
|
|
101
|
+
// Generic prompt files
|
|
85
102
|
"**/*.prompt",
|
|
86
103
|
"**/*.prompt.md"
|
|
87
104
|
];
|
|
@@ -1449,8 +1466,10 @@ const hasDocs = [
|
|
|
1449
1466
|
"link-require-descriptive-text",
|
|
1450
1467
|
"link-require-href",
|
|
1451
1468
|
"link-trailing-slash",
|
|
1469
|
+
"nuxt-no-random",
|
|
1452
1470
|
"nuxt-no-redundant-import-meta",
|
|
1453
1471
|
"nuxt-no-side-effects-in-setup",
|
|
1472
|
+
"nuxt-no-unsafe-date",
|
|
1454
1473
|
"nuxt-prefer-navigate-to-over-router-push-replace",
|
|
1455
1474
|
"nuxt-prefer-nuxt-link-over-router-link",
|
|
1456
1475
|
"use-composables-must-use-reactivity",
|
|
@@ -1617,6 +1636,57 @@ const VUE_REACTIVITY_APIS = /* @__PURE__ */ new Set([
|
|
|
1617
1636
|
"getCurrentScope",
|
|
1618
1637
|
"onScopeDispose"
|
|
1619
1638
|
]);
|
|
1639
|
+
const VUEUSE_REACTIVITY_APIS = /* @__PURE__ */ new Set([
|
|
1640
|
+
// Watch variants
|
|
1641
|
+
"whenever",
|
|
1642
|
+
"watchArray",
|
|
1643
|
+
"watchAtMost",
|
|
1644
|
+
"watchDebounced",
|
|
1645
|
+
"watchDeep",
|
|
1646
|
+
"watchIgnorable",
|
|
1647
|
+
"watchImmediate",
|
|
1648
|
+
"watchOnce",
|
|
1649
|
+
"watchPausable",
|
|
1650
|
+
"watchThrottled",
|
|
1651
|
+
"watchTriggerable",
|
|
1652
|
+
"watchWithFilter",
|
|
1653
|
+
"debouncedWatch",
|
|
1654
|
+
"throttledWatch",
|
|
1655
|
+
"until",
|
|
1656
|
+
// Computed variants
|
|
1657
|
+
"computedAsync",
|
|
1658
|
+
"computedEager",
|
|
1659
|
+
"computedInject",
|
|
1660
|
+
"computedWithControl",
|
|
1661
|
+
// Reactive utilities
|
|
1662
|
+
"reactiveComputed",
|
|
1663
|
+
"reactiveOmit",
|
|
1664
|
+
"reactivePick",
|
|
1665
|
+
"toReactive",
|
|
1666
|
+
// Ref utilities
|
|
1667
|
+
"controlledRef",
|
|
1668
|
+
"debouncedRef",
|
|
1669
|
+
"throttledRef",
|
|
1670
|
+
"refAutoReset",
|
|
1671
|
+
"refDebounced",
|
|
1672
|
+
"refDefault",
|
|
1673
|
+
"refThrottled",
|
|
1674
|
+
"refWithControl",
|
|
1675
|
+
"extendRef",
|
|
1676
|
+
"syncRef",
|
|
1677
|
+
"syncRefs",
|
|
1678
|
+
"templateRef",
|
|
1679
|
+
// State management
|
|
1680
|
+
"createGlobalState",
|
|
1681
|
+
"createInjectionState",
|
|
1682
|
+
"createSharedComposable",
|
|
1683
|
+
// Event/lifecycle hooks
|
|
1684
|
+
"onClickOutside",
|
|
1685
|
+
"onKeyStroke",
|
|
1686
|
+
"onLongPress",
|
|
1687
|
+
"onStartTyping",
|
|
1688
|
+
"createEventHook"
|
|
1689
|
+
]);
|
|
1620
1690
|
const SIDE_EFFECT_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
1621
1691
|
// Timer functions
|
|
1622
1692
|
"setTimeout",
|
|
@@ -1728,10 +1798,99 @@ function trackVueImports(node, vueImports) {
|
|
|
1728
1798
|
});
|
|
1729
1799
|
}
|
|
1730
1800
|
}
|
|
1801
|
+
function trackNonVueImports(node, nonVueImports) {
|
|
1802
|
+
if (node.source.value !== "vue") {
|
|
1803
|
+
for (const spec of node.specifiers) {
|
|
1804
|
+
if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier") {
|
|
1805
|
+
nonVueImports.add(spec.imported.name);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
function createReactivityChecker(vueImports, nonVueImports) {
|
|
1811
|
+
function isAutoImportedReactivityCall(node) {
|
|
1812
|
+
if (node.callee.type === "Identifier") {
|
|
1813
|
+
const name = node.callee.name;
|
|
1814
|
+
return (VUE_REACTIVITY_APIS.has(name) || VUEUSE_REACTIVITY_APIS.has(name)) && !nonVueImports.has(name);
|
|
1815
|
+
}
|
|
1816
|
+
return false;
|
|
1817
|
+
}
|
|
1818
|
+
function isReactiveLifecycleCall(node) {
|
|
1819
|
+
return node.callee.type === "Identifier" && /^tryOn[A-Z]/.test(node.callee.name);
|
|
1820
|
+
}
|
|
1821
|
+
function hasReactivityInExpression(expr) {
|
|
1822
|
+
if (!expr)
|
|
1823
|
+
return false;
|
|
1824
|
+
switch (expr.type) {
|
|
1825
|
+
case "CallExpression":
|
|
1826
|
+
if (isReactivityCall(expr, vueImports) || isAutoImportedReactivityCall(expr) || isComposableCall(expr) || isReactiveLifecycleCall(expr))
|
|
1827
|
+
return true;
|
|
1828
|
+
return expr.arguments.some((arg) => hasReactivityInArg(arg));
|
|
1829
|
+
case "NewExpression":
|
|
1830
|
+
return expr.arguments.some((arg) => hasReactivityInArg(arg));
|
|
1831
|
+
case "MemberExpression":
|
|
1832
|
+
return hasReactivityInExpression(expr.object);
|
|
1833
|
+
case "AssignmentExpression":
|
|
1834
|
+
return hasReactivityInExpression(expr.right);
|
|
1835
|
+
case "ObjectExpression":
|
|
1836
|
+
return expr.properties.some((prop) => prop.type === "Property" && hasReactivityInExpression(prop.value));
|
|
1837
|
+
case "ArrayExpression":
|
|
1838
|
+
return expr.elements.some((elem) => hasReactivityInExpression(elem));
|
|
1839
|
+
case "AwaitExpression":
|
|
1840
|
+
return hasReactivityInExpression(expr.argument);
|
|
1841
|
+
case "ConditionalExpression":
|
|
1842
|
+
return hasReactivityInExpression(expr.consequent) || hasReactivityInExpression(expr.alternate);
|
|
1843
|
+
case "LogicalExpression":
|
|
1844
|
+
return hasReactivityInExpression(expr.left) || hasReactivityInExpression(expr.right);
|
|
1845
|
+
default:
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
function hasReactivityInArg(arg) {
|
|
1850
|
+
if (arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression") {
|
|
1851
|
+
if (arg.body.type === "BlockStatement")
|
|
1852
|
+
return arg.body.body.some((stmt) => hasReactivityInStatement(stmt));
|
|
1853
|
+
return hasReactivityInExpression(arg.body);
|
|
1854
|
+
}
|
|
1855
|
+
if (arg.type === "SpreadElement")
|
|
1856
|
+
return hasReactivityInExpression(arg.argument);
|
|
1857
|
+
return hasReactivityInExpression(arg);
|
|
1858
|
+
}
|
|
1859
|
+
function hasReactivityInStatement(stmt) {
|
|
1860
|
+
if (!stmt)
|
|
1861
|
+
return false;
|
|
1862
|
+
switch (stmt.type) {
|
|
1863
|
+
case "ExpressionStatement":
|
|
1864
|
+
return hasReactivityInExpression(stmt.expression);
|
|
1865
|
+
case "VariableDeclaration":
|
|
1866
|
+
return stmt.declarations.some((decl) => hasReactivityInExpression(decl.init));
|
|
1867
|
+
case "ReturnStatement":
|
|
1868
|
+
return hasReactivityInExpression(stmt.argument);
|
|
1869
|
+
case "BlockStatement":
|
|
1870
|
+
return stmt.body.some((s) => hasReactivityInStatement(s));
|
|
1871
|
+
case "IfStatement":
|
|
1872
|
+
return hasReactivityInStatement(stmt.consequent) || (stmt.alternate ? hasReactivityInStatement(stmt.alternate) : false);
|
|
1873
|
+
case "WhileStatement":
|
|
1874
|
+
case "DoWhileStatement":
|
|
1875
|
+
return hasReactivityInStatement(stmt.body);
|
|
1876
|
+
case "ForStatement":
|
|
1877
|
+
case "ForInStatement":
|
|
1878
|
+
case "ForOfStatement":
|
|
1879
|
+
return hasReactivityInStatement(stmt.body);
|
|
1880
|
+
case "TryStatement":
|
|
1881
|
+
return hasReactivityInStatement(stmt.block) || (stmt.handler ? hasReactivityInStatement(stmt.handler.body) : false) || (stmt.finalizer ? hasReactivityInStatement(stmt.finalizer) : false);
|
|
1882
|
+
case "SwitchStatement":
|
|
1883
|
+
return stmt.cases.some((switchCase) => switchCase.consequent.some((s) => hasReactivityInStatement(s)));
|
|
1884
|
+
default:
|
|
1885
|
+
return false;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
return { hasReactivityInStatement, hasReactivityInExpression };
|
|
1889
|
+
}
|
|
1731
1890
|
|
|
1732
|
-
const RULE_NAME$
|
|
1891
|
+
const RULE_NAME$l = "link-ascii-only";
|
|
1733
1892
|
const linkAsciiOnly = createEslintRule({
|
|
1734
|
-
name: RULE_NAME$
|
|
1893
|
+
name: RULE_NAME$l,
|
|
1735
1894
|
meta: {
|
|
1736
1895
|
type: "suggestion",
|
|
1737
1896
|
docs: {
|
|
@@ -1803,9 +1962,9 @@ const linkAsciiOnly = createEslintRule({
|
|
|
1803
1962
|
}
|
|
1804
1963
|
});
|
|
1805
1964
|
|
|
1806
|
-
const RULE_NAME$
|
|
1965
|
+
const RULE_NAME$k = "link-lowercase";
|
|
1807
1966
|
const linkLowercase = createEslintRule({
|
|
1808
|
-
name: RULE_NAME$
|
|
1967
|
+
name: RULE_NAME$k,
|
|
1809
1968
|
meta: {
|
|
1810
1969
|
type: "suggestion",
|
|
1811
1970
|
docs: {
|
|
@@ -1881,7 +2040,7 @@ const linkLowercase = createEslintRule({
|
|
|
1881
2040
|
}
|
|
1882
2041
|
});
|
|
1883
2042
|
|
|
1884
|
-
const RULE_NAME$
|
|
2043
|
+
const RULE_NAME$j = "link-no-double-slashes";
|
|
1885
2044
|
function fixDoubleSlashesInUrl(url) {
|
|
1886
2045
|
if (url.startsWith("//") || url.includes("://"))
|
|
1887
2046
|
return url;
|
|
@@ -1901,7 +2060,7 @@ function fixDoubleSlashesInUrl(url) {
|
|
|
1901
2060
|
return `${path.replace(/\/+/g, "/")}${search}${hash}`;
|
|
1902
2061
|
}
|
|
1903
2062
|
const linkNoDoubleSlashes = createEslintRule({
|
|
1904
|
-
name: RULE_NAME$
|
|
2063
|
+
name: RULE_NAME$j,
|
|
1905
2064
|
meta: {
|
|
1906
2065
|
type: "problem",
|
|
1907
2066
|
docs: {
|
|
@@ -1977,9 +2136,9 @@ const linkNoDoubleSlashes = createEslintRule({
|
|
|
1977
2136
|
}
|
|
1978
2137
|
});
|
|
1979
2138
|
|
|
1980
|
-
const RULE_NAME$
|
|
2139
|
+
const RULE_NAME$i = "link-no-underscores";
|
|
1981
2140
|
const linkNoUnderscores = createEslintRule({
|
|
1982
|
-
name: RULE_NAME$
|
|
2141
|
+
name: RULE_NAME$i,
|
|
1983
2142
|
meta: {
|
|
1984
2143
|
type: "suggestion",
|
|
1985
2144
|
docs: {
|
|
@@ -2053,9 +2212,9 @@ const linkNoUnderscores = createEslintRule({
|
|
|
2053
2212
|
}
|
|
2054
2213
|
});
|
|
2055
2214
|
|
|
2056
|
-
const RULE_NAME$
|
|
2215
|
+
const RULE_NAME$h = "link-no-whitespace";
|
|
2057
2216
|
const linkNoWhitespace = createEslintRule({
|
|
2058
|
-
name: RULE_NAME$
|
|
2217
|
+
name: RULE_NAME$h,
|
|
2059
2218
|
meta: {
|
|
2060
2219
|
type: "suggestion",
|
|
2061
2220
|
docs: {
|
|
@@ -2129,7 +2288,7 @@ const linkNoWhitespace = createEslintRule({
|
|
|
2129
2288
|
}
|
|
2130
2289
|
});
|
|
2131
2290
|
|
|
2132
|
-
const RULE_NAME$
|
|
2291
|
+
const RULE_NAME$g = "link-require-descriptive-text";
|
|
2133
2292
|
const BAD_LINK_TEXTS = /* @__PURE__ */ new Set([
|
|
2134
2293
|
"click here",
|
|
2135
2294
|
"click this",
|
|
@@ -2149,7 +2308,10 @@ const BAD_LINK_TEXTS = /* @__PURE__ */ new Set([
|
|
|
2149
2308
|
"discover"
|
|
2150
2309
|
]);
|
|
2151
2310
|
function getVueElementText(node) {
|
|
2152
|
-
|
|
2311
|
+
const children = node.children || [];
|
|
2312
|
+
if (children.some((child) => child.type === "VElement" || child.type === "VExpressionContainer"))
|
|
2313
|
+
return "[dynamic]";
|
|
2314
|
+
return children.filter((child) => child.type === "VText").map((child) => child.value).join("").trim() || null;
|
|
2153
2315
|
}
|
|
2154
2316
|
function getVueAttrValue(node, name) {
|
|
2155
2317
|
if (!node.startTag?.attributes)
|
|
@@ -2157,6 +2319,8 @@ function getVueAttrValue(node, name) {
|
|
|
2157
2319
|
for (const attr of node.startTag.attributes) {
|
|
2158
2320
|
if (attr.key?.name === name && attr.value?.type === "VLiteral")
|
|
2159
2321
|
return attr.value.value;
|
|
2322
|
+
if (attr.directive && attr.key?.name?.name === "bind" && attr.key?.argument?.name === name)
|
|
2323
|
+
return "[dynamic]";
|
|
2160
2324
|
}
|
|
2161
2325
|
return null;
|
|
2162
2326
|
}
|
|
@@ -2170,7 +2334,7 @@ function getVueLinkUrl(node) {
|
|
|
2170
2334
|
return null;
|
|
2171
2335
|
}
|
|
2172
2336
|
const linkRequireDescriptiveText = createEslintRule({
|
|
2173
|
-
name: RULE_NAME$
|
|
2337
|
+
name: RULE_NAME$g,
|
|
2174
2338
|
meta: {
|
|
2175
2339
|
type: "suggestion",
|
|
2176
2340
|
docs: {
|
|
@@ -2246,9 +2410,9 @@ const linkRequireDescriptiveText = createEslintRule({
|
|
|
2246
2410
|
}
|
|
2247
2411
|
});
|
|
2248
2412
|
|
|
2249
|
-
const RULE_NAME$
|
|
2413
|
+
const RULE_NAME$f = "link-require-href";
|
|
2250
2414
|
const linkRequireHref = createEslintRule({
|
|
2251
|
-
name: RULE_NAME$
|
|
2415
|
+
name: RULE_NAME$f,
|
|
2252
2416
|
meta: {
|
|
2253
2417
|
type: "problem",
|
|
2254
2418
|
docs: {
|
|
@@ -2313,12 +2477,12 @@ const linkRequireHref = createEslintRule({
|
|
|
2313
2477
|
}
|
|
2314
2478
|
});
|
|
2315
2479
|
|
|
2316
|
-
const RULE_NAME$
|
|
2480
|
+
const RULE_NAME$e = "link-trailing-slash";
|
|
2317
2481
|
function shouldSkipUrl(url) {
|
|
2318
2482
|
return url.startsWith("#") || url.includes(":") || url === "/" || url === "";
|
|
2319
2483
|
}
|
|
2320
2484
|
const linkTrailingSlash = createEslintRule({
|
|
2321
|
-
name: RULE_NAME$
|
|
2485
|
+
name: RULE_NAME$e,
|
|
2322
2486
|
meta: {
|
|
2323
2487
|
type: "suggestion",
|
|
2324
2488
|
docs: {
|
|
@@ -2433,9 +2597,9 @@ const linkTrailingSlash = createEslintRule({
|
|
|
2433
2597
|
}
|
|
2434
2598
|
});
|
|
2435
2599
|
|
|
2436
|
-
const RULE_NAME$
|
|
2600
|
+
const RULE_NAME$d = "nuxt-await-navigate-to";
|
|
2437
2601
|
const nuxtAwaitNavigateTo = createEslintRule({
|
|
2438
|
-
name: RULE_NAME$
|
|
2602
|
+
name: RULE_NAME$d,
|
|
2439
2603
|
meta: {
|
|
2440
2604
|
type: "problem",
|
|
2441
2605
|
docs: {
|
|
@@ -2484,9 +2648,188 @@ const nuxtAwaitNavigateTo = createEslintRule({
|
|
|
2484
2648
|
}
|
|
2485
2649
|
});
|
|
2486
2650
|
|
|
2487
|
-
const
|
|
2651
|
+
const CLIENT_LIFECYCLE_HOOKS = /* @__PURE__ */ new Set([
|
|
2652
|
+
"onMounted",
|
|
2653
|
+
"onBeforeMount",
|
|
2654
|
+
"onUpdated",
|
|
2655
|
+
"onBeforeUpdate",
|
|
2656
|
+
"onActivated",
|
|
2657
|
+
"onDeactivated"
|
|
2658
|
+
]);
|
|
2659
|
+
const DEFERRED_CALLBACK_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
2660
|
+
"watch"
|
|
2661
|
+
]);
|
|
2662
|
+
const SERVER_HANDLER_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
2663
|
+
"defineEventHandler",
|
|
2664
|
+
"defineCachedEventHandler",
|
|
2665
|
+
"defineNitroPlugin",
|
|
2666
|
+
"defineTask"
|
|
2667
|
+
]);
|
|
2668
|
+
function isNamedFunction(funcNode) {
|
|
2669
|
+
if (funcNode.type === "FunctionDeclaration")
|
|
2670
|
+
return true;
|
|
2671
|
+
const parent = funcNode.parent;
|
|
2672
|
+
if (!parent)
|
|
2673
|
+
return false;
|
|
2674
|
+
if (parent.type === "VariableDeclarator")
|
|
2675
|
+
return true;
|
|
2676
|
+
if (parent.type === "Property") {
|
|
2677
|
+
if (parent.key.type === "Identifier" && parent.key.name === "setup" && parent.parent?.type === "ObjectExpression" && parent.parent.parent?.type === "CallExpression" && parent.parent.parent.callee.type === "Identifier" && parent.parent.parent.callee.name === "defineComponent") {
|
|
2678
|
+
return false;
|
|
2679
|
+
}
|
|
2680
|
+
return true;
|
|
2681
|
+
}
|
|
2682
|
+
if (parent.type === "MethodDefinition")
|
|
2683
|
+
return true;
|
|
2684
|
+
if (parent.type === "AssignmentExpression" && parent.left.type === "Identifier")
|
|
2685
|
+
return true;
|
|
2686
|
+
return false;
|
|
2687
|
+
}
|
|
2688
|
+
function executedDuringSetup(node) {
|
|
2689
|
+
let current = node.parent;
|
|
2690
|
+
while (current) {
|
|
2691
|
+
if (current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression" || current.type === "FunctionDeclaration") {
|
|
2692
|
+
if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent?.type === "CallExpression" && current.parent.callee.type === "Identifier") {
|
|
2693
|
+
const calleeName = current.parent.callee.name;
|
|
2694
|
+
if (CLIENT_LIFECYCLE_HOOKS.has(calleeName))
|
|
2695
|
+
return false;
|
|
2696
|
+
if (SERVER_HANDLER_FUNCTIONS.has(calleeName))
|
|
2697
|
+
return false;
|
|
2698
|
+
if (DEFERRED_CALLBACK_FUNCTIONS.has(calleeName)) {
|
|
2699
|
+
const args = current.parent.arguments;
|
|
2700
|
+
const argIndex = args.indexOf(current);
|
|
2701
|
+
if (argIndex > 0)
|
|
2702
|
+
return false;
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
if (isNamedFunction(current))
|
|
2706
|
+
return false;
|
|
2707
|
+
}
|
|
2708
|
+
if (current.type === "Program")
|
|
2709
|
+
return true;
|
|
2710
|
+
current = current.parent;
|
|
2711
|
+
}
|
|
2712
|
+
return false;
|
|
2713
|
+
}
|
|
2714
|
+
function isClientGuardTest(test) {
|
|
2715
|
+
if (test.type === "MemberExpression" && test.object.type === "MetaProperty" && test.property.type === "Identifier" && test.property.name === "client") {
|
|
2716
|
+
return true;
|
|
2717
|
+
}
|
|
2718
|
+
if (test.type === "MemberExpression" && test.object.type === "Identifier" && test.object.name === "process" && test.property.type === "Identifier" && test.property.name === "client") {
|
|
2719
|
+
return true;
|
|
2720
|
+
}
|
|
2721
|
+
if (test.type === "BinaryExpression" && test.left.type === "UnaryExpression" && test.left.operator === "typeof" && test.left.argument.type === "Identifier" && test.left.argument.name === "window") {
|
|
2722
|
+
return true;
|
|
2723
|
+
}
|
|
2724
|
+
return false;
|
|
2725
|
+
}
|
|
2726
|
+
function isInsideClientGuard(node) {
|
|
2727
|
+
let parent = node.parent;
|
|
2728
|
+
while (parent) {
|
|
2729
|
+
if (parent.type === "IfStatement" && isClientGuardTest(parent.test))
|
|
2730
|
+
return isDescendantOfConsequent(node, parent);
|
|
2731
|
+
if (parent.type === "ConditionalExpression" && isClientGuardTest(parent.test)) {
|
|
2732
|
+
return isInConsequentBranch(node, parent);
|
|
2733
|
+
}
|
|
2734
|
+
parent = parent.parent;
|
|
2735
|
+
}
|
|
2736
|
+
return false;
|
|
2737
|
+
}
|
|
2738
|
+
function isDescendantOfConsequent(node, ifStmt) {
|
|
2739
|
+
let current = node;
|
|
2740
|
+
while (current && current !== ifStmt) {
|
|
2741
|
+
if (current === ifStmt.consequent)
|
|
2742
|
+
return true;
|
|
2743
|
+
current = current.parent;
|
|
2744
|
+
}
|
|
2745
|
+
return false;
|
|
2746
|
+
}
|
|
2747
|
+
function isInConsequentBranch(node, ternary) {
|
|
2748
|
+
let current = node;
|
|
2749
|
+
while (current && current !== ternary) {
|
|
2750
|
+
if (current === ternary.consequent)
|
|
2751
|
+
return true;
|
|
2752
|
+
if (current === ternary.alternate)
|
|
2753
|
+
return false;
|
|
2754
|
+
current = current.parent;
|
|
2755
|
+
}
|
|
2756
|
+
return false;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
const RULE_NAME$c = "nuxt-no-random";
|
|
2760
|
+
function isMathRandomCall(node) {
|
|
2761
|
+
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";
|
|
2762
|
+
}
|
|
2763
|
+
function isCryptoRandomCall(node) {
|
|
2764
|
+
if (node.callee.type !== "MemberExpression" || node.callee.property.type !== "Identifier")
|
|
2765
|
+
return false;
|
|
2766
|
+
const method = node.callee.property.name;
|
|
2767
|
+
if (method !== "randomUUID" && method !== "getRandomValues")
|
|
2768
|
+
return false;
|
|
2769
|
+
const obj = node.callee.object;
|
|
2770
|
+
if (obj.type === "Identifier" && obj.name === "crypto")
|
|
2771
|
+
return true;
|
|
2772
|
+
if (obj.type === "MemberExpression" && obj.object.type === "Identifier" && (obj.object.name === "globalThis" || obj.object.name === "window") && obj.property.type === "Identifier" && obj.property.name === "crypto") {
|
|
2773
|
+
return true;
|
|
2774
|
+
}
|
|
2775
|
+
return false;
|
|
2776
|
+
}
|
|
2777
|
+
function reportRandom(context, node, template) {
|
|
2778
|
+
if (isMathRandomCall(node)) {
|
|
2779
|
+
context.report({ node, messageId: template ? "noMathRandomTemplate" : "noMathRandom" });
|
|
2780
|
+
} else if (isCryptoRandomCall(node)) {
|
|
2781
|
+
const method = node.callee.property;
|
|
2782
|
+
context.report({
|
|
2783
|
+
node,
|
|
2784
|
+
messageId: template ? "noCryptoRandomTemplate" : "noCryptoRandom",
|
|
2785
|
+
data: { method: `crypto.${method.name}` }
|
|
2786
|
+
});
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
const nuxtNoRandom = createEslintRule({
|
|
2790
|
+
name: RULE_NAME$c,
|
|
2791
|
+
meta: {
|
|
2792
|
+
type: "problem",
|
|
2793
|
+
docs: {
|
|
2794
|
+
description: "disallow Math.random() and crypto random APIs in SSR-rendered code to prevent hydration mismatches"
|
|
2795
|
+
},
|
|
2796
|
+
schema: [],
|
|
2797
|
+
messages: {
|
|
2798
|
+
noMathRandom: "Math.random() produces different values on server and client, causing hydration mismatches. Move to onMounted() or guard with `if (import.meta.client)`.",
|
|
2799
|
+
noCryptoRandom: "{{method}}() produces different values on server and client, causing hydration mismatches. Move to onMounted() or guard with `if (import.meta.client)`.",
|
|
2800
|
+
noMathRandomTemplate: "Math.random() in templates produces different values on server and client, causing hydration mismatches. Move to a computed property backed by onMounted().",
|
|
2801
|
+
noCryptoRandomTemplate: "{{method}}() in templates produces different values on server and client, causing hydration mismatches. Move to a computed property backed by onMounted()."
|
|
2802
|
+
}
|
|
2803
|
+
},
|
|
2804
|
+
defaultOptions: [],
|
|
2805
|
+
create: (context) => {
|
|
2806
|
+
function checkScript(node) {
|
|
2807
|
+
if (!isMathRandomCall(node) && !isCryptoRandomCall(node))
|
|
2808
|
+
return;
|
|
2809
|
+
if (!executedDuringSetup(node))
|
|
2810
|
+
return;
|
|
2811
|
+
if (isInsideClientGuard(node))
|
|
2812
|
+
return;
|
|
2813
|
+
reportRandom(context, node, false);
|
|
2814
|
+
}
|
|
2815
|
+
if (isVueParser(context)) {
|
|
2816
|
+
return defineTemplateBodyVisitor(context, {
|
|
2817
|
+
// Template expressions — always execute during render
|
|
2818
|
+
CallExpression(node) {
|
|
2819
|
+
if (isMathRandomCall(node) || isCryptoRandomCall(node))
|
|
2820
|
+
reportRandom(context, node, true);
|
|
2821
|
+
}
|
|
2822
|
+
}, {
|
|
2823
|
+
CallExpression: checkScript
|
|
2824
|
+
});
|
|
2825
|
+
}
|
|
2826
|
+
return { CallExpression: checkScript };
|
|
2827
|
+
}
|
|
2828
|
+
});
|
|
2829
|
+
|
|
2830
|
+
const RULE_NAME$b = "nuxt-no-redundant-import-meta";
|
|
2488
2831
|
const nuxtNoRedundantImportMeta = createEslintRule({
|
|
2489
|
-
name: RULE_NAME$
|
|
2832
|
+
name: RULE_NAME$b,
|
|
2490
2833
|
meta: {
|
|
2491
2834
|
type: "problem",
|
|
2492
2835
|
docs: {
|
|
@@ -2523,7 +2866,7 @@ const nuxtNoRedundantImportMeta = createEslintRule({
|
|
|
2523
2866
|
}
|
|
2524
2867
|
});
|
|
2525
2868
|
|
|
2526
|
-
const RULE_NAME$
|
|
2869
|
+
const RULE_NAME$a = "nuxt-no-side-effects-in-async-data-handler";
|
|
2527
2870
|
const SIDE_EFFECT_PATTERNS = /* @__PURE__ */ new Set([
|
|
2528
2871
|
// Store/State mutations
|
|
2529
2872
|
"$patch",
|
|
@@ -2631,7 +2974,7 @@ function findSideEffectsInFunction(functionNode) {
|
|
|
2631
2974
|
return sideEffects;
|
|
2632
2975
|
}
|
|
2633
2976
|
const nuxtNoSideEffectsInAsyncDataHandler = createEslintRule({
|
|
2634
|
-
name: RULE_NAME$
|
|
2977
|
+
name: RULE_NAME$a,
|
|
2635
2978
|
meta: {
|
|
2636
2979
|
type: "problem",
|
|
2637
2980
|
docs: {
|
|
@@ -2722,9 +3065,9 @@ ${indent}${callOnceBlock}`)
|
|
|
2722
3065
|
}
|
|
2723
3066
|
});
|
|
2724
3067
|
|
|
2725
|
-
const RULE_NAME$
|
|
3068
|
+
const RULE_NAME$9 = "nuxt-no-side-effects-in-setup";
|
|
2726
3069
|
const nuxtNoSideEffectsInSetup = createEslintRule({
|
|
2727
|
-
name: RULE_NAME$
|
|
3070
|
+
name: RULE_NAME$9,
|
|
2728
3071
|
meta: {
|
|
2729
3072
|
type: "problem",
|
|
2730
3073
|
docs: {
|
|
@@ -2817,9 +3160,82 @@ ${indent}})`;
|
|
|
2817
3160
|
}
|
|
2818
3161
|
});
|
|
2819
3162
|
|
|
2820
|
-
const RULE_NAME$
|
|
3163
|
+
const RULE_NAME$8 = "nuxt-no-unsafe-date";
|
|
3164
|
+
function isDateNowCall(node) {
|
|
3165
|
+
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";
|
|
3166
|
+
}
|
|
3167
|
+
function isDateFunctionCall(node) {
|
|
3168
|
+
return node.callee.type === "Identifier" && node.callee.name === "Date" && node.parent?.type !== "NewExpression";
|
|
3169
|
+
}
|
|
3170
|
+
function isNewDateCall(node) {
|
|
3171
|
+
return node.callee.type === "Identifier" && node.callee.name === "Date" && node.arguments.length === 0;
|
|
3172
|
+
}
|
|
3173
|
+
const nuxtNoUnsafeDate = createEslintRule({
|
|
3174
|
+
name: RULE_NAME$8,
|
|
3175
|
+
meta: {
|
|
3176
|
+
type: "problem",
|
|
3177
|
+
docs: {
|
|
3178
|
+
description: "disallow Date.now() and new Date() in SSR-rendered code to prevent hydration mismatches"
|
|
3179
|
+
},
|
|
3180
|
+
schema: [],
|
|
3181
|
+
messages: {
|
|
3182
|
+
noDateNow: "Date.now() returns different timestamps on server and client, causing hydration mismatches. Use the <NuxtTime> component or move to onMounted().",
|
|
3183
|
+
noNewDate: "new Date() returns different timestamps on server and client, causing hydration mismatches. Use the <NuxtTime> component or move to onMounted().",
|
|
3184
|
+
noDateCall: "Date() returns the current time as a string, which differs on server and client. Use the <NuxtTime> component or move to onMounted().",
|
|
3185
|
+
noDateNowTemplate: "Date.now() in templates returns different timestamps on server and client. Use the <NuxtTime> component instead.",
|
|
3186
|
+
noNewDateTemplate: "new Date() in templates returns different timestamps on server and client. Use the <NuxtTime> component instead.",
|
|
3187
|
+
noDateCallTemplate: "Date() in templates returns the current time, which differs on server and client. Use the <NuxtTime> component instead."
|
|
3188
|
+
}
|
|
3189
|
+
},
|
|
3190
|
+
defaultOptions: [],
|
|
3191
|
+
create: (context) => {
|
|
3192
|
+
function checkCallScript(node) {
|
|
3193
|
+
const isNow = isDateNowCall(node);
|
|
3194
|
+
const isCall = !isNow && isDateFunctionCall(node);
|
|
3195
|
+
if (!isNow && !isCall)
|
|
3196
|
+
return;
|
|
3197
|
+
if (!executedDuringSetup(node))
|
|
3198
|
+
return;
|
|
3199
|
+
if (isInsideClientGuard(node))
|
|
3200
|
+
return;
|
|
3201
|
+
context.report({ node, messageId: isNow ? "noDateNow" : "noDateCall" });
|
|
3202
|
+
}
|
|
3203
|
+
function checkNewScript(node) {
|
|
3204
|
+
if (!isNewDateCall(node))
|
|
3205
|
+
return;
|
|
3206
|
+
if (!executedDuringSetup(node))
|
|
3207
|
+
return;
|
|
3208
|
+
if (isInsideClientGuard(node))
|
|
3209
|
+
return;
|
|
3210
|
+
context.report({ node, messageId: "noNewDate" });
|
|
3211
|
+
}
|
|
3212
|
+
if (isVueParser(context)) {
|
|
3213
|
+
return defineTemplateBodyVisitor(context, {
|
|
3214
|
+
CallExpression(node) {
|
|
3215
|
+
if (isDateNowCall(node))
|
|
3216
|
+
context.report({ node, messageId: "noDateNowTemplate" });
|
|
3217
|
+
else if (isDateFunctionCall(node))
|
|
3218
|
+
context.report({ node, messageId: "noDateCallTemplate" });
|
|
3219
|
+
},
|
|
3220
|
+
NewExpression(node) {
|
|
3221
|
+
if (isNewDateCall(node))
|
|
3222
|
+
context.report({ node, messageId: "noNewDateTemplate" });
|
|
3223
|
+
}
|
|
3224
|
+
}, {
|
|
3225
|
+
CallExpression: checkCallScript,
|
|
3226
|
+
NewExpression: checkNewScript
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
return {
|
|
3230
|
+
CallExpression: checkCallScript,
|
|
3231
|
+
NewExpression: checkNewScript
|
|
3232
|
+
};
|
|
3233
|
+
}
|
|
3234
|
+
});
|
|
3235
|
+
|
|
3236
|
+
const RULE_NAME$7 = "nuxt-prefer-navigate-to-over-router-push-replace";
|
|
2821
3237
|
const nuxtPreferNavigateToOverRouterPushReplace = createEslintRule({
|
|
2822
|
-
name: RULE_NAME$
|
|
3238
|
+
name: RULE_NAME$7,
|
|
2823
3239
|
meta: {
|
|
2824
3240
|
type: "suggestion",
|
|
2825
3241
|
docs: {
|
|
@@ -2882,9 +3298,9 @@ const nuxtPreferNavigateToOverRouterPushReplace = createEslintRule({
|
|
|
2882
3298
|
}
|
|
2883
3299
|
});
|
|
2884
3300
|
|
|
2885
|
-
const RULE_NAME$
|
|
3301
|
+
const RULE_NAME$6 = "nuxt-prefer-nuxt-link-over-router-link";
|
|
2886
3302
|
const nuxtPreferNuxtLinkOverRouterLink = createEslintRule({
|
|
2887
|
-
name: RULE_NAME$
|
|
3303
|
+
name: RULE_NAME$6,
|
|
2888
3304
|
meta: {
|
|
2889
3305
|
type: "suggestion",
|
|
2890
3306
|
docs: {
|
|
@@ -2955,9 +3371,9 @@ const nuxtPreferNuxtLinkOverRouterLink = createEslintRule({
|
|
|
2955
3371
|
}
|
|
2956
3372
|
});
|
|
2957
3373
|
|
|
2958
|
-
const RULE_NAME$
|
|
3374
|
+
const RULE_NAME$5 = "vue-no-faux-composables";
|
|
2959
3375
|
const vueNoFauxComposables = createEslintRule({
|
|
2960
|
-
name: RULE_NAME$
|
|
3376
|
+
name: RULE_NAME$5,
|
|
2961
3377
|
meta: {
|
|
2962
3378
|
type: "problem",
|
|
2963
3379
|
docs: {
|
|
@@ -2973,64 +3389,16 @@ const vueNoFauxComposables = createEslintRule({
|
|
|
2973
3389
|
const vueImports = /* @__PURE__ */ new Set();
|
|
2974
3390
|
const nonVueImports = /* @__PURE__ */ new Set();
|
|
2975
3391
|
const composableFunctions = /* @__PURE__ */ new Map();
|
|
2976
|
-
|
|
2977
|
-
if (node.callee.type === "Identifier") {
|
|
2978
|
-
const name = node.callee.name;
|
|
2979
|
-
return VUE_REACTIVITY_APIS.has(name) && !nonVueImports.has(name);
|
|
2980
|
-
}
|
|
2981
|
-
return false;
|
|
2982
|
-
}
|
|
2983
|
-
function hasReactivityInStatement(stmt) {
|
|
2984
|
-
if (!stmt)
|
|
2985
|
-
return false;
|
|
2986
|
-
switch (stmt.type) {
|
|
2987
|
-
case "ExpressionStatement":
|
|
2988
|
-
return hasReactivityInExpression(stmt.expression);
|
|
2989
|
-
case "VariableDeclaration":
|
|
2990
|
-
return stmt.declarations.some((decl) => hasReactivityInExpression(decl.init));
|
|
2991
|
-
case "ReturnStatement":
|
|
2992
|
-
return hasReactivityInExpression(stmt.argument);
|
|
2993
|
-
case "BlockStatement":
|
|
2994
|
-
return stmt.body.some((s) => hasReactivityInStatement(s));
|
|
2995
|
-
case "IfStatement":
|
|
2996
|
-
return hasReactivityInStatement(stmt.consequent) || (stmt.alternate ? hasReactivityInStatement(stmt.alternate) : false);
|
|
2997
|
-
case "WhileStatement":
|
|
2998
|
-
case "DoWhileStatement":
|
|
2999
|
-
return hasReactivityInStatement(stmt.body);
|
|
3000
|
-
case "ForStatement":
|
|
3001
|
-
case "ForInStatement":
|
|
3002
|
-
case "ForOfStatement":
|
|
3003
|
-
return hasReactivityInStatement(stmt.body);
|
|
3004
|
-
case "TryStatement":
|
|
3005
|
-
return hasReactivityInStatement(stmt.block) || (stmt.handler ? hasReactivityInStatement(stmt.handler.body) : false) || (stmt.finalizer ? hasReactivityInStatement(stmt.finalizer) : false);
|
|
3006
|
-
case "SwitchStatement":
|
|
3007
|
-
return stmt.cases.some((switchCase) => switchCase.consequent.some((s) => hasReactivityInStatement(s)));
|
|
3008
|
-
default:
|
|
3009
|
-
return false;
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
function hasReactivityInExpression(expr) {
|
|
3013
|
-
if (!expr)
|
|
3014
|
-
return false;
|
|
3015
|
-
switch (expr.type) {
|
|
3016
|
-
case "CallExpression":
|
|
3017
|
-
if (isReactivityCall(expr, vueImports) || isAutoImportedReactivityCall(expr) || isComposableCall(expr))
|
|
3018
|
-
return true;
|
|
3019
|
-
return false;
|
|
3020
|
-
case "ObjectExpression":
|
|
3021
|
-
return expr.properties.some((prop) => prop.type === "Property" && hasReactivityInExpression(prop.value));
|
|
3022
|
-
case "ArrayExpression":
|
|
3023
|
-
return expr.elements.some((elem) => hasReactivityInExpression(elem));
|
|
3024
|
-
case "AwaitExpression":
|
|
3025
|
-
return hasReactivityInExpression(expr.argument);
|
|
3026
|
-
default:
|
|
3027
|
-
return false;
|
|
3028
|
-
}
|
|
3029
|
-
}
|
|
3392
|
+
const { hasReactivityInStatement, hasReactivityInExpression } = createReactivityChecker(vueImports, nonVueImports);
|
|
3030
3393
|
function checkFunctionForReactivity(functionNode, functionName) {
|
|
3031
|
-
if (!functionNode.body
|
|
3394
|
+
if (!functionNode.body)
|
|
3032
3395
|
return;
|
|
3033
|
-
|
|
3396
|
+
let hasReactivity;
|
|
3397
|
+
if (functionNode.body.type === "BlockStatement") {
|
|
3398
|
+
hasReactivity = functionNode.body.body.some((stmt) => hasReactivityInStatement(stmt));
|
|
3399
|
+
} else {
|
|
3400
|
+
hasReactivity = hasReactivityInExpression(functionNode.body);
|
|
3401
|
+
}
|
|
3034
3402
|
if (!hasReactivity) {
|
|
3035
3403
|
context.report({
|
|
3036
3404
|
node: functionNode,
|
|
@@ -3047,13 +3415,7 @@ const vueNoFauxComposables = createEslintRule({
|
|
|
3047
3415
|
},
|
|
3048
3416
|
ImportDeclaration(node) {
|
|
3049
3417
|
trackVueImports(node, vueImports);
|
|
3050
|
-
|
|
3051
|
-
for (const spec of node.specifiers) {
|
|
3052
|
-
if (spec.type === "ImportSpecifier" && spec.imported.type === "Identifier") {
|
|
3053
|
-
nonVueImports.add(spec.imported.name);
|
|
3054
|
-
}
|
|
3055
|
-
}
|
|
3056
|
-
}
|
|
3418
|
+
trackNonVueImports(node, nonVueImports);
|
|
3057
3419
|
},
|
|
3058
3420
|
"Program:exit": function() {
|
|
3059
3421
|
for (const [name, functionNode] of composableFunctions)
|
|
@@ -3077,9 +3439,9 @@ const vueNoFauxComposables = createEslintRule({
|
|
|
3077
3439
|
}
|
|
3078
3440
|
});
|
|
3079
3441
|
|
|
3080
|
-
const RULE_NAME$
|
|
3442
|
+
const RULE_NAME$4 = "vue-no-nested-reactivity";
|
|
3081
3443
|
const vueNoNestedReactivity = createEslintRule({
|
|
3082
|
-
name: RULE_NAME$
|
|
3444
|
+
name: RULE_NAME$4,
|
|
3083
3445
|
meta: {
|
|
3084
3446
|
type: "problem",
|
|
3085
3447
|
docs: {
|
|
@@ -3094,12 +3456,15 @@ const vueNoNestedReactivity = createEslintRule({
|
|
|
3094
3456
|
noNestedInShallowReactive: "Avoid nesting reactivity primitives inside shallowReactive().",
|
|
3095
3457
|
noNestedInComputed: "Avoid nesting reactivity primitives inside computed().",
|
|
3096
3458
|
noNestedInWatch: "Avoid nesting reactivity primitives inside watch().",
|
|
3097
|
-
noNestedInWatchEffect: "Avoid nesting reactivity primitives inside watchEffect()."
|
|
3459
|
+
noNestedInWatchEffect: "Avoid nesting reactivity primitives inside watchEffect().",
|
|
3460
|
+
reactiveInWatchCallback: "Avoid creating reactive primitives inside watch() callbacks. They will be recreated on every trigger.",
|
|
3461
|
+
reactiveInWatchEffectCallback: "Avoid creating reactive primitives inside watchEffect() callbacks. They will be recreated on every run."
|
|
3098
3462
|
}
|
|
3099
3463
|
},
|
|
3100
3464
|
defaultOptions: [],
|
|
3101
3465
|
create: (context) => {
|
|
3102
3466
|
const reactiveAPIs = /* @__PURE__ */ new Set(["ref", "reactive", "shallowRef", "shallowReactive", "computed", "watch", "watchEffect"]);
|
|
3467
|
+
const stateCreatingAPIs = /* @__PURE__ */ new Set(["ref", "reactive", "shallowRef", "shallowReactive", "computed"]);
|
|
3103
3468
|
const vueImports = /* @__PURE__ */ new Set();
|
|
3104
3469
|
const nonVueImports = /* @__PURE__ */ new Set();
|
|
3105
3470
|
const reactiveVariables = /* @__PURE__ */ new Map();
|
|
@@ -3227,6 +3592,8 @@ const vueNoNestedReactivity = createEslintRule({
|
|
|
3227
3592
|
function checkForNestedReactivity(node, outerType) {
|
|
3228
3593
|
if (!node.arguments.length)
|
|
3229
3594
|
return;
|
|
3595
|
+
if (outerType === "watch" || outerType === "watchEffect")
|
|
3596
|
+
return;
|
|
3230
3597
|
const arg = node.arguments[0];
|
|
3231
3598
|
if (arg.type === "ObjectExpression") {
|
|
3232
3599
|
checkObjectExpressionForReactivity(arg, outerType);
|
|
@@ -3240,6 +3607,34 @@ const vueNoNestedReactivity = createEslintRule({
|
|
|
3240
3607
|
}
|
|
3241
3608
|
}
|
|
3242
3609
|
}
|
|
3610
|
+
function getEnclosingWatchType(node) {
|
|
3611
|
+
let current = node.parent;
|
|
3612
|
+
while (current) {
|
|
3613
|
+
if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent?.type === "CallExpression" && current.parent.callee.type === "Identifier") {
|
|
3614
|
+
const callee = current.parent.callee.name;
|
|
3615
|
+
const isVueWatch = vueImports.has(callee) || VUE_REACTIVITY_APIS.has(callee) && !nonVueImports.has(callee);
|
|
3616
|
+
if (isVueWatch) {
|
|
3617
|
+
if (callee === "watchEffect" && current.parent.arguments[0] === current)
|
|
3618
|
+
return "watchEffect";
|
|
3619
|
+
if (callee === "watch" && current.parent.arguments[1] === current)
|
|
3620
|
+
return "watch";
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
current = current.parent;
|
|
3624
|
+
}
|
|
3625
|
+
return null;
|
|
3626
|
+
}
|
|
3627
|
+
function checkReactiveInWatchCallback(node, reactiveType) {
|
|
3628
|
+
if (!stateCreatingAPIs.has(reactiveType))
|
|
3629
|
+
return;
|
|
3630
|
+
const watchType = getEnclosingWatchType(node);
|
|
3631
|
+
if (watchType) {
|
|
3632
|
+
context.report({
|
|
3633
|
+
node,
|
|
3634
|
+
messageId: watchType === "watch" ? "reactiveInWatchCallback" : "reactiveInWatchEffectCallback"
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3243
3638
|
const scriptVisitor = {
|
|
3244
3639
|
Program() {
|
|
3245
3640
|
vueImports.clear();
|
|
@@ -3260,6 +3655,7 @@ const vueNoNestedReactivity = createEslintRule({
|
|
|
3260
3655
|
const reactiveType = isReactiveCall(node);
|
|
3261
3656
|
if (reactiveType) {
|
|
3262
3657
|
checkForNestedReactivity(node, reactiveType);
|
|
3658
|
+
checkReactiveInWatchCallback(node, reactiveType);
|
|
3263
3659
|
}
|
|
3264
3660
|
checkComputedCallback(node);
|
|
3265
3661
|
},
|
|
@@ -3278,6 +3674,7 @@ const vueNoNestedReactivity = createEslintRule({
|
|
|
3278
3674
|
const reactiveType = isReactiveCall(node);
|
|
3279
3675
|
if (reactiveType) {
|
|
3280
3676
|
checkForNestedReactivity(node, reactiveType);
|
|
3677
|
+
checkReactiveInWatchCallback(node, reactiveType);
|
|
3281
3678
|
}
|
|
3282
3679
|
checkComputedCallback(node);
|
|
3283
3680
|
}
|
|
@@ -3289,9 +3686,9 @@ const vueNoNestedReactivity = createEslintRule({
|
|
|
3289
3686
|
}
|
|
3290
3687
|
});
|
|
3291
3688
|
|
|
3292
|
-
const RULE_NAME$
|
|
3689
|
+
const RULE_NAME$3 = "vue-no-passing-refs-as-props";
|
|
3293
3690
|
const vueNoPassingRefsAsProps = createEslintRule({
|
|
3294
|
-
name: RULE_NAME$
|
|
3691
|
+
name: RULE_NAME$3,
|
|
3295
3692
|
meta: {
|
|
3296
3693
|
type: "problem",
|
|
3297
3694
|
docs: {
|
|
@@ -3398,9 +3795,9 @@ const vueNoReactiveDestructuring = createEslintRule({
|
|
|
3398
3795
|
}
|
|
3399
3796
|
});
|
|
3400
3797
|
|
|
3401
|
-
const RULE_NAME$
|
|
3798
|
+
const RULE_NAME$2 = "vue-no-ref-access-in-templates";
|
|
3402
3799
|
const vueNoRefAccessInTemplates = createEslintRule({
|
|
3403
|
-
name: RULE_NAME$
|
|
3800
|
+
name: RULE_NAME$2,
|
|
3404
3801
|
meta: {
|
|
3405
3802
|
type: "suggestion",
|
|
3406
3803
|
docs: {
|
|
@@ -3510,9 +3907,9 @@ const vueNoRefAccessInTemplates = createEslintRule({
|
|
|
3510
3907
|
}
|
|
3511
3908
|
});
|
|
3512
3909
|
|
|
3513
|
-
const RULE_NAME = "vue-no-torefs-on-props";
|
|
3910
|
+
const RULE_NAME$1 = "vue-no-torefs-on-props";
|
|
3514
3911
|
const vueNoTorefsOnProps = createEslintRule({
|
|
3515
|
-
name: RULE_NAME,
|
|
3912
|
+
name: RULE_NAME$1,
|
|
3516
3913
|
meta: {
|
|
3517
3914
|
type: "suggestion",
|
|
3518
3915
|
docs: {
|
|
@@ -3573,6 +3970,94 @@ const vueNoTorefsOnProps = createEslintRule({
|
|
|
3573
3970
|
}
|
|
3574
3971
|
});
|
|
3575
3972
|
|
|
3973
|
+
const RULE_NAME = "vue-require-composable-prefix";
|
|
3974
|
+
const vueRequireComposablePrefix = createEslintRule({
|
|
3975
|
+
name: RULE_NAME,
|
|
3976
|
+
meta: {
|
|
3977
|
+
type: "suggestion",
|
|
3978
|
+
docs: {
|
|
3979
|
+
description: "enforce use* prefix for functions that use Vue reactivity"
|
|
3980
|
+
},
|
|
3981
|
+
hasSuggestions: true,
|
|
3982
|
+
schema: [],
|
|
3983
|
+
messages: {
|
|
3984
|
+
requirePrefix: 'Function "{{name}}" uses Vue reactivity \u2014 consider renaming to "use{{Name}}"'
|
|
3985
|
+
}
|
|
3986
|
+
},
|
|
3987
|
+
defaultOptions: [],
|
|
3988
|
+
create: (context) => {
|
|
3989
|
+
const vueImports = /* @__PURE__ */ new Set();
|
|
3990
|
+
const nonVueImports = /* @__PURE__ */ new Set();
|
|
3991
|
+
const candidateFunctions = /* @__PURE__ */ new Map();
|
|
3992
|
+
const { hasReactivityInStatement, hasReactivityInExpression } = createReactivityChecker(vueImports, nonVueImports);
|
|
3993
|
+
function isExcludedName(name) {
|
|
3994
|
+
return /^define[A-Z]/.test(name) || name === "setup";
|
|
3995
|
+
}
|
|
3996
|
+
function checkFunctionForReactivity(functionNode, idNode, functionName) {
|
|
3997
|
+
if (!functionNode.body)
|
|
3998
|
+
return;
|
|
3999
|
+
let hasReactivity;
|
|
4000
|
+
if (functionNode.body.type === "BlockStatement") {
|
|
4001
|
+
hasReactivity = functionNode.body.body.some((stmt) => hasReactivityInStatement(stmt));
|
|
4002
|
+
} else {
|
|
4003
|
+
hasReactivity = hasReactivityInExpression(functionNode.body);
|
|
4004
|
+
}
|
|
4005
|
+
if (hasReactivity) {
|
|
4006
|
+
const capitalizedName = `${functionName.charAt(0).toUpperCase()}${functionName.slice(1)}`;
|
|
4007
|
+
const suggestedName = `use${capitalizedName}`;
|
|
4008
|
+
context.report({
|
|
4009
|
+
node: idNode,
|
|
4010
|
+
messageId: "requirePrefix",
|
|
4011
|
+
data: { name: functionName, Name: capitalizedName },
|
|
4012
|
+
suggest: [
|
|
4013
|
+
{
|
|
4014
|
+
messageId: "requirePrefix",
|
|
4015
|
+
data: { name: functionName, Name: capitalizedName },
|
|
4016
|
+
fix(fixer) {
|
|
4017
|
+
return fixer.replaceText(idNode, suggestedName);
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
]
|
|
4021
|
+
});
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
return {
|
|
4025
|
+
Program() {
|
|
4026
|
+
vueImports.clear();
|
|
4027
|
+
nonVueImports.clear();
|
|
4028
|
+
candidateFunctions.clear();
|
|
4029
|
+
},
|
|
4030
|
+
ImportDeclaration(node) {
|
|
4031
|
+
trackVueImports(node, vueImports);
|
|
4032
|
+
trackNonVueImports(node, nonVueImports);
|
|
4033
|
+
},
|
|
4034
|
+
"Program:exit": function() {
|
|
4035
|
+
for (const [name, { node, idNode }] of candidateFunctions)
|
|
4036
|
+
checkFunctionForReactivity(node, idNode, name);
|
|
4037
|
+
},
|
|
4038
|
+
FunctionDeclaration(node) {
|
|
4039
|
+
if (!node.id || isComposableName(node.id.name) || isExcludedName(node.id.name))
|
|
4040
|
+
return;
|
|
4041
|
+
if (node.parent.type === "Program" || node.parent.type === "ExportNamedDeclaration")
|
|
4042
|
+
candidateFunctions.set(node.id.name, { node, idNode: node.id });
|
|
4043
|
+
},
|
|
4044
|
+
VariableDeclarator(node) {
|
|
4045
|
+
if (node.id.type !== "Identifier" || isComposableName(node.id.name) || isExcludedName(node.id.name)) {
|
|
4046
|
+
return;
|
|
4047
|
+
}
|
|
4048
|
+
if (node.init?.type !== "FunctionExpression" && node.init?.type !== "ArrowFunctionExpression")
|
|
4049
|
+
return;
|
|
4050
|
+
const varDecl = node.parent;
|
|
4051
|
+
if (varDecl?.type !== "VariableDeclaration")
|
|
4052
|
+
return;
|
|
4053
|
+
if (varDecl.parent?.type !== "Program" && varDecl.parent?.type !== "ExportNamedDeclaration")
|
|
4054
|
+
return;
|
|
4055
|
+
candidateFunctions.set(node.id.name, { node: node.init, idNode: node.id });
|
|
4056
|
+
}
|
|
4057
|
+
};
|
|
4058
|
+
}
|
|
4059
|
+
});
|
|
4060
|
+
|
|
3576
4061
|
const plugin = {
|
|
3577
4062
|
meta: {
|
|
3578
4063
|
name: "harlanzw",
|
|
@@ -3592,9 +4077,11 @@ const plugin = {
|
|
|
3592
4077
|
"link-require-href": linkRequireHref,
|
|
3593
4078
|
"link-trailing-slash": linkTrailingSlash,
|
|
3594
4079
|
"nuxt-await-navigate-to": nuxtAwaitNavigateTo,
|
|
4080
|
+
"nuxt-no-random": nuxtNoRandom,
|
|
3595
4081
|
"nuxt-no-redundant-import-meta": nuxtNoRedundantImportMeta,
|
|
3596
4082
|
"nuxt-no-side-effects-in-async-data-handler": nuxtNoSideEffectsInAsyncDataHandler,
|
|
3597
4083
|
"nuxt-no-side-effects-in-setup": nuxtNoSideEffectsInSetup,
|
|
4084
|
+
"nuxt-no-unsafe-date": nuxtNoUnsafeDate,
|
|
3598
4085
|
"nuxt-prefer-navigate-to-over-router-push-replace": nuxtPreferNavigateToOverRouterPushReplace,
|
|
3599
4086
|
"nuxt-prefer-nuxt-link-over-router-link": nuxtPreferNuxtLinkOverRouterLink,
|
|
3600
4087
|
"prompt-ambiguous-quantifier": promptAmbiguousQuantifier,
|
|
@@ -3623,7 +4110,8 @@ const plugin = {
|
|
|
3623
4110
|
"vue-no-passing-refs-as-props": vueNoPassingRefsAsProps,
|
|
3624
4111
|
"vue-no-reactive-destructuring": vueNoReactiveDestructuring,
|
|
3625
4112
|
"vue-no-ref-access-in-templates": vueNoRefAccessInTemplates,
|
|
3626
|
-
"vue-no-torefs-on-props": vueNoTorefsOnProps
|
|
4113
|
+
"vue-no-torefs-on-props": vueNoTorefsOnProps,
|
|
4114
|
+
"vue-require-composable-prefix": vueRequireComposablePrefix
|
|
3627
4115
|
},
|
|
3628
4116
|
configs: {}
|
|
3629
4117
|
};
|
|
@@ -3720,9 +4208,11 @@ plugin.configs.nuxt = [
|
|
|
3720
4208
|
plugins: { harlanzw: plugin },
|
|
3721
4209
|
rules: {
|
|
3722
4210
|
"harlanzw/nuxt-await-navigate-to": "error",
|
|
4211
|
+
"harlanzw/nuxt-no-random": "error",
|
|
3723
4212
|
"harlanzw/nuxt-no-redundant-import-meta": "error",
|
|
3724
4213
|
"harlanzw/nuxt-no-side-effects-in-async-data-handler": "error",
|
|
3725
4214
|
"harlanzw/nuxt-no-side-effects-in-setup": "error",
|
|
4215
|
+
"harlanzw/nuxt-no-unsafe-date": "warn",
|
|
3726
4216
|
"harlanzw/nuxt-prefer-navigate-to-over-router-push-replace": "warn",
|
|
3727
4217
|
"harlanzw/nuxt-prefer-nuxt-link-over-router-link": "warn"
|
|
3728
4218
|
}
|
|
@@ -3739,7 +4229,8 @@ plugin.configs.vue = [
|
|
|
3739
4229
|
"harlanzw/vue-no-passing-refs-as-props": "error",
|
|
3740
4230
|
"harlanzw/vue-no-reactive-destructuring": "error",
|
|
3741
4231
|
"harlanzw/vue-no-ref-access-in-templates": "warn",
|
|
3742
|
-
"harlanzw/vue-no-torefs-on-props": "warn"
|
|
4232
|
+
"harlanzw/vue-no-torefs-on-props": "warn",
|
|
4233
|
+
"harlanzw/vue-require-composable-prefix": "warn"
|
|
3743
4234
|
}
|
|
3744
4235
|
}
|
|
3745
4236
|
];
|
|
@@ -3748,6 +4239,19 @@ plugin.configs.recommended = [
|
|
|
3748
4239
|
...plugin.configs.nuxt,
|
|
3749
4240
|
...plugin.configs.vue
|
|
3750
4241
|
];
|
|
4242
|
+
const PROMPT_MARKERS = [
|
|
4243
|
+
".claude",
|
|
4244
|
+
".cursor",
|
|
4245
|
+
".github/copilot-instructions.md",
|
|
4246
|
+
".windsurfrules",
|
|
4247
|
+
".clinerules",
|
|
4248
|
+
".goose",
|
|
4249
|
+
".amp",
|
|
4250
|
+
"CLAUDE.md",
|
|
4251
|
+
"AGENTS.md",
|
|
4252
|
+
".cursorrules",
|
|
4253
|
+
".gemini"
|
|
4254
|
+
];
|
|
3751
4255
|
function buildLinkRules(linkOpts) {
|
|
3752
4256
|
const { requireTrailingSlash, ...baseOpts } = linkOpts;
|
|
3753
4257
|
const rules = {
|
|
@@ -3761,8 +4265,9 @@ function buildLinkRules(linkOpts) {
|
|
|
3761
4265
|
return rules;
|
|
3762
4266
|
}
|
|
3763
4267
|
function harlanzw(options = {}, ...extraConfigs) {
|
|
4268
|
+
const detected = detectFramework();
|
|
3764
4269
|
const configs = [];
|
|
3765
|
-
if (options.link !== false
|
|
4270
|
+
if (options.link !== false) {
|
|
3766
4271
|
const linkOpts = typeof options.link === "object" ? options.link : {};
|
|
3767
4272
|
configs.push({
|
|
3768
4273
|
name: "harlanzw/link",
|
|
@@ -3771,19 +4276,38 @@ function harlanzw(options = {}, ...extraConfigs) {
|
|
|
3771
4276
|
rules: buildLinkRules(linkOpts)
|
|
3772
4277
|
});
|
|
3773
4278
|
}
|
|
3774
|
-
|
|
4279
|
+
const enablePrompt = options.prompt ?? detected.prompt;
|
|
4280
|
+
if (enablePrompt) {
|
|
3775
4281
|
const preset = typeof options.prompt === "string" ? options.prompt : "recommended";
|
|
3776
4282
|
configs.push(...plugin.configs[`prompt:${preset}`]);
|
|
3777
4283
|
}
|
|
3778
|
-
|
|
4284
|
+
const enableNuxt = options.nuxt ?? detected.nuxt;
|
|
4285
|
+
if (enableNuxt) {
|
|
3779
4286
|
configs.push(...plugin.configs.nuxt);
|
|
3780
4287
|
}
|
|
3781
|
-
|
|
4288
|
+
const enableVue = options.vue ?? detected.vue;
|
|
4289
|
+
if (enableVue) {
|
|
3782
4290
|
configs.push(...plugin.configs.vue);
|
|
3783
4291
|
}
|
|
3784
4292
|
configs.push(...extraConfigs);
|
|
3785
4293
|
return configs;
|
|
3786
4294
|
}
|
|
4295
|
+
function detectFramework() {
|
|
4296
|
+
const cwd = process.cwd();
|
|
4297
|
+
const nuxt = existsSync(resolve(cwd, "nuxt.config.ts")) || existsSync(resolve(cwd, "nuxt.config.js"));
|
|
4298
|
+
let vue = nuxt;
|
|
4299
|
+
if (!vue) {
|
|
4300
|
+
try {
|
|
4301
|
+
const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
|
|
4302
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4303
|
+
vue = !!(deps.vue || deps.nuxt);
|
|
4304
|
+
} catch {
|
|
4305
|
+
}
|
|
4306
|
+
}
|
|
4307
|
+
const prompt = PROMPT_MARKERS.some((m) => existsSync(resolve(cwd, m)));
|
|
4308
|
+
return { nuxt, vue, prompt };
|
|
4309
|
+
}
|
|
3787
4310
|
harlanzw.plugin = plugin;
|
|
4311
|
+
harlanzw.detectFramework = detectFramework;
|
|
3788
4312
|
|
|
3789
4313
|
export { harlanzw as default, plugin };
|