Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ESLint Plugin: Google Font rules #24766

Merged
merged 5 commits into from
May 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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