Skip to content

Commit

Permalink
Refactor next-font-manifest-plugin (#51835)
Browse files Browse the repository at this point in the history
## Background

Currently we're track all imported CSS modules by an entry, in the
client manifest:

```js
// client manifest
{
  entryCSSFiles: {
    'app/entry_name': {
      modules: ['app/foo.css', ...]
    }
  }
}
```

And then, in the font manifest we track all emitted assets (fonts) by
each CSS module:

```js
// font manifest
{
  app: {
    'app/foo.css': [
      'static/font/abc.woff', ...
    ]
  }
}
```

These two fields are only used together by `get-preloadable-fonts.tsx`,
so it can know which font files need to be preloaded for this entry.

(Although previously we use `.modules` for something else, but it's gone
now)

## Changes

Since we only need the font assets per entry, it's unnecessary to track
these in the module level and then join them together. This PR removes
the `modules` field from the client manifest, and changes the font
manifest to directly keep the entry—font mapping.

This gets rid of one module traversal from the client manifest plugin.
  • Loading branch information
shuding committed Jun 26, 2023
1 parent 57ab281 commit f1240b8
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 108 deletions.
Expand Up @@ -74,10 +74,7 @@ export function createManifests() {
if (type === 'entryCSSFiles') {
const cssChunks = JSON.parse(key)
// TODO(WEB-856) subscribe to changes
return {
modules: [],
files: cssChunks.filter(filterAvailable).map(toPath),
}
return cssChunks.filter(filterAvailable).map(toPath)
}
},
}
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/build/webpack-config.ts
Expand Up @@ -2445,7 +2445,7 @@ export default async function getBaseWebpackConfig(
new SubresourceIntegrityPlugin(config.experimental.sri.algorithm),
isClient &&
new NextFontManifestPlugin({
appDirEnabled: !!config.experimental.appDir,
appDir,
}),
!dev &&
isClient &&
Expand Down
45 changes: 6 additions & 39 deletions packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts
Expand Up @@ -12,7 +12,6 @@ import {
SYSTEM_ENTRYPOINTS,
} from '../../../shared/lib/constants'
import { relative } from 'path'
import { isCSSMod } from '../loaders/utils'
import { getProxiedPluginState } from '../../build-context'

import { nonNullable } from '../../../lib/non-nullable'
Expand Down Expand Up @@ -68,10 +67,7 @@ export type ClientReferenceManifest = {
[moduleId: string]: ManifestNode
}
entryCSSFiles: {
[entry: string]: {
modules: string[]
files: string[]
}
[entry: string]: string[]
}
}

Expand Down Expand Up @@ -163,14 +159,11 @@ export class ClientReferenceManifestPlugin {
/[\\/]/g,
path.sep
)
manifest.entryCSSFiles[chunkEntryName] = {
modules: [],
files: chunkGroup
.getFiles()
.filter(
(f) => !f.startsWith('static/css/pages/') && f.endsWith('.css')
),
}
manifest.entryCSSFiles[chunkEntryName] = chunkGroup
.getFiles()
.filter(
(f) => !f.startsWith('static/css/pages/') && f.endsWith('.css')
)
}

const recordModule = (id: ModuleId, mod: webpack.NormalModule) => {
Expand Down Expand Up @@ -320,33 +313,7 @@ export class ClientReferenceManifestPlugin {
}
}
}

// Track CSS modules. This is necessary for next/font preloading.
// TODO: Unfortunately we have to keep this iteration for now but we
// will optimize it altogether in the future.
const chunkModules = compilation.chunkGraph.getChunkModulesIterable(
chunk
) as Iterable<webpack.NormalModule>
for (const mod of chunkModules) {
if (isCSSMod(mod)) {
if (chunkEntryName) {
const resource =
mod.type === 'css/mini-extract'
? // @ts-expect-error TODO: use `identifier()` instead.
mod._identifier.slice(mod._identifier.lastIndexOf('!') + 1)
: mod.resource
manifest.entryCSSFiles[chunkEntryName].modules.push(resource)
}
}
}
})

if (chunkEntryName) {
// Make sure CSS modules are deduped
manifest.entryCSSFiles[chunkEntryName].modules = [
...new Set(manifest.entryCSSFiles[chunkEntryName].modules),
]
}
})

const file = 'server/' + CLIENT_REFERENCE_MANIFEST
Expand Down
@@ -1,13 +1,15 @@
import { webpack, sources } from 'next/dist/compiled/webpack/webpack'
import getRouteFromEntrypoint from '../../../server/get-route-from-entrypoint'
import { NEXT_FONT_MANIFEST } from '../../../shared/lib/constants'
import { traverseModules } from '../utils'
import path from 'path'

export type NextFontManifest = {
pages: {
[path: string]: string[]
}
app: {
[moduleRequest: string]: string[]
[entry: string]: string[]
}
appUsingSizeAdjust: boolean
pagesUsingSizeAdjust: boolean
Expand Down Expand Up @@ -51,28 +53,15 @@ function getPageIsUsingSizeAdjust(fontFiles: string[]) {
* When creating the component tree in app-render it looks for font files to preload: manifest.app[moduleBeingRendered]
*/
export class NextFontManifestPlugin {
private appDirEnabled: boolean
private appDir: undefined | string

constructor(options: { appDirEnabled: boolean }) {
this.appDirEnabled = options.appDirEnabled
constructor(options: { appDir: undefined | string }) {
this.appDir = options.appDir
}

apply(compiler: webpack.Compiler) {
compiler.hooks.make.tap(PLUGIN_NAME, (compilation) => {
let nextFontModules: webpack.Module[]

// After all modules are created, we collect the modules that was created by next-font-loader.
if (this.appDirEnabled) {
compilation.hooks.finishModules.tap(PLUGIN_NAME, (modules) => {
const modulesArr = Array.from(modules)
nextFontModules = modulesArr.filter((mod: any) =>
mod?.loaders?.some(({ loader }: any) =>
loader.includes('next-font-loader/index.js')
)
)
})
}

// In this stage the font files are emitted and we can collect all files emitted by each chunkGroup (entry).
compilation.hooks.processAssets.tap(
{
name: PLUGIN_NAME,
Expand All @@ -86,31 +75,52 @@ export class NextFontManifestPlugin {
pagesUsingSizeAdjust: false,
}

if (this.appDirEnabled) {
// In this stage the font files are emitted and we can collect all files emitted by each next/font module.
for (const mod of nextFontModules) {
if (!mod.buildInfo?.assets) continue
const modAssets = Object.keys(mod.buildInfo.assets)
const fontFiles: string[] = modAssets.filter((file: string) =>
/\.(woff|woff2|eot|ttf|otf)$/.test(file)
)

// Look if size-adjust fallback font is being used
if (!nextFontManifest.appUsingSizeAdjust) {
nextFontManifest.appUsingSizeAdjust =
getPageIsUsingSizeAdjust(fontFiles)
if (this.appDir) {
const appDirBase = path.dirname(this.appDir) + path.sep

// After all modules are created, we collect the modules that was created by next-font-loader.
traverseModules(
compilation,
(mod, _chunk, chunkGroup) => {
if (mod?.request?.includes('/next-font-loader/index.js?')) {
if (!mod.buildInfo?.assets) return

const chunkEntryName = (appDirBase + chunkGroup.name).replace(
/[\\/]/g,
path.sep
)

const modAssets = Object.keys(mod.buildInfo.assets)
const fontFiles: string[] = modAssets.filter((file: string) =>
/\.(woff|woff2|eot|ttf|otf)$/.test(file)
)

// Look if size-adjust fallback font is being used
if (!nextFontManifest.appUsingSizeAdjust) {
nextFontManifest.appUsingSizeAdjust =
getPageIsUsingSizeAdjust(fontFiles)
}

const preloadedFontFiles = getPreloadedFontFiles(fontFiles)

// Add an entry of the module's font files in the manifest.
// We'll add an entry even if no files should preload.
// When an entry is present but empty, instead of preloading the font files, a preconnect tag is added.
if (fontFiles.length > 0) {
if (!nextFontManifest.app[chunkEntryName]) {
nextFontManifest.app[chunkEntryName] = []
}
nextFontManifest.app[chunkEntryName].push(
...preloadedFontFiles
)
}
}
},
(chunkGroup) => {
// Only loop through entrypoints that are under app/.
return !!chunkGroup.name?.startsWith('app/')
}

const preloadedFontFiles = getPreloadedFontFiles(fontFiles)

// Add an entry of the module's font files in the manifest.
// We'll add an entry even if no files should preload.
// When an entry is present but empty, instead of preloading the font files, a preconnect tag is added.
if (fontFiles.length > 0) {
nextFontManifest.app[(mod as any).userRequest] =
preloadedFontFiles
}
}
)
}

// Look at all the entrypoints created for pages/.
Expand Down
6 changes: 5 additions & 1 deletion packages/next/src/build/webpack/utils.ts
Expand Up @@ -8,9 +8,13 @@ export function traverseModules(
chunk: webpack.Chunk,
chunkGroup: (typeof compilation.chunkGroups)[0],
modId: string | number
) => any
) => any,
filterChunkGroup?: (chunkGroup: webpack.ChunkGroup) => boolean
) {
compilation.chunkGroups.forEach((chunkGroup) => {
if (filterChunkGroup && !filterChunkGroup(chunkGroup)) {
return
}
chunkGroup.chunks.forEach((chunk: webpack.Chunk) => {
const chunkModules = compilation.chunkGraph.getChunkModulesIterable(
chunk
Expand Down
2 changes: 0 additions & 2 deletions packages/next/src/server/app-render/app-render.tsx
Expand Up @@ -451,7 +451,6 @@ export async function renderToHTMLOrFlight(

const preloadedFontFiles = layoutOrPagePath
? getPreloadableFonts(
clientReferenceManifest,
nextFontManifest,
layoutOrPagePath,
injectedFontPreloadTagsWithCurrentLayout
Expand Down Expand Up @@ -1104,7 +1103,6 @@ export async function renderToHTMLOrFlight(
true
)
getPreloadableFonts(
clientReferenceManifest,
nextFontManifest,
layoutPath,
injectedFontPreloadTagsWithCurrentLayout
Expand Down
Expand Up @@ -16,7 +16,7 @@ export function getCssInlinedLinkTags(
clientReferenceManifest.entryCSSFiles[filePathWithoutExt]

if (entryCSSFiles) {
for (const file of entryCSSFiles.files) {
for (const file of entryCSSFiles) {
if (!injectedCSS.has(file)) {
if (collectNewCSSImports) {
injectedCSS.add(file)
Expand Down
25 changes: 7 additions & 18 deletions packages/next/src/server/app-render/get-preloadable-fonts.tsx
@@ -1,5 +1,4 @@
import { NextFontManifest } from '../../build/webpack/plugins/next-font-manifest-plugin'
import { ClientReferenceManifest } from '../../build/webpack/plugins/flight-manifest-plugin'

/**
* Get hrefs for fonts to preload
Expand All @@ -9,7 +8,6 @@ import { ClientReferenceManifest } from '../../build/webpack/plugins/flight-mani
* Returns null if there are fonts but none to preload and at least some were previously preloaded
*/
export function getPreloadableFonts(
clientReferenceManifest: ClientReferenceManifest,
nextFontManifest: NextFontManifest | undefined,
filePath: string | undefined,
injectedFontPreloadTags: Set<string>
Expand All @@ -18,25 +16,16 @@ export function getPreloadableFonts(
return null
}
const filepathWithoutExtension = filePath.replace(/\.[^.]+$/, '')
const entryCSS =
clientReferenceManifest.entryCSSFiles[filepathWithoutExtension]

if (!entryCSS) {
return null
}

const fontFiles = new Set<string>()
let foundFontUsage = false

for (const cssModules of entryCSS.modules) {
const preloadedFontFiles = nextFontManifest.app[cssModules]
if (preloadedFontFiles) {
foundFontUsage = true
for (const fontFile of preloadedFontFiles) {
if (!injectedFontPreloadTags.has(fontFile)) {
fontFiles.add(fontFile)
injectedFontPreloadTags.add(fontFile)
}
const preloadedFontFiles = nextFontManifest.app[filepathWithoutExtension]
if (preloadedFontFiles) {
foundFontUsage = true
for (const fontFile of preloadedFontFiles) {
if (!injectedFontPreloadTags.has(fontFile)) {
fontFiles.add(fontFile)
injectedFontPreloadTags.add(fontFile)
}
}
}
Expand Down

0 comments on commit f1240b8

Please sign in to comment.