rsnext/packages/next/build/webpack/loaders/next-font-loader/postcss-font-loader.ts
Hannes Bornö bf8ee1edb4
Add support for font loaders (#40746)
For some context:
[https://vercel.slack.com/archives/CGU8HUTUH/p1662124179102509](https://vercel.slack.com/archives/CGU8HUTUH/p1662124179102509)

Continuation of #40221 and #40227

Adds `experimental.fontLoaders`.

SWC next-font-loaders (#40221) transforms font loader (e.g. #40227) call
expressions into an import with the function call arguments as a query.

The imports will be matched by `next-font-loader`. It runs the
configured font loaders - emits font files and returns CSS. Exports are
added, and the font-family is made locally scoped. The returned CSS is
turned into a CSS module with `css-loader` which lets you consume the
font-family.

`FontLoaderManifestPlugin` creates a manifest of the preloaded font
files for each entrypoint. Preload/preconnect are then added in
`_document.tsx` if any font files were found for that path.

Co-authored-by: JJ Kasper <jj@jjsweb.site>
2022-09-21 22:12:59 -07:00

135 lines
4.2 KiB
TypeScript

import postcss, { Declaration } from 'postcss'
const postcssFontLoaderPlugn = (
exports: { name: any; value: any }[],
fontFamilyHash: string,
fallbackFonts: string[] = []
) => {
return {
postcssPlugin: 'postcss-font-loader',
Once(root: any) {
const fontFamilies: string[] = []
let rawFamily: string | undefined
let fontWeight: string | undefined
let fontStyle: string | undefined
const formatFamily = (family: string) => {
if (family[0] === "'" || family[0] === '"') {
family = family.slice(1, family.length - 1)
}
// Turn the font family unguessable to make it localy scoped
return `'__${family.replace(/ /g, '_')}_${fontFamilyHash}'`
}
for (const node of root.nodes) {
if (node.type === 'atrule' && node.name === 'font-face') {
const familyNode = node.nodes.find(
(decl: Declaration) => decl.prop === 'font-family'
)
if (!familyNode) {
continue
}
if (!rawFamily) {
let family: string = familyNode.value
if (family[0] === "'" || family[0] === '"') {
family = family.slice(1, family.length - 1)
}
rawFamily = family
}
const formattedFamily = formatFamily(familyNode.value)
familyNode.value = formattedFamily
if (fontFamilies.includes(formattedFamily)) {
continue
}
fontFamilies.push(formattedFamily)
// Only extract weight and style from first encountered family, the rest will treated as fallbacks
if (fontFamilies.length > 1) {
continue
}
// Extract weight and style from first encountered @font-face
const weight = node.nodes.find(
(decl: Declaration) => decl.prop === 'font-weight'
)
// Skip if the value includes ' ', then it's a range of possible values
if (weight && !weight.value.includes(' ')) {
fontWeight = weight.value
}
const style = node.nodes.find(
(decl: Declaration) => decl.prop === 'font-style'
)
// Skip if the value includes ' ', then it's a range of possible values
if (style && !style.value.includes(' ')) {
fontStyle = style.value
}
}
}
const [mainFontFamily, ...adjustFontFallbacks] = fontFamilies
// If fallback fonts were provided from the font loader, they should be used before the adjustFontFallbacks
const formattedFontFamilies = [
mainFontFamily,
...fallbackFonts,
...adjustFontFallbacks,
].join(', ')
// Add class with family, weight and style
const classRule = new postcss.Rule({ selector: '.className' })
classRule.nodes = [
new postcss.Declaration({
prop: 'font-family',
value: formattedFontFamilies,
}),
...(fontWeight
? [
new postcss.Declaration({
prop: 'font-weight',
value: fontWeight,
}),
]
: []),
...(fontStyle
? [
new postcss.Declaration({
prop: 'font-style',
value: fontStyle,
}),
]
: []),
]
root.nodes.push(classRule)
// Add class that defines a variable with the font family
const varialbeRule = new postcss.Rule({ selector: '.variable' })
varialbeRule.nodes = [
new postcss.Declaration({
prop: rawFamily
? `--next-font-${rawFamily.toLowerCase().replace(/ /g, '-')}${
fontWeight ? `-${fontWeight}` : ''
}${fontStyle === 'italic' ? `-${fontStyle}` : ''}`
: '',
value: formattedFontFamilies,
}),
]
root.nodes.push(varialbeRule)
// Export @font-face values as is
exports.push({
name: 'style',
value: {
fontFamily: formattedFontFamilies,
fontWeight: fontWeight && Number(fontWeight),
fontStyle,
},
})
},
}
}
postcssFontLoaderPlugn.postcss = true
export default postcssFontLoaderPlugn