diff --git a/packages/eslint-config-next/index.js b/packages/eslint-config-next/index.js index e5f39af71a40..5c846dee12af 100644 --- a/packages/eslint-config-next/index.js +++ b/packages/eslint-config-next/index.js @@ -4,6 +4,51 @@ * * https://www.npmjs.com/package/@rushstack/eslint-patch */ +const keptPaths = [] +const sortedPaths = [] +const cwd = process.cwd().replace(/\\/g, '/') +const originalPaths = require.resolve.paths('eslint-plugin-import') + +// eslint throws a conflict error when plugins resolve to different +// locations, since we want to lock our dependencies by default +// but also need to allow using user dependencies this updates +// our resolve paths to first check the cwd and iterate to +// eslint-config-next's dependencies if needed + +for (let i = originalPaths.length - 1; i >= 0; i--) { + const currentPath = originalPaths[i] + + if (currentPath.replace(/\\/g, '/').startsWith(cwd)) { + sortedPaths.push(currentPath) + } else { + keptPaths.unshift(currentPath) + } +} + +// maintain order of node_modules outside of cwd +sortedPaths.push(...keptPaths) + +const hookPropertyMap = new Map( + [ + ['eslint-plugin-import', 'eslint-plugin-import'], + ['eslint-plugin-react', 'eslint-plugin-react'], + ['eslint-plugin-jsx-a11y', 'eslint-plugin-jsx-a11y'], + ].map(([request, replacement]) => [ + request, + require.resolve(replacement, { paths: sortedPaths }), + ]) +) + +const mod = require('module') +const resolveFilename = mod._resolveFilename +mod._resolveFilename = function (request, parent, isMain, options) { + const hookResolved = hookPropertyMap.get(request) + if (hookResolved) { + request = hookResolved + } + return resolveFilename.call(mod, request, parent, isMain, options) +} + require('@rushstack/eslint-patch/modern-module-resolution') module.exports = { diff --git a/test/lib/create-next-install.js b/test/lib/create-next-install.js index 466ff311da21..bb3abf8bb49e 100644 --- a/test/lib/create-next-install.js +++ b/test/lib/create-next-install.js @@ -51,7 +51,11 @@ async function createNextInstall( const pkgPaths = await linkPackages(tmpRepoDir) const combinedDependencies = { - ...dependencies, + ...Object.keys(dependencies).reduce((prev, pkg) => { + const pkgPath = pkgPaths.get(pkg) + prev[pkg] = pkgPath || dependencies[pkg] + return prev + }, {}), next: pkgPaths.get('next'), } diff --git a/test/production/eslint-plugin-deps/index.test.ts b/test/production/eslint-plugin-deps/index.test.ts new file mode 100644 index 000000000000..4ff95f1a0231 --- /dev/null +++ b/test/production/eslint-plugin-deps/index.test.ts @@ -0,0 +1,118 @@ +import { createNext } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { renderViaHTTP } from 'next-test-utils' + +describe('eslint plugin deps', () => { + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: { + 'pages/index.tsx': `export default function Page() { + return

hello world

; +} +`, + '.eslintrc': ` +{ + "parser": "@typescript-eslint/parser", + "plugins": ["react", "@typescript-eslint"], + "extends": [ + "eslint:recommended", + "next/core-web-vitals", + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:import/typescript", + "plugin:import/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "prettier" + ], + "env": { + "es2021": true, + "browser": true + }, + "parserOptions": { + "ecmaVersion": 12, + "sourceType": "module", + "project": ["./tsconfig.json"], + "ecmaFeatures": { "jsx": true } + }, + "settings": { + "react": { "version": "detect" }, + "import/resolver": { "typescript": {} } + }, + "rules": { + "no-else-return": "error", + "semi": ["error", "always"], + "no-useless-rename": "error", + "quotes": ["error", "double"], + "eol-last": ["error", "always"], + "no-console": [2, { "allow": ["warn", "error"] }], + "no-multiple-empty-lines": ["error", { "max": 1 }], + "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true, "enforceForJSX": true }], + + "import/named": 0, + "import/order": [ + "error", + { + "warnOnUnassignedImports": true, + "newlines-between": "always", + "groups": ["builtin", "external", "internal", "parent", ["sibling", "index"], "object", "type"] + } + ], + + "react/display-name": 0, + "react/prop-types": 0, + "react/react-in-jsx-scope": 0, + "react/self-closing-comp": ["error", { "component": true }], + + "react-hooks/exhaustive-deps": ["warn", { "additionalHooks": "useIsomorphicLayoutEffect" }], + + "@typescript-eslint/indent": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/no-var-requires": 0, + "@typescript-eslint/no-use-before-define": 0, + "@typescript-eslint/member-delimiter-style": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/explicit-member-accessibility": 0, + "@typescript-eslint/no-unused-vars": [2, { "argsIgnorePattern": "^_" }], + "@typescript-eslint/array-type": ["error", { "default": "array-simple" }], + "@typescript-eslint/no-unnecessary-boolean-literal-compare": [ + "error", + { "allowComparingNullableBooleansToTrue": false } + ] + } +} + `, + }, + dependencies: { + '@typescript-eslint/eslint-plugin': '^5.16.0', + '@typescript-eslint/parser': '^5.16.0', + 'eslint-config-prettier': '^8.5.0', + 'eslint-plugin-import': '^2.25.4', + 'eslint-plugin-react': '^7.29.4', + next: '12.1.1', + react: '17.0.2', + 'react-dom': '17.0.2', + '@types/node': '17.0.23', + '@types/react': '17.0.43', + '@types/react-dom': '17.0.14', + eslint: '^8.12.0', + 'eslint-config-next': '^12.1.1', + typescript: '4.6.3', + }, + packageJson: { + scripts: { + build: 'next build --no-lint && next lint', + }, + }, + buildCommand: 'yarn build', + }) + }) + afterAll(() => next.destroy()) + + it('should work', async () => { + const html = await renderViaHTTP(next.url, '/') + expect(html).toContain('hello world') + }) +})