diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index c1eedc43bce69..e2240be056748 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -1069,6 +1069,16 @@ export default async function loadConfig( } } + if ( + phase === PHASE_DEVELOPMENT_SERVER && + URL.canParse(userConfig.assetPrefix ?? '') + ) { + curLog.warn( + `Absolute URL assetPrefix "${userConfig.assetPrefix}" may disrupt development HMR.\n` + + 'See more info here https://nextjs.org/docs/app/api-reference/next-config-js/assetPrefix' + ) + } + if (userConfig.target && userConfig.target !== 'server') { throw new Error( `The "target" property is no longer supported in ${configFileName}.\n` + diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts index fc9dfd76a0678..70be360d51035 100644 --- a/packages/next/src/server/lib/router-server.ts +++ b/packages/next/src/server/lib/router-server.ts @@ -668,7 +668,15 @@ export async function initialize(opts: { // assetPrefix overrides basePath for HMR path if (assetPrefix) { hmrPrefix = normalizedAssetPrefix(assetPrefix) + + if (URL.canParse(hmrPrefix)) { + // remove trailing slash from pathname + // return empty string if pathname is '/' + // to avoid conflicts with '/_next' below + hmrPrefix = new URL(hmrPrefix).pathname.replace(/\/$/, '') + } } + const isHMRRequest = req.url.startsWith( ensureLeadingSlash(`${hmrPrefix}/_next/webpack-hmr`) ) diff --git a/test/development/app-dir/hmr-asset-prefix-full-url/app/layout.tsx b/test/development/app-dir/hmr-asset-prefix-full-url/app/layout.tsx new file mode 100644 index 0000000000000..a3a86a5ca1e12 --- /dev/null +++ b/test/development/app-dir/hmr-asset-prefix-full-url/app/layout.tsx @@ -0,0 +1,7 @@ +export default function Root({ children }) { + return ( + +
{children} + + ) +} diff --git a/test/development/app-dir/hmr-asset-prefix-full-url/app/page.tsx b/test/development/app-dir/hmr-asset-prefix-full-url/app/page.tsx new file mode 100644 index 0000000000000..fb9b4085fcd27 --- /dev/null +++ b/test/development/app-dir/hmr-asset-prefix-full-url/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + returnbefore edit
+} diff --git a/test/development/app-dir/hmr-asset-prefix-full-url/asset-prefix.test.ts b/test/development/app-dir/hmr-asset-prefix-full-url/asset-prefix.test.ts new file mode 100644 index 0000000000000..16b582dcf5295 --- /dev/null +++ b/test/development/app-dir/hmr-asset-prefix-full-url/asset-prefix.test.ts @@ -0,0 +1,32 @@ +import { createNext } from 'e2e-utils' +import { findPort, retry } from 'next-test-utils' + +describe('app-dir assetPrefix full URL', () => { + let next, forcedPort + beforeAll(async () => { + forcedPort = ((await findPort()) ?? '54321').toString() + + next = await createNext({ + files: __dirname, + forcedPort, + nextConfig: { + assetPrefix: `http://localhost:${forcedPort}`, + }, + }) + }) + afterAll(() => next.destroy()) + + it('should not break HMR when asset prefix set to full URL', async () => { + const browser = await next.browser('/') + const text = await browser.elementByCss('p').text() + expect(text).toBe('before edit') + + await next.patchFile('app/page.tsx', (content) => { + return content.replace('before', 'after') + }) + + await retry(async () => { + expect(await browser.elementByCss('p').text()).toContain('after edit') + }) + }) +})