diff --git a/eslint.config.mjs b/eslint.config.mjs index 28d091533f2..f6ac8cd746b 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -8,6 +8,7 @@ import {defineConfig, globalIgnores} from 'eslint/config' import githubPlugin from 'eslint-plugin-github' import jest from 'eslint-plugin-jest' import storybook from 'eslint-plugin-storybook' +import primerDev from 'eslint-plugin-primer-dev' import react from 'eslint-plugin-react' import reactCompiler from 'eslint-plugin-react-compiler' import reactHooks from 'eslint-plugin-react-hooks' @@ -118,6 +119,7 @@ const config = defineConfig([ 'primer-react/prefer-action-list-item-onselect': 'error', }, }, + primerDev.configs.recommended, { languageOptions: { diff --git a/package-lock.json b/package-lock.json index 56fdf11a3f7..1cb2e3af5c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,7 +92,7 @@ "react-dom": "^18.3.1" }, "devDependencies": { - "@primer/react": "37.26.0", + "@primer/react": "37.27.0", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.11.0", @@ -445,7 +445,7 @@ "name": "example-nextjs", "version": "0.0.0", "dependencies": { - "@primer/react": "37.26.0", + "@primer/react": "37.27.0", "next": "^15.2.3", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -630,7 +630,7 @@ "version": "0.0.0", "dependencies": { "@primer/octicons-react": "^19.14.0", - "@primer/react": "37.26.0", + "@primer/react": "37.27.0", "clsx": "^2.1.1", "next": "^14.2.30", "react": "^18.3.1", @@ -11268,7 +11268,9 @@ } }, "node_modules/acorn": { - "version": "8.14.0", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", "bin": { @@ -15440,6 +15442,10 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/eslint-plugin-primer-dev": { + "resolved": "packages/eslint-plugin-primer-dev", + "link": true + }, "node_modules/eslint-plugin-primer-react": { "version": "6.1.6", "resolved": "https://registry.npmjs.org/eslint-plugin-primer-react/-/eslint-plugin-primer-react-6.1.6.tgz", @@ -32499,6 +32505,344 @@ "url": "https://github.com/sponsors/wooorm" } }, + "packages/eslint-plugin-primer-dev": { + "version": "0.0.0", + "devDependencies": { + "eslint": "^9.29.0" + }, + "peerDependencies": { + "eslint": "9.x" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/@eslint/config-array": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.1.tgz", + "integrity": "sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/@eslint/js": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.29.0.tgz", + "integrity": "sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/eslint": { + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.29.0.tgz", + "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.1", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.29.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "packages/eslint-plugin-primer-dev/node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "packages/eslint-plugin-primer-dev/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "packages/eslint-plugin-primer-dev/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "packages/postcss-preset-primer": { "version": "0.0.0", "dependencies": { @@ -32661,7 +33005,7 @@ }, "packages/react": { "name": "@primer/react", - "version": "37.26.0", + "version": "37.27.0", "license": "MIT", "dependencies": { "@github/relative-time-element": "^4.4.5", diff --git a/package.json b/package.json index de19096d310..f582aae1876 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "build:docs:preview": "NODE_OPTIONS=--openssl-legacy-provider script/build-docs preview", "build:components.json": "npm run build:components.json -w @primer/react", "build:hooks.json": "npm run build:hooks.json -w @primer/react", - "lint": "eslint '**/*.{js,ts,tsx,md,mdx}' --max-warnings=0", + "lint": "NODE_OPTIONS='--experimental-strip-types' eslint '**/*.{js,ts,tsx,md,mdx}' --max-warnings=0 --flag unstable_native_nodejs_ts_config", "lint:css": "stylelint --rd -q '**/*.css'", "lint:css:fix": "npm run lint:css -- --fix", "lint:fix": "npm run lint -- --fix", diff --git a/packages/eslint-plugin-primer-dev/package.json b/packages/eslint-plugin-primer-dev/package.json new file mode 100644 index 00000000000..c38840618d1 --- /dev/null +++ b/packages/eslint-plugin-primer-dev/package.json @@ -0,0 +1,12 @@ +{ + "name": "eslint-plugin-primer-dev", + "type": "module", + "version": "0.0.0", + "exports": "./src/index.ts", + "peerDependencies": { + "eslint": "9.x" + }, + "devDependencies": { + "eslint": "^9.29.0" + } +} diff --git a/packages/eslint-plugin-primer-dev/src/__tests__/prefer-spread-before-props.test.ts b/packages/eslint-plugin-primer-dev/src/__tests__/prefer-spread-before-props.test.ts new file mode 100644 index 00000000000..270aabea601 --- /dev/null +++ b/packages/eslint-plugin-primer-dev/src/__tests__/prefer-spread-before-props.test.ts @@ -0,0 +1,36 @@ +import {RuleTester} from 'eslint' +import {test} from 'vitest' +import {rule} from '../rules/prefer-spread-before-props' + +const ruleTester = new RuleTester({ + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, +}) + +test('prefer-spread-before-props', () => { + ruleTester.run('prefer-spread-before-props', rule, { + valid: [ + { + code: '', + }, + ], + invalid: [ + { + code: '', + output: '', + errors: [ + { + messageId: 'spreadBeforeProps', + }, + ], + }, + ], + }) +}) diff --git a/packages/eslint-plugin-primer-dev/src/index.ts b/packages/eslint-plugin-primer-dev/src/index.ts new file mode 100644 index 00000000000..876835a055c --- /dev/null +++ b/packages/eslint-plugin-primer-dev/src/index.ts @@ -0,0 +1,24 @@ +import {rule as preferSpreadBeforeProps} from './rules/prefer-spread-before-props.ts' + +const plugin = { + meta: 'eslint-plugin-primer-dev', + rules: { + 'prefer-spread-before-props': preferSpreadBeforeProps, + }, + configs: {}, +} + +Object.assign(plugin.configs, { + recommended: [ + { + plugins: { + 'primer-dev': plugin, + }, + rules: { + 'primer-dev/prefer-spread-before-props': 'error', + }, + }, + ], +}) + +export default plugin diff --git a/packages/eslint-plugin-primer-dev/src/rules/prefer-spread-before-props.ts b/packages/eslint-plugin-primer-dev/src/rules/prefer-spread-before-props.ts new file mode 100644 index 00000000000..9de33e691ad --- /dev/null +++ b/packages/eslint-plugin-primer-dev/src/rules/prefer-spread-before-props.ts @@ -0,0 +1,46 @@ +import type {Rule} from 'eslint' + +export const rule: Rule.RuleModule = { + meta: { + type: 'problem', + fixable: 'code', + messages: { + spreadBeforeProps: 'Spread attributes must be placed before other props', + }, + }, + create(context) { + return { + JSXOpeningElement(node) { + const index = node.attributes.findIndex(attribute => { + return attribute.type === 'JSXSpreadAttribute' + }) + if (index === -1) { + return + } + + if (index !== 0) { + context.report({ + node, + messageId: 'spreadBeforeProps', + fix(fixer) { + const {sourceCode} = context + const attributes = node.attributes.slice() + const [spreadAttribute] = attributes.splice(index, 1) + attributes.unshift(spreadAttribute) + + // Get the range from the first attribute to the last attribute + const firstAttr = node.attributes[0] + const lastAttr = node.attributes[node.attributes.length - 1] + const range: [number, number] = [firstAttr.range![0], lastAttr.range![1]] + + // Generate the new attributes text with proper spacing + const newAttributesText = attributes.map(attr => sourceCode.getText(attr)).join(' ') + + return fixer.replaceTextRange(range, newAttributesText) + }, + }) + } + }, + } + }, +} diff --git a/packages/eslint-plugin-primer-dev/vitest.config.ts b/packages/eslint-plugin-primer-dev/vitest.config.ts new file mode 100644 index 00000000000..e28d08c4e78 --- /dev/null +++ b/packages/eslint-plugin-primer-dev/vitest.config.ts @@ -0,0 +1,7 @@ +import {defineConfig} from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + }, +})