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 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.2.6";
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
- "**/.claude/**/*.md",
75
- "**/AGENTS.md",
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
- "**/SKILL.md",
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$i = "link-ascii-only";
1891
+ const RULE_NAME$l = "link-ascii-only";
1733
1892
  const linkAsciiOnly = createEslintRule({
1734
- name: RULE_NAME$i,
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$h = "link-lowercase";
1965
+ const RULE_NAME$k = "link-lowercase";
1807
1966
  const linkLowercase = createEslintRule({
1808
- name: RULE_NAME$h,
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$g = "link-no-double-slashes";
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$g,
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$f = "link-no-underscores";
2139
+ const RULE_NAME$i = "link-no-underscores";
1981
2140
  const linkNoUnderscores = createEslintRule({
1982
- name: RULE_NAME$f,
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$e = "link-no-whitespace";
2215
+ const RULE_NAME$h = "link-no-whitespace";
2057
2216
  const linkNoWhitespace = createEslintRule({
2058
- name: RULE_NAME$e,
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$d = "link-require-descriptive-text";
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
- return (node.children || []).filter((child) => child.type === "VText").map((child) => child.value).join("").trim();
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$d,
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$c = "link-require-href";
2413
+ const RULE_NAME$f = "link-require-href";
2250
2414
  const linkRequireHref = createEslintRule({
2251
- name: RULE_NAME$c,
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$b = "link-trailing-slash";
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$b,
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$a = "nuxt-await-navigate-to";
2600
+ const RULE_NAME$d = "nuxt-await-navigate-to";
2437
2601
  const nuxtAwaitNavigateTo = createEslintRule({
2438
- name: RULE_NAME$a,
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 RULE_NAME$9 = "nuxt-no-redundant-import-meta";
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$9,
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$8 = "nuxt-no-side-effects-in-async-data-handler";
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$8,
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$7 = "nuxt-no-side-effects-in-setup";
3068
+ const RULE_NAME$9 = "nuxt-no-side-effects-in-setup";
2726
3069
  const nuxtNoSideEffectsInSetup = createEslintRule({
2727
- name: RULE_NAME$7,
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$6 = "nuxt-prefer-navigate-to-over-router-push-replace";
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$6,
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$5 = "nuxt-prefer-nuxt-link-over-router-link";
3301
+ const RULE_NAME$6 = "nuxt-prefer-nuxt-link-over-router-link";
2886
3302
  const nuxtPreferNuxtLinkOverRouterLink = createEslintRule({
2887
- name: RULE_NAME$5,
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$4 = "vue-no-faux-composables";
3374
+ const RULE_NAME$5 = "vue-no-faux-composables";
2959
3375
  const vueNoFauxComposables = createEslintRule({
2960
- name: RULE_NAME$4,
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
- function isAutoImportedReactivityCall(node) {
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 || functionNode.body.type !== "BlockStatement")
3394
+ if (!functionNode.body)
3032
3395
  return;
3033
- const hasReactivity = functionNode.body.body.some((stmt) => hasReactivityInStatement(stmt));
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
- if (node.source.value !== "vue") {
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$3 = "vue-no-nested-reactivity";
3442
+ const RULE_NAME$4 = "vue-no-nested-reactivity";
3081
3443
  const vueNoNestedReactivity = createEslintRule({
3082
- name: RULE_NAME$3,
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$2 = "vue-no-passing-refs-as-props";
3689
+ const RULE_NAME$3 = "vue-no-passing-refs-as-props";
3293
3690
  const vueNoPassingRefsAsProps = createEslintRule({
3294
- name: RULE_NAME$2,
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$1 = "vue-no-ref-access-in-templates";
3798
+ const RULE_NAME$2 = "vue-no-ref-access-in-templates";
3402
3799
  const vueNoRefAccessInTemplates = createEslintRule({
3403
- name: RULE_NAME$1,
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 && options.link !== void 0) {
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
- if (options.prompt !== false) {
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
- if (options.nuxt) {
4284
+ const enableNuxt = options.nuxt ?? detected.nuxt;
4285
+ if (enableNuxt) {
3779
4286
  configs.push(...plugin.configs.nuxt);
3780
4287
  }
3781
- if (options.vue) {
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 };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eslint-plugin-harlanzw",
3
3
  "type": "module",
4
- "version": "0.2.6",
4
+ "version": "0.4.0",
5
5
  "description": "Harlan's opinionated ESLint rules",
6
6
  "author": "Harlan Wilton <harlan@harlanzw.com>",
7
7
  "license": "MIT",