forked from vercel/next.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ESLint Plugin: Google Font rules (vercel#24766)
- Loading branch information
1 parent
0425763
commit 59d50ff
Showing
9 changed files
with
416 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
packages/eslint-plugin-next/lib/rules/google-font-display.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
40
packages/eslint-plugin-next/lib/rules/google-font-preconnect.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.`, | ||
}) | ||
} | ||
}, | ||
} | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
143
test/eslint-plugin-next/google-font-display.unit.test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
], | ||
}, | ||
], | ||
}) |
Oops, something went wrong.