diff --git a/packages/eslint-plugin-next/lib/index.js b/packages/eslint-plugin-next/lib/index.js index 876aa2e91630e..07501954c2ef8 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'), + 'missing-preload': require('./rules/missing-preload'), }, configs: { recommended: { @@ -13,6 +14,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/missing-preload': 1, }, }, }, diff --git a/packages/eslint-plugin-next/lib/rules/missing-preload.js b/packages/eslint-plugin-next/lib/rules/missing-preload.js new file mode 100644 index 0000000000000..55c58f63f3641 --- /dev/null +++ b/packages/eslint-plugin-next/lib/rules/missing-preload.js @@ -0,0 +1,57 @@ +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, + `` + ) + }, + }) + } + } + + 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) + } + }, + } + }, +} diff --git a/test/eslint-plugin-next/missing-preload.test.js b/test/eslint-plugin-next/missing-preload.test.js new file mode 100644 index 0000000000000..1fc9ed814bc8b --- /dev/null +++ b/test/eslint-plugin-next/missing-preload.test.js @@ -0,0 +1,109 @@ +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 ( +
+

Hello title

+ + +
+ ); + } + }`, + `import {Head} from 'next/document'; + export class Blah extends Head { + render() { + return ( +
+

Hello title

+ +
+ ); + } + }`, + `import {Head} from 'next/head'; + export class Blah { + render() { + return ( +
+
+ +
+

Hello title

+ +
+ ); + } + }`, + ], + + invalid: [ + { + code: ` + import {Head} from 'next/document'; + export class Blah extends Head { + render() { + return ( +
+

Hello title

+ +
+ ); + } + }`, + 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 ( +
+

Hello title

+ +
+ ); + } + }`, + }, + { + code: ` +
+ +
`, + errors: [ + { + message: + 'Stylesheet does not have an associated preload tag. This could potentially impact First paint.', + type: 'JSXOpeningElement', + }, + ], + output: ` +
+ +
`, + }, + ], +})