@hs-web-team/eslint-config-node 4.0.0 → 4.1.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.
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,17 @@
1
+ name: Release Please
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ release-please:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: googleapis/release-please-action@v5
16
+ with:
17
+ token: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "4.1.0"
3
+ }
package/.stylelintrc.json CHANGED
@@ -11,10 +11,14 @@
11
11
  }
12
12
  ],
13
13
  "scss/dollar-variable-pattern": null,
14
- "scss/at-import-no-partial-leading-underscore": null,
15
- "selector-class-pattern": null,
14
+ "selector-class-pattern": [
15
+ "^-?[a-z0-9]([a-z0-9]|-(?!-))*$",
16
+ {
17
+ "message": "Class names should only contain lowercase alphanumeric characters and hyphens, with no consecutive hyphens",
18
+ "severity": "warning"
19
+ }
20
+ ],
16
21
  "declaration-block-no-redundant-longhand-properties": null,
17
- "scss/at-import-partial-extension": null,
18
22
  "alpha-value-notation": null,
19
23
  "scss/percent-placeholder-pattern": null,
20
24
  "scss/at-mixin-argumentless-call-parentheses": null,
@@ -35,6 +39,13 @@
35
39
  {
36
40
  "camelCaseSvgKeywords": true
37
41
  }
42
+ ],
43
+ "selector-disallowed-list": [
44
+ ["/(?:^|[\\s>+~])\\s*\\.-[a-z]/i"],
45
+ {
46
+ "message": "Modifier classes (starting with '-') must not be standalone selectors; always combine with a base class (e.g., .prefix-element.-modifier or &.-modifier in nested rules)",
47
+ "severity": "error"
48
+ }
38
49
  ]
39
50
  }
40
51
  }
package/README.md CHANGED
@@ -13,6 +13,7 @@ This package provides ESLint rules and configurations for **Hubspot Marketing We
13
13
  - [Accessibility Testing](#accessibility-testing-optional)
14
14
  - [Where to use it](#where-to-use-it)
15
15
  - [Using the Prettier Scripts](#using-the-prettier-scripts)
16
+ - [Contributing](#contributing)
16
17
  <!-- index-end -->
17
18
 
18
19
  ## Node.js Setup
@@ -199,3 +200,57 @@ See [MIGRATION-V2.md](./docs/MIGRATION-V2.md)
199
200
  ## Migration from v2 to v3
200
201
 
201
202
  See [MIGRATION-V3.md](./docs/MIGRATION-V3.md)
203
+
204
+ ## Contributing
205
+
206
+ ### Conventional Commits
207
+
208
+ This repo uses [Conventional Commits](https://www.conventionalcommits.org) for all commit messages. A `commit-msg` git hook validates the format automatically — run `npm install` once to activate it.
209
+
210
+ **Format:** `type(optional-scope): description`
211
+
212
+ | Type | Use for | Version bump |
213
+ |------|---------|-------------|
214
+ | `feat` | New feature or config rule | Minor (`4.0.0` → `4.1.0`) |
215
+ | `fix` | Bug fix or rule correction | Patch (`4.0.0` → `4.0.1`) |
216
+ | `feat!` or `BREAKING CHANGE:` footer | Breaking API/config change | Major (`4.0.0` → `5.0.0`) |
217
+ | `chore` | Dependency updates, tooling | No release |
218
+ | `docs` | Documentation only | No release |
219
+ | `refactor` | Internal restructuring | No release |
220
+ | `ci` | CI/CD changes | No release |
221
+ | `test` | Test changes | No release |
222
+
223
+ **Examples:**
224
+
225
+ ```
226
+ feat: add import/order rule to Node config
227
+ fix: align browser comma-dangle with prettier defaults
228
+ feat(browser): add React 19 peer dep support
229
+ feat!: drop Node 18 support — requires Node 24+
230
+ chore: bump typescript-eslint to v8.60
231
+ docs: update Cypress usage examples
232
+ ```
233
+
234
+ A breaking change can also be indicated by adding a `BREAKING CHANGE:` footer to any commit type:
235
+
236
+ ```
237
+ feat: remove deprecated stylelint rules
238
+
239
+ BREAKING CHANGE: Removed rules that were dropped in stylelint-scss@7.
240
+ Projects relying on those rules must update their configs before upgrading.
241
+ ```
242
+
243
+ ### Release Process
244
+
245
+ Releases are automated via [release-please](https://github.com/googleapis/release-please). No manual version bumping or GitHub release creation is needed.
246
+
247
+ **How it works:**
248
+
249
+ 1. Merge PRs to `main` using conventional commit messages (see above).
250
+ 2. After each merge, release-please opens or updates a **Release PR** (titled `chore(main): release X.Y.Z`) that accumulates all unreleased changes, bumps `package.json`, and generates a `CHANGELOG.md` entry.
251
+ 3. When you are ready to publish, **merge the Release PR**.
252
+ 4. release-please creates a GitHub release and git tag automatically.
253
+ 5. The `release.yml` workflow fires on the new release and publishes to npm.
254
+
255
+ **To release a prerelease (`-next.N`):**
256
+ Update the version in `package.json` manually on a branch (e.g. `4.1.0-next.0`) and create a GitHub release by hand — the existing `release.yml` workflow will publish it with the `next` tag on npm. Prereleases are not yet managed by release-please in this repo.
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const projectRoot = process.env.INIT_CWD || process.env.npm_config_local_prefix;
7
+
8
+ // Skip if we can't determine the consuming project's root, or if postinstall
9
+ // is running inside this package's own development environment.
10
+ if (!projectRoot || projectRoot === path.resolve(__dirname, '..')) {
11
+ process.exit(0);
12
+ }
13
+
14
+ const ownPkg = require('../package.json');
15
+
16
+ const stylelintPeers = Object.entries(ownPkg.peerDependencies || {}).filter(([name]) =>
17
+ name.startsWith('stylelint')
18
+ );
19
+
20
+ // Extracts the minimum required major version from a semver range like ^17.0.0 or >=17.1.1.
21
+ const minMajor = (range) => {
22
+ const match = range.match(/(\d+)/);
23
+ return match ? parseInt(match[1], 10) : 0;
24
+ };
25
+
26
+ const isInstalled = (pkg) =>
27
+ fs.existsSync(path.join(projectRoot, 'node_modules', pkg, 'package.json'));
28
+
29
+ // Only warn about missing stylelint plugins if stylelint itself is installed —
30
+ // consumers who don't use stylelint at all don't need any of these.
31
+ const stylelintInUse = isInstalled('stylelint');
32
+
33
+ const outdated = [];
34
+ const missing = [];
35
+
36
+ for (const [pkg, requiredRange] of stylelintPeers) {
37
+ const pkgJsonPath = path.join(projectRoot, 'node_modules', pkg, 'package.json');
38
+
39
+ if (!fs.existsSync(pkgJsonPath)) {
40
+ if (stylelintInUse && pkg !== 'stylelint') {
41
+ missing.push({ pkg, requiredRange });
42
+ }
43
+ continue;
44
+ }
45
+
46
+ const { version } = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
47
+ const installedMajor = parseInt(version.split('.')[0], 10);
48
+
49
+ if (installedMajor < minMajor(requiredRange)) {
50
+ outdated.push({ pkg, version, requiredRange });
51
+ }
52
+ }
53
+
54
+ if (outdated.length === 0 && missing.length === 0) process.exit(0);
55
+
56
+ const pkgName = ownPkg.name;
57
+
58
+ if (missing.length > 0) {
59
+ console.warn(`\n⚠️ ${pkgName}: missing stylelint peer dependencies.\n`);
60
+ console.warn(' These packages are required for stylelint to work correctly:');
61
+ for (const { pkg, requiredRange } of missing) {
62
+ console.warn(` • ${pkg} (required: ${requiredRange})`);
63
+ }
64
+ const installCmd = missing.map(({ pkg, requiredRange }) => `${pkg}@"${requiredRange}"`).join(' ');
65
+ console.warn(`\n Run: npm install --save-dev ${installCmd}`);
66
+ console.warn(' Linting will fail without these packages.\n');
67
+ }
68
+
69
+ if (outdated.length > 0) {
70
+ console.warn(`\n⚠️ ${pkgName}: outdated stylelint peer dependencies detected.\n`);
71
+ console.warn(' These packages are installed at versions that are no longer compatible:');
72
+ for (const { pkg, version, requiredRange } of outdated) {
73
+ console.warn(` • ${pkg}@${version} → required: ${requiredRange}`);
74
+ }
75
+ const upgradeCmd = outdated.map(({ pkg, requiredRange }) => `${pkg}@"${requiredRange}"`).join(' ');
76
+ console.warn(`\n Run: npm install ${upgradeCmd}`);
77
+ console.warn(' Failure to upgrade may cause unexpected lint errors.\n');
78
+ }
package/browser.js CHANGED
@@ -7,7 +7,7 @@ import jsxA11yPlugin from 'eslint-plugin-jsx-a11y';
7
7
 
8
8
  // Base rules adapted from the browser config
9
9
  const baseRules = {
10
- 'comma-dangle': 'warn',
10
+ 'comma-dangle': ['warn', 'always-multiline'],
11
11
  'no-param-reassign': ['warn', { props: false }],
12
12
  'arrow-parens': 0,
13
13
  'no-plusplus': 0,
@@ -19,8 +19,8 @@ const baseRules = {
19
19
  allow: [
20
20
  '_hsg',
21
21
  '_hsq',
22
- ]
23
- }
22
+ ],
23
+ },
24
24
  ],
25
25
  'no-trailing-spaces': ['error', { skipBlankLines: true }],
26
26
  'no-unused-expressions': ['warn', { allowTernary: true }],
@@ -29,15 +29,15 @@ const baseRules = {
29
29
  {
30
30
  code: 120,
31
31
  ignoreStrings: true,
32
- ignoreTemplateLiterals: true
33
- }
32
+ ignoreTemplateLiterals: true,
33
+ },
34
34
  ],
35
35
  'operator-linebreak': 0,
36
36
  'implicit-arrow-linebreak': 0,
37
37
  indent: 0,
38
38
  'object-curly-newline': 0,
39
39
  'function-paren-newline': 0,
40
- 'nonblock-statement-body-position': 0
40
+ 'nonblock-statement-body-position': 0,
41
41
  };
42
42
 
43
43
  // React-specific rules
@@ -50,10 +50,10 @@ const reactRules = {
50
50
  2,
51
51
  {
52
52
  component: true,
53
- html: false
54
- }
53
+ html: false,
54
+ },
55
55
  ],
56
- 'react/forbid-prop-types': 0
56
+ 'react/forbid-prop-types': 0,
57
57
  };
58
58
 
59
59
  // Common ignore patterns
@@ -63,13 +63,13 @@ const commonIgnores = [
63
63
  '**/build/**',
64
64
  '**/.next/**',
65
65
  '**/coverage/**',
66
- 'eslint.config.js'
66
+ 'eslint.config.js',
67
67
  ];
68
68
 
69
69
  export default [
70
70
  // Global ignores
71
71
  {
72
- ignores: commonIgnores
72
+ ignores: commonIgnores,
73
73
  },
74
74
  // Base config for all JavaScript files
75
75
  js.configs.recommended,
@@ -85,17 +85,17 @@ export default [
85
85
  // Custom browser globals from original config
86
86
  $: true,
87
87
  jQuery: true,
88
- Invoca: true
88
+ Invoca: true,
89
89
  },
90
90
  parserOptions: {
91
91
  ecmaFeatures: {
92
- jsx: true
93
- }
94
- }
92
+ jsx: true,
93
+ },
94
+ },
95
95
  },
96
96
  rules: {
97
- ...baseRules
98
- }
97
+ ...baseRules,
98
+ },
99
99
  },
100
100
  // React configuration
101
101
  {
@@ -103,24 +103,24 @@ export default [
103
103
  plugins: {
104
104
  react: reactPlugin,
105
105
  'react-hooks': reactHooksPlugin,
106
- 'jsx-a11y': jsxA11yPlugin
106
+ 'jsx-a11y': jsxA11yPlugin,
107
107
  },
108
108
  settings: {
109
109
  react: {
110
- version: 'detect'
111
- }
110
+ version: 'detect',
111
+ },
112
112
  },
113
113
  rules: {
114
114
  ...reactPlugin.configs.recommended.rules,
115
115
  ...reactHooksPlugin.configs.recommended.rules,
116
116
  ...jsxA11yPlugin.configs.recommended.rules,
117
- ...reactRules
118
- }
117
+ ...reactRules,
118
+ },
119
119
  },
120
120
  // TypeScript config
121
121
  ...tseslint.configs.recommended.map(config => ({
122
122
  ...config,
123
- files: ['**/*.{ts,mts,cts,tsx}']
123
+ files: ['**/*.{ts,mts,cts,tsx}'],
124
124
  })),
125
125
  {
126
126
  files: ['**/*.{ts,mts,cts,tsx}'],
@@ -133,17 +133,17 @@ export default [
133
133
  ...globals.jest,
134
134
  $: true,
135
135
  jQuery: true,
136
- Invoca: true
136
+ Invoca: true,
137
137
  },
138
138
  parserOptions: {
139
139
  ecmaFeatures: {
140
- jsx: true
141
- }
142
- }
140
+ jsx: true,
141
+ },
142
+ },
143
143
  },
144
144
  rules: {
145
- ...baseRules
146
- }
145
+ ...baseRules,
146
+ },
147
147
  },
148
148
  // React configuration for TypeScript files
149
149
  {
@@ -151,18 +151,18 @@ export default [
151
151
  plugins: {
152
152
  react: reactPlugin,
153
153
  'react-hooks': reactHooksPlugin,
154
- 'jsx-a11y': jsxA11yPlugin
154
+ 'jsx-a11y': jsxA11yPlugin,
155
155
  },
156
156
  settings: {
157
157
  react: {
158
- version: 'detect'
159
- }
158
+ version: 'detect',
159
+ },
160
160
  },
161
161
  rules: {
162
162
  ...reactPlugin.configs.recommended.rules,
163
163
  ...reactHooksPlugin.configs.recommended.rules,
164
164
  ...jsxA11yPlugin.configs.recommended.rules,
165
- ...reactRules
166
- }
167
- }
165
+ ...reactRules,
166
+ },
167
+ },
168
168
  ];
package/index.js CHANGED
@@ -20,8 +20,8 @@ const baseRules = {
20
20
  allow: [
21
21
  '__dirname',
22
22
  '__filename',
23
- ]
24
- }
23
+ ],
24
+ },
25
25
  ],
26
26
  'no-confusing-arrow': 0,
27
27
  'import/no-unresolved': 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hs-web-team/eslint-config-node",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "HubSpot Marketing WebTeam shared configurations for ESLint, Prettier, Stylelint, and Cypress",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -29,8 +29,10 @@
29
29
  }
30
30
  },
31
31
  "scripts": {
32
+ "postinstall": "node bin/check-peer-deps.cjs",
32
33
  "lint": "npx eslint -c ./index.js *.js --fix",
33
- "test": "echo \"Error: no test specified\" && exit 1"
34
+ "test": "echo \"Error: no test specified\" && exit 1",
35
+ "prepare": "git rev-parse --git-dir > /dev/null 2>&1 && git config core.hooksPath .githooks || true"
34
36
  },
35
37
  "engines": {
36
38
  "node": ">=24"
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "release-type": "node",
4
+ "include-component-in-tag": false,
5
+ "packages": {
6
+ ".": {}
7
+ }
8
+ }