@hs-web-team/eslint-config-node 4.2.2 → 5.0.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/.stylelintrc.json +4 -1
- package/README.md +11 -0
- package/bin/check-peer-deps.cjs +1 -1
- package/browser.js +9 -3
- package/cypress.config.cjs +10 -11
- package/docs/adr/0001-lint-claude-md-with-eslint.md +123 -0
- package/docs/adr/0002-extend-max-lines-to-skill-md.md +105 -0
- package/examples/claude-code-usage.md +73 -0
- package/examples/custom-rules.md +59 -0
- package/index.js +9 -3
- package/package.json +38 -23
- package/plugins/claude-code/index.js +50 -0
- package/plugins/claude-code/rules/max-lines.js +52 -0
- package/plugins/hs-web-team/index.js +21 -0
- package/plugins/hs-web-team/rules/no-abbreviations.js +135 -0
- package/plugins/hs-web-team/rules/no-reduce-accumulator-copy.js +90 -0
- package/.githooks/commit-msg +0 -28
- package/.github/CODEOWNERS +0 -1
- package/.github/workflows/main.yml +0 -24
- package/.github/workflows/pr.yml +0 -24
- package/.github/workflows/release-please.yml +0 -44
- package/.nvmrc +0 -1
- package/.release-please-manifest.json +0 -3
- package/CHANGELOG.md +0 -23
- package/CLAUDE.md +0 -145
- package/release-please-config.json +0 -8
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import markdown from '@eslint/markdown';
|
|
2
|
+
import maxLines from './rules/max-lines.js';
|
|
3
|
+
|
|
4
|
+
// First-party ESLint plugin holding lint rules for Claude Code context files:
|
|
5
|
+
// `CLAUDE.md` project-context files and the `SKILL.md` files under
|
|
6
|
+
// `.claude/skills/`. Rules are referenced as `claude-code/<rule>` wherever the
|
|
7
|
+
// plugin is registered.
|
|
8
|
+
export const claudeCodePlugin = {
|
|
9
|
+
meta: { name: 'claude-code' },
|
|
10
|
+
rules: {
|
|
11
|
+
'max-lines': maxLines,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Self-reference via `configs.recommended`, following the same pattern as
|
|
16
|
+
// hsWebTeamPlugin (and typescript-eslint / eslint-plugin-react). It carries the
|
|
17
|
+
// flat-config blocks that scope the rules to the right files. Severity is `warn`
|
|
18
|
+
// so it never blocks; consumers can bump a rule to `error` or turn it `off`.
|
|
19
|
+
//
|
|
20
|
+
// `@eslint/markdown` supplies the `markdown/gfm` language — without it ESLint
|
|
21
|
+
// would try to parse these files as JavaScript and error.
|
|
22
|
+
//
|
|
23
|
+
// Skills carry more detail than a `CLAUDE.md`, so `SKILL.md` gets a higher
|
|
24
|
+
// (500-line) guideline than `CLAUDE.md` (100 lines), with tailored advice.
|
|
25
|
+
claudeCodePlugin.configs = {
|
|
26
|
+
recommended: [
|
|
27
|
+
{
|
|
28
|
+
files: ['**/CLAUDE.md'],
|
|
29
|
+
language: 'markdown/gfm',
|
|
30
|
+
plugins: { markdown, 'claude-code': claudeCodePlugin },
|
|
31
|
+
rules: {
|
|
32
|
+
'claude-code/max-lines': [
|
|
33
|
+
'warn',
|
|
34
|
+
{ max: 100, advice: 'Trim it or split sections into files Claude Code can import on demand.' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
files: ['**/.claude/skills/**/SKILL.md'],
|
|
40
|
+
language: 'markdown/gfm',
|
|
41
|
+
plugins: { markdown, 'claude-code': claudeCodePlugin },
|
|
42
|
+
rules: {
|
|
43
|
+
'claude-code/max-lines': [
|
|
44
|
+
'warn',
|
|
45
|
+
{ max: 500, advice: 'Trim it or move detail into reference files the skill loads on demand.' },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const DEFAULT_MAX = 100;
|
|
2
|
+
const DEFAULT_ADVICE = 'Trim it or split sections into files Claude Code can import on demand.';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'suggestion',
|
|
7
|
+
docs: {
|
|
8
|
+
description: 'Warn when a doc file exceeds a maximum number of lines',
|
|
9
|
+
recommended: true,
|
|
10
|
+
},
|
|
11
|
+
schema: [
|
|
12
|
+
{
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
max: { type: 'integer', minimum: 1 },
|
|
16
|
+
advice: { type: 'string' },
|
|
17
|
+
},
|
|
18
|
+
additionalProperties: false,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
messages: {
|
|
22
|
+
tooManyLines: '{{file}} has {{count}} lines, over the {{max}}-line guideline. {{advice}}',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
create(context) {
|
|
26
|
+
const { max = DEFAULT_MAX, advice = DEFAULT_ADVICE } = context.options[0] || {};
|
|
27
|
+
const { sourceCode } = context;
|
|
28
|
+
// Report against the file's basename so the message names the offending
|
|
29
|
+
// file (e.g. CLAUDE.md or SKILL.md) regardless of its directory.
|
|
30
|
+
const file = (context.filename || '').split('/').pop() || 'File';
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
root() {
|
|
34
|
+
const { lines } = sourceCode;
|
|
35
|
+
// `lines` splits on line breaks; a trailing newline yields a final
|
|
36
|
+
// empty entry that should not count as a content line.
|
|
37
|
+
const count = lines.length > 0 && lines[lines.length - 1] === '' ? lines.length - 1 : lines.length;
|
|
38
|
+
|
|
39
|
+
if (count > max) {
|
|
40
|
+
context.report({
|
|
41
|
+
loc: {
|
|
42
|
+
start: { line: max + 1, column: 0 },
|
|
43
|
+
end: { line: lines.length, column: lines[lines.length - 1].length },
|
|
44
|
+
},
|
|
45
|
+
messageId: 'tooManyLines',
|
|
46
|
+
data: { file, count, max, advice },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { noAbbreviations } from './rules/no-abbreviations.js';
|
|
2
|
+
import { noReduceAccumulatorCopy } from './rules/no-reduce-accumulator-copy.js';
|
|
3
|
+
|
|
4
|
+
export const hsWebTeamPlugin = {
|
|
5
|
+
rules: {
|
|
6
|
+
'no-abbreviations': noAbbreviations,
|
|
7
|
+
'no-reduce-accumulator-copy': noReduceAccumulatorCopy,
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Self-reference allows the plugin to register itself via configs.recommended,
|
|
12
|
+
// following the same pattern as typescript-eslint and eslint-plugin-react.
|
|
13
|
+
hsWebTeamPlugin.configs = {
|
|
14
|
+
recommended: {
|
|
15
|
+
plugins: { 'hs-web-team': hsWebTeamPlugin },
|
|
16
|
+
rules: {
|
|
17
|
+
'hs-web-team/no-abbreviations': 'warn',
|
|
18
|
+
'hs-web-team/no-reduce-accumulator-copy': 'error',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const DEFAULT_EXCEPTIONS = new Set(['e', '_']);
|
|
2
|
+
const DEFAULT_MIN_LENGTH = 2;
|
|
3
|
+
|
|
4
|
+
const SORT_METHODS = new Set(['sort', 'toSorted']);
|
|
5
|
+
const ARRAY_ITERATION_METHODS = new Set([
|
|
6
|
+
'map',
|
|
7
|
+
'flatMap',
|
|
8
|
+
'forEach',
|
|
9
|
+
'filter',
|
|
10
|
+
'find',
|
|
11
|
+
'findIndex',
|
|
12
|
+
'findLast',
|
|
13
|
+
'findLastIndex',
|
|
14
|
+
'some',
|
|
15
|
+
'every',
|
|
16
|
+
// reduce/reduceRight are intentionally excluded: their callback signature is
|
|
17
|
+
// (accumulator, currentValue, currentIndex, …), so position 1 is the current
|
|
18
|
+
// *value*, not the index. The i-at-position-1 heuristic does not apply.
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns true if the VariableDeclarator is in the init of a for/for...of/for...in statement.
|
|
23
|
+
* Walks the parent chain directly — no scope API needed.
|
|
24
|
+
*
|
|
25
|
+
* @param {import('eslint').Rule.Node} declaratorNode - VariableDeclarator node
|
|
26
|
+
*/
|
|
27
|
+
function isDeclaredInForLoopInit(declaratorNode) {
|
|
28
|
+
const declaration = declaratorNode.parent; // VariableDeclaration
|
|
29
|
+
const enclosing = declaration?.parent;
|
|
30
|
+
if (!enclosing) return false;
|
|
31
|
+
return (
|
|
32
|
+
(enclosing.type === 'ForStatement' && enclosing.init === declaration) ||
|
|
33
|
+
enclosing.type === 'ForInStatement' ||
|
|
34
|
+
enclosing.type === 'ForOfStatement'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Returns true if funcNode is a direct inline callback to a method whose name is in methodNames.
|
|
40
|
+
* Only matches non-computed method calls (arr.sort(...), not arr['sort'](...)).
|
|
41
|
+
*
|
|
42
|
+
* @param {import('eslint').Rule.Node} funcNode - ArrowFunctionExpression or FunctionExpression
|
|
43
|
+
* @param {Set<string>} methodNames
|
|
44
|
+
*/
|
|
45
|
+
function isCallbackToMethod(funcNode, methodNames) {
|
|
46
|
+
const callExpr = funcNode.parent;
|
|
47
|
+
if (callExpr?.type !== 'CallExpression') return false;
|
|
48
|
+
if (!callExpr.arguments.includes(funcNode)) return false;
|
|
49
|
+
const { callee } = callExpr;
|
|
50
|
+
return (
|
|
51
|
+
callee?.type === 'MemberExpression' &&
|
|
52
|
+
!callee.computed &&
|
|
53
|
+
methodNames.has(callee.property.name)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
58
|
+
export const noAbbreviations = {
|
|
59
|
+
meta: {
|
|
60
|
+
type: 'suggestion',
|
|
61
|
+
docs: {
|
|
62
|
+
description: 'Disallow abbreviated identifier names (e.g. `w` instead of `warnings`)',
|
|
63
|
+
url: 'https://docs.hubwt.com/docs/developers/coding-on-the-web-team/coding-standards/javascript-standards/#avoid-abbreviation',
|
|
64
|
+
recommended: true,
|
|
65
|
+
},
|
|
66
|
+
messages: {
|
|
67
|
+
tooShort:
|
|
68
|
+
'Identifier "{{name}}" is too short ({{length}} < {{min}}). Use a descriptive name.',
|
|
69
|
+
},
|
|
70
|
+
schema: [
|
|
71
|
+
{
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
exceptions: { type: 'array', items: { type: 'string' }, uniqueItems: true },
|
|
75
|
+
minLength: { type: 'integer', minimum: 1 },
|
|
76
|
+
},
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
create(context) {
|
|
83
|
+
const options = context.options[0] ?? {};
|
|
84
|
+
const exceptions = new Set([...DEFAULT_EXCEPTIONS, ...(options.exceptions ?? [])]);
|
|
85
|
+
const minLength = options.minLength ?? DEFAULT_MIN_LENGTH;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
Identifier(node) {
|
|
89
|
+
const { name, parent } = node;
|
|
90
|
+
|
|
91
|
+
if (name.length >= minLength) return;
|
|
92
|
+
if (exceptions.has(name)) return;
|
|
93
|
+
|
|
94
|
+
// Only flag declaration sites — not usage sites.
|
|
95
|
+
//
|
|
96
|
+
// Object destructuring shorthand (const { w } = response) is intentionally
|
|
97
|
+
// excluded: `w` may be an external API property name the developer doesn't
|
|
98
|
+
// control. The idiomatic fix — const { w: warning } = response — is correct
|
|
99
|
+
// but non-obvious enough to leave as opt-in. Array destructuring IS included
|
|
100
|
+
// because the element binding name is always chosen by the developer.
|
|
101
|
+
const isDeclaration =
|
|
102
|
+
(parent.type === 'VariableDeclarator' && parent.id === node) ||
|
|
103
|
+
(parent.type === 'ArrayPattern' && parent.elements.includes(node)) ||
|
|
104
|
+
(parent.type === 'FunctionDeclaration' && parent.id === node) ||
|
|
105
|
+
(parent.type === 'FunctionDeclaration' && parent.params.includes(node)) ||
|
|
106
|
+
(parent.type === 'ArrowFunctionExpression' && parent.params.includes(node)) ||
|
|
107
|
+
(parent.type === 'FunctionExpression' && parent.params.includes(node));
|
|
108
|
+
|
|
109
|
+
if (!isDeclaration) return;
|
|
110
|
+
|
|
111
|
+
// For-loop variables (i, j, k…) are exempt regardless of length.
|
|
112
|
+
if (parent.type === 'VariableDeclarator' && isDeclaredInForLoopInit(parent)) return;
|
|
113
|
+
|
|
114
|
+
// a and b are conventional sort-comparator names — exempt only inside .sort()/.toSorted().
|
|
115
|
+
if ((name === 'a' || name === 'b') && isCallbackToMethod(parent, SORT_METHODS)) return;
|
|
116
|
+
|
|
117
|
+
// i is a conventional index name — exempt only when it is the index param (position 1)
|
|
118
|
+
// of an array iteration callback: items.map((item, i) => ...).
|
|
119
|
+
// Position 1 is the index slot for every array iteration method except reduce/reduceRight
|
|
120
|
+
// (where it's position 2), but naming the reduce index `i` is uncommon enough to skip.
|
|
121
|
+
if (
|
|
122
|
+
name === 'i' &&
|
|
123
|
+
parent.params?.indexOf(node) === 1 &&
|
|
124
|
+
isCallbackToMethod(parent, ARRAY_ITERATION_METHODS)
|
|
125
|
+
) return;
|
|
126
|
+
|
|
127
|
+
context.report({
|
|
128
|
+
node,
|
|
129
|
+
messageId: 'tooShort',
|
|
130
|
+
data: { name, length: name.length, min: minLength },
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const RECOMMENDATION =
|
|
2
|
+
'Prefer a for...of loop with in-place mutation, or a purpose-built method (flatMap, Object.groupBy, flat) if it fits your pattern.';
|
|
3
|
+
|
|
4
|
+
function isReduceCall(node) {
|
|
5
|
+
if (!node) return false;
|
|
6
|
+
|
|
7
|
+
const callee = node.callee;
|
|
8
|
+
if (!callee || !callee.property || !callee.property.name) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
callee.property.name === 'reduce' || callee.property.name === 'reduceRight'
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isReduceAccumulator(def) {
|
|
18
|
+
return (
|
|
19
|
+
def &&
|
|
20
|
+
def.type === 'Parameter' &&
|
|
21
|
+
def.node.parent &&
|
|
22
|
+
isReduceCall(def.node.parent) &&
|
|
23
|
+
def.node.params &&
|
|
24
|
+
def.node.params.length &&
|
|
25
|
+
// def.name is the Identifier AST node for the definition; this identity
|
|
26
|
+
// check confirms it is the first parameter (the accumulator), not a later one.
|
|
27
|
+
def.name === def.node.params[0]
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const noReduceAccumulatorCopy = {
|
|
32
|
+
meta: {
|
|
33
|
+
type: 'problem',
|
|
34
|
+
docs: {
|
|
35
|
+
description:
|
|
36
|
+
'Disallow O(n²) accumulator copies (spread, concat) inside reduce/reduceRight callbacks',
|
|
37
|
+
recommended: true,
|
|
38
|
+
},
|
|
39
|
+
messages: {
|
|
40
|
+
AccumulatorSpread: `Spreading the reduce accumulator is O(n²). ${RECOMMENDATION}`,
|
|
41
|
+
AccumulatorShallowCopy: `Calling .{{ method }}() on the reduce accumulator is O(n²). ${RECOMMENDATION}`,
|
|
42
|
+
},
|
|
43
|
+
schema: [],
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
create(context) {
|
|
47
|
+
const { sourceCode } = context;
|
|
48
|
+
|
|
49
|
+
function checkIsAccumulatorSpread(node) {
|
|
50
|
+
if (!node.argument || node.argument.type !== 'Identifier') return;
|
|
51
|
+
|
|
52
|
+
const scope = sourceCode.getScope(node);
|
|
53
|
+
const spreadee = scope.set.get(node.argument.name);
|
|
54
|
+
const spreadeeDef =
|
|
55
|
+
spreadee && spreadee.defs && spreadee.defs.length && spreadee.defs[0];
|
|
56
|
+
|
|
57
|
+
if (isReduceAccumulator(spreadeeDef)) {
|
|
58
|
+
context.report({ node, messageId: 'AccumulatorSpread' });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function checkIsAccumulatorShallowCopy(node) {
|
|
63
|
+
if (
|
|
64
|
+
node.callee.type !== 'MemberExpression' ||
|
|
65
|
+
!node.callee.property ||
|
|
66
|
+
!node.callee.object ||
|
|
67
|
+
node.callee.object.type !== 'Identifier'
|
|
68
|
+
) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const method = node.callee.property.name;
|
|
73
|
+
if (method !== 'concat') return;
|
|
74
|
+
|
|
75
|
+
const scope = sourceCode.getScope(node);
|
|
76
|
+
const variable = scope.set.get(node.callee.object.name);
|
|
77
|
+
const definition =
|
|
78
|
+
variable && variable.defs && variable.defs.length && variable.defs[0];
|
|
79
|
+
|
|
80
|
+
if (isReduceAccumulator(definition)) {
|
|
81
|
+
context.report({ node, messageId: 'AccumulatorShallowCopy', data: { method } });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
SpreadElement: checkIsAccumulatorSpread,
|
|
87
|
+
CallExpression: checkIsAccumulatorShallowCopy,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
package/.githooks/commit-msg
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
|
|
3
|
-
commit_msg=$(head -1 "$1")
|
|
4
|
-
|
|
5
|
-
# Skip merge and revert commits (auto-generated messages)
|
|
6
|
-
case "$commit_msg" in
|
|
7
|
-
Merge\ *|Revert\ *) exit 0 ;;
|
|
8
|
-
esac
|
|
9
|
-
|
|
10
|
-
pattern="^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-zA-Z0-9_/-]+\))?!?: .+"
|
|
11
|
-
|
|
12
|
-
if ! echo "$commit_msg" | grep -qE "$pattern"; then
|
|
13
|
-
echo ""
|
|
14
|
-
echo "ERROR: Commit message does not follow Conventional Commits format."
|
|
15
|
-
echo ""
|
|
16
|
-
echo " Expected: type(optional-scope): description"
|
|
17
|
-
echo " Types: feat | fix | docs | style | refactor | test | chore | ci | perf | revert | build"
|
|
18
|
-
echo ""
|
|
19
|
-
echo " Examples:"
|
|
20
|
-
echo " feat: add new rule for import order"
|
|
21
|
-
echo " fix: correct comma-dangle config for browser"
|
|
22
|
-
echo " feat!: drop support for Node 18 (breaking change -> major bump)"
|
|
23
|
-
echo " feat(browser): add React 19 support"
|
|
24
|
-
echo ""
|
|
25
|
-
echo " See: https://www.conventionalcommits.org"
|
|
26
|
-
echo ""
|
|
27
|
-
exit 1
|
|
28
|
-
fi
|
package/.github/CODEOWNERS
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
* @HubSpotWebTeam/webteamdevs
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
name: Merge to Main
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
workflow_dispatch:
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
build:
|
|
10
|
-
# The type of runner that the job will run on
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
strategy:
|
|
13
|
-
matrix:
|
|
14
|
-
node: [24]
|
|
15
|
-
|
|
16
|
-
name: Node ${{ matrix.node }} lint
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v6
|
|
19
|
-
- name: Run unit tests
|
|
20
|
-
uses: actions/setup-node@v6
|
|
21
|
-
with:
|
|
22
|
-
node-version: ${{ matrix.node }}
|
|
23
|
-
- run: npm install
|
|
24
|
-
- run: npm run lint
|
package/.github/workflows/pr.yml
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
name: Pull Request
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches: [main]
|
|
6
|
-
workflow_dispatch:
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
build:
|
|
10
|
-
# The type of runner that the job will run on
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
strategy:
|
|
13
|
-
matrix:
|
|
14
|
-
node: [24]
|
|
15
|
-
|
|
16
|
-
name: Node ${{ matrix.node }} sample
|
|
17
|
-
steps:
|
|
18
|
-
- uses: actions/checkout@v6
|
|
19
|
-
- name: Run unit tests
|
|
20
|
-
uses: actions/setup-node@v6
|
|
21
|
-
with:
|
|
22
|
-
node-version: ${{ matrix.node }}
|
|
23
|
-
- run: npm install
|
|
24
|
-
- run: npm run lint
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
name: Release Please
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
contents: write
|
|
9
|
-
pull-requests: write
|
|
10
|
-
id-token: write # Required for OIDC npm publish
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
release-please:
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
outputs:
|
|
16
|
-
release_created: ${{ steps.release.outputs.release_created }}
|
|
17
|
-
steps:
|
|
18
|
-
- uses: googleapis/release-please-action@v5
|
|
19
|
-
id: release
|
|
20
|
-
with:
|
|
21
|
-
token: ${{ secrets.GITHUB_TOKEN }}
|
|
22
|
-
|
|
23
|
-
publish:
|
|
24
|
-
needs: release-please
|
|
25
|
-
if: ${{ needs.release-please.outputs.release_created == 'true' }}
|
|
26
|
-
runs-on: ubuntu-latest
|
|
27
|
-
steps:
|
|
28
|
-
- uses: actions/checkout@v6
|
|
29
|
-
- uses: actions/setup-node@v6
|
|
30
|
-
with:
|
|
31
|
-
node-version: 24
|
|
32
|
-
registry-url: 'https://registry.npmjs.org'
|
|
33
|
-
- run: npm ci
|
|
34
|
-
- run: npm run build --if-present
|
|
35
|
-
- name: Publish to npm
|
|
36
|
-
run: |
|
|
37
|
-
VERSION=$(node -p "require('./package.json').version")
|
|
38
|
-
if [[ "$VERSION" =~ -next\. ]] || [[ "$VERSION" =~ -(alpha|beta|rc)\. ]]; then
|
|
39
|
-
echo "Publishing prerelease $VERSION with tag 'next'"
|
|
40
|
-
npm publish --tag next
|
|
41
|
-
else
|
|
42
|
-
echo "Publishing stable $VERSION"
|
|
43
|
-
npm publish
|
|
44
|
-
fi
|
package/.nvmrc
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
v24
|
package/CHANGELOG.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## [4.2.2](https://github.com/HubSpotWebTeam/wt-eslint-node/compare/v4.2.1...v4.2.2) (2026-06-18)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
### Bug Fixes
|
|
7
|
-
|
|
8
|
-
* add default export condition for cypress.config subpath ([a15f847](https://github.com/HubSpotWebTeam/wt-eslint-node/commit/a15f847cc00430dec3ec59cc5c4cccabcabee2a1))
|
|
9
|
-
* add default export condition for cypress.config subpath ([41679a9](https://github.com/HubSpotWebTeam/wt-eslint-node/commit/41679a98cba0c514f8b66bd2495ec040c1a27775))
|
|
10
|
-
|
|
11
|
-
## [4.2.1](https://github.com/HubSpotWebTeam/wt-eslint-node/compare/v4.2.0...v4.2.1) (2026-06-05)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
### Bug Fixes
|
|
15
|
-
|
|
16
|
-
* re-trigger publish after release-please CI fix ([45fe949](https://github.com/HubSpotWebTeam/wt-eslint-node/commit/45fe94930c06d61e6f8f3015bc4e6518e187aaa1))
|
|
17
|
-
|
|
18
|
-
## [4.2.0](https://github.com/HubSpotWebTeam/wt-eslint-node/compare/v4.1.0...v4.2.0) (2026-06-05)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
### Features
|
|
22
|
-
|
|
23
|
-
* warn on hardcoded color, font-size, and font-weight values ([#37](https://github.com/HubSpotWebTeam/wt-eslint-node/issues/37)) ([783a320](https://github.com/HubSpotWebTeam/wt-eslint-node/commit/783a320a3d0e858cd370670be452aaa847ba9c39))
|
package/CLAUDE.md
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md
|
|
2
|
-
|
|
3
|
-
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
This is a configuration package (`@hs-web-team/eslint-config-node`) for HubSpot Marketing WebTeam projects. It provides shared configurations for:
|
|
8
|
-
- **ESLint** rules for Node.js and browser/React projects
|
|
9
|
-
- **Prettier** formatting configuration
|
|
10
|
-
- **Stylelint** rules for SCSS/CSS linting
|
|
11
|
-
- **Cypress** E2E testing configuration
|
|
12
|
-
|
|
13
|
-
## Key Commands
|
|
14
|
-
|
|
15
|
-
### Linting
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
npm run lint
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Runs ESLint on the codebase with auto-fix enabled. Uses the configuration defined in `index.js`.
|
|
22
|
-
|
|
23
|
-
### Testing in Projects
|
|
24
|
-
When testing changes to this package in downstream projects, you'll typically:
|
|
25
|
-
1. Make changes to the ESLint config in `index.js`
|
|
26
|
-
2. Run `npm link` in this repository
|
|
27
|
-
3. Run `npm link @hs-web-team/eslint-config-node` in the consuming project
|
|
28
|
-
4. Test the linting behavior in the consuming project
|
|
29
|
-
|
|
30
|
-
## Architecture
|
|
31
|
-
|
|
32
|
-
### Core Configuration Files
|
|
33
|
-
|
|
34
|
-
#### `index.js` - Node.js Configuration
|
|
35
|
-
- The main ESLint configuration export using ESLint 9's flat config format
|
|
36
|
-
- Uses `@eslint/js`, `typescript-eslint`, and `globals` packages
|
|
37
|
-
- Exports an array of configuration objects (flat config format)
|
|
38
|
-
- Structure:
|
|
39
|
-
1. Global ignores object
|
|
40
|
-
2. `js.configs.recommended` for JavaScript baseline
|
|
41
|
-
3. JavaScript-specific config for `**/*.{js,mjs,cjs}` files
|
|
42
|
-
4. Spreads `tseslint.configs.recommended` for TypeScript baseline
|
|
43
|
-
5. TypeScript-specific config for `**/*.{ts,mts,cts,tsx}` files
|
|
44
|
-
- Includes Node.js, ES2022, and Jest globals
|
|
45
|
-
- Common ignores: `node_modules`, `.serverless`, `.webpack`, `dist`, `eslint.config.js`
|
|
46
|
-
- Key custom rules: `no-console` (allows info/warn/error), `max-len` (120 chars), camelcase disabled
|
|
47
|
-
|
|
48
|
-
#### `browser.js` - Browser/React Configuration
|
|
49
|
-
- Browser and React-focused ESLint configuration using ESLint 9's flat config format
|
|
50
|
-
- Uses `@eslint/js`, `typescript-eslint`, `globals`, and React-specific plugins
|
|
51
|
-
- Includes `eslint-plugin-react`, `eslint-plugin-react-hooks`, and `eslint-plugin-jsx-a11y`
|
|
52
|
-
- Supports both JavaScript/JSX and TypeScript/TSX files
|
|
53
|
-
- Structure:
|
|
54
|
-
1. Global ignores (node_modules, dist, build, .next, coverage)
|
|
55
|
-
2. Base JavaScript/JSX config with browser globals
|
|
56
|
-
3. React plugin configuration
|
|
57
|
-
4. TypeScript config with browser globals
|
|
58
|
-
5. React plugin configuration for TypeScript files
|
|
59
|
-
- Includes browser, ES2021, and Jest globals, plus custom globals ($, jQuery, Invoca)
|
|
60
|
-
- Key custom rules: Similar base rules to Node config, plus React-specific rules
|
|
61
|
-
|
|
62
|
-
### Prettier Integration
|
|
63
|
-
- **`bin/add-prettier-scripts.js`**: Executable script that consuming projects can run to set up Prettier
|
|
64
|
-
- Uses CommonJS (`require`) syntax (different from the main package which is ESM)
|
|
65
|
-
- Adds `prettier:check` and `prettier:write` scripts to package.json
|
|
66
|
-
- Creates `.prettierrc.js` file that imports from this package's `.prettierrc.json`
|
|
67
|
-
- Creates `.prettierignore` file with sensible defaults (npm-shrinkwrap.json, package-lock.json, .eslintrc, *.html)
|
|
68
|
-
- **`.prettierrc.json`**: Shared Prettier configuration
|
|
69
|
-
- 120 char width, single quotes, 2-space tabs, trailing commas
|
|
70
|
-
- Arrow parens: avoid, LF line endings, single attribute per line
|
|
71
|
-
- Override for YAML files: uses double quotes instead of single
|
|
72
|
-
|
|
73
|
-
### Stylelint Configuration
|
|
74
|
-
- **`.stylelintrc.json`**: Shared Stylelint configuration for SCSS/CSS linting
|
|
75
|
-
- Extends `stylelint-config-standard-scss` for standard SCSS linting rules
|
|
76
|
-
- Customized rules for HubSpot projects:
|
|
77
|
-
- Disables strict class name patterns and variable naming patterns
|
|
78
|
-
- Allows zero units (e.g., `0px`), redundant longhand properties, and duplicate selectors
|
|
79
|
-
- Configures SCSS-specific rules to ignore common directives (`@function`, `@if`, `@mixin`, etc.)
|
|
80
|
-
- Enforces lowercase keywords with camelCase for SVG keywords
|
|
81
|
-
- Sets media feature range notation to "context" with warning severity
|
|
82
|
-
- Consuming projects extend this via `.stylelintrc.json`: `{ "extends": "@hs-web-team/eslint-config-node/.stylelintrc.json" }`
|
|
83
|
-
- Documentation: `examples/stylelint-usage.md`
|
|
84
|
-
|
|
85
|
-
### Cypress Configuration
|
|
86
|
-
- **`cypress.config.cjs`**: Shared Cypress configuration for E2E testing
|
|
87
|
-
- Uses CommonJS (`require`) syntax
|
|
88
|
-
- Core settings:
|
|
89
|
-
- Disables Chrome web security for cross-origin testing
|
|
90
|
-
- Timeouts: 20s default command, page load, and response timeouts
|
|
91
|
-
- Viewport: 1920x1080
|
|
92
|
-
- Retries: 1 retry in run mode, 0 in open mode
|
|
93
|
-
- Screenshots on failure enabled, video disabled by default
|
|
94
|
-
- Uses **esbuild** preprocessor for fast bundling (much faster than webpack)
|
|
95
|
-
- Includes Cucumber preprocessor setup with native esbuild support
|
|
96
|
-
- Native TypeScript support via esbuild (no additional loader needed)
|
|
97
|
-
- `@badeball/cypress-cucumber-preprocessor`, `@bahmutov/cypress-esbuild-preprocessor`, `esbuild`, and `js-yaml` are all bundled — consumers only need to install `cypress`
|
|
98
|
-
- Spec pattern: `cypress/e2e/*.cy.js`
|
|
99
|
-
- Exported utilities:
|
|
100
|
-
- `config`: Main Cypress configuration object
|
|
101
|
-
- `envs`: Environment constants (DEV, QA, PROD)
|
|
102
|
-
- `getDevBaseUrl()`: Reads baseUrl from `hubspot.config.yml` DEV portal
|
|
103
|
-
- `getBaseUrls()`: Reads URLs from `.ci/config.yml` for multiple environments
|
|
104
|
-
- HubSpot-specific: Designed to work with HubSpot CMS projects using `hubspot.config.yml`
|
|
105
|
-
- Documentation: `examples/cypress-usage.md`
|
|
106
|
-
|
|
107
|
-
### Package Details
|
|
108
|
-
- **Type**: ESM module (`"type": "module"`)
|
|
109
|
-
- **Node requirement**: >= 24
|
|
110
|
-
- **Exports**:
|
|
111
|
-
- Main export (`.`): `index.js` - Node.js ESLint configuration
|
|
112
|
-
- Browser export (`./browser`): `browser.js` - Browser/React ESLint configuration
|
|
113
|
-
- Prettier export (`./.prettierrc.json`): `.prettierrc.json` - Prettier configuration
|
|
114
|
-
- Stylelint export (`./.stylelintrc.json`): `.stylelintrc.json` - Stylelint configuration
|
|
115
|
-
- Cypress export (`./cypress.config`): `cypress.config.cjs` (runtime), `cypress.config.d.ts` (types)
|
|
116
|
-
- **Binary command**: `add-prettier` maps to `bin/add-prettier-scripts.js`
|
|
117
|
-
|
|
118
|
-
### Migration Context
|
|
119
|
-
This package is currently on v3, which uses ESLint 9's flat config format. The project has migrated from:
|
|
120
|
-
- v1 → v2: See `docs/MIGRATION-V2.md`
|
|
121
|
-
- v2 → v3: See `docs/MIGRATION-V3.md` (ESLint 9 flat config migration requiring Node.js 24+)
|
|
122
|
-
|
|
123
|
-
## Important Notes
|
|
124
|
-
|
|
125
|
-
- This package supports multiple use cases:
|
|
126
|
-
- **ESLint**: Backend Node.js projects (default export) and browser/React projects (`/browser` export)
|
|
127
|
-
- **Prettier**: Code formatting (`./.prettierrc.json` export)
|
|
128
|
-
- **Stylelint**: SCSS/CSS linting (`./.stylelintrc.json` export)
|
|
129
|
-
- **Cypress**: E2E testing (`./cypress.config` export)
|
|
130
|
-
- ESLint configuration uses ESLint 9's flat config format (not the legacy `.eslintrc` format)
|
|
131
|
-
- Downstream projects extend configs:
|
|
132
|
-
- ESLint (Node.js): `...wtConfig` in `eslint.config.js`
|
|
133
|
-
- ESLint (Browser): `...wtBrowserConfig` in `eslint.config.js`
|
|
134
|
-
- Prettier: Import via `.prettierrc.js` using `require('@hs-web-team/eslint-config-node/.prettierrc.json')`
|
|
135
|
-
- Stylelint: Extend in `.stylelintrc.json` with `"extends": "@hs-web-team/eslint-config-node/.stylelintrc.json"`
|
|
136
|
-
- Cypress: Import and spread in `cypress.config.js`
|
|
137
|
-
- Mixed module systems:
|
|
138
|
-
- Main package is ESM (`index.js`, `browser.js`)
|
|
139
|
-
- Utility scripts use CommonJS (`bin/add-prettier-scripts.js`, `cypress.config.cjs`)
|
|
140
|
-
- CI runs on Node 24 (see `.github/workflows/pr.yml`)
|
|
141
|
-
- No automated tests currently (`npm test` will fail with "Error: no test specified")
|
|
142
|
-
- Detailed documentation available in `examples/`:
|
|
143
|
-
- `browser-usage.md` - Browser/React ESLint configuration
|
|
144
|
-
- `stylelint-usage.md` - Stylelint configuration
|
|
145
|
-
- `cypress-usage.md` - Cypress E2E testing configuration
|