diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index fac7fe4b5a787c1..21209d17a4c18c4 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -98,6 +98,14 @@ const BABEL_CONFIG_FILES = [ 'babel.config.cjs', ] +function appDirIssuerLayer(layer: string) { + return ( + layer === WEBPACK_LAYERS.client || + layer === WEBPACK_LAYERS.server || + layer === WEBPACK_LAYERS.appClient + ) +} + export const getBabelConfigFile = async (dir: string) => { const babelConfigFile = await BABEL_CONFIG_FILES.reduce( async (memo: Promise, filename) => { @@ -1709,6 +1717,19 @@ export default async function getBaseWebpackConfig( : []), ...(hasServerComponents ? [ + // Alias next/head component to noop for RSC + { + test: codeCondition.test, + issuerLayer: appDirIssuerLayer, + resolve: { + alias: { + // Alias `next/dynamic` to React.lazy implementation for RSC + [require.resolve('next/head')]: require.resolve( + 'next/dist/client/components/noop-head' + ), + }, + }, + }, { // Alias react-dom for ReactDOM.preload usage. // Alias react for switching between default set and share subset. diff --git a/packages/next/client/components/noop-head.tsx b/packages/next/client/components/noop-head.tsx new file mode 100644 index 000000000000000..42fbdb94f230f03 --- /dev/null +++ b/packages/next/client/components/noop-head.tsx @@ -0,0 +1,11 @@ +import { warnOnce } from '../../shared/lib/utils/warn-once' + +if (process.env.NODE_ENV !== 'production') { + warnOnce( + `You're using \`next/head\` inside app directory, please migrate to \`head.js\`. Checkout https://beta.nextjs.org/docs/api-reference/file-conventions/head for details.` + ) +} + +export default function NoopHead() { + return null +} diff --git a/test/e2e/app-dir/head.test.ts b/test/e2e/app-dir/head.test.ts index c53d63211459d67..b9739f5938f8c34 100644 --- a/test/e2e/app-dir/head.test.ts +++ b/test/e2e/app-dir/head.test.ts @@ -1,9 +1,11 @@ +import fs from 'fs-extra' import path from 'path' import cheerio from 'cheerio' import { createNext, FileRef } from 'e2e-utils' import { NextInstance } from 'test/lib/next-modes/base' import { renderViaHTTP } from 'next-test-utils' import webdriver from 'next-webdriver' +import escapeStringRegexp from 'escape-string-regexp' describe('app dir head', () => { if ((global as any).isNextDeploy) { @@ -115,8 +117,38 @@ describe('app dir head', () => { }) it('should treat next/head as client components but not apply', async () => { + const errors = [] + next.on('stderr', (args) => { + errors.push(args) + }) const html = await renderViaHTTP(next.url, '/next-head') expect(html).not.toMatch(/legacy-head<\/title>/) + + if (globalThis.isNextDev) { + expect( + errors.some( + (output) => + output === + `You're using \`next/head\` inside app directory, please migrate to \`head.js\`. Checkout https://beta.nextjs.org/docs/api-reference/file-conventions/head for details.\n` + ) + ).toBe(true) + + const dynamicChunkPath = path.join( + next.testDir, + '.next', + 'static/chunks/_app-client_app_next-head_client-head_js.js' + ) + const content = await fs.readFile(dynamicChunkPath, 'utf-8') + expect(content).not.toMatch( + new RegExp(escapeStringRegexp(`next/dist/shared/lib/head.js`), 'm') + ) + expect(content).toMatch( + new RegExp( + escapeStringRegexp(`next/dist/client/components/noop-head.js`), + 'm' + ) + ) + } }) } diff --git a/test/e2e/app-dir/head/app/next-head/client-head.js b/test/e2e/app-dir/head/app/next-head/client-head.js new file mode 100644 index 000000000000000..76f0091490fb33a --- /dev/null +++ b/test/e2e/app-dir/head/app/next-head/client-head.js @@ -0,0 +1,9 @@ +import Head from 'next/head' + +export default function ClientHead() { + return ( + <Head> + <meta content="dynamic" property="dynamic-head" /> + </Head> + ) +} diff --git a/test/e2e/app-dir/head/app/next-head/dynamic-head.js b/test/e2e/app-dir/head/app/next-head/dynamic-head.js new file mode 100644 index 000000000000000..b7be754b7b16c08 --- /dev/null +++ b/test/e2e/app-dir/head/app/next-head/dynamic-head.js @@ -0,0 +1,7 @@ +'use client' + +import dynamic from 'next/dynamic' + +const ClientHead = dynamic(() => import('./client-head'), { ssr: false }) + +export default ClientHead diff --git a/test/e2e/app-dir/head/app/next-head/page.js b/test/e2e/app-dir/head/app/next-head/page.js index e55823f8d945c87..f8ab168be047927 100644 --- a/test/e2e/app-dir/head/app/next-head/page.js +++ b/test/e2e/app-dir/head/app/next-head/page.js @@ -1,4 +1,5 @@ import Head from 'next/head' +import DynamicHead from './dynamic-head' export default function page() { return ( @@ -6,6 +7,7 @@ export default function page() { <Head> <title>legacy-head +

page

)