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 +5 -0
- package/dist/index.mjs +101 -10
- 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,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.
|
|
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
|
-
|
|
75
|
-
"**/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3849
|
+
const enableNuxt = options.nuxt ?? detected.nuxt;
|
|
3850
|
+
if (enableNuxt) {
|
|
3777
3851
|
configs.push(...plugin.configs.nuxt);
|
|
3778
3852
|
}
|
|
3779
|
-
|
|
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 };
|