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:
parent
49cd08da17
commit
e783b0a2e8
59 changed files with 1122 additions and 358 deletions
|
@ -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
|
||||
|
|
|
@ -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
38
errors/no-css-tags.md
Normal 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.
|
45
errors/no-html-link-for-pages.md
Normal file
45
errors/no-html-link-for-pages.md
Normal 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
32
errors/no-sync-scripts.md
Normal 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/)
|
13
errors/no-unwanted-polyfillio.md
Normal file
13
errors/no-unwanted-polyfillio.md
Normal 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)
|
4
examples/with-eslint/.eslintrc
Normal file
4
examples/with-eslint/.eslintrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "next",
|
||||
"root": true
|
||||
}
|
34
examples/with-eslint/.gitignore
vendored
Normal file
34
examples/with-eslint/.gitignore
vendored
Normal 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
|
23
examples/with-eslint/README.md
Normal file
23
examples/with-eslint/README.md
Normal 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)).
|
5
examples/with-eslint/next.config.js
Normal file
5
examples/with-eslint/next.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
experimental: {
|
||||
eslint: true,
|
||||
},
|
||||
}
|
19
examples/with-eslint/package.json
Normal file
19
examples/with-eslint/package.json
Normal 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"
|
||||
}
|
||||
}
|
8
examples/with-eslint/pages/index.js
Normal file
8
examples/with-eslint/pages/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
const Home = () => (
|
||||
<div>
|
||||
<script src="https://fake-script.com" />
|
||||
<p>Home</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Home
|
55
packages/eslint-config-next/index.js
Normal file
55
packages/eslint-config-next/index.js
Normal 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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
30
packages/eslint-config-next/package.json
Normal file
30
packages/eslint-config-next/package.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
9
packages/eslint-config-next/parser.js
Normal file
9
packages/eslint-config-next/parser.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
const {
|
||||
parse,
|
||||
parseForESLint,
|
||||
} = require('next/dist/compiled/babel/eslint-parser')
|
||||
|
||||
module.exports = {
|
||||
parse,
|
||||
parseForESLint,
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
|
@ -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.',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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.`,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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.',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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`,
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
1
packages/next/bundles/babel/packages/eslint-parser.js
Normal file
1
packages/next/bundles/babel/packages/eslint-parser.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require('./bundle').eslintParser()
|
File diff suppressed because one or more lines are too long
1
packages/next/compiled/babel/eslint-parser.js
Normal file
1
packages/next/compiled/babel/eslint-parser.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = require('./bundle').eslintParser()
|
1
packages/next/lib/compile-error.ts
Normal file
1
packages/next/lib/compile-error.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export class CompileError extends Error {}
|
81
packages/next/lib/eslint/customFormatter.ts
Normal file
81
packages/next/lib/eslint/customFormatter.ts
Normal 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'
|
||||
)
|
||||
}
|
32
packages/next/lib/eslint/getLintIntent.ts
Normal file
32
packages/next/lib/eslint/getLintIntent.ts
Normal 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
|
||||
}
|
167
packages/next/lib/eslint/runLintCheck.ts
Normal file
167
packages/next/lib/eslint/runLintCheck.ts
Normal 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
|
||||
}
|
||||
}
|
64
packages/next/lib/eslint/writeDefaultConfig.ts
Normal file
64
packages/next/lib/eslint/writeDefaultConfig.ts
Normal 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'
|
||||
)
|
||||
}
|
||||
}
|
1
packages/next/lib/fatal-error.ts
Normal file
1
packages/next/lib/fatal-error.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export class FatalError extends Error {}
|
|
@ -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'
|
||||
)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export class FatalTypeScriptError extends Error {}
|
|
@ -1 +0,0 @@
|
|||
export class TypeScriptCompileError extends Error {}
|
|
@ -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') +
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
38
packages/next/lib/verifyAndLint.ts
Normal file
38
packages/next/lib/verifyAndLint.ts
Normal 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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>`,
|
||||
},
|
||||
],
|
||||
})
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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."
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
|
|
8
test/integration/eslint/custom-config/.eslintrc
Normal file
8
test/integration/eslint/custom-config/.eslintrc
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "next",
|
||||
"root": true,
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": 0,
|
||||
"@next/next/no-sync-scripts": 2
|
||||
}
|
||||
}
|
1
test/integration/eslint/custom-config/next.config.js
Normal file
1
test/integration/eslint/custom-config/next.config.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = { experimental: { eslint: true } }
|
10
test/integration/eslint/custom-config/package.json
Normal file
10
test/integration/eslint/custom-config/package.json
Normal 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"
|
||||
}
|
||||
}
|
8
test/integration/eslint/custom-config/pages/index.js
Normal file
8
test/integration/eslint/custom-config/pages/index.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
const Home = () => (
|
||||
<div>
|
||||
<p>Home</p>
|
||||
/* Badly formatted comment */
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Home
|
3
test/integration/eslint/first-time-setup/.eslintrc
Normal file
3
test/integration/eslint/first-time-setup/.eslintrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next"
|
||||
}
|
1
test/integration/eslint/first-time-setup/next.config.js
Normal file
1
test/integration/eslint/first-time-setup/next.config.js
Normal file
|
@ -0,0 +1 @@
|
|||
module.exports = { experimental: { eslint: true } }
|
5
test/integration/eslint/first-time-setup/package.json
Normal file
5
test/integration/eslint/first-time-setup/package.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"eslint-config-next": "*"
|
||||
}
|
||||
}
|
9
test/integration/eslint/first-time-setup/pages/index.js
Normal file
9
test/integration/eslint/first-time-setup/pages/index.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
export default class Test {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello title</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
68
test/integration/eslint/test/index.test.js
Normal file
68
test/integration/eslint/test/index.test.js
Normal 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'
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
34
yarn.lock
34
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue