From f1240b8efd24b03ce1b549d93d0de79dc0600b9b Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 26 Jun 2023 22:43:37 +0200 Subject: [PATCH] Refactor `next-font-manifest-plugin` (#51835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. --- .../next-core/js/src/entry/app/manifest.ts | 5 +- packages/next/src/build/webpack-config.ts | 2 +- .../webpack/plugins/flight-manifest-plugin.ts | 45 ++------- .../plugins/next-font-manifest-plugin.ts | 94 ++++++++++--------- packages/next/src/build/webpack/utils.ts | 6 +- .../next/src/server/app-render/app-render.tsx | 2 - .../app-render/get-css-inlined-link-tags.tsx | 2 +- .../app-render/get-preloadable-fonts.tsx | 25 ++--- 8 files changed, 73 insertions(+), 108 deletions(-) diff --git a/packages/next-swc/crates/next-core/js/src/entry/app/manifest.ts b/packages/next-swc/crates/next-core/js/src/entry/app/manifest.ts index 912f3ae75d95..2a9fd6851739 100644 --- a/packages/next-swc/crates/next-core/js/src/entry/app/manifest.ts +++ b/packages/next-swc/crates/next-core/js/src/entry/app/manifest.ts @@ -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) } }, } diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 698a2804ef31..ffebeb723364 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -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 && diff --git a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts index d0a49f12a69a..b64bf5ec1ca3 100644 --- a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts @@ -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' @@ -68,10 +67,7 @@ export type ClientReferenceManifest = { [moduleId: string]: ManifestNode } entryCSSFiles: { - [entry: string]: { - modules: string[] - files: string[] - } + [entry: string]: string[] } } @@ -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) => { @@ -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 - 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 diff --git a/packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts b/packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts index f0dac92b93dd..7c5633afea68 100644 --- a/packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts +++ b/packages/next/src/build/webpack/plugins/next-font-manifest-plugin.ts @@ -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 @@ -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, @@ -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/. diff --git a/packages/next/src/build/webpack/utils.ts b/packages/next/src/build/webpack/utils.ts index ca9d76c12015..8470a323642e 100644 --- a/packages/next/src/build/webpack/utils.ts +++ b/packages/next/src/build/webpack/utils.ts @@ -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 diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 4caa4eccf065..3a4c6c9dcb0d 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -451,7 +451,6 @@ export async function renderToHTMLOrFlight( const preloadedFontFiles = layoutOrPagePath ? getPreloadableFonts( - clientReferenceManifest, nextFontManifest, layoutOrPagePath, injectedFontPreloadTagsWithCurrentLayout @@ -1104,7 +1103,6 @@ export async function renderToHTMLOrFlight( true ) getPreloadableFonts( - clientReferenceManifest, nextFontManifest, layoutPath, injectedFontPreloadTagsWithCurrentLayout diff --git a/packages/next/src/server/app-render/get-css-inlined-link-tags.tsx b/packages/next/src/server/app-render/get-css-inlined-link-tags.tsx index 7a82bb240090..0311a3d92d5f 100644 --- a/packages/next/src/server/app-render/get-css-inlined-link-tags.tsx +++ b/packages/next/src/server/app-render/get-css-inlined-link-tags.tsx @@ -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) diff --git a/packages/next/src/server/app-render/get-preloadable-fonts.tsx b/packages/next/src/server/app-render/get-preloadable-fonts.tsx index f4949b7a41c8..860e4e1a5042 100644 --- a/packages/next/src/server/app-render/get-preloadable-fonts.tsx +++ b/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 @@ -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 @@ -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() 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) } } }