Adds ESLint with default rule-set (#23702)

This PR re-includes ESLint with some notable changes, namely a guided setup similar to how TypeScript is instantiated in a Next.js application.

To add ESLint to a project, developers will have to create an `.eslintrc` file in the root of their project or add an empty `eslintConfig` object to their `package.json` file.

```js
touch .eslintrc
```

Then running `next build` will show instructions to install the required packages needed:

<img width="862" alt="Screen Shot 2021-04-19 at 7 38 27 PM" src="https://user-images.githubusercontent.com/12476932/115316182-dfd51b00-a146-11eb-830c-90bad20ed151.png">

Once installed and `next build` is run again, `.eslintrc` will be automatically configured to include the default config:

```json
{
  "extends": "next"
}
```

In addition to this change:

- The feature is now under the experimental flag and requires opt-in. After testing and feedback, it will be switched to the top-level namespace and turned on by default.
- A new ESLint shareable configuration package is included that can be extended in any application with `{ extends: 'next' }`
  - This default config extends recommended rule sets from [`eslint-plugin-react`](https://www.npmjs.com/package/eslint-plugin-react), [`eslint-plugin-react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks), and [`eslint-plugin-next`](https://www.npmjs.com/package/@next/eslint-plugin-next)
- All rules in [`eslint-plugin-next`](https://www.npmjs.com/package/@next/eslint-plugin-next) have been modified to include actionable links that show more information to help resolve each issue
This commit is contained in:
Houssein Djirdeh 2021-04-30 07:09:07 -04:00 committed by GitHub
parent 49cd08da17
commit e783b0a2e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 1122 additions and 358 deletions

View file

@ -2,6 +2,8 @@ node_modules
**/.next/**
**/_next/**
**/dist/**
e2e-tests/**
examples/with-eslint/**
examples/with-typescript-eslint-jest/**
examples/with-kea/**
packages/next/bundles/webpack/packages/*.runtime.js
@ -16,4 +18,5 @@ packages/next-codemod/**/*.js
packages/next-codemod/**/*.d.ts
packages/next-env/**/*.d.ts
test/integration/async-modules/**
test-timings.json
test/integration/eslint/**
test-timings.json

View file

@ -242,6 +242,7 @@
"path": "/errors/next-start-serverless.md"
},
{ "title": "no-cache", "path": "/errors/no-cache.md" },
{ "title": "no-css-tags", "path": "/errors/no-css-tags.md" },
{
"title": "no-document-title",
"path": "/errors/no-document-title.md"
@ -250,6 +251,10 @@
"title": "no-document-viewport-meta",
"path": "/errors/no-document-viewport-meta.md"
},
{
"title": "no-html-link-for-pages",
"path": "/errors/no-html-link-for-pages.md"
},
{
"title": "no-on-app-updated-hook",
"path": "/errors/no-on-app-updated-hook.md"
@ -258,6 +263,14 @@
"title": "no-router-instance",
"path": "/errors/no-router-instance.md"
},
{
"title": "no-sync-scripts",
"path": "/errors/no-sync-scripts.md"
},
{
"title": "no-unwanted-polyfillio",
"path": "/errors/no-unwanted-polyfillio.md"
},
{
"title": "non-standard-node-env",
"path": "/errors/non-standard-node-env.md"

38
errors/no-css-tags.md Normal file
View file

@ -0,0 +1,38 @@
# No CSS Tags
### Why This Error Occurred
An HTML link element was used to link to an external stylesheet. This can negatively affect CSS resource loading on your web page.
### Possible Ways to Fix It
There are multiple ways to include styles using Next.js' built-in CSS support, including the option to use `@import` within the root stylesheet that is imported in `pages/_app.js`:
```css
/* Root stylesheet */
@import 'extra.css';
body {
/* ... */
}
```
Another option is to use CSS Modules to import the CSS file scoped specifically to the component.
```jsx
import styles from './extra.module.css'
export class Home {
render() {
return (
<div>
<button type="button" className={styles.active}>
Open
</button>
</div>
)
}
}
```
Refer to the [Built-In CSS Support](https://nextjs.org/docs/basic-features/built-in-css-support) documentation to learn about all the ways to include CSS to your application.

View file

@ -0,0 +1,45 @@
# No HTML link for pages
### Why This Error Occurred
An HTML anchor element, `<a>`, was used to navigate to a page route without using the `Link` component.
The `Link` component is required in order to enable client-side route transitions between pages and provide a single-page app experience.
### Possible Ways to Fix It
Make sure to import the `Link` component and wrap anchor elements that route to different page routes.
**Before:**
```jsx
function Home() {
return (
<div>
<a href="/about">About Us</a>
</div>
)
}
```
**After:**
```jsx
import Link from 'next/link'
function Home() {
return (
<div>
<Link href="/about">
<a>About Us</a>
</Link>
</div>
)
}
export default Home
```
### Useful Links
- [next/link API Reference](https://nextjs.org/docs/api-reference/next/link)

32
errors/no-sync-scripts.md Normal file
View file

@ -0,0 +1,32 @@
# No Sync Scripts
### Why This Error Occurred
A synchronous script was used which can impact your webpage's performance.
### Possible Ways to Fix It
#### Script component (experimental)
Use the Script component with the right loading strategy to defer loading of the script until necessary.
```jsx
import Script from 'next/experimental-script'
const Home = () => {
return (
<div class="container">
<Script src="https://third-party-script.js" strategy="defer"></Script>
<div>Home Page</div>
</div>
)
}
export default Home
```
Note: This is still an experimental feature and needs to be enabled via the `experimental.scriptLoader` flag in `next.config.js`.
### Useful Links
- [Efficiently load third-party JavaScript](https://web.dev/efficiently-load-third-party-javascript/)

View file

@ -0,0 +1,13 @@
# Duplicate Polyfills from Polyfill.io
#### Why This Error Occurred
You are using Polyfill.io and including duplicate polyfills already shipped with Next.js. This increases page weight unnecessarily which can affect loading performance.
#### Possible Ways to Fix It
Remove all duplicate polyfills that are included with Polyfill.io. If you need to add polyfills but are not sure if Next.js already includes it, take a look at the list of [supported browsers and features](https://nextjs.org/docs/basic-features/supported-browsers-features) first.
### Useful Links
- [Supported Browsers and Features](https://nextjs.org/docs/basic-features/supported-browsers-features)

View file

@ -0,0 +1,4 @@
{
"extends": "next",
"root": true
}

34
examples/with-eslint/.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel

View file

@ -0,0 +1,23 @@
# ESLint Example
This example shows a Next.js application using the built-in ESLint setup with the `next` shareable configuration enabled in `.eslintrc`.
Note: ESLint running during build (`next build`) is still experimental and needs to be enabled via an `{ experimental: eslint }` flag in `next.config.js`.
## Deploy your own
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-service-worker&project-name=with-service-worker&repository-name=with-service-worker)
## How to use
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
```bash
npx create-next-app --example with-eslint with-eslint
# or
yarn create next-app --example with-eslint with-eslint
```
Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).

View file

@ -0,0 +1,5 @@
module.exports = {
experimental: {
eslint: true,
},
}

View file

@ -0,0 +1,19 @@
{
"name": "with-eslint",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"license": "MIT",
"dependencies": {
"next": "latest",
"react": "^17.0.1",
"react-dom": "^17.0.1"
},
"devDependencies": {
"eslint": "^7.24.0",
"eslint-config-next": "latest"
}
}

View file

@ -0,0 +1,8 @@
const Home = () => (
<div>
<script src="https://fake-script.com" />
<p>Home</p>
</div>
)
export default Home

View file

@ -0,0 +1,55 @@
/*
* @rushstack/eslint-patch is used to include plugins as dev
* dependencies instead of imposing them as peer dependencies
*
* https://www.npmjs.com/package/@rushstack/eslint-patch
*/
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
extends: [
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:@next/next/recommended',
],
plugins: ['import', 'react'],
rules: {
'import/no-anonymous-default-export': 'warn',
'react/react-in-jsx-scope': 'off',
},
parser: './parser.js',
parserOptions: {
requireConfigFile: false,
sourceType: 'module',
allowImportExportEverywhere: true,
babelOptions: {
presets: ['next/babel'],
},
},
overrides: [
{
files: ['**/*.ts?(x)'],
parser: '@typescript-eslint/parser',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
warnOnUnsupportedTypeScriptVersion: true,
},
},
],
settings: {
react: {
version: 'detect',
},
'import/parsers': {
[require.resolve('@typescript-eslint/parser')]: ['.ts', '.tsx', '.d.ts'],
},
'import/resolver': {
[require.resolve('eslint-import-resolver-node')]: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
}

View file

@ -0,0 +1,30 @@
{
"name": "eslint-config-next",
"version": "10.1.4-canary.15",
"description": "ESLint configuration used by NextJS.",
"main": "index.js",
"license": "MIT",
"repository": {
"url": "vercel/next.js",
"directory": "packages/eslint-config-next"
},
"dependencies": {
"@rushstack/eslint-patch": "^1.0.6",
"@next/eslint-plugin-next": "^10.1.3",
"@typescript-eslint/parser": "^4.20.0",
"eslint-import-resolver-node": "^0.3.4",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-react": "^7.23.1",
"eslint-plugin-react-hooks": "^4.2.0"
},
"peerDependencies": {
"eslint": "^7.23.0",
"next": ">=10.2.0",
"typescript": ">=3.3.1"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
}

View file

@ -0,0 +1,9 @@
const {
parse,
parseForESLint,
} = require('next/dist/compiled/babel/eslint-parser')
module.exports = {
parse,
parseForESLint,
}

View file

@ -4,7 +4,6 @@ module.exports = {
'no-sync-scripts': require('./rules/no-sync-scripts'),
'no-html-link-for-pages': require('./rules/no-html-link-for-pages'),
'no-unwanted-polyfillio': require('./rules/no-unwanted-polyfillio'),
'missing-preload': require('./rules/missing-preload'),
},
configs: {
recommended: {
@ -14,7 +13,6 @@ module.exports = {
'@next/next/no-sync-scripts': 1,
'@next/next/no-html-link-for-pages': 1,
'@next/next/no-unwanted-polyfillio': 1,
'@next/next/missing-preload': 1,
},
},
},

View file

@ -1,57 +0,0 @@
module.exports = {
meta: {
type: 'suggestion',
fixable: 'code',
docs: {
description: 'Ensure stylesheets are preloaded',
category: 'Optimizations',
recommended: true,
},
},
create: function (context) {
const preloads = new Set()
const links = new Map()
return {
'Program:exit': function (node) {
for (let [href, linkNode] of links.entries()) {
if (!preloads.has(href)) {
context.report({
node: linkNode,
message:
'Stylesheet does not have an associated preload tag. This could potentially impact First paint.',
fix: function (fixer) {
return fixer.insertTextBefore(
linkNode,
`<link rel="preload" href="${href}" as="style" />`
)
},
})
}
}
links.clear()
preloads.clear()
},
'JSXOpeningElement[name.name=link][attributes.length>0]': function (
node
) {
const attributes = node.attributes.filter(
(attr) => attr.type === 'JSXAttribute'
)
const rel = attributes.find((attr) => attr.name.name === 'rel')
const relValue = rel && rel.value.value
const href = attributes.find((attr) => attr.name.name === 'href')
const hrefValue = href && href.value.value
const media = attributes.find((attr) => attr.name.name === 'media')
const mediaValue = media && media.value.value
if (relValue === 'preload') {
preloads.add(hrefValue)
} else if (relValue === 'stylesheet' && mediaValue !== 'print') {
links.set(hrefValue, node)
}
},
}
},
}

View file

@ -26,7 +26,7 @@ module.exports = function (context) {
context.report({
node,
message:
'In order to use external stylesheets use @import in the root stylesheet compiled with NextJS. This ensures proper priority to CSS when loading a webpage.',
'Do not include stylesheets manually. See: https://nextjs.org/docs/messages/no-css-tags.',
})
}
},

View file

@ -1,10 +1,18 @@
const path = require('path')
const fs = require('fs')
const { getUrlFromPagesDirectory, normalizeURL } = require('../utils/url')
const {
getUrlFromPagesDirectory,
normalizeURL,
execOnce,
} = require('../utils/url')
const pagesDirWarning = execOnce((pagesDirs) => {
console.warn(
`Pages directory cannot be found at ${pagesDirs.join(' or ')}. ` +
`If using a custom path, please configure with the no-html-link-for-pages rule in your eslint config file`
)
})
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
@ -26,10 +34,8 @@ module.exports = {
]
const pagesDir = pagesDirs.find((dir) => fs.existsSync(dir))
if (!pagesDir) {
throw new Error(
`Pages directory cannot be found at ${pagesDirs.join(' or ')}. ` +
`If using a custom path, please configure with the no-html-link-for-pages rule`
)
pagesDirWarning(pagesDirs)
return {}
}
const urls = getUrlFromPagesDirectory('/', pagesDir)
@ -61,7 +67,7 @@ module.exports = {
if (url.test(normalizeURL(hrefPath))) {
context.report({
node,
message: `You're using <a> tag to navigate to ${hrefPath}. Use Link from 'next/link' to make sure the app behaves like an SPA.`,
message: `Do not use the HTML <a> tag to navigate to ${hrefPath}. Use Link from 'next/link' instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages.`,
})
}
})

View file

@ -18,7 +18,7 @@ module.exports = function (context) {
context.report({
node,
message:
"A synchronous script tag can impact your webpage's performance",
'Synchronous scripts are forbidden. See: https://nextjs.org/docs/messages/no-sync-scripts.',
})
}
},

View file

@ -95,9 +95,11 @@ module.exports = {
if (unwantedFeatures.length > 0) {
context.report({
node,
message: `You're requesting polyfills from polyfill.io which are already shipped with NextJS. Please remove ${unwantedFeatures.join(
message: `No duplicate polyfills from Polyfill.io are allowed. ${unwantedFeatures.join(
', '
)} from the features list.`,
)} ${
unwantedFeatures.length > 1 ? 'are' : 'is'
} already shipped with Next.js. See: https://nextjs.org/docs/messages/no-unwanted-polyfillio.`,
})
}
}

View file

@ -76,7 +76,21 @@ function normalizeURL(url) {
return url
}
function execOnce(fn) {
let used = false
let result
return (...args) => {
if (!used) {
used = true
result = fn(...args)
}
return result
}
}
module.exports = {
getUrlFromPagesDirectory,
normalizeURL,
execOnce,
}

View file

@ -26,6 +26,7 @@ import loadCustomRoutes, {
} from '../lib/load-custom-routes'
import { nonNullable } from '../lib/non-nullable'
import { recursiveDelete } from '../lib/recursive-delete'
import { verifyAndLint } from '../lib/verifyAndLint'
import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup'
import {
BUILD_ID_FILE,
@ -193,6 +194,19 @@ export default async function build(
typeCheckingSpinner.stopAndPersist()
}
if (config.experimental.eslint) {
await nextBuildSpan
.traceChild('verify-and-lint')
.traceAsyncFn(async () => {
await verifyAndLint(
dir,
pagesDir,
config.experimental.cpus,
config.experimental.workerThreads
)
})
}
const buildSpinner = createSpinner({
prefixText: `${Log.prefixes.info} Creating an optimized production build`,
})

View file

@ -28,6 +28,10 @@ function coreLibPluginPass() {
return require('@babel/core/lib/transformation/plugin-pass')
}
function eslintParser() {
return require('@babel/eslint-parser')
}
function traverse() {
return require('@babel/traverse')
}
@ -100,6 +104,7 @@ module.exports = {
coreLibNormalizeOpts,
coreLibBlockHoistPlugin,
coreLibPluginPass,
eslintParser,
generator,
pluginProposalClassProperties,
pluginProposalExportNamespaceFrom,

View file

@ -0,0 +1 @@
module.exports = require('./bundle').eslintParser()

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
module.exports = require('./bundle').eslintParser()

View file

@ -0,0 +1 @@
export class CompileError extends Error {}

View file

@ -0,0 +1,81 @@
import chalk from 'chalk'
import path from 'path'
// eslint-disable-next-line no-shadow
export enum MessageSeverity {
Warning = 1,
Error = 2,
}
interface LintMessage {
ruleId: string | null
severity: 1 | 2
message: string
line: number
column: number
}
interface LintResult {
filePath: string
messages: LintMessage[]
errorCount: number
warningCount: number
output?: string
source?: string
}
function formatMessage(
dir: string,
messages: LintMessage[],
filePath: string
): string | void {
let fileName = path.posix.normalize(
path.relative(dir, filePath).replace(/\\/g, '/')
)
if (!fileName.startsWith('.')) {
fileName = './' + fileName
}
let output = '\n' + chalk.cyan(fileName)
for (let i = 0; i < messages.length; i++) {
const { message, severity, line, column, ruleId } = messages[i]
output = output + '\n'
if (line && column) {
output =
output +
chalk.yellow(line.toString()) +
':' +
chalk.yellow(column.toString()) +
' '
}
if (severity === MessageSeverity.Warning) {
output += chalk.yellow.bold('Warning') + ': '
} else {
output += chalk.red.bold('Error') + ': '
}
output += message
if (ruleId) {
output += ' ' + chalk.gray.bold(ruleId)
}
}
return output
}
export function formatResults(baseDir: string, results: LintResult[]): string {
return (
results
.filter(({ messages }) => messages?.length)
.map(({ messages, filePath }) =>
formatMessage(baseDir, messages, filePath)
)
.join('\n') + '\n'
)
}

View file

@ -0,0 +1,32 @@
import { promises as fs } from 'fs'
import * as CommentJson from 'next/dist/compiled/comment-json'
export type LintIntent = { firstTimeSetup: boolean }
export async function getLintIntent(
eslintrcFile: string | null,
pkgJsonEslintConfig: string | null
): Promise<LintIntent | false> {
if (eslintrcFile) {
const content = await fs.readFile(eslintrcFile, { encoding: 'utf8' }).then(
(txt) => txt.trim().replace(/\n/g, ''),
() => null
)
// User is setting up ESLint for the first time setup if eslint config exists but is empty
return {
firstTimeSetup:
content === '' ||
content === '{}' ||
content === '---' ||
content === 'module.exports = {}',
}
} else if (pkgJsonEslintConfig) {
return {
firstTimeSetup: CommentJson.stringify(pkgJsonEslintConfig) === '{}',
}
}
return false
}

View file

@ -0,0 +1,167 @@
import { promises } from 'fs'
import { extname } from 'path'
import findUp from 'next/dist/compiled/find-up'
import semver from 'next/dist/compiled/semver'
import { formatResults } from './customFormatter'
import { getLintIntent } from './getLintIntent'
import { writeDefaultConfig } from './writeDefaultConfig'
import { getPackageVersion } from '../get-package-version'
import { CompileError } from '../compile-error'
import {
hasNecessaryDependencies,
NecessaryDependencies,
} from '../has-necessary-dependencies'
import * as Log from '../../build/output/log'
type Config = {
plugins: string[]
rules: { [key: string]: Array<number | string> }
}
const linteableFileTypes = ['jsx', 'js', 'ts', 'tsx']
async function lint(
deps: NecessaryDependencies,
baseDir: string,
pagesDir: string,
eslintrcFile: string | null,
pkgJsonPath: string | null
): Promise<string | null> {
// Load ESLint after we're sure it exists:
const { ESLint } = await import(deps.resolved)
if (!ESLint) {
const eslintVersion: string | null = await getPackageVersion({
cwd: baseDir,
name: 'eslint',
})
if (eslintVersion && semver.lt(eslintVersion, '7.0.0')) {
Log.warn(
`Your project has an older version of ESLint installed (${eslintVersion}). Please upgrade to v7 or later to run ESLint during the build process.`
)
}
return null
}
let options: any = {
useEslintrc: true,
baseConfig: {},
}
let eslint = new ESLint(options)
let nextEslintPluginIsEnabled = false
const pagesDirRules = ['@next/next/no-html-link-for-pages']
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 (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 = {}
}
options.baseConfig!.rules[rule] = [1, pagesDir]
updatedPagesDir = true
}
}
if (updatedPagesDir) {
eslint = new ESLint(options)
}
}
const results = await eslint.lintFiles([
`${pagesDir}/**/*.{${linteableFileTypes.join(',')}}`,
])
if (ESLint.getErrorResults(results)?.length > 0) {
throw new CompileError(await formatResults(baseDir, results))
}
return results?.length > 0 ? formatResults(baseDir, results) : null
}
export async function runLintCheck(
baseDir: string,
pagesDir: string
): Promise<string | null> {
try {
// Check if any pages exist that can be linted
const pages = await promises.readdir(pagesDir)
if (
!pages.some((page) =>
linteableFileTypes.includes(extname(page).replace('.', ''))
)
) {
return null
}
// Find user's .eslintrc file
const eslintrcFile =
(await findUp(
[
'.eslintrc.js',
'.eslintrc.yaml',
'.eslintrc.yml',
'.eslintrc.json',
'.eslintrc',
],
{
cwd: baseDir,
}
)) ?? null
const pkgJsonPath = (await findUp('package.json', { cwd: baseDir })) ?? null
const { eslintConfig: pkgJsonEslintConfig = null } = !!pkgJsonPath
? await import(pkgJsonPath!)
: {}
// Check if the project uses ESLint
const eslintIntent = await getLintIntent(eslintrcFile, pkgJsonEslintConfig)
if (!eslintIntent) {
return null
}
const firstTimeSetup = eslintIntent.firstTimeSetup
// Ensure ESLint and necessary plugins and configs are installed:
const deps: NecessaryDependencies = await hasNecessaryDependencies(
baseDir,
false,
!!eslintIntent,
eslintrcFile
)
// Create the user's eslintrc config for them
if (firstTimeSetup) await writeDefaultConfig(eslintrcFile, pkgJsonPath)
// Run ESLint
return await lint(deps, baseDir, pagesDir, eslintrcFile, pkgJsonPath)
} catch (err) {
throw err
}
}

View file

@ -0,0 +1,64 @@
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'
export async function writeDefaultConfig(
eslintrcFile: string | null,
pkgJsonPath: string | null
) {
const defaultConfig = {
extends: 'next',
}
if (eslintrcFile) {
const ext = path.extname(eslintrcFile)
let fileContent
if (ext === '.yaml' || ext === '.yml') {
fileContent = "extends: 'next'"
} else {
fileContent = CommentJson.stringify(defaultConfig, null, 2)
if (ext === '.js') {
fileContent = 'module.exports = ' + fileContent
}
}
await fs.writeFile(eslintrcFile, fileContent + os.EOL)
console.log(
'\n' +
chalk.green(
`We detected ESLint in your project and updated the ${chalk.bold(
path.basename(eslintrcFile)
)} file for you.`
) +
'\n'
)
} else if (pkgJsonPath) {
const pkgJsonContent = await fs.readFile(pkgJsonPath, {
encoding: 'utf8',
})
let packageJsonConfig = CommentJson.parse(pkgJsonContent)
packageJsonConfig.eslintConfig = defaultConfig
await fs.writeFile(
pkgJsonPath,
CommentJson.stringify(packageJsonConfig, null, 2) + os.EOL
)
console.log(
'\n' +
chalk.green(
`We detected ESLint in your project and updated the ${chalk.bold(
'eslintConfig'
)} field for you in package.json...`
) +
'\n'
)
}
}

View file

@ -0,0 +1 @@
export class FatalError extends Error {}

View file

@ -1,23 +1,39 @@
import chalk from 'chalk'
import path from 'path'
import { fileExists } from '../file-exists'
import { getOxfordCommaList } from '../oxford-comma-list'
import { FatalTypeScriptError } from './FatalTypeScriptError'
const requiredPackages = [
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 type NecessaryDependencies = {
resolvedTypeScript: string
resolved: string
}
export async function hasNecessaryDependencies(
baseDir: string
baseDir: string,
checkTSDeps: boolean,
checkESLintDeps: boolean,
eslintrcFile: string | null = null
): Promise<NecessaryDependencies> {
if (!checkTSDeps && !checkESLintDeps) {
return { resolved: undefined! }
}
let resolutions = new Map<string, string>()
let requiredPackages = checkESLintDeps
? requiredLintPackages
: requiredTSPackages
const missingPackages = requiredPackages.filter((p) => {
try {
@ -29,7 +45,11 @@ export async function hasNecessaryDependencies(
})
if (missingPackages.length < 1) {
return { resolvedTypeScript: resolutions.get('typescript')! }
return {
resolved: checkESLintDeps
? resolutions.get('eslint')!
: resolutions.get('typescript')!,
}
}
const packagesHuman = getOxfordCommaList(missingPackages.map((p) => p.pkg))
@ -37,10 +57,26 @@ export async function hasNecessaryDependencies(
const yarnLockFile = path.join(baseDir, 'yarn.lock')
const isYarn = await fileExists(yarnLockFile).catch(() => false)
const removalMsg = checkTSDeps
? 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).'
)
: chalk.bold(
`If you are not trying to use ESLint, please remove the ${
eslintrcFile
? chalk.cyan(path.basename(eslintrcFile)) +
' file from your application'
: chalk.cyan('eslintConfig') + ' field from your package.json file'
}.`
)
throw new FatalTypeScriptError(
throw new FatalError(
chalk.bold.red(
`It looks like you're trying to use TypeScript but do not have the required package(s) installed.`
`It looks like you're trying to use ${
checkTSDeps ? 'TypeScript' : 'ESLint'
} but do not have the required package(s) installed.`
) +
'\n\n' +
chalk.bold(`Please install ${chalk.bold(packagesHuman)} by running:`) +
@ -51,11 +87,7 @@ export async function hasNecessaryDependencies(
packagesCli
)}` +
'\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).'
) +
removalMsg +
'\n'
)
}

View file

@ -1 +0,0 @@
export class FatalTypeScriptError extends Error {}

View file

@ -1 +0,0 @@
export class TypeScriptCompileError extends Error {}

View file

@ -1,7 +1,8 @@
import chalk from 'chalk'
import os from 'os'
import path from 'path'
import { FatalTypeScriptError } from './FatalTypeScriptError'
import { FatalError } from '../fatal-error'
export async function getTypeScriptConfiguration(
ts: typeof import('typescript'),
@ -17,9 +18,7 @@ export async function getTypeScriptConfiguration(
const { config, error } = ts.readConfigFile(tsConfigPath, ts.sys.readFile)
if (error) {
throw new FatalTypeScriptError(
ts.formatDiagnostic(error, formatDiagnosticsHost)
)
throw new FatalError(ts.formatDiagnostic(error, formatDiagnosticsHost))
}
let configToParse: any = config
@ -48,7 +47,7 @@ export async function getTypeScriptConfiguration(
}
if (result.errors?.length) {
throw new FatalTypeScriptError(
throw new FatalError(
ts.formatDiagnostic(result.errors[0], formatDiagnosticsHost)
)
}
@ -57,7 +56,7 @@ export async function getTypeScriptConfiguration(
} catch (err) {
if (err?.name === 'SyntaxError') {
const reason = '\n' + (err?.message ?? '')
throw new FatalTypeScriptError(
throw new FatalError(
chalk.red.bold(
'Could not parse',
chalk.cyan('tsconfig.json') +

View file

@ -3,9 +3,10 @@ import {
getFormattedDiagnostic,
} from './diagnosticFormatter'
import { getTypeScriptConfiguration } from './getTypeScriptConfiguration'
import { TypeScriptCompileError } from './TypeScriptCompileError'
import { getRequiredConfiguration } from './writeConfigurationDefaults'
import { CompileError } from '../compile-error'
export interface TypeCheckResult {
hasWarnings: boolean
warnings?: string[]
@ -55,7 +56,7 @@ export async function runTypeCheck(
) ?? allDiagnostics.find((d) => d.category === DiagnosticCategory.Error)
if (firstError) {
throw new TypeScriptCompileError(
throw new CompileError(
await getFormattedDiagnostic(ts, baseDir, firstError)
)
}

View file

@ -0,0 +1,38 @@
import chalk from 'chalk'
import { Worker } from 'jest-worker'
export async function verifyAndLint(
dir: string,
pagesDir: string,
numWorkers: number | undefined,
enableWorkerThreads: boolean | undefined
): Promise<void> {
try {
const lintWorkers = new Worker(require.resolve('./eslint/runLintCheck'), {
numWorkers,
enableWorkerThreads,
}) as Worker & {
runLintCheck: typeof import('./eslint/runLintCheck').runLintCheck
}
lintWorkers.getStdout().pipe(process.stdout)
lintWorkers.getStderr().pipe(process.stderr)
const lintResults = await lintWorkers.runLintCheck(dir, pagesDir)
if (lintResults) {
console.log(lintResults)
}
lintWorkers.end()
} catch (err) {
if (err.type === 'CompileError') {
console.error(chalk.red('\nFailed to compile.'))
console.error(err.message)
process.exit(1)
} else if (err.type === 'FatalError') {
console.error(err.message)
process.exit(1)
}
throw err
}
}

View file

@ -1,13 +1,14 @@
import chalk from 'chalk'
import path from 'path'
import { FatalTypeScriptError } from './typescript/FatalTypeScriptError'
import { getTypeScriptIntent } from './typescript/getTypeScriptIntent'
import {
hasNecessaryDependencies,
NecessaryDependencies,
} from './typescript/hasNecessaryDependencies'
} from './has-necessary-dependencies'
import { CompileError } from './compile-error'
import { FatalError } from './fatal-error'
import { getTypeScriptIntent } from './typescript/getTypeScriptIntent'
import type { TypeCheckResult } from './typescript/runTypeCheck'
import { TypeScriptCompileError } from './typescript/TypeScriptCompileError'
import { writeAppTypeDeclarations } from './typescript/writeAppTypeDeclarations'
import { writeConfigurationDefaults } from './typescript/writeConfigurationDefaults'
@ -27,12 +28,14 @@ export async function verifyTypeScriptSetup(
const firstTimeSetup = intent.firstTimeSetup
// Ensure TypeScript and necessary `@types/*` are installed:
const deps: NecessaryDependencies = await hasNecessaryDependencies(dir)
const deps: NecessaryDependencies = await hasNecessaryDependencies(
dir,
!!intent,
false
)
// Load TypeScript after we're sure it exists:
const ts = (await import(
deps.resolvedTypeScript
)) as typeof import('typescript')
const ts = (await import(deps.resolved)) as typeof import('typescript')
// Reconfigure (or create) the user's `tsconfig.json` for them:
await writeConfigurationDefaults(ts, tsConfigPath, firstTimeSetup)
@ -49,11 +52,11 @@ export async function verifyTypeScriptSetup(
return true
} catch (err) {
// These are special errors that should not show a stack trace:
if (err instanceof TypeScriptCompileError) {
if (err instanceof CompileError) {
console.error(chalk.red('Failed to compile.\n'))
console.error(err.message)
process.exit(1)
} else if (err instanceof FatalTypeScriptError) {
} else if (err instanceof FatalError) {
console.error(err.message)
process.exit(1)
}

View file

@ -59,6 +59,7 @@ export type NextConfig = { [key: string]: any } & {
skipValidation?: boolean
}
turboMode: boolean
eslint?: boolean
reactRoot: boolean
}
}
@ -114,6 +115,7 @@ export const defaultConfig: NextConfig = {
externalDir: false,
serialWebpackBuild: false,
turboMode: false,
eslint: false,
reactRoot: Number(process.env.NEXT_PRIVATE_REACT_ROOT) > 0,
},
future: {

View file

@ -135,6 +135,7 @@
"@ampproject/toolbox-optimizer": "2.7.1-alpha.0",
"@babel/code-frame": "7.12.11",
"@babel/core": "7.12.10",
"@babel/eslint-parser": "7.13.14",
"@babel/generator": "^7.12.10",
"@babel/plugin-proposal-class-properties": "7.12.1",
"@babel/plugin-proposal-export-namespace-from": "7.12.1",

View file

@ -161,6 +161,7 @@ const babelBundlePackages = {
'@babel/preset-env': 'next/dist/compiled/babel/preset-env',
'@babel/preset-react': 'next/dist/compiled/babel/preset-react',
'@babel/preset-typescript': 'next/dist/compiled/babel/preset-typescript',
'@babel/eslint-parser': 'next/dist/compiled/babel/eslint-parser',
}
Object.assign(externals, babelBundlePackages)

View file

@ -1,7 +1,7 @@
declare -a testCases=(
# Tests the webpack require hook
"progressive-web-app"
"with-eslint"
"with-typescript"
"with-next-sass"
# Tests @next/mdx

View file

@ -1,109 +0,0 @@
const rule = require('@next/eslint-plugin-next/lib/rules/missing-preload')
const RuleTester = require('eslint').RuleTester
RuleTester.setDefaultConfig({
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
modules: true,
jsx: true,
},
},
})
var ruleTester = new RuleTester()
ruleTester.run('missing-preload', rule, {
valid: [
`import {Head} from 'next/document';
export class Blah extends Head {
render() {
return (
<div>
<h1>Hello title</h1>
<link href="/_next/static/css/styles.css" rel="preload" />
<link href="/_next/static/css/styles.css" rel="stylesheet" />
</div>
);
}
}`,
`import {Head} from 'next/document';
export class Blah extends Head {
render() {
return (
<div>
<h1>Hello title</h1>
<link href="/_next/static/css/styles.css" rel="stylesheet" media="print" />
</div>
);
}
}`,
`import {Head} from 'next/head';
export class Blah {
render() {
return (
<div>
<div>
<Head><link href="/_next/static/css/styles.css" rel="preload" /></Head>
</div>
<h1>Hello title</h1>
<Head><link href="/_next/static/css/styles.css" rel="stylesheet" /></Head>
</div>
);
}
}`,
],
invalid: [
{
code: `
import {Head} from 'next/document';
export class Blah extends Head {
render() {
return (
<div>
<h1>Hello title</h1>
<link href="/_next/static/css/styles.css" rel="stylesheet" />
</div>
);
}
}`,
errors: [
{
message:
'Stylesheet does not have an associated preload tag. This could potentially impact First paint.',
type: 'JSXOpeningElement',
},
],
output: `
import {Head} from 'next/document';
export class Blah extends Head {
render() {
return (
<div>
<h1>Hello title</h1>
<link rel="preload" href="/_next/static/css/styles.css" as="style" /><link href="/_next/static/css/styles.css" rel="stylesheet" />
</div>
);
}
}`,
},
{
code: `
<div>
<link href="/_next/static/css/styles.css" rel="stylesheet" />
</div>`,
errors: [
{
message:
'Stylesheet does not have an associated preload tag. This could potentially impact First paint.',
type: 'JSXOpeningElement',
},
],
output: `
<div>
<link rel="preload" href="/_next/static/css/styles.css" as="style" /><link href="/_next/static/css/styles.css" rel="stylesheet" />
</div>`,
},
],
})

View file

@ -82,7 +82,7 @@ ruleTester.run('no-css-tags', rule, {
errors: [
{
message:
'In order to use external stylesheets use @import in the root stylesheet compiled with NextJS. This ensures proper priority to CSS when loading a webpage.',
'Do not include stylesheets manually. See: https://nextjs.org/docs/messages/no-css-tags.',
type: 'JSXOpeningElement',
},
],
@ -95,7 +95,7 @@ ruleTester.run('no-css-tags', rule, {
errors: [
{
message:
'In order to use external stylesheets use @import in the root stylesheet compiled with NextJS. This ensures proper priority to CSS when loading a webpage.',
'Do not include stylesheets manually. See: https://nextjs.org/docs/messages/no-css-tags.',
type: 'JSXOpeningElement',
},
],

View file

@ -129,7 +129,7 @@ describe('no-html-link-for-pages', function () {
assert.notEqual(report, undefined, 'No lint errors found.')
assert.equal(
report.message,
"You're using <a> tag to navigate to /. Use Link from 'next/link' to make sure the app behaves like an SPA."
"Do not use the HTML <a> tag to navigate to /. Use Link from 'next/link' instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages."
)
})
@ -140,7 +140,7 @@ describe('no-html-link-for-pages', function () {
assert.notEqual(report, undefined, 'No lint errors found.')
assert.equal(
report.message,
"You're using <a> tag to navigate to /list/blah/. Use Link from 'next/link' to make sure the app behaves like an SPA."
"Do not use the HTML <a> tag to navigate to /list/blah/. Use Link from 'next/link' instead. See: https://nextjs.org/docs/messages/no-html-link-for-pages."
)
})
})

View file

@ -60,7 +60,7 @@ ruleTester.run('sync-scripts', rule, {
errors: [
{
message:
"A synchronous script tag can impact your webpage's performance",
'Synchronous scripts are forbidden. See: https://nextjs.org/docs/messages/no-sync-scripts.',
type: 'JSXOpeningElement',
},
],
@ -82,7 +82,7 @@ ruleTester.run('sync-scripts', rule, {
errors: [
{
message:
"A synchronous script tag can impact your webpage's performance",
'Synchronous scripts are forbidden. See: https://nextjs.org/docs/messages/no-sync-scripts.',
type: 'JSXOpeningElement',
},
],

View file

@ -59,7 +59,7 @@ ruleTester.run('unwanted-polyfillsio', rule, {
errors: [
{
message:
"You're requesting polyfills from polyfill.io which are already shipped with NextJS. Please remove WeakSet, Promise, Promise.prototype.finally, es2015, es5, es6 from the features list.",
'No duplicate polyfills from Polyfill.io are allowed. WeakSet, Promise, Promise.prototype.finally, es2015, es5, es6 are already shipped with Next.js. See: https://nextjs.org/docs/messages/no-unwanted-polyfillio.',
type: 'JSXOpeningElement',
},
],
@ -79,7 +79,7 @@ ruleTester.run('unwanted-polyfillsio', rule, {
errors: [
{
message:
"You're requesting polyfills from polyfill.io which are already shipped with NextJS. Please remove Array.prototype.copyWithin from the features list.",
'No duplicate polyfills from Polyfill.io are allowed. Array.prototype.copyWithin is already shipped with Next.js. See: https://nextjs.org/docs/messages/no-unwanted-polyfillio.',
type: 'JSXOpeningElement',
},
],

View file

@ -0,0 +1,8 @@
{
"extends": "next",
"root": true,
"rules": {
"@next/next/no-html-link-for-pages": 0,
"@next/next/no-sync-scripts": 2
}
}

View file

@ -0,0 +1 @@
module.exports = { experimental: { eslint: true } }

View file

@ -0,0 +1,10 @@
{
"name": "eslint-custom-config",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"devDependencies": {
"eslint-config-next": "*",
"eslint": "7.23.0"
}
}

View file

@ -0,0 +1,8 @@
const Home = () => (
<div>
<p>Home</p>
/* Badly formatted comment */
</div>
)
export default Home

View file

@ -0,0 +1,3 @@
{
"extends": "next"
}

View file

@ -0,0 +1 @@
module.exports = { experimental: { eslint: true } }

View file

@ -0,0 +1,5 @@
{
"devDependencies": {
"eslint-config-next": "*"
}
}

View file

@ -0,0 +1,9 @@
export default class Test {
render() {
return (
<div>
<h1>Hello title</h1>
</div>
)
}
}

View file

@ -0,0 +1,68 @@
import { join } from 'path'
import { runNextCommand } from 'next-test-utils'
import { writeFile, readFile } from 'fs-extra'
import semver from 'next/dist/compiled/semver'
jest.setTimeout(1000 * 60 * 2)
const dirFirstTimeSetup = join(__dirname, '../first-time-setup')
const dirCustomConfig = join(__dirname, '../custom-config')
async function eslintVersion() {
const eslint = require.resolve('eslint')
const { ESLint, Linter } = await import(eslint)
if (!ESLint && !Linter) return null // A very old version (<v4) if both ESLint and Linter properties are not present
return ESLint ? ESLint.version : Linter.version
}
describe('ESLint', () => {
it('should populate eslint config automatically for first time setup', async () => {
const eslintrc = join(dirFirstTimeSetup, '.eslintrc')
await writeFile(eslintrc, '')
const { stdout } = await runNextCommand(['build', dirFirstTimeSetup], {
stdout: true,
})
const eslintrcContent = await readFile(eslintrc, 'utf8')
expect(stdout).toContain(
'We detected ESLint in your project and updated the .eslintrc file for you.'
)
expect(eslintrcContent.trim().replace(/\s/g, '')).toMatch(
'{"extends":"next"}'
)
})
test('shows warnings and errors', async () => {
let output = ''
const { stdout, stderr } = await runNextCommand(
['build', dirCustomConfig],
{
stdout: true,
stderr: true,
}
)
output = stdout + stderr
const version = await eslintVersion()
if (!version || (version && semver.lt(version, '7.0.0'))) {
expect(output).toContain(
'Your project has an older version of ESLint installed'
)
expect(output).toContain(
'Please upgrade to v7 or later to run ESLint during the build process'
)
} else {
expect(output).toContain('Failed to compile')
expect(output).toContain(
'Error: Comments inside children section of tag should be placed inside braces'
)
}
})
})

View file

@ -93,6 +93,15 @@
semver "^5.4.1"
source-map "^0.5.0"
"@babel/eslint-parser@7.13.14":
version "7.13.14"
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.13.14.tgz#f80fd23bdd839537221914cb5d17720a5ea6ba3a"
integrity sha512-I0HweR36D73Ibn/FfrRDMKlMqJHFwidIUgYdMpH+aXYuQC+waq59YaJ6t9e9N36axJ82v1jR041wwqDrDXEwRA==
dependencies:
eslint-scope "^5.1.0"
eslint-visitor-keys "^1.3.0"
semver "^6.3.0"
"@babel/generator@^7.12.10", "@babel/generator@^7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af"
@ -2374,6 +2383,11 @@
call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0"
"@next/eslint-plugin-next@^10.1.3":
version "10.1.3"
resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-10.1.3.tgz#739743aa33aed28d97e670b9f80b84440cf2a3c7"
integrity sha512-KrZUb6cHXt/rPhN9bSrlVLAq+9LyNOWurqbrUww3OXmrFlXBDI8W6Z0ToDkQMkIH6gTtmpGbuctxCkQ2pgeXzQ==
"@nodelib/fs.scandir@2.1.3":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b"
@ -2695,6 +2709,11 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@rushstack/eslint-patch@^1.0.6":
version "1.0.6"
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.0.6.tgz#023d72a5c4531b4ce204528971700a78a85a0c50"
integrity sha512-Myxw//kzromB9yWgS8qYGuGVf91oBUUJpNvy5eM50sqvmKLbKjwLxohJnkWGTeeI9v9IBMtPLxz5Gc60FIfvCA==
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
@ -3342,7 +3361,7 @@
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
"@typescript-eslint/parser@4.22.0":
"@typescript-eslint/parser@4.22.0", "@typescript-eslint/parser@^4.20.0":
version "4.22.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.22.0.tgz#e1637327fcf796c641fe55f73530e90b16ac8fe8"
integrity sha512-z/bGdBJJZJN76nvAY9DkJANYgK3nlRstRRi74WHm3jjgf2I8AglrSY+6l7ogxOmn55YJ6oKZCLLy+6PW70z15Q==
@ -6283,6 +6302,7 @@ deep-extend@^0.6.0:
deep-is@^0.1.3, deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
deepmerge@^4.2.2:
version "4.2.2"
@ -6899,7 +6919,7 @@ eslint-module-utils@^2.6.0:
debug "^2.6.9"
pkg-dir "^2.0.0"
eslint-plugin-import@2.22.1:
eslint-plugin-import@2.22.1, eslint-plugin-import@^2.22.1:
version "2.22.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702"
integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw==
@ -6925,12 +6945,12 @@ eslint-plugin-jest@24.3.5:
dependencies:
"@typescript-eslint/experimental-utils" "^4.0.1"
eslint-plugin-react-hooks@4.2.0:
eslint-plugin-react-hooks@4.2.0, eslint-plugin-react-hooks@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556"
integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==
eslint-plugin-react@7.23.2:
eslint-plugin-react@7.23.2, eslint-plugin-react@^7.23.1:
version "7.23.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.23.2.tgz#2d2291b0f95c03728b55869f01102290e792d494"
integrity sha512-AfjgFQB+nYszudkxRkTFu0UR1zEQig0ArVMPloKhxwlwkzaw/fBiH0QWcBBhZONlXqQC51+nfqFrkn4EzHcGBw==
@ -6963,7 +6983,7 @@ eslint-scope@^5.0.0:
esrecurse "^4.1.0"
estraverse "^4.1.1"
eslint-scope@^5.1.1:
eslint-scope@^5.1.0, eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
@ -7394,6 +7414,7 @@ fast-json-stable-stringify@^2.0.0:
fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fastparse@^1.1.1:
version "1.1.2"
@ -10207,6 +10228,7 @@ levn@^0.4.1:
levn@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=
dependencies:
prelude-ls "~1.1.2"
type-check "~0.3.2"
@ -11883,6 +11905,7 @@ optimize-css-assets-webpack-plugin@^5.0.1:
optionator@^0.8.1:
version "0.8.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
dependencies:
deep-is "~0.1.3"
fast-levenshtein "~2.0.6"
@ -16798,6 +16821,7 @@ wide-align@^1.1.0:
word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
wordwrap@^1.0.0:
version "1.0.0"