Skip to content

Commit

Permalink
ESLint Plugin: Google Font rules (vercel#24766)
Browse files Browse the repository at this point in the history
  • Loading branch information
housseindjirdeh authored May 10, 2021
1 parent 0425763 commit 59d50ff
Show file tree
Hide file tree
Showing 9 changed files with 416 additions and 0 deletions.
36 changes: 36 additions & 0 deletions errors/google-font-display.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Google Font Display

### Why This Error Occurred

For a Google Font, the `display` descriptor was either not assigned or set to `auto`, `fallback`, or `block`.

### Possible Ways to Fix It

For most cases, the best font display strategy for custom fonts is `optional`.

```jsx
import Head from 'next/head'

export default function IndexPage() {
return (
<div>
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Krona+One&display=optional"
rel="stylesheet"
/>
</Head>
</div>
)
}
```

Specifying `display=optional` minimizes the risk of invisible text or layout shift. If swapping to the custom font after it has loaded is important to you, then use `display=swap` instead.

### When Not To Use It

If you want to specifically display a font using a `block` or `fallback` strategy, then you can disable this rule.

### Useful Links

- [Font-display](https://font-display.glitch.me/)
17 changes: 17 additions & 0 deletions errors/google-font-preconnect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Google Font Preconnect

### Why This Error Occurred

A preconnect resource hint was not used with a request to the Google Fonts domain. Adding `preconnect` is recommended to initiate an early connection to the origin.

### Possible Ways to Fix It

Add `rel="preconnect"` to the Google Font domain `<link>` tag:

```jsx
<link rel="preconnect" href="https://fonts.gstatic.com" />
```

### Useful Links

- [Preconnect to required origins](https://web.dev/uses-rel-preconnect/)
8 changes: 8 additions & 0 deletions errors/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@
"title": "generatebuildid-not-a-string",
"path": "/errors/generatebuildid-not-a-string.md"
},
{
"title": "google-font-display",
"path": "/errors/google-font-display.md"
},
{
"title": "google-font-preconnect",
"path": "/errors/google-font-preconnect.md"
},
{
"title": "get-initial-props-as-an-instance-method",
"path": "/errors/get-initial-props-as-an-instance-method.md"
Expand Down
4 changes: 4 additions & 0 deletions packages/eslint-plugin-next/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ module.exports = {
'no-html-link-for-pages': require('./rules/no-html-link-for-pages'),
'no-unwanted-polyfillio': require('./rules/no-unwanted-polyfillio'),
'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'),
},
configs: {
recommended: {
Expand All @@ -15,6 +17,8 @@ module.exports = {
'@next/next/no-html-link-for-pages': 1,
'@next/next/no-unwanted-polyfillio': 1,
'@next/next/no-title-in-document-head': 1,
'@next/next/google-font-display': 1,
'@next/next/google-font-preconnect': 1,
},
},
},
Expand Down
56 changes: 56 additions & 0 deletions packages/eslint-plugin-next/lib/rules/google-font-display.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const NodeAttributes = require('../utils/nodeAttributes.js')

module.exports = {
meta: {
docs: {
description:
'Ensure correct font-display property is assigned for Google Fonts',
recommended: true,
},
},
create: function (context) {
return {
JSXOpeningElement(node) {
let message

if (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) {
const params = new URLSearchParams(hrefValue.split('?')[1])
const displayValue = params.get('display')

if (!params.has('display')) {
message = 'Display parameter is missing.'
} else if (
displayValue === 'block' ||
displayValue === 'fallback' ||
displayValue === 'auto'
) {
message = `${
displayValue[0].toUpperCase() + displayValue.slice(1)
} behavior is not recommended.`
}
}

if (message) {
context.report({
node,
message: `${message} See https://nextjs.org/docs/messages/google-font-display.`,
})
}
},
}
},
}
40 changes: 40 additions & 0 deletions packages/eslint-plugin-next/lib/rules/google-font-preconnect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const NodeAttributes = require('../utils/nodeAttributes.js')

module.exports = {
meta: {
docs: {
description: 'Ensure preconnect is used with Google Fonts',
recommended: true,
},
},
create: function (context) {
return {
JSXOpeningElement(node) {
if (node.name.name !== 'link') {
return
}

const attributes = new NodeAttributes(node)
if (!attributes.has('href') || !attributes.hasValue('href')) {
return
}

const hrefValue = attributes.value('href')
const preconnectMissing =
!attributes.has('rel') ||
!attributes.hasValue('rel') ||
attributes.value('rel') !== 'preconnect'

if (
hrefValue.includes('https://fonts.gstatic.com') &&
preconnectMissing
) {
context.report({
node,
message: `Preconnect is missing. See https://nextjs.org/docs/messages/google-font-preconnect.`,
})
}
},
}
},
}
52 changes: 52 additions & 0 deletions packages/eslint-plugin-next/lib/utils/nodeAttributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Return attributes and values of a node in a convenient way:
/* example:
<ExampleElement attr1="15" attr2>
{ attr1: {
hasValue: true,
value: 15
},
attr2: {
hasValue: false
}
Inclusion of hasValue is in case an eslint rule cares about boolean values
explicitely assigned to attribute vs the attribute being used as a flag
*/
class NodeAttributes {
constructor(ASTnode) {
this.attributes = {}
ASTnode.attributes.forEach((attribute) => {
if (!attribute.type || attribute.type !== 'JSXAttribute') {
return
}
this.attributes[attribute.name.name] = {
hasValue: !!attribute.value,
}
if (attribute.value) {
if (attribute.value.value) {
this.attributes[attribute.name.name].value = attribute.value.value
} else if (attribute.value.expression) {
this.attributes[attribute.name.name].value =
attribute.value.expression.value
}
}
})
}
hasAny() {
return !!Object.keys(this.attributes).length
}
has(attrName) {
return !!this.attributes[attrName]
}
hasValue(attrName) {
return !!this.attributes[attrName].hasValue
}
value(attrName) {
if (!this.attributes[attrName]) {
return true
}

return this.attributes[attrName].value
}
}

module.exports = NodeAttributes
143 changes: 143 additions & 0 deletions test/eslint-plugin-next/google-font-display.unit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
const rule = require('@next/eslint-plugin-next/lib/rules/google-font-display')
const RuleTester = require('eslint').RuleTester

RuleTester.setDefaultConfig({
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
modules: true,
jsx: true,
},
},
})

var ruleTester = new RuleTester()
ruleTester.run('google-font-display', rule, {
valid: [
`import Head from "next/head";
export default Test = () => {
return (
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Krona+One&display=optional"
rel="stylesheet"
/>
</Head>
);
};
`,

`import Document, { Html, Head } from "next/document";
class MyDocument extends Document {
render() {
return (
<Html>
<Head>
<link
href="https://fonts.googleapis.com/css?family=Krona+One&display=swap"
rel="stylesheet"
/>
</Head>
</Html>
);
}
}
export default MyDocument;
`,
],

invalid: [
{
code: `import Head from "next/head";
export default Test = () => {
return (
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Krona+One"
rel="stylesheet"
/>
</Head>
);
};
`,
errors: [
{
message:
'Display parameter is missing. See https://nextjs.org/docs/messages/google-font-display.',
type: 'JSXOpeningElement',
},
],
},
{
code: `import Head from "next/head";
export default Test = () => {
return (
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Krona+One&display=block"
rel="stylesheet"
/>
</Head>
);
};
`,
errors: [
{
message:
'Block behavior is not recommended. See https://nextjs.org/docs/messages/google-font-display.',
type: 'JSXOpeningElement',
},
],
},
{
code: `import Head from "next/head";
export default Test = () => {
return (
<Head>
<link
href="https://fonts.googleapis.com/css2?family=Krona+One&display=auto"
rel="stylesheet"
/>
</Head>
);
};
`,
errors: [
{
message:
'Auto behavior is not recommended. See https://nextjs.org/docs/messages/google-font-display.',
type: 'JSXOpeningElement',
},
],
},
{
code: `import Head from "next/head";
export default Test = () => {
return (
<Head>
<link
href="https://fonts.googleapis.com/css2?display=fallback&family=Krona+One"
rel="stylesheet"
/>
</Head>
);
};
`,
errors: [
{
message:
'Fallback behavior is not recommended. See https://nextjs.org/docs/messages/google-font-display.',
type: 'JSXOpeningElement',
},
],
},
],
})
Loading

0 comments on commit 59d50ff

Please sign in to comment.