diff --git a/README.md b/README.md index 87ddc0f..45ac488 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ import eslintConfig from "@jimmy.codes/eslint-config"; export default eslintConfig({ astro: false, jest: false, + nextjs: false, playwright: false, react: false, storybook: false, diff --git a/package.json b/package.json index 659816a..945d6f3 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "dependencies": { "@eslint-community/eslint-plugin-eslint-comments": "^4.4.1", "@eslint/js": "^9.16.0", + "@next/eslint-plugin-next": "^15.0.3", "@tanstack/eslint-plugin-query": "^5.62.1", "@types/eslint": "9.6.1", "@typescript-eslint/parser": "^8.17.0", @@ -102,6 +103,7 @@ "is-ci": "3.0.1", "jiti": "2.4.1", "lefthook": "1.8.5", + "next": "15.0.3", "pkgroll": "2.5.1", "prettier": "3.4.1", "react": "18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2baab75..6b5117e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@eslint/js': specifier: ^9.16.0 version: 9.16.0 + '@next/eslint-plugin-next': + specifier: ^15.0.3 + version: 15.0.3 '@tanstack/eslint-plugin-query': specifier: ^5.62.1 version: 5.62.1(eslint@9.16.0(jiti@2.4.1))(typescript@5.7.2) @@ -168,6 +171,9 @@ importers: lefthook: specifier: 1.8.5 version: 1.8.5 + next: + specifier: 15.0.3 + version: 15.0.3(@babel/core@7.26.0)(@playwright/test@1.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) pkgroll: specifier: 2.5.1 version: 2.5.1(typescript@5.7.2) @@ -1055,6 +1061,60 @@ packages: '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@next/env@15.0.3': + resolution: {integrity: sha512-t9Xy32pjNOvVn2AS+Utt6VmyrshbpfUMhIjFO60gI58deSo/KgLOp31XZ4O+kY/Is8WAGYwA5gR7kOb1eORDBA==} + + '@next/eslint-plugin-next@15.0.3': + resolution: {integrity: sha512-3Ln/nHq2V+v8uIaxCR6YfYo7ceRgZNXfTd3yW1ukTaFbO+/I8jNakrjYWODvG9BuR2v5kgVtH/C8r0i11quOgw==} + + '@next/swc-darwin-arm64@15.0.3': + resolution: {integrity: sha512-s3Q/NOorCsLYdCKvQlWU+a+GeAd3C8Rb3L1YnetsgwXzhc3UTWrtQpB/3eCjFOdGUj5QmXfRak12uocd1ZiiQw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.0.3': + resolution: {integrity: sha512-Zxl/TwyXVZPCFSf0u2BNj5sE0F2uR6iSKxWpq4Wlk/Sv9Ob6YCKByQTkV2y6BCic+fkabp9190hyrDdPA/dNrw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.0.3': + resolution: {integrity: sha512-T5+gg2EwpsY3OoaLxUIofmMb7ohAUlcNZW0fPQ6YAutaWJaxt1Z1h+8zdl4FRIOr5ABAAhXtBcpkZNwUcKI2fw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@15.0.3': + resolution: {integrity: sha512-WkAk6R60mwDjH4lG/JBpb2xHl2/0Vj0ZRu1TIzWuOYfQ9tt9NFsIinI1Epma77JVgy81F32X/AeD+B2cBu/YQA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@15.0.3': + resolution: {integrity: sha512-gWL/Cta1aPVqIGgDb6nxkqy06DkwJ9gAnKORdHWX1QBbSZZB+biFYPFti8aKIQL7otCE1pjyPaXpFzGeG2OS2w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@15.0.3': + resolution: {integrity: sha512-QQEMwFd8r7C0GxQS62Zcdy6GKx999I/rTO2ubdXEe+MlZk9ZiinsrjwoiBL5/57tfyjikgh6GOU2WRQVUej3UA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@15.0.3': + resolution: {integrity: sha512-9TEp47AAd/ms9fPNgtgnT7F3M1Hf7koIYYWCMQ9neOwjbVWJsHZxrFbI3iEDJ8rf1TDGpmHbKxXf2IFpAvheIQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.0.3': + resolution: {integrity: sha512-VNAz+HN4OGgvZs6MOoVfnn41kBzT+M+tB+OK4cww6DNyWS6wKaDpaAm/qLeOUbnMh0oVx1+mg0uoYARF69dJyA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1487,6 +1547,12 @@ packages: '@storybook/csf@0.1.11': resolution: {integrity: sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==} + '@swc/counter@0.1.3': + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + + '@swc/helpers@0.5.13': + resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} + '@tanstack/eslint-plugin-query@5.62.1': resolution: {integrity: sha512-1886D5U+re1TW0wSH4/kUGG36yIoW5Wkz4twVEzlk3ZWmjF3XkRSWgB+Sc7n+Lyzt8usNV8ZqkZE6DA7IC47fQ==} peerDependencies: @@ -1951,6 +2017,10 @@ packages: peerDependencies: esbuild: '>=0.18' + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2055,6 +2125,9 @@ packages: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -2722,6 +2795,10 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} + fast-glob@3.3.2: resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} engines: {node: '>=8.6.0'} @@ -3859,6 +3936,27 @@ packages: nerf-dart@1.0.0: resolution: {integrity: sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==} + next@15.0.3: + resolution: {integrity: sha512-ontCbCRKJUIoivAdGB34yCaOcPgYXr9AAkV/IwqFfWWTXEPUgLYkSkqBhIk9KK7gGmgjc64B+RdoeIDM13Irnw==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.41.2 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-66855b96-20241106 + react-dom: ^18.2.0 || 19.0.0-rc-66855b96-20241106 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + nlcst-to-string@4.0.0: resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} @@ -4249,6 +4347,10 @@ packages: resolution: {integrity: sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==} engines: {node: '>=4'} + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.4.47: resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==} engines: {node: ^10 || ^12 || >=14} @@ -4727,6 +4829,10 @@ packages: stream-combiner2@1.1.1: resolution: {integrity: sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -4807,6 +4913,19 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + suf-log@2.5.3: resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==} @@ -6089,6 +6208,36 @@ snapshots: '@jsdevtools/ono@7.1.3': {} + '@next/env@15.0.3': {} + + '@next/eslint-plugin-next@15.0.3': + dependencies: + fast-glob: 3.3.1 + + '@next/swc-darwin-arm64@15.0.3': + optional: true + + '@next/swc-darwin-x64@15.0.3': + optional: true + + '@next/swc-linux-arm64-gnu@15.0.3': + optional: true + + '@next/swc-linux-arm64-musl@15.0.3': + optional: true + + '@next/swc-linux-x64-gnu@15.0.3': + optional: true + + '@next/swc-linux-x64-musl@15.0.3': + optional: true + + '@next/swc-win32-arm64-msvc@15.0.3': + optional: true + + '@next/swc-win32-x64-msvc@15.0.3': + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6520,6 +6669,12 @@ snapshots: dependencies: type-fest: 2.19.0 + '@swc/counter@0.1.3': {} + + '@swc/helpers@0.5.13': + dependencies: + tslib: 2.8.1 + '@tanstack/eslint-plugin-query@5.62.1(eslint@9.16.0(jiti@2.4.1))(typescript@5.7.2)': dependencies: '@typescript-eslint/utils': 8.17.0(eslint@9.16.0(jiti@2.4.1))(typescript@5.7.2) @@ -7142,6 +7297,10 @@ snapshots: esbuild: 0.24.0 load-tsconfig: 0.2.5 + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + cac@6.7.14: {} call-bind@1.0.7: @@ -7234,6 +7393,8 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 + client-only@0.0.1: {} + cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -8112,6 +8273,14 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-glob@3.3.1: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -9418,6 +9587,32 @@ snapshots: nerf-dart@1.0.0: {} + next@15.0.3(@babel/core@7.26.0)(@playwright/test@1.49.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 15.0.3 + '@swc/counter': 0.1.3 + '@swc/helpers': 0.5.13 + busboy: 1.6.0 + caniuse-lite: 1.0.30001677 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.6(@babel/core@7.26.0)(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 15.0.3 + '@next/swc-darwin-x64': 15.0.3 + '@next/swc-linux-arm64-gnu': 15.0.3 + '@next/swc-linux-arm64-musl': 15.0.3 + '@next/swc-linux-x64-gnu': 15.0.3 + '@next/swc-linux-x64-musl': 15.0.3 + '@next/swc-win32-arm64-msvc': 15.0.3 + '@next/swc-win32-x64-msvc': 15.0.3 + '@playwright/test': 1.49.0 + sharp: 0.33.5 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + nlcst-to-string@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -9739,6 +9934,12 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss@8.4.31: + dependencies: + nanoid: 3.3.7 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.4.47: dependencies: nanoid: 3.3.7 @@ -10330,6 +10531,8 @@ snapshots: duplexer2: 0.1.4 readable-stream: 2.3.8 + streamsearch@1.1.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -10428,6 +10631,13 @@ snapshots: strip-json-comments@3.1.1: {} + styled-jsx@5.1.6(@babel/core@7.26.0)(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + optionalDependencies: + '@babel/core': 7.26.0 + suf-log@2.5.3: dependencies: s.color: 0.0.15 diff --git a/src/configs/nextjs.ts b/src/configs/nextjs.ts new file mode 100644 index 0000000..5eb48c7 --- /dev/null +++ b/src/configs/nextjs.ts @@ -0,0 +1,18 @@ +import { GLOB_NEXTJS } from "../constants"; +import { nextjsRules } from "../rules/nextjs"; +import { interopDefault } from "../utils/interop-default"; + +export const nextjsConfig = async () => { + const nextjsPlugin = await interopDefault(import("@next/eslint-plugin-next")); + + return [ + { + files: GLOB_NEXTJS, + name: "jimmy.codes/nextjs", + plugins: { + "@next/next": nextjsPlugin, + }, + rules: await nextjsRules(), + }, + ]; +}; diff --git a/src/constants.ts b/src/constants.ts index 9d269a0..3a2d77a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -70,6 +70,8 @@ export const GLOB_E2E = [ `**/cypress/**/*.test.${GLOB_SRC_EXT}`, ]; +export const GLOB_NEXTJS = [GLOB_JS, GLOB_JSX, GLOB_TS, GLOB_TSX]; + export const GLOB_CJS = "**/*.cjs"; export const GLOB_ASTRO = "**/*.astro"; diff --git a/src/factory.spec.ts b/src/factory.spec.ts index d15aee1..fc91b31 100644 --- a/src/factory.spec.ts +++ b/src/factory.spec.ts @@ -198,6 +198,15 @@ describe("eslintConfig", () => { expect(configs.at(7)?.name).toBe("jimmy.codes/storybook/setup"); }); + it("should create configuration w/ nextjs", async () => { + const configs = await eslintConfig({ + autoDetect: false, + nextjs: true, + }); + + expect(configs.at(7)?.name).toBe("jimmy.codes/nextjs"); + }); + describe("autoDetect", () => { it("should include typescript when auto detection is enabled", async () => { vi.mocked(isPackageExists).mockImplementation((name) => { @@ -351,5 +360,17 @@ describe("eslintConfig", () => { expect(configs.at(7)?.name).toBe("jimmy.codes/storybook/setup"); }); + + it("should include nextjs when auto detection is enabled", async () => { + vi.mocked(isPackageExists).mockImplementation((name) => { + return name === "next"; + }); + + const configs = await eslintConfig({ + autoDetect: true, + }); + + expect(configs.at(7)?.name).toBe("jimmy.codes/nextjs"); + }); }); }); diff --git a/src/factory.ts b/src/factory.ts index 7e52ab8..2903151 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -8,6 +8,7 @@ import { eslintCommentsConfig } from "./configs/eslint-comments"; import { ignoresConfig } from "./configs/ignores"; import { importsConfig } from "./configs/imports"; import { javascriptConfig } from "./configs/javascript"; +import { nextjsConfig } from "./configs/nextjs"; import { nodeConfig } from "./configs/node"; import { perfectionistConfig } from "./configs/perfectionist"; import { playwrightConfig } from "./configs/playwright"; @@ -27,6 +28,7 @@ import { } from "./utils/get-options"; import { hasAstro, + hasNext, hasPlaywright, hasReact, hasStorybook, @@ -45,6 +47,7 @@ export const eslintConfig = async ( configs = [], ignores = [], jest = false, + nextjs = false, overrides = [], playwright = false, react = false, @@ -81,6 +84,7 @@ export const eslintConfig = async ( ); const isPlaywrightEnabled = playwright || (autoDetect && hasPlaywright()); const isStorybookEnabled = storybook || (autoDetect && hasStorybook()); + const isNextjsEnabled = nextjs || (autoDetect && hasNext()); return [ javascriptConfig(), @@ -98,6 +102,7 @@ export const eslintConfig = async ( isTestingLibraryEnabled ? await testingLibraryConfig() : [], isPlaywrightEnabled ? await playwrightConfig() : [], isStorybookEnabled ? await storybookConfig() : [], + isNextjsEnabled ? await nextjsConfig() : [], prettierConfig(), commonjsConfig(), ignoresConfig(ignores), diff --git a/src/rules.gen.d.ts b/src/rules.gen.d.ts index bd9d474..c74909d 100644 --- a/src/rules.gen.d.ts +++ b/src/rules.gen.d.ts @@ -48,6 +48,110 @@ export interface RuleOptions { * @see https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/require-description.html */ '@eslint-community/eslint-comments/require-description'?: Linter.RuleEntry + /** + * Enforce font-display behavior with Google Fonts. + * @see https://nextjs.org/docs/messages/google-font-display + */ + '@next/next/google-font-display'?: Linter.RuleEntry<[]> + /** + * Ensure `preconnect` is used with Google Fonts. + * @see https://nextjs.org/docs/messages/google-font-preconnect + */ + '@next/next/google-font-preconnect'?: Linter.RuleEntry<[]> + /** + * Enforce `id` attribute on `next/script` components with inline content. + * @see https://nextjs.org/docs/messages/inline-script-id + */ + '@next/next/inline-script-id'?: Linter.RuleEntry<[]> + /** + * Prefer `next/script` component when using the inline script for Google Analytics. + * @see https://nextjs.org/docs/messages/next-script-for-ga + */ + '@next/next/next-script-for-ga'?: Linter.RuleEntry<[]> + /** + * Prevent assignment to the `module` variable. + * @see https://nextjs.org/docs/messages/no-assign-module-variable + */ + '@next/next/no-assign-module-variable'?: Linter.RuleEntry<[]> + /** + * Prevent client components from being async functions. + * @see https://nextjs.org/docs/messages/no-async-client-component + */ + '@next/next/no-async-client-component'?: Linter.RuleEntry<[]> + /** + * Prevent usage of `next/script`'s `beforeInteractive` strategy outside of `pages/_document.js`. + * @see https://nextjs.org/docs/messages/no-before-interactive-script-outside-document + */ + '@next/next/no-before-interactive-script-outside-document'?: Linter.RuleEntry<[]> + /** + * Prevent manual stylesheet tags. + * @see https://nextjs.org/docs/messages/no-css-tags + */ + '@next/next/no-css-tags'?: Linter.RuleEntry<[]> + /** + * Prevent importing `next/document` outside of `pages/_document.js`. + * @see https://nextjs.org/docs/messages/no-document-import-in-page + */ + '@next/next/no-document-import-in-page'?: Linter.RuleEntry<[]> + /** + * Prevent duplicate usage of `` in `pages/_document.js`. + * @see https://nextjs.org/docs/messages/no-duplicate-head + */ + '@next/next/no-duplicate-head'?: Linter.RuleEntry<[]> + /** + * Prevent usage of `` element. + * @see https://nextjs.org/docs/messages/no-head-element + */ + '@next/next/no-head-element'?: Linter.RuleEntry<[]> + /** + * Prevent usage of `next/head` in `pages/_document.js`. + * @see https://nextjs.org/docs/messages/no-head-import-in-document + */ + '@next/next/no-head-import-in-document'?: Linter.RuleEntry<[]> + /** + * Prevent usage of `` elements to navigate to internal Next.js pages. + * @see https://nextjs.org/docs/messages/no-html-link-for-pages + */ + '@next/next/no-html-link-for-pages'?: Linter.RuleEntry + /** + * Prevent usage of `` element due to slower LCP and higher bandwidth. + * @see https://nextjs.org/docs/messages/no-img-element + */ + '@next/next/no-img-element'?: Linter.RuleEntry<[]> + /** + * Prevent page-only custom fonts. + * @see https://nextjs.org/docs/messages/no-page-custom-font + */ + '@next/next/no-page-custom-font'?: Linter.RuleEntry<[]> + /** + * Prevent usage of `next/script` in `next/head` component. + * @see https://nextjs.org/docs/messages/no-script-component-in-head + */ + '@next/next/no-script-component-in-head'?: Linter.RuleEntry<[]> + /** + * Prevent usage of `styled-jsx` in `pages/_document.js`. + * @see https://nextjs.org/docs/messages/no-styled-jsx-in-document + */ + '@next/next/no-styled-jsx-in-document'?: Linter.RuleEntry<[]> + /** + * Prevent synchronous scripts. + * @see https://nextjs.org/docs/messages/no-sync-scripts + */ + '@next/next/no-sync-scripts'?: Linter.RuleEntry<[]> + /** + * Prevent usage of `` with `Head` component from `next/document`. + * @see https://nextjs.org/docs/messages/no-title-in-document-head + */ + '@next/next/no-title-in-document-head'?: Linter.RuleEntry<[]> + /** + * Prevent common typos in Next.js data fetching functions. + */ + '@next/next/no-typos'?: Linter.RuleEntry<[]> + /** + * Prevent duplicate polyfills from Polyfill.io. + * @see https://nextjs.org/docs/messages/no-unwanted-polyfillio + */ + '@next/next/no-unwanted-polyfillio'?: Linter.RuleEntry<[]> /** * Exhaustive deps rule for useQuery * @see https://tanstack.com/query/latest/docs/eslint/exhaustive-deps @@ -5732,6 +5836,8 @@ type EslintCommunityEslintCommentsNoUse = []|[{ type EslintCommunityEslintCommentsRequireDescription = []|[{ ignore?: ("eslint" | "eslint-disable" | "eslint-disable-line" | "eslint-disable-next-line" | "eslint-enable" | "eslint-env" | "exported" | "global" | "globals")[] }] +// ----- @next/next/no-html-link-for-pages ----- +type NextNextNoHtmlLinkForPages = []|[(string | string[])] // ----- @typescript-eslint/array-type ----- type TypescriptEslintArrayType = []|[{ diff --git a/src/rules/__snapshots__/nextjs.spec.ts.snap b/src/rules/__snapshots__/nextjs.spec.ts.snap new file mode 100644 index 0000000..641d99e --- /dev/null +++ b/src/rules/__snapshots__/nextjs.spec.ts.snap @@ -0,0 +1,8 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`should create nextjs rules 1`] = ` +{ + "@next/next/no-html-link-for-pages": "error", + "@next/next/no-sync-scripts": "error", +} +`; diff --git a/src/rules/nextjs.spec.ts b/src/rules/nextjs.spec.ts new file mode 100644 index 0000000..bc5f1ef --- /dev/null +++ b/src/rules/nextjs.spec.ts @@ -0,0 +1,5 @@ +import { nextjsRules } from "./nextjs"; + +test("should create nextjs rules", async () => { + await expect(nextjsRules()).resolves.toMatchSnapshot(); +}); diff --git a/src/rules/nextjs.ts b/src/rules/nextjs.ts new file mode 100644 index 0000000..d979436 --- /dev/null +++ b/src/rules/nextjs.ts @@ -0,0 +1,11 @@ +import type { Rules } from "../types"; + +import { interopDefault } from "../utils/interop-default"; + +export const nextjsRules = async () => { + const nextjsPlugin = await interopDefault(import("@next/eslint-plugin-next")); + + return { + ...nextjsPlugin.configs["core-web-vitals"].rules, + } satisfies Rules; +}; diff --git a/src/rules/react.ts b/src/rules/react.ts index 41bea53..ed87e14 100644 --- a/src/rules/react.ts +++ b/src/rules/react.ts @@ -25,7 +25,7 @@ export const reactRules = async () => { interopDefault(import("eslint-plugin-react")), interopDefault(import("eslint-plugin-jsx-a11y")), ]); - const isUsingNext = hasNext(); + const isUsingNextjs = hasNext(); const isUsingVite = hasVite(); return { @@ -39,7 +39,7 @@ export const reactRules = async () => { "warn", { allowConstantExport: isUsingVite, - allowExportNames: isUsingNext ? nextAllowedExportNames : [], + allowExportNames: isUsingNextjs ? nextAllowedExportNames : [], }, ], "react/boolean-prop-naming": "off", // revisit diff --git a/src/stubs.d.ts b/src/stubs.d.ts index 2b42b9d..83eb747 100644 --- a/src/stubs.d.ts +++ b/src/stubs.d.ts @@ -53,3 +53,14 @@ declare module "eslint-plugin-react-compiler" { export default plugin; } + +declare module "@next/eslint-plugin-next" { + import type { ESLint, Linter } from "eslint"; + + const recommended: Linter.Config; + const coreWebVitals: Linter.Config; + const plugin: ESLint.Plugin; + + export = { configs: { "core-web-vitals": coreWebVitals, recommended } }; + export default plugin; +} diff --git a/src/types.ts b/src/types.ts index 006ca9b..d634018 100644 --- a/src/types.ts +++ b/src/types.ts @@ -87,6 +87,11 @@ export interface Options { * @default false */ jest?: boolean; + /** + * Are Next.js rules enabled? + * @default false + */ + nextjs?: boolean; /** * Additional configs to either extend or overrides configurations * @default []