From 44072205d83eca4add74507397fa894fba6ad3c9 Mon Sep 17 00:00:00 2001 From: Houssein Djirdeh Date: Mon, 10 May 2021 15:08:14 -0400 Subject: [PATCH 1/4] ESLint Plugin: Custom Font at page-level rule (#24789) Adds a lint rule warning to the Next.js ESLint plugin if a custom Google Font is added at page-level instead of with a custom document (`.document.js`) _Note: This will be generalized to include more font providers in the near future._ --- errors/manifest.json | 4 + errors/no-page-custom-font.md | 45 +++++++++ packages/eslint-plugin-next/lib/index.js | 2 + .../lib/rules/no-page-custom-font.js | 55 +++++++++++ .../no-page-custom-font.unit.test.js | 95 +++++++++++++++++++ 5 files changed, 201 insertions(+) create mode 100644 errors/no-page-custom-font.md create mode 100644 packages/eslint-plugin-next/lib/rules/no-page-custom-font.js create mode 100644 test/eslint-plugin-next/no-page-custom-font.unit.test.js diff --git a/errors/manifest.json b/errors/manifest.json index b0ccb3e18df37..f6fb25c29936f 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -271,6 +271,10 @@ "title": "no-on-app-updated-hook", "path": "/errors/no-on-app-updated-hook.md" }, + { + "title": "no-page-custom-font", + "path": "/errors/no-page-custom-font.md" + }, { "title": "no-router-instance", "path": "/errors/no-router-instance.md" diff --git a/errors/no-page-custom-font.md b/errors/no-page-custom-font.md new file mode 100644 index 0000000000000..9dc8288b2cd57 --- /dev/null +++ b/errors/no-page-custom-font.md @@ -0,0 +1,45 @@ +# No Page Custom Font + +### Why This Error Occurred + +A custom font was added to a page and not with a custom `Document`. This only adds the font to the specific page and not to the entire application. + +### Possible Ways to Fix It + +Create the file `./pages/document.js` and add the font to a custom Document: + +```jsx +// pages/_document.js + +import Document, { Html, Head, Main, NextScript } from 'next/document' + +class MyDocument extends Document { + render() { + return ( + + + + + +
+ + + + ) + } +} + +export default MyDocument +``` + +### When Not To Use It + +If you have a reason to only load a font for a particular page, then you can disable this rule. + +### Useful Links + +- [Custom Document](https://nextjs.org/docs/advanced-features/custom-document) +- [Font Optimization](https://nextjs.org/docs/basic-features/font-optimization) diff --git a/packages/eslint-plugin-next/lib/index.js b/packages/eslint-plugin-next/lib/index.js index bcc6f65bf7fee..ebc0e0f6ef77f 100644 --- a/packages/eslint-plugin-next/lib/index.js +++ b/packages/eslint-plugin-next/lib/index.js @@ -4,6 +4,7 @@ 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'), + 'no-page-custom-font': require('./rules/no-page-custom-font'), 'no-title-in-document-head': require('./rules/no-title-in-document-head'), 'google-font-display': require('./rules/google-font-display'), 'google-font-preconnect': require('./rules/google-font-preconnect'), @@ -17,6 +18,7 @@ module.exports = { '@next/next/no-sync-scripts': 1, '@next/next/no-html-link-for-pages': 1, '@next/next/no-unwanted-polyfillio': 1, + '@next/next/no-page-custom-font': 1, '@next/next/no-title-in-document-head': 1, '@next/next/google-font-display': 1, '@next/next/google-font-preconnect': 1, diff --git a/packages/eslint-plugin-next/lib/rules/no-page-custom-font.js b/packages/eslint-plugin-next/lib/rules/no-page-custom-font.js new file mode 100644 index 0000000000000..15a6d8aba8f05 --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/no-page-custom-font.js @@ -0,0 +1,55 @@ +const NodeAttributes = require('../utils/nodeAttributes.js') + +module.exports = { + meta: { + docs: { + description: + 'Recommend adding custom font in a custom document and not in a specific page', + recommended: true, + }, + }, + create: function (context) { + let documentImport = false + return { + ImportDeclaration(node) { + if (node.source.value === 'next/document') { + if (node.specifiers.some(({ local }) => local.name === 'Document')) { + documentImport = true + } + } + }, + JSXOpeningElement(node) { + const documentClass = context + .getAncestors() + .find( + (ancestorNode) => + ancestorNode.type === 'ClassDeclaration' && + ancestorNode.superClass && + ancestorNode.superClass.name === 'Document' + ) + + if ((documentImport && documentClass) || node.name.name !== 'link') { + return + } + + const attributes = new NodeAttributes(node) + if (!attributes.has('href') || !attributes.hasValue('href')) { + return + } + + const hrefValue = attributes.value('href') + const isGoogleFont = hrefValue.includes( + 'https://fonts.googleapis.com/css' + ) + + if (isGoogleFont) { + context.report({ + node, + message: + 'Custom fonts should be added at the document level. See https://nextjs.org/docs/messages/no-page-custom-font.', + }) + } + }, + } + }, +} diff --git a/test/eslint-plugin-next/no-page-custom-font.unit.test.js b/test/eslint-plugin-next/no-page-custom-font.unit.test.js new file mode 100644 index 0000000000000..064f55fe74210 --- /dev/null +++ b/test/eslint-plugin-next/no-page-custom-font.unit.test.js @@ -0,0 +1,95 @@ +const rule = require('@next/eslint-plugin-next/lib/rules/no-page-custom-font') +const RuleTester = require('eslint').RuleTester + +RuleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) + +var ruleTester = new RuleTester() +ruleTester.run('no-page-custom-font', rule, { + valid: [ + `import Document, { Html, Head } from "next/document"; + + class MyDocument extends Document { + render() { + return ( + + + + + + ); + } + } + + export default MyDocument; + `, + ], + + invalid: [ + { + code: ` + import Head from 'next/head' + + export default function IndexPage() { + return ( +
+ + + +

Hello world!

+
+ ) + } + `, + errors: [ + { + message: + 'Custom fonts should be added at the document level. See https://nextjs.org/docs/messages/no-page-custom-font.', + type: 'JSXOpeningElement', + }, + ], + }, + { + code: ` + import Document, { Html, Head } from "next/document"; + + class MyDocument { + render() { + return ( + + + + + + ); + } + } + + export default MyDocument;`, + errors: [ + { + message: + 'Custom fonts should be added at the document level. See https://nextjs.org/docs/messages/no-page-custom-font.', + type: 'JSXOpeningElement', + }, + ], + }, + ], +}) From ea9fe972fb22f5afcd1bbb436b52f3836bc7d4c7 Mon Sep 17 00:00:00 2001 From: Mike Plis Date: Mon, 10 May 2021 15:59:49 -0400 Subject: [PATCH 2/4] Fix build in blog-starter-typescript example (#24695) This example currently fails to build with the error: ``` -> % yarn build yarn run v1.22.10 $ next build warn - React 17.0.1 or newer will be required to leverage all of the upcoming features in Next.js 11. Read more: https://nextjs.org/docs/messages/react-version info - Using webpack 5. Reason: no next.config.js https://nextjs.org/docs/messages/webpack5 Failed to compile. ./components/alert.tsx:2:16 Type error: Could not find a declaration file for module 'classnames'. '/Users/mike/workspace/blog/node_modules/classnames/index.js' implicitly has an 'any' type. Try `npm i --save-dev @types/classnames` if it exists or add a new declaration (.d.ts) file containing `declare module 'classnames';` 1 | import Container from './container' > 2 | import cn from 'classnames' | ^ 3 | import { EXAMPLE_PATH } from '../lib/constants' 4 | 5 | type Props = { error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. ``` As of [`v2.3.1`](https://github.com/JedWatson/classnames/blob/master/HISTORY.md#v230--2021-04-01), the `classnames` package started providing its own types, so the `@types/classnames` package [became a stub](https://unpkg.com/browse/@types/classnames@2.3.1/). We get the error because the `classnames` version is pinned to the old, type-less version, while `@types/classnames` picked up the new stubbed version. Removing `@types/classnames` and updating `classnames` to the latest version fixes the build error. ## Documentation / Examples - [x] Make sure the linting passes --- examples/blog-starter-typescript/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/blog-starter-typescript/package.json b/examples/blog-starter-typescript/package.json index 1a0e3a0536cbe..8ccc30f00ee90 100644 --- a/examples/blog-starter-typescript/package.json +++ b/examples/blog-starter-typescript/package.json @@ -8,7 +8,7 @@ "typecheck": "tsc" }, "dependencies": { - "classnames": "2.2.6", + "classnames": "2.3.1", "date-fns": "2.10.0", "gray-matter": "4.0.2", "next": "latest", @@ -19,7 +19,6 @@ "typescript": "^4.0.3" }, "devDependencies": { - "@types/classnames": "^2.2.10", "@types/jest": "^25.2.2", "@types/node": "^14.0.1", "@types/react": "^16.9.35", From a18c3c24933855f7611514427144252e38baa0a9 Mon Sep 17 00:00:00 2001 From: Henrik Wenz Date: Mon, 10 May 2021 22:23:25 +0200 Subject: [PATCH 3/4] Update with-three-js example (#24857) ## Changes - Update dependencies - Use new `useAnimations` hook - Remove next-transpile-modules in favour of drei - Refactor Components - Remove dynamic import - Enable webpack5 (by removing `next.config.js`) - Removed missing key warning ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. ## Documentation / Examples - [x] Make sure the linting passes --- examples/with-three-js/components/Bird.js | 38 ++++++------- examples/with-three-js/components/Box.js | 29 ++++++++++ examples/with-three-js/next.config.js | 3 -- examples/with-three-js/package.json | 15 +++--- examples/with-three-js/pages/birds.js | 65 +++++++++++------------ examples/with-three-js/pages/boxes.js | 58 ++++++-------------- examples/with-three-js/pages/index.js | 4 +- 7 files changed, 99 insertions(+), 113 deletions(-) create mode 100644 examples/with-three-js/components/Box.js delete mode 100644 examples/with-three-js/next.config.js diff --git a/examples/with-three-js/components/Bird.js b/examples/with-three-js/components/Bird.js index 024dcd5f06b9d..990d6f5f86188 100644 --- a/examples/with-three-js/components/Bird.js +++ b/examples/with-three-js/components/Bird.js @@ -1,38 +1,36 @@ -import { useRef, useState, useEffect } from 'react' -import * as THREE from 'three' +import { useEffect } from 'react' +import { useFrame } from '@react-three/fiber' +import { useAnimations, useGLTF } from '@react-three/drei' -import { useFrame, useLoader } from 'react-three-fiber' -import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' +export default function Bird({ speed, factor, url, ...props }) { + const { nodes, animations } = useGLTF(url) + const { ref, mixer } = useAnimations(animations) -const Bird = ({ speed, factor, url, ...props }) => { - const gltf = useLoader(GLTFLoader, url) - const group = useRef() - const [mixer] = useState(() => new THREE.AnimationMixer()) - - useEffect( - () => void mixer.clipAction(gltf.animations[0], group.current).play(), - [gltf.animations, mixer] - ) + useEffect(() => void mixer.clipAction(animations[0], ref.current).play(), [ + mixer, + animations, + ref, + ]) useFrame((state, delta) => { - group.current.rotation.y += + ref.current.rotation.y += Math.sin((delta * factor) / 2) * Math.cos((delta * factor) / 2) * 1.5 mixer.update(delta * speed) }) return ( - + - + @@ -40,5 +38,3 @@ const Bird = ({ speed, factor, url, ...props }) => { ) } - -export default Bird diff --git a/examples/with-three-js/components/Box.js b/examples/with-three-js/components/Box.js new file mode 100644 index 0000000000000..7b1ae2df63af8 --- /dev/null +++ b/examples/with-three-js/components/Box.js @@ -0,0 +1,29 @@ +import { useRef, useState } from 'react' +import { useFrame } from '@react-three/fiber' +import { Box as NativeBox } from '@react-three/drei' + +export default function Box(props) { + const mesh = useRef() + + const [hovered, setHover] = useState(false) + const [active, setActive] = useState(false) + + useFrame(() => (mesh.current.rotation.x = mesh.current.rotation.y += 0.01)) + + return ( + setActive(!active)} + onPointerOver={() => setHover(true)} + onPointerOut={() => setHover(false)} + > + + + ) +} diff --git a/examples/with-three-js/next.config.js b/examples/with-three-js/next.config.js deleted file mode 100644 index a5dda6cbfc5d8..0000000000000 --- a/examples/with-three-js/next.config.js +++ /dev/null @@ -1,3 +0,0 @@ -const withTM = require('next-transpile-modules')(['@react-three/drei', 'three']) - -module.exports = withTM() diff --git a/examples/with-three-js/package.json b/examples/with-three-js/package.json index eaf073e0b45b5..bf1c405de7649 100644 --- a/examples/with-three-js/package.json +++ b/examples/with-three-js/package.json @@ -8,15 +8,12 @@ "start": "next start" }, "dependencies": { - "@react-three/drei": "3.8.6", - "next": "10.0.7", - "react": "17.0.1", - "react-dom": "17.0.1", - "react-three-fiber": "5.3.19", - "three": "0.126.0" - }, - "devDependencies": { - "next-transpile-modules": "6.3.0" + "@react-three/drei": "4.3.3", + "@react-three/fiber": "6.0.19", + "next": "10.2.0", + "react": "17.0.2", + "react-dom": "17.0.2", + "three": "0.128.0" }, "license": "MIT" } diff --git a/examples/with-three-js/pages/birds.js b/examples/with-three-js/pages/birds.js index 7a97047ddb65f..6d2245e0cfeca 100644 --- a/examples/with-three-js/pages/birds.js +++ b/examples/with-three-js/pages/birds.js @@ -1,38 +1,9 @@ -import dynamic from 'next/dynamic' import { Suspense } from 'react' -import { Canvas } from 'react-three-fiber' +import { Canvas } from '@react-three/fiber' import { OrbitControls } from '@react-three/drei' +import Bird from '../components/Bird' -const Bird = dynamic(() => import('../components/Bird'), { ssr: false }) - -const Birds = () => { - return new Array(5).fill().map((_, i) => { - const x = (15 + Math.random() * 30) * (Math.round(Math.random()) ? -1 : 1) - const y = -10 + Math.random() * 20 - const z = -5 + Math.random() * 10 - const bird = ['stork', 'parrot', 'flamingo'][Math.round(Math.random() * 2)] - let speed = bird === 'stork' ? 0.5 : bird === 'flamingo' ? 2 : 5 - let factor = - bird === 'stork' - ? 0.5 + Math.random() - : bird === 'flamingo' - ? 0.25 + Math.random() - : 1 + Math.random() - 0.5 - - return ( - 0 ? Math.PI : 0, 0]} - speed={speed} - factor={factor} - url={`/glb/${bird}.glb`} - /> - ) - }) -} - -const BirdsPage = (props) => { +export default function BirdsPage() { return ( <> @@ -40,11 +11,35 @@ const BirdsPage = (props) => { - + {new Array(6).fill().map((_, i) => { + const x = + (15 + Math.random() * 30) * (Math.round(Math.random()) ? -1 : 1) + const y = -10 + Math.random() * 20 + const z = -5 + Math.random() * 10 + const bird = ['stork', 'parrot', 'flamingo'][ + Math.round(Math.random() * 2) + ] + let speed = bird === 'stork' ? 0.5 : bird === 'flamingo' ? 2 : 5 + let factor = + bird === 'stork' + ? 0.5 + Math.random() + : bird === 'flamingo' + ? 0.25 + Math.random() + : 1 + Math.random() - 0.5 + + return ( + 0 ? Math.PI : 0, 0]} + speed={speed} + factor={factor} + url={`/glb/${bird}.glb`} + /> + ) + })} ) } - -export default BirdsPage diff --git a/examples/with-three-js/pages/boxes.js b/examples/with-three-js/pages/boxes.js index 6c44b90c8851d..bfc19fc390773 100644 --- a/examples/with-three-js/pages/boxes.js +++ b/examples/with-three-js/pages/boxes.js @@ -1,46 +1,20 @@ -import { useRef, useState } from 'react' -import { Canvas, useFrame } from 'react-three-fiber' -import { OrbitControls, Box } from '@react-three/drei' - -const MyBox = (props) => { - const mesh = useRef() - - const [hovered, setHover] = useState(false) - const [active, setActive] = useState(false) - - useFrame(() => (mesh.current.rotation.x = mesh.current.rotation.y += 0.01)) +import { Canvas } from '@react-three/fiber' +import { OrbitControls } from '@react-three/drei' +import Box from '../components/Box' +export default function BoxesPage() { return ( - setActive(!active)} - onPointerOver={() => setHover(true)} - onPointerOut={() => setHover(false)} - > - - + <> +

Click on me - Hover me :)

+ + + + + + + + + + ) } - -const BoxesPage = () => { - return [ -

Click on me - Hover me :)

, - - - - - - - - - , - ] -} - -export default BoxesPage diff --git a/examples/with-three-js/pages/index.js b/examples/with-three-js/pages/index.js index 5fb26f1b44ef1..bbab8905619f1 100644 --- a/examples/with-three-js/pages/index.js +++ b/examples/with-three-js/pages/index.js @@ -1,6 +1,6 @@ import Link from 'next/link' -const Index = () => { +export default function IndexPage() { return (
@@ -12,5 +12,3 @@ const Index = () => {
) } - -export default Index From 6d0150f02e750a71a3cf1c8d570f380008d68f2f Mon Sep 17 00:00:00 2001 From: Houssein Djirdeh Date: Mon, 10 May 2021 17:28:06 -0400 Subject: [PATCH 4/4] ESLint Plugin: Prevent bad imports of next/document and next/head (#24832) Adds lint rules to the Next.js ESLint plugin to: - Disallow importing `next/head` inside `pages/_document.js` - Disallow importing `next/document` outside of `pages/_document.js` Both rules will be surfaced as **errors** within the recommended config of the plugin. Fixes #13712 #13958 --- errors/manifest.json | 8 ++ errors/no-document-import-in-page.md | 24 +++++ errors/no-head-import-in-document.md | 34 +++++++ packages/eslint-plugin-next/lib/index.js | 4 + .../lib/rules/no-document-import-in-page.js | 30 +++++++ .../lib/rules/no-head-import-in-document.js | 29 ++++++ .../no-document-import-in-page.unit.test.js | 66 ++++++++++++++ .../no-head-import-in-document.unit.test.js | 88 +++++++++++++++++++ 8 files changed, 283 insertions(+) create mode 100644 errors/no-document-import-in-page.md create mode 100644 errors/no-head-import-in-document.md create mode 100644 packages/eslint-plugin-next/lib/rules/no-document-import-in-page.js create mode 100644 packages/eslint-plugin-next/lib/rules/no-head-import-in-document.js create mode 100644 test/eslint-plugin-next/no-document-import-in-page.unit.test.js create mode 100644 test/eslint-plugin-next/no-head-import-in-document.unit.test.js diff --git a/errors/manifest.json b/errors/manifest.json index f6fb25c29936f..fc392574965e6 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -255,6 +255,10 @@ }, { "title": "no-cache", "path": "/errors/no-cache.md" }, { "title": "no-css-tags", "path": "/errors/no-css-tags.md" }, + { + "title": "no-document-import-in-page", + "path": "/errors/no-document-import-in-page.md" + }, { "title": "no-document-title", "path": "/errors/no-document-title.md" @@ -263,6 +267,10 @@ "title": "no-document-viewport-meta", "path": "/errors/no-document-viewport-meta.md" }, + { + "title": "no-head-import-in-document", + "path": "/errors/no-head-import-in-document.md" + }, { "title": "no-html-link-for-pages", "path": "/errors/no-html-link-for-pages.md" diff --git a/errors/no-document-import-in-page.md b/errors/no-document-import-in-page.md new file mode 100644 index 0000000000000..79195504d6ee5 --- /dev/null +++ b/errors/no-document-import-in-page.md @@ -0,0 +1,24 @@ +# No Document Import in Page + +### Why This Error Occurred + +`next/document` was imported in a page outside of `pages/_document.js`. This can cause unexpected issues in your application. + +### Possible Ways to Fix It + +Only import and use `next/document` within `pages/_document.js` to override the default `Document` component: + +```jsx +// pages/_document.js +import Document, { Html, Head, Main, NextScript } from 'next/document' + +class MyDocument extends Document { + //... +} + +export default MyDocument +``` + +### Useful Links + +- [Custom Document](https://nextjs.org/docs/advanced-features/custom-document) diff --git a/errors/no-head-import-in-document.md b/errors/no-head-import-in-document.md new file mode 100644 index 0000000000000..9336cff2c8eca --- /dev/null +++ b/errors/no-head-import-in-document.md @@ -0,0 +1,34 @@ +# No Head Import in Document + +### Why This Error Occurred + +`next/head` was imported in `pages/_document.js`. This can cause unexpected issues in your application. + +### Possible Ways to Fix It + +Only import and use `next/document` within `pages/_document.js` to override the default `Document` component. If you are importing `next/head` to use the `Head` component, import it from `next/document` instead in order to modify `` code across all pages: + +```jsx +// pages/_document.js +import Document, { Html, Head, Main, NextScript } from 'next/document' + +class MyDocument extends Document { + static async getInitialProps(ctx) { + //... + } + + render() { + return ( + + + + ) + } +} + +export default MyDocument +``` + +### Useful Links + +- [Custom Document](https://nextjs.org/docs/advanced-features/custom-document) diff --git a/packages/eslint-plugin-next/lib/index.js b/packages/eslint-plugin-next/lib/index.js index ebc0e0f6ef77f..9b75a6f9be71b 100644 --- a/packages/eslint-plugin-next/lib/index.js +++ b/packages/eslint-plugin-next/lib/index.js @@ -9,6 +9,8 @@ module.exports = { 'google-font-display': require('./rules/google-font-display'), 'google-font-preconnect': require('./rules/google-font-preconnect'), 'link-passhref': require('./rules/link-passhref'), + 'no-document-import-in-page': require('./rules/no-document-import-in-page'), + 'no-head-import-in-document': require('./rules/no-head-import-in-document'), }, configs: { recommended: { @@ -23,6 +25,8 @@ module.exports = { '@next/next/google-font-display': 1, '@next/next/google-font-preconnect': 1, '@next/next/link-passhref': 1, + '@next/next/no-document-import-in-page': 2, + '@next/next/no-head-import-in-document': 2, }, }, }, diff --git a/packages/eslint-plugin-next/lib/rules/no-document-import-in-page.js b/packages/eslint-plugin-next/lib/rules/no-document-import-in-page.js new file mode 100644 index 0000000000000..f7fa6fdd2e301 --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/no-document-import-in-page.js @@ -0,0 +1,30 @@ +const path = require('path') + +module.exports = { + meta: { + docs: { + description: + 'Disallow importing next/document outside of pages/document.js', + recommended: true, + }, + }, + create: function (context) { + return { + ImportDeclaration(node) { + if (node.source.value !== 'next/document') { + return + } + + const page = context.getFilename().split('pages')[1] + if (!page || path.parse(page).name === '_document') { + return + } + + context.report({ + node, + message: `next/document should not be imported outside of pages/_document.js. See https://nextjs.org/docs/messages/no-document-import-in-page.`, + }) + }, + } + }, +} diff --git a/packages/eslint-plugin-next/lib/rules/no-head-import-in-document.js b/packages/eslint-plugin-next/lib/rules/no-head-import-in-document.js new file mode 100644 index 0000000000000..9da89fc59aa1c --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/no-head-import-in-document.js @@ -0,0 +1,29 @@ +const path = require('path') + +module.exports = { + meta: { + docs: { + description: 'Disallow importing next/head in pages/document.js', + recommended: true, + }, + }, + create: function (context) { + return { + ImportDeclaration(node) { + if (node.source.value !== 'next/head') { + return + } + + const document = context.getFilename().split('pages')[1] + if (!document || path.parse(document).name !== '_document') { + return + } + + context.report({ + node, + message: `next/head should not be imported in pages${document}. Import Head from next/document instead. See https://nextjs.org/docs/messages/no-head-import-in-document.`, + }) + }, + } + }, +} diff --git a/test/eslint-plugin-next/no-document-import-in-page.unit.test.js b/test/eslint-plugin-next/no-document-import-in-page.unit.test.js new file mode 100644 index 0000000000000..cc0fba94d1070 --- /dev/null +++ b/test/eslint-plugin-next/no-document-import-in-page.unit.test.js @@ -0,0 +1,66 @@ +const rule = require('@next/eslint-plugin-next/lib/rules/no-document-import-in-page') + +const RuleTester = require('eslint').RuleTester + +RuleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) + +var ruleTester = new RuleTester() +ruleTester.run('no-document-import-in-page', rule, { + valid: [ + { + code: `import Document from "next/document" + + export default class MyDocument extends Document { + render() { + return ( + + + ); + } + } + `, + filename: 'pages/_document.js', + }, + { + code: `import Document from "next/document" + + export default class MyDocument extends Document { + render() { + return ( + + + ); + } + } + `, + filename: 'pages/_document.tsx', + }, + ], + invalid: [ + { + code: `import Document from "next/document" + + export const Test = () => ( +

Test

+ ) + `, + filename: 'pages/test.js', + errors: [ + { + message: + 'next/document should not be imported outside of pages/_document.js. See https://nextjs.org/docs/messages/no-document-import-in-page.', + type: 'ImportDeclaration', + }, + ], + }, + ], +}) diff --git a/test/eslint-plugin-next/no-head-import-in-document.unit.test.js b/test/eslint-plugin-next/no-head-import-in-document.unit.test.js new file mode 100644 index 0000000000000..3b2b84024c9b6 --- /dev/null +++ b/test/eslint-plugin-next/no-head-import-in-document.unit.test.js @@ -0,0 +1,88 @@ +const rule = require('@next/eslint-plugin-next/lib/rules/no-head-import-in-document') + +const RuleTester = require('eslint').RuleTester + +RuleTester.setDefaultConfig({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + modules: true, + jsx: true, + }, + }, +}) + +var ruleTester = new RuleTester() +ruleTester.run('no-head-import-in-document', rule, { + valid: [ + { + code: `import Document, { Html, Head, Main, NextScript } from 'next/document' + + class MyDocument extends Document { + static async getInitialProps(ctx) { + //... + } + + render() { + return ( + + + + + ) + } + } + + export default MyDocument + `, + filename: 'pages/_document.tsx', + }, + { + code: `import Head from "next/head"; + + export default function IndexPage() { + return ( + + My page title + + + ); + } + `, + filename: 'pages/index.tsx', + }, + ], + invalid: [ + { + code: ` + import Document, { Html, Main, NextScript } from 'next/document' + import Head from 'next/head' + + class MyDocument extends Document { + render() { + return ( + + + +
+ + + + ) + } + } + + export default MyDocument + `, + filename: 'pages/_document.js', + errors: [ + { + message: + 'next/head should not be imported in pages/_document.js. Import Head from next/document instead. See https://nextjs.org/docs/messages/no-head-import-in-document.', + type: 'ImportDeclaration', + }, + ], + }, + ], +})