eslint-plugin-harlanzw 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,9 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
1
3
  import { TextSourceCodeBase, ConfigCommentParser, Directive, VisitNodeStep } from '@eslint/plugin-kit';
2
4
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
3
5
 
4
- const version = "0.2.5";
6
+ const version = "0.3.0";
5
7
 
6
8
  const STRENGTH_PATTERNS = {
7
9
  strong: ["never", "must", "always", "under no circumstances", "absolutely", "required", "mandatory", "forbidden", "prohibited"],
@@ -70,18 +72,32 @@ const COMMON_CONTEXT_VARIABLES = /* @__PURE__ */ new Set([
70
72
  "branch"
71
73
  ]);
72
74
  const PROMPT_FILES = [
75
+ // Claude Code — root + nested CLAUDE.md are auto-loaded
73
76
  "**/CLAUDE.md",
74
- "**/.claude/**/*.md",
75
- "**/AGENTS.md",
77
+ // Claude Code — skills
78
+ "**/SKILL.md",
79
+ // Claude Code — commands (loaded on invoke)
80
+ "**/.claude/commands/**/*.md",
81
+ // Gemini
82
+ "**/.gemini/style_guide.md",
83
+ "**/GEMINI.md",
84
+ // GitHub Copilot
76
85
  "**/.github/copilot-instructions.md",
86
+ // Cursor
77
87
  "**/.cursorrules",
78
88
  "**/.cursor/rules/**/*.md",
79
89
  "**/.cursor/rules/**/*.mdc",
80
- "**/SKILL.md",
90
+ // Windsurf
81
91
  "**/.windsurfrules",
92
+ // Cline
82
93
  "**/.clinerules",
94
+ // Goose
83
95
  "**/.goose/instructions.md",
96
+ // Amp
84
97
  "**/.amp/RULES.md",
98
+ // Codex
99
+ "**/AGENTS.md",
100
+ // Generic prompt files
85
101
  "**/*.prompt",
86
102
  "**/*.prompt.md"
87
103
  ];
@@ -2149,7 +2165,10 @@ const BAD_LINK_TEXTS = /* @__PURE__ */ new Set([
2149
2165
  "discover"
2150
2166
  ]);
2151
2167
  function getVueElementText(node) {
2152
- return (node.children || []).filter((child) => child.type === "VText").map((child) => child.value).join("").trim();
2168
+ const children = node.children || [];
2169
+ if (children.some((child) => child.type === "VElement" || child.type === "VExpressionContainer"))
2170
+ return "[dynamic]";
2171
+ return children.filter((child) => child.type === "VText").map((child) => child.value).join("").trim() || null;
2153
2172
  }
2154
2173
  function getVueAttrValue(node, name) {
2155
2174
  if (!node.startTag?.attributes)
@@ -2157,6 +2176,8 @@ function getVueAttrValue(node, name) {
2157
2176
  for (const attr of node.startTag.attributes) {
2158
2177
  if (attr.key?.name === name && attr.value?.type === "VLiteral")
2159
2178
  return attr.value.value;
2179
+ if (attr.directive && attr.key?.name?.name === "bind" && attr.key?.argument?.name === name)
2180
+ return "[dynamic]";
2160
2181
  }
2161
2182
  return null;
2162
2183
  }
@@ -3021,6 +3042,8 @@ const vueNoFauxComposables = createEslintRule({
3021
3042
  return expr.properties.some((prop) => prop.type === "Property" && hasReactivityInExpression(prop.value));
3022
3043
  case "ArrayExpression":
3023
3044
  return expr.elements.some((elem) => hasReactivityInExpression(elem));
3045
+ case "AwaitExpression":
3046
+ return hasReactivityInExpression(expr.argument);
3024
3047
  default:
3025
3048
  return false;
3026
3049
  }
@@ -3092,12 +3115,15 @@ const vueNoNestedReactivity = createEslintRule({
3092
3115
  noNestedInShallowReactive: "Avoid nesting reactivity primitives inside shallowReactive().",
3093
3116
  noNestedInComputed: "Avoid nesting reactivity primitives inside computed().",
3094
3117
  noNestedInWatch: "Avoid nesting reactivity primitives inside watch().",
3095
- noNestedInWatchEffect: "Avoid nesting reactivity primitives inside watchEffect()."
3118
+ noNestedInWatchEffect: "Avoid nesting reactivity primitives inside watchEffect().",
3119
+ reactiveInWatchCallback: "Avoid creating reactive primitives inside watch() callbacks. They will be recreated on every trigger.",
3120
+ reactiveInWatchEffectCallback: "Avoid creating reactive primitives inside watchEffect() callbacks. They will be recreated on every run."
3096
3121
  }
3097
3122
  },
3098
3123
  defaultOptions: [],
3099
3124
  create: (context) => {
3100
3125
  const reactiveAPIs = /* @__PURE__ */ new Set(["ref", "reactive", "shallowRef", "shallowReactive", "computed", "watch", "watchEffect"]);
3126
+ const stateCreatingAPIs = /* @__PURE__ */ new Set(["ref", "reactive", "shallowRef", "shallowReactive", "computed"]);
3101
3127
  const vueImports = /* @__PURE__ */ new Set();
3102
3128
  const nonVueImports = /* @__PURE__ */ new Set();
3103
3129
  const reactiveVariables = /* @__PURE__ */ new Map();
@@ -3225,6 +3251,8 @@ const vueNoNestedReactivity = createEslintRule({
3225
3251
  function checkForNestedReactivity(node, outerType) {
3226
3252
  if (!node.arguments.length)
3227
3253
  return;
3254
+ if (outerType === "watch" || outerType === "watchEffect")
3255
+ return;
3228
3256
  const arg = node.arguments[0];
3229
3257
  if (arg.type === "ObjectExpression") {
3230
3258
  checkObjectExpressionForReactivity(arg, outerType);
@@ -3238,6 +3266,34 @@ const vueNoNestedReactivity = createEslintRule({
3238
3266
  }
3239
3267
  }
3240
3268
  }
3269
+ function getEnclosingWatchType(node) {
3270
+ let current = node.parent;
3271
+ while (current) {
3272
+ if ((current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") && current.parent?.type === "CallExpression" && current.parent.callee.type === "Identifier") {
3273
+ const callee = current.parent.callee.name;
3274
+ const isVueWatch = vueImports.has(callee) || VUE_REACTIVITY_APIS.has(callee) && !nonVueImports.has(callee);
3275
+ if (isVueWatch) {
3276
+ if (callee === "watchEffect" && current.parent.arguments[0] === current)
3277
+ return "watchEffect";
3278
+ if (callee === "watch" && current.parent.arguments[1] === current)
3279
+ return "watch";
3280
+ }
3281
+ }
3282
+ current = current.parent;
3283
+ }
3284
+ return null;
3285
+ }
3286
+ function checkReactiveInWatchCallback(node, reactiveType) {
3287
+ if (!stateCreatingAPIs.has(reactiveType))
3288
+ return;
3289
+ const watchType = getEnclosingWatchType(node);
3290
+ if (watchType) {
3291
+ context.report({
3292
+ node,
3293
+ messageId: watchType === "watch" ? "reactiveInWatchCallback" : "reactiveInWatchEffectCallback"
3294
+ });
3295
+ }
3296
+ }
3241
3297
  const scriptVisitor = {
3242
3298
  Program() {
3243
3299
  vueImports.clear();
@@ -3258,6 +3314,7 @@ const vueNoNestedReactivity = createEslintRule({
3258
3314
  const reactiveType = isReactiveCall(node);
3259
3315
  if (reactiveType) {
3260
3316
  checkForNestedReactivity(node, reactiveType);
3317
+ checkReactiveInWatchCallback(node, reactiveType);
3261
3318
  }
3262
3319
  checkComputedCallback(node);
3263
3320
  },
@@ -3276,6 +3333,7 @@ const vueNoNestedReactivity = createEslintRule({
3276
3333
  const reactiveType = isReactiveCall(node);
3277
3334
  if (reactiveType) {
3278
3335
  checkForNestedReactivity(node, reactiveType);
3336
+ checkReactiveInWatchCallback(node, reactiveType);
3279
3337
  }
3280
3338
  checkComputedCallback(node);
3281
3339
  }
@@ -3746,6 +3804,19 @@ plugin.configs.recommended = [
3746
3804
  ...plugin.configs.nuxt,
3747
3805
  ...plugin.configs.vue
3748
3806
  ];
3807
+ const PROMPT_MARKERS = [
3808
+ ".claude",
3809
+ ".cursor",
3810
+ ".github/copilot-instructions.md",
3811
+ ".windsurfrules",
3812
+ ".clinerules",
3813
+ ".goose",
3814
+ ".amp",
3815
+ "CLAUDE.md",
3816
+ "AGENTS.md",
3817
+ ".cursorrules",
3818
+ ".gemini"
3819
+ ];
3749
3820
  function buildLinkRules(linkOpts) {
3750
3821
  const { requireTrailingSlash, ...baseOpts } = linkOpts;
3751
3822
  const rules = {
@@ -3759,8 +3830,9 @@ function buildLinkRules(linkOpts) {
3759
3830
  return rules;
3760
3831
  }
3761
3832
  function harlanzw(options = {}, ...extraConfigs) {
3833
+ const detected = detectFramework();
3762
3834
  const configs = [];
3763
- if (options.link !== false && options.link !== void 0) {
3835
+ if (options.link !== false) {
3764
3836
  const linkOpts = typeof options.link === "object" ? options.link : {};
3765
3837
  configs.push({
3766
3838
  name: "harlanzw/link",
@@ -3769,19 +3841,38 @@ function harlanzw(options = {}, ...extraConfigs) {
3769
3841
  rules: buildLinkRules(linkOpts)
3770
3842
  });
3771
3843
  }
3772
- if (options.prompt !== false) {
3844
+ const enablePrompt = options.prompt ?? detected.prompt;
3845
+ if (enablePrompt) {
3773
3846
  const preset = typeof options.prompt === "string" ? options.prompt : "recommended";
3774
3847
  configs.push(...plugin.configs[`prompt:${preset}`]);
3775
3848
  }
3776
- if (options.nuxt) {
3849
+ const enableNuxt = options.nuxt ?? detected.nuxt;
3850
+ if (enableNuxt) {
3777
3851
  configs.push(...plugin.configs.nuxt);
3778
3852
  }
3779
- if (options.vue) {
3853
+ const enableVue = options.vue ?? detected.vue;
3854
+ if (enableVue) {
3780
3855
  configs.push(...plugin.configs.vue);
3781
3856
  }
3782
3857
  configs.push(...extraConfigs);
3783
3858
  return configs;
3784
3859
  }
3860
+ function detectFramework() {
3861
+ const cwd = process.cwd();
3862
+ const nuxt = existsSync(resolve(cwd, "nuxt.config.ts")) || existsSync(resolve(cwd, "nuxt.config.js"));
3863
+ let vue = nuxt;
3864
+ if (!vue) {
3865
+ try {
3866
+ const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
3867
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
3868
+ vue = !!(deps.vue || deps.nuxt);
3869
+ } catch {
3870
+ }
3871
+ }
3872
+ const prompt = PROMPT_MARKERS.some((m) => existsSync(resolve(cwd, m)));
3873
+ return { nuxt, vue, prompt };
3874
+ }
3785
3875
  harlanzw.plugin = plugin;
3876
+ harlanzw.detectFramework = detectFramework;
3786
3877
 
3787
3878
  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.5",
4
+ "version": "0.3.0",
5
5
  "description": "Harlan's opinionated ESLint rules",
6
6
  "author": "Harlan Wilton <harlan@harlanzw.com>",
7
7
  "license": "MIT",