From 44072205d83eca4add74507397fa894fba6ad3c9 Mon Sep 17 00:00:00 2001 From: Houssein Djirdeh Date: Mon, 10 May 2021 15:08:14 -0400 Subject: [PATCH] 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', + }, + ], + }, + ], +})