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', + }, + ], + }, + ], +})