diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 5559ff71b6f7e..cf3fcc41bcc37 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1967,12 +1967,10 @@ export default async function getBaseWebpackConfig( (isClient ? new FlightManifestPlugin({ dev, - fontLoaderTargets, }) : new FlightClientEntryPlugin({ dev, isEdgeServer, - fontLoaderTargets, })), !dev && isClient && diff --git a/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts index 08b02e64a702e..b35b000bdffac 100644 --- a/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts +++ b/packages/next/build/webpack/loaders/next-flight-client-entry-loader.ts @@ -1,5 +1,6 @@ import { RSC_MODULE_TYPES } from '../../../shared/lib/constants' import { getModuleBuildInfo } from './get-module-build-info' +import { regexCSS } from './utils' export type ClientComponentImports = string[] export type CssImports = Record @@ -22,7 +23,7 @@ export default async function transformSource(this: any): Promise { const requests = modules as string[] const code = requests // Filter out css files on the server - .filter((request) => (isServer ? !request.endsWith('.css') : true)) + .filter((request) => (isServer ? !regexCSS.test(request) : true)) .map((request) => request.endsWith('.css') ? `(() => import(/* webpackMode: "lazy" */ ${JSON.stringify(request)}))` diff --git a/packages/next/build/webpack/loaders/utils.ts b/packages/next/build/webpack/loaders/utils.ts index 6bbcacc2268a6..dbaf39b0508ac 100644 --- a/packages/next/build/webpack/loaders/utils.ts +++ b/packages/next/build/webpack/loaders/utils.ts @@ -10,3 +10,6 @@ export function isClientComponentModule(mod: { const hasClientDirective = mod.buildInfo.rsc?.type === RSC_MODULE_TYPES.client return hasClientDirective || imageRegex.test(mod.resource) } + +// TODO-APP: ensure .scss / .sass also works. +export const regexCSS = /\.css(\?.*)?$/ diff --git a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts index 88d7f385f4a47..31c8eb5dbcfb5 100644 --- a/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-client-entry-plugin.ts @@ -19,12 +19,11 @@ import { } from '../../../shared/lib/constants' import { FlightCSSManifest, traverseModules } from './flight-manifest-plugin' import { ASYNC_CLIENT_MODULES } from './flight-manifest-plugin' -import { isClientComponentModule } from '../loaders/utils' +import { isClientComponentModule, regexCSS } from '../loaders/utils' interface Options { dev: boolean isEdgeServer: boolean - fontLoaderTargets?: string[] } const PLUGIN_NAME = 'ClientEntryPlugin' @@ -34,21 +33,16 @@ export const injectedClientEntries = new Map() export const serverModuleIds = new Map() export const edgeServerModuleIds = new Map() -// TODO-APP: ensure .scss / .sass also works. -const regexCSS = /\.css$/ - // TODO-APP: move CSS manifest generation to the flight manifest plugin. const flightCSSManifest: FlightCSSManifest = {} export class FlightClientEntryPlugin { dev: boolean isEdgeServer: boolean - fontLoaderTargets?: string[] constructor(options: Options) { this.dev = options.dev this.isEdgeServer = options.isEdgeServer - this.fontLoaderTargets = options.fontLoaderTargets } apply(compiler: webpack.Compiler) { @@ -376,18 +370,14 @@ export class FlightClientEntryPlugin { // Request could be undefined or '' if (!rawRequest) return - const isFontLoader = this.fontLoaderTargets?.some((fontLoaderTarget) => - mod.userRequest.startsWith(`${fontLoaderTarget}?`) - ) + const isCSS = regexCSS.test(rawRequest) const modRequest: string | undefined = - !rawRequest.endsWith('.css') && + !isCSS && !rawRequest.startsWith('.') && !rawRequest.startsWith('/') && !rawRequest.startsWith(APP_DIR_ALIAS) - ? isFontLoader - ? mod.userRequest - : rawRequest - : mod.resourceResolveData?.path + ? rawRequest + : mod.resourceResolveData?.path + mod.resourceResolveData?.query // Ensure module is not walked again if it's already been visited if (!visitedBySegment[layoutOrPageRequest]) { @@ -401,7 +391,6 @@ export class FlightClientEntryPlugin { } visitedBySegment[layoutOrPageRequest].add(modRequest) - const isCSS = isFontLoader || regexCSS.test(modRequest) const isClientComponent = isClientComponentModule(mod) if (isCSS) { diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index 436a9121244f4..39d254dc49b7b 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -8,7 +8,7 @@ import { webpack, sources } from 'next/dist/compiled/webpack/webpack' import { FLIGHT_MANIFEST } from '../../../shared/lib/constants' import { relative } from 'path' -import { isClientComponentModule } from '../loaders/utils' +import { isClientComponentModule, regexCSS } from '../loaders/utils' import { edgeServerModuleIds, @@ -24,7 +24,6 @@ import { interface Options { dev: boolean - fontLoaderTargets?: string[] } /** @@ -111,11 +110,9 @@ export function traverseModules( export class FlightManifestPlugin { dev: Options['dev'] = false - fontLoaderTargets?: Options['fontLoaderTargets'] constructor(options: Options) { this.dev = options.dev - this.fontLoaderTargets = options.fontLoaderTargets } apply(compiler: webpack.Compiler) { @@ -156,7 +153,6 @@ export class FlightManifestPlugin { __edge_ssr_module_mapping__: {}, } const dev = this.dev - const fontLoaderTargets = this.fontLoaderTargets const clientRequestsSet = new Set() @@ -181,12 +177,8 @@ export class FlightManifestPlugin { id: ModuleId, mod: webpack.NormalModule ) { - const isFontLoader = fontLoaderTargets?.some((fontLoaderTarget) => - mod.resource?.startsWith(`${fontLoaderTarget}?`) - ) const isCSSModule = - isFontLoader || - mod.resource?.endsWith('.css') || + regexCSS.test(mod.resource) || mod.type === 'css/mini-extract' || (!!mod.loaders && (dev diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index cdc6b0588d338..4092ef78603a2 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -582,9 +582,8 @@ function getPreloadedFontFilesInlineLinkTags( const fontFiles = new Set() for (const css of layoutOrPageCss) { - // We only include the CSS if it's a global CSS, or it is used by this - // entrypoint. - if (serverCSSForEntries.includes(css) || !/\.module\.css/.test(css)) { + // We only include the CSS if it is used by this entrypoint. + if (serverCSSForEntries.includes(css)) { const preloadedFontFiles = fontLoaderManifest.app[css] if (preloadedFontFiles) { for (const fontFile of preloadedFontFiles) { diff --git a/test/e2e/app-dir/next-font.test.ts b/test/e2e/app-dir/next-font.test.ts index ba15c8e6f935d..faab97b58644d 100644 --- a/test/e2e/app-dir/next-font.test.ts +++ b/test/e2e/app-dir/next-font.test.ts @@ -232,133 +232,135 @@ describe('app dir next-font', () => { }) }) - describe('preload', () => { - it('should preload correctly with server components', async () => { - const html = await renderViaHTTP(next.url, '/') - const $ = cheerio.load(html) - - // Preconnect - expect($('link[rel="preconnect"]').length).toBe(0) - - expect($('link[as="font"]').length).toBe(3) - expect($('link[as="font"]').get(0).attribs).toEqual({ - as: 'font', - crossorigin: '', - href: '/_next/static/media/e9b9dc0d8ba35f48.p.woff2', - rel: 'preload', - type: 'font/woff2', + if (!isDev) { + describe('preload', () => { + it('should preload correctly with server components', async () => { + const html = await renderViaHTTP(next.url, '/') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(0) + + expect($('link[as="font"]').length).toBe(3) + expect($('link[as="font"]').get(0).attribs).toEqual({ + as: 'font', + crossorigin: '', + href: '/_next/static/media/e9b9dc0d8ba35f48.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + expect($('link[as="font"]').get(1).attribs).toEqual({ + as: 'font', + crossorigin: '', + href: '/_next/static/media/b61859a50be14c53.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + expect($('link[as="font"]').get(2).attribs).toEqual({ + as: 'font', + crossorigin: '', + href: '/_next/static/media/b2104791981359ae.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) }) - expect($('link[as="font"]').get(1).attribs).toEqual({ - as: 'font', - crossorigin: '', - href: '/_next/static/media/b2104791981359ae.p.woff2', - rel: 'preload', - type: 'font/woff2', - }) - expect($('link[as="font"]').get(2).attribs).toEqual({ - as: 'font', - crossorigin: '', - href: '/_next/static/media/b61859a50be14c53.p.woff2', - rel: 'preload', - type: 'font/woff2', - }) - }) - it('should preload correctly with client components', async () => { - const html = await renderViaHTTP(next.url, '/client') - const $ = cheerio.load(html) - - // Preconnect - expect($('link[rel="preconnect"]').length).toBe(0) - - expect($('link[as="font"]').length).toBe(3) - // From root layout - expect($('link[as="font"]').get(0).attribs).toEqual({ - as: 'font', - crossorigin: '', - href: '/_next/static/media/e9b9dc0d8ba35f48.p.woff2', - rel: 'preload', - type: 'font/woff2', + it('should preload correctly with client components', async () => { + const html = await renderViaHTTP(next.url, '/client') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(0) + + expect($('link[as="font"]').length).toBe(3) + // From root layout + expect($('link[as="font"]').get(0).attribs).toEqual({ + as: 'font', + crossorigin: '', + href: '/_next/static/media/e9b9dc0d8ba35f48.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + + expect($('link[as="font"]').get(1).attribs).toEqual({ + as: 'font', + crossorigin: '', + href: '/_next/static/media/e1053f04babc7571.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + expect($('link[as="font"]').get(2).attribs).toEqual({ + as: 'font', + crossorigin: '', + href: '/_next/static/media/feab2c68f2a8e9a4.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) }) - expect($('link[as="font"]').get(1).attribs).toEqual({ - as: 'font', - crossorigin: '', - href: '/_next/static/media/e1053f04babc7571.p.woff2', - rel: 'preload', - type: 'font/woff2', + it('should preload correctly with layout using fonts', async () => { + const html = await renderViaHTTP(next.url, '/layout-with-fonts') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(0) + + expect($('link[as="font"]').length).toBe(2) + // From root layout + expect($('link[as="font"]').get(0).attribs).toEqual({ + as: 'font', + crossorigin: '', + href: '/_next/static/media/e9b9dc0d8ba35f48.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + + expect($('link[as="font"]').get(1).attribs).toEqual({ + as: 'font', + crossorigin: '', + href: '/_next/static/media/75c5faeeb9c86969.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) }) - expect($('link[as="font"]').get(2).attribs).toEqual({ - as: 'font', - crossorigin: '', - href: '/_next/static/media/feab2c68f2a8e9a4.p.woff2', - rel: 'preload', - type: 'font/woff2', - }) - }) - it('should preload correctly with layout using fonts', async () => { - const html = await renderViaHTTP(next.url, '/layout-with-fonts') - const $ = cheerio.load(html) - - // Preconnect - expect($('link[rel="preconnect"]').length).toBe(0) - - expect($('link[as="font"]').length).toBe(2) - // From root layout - expect($('link[as="font"]').get(0).attribs).toEqual({ - as: 'font', - crossorigin: '', - href: '/_next/static/media/e9b9dc0d8ba35f48.p.woff2', - rel: 'preload', - type: 'font/woff2', - }) - - expect($('link[as="font"]').get(1).attribs).toEqual({ - as: 'font', - crossorigin: '', - href: '/_next/static/media/75c5faeeb9c86969.p.woff2', - rel: 'preload', - type: 'font/woff2', - }) - }) - - it('should preload correctly with page using fonts', async () => { - const html = await renderViaHTTP(next.url, '/page-with-fonts') - const $ = cheerio.load(html) - - // Preconnect - expect($('link[rel="preconnect"]').length).toBe(0) - - expect($('link[as="font"]').length).toBe(2) - // From root layout - expect($('link[as="font"]').get(0).attribs).toEqual({ - as: 'font', - crossorigin: '', - href: '/_next/static/media/e9b9dc0d8ba35f48.p.woff2', - rel: 'preload', - type: 'font/woff2', - }) - - expect($('link[as="font"]').get(1).attribs).toEqual({ - as: 'font', - crossorigin: '', - href: '/_next/static/media/568e4c6d8123c4d6.p.woff2', - rel: 'preload', - type: 'font/woff2', + it('should preload correctly with page using fonts', async () => { + const html = await renderViaHTTP(next.url, '/page-with-fonts') + const $ = cheerio.load(html) + + // Preconnect + expect($('link[rel="preconnect"]').length).toBe(0) + + expect($('link[as="font"]').length).toBe(2) + // From root layout + expect($('link[as="font"]').get(0).attribs).toEqual({ + as: 'font', + crossorigin: '', + href: '/_next/static/media/e9b9dc0d8ba35f48.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) + + expect($('link[as="font"]').get(1).attribs).toEqual({ + as: 'font', + crossorigin: '', + href: '/_next/static/media/568e4c6d8123c4d6.p.woff2', + rel: 'preload', + type: 'font/woff2', + }) }) }) - }) + } if (isDev) { describe('Dev errors', () => { it('should recover on font loader error', async () => { const browser = await webdriver(next.url, '/') - const font1Content = await next.readFile('fonts/font1.js') + const font1Content = await next.readFile('fonts/index.js') // Break file await next.patchFile( - 'fonts/font1.js', + 'fonts/index.js', font1Content.replace('./font1.woff2', './does-not-exist.woff2') ) expect(await hasRedbox(browser, true)).toBeTrue() @@ -367,7 +369,7 @@ describe('app dir next-font', () => { ) // Fix file - await next.patchFile('fonts/font1.js', font1Content) + await next.patchFile('fonts/index.js', font1Content) await browser.waitForElementByCss('#root-page') }) }) diff --git a/test/e2e/app-dir/next-font/app/Comp.js b/test/e2e/app-dir/next-font/app/Comp.js index 37719b67dfbc2..563dead83bb3a 100644 --- a/test/e2e/app-dir/next-font/app/Comp.js +++ b/test/e2e/app-dir/next-font/app/Comp.js @@ -1,4 +1,4 @@ -import font3 from '../fonts/font3' +import { font3 } from '../fonts' export default function Component() { return ( diff --git a/test/e2e/app-dir/next-font/app/client/Comp.js b/test/e2e/app-dir/next-font/app/client/Comp.js index f551e14d9f444..1bf584eb62521 100644 --- a/test/e2e/app-dir/next-font/app/client/Comp.js +++ b/test/e2e/app-dir/next-font/app/client/Comp.js @@ -1,5 +1,5 @@ 'use client' -import font6 from '../../fonts/font6' +import { font6 } from '../../fonts' export default function Component() { return ( diff --git a/test/e2e/app-dir/next-font/app/client/layout.js b/test/e2e/app-dir/next-font/app/client/layout.js index c7b24ca524bd1..16215505a495f 100644 --- a/test/e2e/app-dir/next-font/app/client/layout.js +++ b/test/e2e/app-dir/next-font/app/client/layout.js @@ -1,5 +1,5 @@ 'use client' -import font4 from '../../fonts/font4' +import { font4 } from '../../fonts' export default function Root({ children }) { return ( diff --git a/test/e2e/app-dir/next-font/app/client/page.js b/test/e2e/app-dir/next-font/app/client/page.js index d3204c392bd43..9b22f3e5b97fd 100644 --- a/test/e2e/app-dir/next-font/app/client/page.js +++ b/test/e2e/app-dir/next-font/app/client/page.js @@ -1,6 +1,6 @@ 'use client' import Comp from './Comp' -import font5 from '../../fonts/font5' +import { font5 } from '../../fonts' export default function HomePage() { return ( diff --git a/test/e2e/app-dir/next-font/app/layout.js b/test/e2e/app-dir/next-font/app/layout.js index 15d1cf9dff20f..49397b965799d 100644 --- a/test/e2e/app-dir/next-font/app/layout.js +++ b/test/e2e/app-dir/next-font/app/layout.js @@ -1,4 +1,4 @@ -import font1 from '../fonts/font1' +import { font1 } from '../fonts' export default function Root({ children }) { return ( diff --git a/test/e2e/app-dir/next-font/app/page.js b/test/e2e/app-dir/next-font/app/page.js index 80dbc796363ca..4d2b19abd5385 100644 --- a/test/e2e/app-dir/next-font/app/page.js +++ b/test/e2e/app-dir/next-font/app/page.js @@ -1,6 +1,5 @@ import Comp from './Comp' -import font1 from '../fonts/font1' -import font2 from '../fonts/font2' +import { font1, font2 } from '../fonts' export default function HomePage() { return ( diff --git a/test/e2e/app-dir/next-font/fonts/font1.js b/test/e2e/app-dir/next-font/fonts/font1.js deleted file mode 100644 index 7d768d0f49b7b..0000000000000 --- a/test/e2e/app-dir/next-font/fonts/font1.js +++ /dev/null @@ -1,5 +0,0 @@ -import localFont from '@next/font/local' - -const font1 = localFont({ src: './font1.woff2', variable: '--font-1' }) - -export default font1 diff --git a/test/e2e/app-dir/next-font/fonts/font2.js b/test/e2e/app-dir/next-font/fonts/font2.js deleted file mode 100644 index bfbc65a5a2349..0000000000000 --- a/test/e2e/app-dir/next-font/fonts/font2.js +++ /dev/null @@ -1,5 +0,0 @@ -import localFont from '@next/font/local' - -export const font2 = localFont({ src: './font2.woff2', variable: '--font-2' }) - -export default font2 diff --git a/test/e2e/app-dir/next-font/fonts/font3.js b/test/e2e/app-dir/next-font/fonts/font3.js deleted file mode 100644 index 7d482753f77b5..0000000000000 --- a/test/e2e/app-dir/next-font/fonts/font3.js +++ /dev/null @@ -1,9 +0,0 @@ -import localFont from '@next/font/local' - -export const font3 = localFont({ - src: './font3.woff2', - weight: '900', - style: 'italic', -}) - -export default font3 diff --git a/test/e2e/app-dir/next-font/fonts/font4.js b/test/e2e/app-dir/next-font/fonts/font4.js deleted file mode 100644 index 2233f7db242d5..0000000000000 --- a/test/e2e/app-dir/next-font/fonts/font4.js +++ /dev/null @@ -1,5 +0,0 @@ -import localFont from '@next/font/local' - -export const font4 = localFont({ src: './font4.woff2', weight: '100' }) - -export default font4 diff --git a/test/e2e/app-dir/next-font/fonts/font5.js b/test/e2e/app-dir/next-font/fonts/font5.js deleted file mode 100644 index 828fc27d22725..0000000000000 --- a/test/e2e/app-dir/next-font/fonts/font5.js +++ /dev/null @@ -1,9 +0,0 @@ -import localFont from '@next/font/local' - -export const font5 = localFont({ - src: './font5.woff2', - style: 'italic', - preload: false, -}) - -export default font5 diff --git a/test/e2e/app-dir/next-font/fonts/font6.js b/test/e2e/app-dir/next-font/fonts/font6.js deleted file mode 100644 index f2b1e43e4a4b0..0000000000000 --- a/test/e2e/app-dir/next-font/fonts/font6.js +++ /dev/null @@ -1,5 +0,0 @@ -import localFont from '@next/font/local' - -export const font6 = localFont({ src: './font6.woff2' }) - -export default font6 diff --git a/test/e2e/app-dir/next-font/fonts/index.js b/test/e2e/app-dir/next-font/fonts/index.js new file mode 100644 index 0000000000000..55074a79d13cc --- /dev/null +++ b/test/e2e/app-dir/next-font/fonts/index.js @@ -0,0 +1,16 @@ +import localFont from '@next/font/local' + +export const font1 = localFont({ src: './font1.woff2', variable: '--font-1' }) +export const font2 = localFont({ src: './font2.woff2', variable: '--font-2' }) +export const font3 = localFont({ + src: './font3.woff2', + weight: '900', + style: 'italic', +}) +export const font4 = localFont({ src: './font4.woff2', weight: '100' }) +export const font5 = localFont({ + src: './font5.woff2', + style: 'italic', + preload: false, +}) +export const font6 = localFont({ src: './font6.woff2' })