Skip to content

Commit

Permalink
Add support for font loaders (#40746)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Hannes Bornö and ijjk committed Sep 22, 2022
1 parent ef9ba45 commit bf8ee1e
Show file tree
Hide file tree
Showing 54 changed files with 1,994 additions and 15 deletions.
1 change: 1 addition & 0 deletions packages/next/build/entries.ts
Expand Up @@ -204,6 +204,7 @@ export function getEdgeServerEntry(opts: {
pagesType: opts.pagesType,
appDirLoader: Buffer.from(opts.appDirLoader || '').toString('base64'),
sriEnabled: !opts.isDev && !!opts.config.experimental.sri?.algorithm,
hasFontLoaders: !!opts.config.experimental.fontLoaders,
}

return {
Expand Down
4 changes: 4 additions & 0 deletions packages/next/build/index.ts
Expand Up @@ -59,6 +59,7 @@ import {
APP_BUILD_MANIFEST,
FLIGHT_SERVER_CSS_MANIFEST,
RSC_MODULE_TYPES,
FONT_LOADER_MANIFEST,
} from '../shared/lib/constants'
import { getSortedRoutes, isDynamicRoute } from '../shared/lib/router/utils'
import { __ApiPreviewProps } from '../server/api-utils'
Expand Down Expand Up @@ -828,6 +829,9 @@ export default async function build(
config.optimizeFonts ? path.join(serverDir, FONT_MANIFEST) : null,
BUILD_ID_FILE,
appDir ? path.join(serverDir, APP_PATHS_MANIFEST) : null,
config.experimental.fontLoaders
? path.join(serverDir, FONT_LOADER_MANIFEST)
: null,
]
.filter(nonNullable)
.map((file) => path.join(config.distDir, file)),
Expand Down
3 changes: 3 additions & 0 deletions packages/next/build/swc/options.js
Expand Up @@ -123,6 +123,9 @@ function getBaseSWCOptions({
isServer: !!isServerLayer,
}
: false,
fontLoaders:
nextConfig?.experimental?.fontLoaders &&
Object.keys(nextConfig.experimental.fontLoaders),
}
}

Expand Down
6 changes: 6 additions & 0 deletions packages/next/build/webpack-config.ts
Expand Up @@ -61,6 +61,7 @@ import loadJsConfig from './load-jsconfig'
import { loadBindings } from './swc'
import { AppBuildManifestPlugin } from './webpack/plugins/app-build-manifest-plugin'
import { SubresourceIntegrityPlugin } from './webpack/plugins/subresource-integrity-plugin'
import { FontLoaderManifestPlugin } from './webpack/plugins/font-loader-manifest-plugin'

const NEXT_PROJECT_ROOT = pathJoin(__dirname, '..', '..')
const NEXT_PROJECT_ROOT_DIST = pathJoin(NEXT_PROJECT_ROOT, 'dist')
Expand Down Expand Up @@ -1508,6 +1509,7 @@ export default async function getBaseWebpackConfig(
'next-middleware-asset-loader',
'next-middleware-wasm-loader',
'next-app-loader',
'next-font-loader',
].reduce((alias, loader) => {
// using multiple aliases to replace `resolveLoader.modules`
alias[loader] = path.join(__dirname, 'webpack', 'loaders', loader)
Expand Down Expand Up @@ -1838,6 +1840,7 @@ export default async function getBaseWebpackConfig(
new MiddlewarePlugin({
dev,
sriEnabled: !dev && !!config.experimental.sri?.algorithm,
hasFontLoaders: !!config.experimental.fontLoaders,
}),
isClient &&
new BuildManifestPlugin({
Expand Down Expand Up @@ -1890,6 +1893,9 @@ export default async function getBaseWebpackConfig(
isClient &&
!!config.experimental.sri?.algorithm &&
new SubresourceIntegrityPlugin(config.experimental.sri.algorithm),
isClient &&
config.experimental.fontLoaders &&
new FontLoaderManifestPlugin(),
!dev &&
isClient &&
new (require('./webpack/plugins/telemetry-plugin').TelemetryPlugin)(
Expand Down
55 changes: 55 additions & 0 deletions packages/next/build/webpack/config/blocks/css/index.ts
Expand Up @@ -3,11 +3,13 @@ import { webpack } from 'next/dist/compiled/webpack/webpack'
import { loader, plugin } from '../../helpers'
import { ConfigurationContext, ConfigurationFn, pipe } from '../../utils'
import { getCssModuleLoader, getGlobalCssLoader } from './loaders'
import { getFontLoader } from './loaders/font-loader'
import {
getCustomDocumentError,
getGlobalImportError,
getGlobalModuleImportError,
getLocalModuleImportError,
getFontLoaderDocumentImportError,
} from './messages'
import { getPostCssPlugins } from './plugins'

Expand Down Expand Up @@ -199,6 +201,59 @@ export const css = curry(async function css(
})
)

// Resolve the configured font loaders, the resolved files are noop files that next-font-loader will match
let fontLoaders: [string, string][] | undefined = ctx.experimental.fontLoaders
? Object.entries(ctx.experimental.fontLoaders).map(
([fontLoader, fontLoaderOptions]: any) => [
require.resolve(fontLoader),
fontLoaderOptions,
]
)
: undefined

// Font loaders cannot be imported in _document.
fontLoaders?.forEach(([fontLoaderPath, fontLoaderOptions]) => {
fns.push(
loader({
oneOf: [
markRemovable({
test: fontLoaderPath,
// Use a loose regex so we don't have to crawl the file system to
// find the real file name (if present).
issuer: /pages[\\/]_document\./,
use: {
loader: 'error-loader',
options: {
reason: getFontLoaderDocumentImportError(),
},
},
}),
],
})
)

// Matches the resolved font loaders noop files to run next-font-loader
fns.push(
loader({
oneOf: [
markRemovable({
sideEffects: false,
test: fontLoaderPath,
issuer: {
and: [
{
or: [ctx.rootDirectory, regexClientEntry],
},
],
not: [/node_modules/],
},
use: getFontLoader(ctx, lazyPostCSSInitializer, fontLoaderOptions),
}),
],
})
)
})

// CSS Modules support must be enabled on the server and client so the class
// names are available for SSR or Prerendering.
if (ctx.experimental.appDir && !ctx.isProduction) {
Expand Down
@@ -0,0 +1,70 @@
import { webpack } from 'next/dist/compiled/webpack/webpack'
import { ConfigurationContext } from '../../../utils'
import { getClientStyleLoader } from './client'
import { cssFileResolve } from './file-resolve'

export function getFontLoader(
ctx: ConfigurationContext,
postcss: any,
fontLoaderOptions: any
): webpack.RuleSetUseItem[] {
const loaders: webpack.RuleSetUseItem[] = []

if (ctx.isClient) {
// Add appropriate development mode or production mode style
// loader
loaders.push(
getClientStyleLoader({
isAppDir: !!ctx.experimental.appDir,
isDevelopment: ctx.isDevelopment,
assetPrefix: ctx.assetPrefix,
})
)
}

loaders.push({
loader: require.resolve('../../../../loaders/css-loader/src'),
options: {
postcss,
importLoaders: 1,
// Use CJS mode for backwards compatibility:
esModule: false,
url: (url: string, resourcePath: string) =>
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
import: (url: string, _: any, resourcePath: string) =>
cssFileResolve(url, resourcePath, ctx.experimental.urlImports),
modules: {
// Do not transform class names (CJS mode backwards compatibility):
exportLocalsConvention: 'asIs',
// Server-side (Node.js) rendering support:
exportOnlyLocals: ctx.isServer,
// Disallow global style exports so we can code-split CSS and
// not worry about loading order.
mode: 'pure',
getLocalIdent: (
_context: any,
_localIdentName: any,
exportName: string,
_options: any,
meta: any
) => {
// hash from next-font-loader
return `__${exportName}_${meta.fontFamilyHash}`
},
},
fontLoader: true,
},
})

loaders.push({
loader: 'next-font-loader',
options: {
isServer: ctx.isServer,
assetPrefix: ctx.assetPrefix,
fontLoaderOptions,
postcss,
},
})

return loaders
}
6 changes: 6 additions & 0 deletions packages/next/build/webpack/config/blocks/css/messages.ts
Expand Up @@ -31,3 +31,9 @@ export function getCustomDocumentError() {
'pages/_document.js'
)}. Please move global styles to ${chalk.cyan('pages/_app.js')}.`
}

export function getFontLoaderDocumentImportError() {
return `Font loaders ${chalk.bold('cannot')} be used within ${chalk.cyan(
'pages/_document.js'
)}.`
}
7 changes: 4 additions & 3 deletions packages/next/build/webpack/loaders/css-loader/src/index.js
Expand Up @@ -4,7 +4,6 @@
*/
import CssSyntaxError from './CssSyntaxError'
import Warning from '../../postcss-loader/src/Warning'
// import { icssParser, importParser, urlParser } from './plugins'
import { stringifyRequest } from '../../../stringify-request'

const moduleRegExp = /\.module\.\w+$/i
Expand Down Expand Up @@ -128,6 +127,7 @@ function normalizeOptions(rawOptions, loaderContext) {
: rawOptions.importLoaders,
esModule:
typeof rawOptions.esModule === 'undefined' ? true : rawOptions.esModule,
fontLoader: rawOptions.fontLoader,
}
}

Expand Down Expand Up @@ -169,10 +169,11 @@ export default async function loader(content, map, meta) {
const { icssParser, importParser, urlParser } = require('./plugins')

const replacements = []
const exports = []
// if it's a font loader next-font-loader will have exports that should be exported as is
const exports = options.fontLoader ? meta.exports : []

if (shouldUseModulesPlugins(options)) {
plugins.push(...getModulesPlugins(options, this))
plugins.push(...getModulesPlugins(options, this, meta))
}

const importPluginImports = []
Expand Down
18 changes: 12 additions & 6 deletions packages/next/build/webpack/loaders/css-loader/src/utils.js
Expand Up @@ -135,7 +135,7 @@ function shouldUseIcssPlugin(options) {
return options.icss === true || Boolean(options.modules)
}

function getModulesPlugins(options, loaderContext) {
function getModulesPlugins(options, loaderContext, meta) {
const {
mode,
getLocalIdent,
Expand All @@ -154,11 +154,17 @@ function getModulesPlugins(options, loaderContext) {
extractImports(),
modulesScope({
generateScopedName(exportName) {
return getLocalIdent(loaderContext, localIdentName, exportName, {
context: localIdentContext,
hashPrefix: localIdentHashPrefix,
regExp: localIdentRegExp,
})
return getLocalIdent(
loaderContext,
localIdentName,
exportName,
{
context: localIdentContext,
hashPrefix: localIdentHashPrefix,
regExp: localIdentRegExp,
},
meta
)
},
exportGlobals: options.modules.exportGlobals,
}),
Expand Down
Expand Up @@ -15,6 +15,7 @@ export type EdgeSSRLoaderQuery = {
appDirLoader?: string
pagesType?: 'app' | 'pages' | 'root'
sriEnabled: boolean
hasFontLoaders: boolean
}

export default async function edgeSSRLoader(this: any) {
Expand All @@ -32,6 +33,7 @@ export default async function edgeSSRLoader(this: any) {
appDirLoader: appDirLoaderBase64,
pagesType,
sriEnabled,
hasFontLoaders,
} = this.getOptions()

const appDirLoader = Buffer.from(
Expand Down Expand Up @@ -103,6 +105,9 @@ export default async function edgeSSRLoader(this: any) {
const subresourceIntegrityManifest = ${
sriEnabled ? 'self.__SUBRESOURCE_INTEGRITY_MANIFEST' : 'undefined'
}
const fontLoaderManifest = ${
hasFontLoaders ? 'self.__FONT_LOADER_MANIFEST' : 'undefined'
}
const render = getRender({
pageType,
Expand All @@ -122,6 +127,7 @@ export default async function edgeSSRLoader(this: any) {
subresourceIntegrityManifest,
config: ${stringifiedConfig},
buildId: ${JSON.stringify(buildId)},
fontLoaderManifest,
})
export const ComponentMod = pageMod
Expand Down
Expand Up @@ -2,6 +2,7 @@ import type { NextConfig } from '../../../../server/config-shared'
import type { DocumentType, AppType } from '../../../../shared/lib/utils'
import type { BuildManifest } from '../../../../server/get-page-files'
import type { ReactLoadableManifest } from '../../../../server/load-components'
import type { FontLoaderManifest } from '../../plugins/font-loader-manifest-plugin'

import WebServer from '../../../../server/web-server'
import {
Expand All @@ -28,6 +29,7 @@ export function getRender({
serverCSSManifest,
config,
buildId,
fontLoaderManifest,
}: {
pagesType?: 'app' | 'pages' | 'root'
dev: boolean
Expand All @@ -47,13 +49,15 @@ export function getRender({
appServerMod: any
config: NextConfig
buildId: string
fontLoaderManifest: FontLoaderManifest
}) {
const isAppPath = pagesType === 'app'
const baseLoadComponentResult = {
dev,
buildManifest,
reactLoadableManifest,
subresourceIntegrityManifest,
fontLoaderManifest,
Document,
App: appMod?.default as AppType,
}
Expand Down

0 comments on commit bf8ee1e

Please sign in to comment.