diff --git a/docs/basic-features/eslint.md b/docs/basic-features/eslint.md index d12e2b46d5708..232772aa9d67a 100644 --- a/docs/basic-features/eslint.md +++ b/docs/basic-features/eslint.md @@ -18,63 +18,67 @@ Then run `npm run lint` or `yarn lint`: yarn lint ``` -If you don't already have ESLint configured in your application, you will be guided through the installation of the required packages. +If you don't already have ESLint configured in your application, you will be guided through the installation and configuration process. ```bash yarn lint -# You'll see instructions like these: +# You'll see a prompt like this: # -# Please install eslint and eslint-config-next by running: +# ? How would you like to configure ESLint? # -# yarn add --dev eslint eslint-config-next -# -# ... +# ❯ Base configuration + Core Web Vitals rule-set (recommended) +# Base configuration +# None ``` -If no ESLint configuration is present, Next.js will create an `.eslintrc` file in the root of your project and automatically configure it with the base configuration: +One of the following three options can be selected: -```js -{ - "extends": "next" -} -``` +- **Strict**: Includes Next.js' base ESLint configuration along with a stricter [Core Web Vitals rule-set](/docs/basic-features/eslint.md#core-web-vitals). This is the recommended configuration for developers setting up ESLint for the first time. -You can now run `next lint` every time you want to run ESLint to catch errors. + ```json + { + "extends": "next/core-web-vitals" + } + ``` -> The default base configuration (`"extends": "next"`) can be updated at any time and will only be included if no ESLint configuration is present. +- **Base**: Includes Next.js' base ESLint configuration. -We recommend using an appropriate [integration](https://eslint.org/docs/user-guide/integrations#editors) to view warnings and errors directly in your code editor during development. + ```json + { + "extends": "next" + } + ``` -## Linting During Builds +- **Cancel**: Does not include any ESLint configuration. Only select this option if you plan on setting up your own custom ESLint configuration. -Once ESLint has been set up, it will automatically run during every build (`next build`). Errors will fail the build, while warnings will not. +If either of the two configuration options are selected, Next.js will automatically install `eslint` and `eslint-config-next` as development dependencies in your application and create an `.eslintrc.json` file in the root of your project that includes your selected configuration. -If you do not want ESLint to run as a build step, refer to the documentation for [Ignoring ESLint](/docs/api-reference/next.config.js/ignoring-eslint.md): +You can now run `next lint` every time you want to run ESLint to catch errors. Once ESLint has been set up, it will also automatically run during every build (`next build`). Errors will fail the build, while warnings will not. -## Linting Custom Directories +> If you do not want ESLint to run during `next build`, refer to the documentation for [Ignoring ESLint](/docs/api-reference/next.config.js/ignoring-eslint.md). -By default, Next.js will run ESLint for all files in the `pages/`, `components/`, and `lib/` directories. However, you can specify which directories using the `dirs` option in the `eslint` config in `next.config.js`: +We recommend using an appropriate [integration](https://eslint.org/docs/user-guide/integrations#editors) to view warnings and errors directly in your code editor during development. -```js -module.exports = { - eslint: { - dirs: ['pages', 'utils'], // Only run ESLint on the 'pages' and 'utils' directories (next build and next lint) - }, -} -``` +## ESLint Config -Also, the `--dir` flag can be used for `next lint`: +The default configuration (`eslint-config-next`) includes everything you need to have an optimal out-of-the-box linting experience in Next.js. If you do not have ESLint already configured in your application, we recommend using `next lint` to set up ESLint along with this configuration. -```bash -yarn lint --dir pages --dir utils -``` +> If you would like to use `eslint-config-next` along with other ESLint configurations, refer to the [Additional Configurations](/docs/basic-features/eslint.md#additional-configurations) section to learn how to do so without causing any conflicts. + +Recommended rule-sets from the following ESLint plugins are all used within `eslint-config-next`: + +- [`eslint-plugin-react`](https://www.npmjs.com/package/eslint-plugin-react) +- [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks) +- [`eslint-plugin-next`](https://www.npmjs.com/package/@next/eslint-plugin-next) + +You can see the full details of the shareable configuration in the [`eslint-config-next`](https://www.npmjs.com/package/eslint-config-next) package. This will take precedence over the configuration from `next.config.js`. ## ESLint Plugin -Next.js provides an ESLint plugin, [`eslint-plugin-next`](https://www.npmjs.com/package/@next/eslint-plugin-next), making it easier to catch common issues and problems in a Next.js application. The full set of rules is as follows: +Next.js provides an ESLint plugin, [`eslint-plugin-next`](https://www.npmjs.com/package/@next/eslint-plugin-next), already bundled within the base configuration that makes it possible to catch common issues and problems in a Next.js application. The full set of rules is as follows: | | Rule | Description | | :-: | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | @@ -94,34 +98,36 @@ Next.js provides an ESLint plugin, [`eslint-plugin-next`](https://www.npmjs.com/ - ✔: Enabled in the recommended configuration -## Base Configuration +If you already have ESLint configured in your application, we recommend extending from this plugin directly instead of including `eslint-config-next` unless a few conditions are met. Refer to the [Recommended Plugin Ruleset](/docs/basic-features/eslint.md#recommended-plugin-ruleset) to learn more. -The Next.js base ESLint configuration is automatically generated when `next lint` is run for the first time: +## Linting Custom Directories + +By default, Next.js will run ESLint for all files in the `pages/`, `components/`, and `lib/` directories. However, you can specify which directories using the `dirs` option in the `eslint` config in `next.config.js` for production builds: ```js -{ - "extends": "next" +module.exports = { + eslint: { + dirs: ['pages', 'utils'], // Only run ESLint on the 'pages' and 'utils' directories during production builds (next build) + }, } ``` -This configuration extends recommended rule sets from various ESLint plugins: - -- [`eslint-plugin-react`](https://www.npmjs.com/package/eslint-plugin-react) -- [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks) -- [`eslint-plugin-next`](https://www.npmjs.com/package/@next/eslint-plugin-next) +Similarly, the `--dir` flag can be used for `next lint`: -You can see the full details of the shareable configuration in the [`eslint-config-next`](https://www.npmjs.com/package/eslint-config-next) package. +```bash +next lint --dir pages --dir utils +``` ## Disabling Rules If you would like to modify or disable any rules provided by the supported plugins (`react`, `react-hooks`, `next`), you can directly change them using the `rules` property in your `.eslintrc`: -```js +```json { "extends": "next", "rules": { "react/no-unescaped-entities": "off", - "@next/next/no-page-custom-font": "off", + "@next/next/no-page-custom-font": "off" } } ``` @@ -140,23 +146,23 @@ If you would like to modify or disable any rules provided by the supported plugi ### Core Web Vitals -A stricter `next/core-web-vitals` rule set can also be added in `.eslintrc`: +The `next/core-web-vitals` rule set is enabled when `next lint` is run for the first time and the **strict** option is selected. -``` +```json { - "extends": ["next", "next/core-web-vitals"] + "extends": "next/core-web-vitals" } ``` `next/core-web-vitals` updates `eslint-plugin-next` to error on a number of rules that are warnings by default if they affect [Core Web Vitals](https://web.dev/vitals/). -> Both `next` and `next/core-web-vitals` entry points are automatically included for new applications built with [Create Next App](/docs/api-reference/create-next-app.md). +> The `next/core-web-vitals` entry point is automatically included for new applications built with [Create Next App](/docs/api-reference/create-next-app.md). ## Usage with Prettier ESLint also contains code formatting rules, which can conflict with your existing [Prettier](https://prettier.io/) setup. We recommend including [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) in your ESLint config to make ESLint and Prettier work together. -```js +```json { "extends": ["next", "prettier"] } diff --git a/packages/create-next-app/create-app.ts b/packages/create-next-app/create-app.ts index 3a40c37db2fc2..2badeabb1bf43 100644 --- a/packages/create-next-app/create-app.ts +++ b/packages/create-next-app/create-app.ts @@ -256,7 +256,7 @@ export async function createApp({ rename: (name) => { switch (name) { case 'gitignore': - case 'eslintrc': { + case 'eslintrc.json': { return '.'.concat(name) } // README.md is ignored by webpack-asset-relocator-loader used by ncc: diff --git a/packages/create-next-app/templates/default/eslintrc b/packages/create-next-app/templates/default/eslintrc deleted file mode 100644 index 97a2bb84efb39..0000000000000 --- a/packages/create-next-app/templates/default/eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["next", "next/core-web-vitals"] -} diff --git a/packages/create-next-app/templates/default/eslintrc.json b/packages/create-next-app/templates/default/eslintrc.json new file mode 100644 index 0000000000000..bffb357a71225 --- /dev/null +++ b/packages/create-next-app/templates/default/eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/packages/create-next-app/templates/typescript/eslintrc b/packages/create-next-app/templates/typescript/eslintrc deleted file mode 100644 index 97a2bb84efb39..0000000000000 --- a/packages/create-next-app/templates/typescript/eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["next", "next/core-web-vitals"] -} diff --git a/packages/create-next-app/templates/typescript/eslintrc.json b/packages/create-next-app/templates/typescript/eslintrc.json new file mode 100644 index 0000000000000..bffb357a71225 --- /dev/null +++ b/packages/create-next-app/templates/typescript/eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/packages/next/cli/next-lint.ts b/packages/next/cli/next-lint.ts index d60208d61f46f..987ba5773ba12 100755 --- a/packages/next/cli/next-lint.ts +++ b/packages/next/cli/next-lint.ts @@ -39,6 +39,7 @@ const nextLint: cliCommand = async (argv) => { '--help': Boolean, '--base-dir': String, '--dir': [String], + '--strict': Boolean, // Aliases '-h': '--help', @@ -100,6 +101,9 @@ const nextLint: cliCommand = async (argv) => { --ext [String] Specify JavaScript file extensions - default: .js, .jsx, .ts, .tsx --resolve-plugins-relative-to path::String A folder where plugins should be resolved from, CWD by default + Initial setup: + --strict Creates an .eslintrc.json file using the Next.js strict configuration (only possible if no .eslintrc.json file is present) + Specifying rules: --rulesdir [path::String] Use additional rules from this directory @@ -156,6 +160,7 @@ const nextLint: cliCommand = async (argv) => { const reportErrorsOnly = Boolean(args['--quiet']) const maxWarnings = args['--max-warnings'] ?? -1 const formatter = args['--format'] || null + const strict = Boolean(args['--strict']) runLintCheck( baseDir, @@ -164,7 +169,8 @@ const nextLint: cliCommand = async (argv) => { eslintOptions(args), reportErrorsOnly, maxWarnings, - formatter + formatter, + strict ) .then(async (lintResults) => { const lintOutput = @@ -193,7 +199,7 @@ const nextLint: cliCommand = async (argv) => { if (lintOutput) { console.log(lintOutput) - } else { + } else if (lintResults && !lintOutput) { console.log(chalk.green('✔ No ESLint warnings or errors')) } }) diff --git a/packages/next/compiled/cli-select/LICENSE b/packages/next/compiled/cli-select/LICENSE new file mode 100644 index 0000000000000..5edd219c102bf --- /dev/null +++ b/packages/next/compiled/cli-select/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Cyril Wanner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/next/compiled/cli-select/index.js b/packages/next/compiled/cli-select/index.js new file mode 100644 index 0000000000000..94290eb55015d --- /dev/null +++ b/packages/next/compiled/cli-select/index.js @@ -0,0 +1 @@ +module.exports=(()=>{"use strict";var e={610:(e,t)=>{Object.defineProperty(t,"__esModule",{value:true});t.withPromise=t.withCallback=void 0;const r=(e,t,r)=>{e.open();e.onSelect((e,s)=>r(t(e,s)))};t.withCallback=r;const s=(e,t)=>{return new Promise((r,s)=>{e.open();e.onSelect((e,i)=>{if(e===null){s()}else{r(t(e,i))}})})};t.withPromise=s},960:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:true});t.default=void 0;var s=_interopRequireDefault(r(213));var i=_interopRequireDefault(r(88));var n=r(610);var u=r(797);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}function _objectSpread(e){for(var t=1;te};const a=(e,t)=>{e=_objectSpread({},o,e);const r=new i.default(e,e.outputStream);const a=new s.default(e.inputStream);a.setDefaultValue(e.defaultValue);a.attachRenderer(r);let l;if(Array.isArray(e.values)){l=(0,u.withArrayValues)(e)}else{l=(0,u.withObjectValues)(e)}e.values=l.input;a.setValues(e.values);if(typeof t==="function"){return(0,n.withCallback)(a,l.output,t)}else{return(0,n.withPromise)(a,l.output)}};t=e.exports=a;Object.defineProperty(t,"__esModule",{value:true});var l=a;t.default=l},213:(e,t,r)=>{Object.defineProperty(t,"__esModule",{value:true});t.default=void 0;var s=_interopRequireDefault(r(58));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}class Input{constructor(e=process.stdin){this.stream=e;this.values=[];this.selectedValue=0;this.onSelectListener=(()=>{});this.onKeyPress=this.onKeyPress.bind(this)}setValues(e){this.values=e;if(this.renderer){this.renderer.setValues(e)}}setDefaultValue(e){this.selectedValue=e}attachRenderer(e){this.renderer=e;this.renderer.setValues(this.values)}onSelect(e){this.onSelectListener=e}open(){s.default.emitKeypressEvents(this.stream);this.stream.on("keypress",this.onKeyPress);if(this.renderer){this.renderer.render(this.selectedValue)}this.stream.setRawMode(true);this.stream.resume()}close(e=false){this.stream.setRawMode(false);this.stream.pause();if(this.renderer){this.renderer.cleanup()}if(e){this.onSelectListener(null)}else{this.onSelectListener(this.selectedValue,this.values[this.selectedValue])}this.stream.removeListener("keypress",this.onKeyPress)}render(){if(!this.renderer){return}this.renderer.render(this.selectedValue)}onKeyPress(e,t){if(t){if(t.name==="up"&&this.selectedValue>0){this.selectedValue--;this.render()}else if(t.name==="down"&&this.selectedValue+1{Object.defineProperty(t,"__esModule",{value:true});t.default=void 0;var s=_interopRequireDefault(r(58));var i=r(211);function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}class Renderer{constructor(e,t=process.stdout){this.options=e;this.stream=t;this.values=[];this.initialRender=true}setValues(e){this.values=e}render(e=0){if(this.initialRender){this.initialRender=false;this.stream.write(i.cursorHide)}else{this.stream.write((0,i.eraseLines)(this.values.length))}this.values.forEach((t,r)=>{const s=e===r?this.options.selected:this.options.unselected;const i=" ".repeat(this.options.indentation);const n=this.options.valueRenderer(t,e===r);const u=r!==this.values.length-1?"\n":"";this.stream.write(i+s+" "+n+u)})}cleanup(){this.stream.write((0,i.eraseLines)(this.values.length));this.stream.write(i.cursorShow)}}t.default=Renderer},797:(e,t)=>{Object.defineProperty(t,"__esModule",{value:true});t.withObjectValues=t.withArrayValues=void 0;const r=e=>{return{input:e.values,output:(e,t)=>{return{id:e,value:t}}}};t.withArrayValues=r;const s=e=>{const t=e.values;return{input:Object.values(t),output:(e,r)=>{return{id:Object.keys(t)[e],value:r}}}};t.withObjectValues=s},211:e=>{const t=e.exports;const r="[";const s="]";const i="";const n=";";const u=process.env.TERM_PROGRAM==="Apple_Terminal";t.cursorTo=((e,t)=>{if(typeof e!=="number"){throw new TypeError("The `x` argument is required")}if(typeof t!=="number"){return r+(e+1)+"G"}return r+(t+1)+";"+(e+1)+"H"});t.cursorMove=((e,t)=>{if(typeof e!=="number"){throw new TypeError("The `x` argument is required")}let s="";if(e<0){s+=r+-e+"D"}else if(e>0){s+=r+e+"C"}if(t<0){s+=r+-t+"A"}else if(t>0){s+=r+t+"B"}return s});t.cursorUp=(e=>r+(typeof e==="number"?e:1)+"A");t.cursorDown=(e=>r+(typeof e==="number"?e:1)+"B");t.cursorForward=(e=>r+(typeof e==="number"?e:1)+"C");t.cursorBackward=(e=>r+(typeof e==="number"?e:1)+"D");t.cursorLeft=r+"G";t.cursorSavePosition=r+(u?"7":"s");t.cursorRestorePosition=r+(u?"8":"u");t.cursorGetPosition=r+"6n";t.cursorNextLine=r+"E";t.cursorPrevLine=r+"F";t.cursorHide=r+"?25l";t.cursorShow=r+"?25h";t.eraseLines=(e=>{let r="";for(let s=0;s{return[s,"8",n,n,t,i,e,s,"8",n,n,i].join("")});t.image=((e,t)=>{t=t||{};let r=s+"1337;File=inline=1";if(t.width){r+=`;width=${t.width}`}if(t.height){r+=`;height=${t.height}`}if(t.preserveAspectRatio===false){r+=";preserveAspectRatio=0"}return r+":"+e.toString("base64")+i});t.iTerm={};t.iTerm.setCwd=(e=>s+"50;CurrentDir="+(e||process.cwd())+i)},58:e=>{e.exports=require("readline")}};var t={};function __nccwpck_require__(r){if(t[r]){return t[r].exports}var s=t[r]={exports:{}};var i=true;try{e[r](s,s.exports,__nccwpck_require__);i=false}finally{if(i)delete t[r]}return s.exports}__nccwpck_require__.ab=__dirname+"/";return __nccwpck_require__(960)})(); \ No newline at end of file diff --git a/packages/next/compiled/cli-select/package.json b/packages/next/compiled/cli-select/package.json new file mode 100644 index 0000000000000..1e92552241d57 --- /dev/null +++ b/packages/next/compiled/cli-select/package.json @@ -0,0 +1 @@ +{"name":"cli-select","main":"index.js","author":"Cyril Wanner ","license":"MIT"} diff --git a/packages/next/compiled/cross-spawn/LICENSE b/packages/next/compiled/cross-spawn/LICENSE new file mode 100644 index 0000000000000..8407b9a30f51b --- /dev/null +++ b/packages/next/compiled/cross-spawn/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Made With MOXY Lda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/next/compiled/cross-spawn/index.js b/packages/next/compiled/cross-spawn/index.js new file mode 100644 index 0000000000000..93899d2273ea2 --- /dev/null +++ b/packages/next/compiled/cross-spawn/index.js @@ -0,0 +1 @@ +module.exports=(()=>{var e={875:(e,t,r)=>{"use strict";const n=r(129);const o=r(915);const s=r(985);function spawn(e,t,r){const c=o(e,t,r);const i=n.spawn(c.command,c.args,c.options);s.hookChildProcess(i,c);return i}function spawnSync(e,t,r){const c=o(e,t,r);const i=n.spawnSync(c.command,c.args,c.options);i.error=i.error||s.verifyENOENTSync(i.status,c);return i}e.exports=spawn;e.exports.spawn=spawn;e.exports.sync=spawnSync;e.exports._parse=o;e.exports._enoent=s},985:e=>{"use strict";const t=process.platform==="win32";function notFoundError(e,t){return Object.assign(new Error(`${t} ${e.command} ENOENT`),{code:"ENOENT",errno:"ENOENT",syscall:`${t} ${e.command}`,path:e.command,spawnargs:e.args})}function hookChildProcess(e,r){if(!t){return}const n=e.emit;e.emit=function(t,o){if(t==="exit"){const t=verifyENOENT(o,r,"spawn");if(t){return n.call(e,"error",t)}}return n.apply(e,arguments)}}function verifyENOENT(e,r){if(t&&e===1&&!r.file){return notFoundError(r.original,"spawn")}return null}function verifyENOENTSync(e,r){if(t&&e===1&&!r.file){return notFoundError(r.original,"spawnSync")}return null}e.exports={hookChildProcess:hookChildProcess,verifyENOENT:verifyENOENT,verifyENOENTSync:verifyENOENTSync,notFoundError:notFoundError}},915:(e,t,r)=>{"use strict";const n=r(622);const o=r(458);const s=r(921);const c=r(266);const i=r(217);const a=r(519);const u=process.platform==="win32";const f=/\.(?:com|exe)$/i;const l=/node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;const p=o(()=>a.satisfies(process.version,"^4.8.0 || ^5.7.0 || >= 6.0.0",true))||false;function detectShebang(e){e.file=s(e);const t=e.file&&i(e.file);if(t){e.args.unshift(e.file);e.command=t;return s(e)}return e.file}function parseNonShell(e){if(!u){return e}const t=detectShebang(e);const r=!f.test(t);if(e.options.forceShell||r){const r=l.test(t);e.command=n.normalize(e.command);e.command=c.command(e.command);e.args=e.args.map(e=>c.argument(e,r));const o=[e.command].concat(e.args).join(" ");e.args=["/d","/s","/c",`"${o}"`];e.command=process.env.comspec||"cmd.exe";e.options.windowsVerbatimArguments=true}return e}function parseShell(e){if(p){return e}const t=[e.command].concat(e.args).join(" ");if(u){e.command=typeof e.options.shell==="string"?e.options.shell:process.env.comspec||"cmd.exe";e.args=["/d","/s","/c",`"${t}"`];e.options.windowsVerbatimArguments=true}else{if(typeof e.options.shell==="string"){e.command=e.options.shell}else if(process.platform==="android"){e.command="/system/bin/sh"}else{e.command="/bin/sh"}e.args=["-c",t]}return e}function parse(e,t,r){if(t&&!Array.isArray(t)){r=t;t=null}t=t?t.slice(0):[];r=Object.assign({},r);const n={command:e,args:t,options:r,file:undefined,original:{command:e,args:t}};return r.shell?parseShell(n):parseNonShell(n)}e.exports=parse},266:e=>{"use strict";const t=/([()\][%!^"`<>&|;, *?])/g;function escapeCommand(e){e=e.replace(t,"^$1");return e}function escapeArgument(e,r){e=`${e}`;e=e.replace(/(\\*)"/g,'$1$1\\"');e=e.replace(/(\\*)$/,"$1$1");e=`"${e}"`;e=e.replace(t,"^$1");if(r){e=e.replace(t,"^$1")}return e}e.exports.command=escapeCommand;e.exports.argument=escapeArgument},217:(e,t,r)=>{"use strict";const n=r(747);const o=r(598);function readShebang(e){const t=150;let r;if(Buffer.alloc){r=Buffer.alloc(t)}else{r=new Buffer(t);r.fill(0)}let s;try{s=n.openSync(e,"r");n.readSync(s,r,0,t,0);n.closeSync(s)}catch(e){}return o(r.toString())}e.exports=readShebang},921:(e,t,r)=>{"use strict";const n=r(622);const o=r(753);const s=r(226)();function resolveCommandAttempt(e,t){const r=process.cwd();const c=e.options.cwd!=null;if(c){try{process.chdir(e.options.cwd)}catch(e){}}let i;try{i=o.sync(e.command,{path:(e.options.env||process.env)[s],pathExt:t?n.delimiter:undefined})}catch(e){}finally{process.chdir(r)}if(i){i=n.resolve(c?e.options.cwd:"",i)}return i}function resolveCommand(e){return resolveCommandAttempt(e)||resolveCommandAttempt(e,true)}e.exports=resolveCommand},226:e=>{"use strict";e.exports=(e=>{e=e||{};const t=e.env||process.env;const r=e.platform||process.platform;if(r!=="win32"){return"PATH"}return Object.keys(t).find(e=>e.toUpperCase()==="PATH")||"Path"})},607:(e,t,r)=>{var n=r(747);var o;if(process.platform==="win32"||global.TESTING_WINDOWS){o=r(750)}else{o=r(821)}e.exports=isexe;isexe.sync=sync;function isexe(e,t,r){if(typeof t==="function"){r=t;t={}}if(!r){if(typeof Promise!=="function"){throw new TypeError("callback not provided")}return new Promise(function(r,n){isexe(e,t||{},function(e,t){if(e){n(e)}else{r(t)}})})}o(e,t||{},function(e,n){if(e){if(e.code==="EACCES"||t&&t.ignoreErrors){e=null;n=false}}r(e,n)})}function sync(e,t){try{return o.sync(e,t||{})}catch(e){if(t&&t.ignoreErrors||e.code==="EACCES"){return false}else{throw e}}}},821:(e,t,r)=>{e.exports=isexe;isexe.sync=sync;var n=r(747);function isexe(e,t,r){n.stat(e,function(e,n){r(e,e?false:checkStat(n,t))})}function sync(e,t){return checkStat(n.statSync(e),t)}function checkStat(e,t){return e.isFile()&&checkMode(e,t)}function checkMode(e,t){var r=e.mode;var n=e.uid;var o=e.gid;var s=t.uid!==undefined?t.uid:process.getuid&&process.getuid();var c=t.gid!==undefined?t.gid:process.getgid&&process.getgid();var i=parseInt("100",8);var a=parseInt("010",8);var u=parseInt("001",8);var f=i|a;var l=r&u||r&a&&o===c||r&i&&n===s||r&f&&s===0;return l}},750:(e,t,r)=>{e.exports=isexe;isexe.sync=sync;var n=r(747);function checkPathExt(e,t){var r=t.pathExt!==undefined?t.pathExt:process.env.PATHEXT;if(!r){return true}r=r.split(";");if(r.indexOf("")!==-1){return true}for(var n=0;n{"use strict";e.exports=function(e){try{return e()}catch(e){}}},598:(e,t,r)=>{"use strict";var n=r(829);e.exports=function(e){var t=e.match(n);if(!t){return null}var r=t[0].replace(/#! ?/,"").split(" ");var o=r[0].split("/").pop();var s=r[1];return o==="env"?s:o+(s?" "+s:"")}},829:e=>{"use strict";e.exports=/^#!.*/},753:(e,t,r)=>{e.exports=which;which.sync=whichSync;var n=process.platform==="win32"||process.env.OSTYPE==="cygwin"||process.env.OSTYPE==="msys";var o=r(622);var s=n?";":":";var c=r(607);function getNotFoundError(e){var t=new Error("not found: "+e);t.code="ENOENT";return t}function getPathInfo(e,t){var r=t.colon||s;var o=t.path||process.env.PATH||"";var c=[""];o=o.split(r);var i="";if(n){o.unshift(process.cwd());i=t.pathExt||process.env.PATHEXT||".EXE;.CMD;.BAT;.COM";c=i.split(r);if(e.indexOf(".")!==-1&&c[0]!=="")c.unshift("")}if(e.match(/\//)||n&&e.match(/\\/))o=[""];return{env:o,ext:c,extExe:i}}function which(e,t,r){if(typeof t==="function"){r=t;t={}}var n=getPathInfo(e,t);var s=n.env;var i=n.ext;var a=n.extExe;var u=[];(function F(n,f){if(n===f){if(t.all&&u.length)return r(null,u);else return r(getNotFoundError(e))}var l=s[n];if(l.charAt(0)==='"'&&l.slice(-1)==='"')l=l.slice(1,-1);var p=o.join(l,e);if(!l&&/^\.[\\\/]/.test(e)){p=e.slice(0,2)+p}(function E(e,o){if(e===o)return F(n+1,f);var s=i[e];c(p+s,{pathExt:a},function(n,c){if(!n&&c){if(t.all)u.push(p+s);else return r(null,p+s)}return E(e+1,o)})})(0,i.length)})(0,s.length)}function whichSync(e,t){t=t||{};var r=getPathInfo(e,t);var n=r.env;var s=r.ext;var i=r.extExe;var a=[];for(var u=0,f=n.length;u{"use strict";e.exports=require("child_process")},747:e=>{"use strict";e.exports=require("fs")},519:e=>{"use strict";e.exports=require("next/dist/compiled/semver")},622:e=>{"use strict";e.exports=require("path")}};var t={};function __nccwpck_require__(r){if(t[r]){return t[r].exports}var n=t[r]={exports:{}};var o=true;try{e[r](n,n.exports,__nccwpck_require__);o=false}finally{if(o)delete t[r]}return n.exports}__nccwpck_require__.ab=__dirname+"/";return __nccwpck_require__(875)})(); \ No newline at end of file diff --git a/packages/next/compiled/cross-spawn/package.json b/packages/next/compiled/cross-spawn/package.json new file mode 100644 index 0000000000000..c461cdd36fb8a --- /dev/null +++ b/packages/next/compiled/cross-spawn/package.json @@ -0,0 +1 @@ +{"name":"cross-spawn","main":"index.js","author":"André Cruz ","license":"MIT"} diff --git a/packages/next/lib/constants.ts b/packages/next/lib/constants.ts index 8c56f65d5ceca..96b0df3fd4f8d 100644 --- a/packages/next/lib/constants.ts +++ b/packages/next/lib/constants.ts @@ -57,3 +57,23 @@ export const ESLINT_DEFAULT_DIRS = [ 'src/components', 'src/lib', ] + +export const ESLINT_PROMPT_VALUES = [ + { + title: 'Strict', + recommended: true, + config: { + extends: 'next/core-web-vitals', + }, + }, + { + title: 'Base', + config: { + extends: 'next', + }, + }, + { + title: 'Cancel', + config: null, + }, +] diff --git a/packages/next/lib/eslint/customFormatter.ts b/packages/next/lib/eslint/customFormatter.ts index 20eb7f111d958..6583f16e4f469 100644 --- a/packages/next/lib/eslint/customFormatter.ts +++ b/packages/next/lib/eslint/customFormatter.ts @@ -126,9 +126,9 @@ export function formatResults( output: resultsWithMessages.length > 0 ? output + - `\n\n${chalk.bold( - 'Need to disable some ESLint rules? Learn more here:' - )} https://nextjs.org/docs/basic-features/eslint#disabling-rules\n` + `\n\n${chalk.cyan( + 'info' + )} - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/basic-features/eslint#disabling-rules` : '', totalNextPluginErrorCount, totalNextPluginWarningCount, diff --git a/packages/next/lib/eslint/hasEslintConfiguration.ts b/packages/next/lib/eslint/hasEslintConfiguration.ts new file mode 100644 index 0000000000000..2b3418f7966b1 --- /dev/null +++ b/packages/next/lib/eslint/hasEslintConfiguration.ts @@ -0,0 +1,46 @@ +import { promises as fs } from 'fs' + +export type ConfigAvailable = { + exists: boolean + emptyEslintrc?: boolean + emptyPkgJsonConfig?: boolean + firstTimeSetup?: true +} + +export async function hasEslintConfiguration( + eslintrcFile: string | null, + packageJsonConfig: { eslintConfig: any } | null +): Promise { + const configObject = { + exists: false, + emptyEslintrc: false, + emptyPkgJsonConfig: false, + } + + if (eslintrcFile) { + const content = await fs.readFile(eslintrcFile, { encoding: 'utf8' }).then( + (txt) => txt.trim().replace(/\n/g, ''), + () => null + ) + + if ( + content === '' || + content === '{}' || + content === '---' || + content === 'module.exports = {}' + ) { + return { ...configObject, emptyEslintrc: true } + } + } else if (packageJsonConfig?.eslintConfig) { + if (Object.entries(packageJsonConfig?.eslintConfig).length === 0) { + return { + ...configObject, + emptyPkgJsonConfig: true, + } + } + } else { + return configObject + } + + return { ...configObject, exists: true } +} diff --git a/packages/next/lib/eslint/runLintCheck.ts b/packages/next/lib/eslint/runLintCheck.ts index 2e8bb08bfa051..0768f36cc12c3 100644 --- a/packages/next/lib/eslint/runLintCheck.ts +++ b/packages/next/lib/eslint/runLintCheck.ts @@ -8,11 +8,13 @@ import * as CommentJson from 'next/dist/compiled/comment-json' import { LintResult, formatResults } from './customFormatter' import { writeDefaultConfig } from './writeDefaultConfig' +import { hasEslintConfiguration } from './hasEslintConfiguration' + +import { ESLINT_PROMPT_VALUES } from '../constants' import { existsSync, findPagesDir } from '../find-pages-dir' -import { - hasNecessaryDependencies, - NecessaryDependencies, -} from '../has-necessary-dependencies' +import { installDependencies } from '../install-dependencies' +import { hasNecessaryDependencies } from '../has-necessary-dependencies' +import { isYarn } from '../is-yarn' import * as Log from '../../build/output/log' import { EventLintCheckCompleted } from '../../telemetry/events/build' @@ -22,12 +24,50 @@ type Config = { rules: { [key: string]: Array } } +const requiredPackages = [ + { file: 'eslint/lib/api.js', pkg: 'eslint' }, + { file: 'eslint-config-next', pkg: 'eslint-config-next' }, +] + +async function cliPrompt() { + console.log( + chalk.bold( + `${chalk.cyan( + '?' + )} How would you like to configure ESLint? https://nextjs.org/docs/basic-features/eslint` + ) + ) + + try { + const cliSelect = (await import('next/dist/compiled/cli-select')).default + const { value } = await cliSelect({ + values: ESLINT_PROMPT_VALUES, + valueRenderer: ( + { + title, + recommended, + }: { title: string; recommended?: boolean; config: any }, + selected: boolean + ) => { + const name = selected ? chalk.bold.underline.cyan(title) : title + return name + (recommended ? chalk.bold.yellow(' (recommended)') : '') + }, + selected: chalk.cyan('❯ '), + unselected: ' ', + }) + + return { config: value?.config } + } catch { + return { config: null } + } +} + async function lint( - deps: NecessaryDependencies, baseDir: string, lintDirs: string[], eslintrcFile: string | null, pkgJsonPath: string | null, + lintDuringBuild: boolean = false, eslintOptions: any = null, reportErrorsOnly: boolean = false, maxWarnings: number = -1, @@ -41,14 +81,27 @@ async function lint( eventInfo: EventLintCheckCompleted } > { - // Load ESLint after we're sure it exists: - const mod = await import(deps.resolved.get('eslint')!) + try { + // Load ESLint after we're sure it exists: + const deps = await hasNecessaryDependencies(baseDir, requiredPackages) - const { ESLint } = mod - let eslintVersion = ESLint?.version + if (deps.missing.some((dep) => dep.pkg === 'eslint')) { + Log.error( + `ESLint must be installed${ + lintDuringBuild ? ' in order to run during builds:' : ':' + } ${chalk.bold.cyan( + isYarn(baseDir) + ? 'yarn add --dev eslint' + : 'npm install --save-dev eslint' + )}` + ) + return null + } + + const mod = await import(deps.resolved.get('eslint')!) - if (!ESLint) { - eslintVersion = mod?.CLIEngine?.version + const { ESLint } = mod + let eslintVersion = ESLint?.version ?? mod?.CLIEngine?.version if (!eslintVersion || semver.lt(eslintVersion, '7.0.0')) { return `${chalk.red( @@ -58,98 +111,111 @@ async function lint( }. Please upgrade to ESLint version 7 or later` } - return `${chalk.red( - 'error' - )} - ESLint class not found. Please upgrade to ESLint version 7 or later` - } - let options: any = { - useEslintrc: true, - baseConfig: {}, - errorOnUnmatchedPattern: false, - extensions: ['.js', '.jsx', '.ts', '.tsx'], - ...eslintOptions, - } - let eslint = new ESLint(options) + let options: any = { + useEslintrc: true, + baseConfig: {}, + errorOnUnmatchedPattern: false, + extensions: ['.js', '.jsx', '.ts', '.tsx'], + ...eslintOptions, + } - let nextEslintPluginIsEnabled = false - const pagesDirRules = ['@next/next/no-html-link-for-pages'] + let eslint = new ESLint(options) - for (const configFile of [eslintrcFile, pkgJsonPath]) { - if (!configFile) continue + let nextEslintPluginIsEnabled = false + const pagesDirRules = ['@next/next/no-html-link-for-pages'] - const completeConfig: Config = await eslint.calculateConfigForFile( - configFile - ) + for (const configFile of [eslintrcFile, pkgJsonPath]) { + if (!configFile) continue + + const completeConfig: Config = await eslint.calculateConfigForFile( + configFile + ) - if (completeConfig.plugins?.includes('@next/next')) { - nextEslintPluginIsEnabled = true - break + if (completeConfig.plugins?.includes('@next/next')) { + nextEslintPluginIsEnabled = true + break + } } - } - const pagesDir = findPagesDir(baseDir) + const pagesDir = findPagesDir(baseDir) - if (nextEslintPluginIsEnabled) { - let updatedPagesDir = false + if (nextEslintPluginIsEnabled) { + let updatedPagesDir = false - for (const rule of pagesDirRules) { - if ( - !options.baseConfig!.rules?.[rule] && - !options.baseConfig!.rules?.[ - rule.replace('@next/next', '@next/babel-plugin-next') - ] - ) { - if (!options.baseConfig!.rules) { - options.baseConfig!.rules = {} + for (const rule of pagesDirRules) { + if ( + !options.baseConfig!.rules?.[rule] && + !options.baseConfig!.rules?.[ + rule.replace('@next/next', '@next/babel-plugin-next') + ] + ) { + if (!options.baseConfig!.rules) { + options.baseConfig!.rules = {} + } + options.baseConfig!.rules[rule] = [1, pagesDir] + updatedPagesDir = true } - options.baseConfig!.rules[rule] = [1, pagesDir] - updatedPagesDir = true } - } - if (updatedPagesDir) { - eslint = new ESLint(options) + if (updatedPagesDir) { + eslint = new ESLint(options) + } + } else { + Log.warn( + 'The Next.js plugin was not detected in your ESLint configuration. See https://nextjs.org/docs/basic-features/eslint#migrating-existing-config' + ) } - } - const lintStart = process.hrtime() - let results = await eslint.lintFiles(lintDirs) - let selectedFormatter = null - - if (options.fix) await ESLint.outputFixes(results) - if (reportErrorsOnly) results = await ESLint.getErrorResults(results) // Only return errors if --quiet flag is used - - if (formatter) selectedFormatter = await eslint.loadFormatter(formatter) - const formattedResult = formatResults( - baseDir, - results, - selectedFormatter?.format - ) - const lintEnd = process.hrtime(lintStart) - const totalWarnings = results.reduce( - (sum: number, file: LintResult) => sum + file.warningCount, - 0 - ) - return { - output: formattedResult.output, - isError: - ESLint.getErrorResults(results)?.length > 0 || - (maxWarnings >= 0 && totalWarnings > maxWarnings), - eventInfo: { - durationInSeconds: lintEnd[0], - eslintVersion: eslintVersion, - lintedFilesCount: results.length, - lintFix: !!options.fix, - nextEslintPluginVersion: nextEslintPluginIsEnabled - ? require(path.join( - path.dirname(deps.resolved.get('eslint-config-next')!), - 'package.json' - )).version - : null, - nextEslintPluginErrorsCount: formattedResult.totalNextPluginErrorCount, - nextEslintPluginWarningsCount: - formattedResult.totalNextPluginWarningCount, - }, + const lintStart = process.hrtime() + + let results = await eslint.lintFiles(lintDirs) + let selectedFormatter = null + + if (options.fix) await ESLint.outputFixes(results) + if (reportErrorsOnly) results = await ESLint.getErrorResults(results) // Only return errors if --quiet flag is used + + if (formatter) selectedFormatter = await eslint.loadFormatter(formatter) + const formattedResult = formatResults( + baseDir, + results, + selectedFormatter?.format + ) + const lintEnd = process.hrtime(lintStart) + const totalWarnings = results.reduce( + (sum: number, file: LintResult) => sum + file.warningCount, + 0 + ) + + return { + output: formattedResult.output, + isError: + ESLint.getErrorResults(results)?.length > 0 || + (maxWarnings >= 0 && totalWarnings > maxWarnings), + eventInfo: { + durationInSeconds: lintEnd[0], + eslintVersion: eslintVersion, + lintedFilesCount: results.length, + lintFix: !!options.fix, + nextEslintPluginVersion: nextEslintPluginIsEnabled + ? require(path.join( + path.dirname(deps.resolved.get('eslint-config-next')!), + 'package.json' + )).version + : null, + nextEslintPluginErrorsCount: formattedResult.totalNextPluginErrorCount, + nextEslintPluginWarningsCount: + formattedResult.totalNextPluginWarningCount, + }, + } + } catch (err) { + if (lintDuringBuild) { + Log.error( + `ESLint: ${err.message ? err.message.replace(/\n/g, ' ') : err}` + ) + return null + } else { + throw new Error(err) + } } } @@ -160,7 +226,8 @@ export async function runLintCheck( eslintOptions: any = null, reportErrorsOnly: boolean = false, maxWarnings: number = -1, - formatter: string | null = null + formatter: string | null = null, + strict: boolean = false ): ReturnType { try { // Find user's .eslintrc file @@ -187,45 +254,77 @@ export async function runLintCheck( packageJsonConfig = CommentJson.parse(pkgJsonContent) } - // Warning displayed if no ESLint configuration is present during build - if (lintDuringBuild && !eslintrcFile && !packageJsonConfig.eslintConfig) { - Log.warn( - `No ESLint configuration detected. Run ${chalk.bold.cyan( - 'next lint' - )} to begin setup` + const config = await hasEslintConfiguration(eslintrcFile, packageJsonConfig) + let deps + + if (config.exists) { + // Run if ESLint config exists + return await lint( + baseDir, + lintDirs, + eslintrcFile, + pkgJsonPath, + lintDuringBuild, + eslintOptions, + reportErrorsOnly, + maxWarnings, + formatter ) - return null - } + } else { + // Display warning if no ESLint configuration is present during "next build" + if (lintDuringBuild) { + Log.warn( + `No ESLint configuration detected. Run ${chalk.bold.cyan( + 'next lint' + )} to begin setup` + ) + return null + } else { + // Ask user what config they would like to start with for first time "next lint" setup + const { config: selectedConfig } = strict + ? ESLINT_PROMPT_VALUES.find( + (opt: { title: string }) => opt.title === 'Strict' + )! + : await cliPrompt() - // Ensure ESLint and necessary plugins and configs are installed: - const deps: NecessaryDependencies = await hasNecessaryDependencies( - baseDir, - false, - true, - lintDuringBuild - ) + if (selectedConfig == null) { + // Show a warning if no option is selected in prompt + Log.warn( + 'If you set up ESLint yourself, we recommend adding the Next.js ESLint plugin. See https://nextjs.org/docs/basic-features/eslint#migrating-existing-config' + ) + return null + } else { + // Check if necessary deps installed, and install any that are missing + deps = await hasNecessaryDependencies(baseDir, requiredPackages) + if (deps.missing.length > 0) + await installDependencies(baseDir, deps.missing, true) - // Write default ESLint config if none is present - // Check for /pages and src/pages is to make sure this happens in Next.js folder - if ( - existsSync(path.join(baseDir, 'pages')) || - existsSync(path.join(baseDir, 'src/pages')) - ) { - await writeDefaultConfig(eslintrcFile, pkgJsonPath, packageJsonConfig) - } + // Write default ESLint config. + // Check for /pages and src/pages is to make sure this happens in Next.js folder + if ( + existsSync(path.join(baseDir, 'pages')) || + existsSync(path.join(baseDir, 'src/pages')) + ) { + await writeDefaultConfig( + baseDir, + config, + selectedConfig, + eslintrcFile, + pkgJsonPath, + packageJsonConfig + ) + } + } - // Run ESLint - return await lint( - deps, - baseDir, - lintDirs, - eslintrcFile, - pkgJsonPath, - eslintOptions, - reportErrorsOnly, - maxWarnings, - formatter - ) + Log.ready( + `ESLint has successfully been configured. Run ${chalk.bold.cyan( + 'next lint' + )} again to view warnings and errors.` + ) + + return null + } + } } catch (err) { throw err } diff --git a/packages/next/lib/eslint/writeDefaultConfig.ts b/packages/next/lib/eslint/writeDefaultConfig.ts index aecf6cea247b4..7c14f0ee703b2 100644 --- a/packages/next/lib/eslint/writeDefaultConfig.ts +++ b/packages/next/lib/eslint/writeDefaultConfig.ts @@ -2,83 +2,65 @@ import { promises as fs } from 'fs' import chalk from 'chalk' import os from 'os' import path from 'path' - import * as CommentJson from 'next/dist/compiled/comment-json' +import { ConfigAvailable } from './hasEslintConfiguration' + +import * as Log from '../../build/output/log' export async function writeDefaultConfig( + baseDir: string, + { exists, emptyEslintrc, emptyPkgJsonConfig }: ConfigAvailable, + selectedConfig: any, eslintrcFile: string | null, pkgJsonPath: string | null, packageJsonConfig: { eslintConfig: any } | null ) { - const defaultConfig = { - extends: 'next', - } + if (!exists && emptyEslintrc && eslintrcFile) { + const ext = path.extname(eslintrcFile) - if (eslintrcFile) { - const content = await fs.readFile(eslintrcFile, { encoding: 'utf8' }).then( - (txt) => txt.trim().replace(/\n/g, ''), - () => null - ) + let newFileContent + if (ext === '.yaml' || ext === '.yml') { + newFileContent = "extends: 'next'" + } else { + newFileContent = CommentJson.stringify(selectedConfig, null, 2) - if ( - content === '' || - content === '{}' || - content === '---' || - content === 'module.exports = {}' - ) { - const ext = path.extname(eslintrcFile) - - let newFileContent - if (ext === '.yaml' || ext === '.yml') { - newFileContent = "extends: 'next'" - } else { - newFileContent = CommentJson.stringify(defaultConfig, null, 2) - - if (ext === '.js') { - newFileContent = 'module.exports = ' + newFileContent - } + if (ext === '.js') { + newFileContent = 'module.exports = ' + newFileContent } - - await fs.writeFile(eslintrcFile, newFileContent + os.EOL) - - console.log( - chalk.green( - `We detected an empty ESLint configuration file (${chalk.bold( - path.basename(eslintrcFile) - )}) and updated it for you to include the base Next.js ESLint configuration.` - ) - ) } - } else if (packageJsonConfig?.eslintConfig) { - // Creates .eslintrc only if package.json's eslintConfig field is empty - if (Object.entries(packageJsonConfig?.eslintConfig).length === 0) { - packageJsonConfig.eslintConfig = defaultConfig - if (pkgJsonPath) - await fs.writeFile( - pkgJsonPath, - CommentJson.stringify(packageJsonConfig, null, 2) + os.EOL - ) + await fs.writeFile(eslintrcFile, newFileContent + os.EOL) + + Log.info( + `We detected an empty ESLint configuration file (${chalk.bold( + path.basename(eslintrcFile) + )}) and updated it for you!` + ) + } else if (!exists && emptyPkgJsonConfig && packageJsonConfig) { + packageJsonConfig.eslintConfig = selectedConfig - console.log( - chalk.green( - `We detected an empty ${chalk.bold( - 'eslintConfig' - )} field in package.json and updated it for you to include the base Next.js ESLint configuration.` - ) + if (pkgJsonPath) + await fs.writeFile( + pkgJsonPath, + CommentJson.stringify(packageJsonConfig, null, 2) + os.EOL ) - } - } else { + + Log.info( + `We detected an empty ${chalk.bold( + 'eslintConfig' + )} field in package.json and updated it for you!` + ) + } else if (!exists) { await fs.writeFile( - '.eslintrc.json', - CommentJson.stringify(defaultConfig, null, 2) + os.EOL + path.join(baseDir, '.eslintrc.json'), + CommentJson.stringify(selectedConfig, null, 2) + os.EOL ) console.log( chalk.green( `We created the ${chalk.bold( '.eslintrc.json' - )} file for you and included the base Next.js ESLint configuration.` + )} file for you and included your selected configuration.` ) ) } diff --git a/packages/next/lib/has-necessary-dependencies.ts b/packages/next/lib/has-necessary-dependencies.ts index e08e316f491e7..55097d400f9a8 100644 --- a/packages/next/lib/has-necessary-dependencies.ts +++ b/packages/next/lib/has-necessary-dependencies.ts @@ -1,40 +1,18 @@ -import chalk from 'chalk' -import { join } from 'path' - -import { fileExists } from './file-exists' -import { getOxfordCommaList } from './oxford-comma-list' -import { FatalError } from './fatal-error' - -const requiredTSPackages = [ - { file: 'typescript', pkg: 'typescript' }, - { file: '@types/react/index.d.ts', pkg: '@types/react' }, - { file: '@types/node/index.d.ts', pkg: '@types/node' }, -] - -const requiredLintPackages = [ - { file: 'eslint/lib/api.js', pkg: 'eslint' }, - { file: 'eslint-config-next', pkg: 'eslint-config-next' }, -] +export interface MissingDependency { + file: string + pkg: string +} export type NecessaryDependencies = { resolved: Map + missing: MissingDependency[] } export async function hasNecessaryDependencies( baseDir: string, - checkTSDeps: boolean, - checkESLintDeps: boolean, - lintDuringBuild: boolean = false + requiredPackages: MissingDependency[] ): Promise { - if (!checkTSDeps && !checkESLintDeps) { - return { resolved: undefined! } - } - let resolutions = new Map() - let requiredPackages = checkESLintDeps - ? requiredLintPackages - : requiredTSPackages - const missingPackages = requiredPackages.filter((p) => { try { resolutions.set(p.pkg, require.resolve(p.file, { paths: [baseDir] })) @@ -44,47 +22,8 @@ export async function hasNecessaryDependencies( } }) - if (missingPackages.length < 1) { - return { - resolved: resolutions, - } + return { + resolved: resolutions, + missing: missingPackages, } - - const packagesHuman = getOxfordCommaList(missingPackages.map((p) => p.pkg)) - const packagesCli = missingPackages.map((p) => p.pkg).join(' ') - - const yarnLockFile = join(baseDir, 'yarn.lock') - const isYarn = await fileExists(yarnLockFile).catch(() => false) - - const removalTSMsg = - '\n\n' + - chalk.bold( - 'If you are not trying to use TypeScript, please remove the ' + - chalk.cyan('tsconfig.json') + - ' file from your package root (and any TypeScript files in your pages directory).' - ) - const removalLintMsg = - `\n\n` + - (lintDuringBuild - ? `If you do not want to run ESLint during builds, disable it in next.config.js. See https://nextjs.org/docs/api-reference/next.config.js/ignoring-eslint` - : `Once installed, run ${chalk.bold.cyan('next lint')} again.`) - const removalMsg = checkTSDeps ? removalTSMsg : removalLintMsg - - throw new FatalError( - chalk.bold.red( - checkTSDeps - ? `It looks like you're trying to use TypeScript but do not have the required package(s) installed.` - : `To use ESLint, additional required package(s) must be installed.` - ) + - '\n\n' + - chalk.bold(`Please install ${chalk.bold(packagesHuman)} by running:`) + - '\n\n' + - `\t${chalk.bold.cyan( - (isYarn ? 'yarn add --dev' : 'npm install --save-dev') + - ' ' + - packagesCli - )}` + - removalMsg + - '\n' - ) } diff --git a/packages/next/lib/helpers/get-online.ts b/packages/next/lib/helpers/get-online.ts new file mode 100644 index 0000000000000..c2f3f83a93ce8 --- /dev/null +++ b/packages/next/lib/helpers/get-online.ts @@ -0,0 +1,40 @@ +import { execSync } from 'child_process' +import dns from 'dns' +import url from 'url' + +function getProxy(): string | undefined { + if (process.env.https_proxy) { + return process.env.https_proxy + } + + try { + const httpsProxy = execSync('npm config get https-proxy').toString().trim() + return httpsProxy !== 'null' ? httpsProxy : undefined + } catch (e) { + return + } +} + +export function getOnline(): Promise { + return new Promise((resolve) => { + dns.lookup('registry.yarnpkg.com', (registryErr) => { + if (!registryErr) { + return resolve(true) + } + + const proxy = getProxy() + if (!proxy) { + return resolve(false) + } + + const { hostname } = url.parse(proxy) + if (!hostname) { + return resolve(false) + } + + dns.lookup(hostname, (proxyErr) => { + resolve(proxyErr == null) + }) + }) + }) +} diff --git a/packages/next/lib/helpers/install.ts b/packages/next/lib/helpers/install.ts new file mode 100644 index 0000000000000..05b7582d414f4 --- /dev/null +++ b/packages/next/lib/helpers/install.ts @@ -0,0 +1,114 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import chalk from 'chalk' +import spawn from 'next/dist/compiled/cross-spawn' + +interface InstallArgs { + /** + * Indicate whether to install packages using Yarn. + */ + useYarn: boolean + /** + * Indicate whether there is an active Internet connection. + */ + isOnline: boolean + /** + * Indicate whether the given dependencies are devDependencies. + */ + devDependencies?: boolean +} + +/** + * Spawn a package manager installation with either Yarn or NPM. + * + * @returns A Promise that resolves once the installation is finished. + */ +export function install( + root: string, + dependencies: string[] | null, + { useYarn, isOnline, devDependencies }: InstallArgs +): Promise { + /** + * NPM-specific command-line flags. + */ + const npmFlags: string[] = [] + /** + * Yarn-specific command-line flags. + */ + const yarnFlags: string[] = [] + /** + * Return a Promise that resolves once the installation is finished. + */ + return new Promise((resolve, reject) => { + let args: string[] + let command: string = useYarn ? 'yarnpkg' : 'npm' + + if (dependencies && dependencies.length) { + /** + * If there are dependencies, run a variation of `{displayCommand} add`. + */ + if (useYarn) { + /** + * Call `yarn add --exact (--offline)? (-D)? ...`. + */ + args = ['add', '--exact'] + if (!isOnline) args.push('--offline') + args.push('--cwd', root) + if (devDependencies) args.push('--dev') + args.push(...dependencies) + } else { + /** + * Call `npm install [--save|--save-dev] ...`. + */ + args = ['install', '--save-exact'] + args.push(devDependencies ? '--save-dev' : '--save') + args.push(...dependencies) + } + } else { + /** + * If there are no dependencies, run a variation of `{displayCommand} + * install`. + */ + args = ['install'] + if (useYarn) { + if (!isOnline) { + console.log(chalk.yellow('You appear to be offline.')) + console.log(chalk.yellow('Falling back to the local Yarn cache.')) + console.log() + args.push('--offline') + } + } else { + if (!isOnline) { + console.log(chalk.yellow('You appear to be offline.')) + console.log() + } + } + } + /** + * Add any package manager-specific flags. + */ + if (useYarn) { + args.push(...yarnFlags) + } else { + args.push(...npmFlags) + } + /** + * Spawn the installation process. + */ + const child = spawn(command, args, { + stdio: 'inherit', + env: { + ...process.env, + NODE_ENV: devDependencies ? 'development' : 'production', + ADBLOCK: '1', + DISABLE_OPENCOLLECTIVE: '1', + }, + }) + child.on('close', (code) => { + if (code !== 0) { + reject({ command: `${command} ${args.join(' ')}` }) + return + } + resolve() + }) + }) +} diff --git a/packages/next/lib/helpers/should-use-yarn.ts b/packages/next/lib/helpers/should-use-yarn.ts new file mode 100644 index 0000000000000..c305361517f0b --- /dev/null +++ b/packages/next/lib/helpers/should-use-yarn.ts @@ -0,0 +1,14 @@ +import { execSync } from 'child_process' + +export function shouldUseYarn(): boolean { + try { + const userAgent = process.env.npm_config_user_agent + if (userAgent) { + return Boolean(userAgent && userAgent.startsWith('yarn')) + } + execSync('yarnpkg --version', { stdio: 'ignore' }) + return true + } catch (e) { + return false + } +} diff --git a/packages/next/lib/install-dependencies.ts b/packages/next/lib/install-dependencies.ts new file mode 100644 index 0000000000000..79184113e14f8 --- /dev/null +++ b/packages/next/lib/install-dependencies.ts @@ -0,0 +1,37 @@ +import chalk from 'chalk' +import path from 'path' + +import { MissingDependency } from './has-necessary-dependencies' +import { shouldUseYarn } from './helpers/should-use-yarn' +import { install } from './helpers/install' +import { getOnline } from './helpers/get-online' + +export type Dependencies = { + resolved: Map +} + +export async function installDependencies( + baseDir: string, + deps: any, + dev: boolean = false +) { + const useYarn = shouldUseYarn() + const isOnline = !useYarn || (await getOnline()) + + if (deps.length) { + console.log() + console.log(`Installing ${dev ? 'devDependencies' : 'dependencies'}:`) + for (const dep of deps) { + console.log(`- ${chalk.cyan(dep.pkg)}`) + } + console.log() + + const devInstallFlags = { devDependencies: dev, ...{ useYarn, isOnline } } + await install( + path.resolve(baseDir), + deps.map((dep: MissingDependency) => dep.pkg), + devInstallFlags + ) + console.log() + } +} diff --git a/packages/next/lib/is-yarn.ts b/packages/next/lib/is-yarn.ts new file mode 100644 index 0000000000000..cbee420209717 --- /dev/null +++ b/packages/next/lib/is-yarn.ts @@ -0,0 +1,5 @@ +import { join } from 'path' +import { fileExists } from './file-exists' + +export const isYarn = async (dir: string) => + await fileExists(join(dir, 'yarn.lock')).catch(() => false) diff --git a/packages/next/lib/typescript/missingDependencyError.ts b/packages/next/lib/typescript/missingDependencyError.ts new file mode 100644 index 0000000000000..a88648879abc3 --- /dev/null +++ b/packages/next/lib/typescript/missingDependencyError.ts @@ -0,0 +1,38 @@ +import chalk from 'chalk' + +import { getOxfordCommaList } from '../oxford-comma-list' +import { MissingDependency } from '../has-necessary-dependencies' +import { FatalError } from '../fatal-error' +import { isYarn } from '../is-yarn' + +export function missingDepsError( + dir: string, + missingPackages: MissingDependency[] +) { + const packagesHuman = getOxfordCommaList(missingPackages.map((p) => p.pkg)) + const packagesCli = missingPackages.map((p) => p.pkg).join(' ') + + const removalMsg = + '\n\n' + + chalk.bold( + 'If you are not trying to use TypeScript, please remove the ' + + chalk.cyan('tsconfig.json') + + ' file from your package root (and any TypeScript files in your pages directory).' + ) + + throw new FatalError( + chalk.bold.red( + `It looks like you're trying to use TypeScript but do not have the required package(s) installed.` + ) + + '\n\n' + + chalk.bold(`Please install ${chalk.bold(packagesHuman)} by running:`) + + '\n\n' + + `\t${chalk.bold.cyan( + (isYarn(dir) ? 'yarn add --dev' : 'npm install --save-dev') + + ' ' + + packagesCli + )}` + + removalMsg + + '\n' + ) +} diff --git a/packages/next/lib/verifyTypeScriptSetup.ts b/packages/next/lib/verifyTypeScriptSetup.ts index 44a201baa6972..280876c143575 100644 --- a/packages/next/lib/verifyTypeScriptSetup.ts +++ b/packages/next/lib/verifyTypeScriptSetup.ts @@ -13,6 +13,13 @@ import { getTypeScriptIntent } from './typescript/getTypeScriptIntent' import { TypeCheckResult } from './typescript/runTypeCheck' import { writeAppTypeDeclarations } from './typescript/writeAppTypeDeclarations' import { writeConfigurationDefaults } from './typescript/writeConfigurationDefaults' +import { missingDepsError } from './typescript/missingDependencyError' + +const requiredPackages = [ + { file: 'typescript', pkg: 'typescript' }, + { file: '@types/react/index.d.ts', pkg: '@types/react' }, + { file: '@types/node/index.d.ts', pkg: '@types/node' }, +] export async function verifyTypeScriptSetup( dir: string, @@ -29,15 +36,17 @@ export async function verifyTypeScriptSetup( if (!intent) { return { version: null } } - const firstTimeSetup = intent.firstTimeSetup // Ensure TypeScript and necessary `@types/*` are installed: const deps: NecessaryDependencies = await hasNecessaryDependencies( dir, - !!intent, - false + requiredPackages ) + if (deps.missing?.length > 0) { + missingDepsError(dir, deps.missing) + } + // Load TypeScript after we're sure it exists: const ts = (await import( deps.resolved.get('typescript')! @@ -50,7 +59,7 @@ export async function verifyTypeScriptSetup( } // Reconfigure (or create) the user's `tsconfig.json` for them: - await writeConfigurationDefaults(ts, tsConfigPath, firstTimeSetup) + await writeConfigurationDefaults(ts, tsConfigPath, intent.firstTimeSetup) // Write out the necessary `next-env.d.ts` file to correctly register // Next.js' types: await writeAppTypeDeclarations(dir, imageImportsEnabled) diff --git a/packages/next/package.json b/packages/next/package.json index 80a6465af0689..9560676db29cf 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -196,11 +196,13 @@ "bfj": "7.0.2", "cacache": "15.0.5", "ci-info": "watson/ci-info#f43f6a1cefff47fb361c88cf4b943fdbcaafe540", + "cli-select": "1.1.2", "comment-json": "3.0.3", "compression": "1.7.4", "conf": "5.0.0", "content-type": "1.0.4", "cookie": "0.4.1", + "cross-spawn": "6.0.5", "css-loader": "4.3.0", "debug": "4.1.1", "devalue": "2.0.1", diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index d09fdd51b7125..78d460afbc3d9 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -196,6 +196,14 @@ export async function ncc_ci_info(task, opts) { .ncc({ packageName: 'ci-info', externals }) .target('compiled/ci-info') } +// eslint-disable-next-line camelcase +externals['cli-select'] = 'next/dist/compiled/cli-select' +export async function ncc_cli_select(task, opts) { + await task + .source(opts.src || relative(__dirname, require.resolve('cli-select'))) + .ncc({ packageName: 'cli-select', externals }) + .target('compiled/cli-select') +} externals['comment-json'] = 'next/dist/compiled/comment-json' export async function ncc_comment_json(task, opts) { await task @@ -236,6 +244,14 @@ export async function ncc_cookie(task, opts) { .target('compiled/cookie') } // eslint-disable-next-line camelcase +externals['cross-spawn'] = 'next/dist/compiled/cross-spawn' +export async function ncc_cross_spawn(task, opts) { + await task + .source(opts.src || relative(__dirname, require.resolve('cross-spawn'))) + .ncc({ packageName: 'cross-spawn', externals }) + .target('compiled/cross-spawn') +} +// eslint-disable-next-line camelcase externals['css-loader'] = 'next/dist/compiled/css-loader' export async function ncc_css_loader(task, opts) { await task @@ -740,11 +756,13 @@ export async function ncc(task, opts) { 'ncc_bfj', 'ncc_cacache', 'ncc_ci_info', + 'ncc_cli_select', 'ncc_comment_json', 'ncc_compression', 'ncc_conf', 'ncc_content_type', 'ncc_cookie', + 'ncc_cross_spawn', 'ncc_css_loader', 'ncc_debug', 'ncc_devalue', diff --git a/packages/next/types/misc.d.ts b/packages/next/types/misc.d.ts index 128e4bb7de1e4..e986ce343ae5c 100644 --- a/packages/next/types/misc.d.ts +++ b/packages/next/types/misc.d.ts @@ -75,6 +75,10 @@ declare module 'next/dist/compiled/ci-info' { import m from 'ci-info' export = m } +declare module 'next/dist/compiled/cli-select' { + import m from 'cli-select' + export = m +} declare module 'next/dist/compiled/compression' { import m from 'compression' export = m @@ -91,6 +95,10 @@ declare module 'next/dist/compiled/cookie' { import m from 'cookie' export = m } +declare module 'next/dist/compiled/cross-spawn' { + import m from 'cross-spawn' + export = m +} declare module 'next/dist/compiled/debug' { import m from 'debug' export = m diff --git a/test/integration/create-next-app/index.test.js b/test/integration/create-next-app/index.test.js index 2ed045c99a834..ac3e8bfb82203 100644 --- a/test/integration/create-next-app/index.test.js +++ b/test/integration/create-next-app/index.test.js @@ -53,7 +53,7 @@ describe('create next app', () => { fs.existsSync(path.join(cwd, projectName, 'pages/index.js')) ).toBeTruthy() expect( - fs.existsSync(path.join(cwd, projectName, '.eslintrc')) + fs.existsSync(path.join(cwd, projectName, '.eslintrc.json')) ).toBeTruthy() expect( fs.existsSync(path.join(cwd, projectName, 'node_modules/next')) @@ -125,7 +125,7 @@ describe('create next app', () => { fs.existsSync(path.join(cwd, projectName, 'next-env.d.ts')) ).toBeTruthy() expect( - fs.existsSync(path.join(cwd, projectName, '.eslintrc')) + fs.existsSync(path.join(cwd, projectName, '.eslintrc.json')) ).toBeTruthy() expect( fs.existsSync(path.join(cwd, projectName, 'node_modules/next')) @@ -254,7 +254,7 @@ describe('create next app', () => { 'package.json', 'pages/index.js', '.gitignore', - '.eslintrc', + '.eslintrc.json', ] files.forEach((file) => expect(fs.existsSync(path.join(cwd, projectName, file))).toBeTruthy() @@ -322,7 +322,7 @@ describe('create next app', () => { 'pages/index.js', '.gitignore', 'node_modules/next', - '.eslintrc', + '.eslintrc.json', ] files.forEach((file) => expect(fs.existsSync(path.join(cwd, file))).toBeTruthy() @@ -341,7 +341,7 @@ describe('create next app', () => { 'pages/index.js', '.gitignore', 'node_modules/next', - '.eslintrc', + '.eslintrc.json', ] files.forEach((file) => expect(fs.existsSync(path.join(cwd, projectName, file))).toBeTruthy() @@ -359,7 +359,7 @@ describe('create next app', () => { 'package.json', 'pages/index.js', '.gitignore', - '.eslintrc', + '.eslintrc.json', 'package-lock.json', 'node_modules/next', ] diff --git a/test/integration/dist-dir/test/index.test.js b/test/integration/dist-dir/test/index.test.js index 0149173a41987..ac7c9396e24cb 100644 --- a/test/integration/dist-dir/test/index.test.js +++ b/test/integration/dist-dir/test/index.test.js @@ -56,7 +56,10 @@ describe('distDir', () => { it('should handle null/undefined distDir', async () => { const origNextConfig = await fs.readFile(nextConfig, 'utf8') - await fs.writeFile(nextConfig, `module.exports = { distDir: null }`) + await fs.writeFile( + nextConfig, + `module.exports = { distDir: null, eslint: { ignoreDuringBuilds: true } }` + ) const { stderr } = await nextBuild(appDir, [], { stderr: true }) await fs.writeFile(nextConfig, origNextConfig) diff --git a/test/integration/eslint/first-time-setup/.eslintrc b/test/integration/eslint/first-time-setup/.eslintrc deleted file mode 100644 index c3796c8047dd0..0000000000000 --- a/test/integration/eslint/first-time-setup/.eslintrc +++ /dev/null @@ -1,4 +0,0 @@ -{ - "root": true, - "extends": "next" -} diff --git a/test/integration/eslint/first-time-setup/.eslintrc.json b/test/integration/eslint/first-time-setup/.eslintrc.json new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/test/integration/eslint/no-config/pages/index.js b/test/integration/eslint/no-config/pages/index.js new file mode 100644 index 0000000000000..6181acb3d2c35 --- /dev/null +++ b/test/integration/eslint/no-config/pages/index.js @@ -0,0 +1,9 @@ +export default class Test { + render() { + return ( +
+

Hello title

+
+ ) + } +} diff --git a/test/integration/eslint/no-eslint-plugin/.eslintrc b/test/integration/eslint/no-eslint-plugin/.eslintrc new file mode 100644 index 0000000000000..0bed6ddbc0e2a --- /dev/null +++ b/test/integration/eslint/no-eslint-plugin/.eslintrc @@ -0,0 +1,4 @@ +{ + "root": true, + "extends": "eslint:recommended" +} diff --git a/test/integration/eslint/no-eslint-plugin/pages/index.js b/test/integration/eslint/no-eslint-plugin/pages/index.js new file mode 100644 index 0000000000000..5a2ab41ccf1aa --- /dev/null +++ b/test/integration/eslint/no-eslint-plugin/pages/index.js @@ -0,0 +1,7 @@ +const Home = () => ( +
+

Home

+
+) + +export default Home diff --git a/test/integration/eslint/test/index.test.js b/test/integration/eslint/test/index.test.js index f00fcb2a21aac..9c7f70802bf4c 100644 --- a/test/integration/eslint/test/index.test.js +++ b/test/integration/eslint/test/index.test.js @@ -1,8 +1,11 @@ import fs from 'fs-extra' -import { join } from 'path' +import os from 'os' +import execa from 'execa' + +import { dirname, join } from 'path' + import findUp from 'next/dist/compiled/find-up' import { nextBuild, nextLint } from 'next-test-utils' -import { writeFile, readFile } from 'fs-extra' jest.setTimeout(1000 * 60 * 2) @@ -24,25 +27,23 @@ const dirInvalidEslintVersion = join(__dirname, '../invalid-eslint-version') const dirMaxWarnings = join(__dirname, '../max-warnings') const dirEmptyDirectory = join(__dirname, '../empty-directory') const dirEslintIgnore = join(__dirname, '../eslint-ignore') +const dirNoEslintPlugin = join(__dirname, '../no-eslint-plugin') +const dirNoConfig = join(__dirname, '../no-config') describe('ESLint', () => { describe('Next Build', () => { test('first time setup', async () => { - const eslintrc = join(dirFirstTimeSetup, '.eslintrc') - await writeFile(eslintrc, '') + const eslintrcJson = join(dirFirstTimeSetup, '.eslintrc.json') + await fs.writeFile(eslintrcJson, '') const { stdout, stderr } = await nextBuild(dirFirstTimeSetup, [], { stdout: true, stderr: true, }) const output = stdout + stderr - const eslintrcContent = await readFile(eslintrc, 'utf8') expect(output).toContain( - 'We detected an empty ESLint configuration file (.eslintrc) and updated it for you to include the base Next.js ESLint configuration.' - ) - expect(eslintrcContent.trim().replace(/\s/g, '')).toMatch( - '{"extends":"next"}' + 'No ESLint configuration detected. Run next lint to begin setup' ) }) @@ -131,27 +132,98 @@ describe('ESLint', () => { ) expect(output).toContain('Compiled successfully') }) - }) - - describe('Next Lint', () => { - test('first time setup', async () => { - const eslintrc = join(dirFirstTimeSetup, '.eslintrc') - await writeFile(eslintrc, '') - const { stdout, stderr } = await nextLint(dirFirstTimeSetup, [], { + test('missing Next.js plugin', async () => { + const { stdout, stderr } = await nextBuild(dirNoEslintPlugin, [], { stdout: true, stderr: true, }) - const output = stdout + stderr - const eslintrcContent = await readFile(eslintrc, 'utf8') + const output = stdout + stderr expect(output).toContain( - 'We detected an empty ESLint configuration file (.eslintrc) and updated it for you to include the base Next.js ESLint configuration.' - ) - expect(eslintrcContent.trim().replace(/\s/g, '')).toMatch( - '{"extends":"next"}' + 'The Next.js plugin was not detected in your ESLint configuration' ) }) + }) + + describe('Next Lint', () => { + describe('First Time Setup ', () => { + async function nextLintTemp() { + const folder = join( + os.tmpdir(), + Math.random().toString(36).substring(2) + ) + await fs.mkdirp(folder) + await fs.copy(dirNoConfig, folder) + + try { + const nextDir = dirname(require.resolve('next/package')) + const nextBin = join(nextDir, 'dist/bin/next') + + const { stdout } = await execa('node', [ + nextBin, + 'lint', + folder, + '--strict', + ]) + + const pkgJson = JSON.parse( + await fs.readFile(join(folder, 'package.json'), 'utf8') + ) + const eslintrcJson = JSON.parse( + await fs.readFile(join(folder, '.eslintrc.json'), 'utf8') + ) + + return { stdout, pkgJson, eslintrcJson } + } finally { + await fs.remove(folder) + } + } + + test('show a prompt to set up ESLint if no configuration detected', async () => { + const eslintrcJson = join(dirFirstTimeSetup, '.eslintrc.json') + await fs.writeFile(eslintrcJson, '') + + const { stdout, stderr } = await nextLint(dirFirstTimeSetup, [], { + stdout: true, + stderr: true, + }) + const output = stdout + stderr + expect(output).toContain('How would you like to configure ESLint?') + + // Different options that can be selected + expect(output).toContain('Strict (recommended)') + expect(output).toContain('Base') + expect(output).toContain('Cancel') + }) + + test('installs eslint and eslint-config-next as devDependencies if missing', async () => { + const { stdout, pkgJson } = await nextLintTemp() + + expect(stdout.replace(/(\r\n|\n|\r)/gm, '')).toContain( + 'Installing devDependencies:- eslint- eslint-config-next' + ) + expect(pkgJson.devDependencies).toHaveProperty('eslint') + expect(pkgJson.devDependencies).toHaveProperty('eslint-config-next') + }) + + test('creates .eslintrc.json file with a default configuration', async () => { + const { stdout, eslintrcJson } = await nextLintTemp() + + expect(stdout).toContain( + 'We created the .eslintrc.json file for you and included your selected configuration' + ) + expect(eslintrcJson).toMatchObject({ extends: 'next/core-web-vitals' }) + }) + + test('shows a successful message when completed', async () => { + const { stdout, eslintrcJson } = await nextLintTemp() + + expect(stdout).toContain( + 'ESLint has successfully been configured. Run next lint again to view warnings and errors' + ) + }) + }) test('shows warnings and errors', async () => { const { stdout, stderr } = await nextLint(dirCustomConfig, [], { @@ -222,6 +294,9 @@ describe('ESLint', () => { }) test('success message when no warnings or errors', async () => { + const eslintrcJson = join(dirFirstTimeSetup, '.eslintrc.json') + await fs.writeFile(eslintrcJson, '{ "extends": "next", "root": true }') + const { stdout, stderr } = await nextLint(dirFirstTimeSetup, [], { stdout: true, stderr: true, @@ -259,7 +334,7 @@ describe('ESLint', () => { const output = stdout + stderr expect(output).not.toContain( - 'We created the .eslintrc file for you and included the base Next.js ESLint configuration' + 'We created the .eslintrc file for you and included your selected configuration' ) } finally { // Restore original .eslintrc file diff --git a/test/integration/image-component/svgo-webpack/next.config.js b/test/integration/image-component/svgo-webpack/next.config.js index b05aad47ccb55..c1e4c555b45f3 100644 --- a/test/integration/image-component/svgo-webpack/next.config.js +++ b/test/integration/image-component/svgo-webpack/next.config.js @@ -1,4 +1,7 @@ module.exports = { + eslint: { + ignoreDuringBuilds: true, + }, webpack(config, options) { config.module.rules.push({ test: /\.svg$/, diff --git a/test/integration/production-build-dir/build/next.config.js b/test/integration/production-build-dir/build/next.config.js new file mode 100644 index 0000000000000..954704d28c2d7 --- /dev/null +++ b/test/integration/production-build-dir/build/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + eslint: { + ignoreDuringBuilds: true, + }, +} diff --git a/test/integration/webpack-require-hook/next.config.js b/test/integration/webpack-require-hook/next.config.js index 3cf986614d4ee..f6a8dd2c0cb0a 100644 --- a/test/integration/webpack-require-hook/next.config.js +++ b/test/integration/webpack-require-hook/next.config.js @@ -1,4 +1,7 @@ module.exports = { + eslint: { + ignoreDuringBuilds: true, + }, webpack(config) { console.log('Initialized config') if ( diff --git a/yarn.lock b/yarn.lock index de1bf29a49507..98e9b30e912ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5134,7 +5134,7 @@ ansi-colors@^4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== -ansi-escapes@^3.0.0: +ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" @@ -6588,6 +6588,13 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" +cli-select@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cli-select/-/cli-select-1.1.2.tgz#456dced464b3346ca661b16a0e37fc4b28db4818" + integrity sha512-PSvWb8G0PPmBNDcz/uM2LkZN3Nn5JmhUl465tTfynQAXjKzFpmHbxStM6X/+awKp5DJuAaHMzzMPefT0suGm1w== + dependencies: + ansi-escapes "^3.2.0" + cli-spinners@^1.1.0: version "1.3.1" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-1.3.1.tgz#002c1990912d0d59580c93bd36c056de99e4259a"