From c252fbc3d55b32b43366afa40d67bafb606940d7 Mon Sep 17 00:00:00 2001
From: Houssein Djirdeh
Date: Mon, 10 May 2021 17:28:06 -0400
Subject: [PATCH] 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',
+ },
+ ],
+ },
+ ],
+})