From 442807bc98b779a88a1db60f270f7dc964caf854 Mon Sep 17 00:00:00 2001 From: Sebastian Silbermann Date: Tue, 25 Jun 2024 16:13:56 +0200 Subject: [PATCH] Test lint config after first-time setup (#67146) --- .../eslint/test/next-build.test.js | 17 - .../first-time-setup-ts/pages/index.tsx | 7 + .../eslint/first-time-setup/pages/index.js | 7 + .../next-build-and-lint.test.ts.snap | 402 ++++++++++++++++++ .../eslint/test/next-build-and-lint.test.ts | 164 +++++++ 5 files changed, 580 insertions(+), 17 deletions(-) create mode 100644 test/production/eslint/first-time-setup-ts/pages/index.tsx create mode 100644 test/production/eslint/first-time-setup/pages/index.js create mode 100644 test/production/eslint/test/__snapshots__/next-build-and-lint.test.ts.snap create mode 100644 test/production/eslint/test/next-build-and-lint.test.ts diff --git a/test/integration/eslint/test/next-build.test.js b/test/integration/eslint/test/next-build.test.js index be61a27cd16a1..1b3ea76b2abc6 100644 --- a/test/integration/eslint/test/next-build.test.js +++ b/test/integration/eslint/test/next-build.test.js @@ -4,7 +4,6 @@ import { join } from 'path' import { nextBuild } from 'next-test-utils' -const dirFirstTimeSetup = join(__dirname, '../first-time-setup') const dirCustomConfig = join(__dirname, '../custom-config') const dirIgnoreDuringBuilds = join(__dirname, '../ignore-during-builds') const dirBaseDirectories = join(__dirname, '../base-directories') @@ -23,22 +22,6 @@ describe('Next Build', () => { ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( 'production mode', () => { - test('first time setup', async () => { - const eslintrcJson = join(dirFirstTimeSetup, '.eslintrc.json') - await fs.writeFile(eslintrcJson, '') - - const { stdout, stderr } = await nextBuild(dirFirstTimeSetup, [], { - stdout: true, - stderr: true, - lint: true, - }) - const output = stdout + stderr - - expect(output).toContain( - 'No ESLint configuration detected. Run next lint to begin setup' - ) - }) - test('shows warnings and errors', async () => { const { stdout, stderr } = await nextBuild(dirCustomConfig, [], { stdout: true, diff --git a/test/production/eslint/first-time-setup-ts/pages/index.tsx b/test/production/eslint/first-time-setup-ts/pages/index.tsx new file mode 100644 index 0000000000000..b81759920c744 --- /dev/null +++ b/test/production/eslint/first-time-setup-ts/pages/index.tsx @@ -0,0 +1,7 @@ +export default function Test() { + return ( +
+

Hello title

+
+ ) +} diff --git a/test/production/eslint/first-time-setup/pages/index.js b/test/production/eslint/first-time-setup/pages/index.js new file mode 100644 index 0000000000000..b81759920c744 --- /dev/null +++ b/test/production/eslint/first-time-setup/pages/index.js @@ -0,0 +1,7 @@ +export default function Test() { + return ( +
+

Hello title

+
+ ) +} diff --git a/test/production/eslint/test/__snapshots__/next-build-and-lint.test.ts.snap b/test/production/eslint/test/__snapshots__/next-build-and-lint.test.ts.snap new file mode 100644 index 0000000000000..c2daff29b4f3a --- /dev/null +++ b/test/production/eslint/test/__snapshots__/next-build-and-lint.test.ts.snap @@ -0,0 +1,402 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Next Build production mode first time setup 1`] = ` +{ + "env": { + "browser": true, + "node": true, + }, + "globals": {}, + "ignorePatterns": [], + "parserOptions": { + "allowImportExportEverywhere": true, + "babelOptions": { + "caller": { + "supportsTopLevelAwait": true, + }, + "presets": [ + "next/babel", + ], + }, + "ecmaFeatures": { + "jsx": true, + }, + "requireConfigFile": false, + "sourceType": "module", + }, + "plugins": [ + "react-hooks", + "jsx-a11y", + "react", + "import", + "@next/next", + ], + "rules": { + "@next/next/google-font-display": [ + "warn", + ], + "@next/next/google-font-preconnect": [ + "warn", + ], + "@next/next/inline-script-id": [ + "error", + ], + "@next/next/next-script-for-ga": [ + "warn", + ], + "@next/next/no-assign-module-variable": [ + "error", + ], + "@next/next/no-async-client-component": [ + "warn", + ], + "@next/next/no-before-interactive-script-outside-document": [ + "warn", + ], + "@next/next/no-css-tags": [ + "warn", + ], + "@next/next/no-document-import-in-page": [ + "error", + ], + "@next/next/no-duplicate-head": [ + "error", + ], + "@next/next/no-head-element": [ + "warn", + ], + "@next/next/no-head-import-in-document": [ + "error", + ], + "@next/next/no-html-link-for-pages": [ + "error", + ], + "@next/next/no-img-element": [ + "warn", + ], + "@next/next/no-page-custom-font": [ + "warn", + ], + "@next/next/no-script-component-in-head": [ + "error", + ], + "@next/next/no-styled-jsx-in-document": [ + "warn", + ], + "@next/next/no-sync-scripts": [ + "error", + ], + "@next/next/no-title-in-document-head": [ + "warn", + ], + "@next/next/no-typos": [ + "warn", + ], + "@next/next/no-unwanted-polyfillio": [ + "warn", + ], + "import/no-anonymous-default-export": [ + "warn", + ], + "jsx-a11y/alt-text": [ + "warn", + { + "elements": [ + "img", + ], + "img": [ + "Image", + ], + }, + ], + "jsx-a11y/aria-props": [ + "warn", + ], + "jsx-a11y/aria-proptypes": [ + "warn", + ], + "jsx-a11y/aria-unsupported-elements": [ + "warn", + ], + "jsx-a11y/role-has-required-aria-props": [ + "warn", + ], + "jsx-a11y/role-supports-aria-props": [ + "warn", + ], + "react-hooks/exhaustive-deps": [ + "warn", + ], + "react-hooks/rules-of-hooks": [ + "error", + ], + "react/display-name": [ + 2, + ], + "react/jsx-key": [ + 2, + ], + "react/jsx-no-comment-textnodes": [ + 2, + ], + "react/jsx-no-duplicate-props": [ + 2, + ], + "react/jsx-no-target-blank": [ + "off", + ], + "react/jsx-no-undef": [ + 2, + ], + "react/jsx-uses-react": [ + 2, + ], + "react/jsx-uses-vars": [ + 2, + ], + "react/no-children-prop": [ + 2, + ], + "react/no-danger-with-children": [ + 2, + ], + "react/no-deprecated": [ + 2, + ], + "react/no-direct-mutation-state": [ + 2, + ], + "react/no-find-dom-node": [ + 2, + ], + "react/no-is-mounted": [ + 2, + ], + "react/no-render-return-value": [ + 2, + ], + "react/no-string-refs": [ + 2, + ], + "react/no-unescaped-entities": [ + 2, + ], + "react/no-unknown-property": [ + "off", + ], + "react/no-unsafe": [ + 0, + ], + "react/prop-types": [ + "off", + ], + "react/react-in-jsx-scope": [ + "off", + ], + "react/require-render-return": [ + 2, + ], + }, +} +`; + +exports[`Next Build production mode first time setup with TypeScript 1`] = ` +{ + "env": { + "browser": true, + "node": true, + }, + "globals": {}, + "ignorePatterns": [], + "parserOptions": { + "allowImportExportEverywhere": true, + "babelOptions": { + "caller": { + "supportsTopLevelAwait": true, + }, + "presets": [ + "next/babel", + ], + }, + "ecmaFeatures": { + "jsx": true, + }, + "requireConfigFile": false, + "sourceType": "module", + "warnOnUnsupportedTypeScriptVersion": true, + }, + "plugins": [ + "react-hooks", + "jsx-a11y", + "react", + "import", + "@next/next", + ], + "rules": { + "@next/next/google-font-display": [ + "warn", + ], + "@next/next/google-font-preconnect": [ + "warn", + ], + "@next/next/inline-script-id": [ + "error", + ], + "@next/next/next-script-for-ga": [ + "warn", + ], + "@next/next/no-assign-module-variable": [ + "error", + ], + "@next/next/no-async-client-component": [ + "warn", + ], + "@next/next/no-before-interactive-script-outside-document": [ + "warn", + ], + "@next/next/no-css-tags": [ + "warn", + ], + "@next/next/no-document-import-in-page": [ + "error", + ], + "@next/next/no-duplicate-head": [ + "error", + ], + "@next/next/no-head-element": [ + "warn", + ], + "@next/next/no-head-import-in-document": [ + "error", + ], + "@next/next/no-html-link-for-pages": [ + "error", + ], + "@next/next/no-img-element": [ + "warn", + ], + "@next/next/no-page-custom-font": [ + "warn", + ], + "@next/next/no-script-component-in-head": [ + "error", + ], + "@next/next/no-styled-jsx-in-document": [ + "warn", + ], + "@next/next/no-sync-scripts": [ + "error", + ], + "@next/next/no-title-in-document-head": [ + "warn", + ], + "@next/next/no-typos": [ + "warn", + ], + "@next/next/no-unwanted-polyfillio": [ + "warn", + ], + "import/no-anonymous-default-export": [ + "warn", + ], + "jsx-a11y/alt-text": [ + "warn", + { + "elements": [ + "img", + ], + "img": [ + "Image", + ], + }, + ], + "jsx-a11y/aria-props": [ + "warn", + ], + "jsx-a11y/aria-proptypes": [ + "warn", + ], + "jsx-a11y/aria-unsupported-elements": [ + "warn", + ], + "jsx-a11y/role-has-required-aria-props": [ + "warn", + ], + "jsx-a11y/role-supports-aria-props": [ + "warn", + ], + "react-hooks/exhaustive-deps": [ + "warn", + ], + "react-hooks/rules-of-hooks": [ + "error", + ], + "react/display-name": [ + 2, + ], + "react/jsx-key": [ + 2, + ], + "react/jsx-no-comment-textnodes": [ + 2, + ], + "react/jsx-no-duplicate-props": [ + 2, + ], + "react/jsx-no-target-blank": [ + "off", + ], + "react/jsx-no-undef": [ + 2, + ], + "react/jsx-uses-react": [ + 2, + ], + "react/jsx-uses-vars": [ + 2, + ], + "react/no-children-prop": [ + 2, + ], + "react/no-danger-with-children": [ + 2, + ], + "react/no-deprecated": [ + 2, + ], + "react/no-direct-mutation-state": [ + 2, + ], + "react/no-find-dom-node": [ + 2, + ], + "react/no-is-mounted": [ + 2, + ], + "react/no-render-return-value": [ + 2, + ], + "react/no-string-refs": [ + 2, + ], + "react/no-unescaped-entities": [ + 2, + ], + "react/no-unknown-property": [ + "off", + ], + "react/no-unsafe": [ + 0, + ], + "react/prop-types": [ + "off", + ], + "react/react-in-jsx-scope": [ + "off", + ], + "react/require-render-return": [ + 2, + ], + }, +} +`; diff --git a/test/production/eslint/test/next-build-and-lint.test.ts b/test/production/eslint/test/next-build-and-lint.test.ts new file mode 100644 index 0000000000000..db34abff5002d --- /dev/null +++ b/test/production/eslint/test/next-build-and-lint.test.ts @@ -0,0 +1,164 @@ +import fs from 'fs-extra' + +import { join } from 'path' +import { execSync } from 'child_process' + +import { FileRef, createNext } from 'e2e-utils' + +const dirFirstTimeSetup = join(__dirname, '../first-time-setup') +const dirFirstTimeSetupTS = join(__dirname, '../first-time-setup-ts') + +describe('Next Build', () => { + ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( + 'production mode', + () => { + test('first time setup', async () => { + const next = await createNext({ + files: new FileRef(dirFirstTimeSetup), + skipStart: true, + }) + + try { + const eslintrcJsonPath = join(next.testDir, '.eslintrc.json') + await fs.writeFile(eslintrcJsonPath, '') + + const nextBuildCommand = await next.build() + const buildOutput = nextBuildCommand.cliOutput + expect(buildOutput).toContain( + 'No ESLint configuration detected. Run next lint to begin setup' + ) + + // TODO: Should we exit non-zero here if the config was created? Should we maybe even directly start linting? + expect(() => { + execSync(`pnpm next lint --strict`, { + cwd: next.testDir, + encoding: 'utf8', + }) + }).toThrow('Command failed: pnpm next lint --strict') + + const eslintConfigAfterSetupJSON = await execSync( + `pnpm eslint --print-config pages/index.js`, + { + cwd: next.testDir, + encoding: 'utf8', + } + ) + const { parser, settings, ...eslintConfigAfterSetup } = JSON.parse( + eslintConfigAfterSetupJSON + ) + + expect(eslintConfigAfterSetup).toMatchSnapshot() + expect({ + parser, + settings, + }).toEqual({ + // parser: require.resolve('eslint-config-next') + parser: expect.stringContaining('eslint-config-next'), + settings: { + 'import/parsers': expect.any(Object), + 'import/resolver': expect.any(Object), + react: { + version: 'detect', + }, + }, + }) + expect(Object.entries(settings['import/parsers'])).toEqual([ + [ + // require.resolve('@typescript-eslint/parser') + expect.stringContaining('@typescript-eslint/parser'), + ['.ts', '.mts', '.cts', '.tsx', '.d.ts'], + ], + ]) + expect(Object.entries(settings['import/resolver'])).toEqual([ + [ + // require.resolve('eslint-import-resolver-node') + expect.stringContaining('eslint-import-resolver-node'), + { extensions: ['.js', '.jsx', '.ts', '.tsx'] }, + ], + [ + // require.resolve('eslint-import-resolver-typescript') + expect.stringContaining('eslint-import-resolver-typescript'), + { alwaysTryTypes: true }, + ], + ]) + } finally { + await next.destroy() + } + }) + + test('first time setup with TypeScript', async () => { + const next = await createNext({ + files: new FileRef(dirFirstTimeSetupTS), + skipStart: true, + }) + + try { + const eslintrcJsonPath = join(next.testDir, '.eslintrc.json') + await fs.writeFile(eslintrcJsonPath, '') + + const nextBuildCommand = await next.build() + const buildOutput = nextBuildCommand.cliOutput + expect(buildOutput).toContain( + 'No ESLint configuration detected. Run next lint to begin setup' + ) + + // TODO: Should we exit non-zero here if the config was created? Should we maybe even directly start linting? + expect(() => { + execSync(`pnpm next lint --strict`, { + cwd: next.testDir, + encoding: 'utf8', + }) + }).toThrow('Command failed: pnpm next lint --strict') + + const eslintConfigAfterSetupJSON = await execSync( + `pnpm eslint --print-config pages/index.tsx`, + { + cwd: next.testDir, + encoding: 'utf8', + } + ) + const { parser, settings, ...eslintConfigAfterSetup } = JSON.parse( + eslintConfigAfterSetupJSON + ) + + expect(eslintConfigAfterSetup).toMatchSnapshot() + expect({ + parser, + settings, + }).toEqual({ + // parser: require.resolve('@typescript-eslint/parser') + parser: expect.stringContaining('@typescript-eslint/parser'), + settings: { + 'import/parsers': expect.any(Object), + 'import/resolver': expect.any(Object), + react: { + version: 'detect', + }, + }, + }) + expect(Object.entries(settings['import/parsers'])).toEqual([ + [ + // require.resolve('@typescript-eslint/parser') + expect.stringContaining('@typescript-eslint/parser'), + ['.ts', '.mts', '.cts', '.tsx', '.d.ts'], + ], + ]) + expect(Object.entries(settings['import/resolver'])).toEqual([ + [ + // require.resolve('eslint-import-resolver-node') + expect.stringContaining('eslint-import-resolver-node'), + { extensions: ['.js', '.jsx', '.ts', '.tsx'] }, + ], + [ + // require.resolve('eslint-import-resolver-typescript') + expect.stringContaining('eslint-import-resolver-typescript'), + { alwaysTryTypes: true }, + ], + ]) + } finally { + await next.destroy() + } + }) + } + ) +})