Skip to content

Commit

Permalink
Eslint preload (#16199)
Browse files Browse the repository at this point in the history
Eslint rule checks for missing preloads for stylesheets.

cc: @prateekbh
  • Loading branch information
janicklas-ralph authored Aug 20, 2020
1 parent 6c9dd6c commit b1ea19a
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/eslint-plugin-next/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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,
},
},
},
Expand Down
57 changes: 57 additions & 0 deletions packages/eslint-plugin-next/lib/rules/missing-preload.js
Original file line number Diff line number Diff line change
@@ -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,
`<link rel="preload" href="${href}" as="style" />`
)
},
})
}
}

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)
}
},
}
},
}
109 changes: 109 additions & 0 deletions test/eslint-plugin-next/missing-preload.test.js
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<h1>Hello title</h1>
<link href="/_next/static/css/styles.css" rel="preload" />
<link href="/_next/static/css/styles.css" rel="stylesheet" />
</div>
);
}
}`,
`import {Head} from 'next/document';
export class Blah extends Head {
render() {
return (
<div>
<h1>Hello title</h1>
<link href="/_next/static/css/styles.css" rel="stylesheet" media="print" />
</div>
);
}
}`,
`import {Head} from 'next/head';
export class Blah {
render() {
return (
<div>
<div>
<Head><link href="/_next/static/css/styles.css" rel="preload" /></Head>
</div>
<h1>Hello title</h1>
<Head><link href="/_next/static/css/styles.css" rel="stylesheet" /></Head>
</div>
);
}
}`,
],

invalid: [
{
code: `
import {Head} from 'next/document';
export class Blah extends Head {
render() {
return (
<div>
<h1>Hello title</h1>
<link href="/_next/static/css/styles.css" rel="stylesheet" />
</div>
);
}
}`,
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 (
<div>
<h1>Hello title</h1>
<link rel="preload" href="/_next/static/css/styles.css" as="style" /><link href="/_next/static/css/styles.css" rel="stylesheet" />
</div>
);
}
}`,
},
{
code: `
<div>
<link href="/_next/static/css/styles.css" rel="stylesheet" />
</div>`,
errors: [
{
message:
'Stylesheet does not have an associated preload tag. This could potentially impact First paint.',
type: 'JSXOpeningElement',
},
],
output: `
<div>
<link rel="preload" href="/_next/static/css/styles.css" as="style" /><link href="/_next/static/css/styles.css" rel="stylesheet" />
</div>`,
},
],
})

0 comments on commit b1ea19a

Please sign in to comment.