diff --git a/packages/next/src/client/components/react-dev-overlay/hot-reloader-client.tsx b/packages/next/src/client/components/react-dev-overlay/hot-reloader-client.tsx index 78ea9c886a3e6..f4a87d7c03901 100644 --- a/packages/next/src/client/components/react-dev-overlay/hot-reloader-client.tsx +++ b/packages/next/src/client/components/react-dev-overlay/hot-reloader-client.tsx @@ -428,7 +428,7 @@ function processMessage( router.fastRefresh() dispatcher.onRefresh() }) - } else { + } else if (pageRes.status === 404) { // We are still on the page, // dispatch an error so it's caught by the NotFound handler dispatcher.onNotFound() @@ -437,6 +437,9 @@ function processMessage( } return } + case 'devPagesManifestUpdate': { + return + } default: { throw new Error('Unexpected action ' + obj.action) } diff --git a/packages/next/src/client/dev/amp-dev.ts b/packages/next/src/client/dev/amp-dev.ts index 4eea82dbd0863..3e39e3e025476 100644 --- a/packages/next/src/client/dev/amp-dev.ts +++ b/packages/next/src/client/dev/amp-dev.ts @@ -85,8 +85,12 @@ addMessageListener((event) => { try { const message = JSON.parse(event.data) - // action `serverError` is not for amp-dev - if (message.action === 'serverError') return + // actions which are not related to amp-dev + if ( + message.action === 'serverError' || + message.action === 'devPagesManifestUpdate' + ) + return if (message.action === 'sync' || message.action === 'built') { if (!message.hash) { return diff --git a/packages/next/src/client/dev/error-overlay/hot-dev-client.ts b/packages/next/src/client/dev/error-overlay/hot-dev-client.ts index 031e670b47b0d..374ffaec10e89 100644 --- a/packages/next/src/client/dev/error-overlay/hot-dev-client.ts +++ b/packages/next/src/client/dev/error-overlay/hot-dev-client.ts @@ -276,8 +276,6 @@ function processMessage(obj: any) { return handleSuccess() } case 'serverComponentChanges': { - // Server component changes don't apply to `pages`. - // TODO-APP: Remove reload once the correct overlay is rendered on initial page load in app dir window.location.reload() return } diff --git a/packages/next/src/client/dev/webpack-hot-middleware-client.ts b/packages/next/src/client/dev/webpack-hot-middleware-client.ts index 91f14b488c39b..c524f9f379d19 100644 --- a/packages/next/src/client/dev/webpack-hot-middleware-client.ts +++ b/packages/next/src/client/dev/webpack-hot-middleware-client.ts @@ -45,7 +45,10 @@ export default () => { } return } - if (obj.action === 'serverError') { + if ( + obj.action === 'serverError' || + obj.action === 'devPagesManifestUpdate' + ) { return } throw new Error('Unexpected action ' + obj.action) diff --git a/packages/next/src/server/dev/hot-reloader.ts b/packages/next/src/server/dev/hot-reloader.ts index 86aeacd5feec4..65b2608bc3734 100644 --- a/packages/next/src/server/dev/hot-reloader.ts +++ b/packages/next/src/server/dev/hot-reloader.ts @@ -201,6 +201,7 @@ export default class HotReloader { staleness: 'unknown', installed: '0.0.0', } + private reloadAfterInvalidation: boolean = false public multiCompiler?: webpack.MultiCompiler public activeConfigs?: Array< UnwrapPromise> @@ -338,6 +339,14 @@ export default class HotReloader { } } + public async refreshServerComponents(): Promise { + this.send({ + action: 'serverComponentChanges', + // TODO: granular reloading of changes + // entrypoints: serverComponentChanges, + }) + } + public onHMR(req: IncomingMessage, _socket: Duplex, head: Buffer) { wsServer.handleUpgrade(req, req.socket, head, (client) => { this.webpackHotMiddleware?.onHMR(client) @@ -1201,6 +1210,9 @@ export default class HotReloader { ) this.multiCompiler.hooks.done.tap('NextjsHotReloaderForServer', () => { + const reloadAfterInvalidation = this.reloadAfterInvalidation + this.reloadAfterInvalidation = false + const serverOnlyChanges = difference( changedServerPages, changedClientPages @@ -1233,12 +1245,12 @@ export default class HotReloader { }) } - if (changedServerComponentPages.size || changedCSSImportPages.size) { - this.send({ - action: 'serverComponentChanges', - // TODO: granular reloading of changes - // entrypoints: serverComponentChanges, - }) + if ( + changedServerComponentPages.size || + changedCSSImportPages.size || + reloadAfterInvalidation + ) { + this.refreshServerComponents() } changedClientPages.clear() @@ -1336,7 +1348,13 @@ export default class HotReloader { ] } - public invalidate() { + public invalidate( + { reloadAfterInvalidation }: { reloadAfterInvalidation: boolean } = { + reloadAfterInvalidation: false, + } + ) { + // Cache the `reloadAfterInvalidation` flag, and use it to reload the page when compilation is done + this.reloadAfterInvalidation = reloadAfterInvalidation const outputPath = this.multiCompiler?.outputPath return outputPath && getInvalidator(outputPath)?.invalidate() } diff --git a/packages/next/src/server/dev/next-dev-server.ts b/packages/next/src/server/dev/next-dev-server.ts index 0b41f27090f6b..d0a7d799213be 100644 --- a/packages/next/src/server/dev/next-dev-server.ts +++ b/packages/next/src/server/dev/next-dev-server.ts @@ -440,6 +440,7 @@ export default class DevServer extends Server { const pagesPageFilePaths = new Map() let envChange = false + let clientRouterFilterChange = false let tsconfigChange = false let conflictingPageChange = 0 @@ -636,7 +637,7 @@ export default class DevServer extends Server { JSON.stringify(previousClientRouterFilters) !== JSON.stringify(clientRouterFilters) ) { - envChange = true + clientRouterFilterChange = true previousClientRouterFilters = clientRouterFilters } } @@ -651,7 +652,7 @@ export default class DevServer extends Server { .catch(() => {}) } - if (envChange || tsconfigChange) { + if (clientRouterFilterChange || envChange || tsconfigChange) { if (envChange) { this.loadEnvConfig({ dev: true, @@ -713,7 +714,7 @@ export default class DevServer extends Server { }) } - if (envChange) { + if (envChange || clientRouterFilterChange) { config.plugins?.forEach((plugin: any) => { // we look for the DefinePlugin definitions so we can // update them on the active compilers @@ -743,7 +744,9 @@ export default class DevServer extends Server { }) } }) - this.hotReloader?.invalidate() + this.hotReloader?.invalidate({ + reloadAfterInvalidation: envChange, + }) } if (nestedMiddleware.length > 0) { diff --git a/test/development/app-hmr/.env.development.local b/test/development/app-hmr/.env.development.local new file mode 100644 index 0000000000000..3cb9cb77d100b --- /dev/null +++ b/test/development/app-hmr/.env.development.local @@ -0,0 +1 @@ +MY_DEVICE="mac" \ No newline at end of file diff --git a/test/development/app-hmr/app/env/edge/page.jsx b/test/development/app-hmr/app/env/edge/page.jsx new file mode 100644 index 0000000000000..9f208857d94ee --- /dev/null +++ b/test/development/app-hmr/app/env/edge/page.jsx @@ -0,0 +1,5 @@ +export default function Page() { + return

{process.env.MY_DEVICE}

+} + +export const runtime = 'edge' diff --git a/test/development/app-hmr/app/env/node/page.jsx b/test/development/app-hmr/app/env/node/page.jsx new file mode 100644 index 0000000000000..68b359703156a --- /dev/null +++ b/test/development/app-hmr/app/env/node/page.jsx @@ -0,0 +1,3 @@ +export default function Page() { + return

{process.env.MY_DEVICE}

+} diff --git a/test/development/app-hmr/hmr.test.ts b/test/development/app-hmr/hmr.test.ts index d96b28e207b7f..7fcc8ba5b053d 100644 --- a/test/development/app-hmr/hmr.test.ts +++ b/test/development/app-hmr/hmr.test.ts @@ -1,15 +1,16 @@ import { createNextDescribe } from 'e2e-utils' import { check } from 'next-test-utils' +const envFile = '.env.development.local' + createNextDescribe( `app-dir-hmr`, { files: __dirname, }, - ({ next }) => { + ({ next, isTurbopack }) => { describe('filesystem changes', () => { it('should not break when renaming a folder', async () => { - console.log(next.url) const browser = await next.browser('/folder') const text = await browser.elementByCss('h1').text() expect(text).toBe('Hello') @@ -33,6 +34,44 @@ createNextDescribe( await next.renameFolder('app/folder-renamed', 'app/folder') } }) + + if (!isTurbopack) { + it('should update server components pages when env files is changed (nodejs)', async () => { + const envContent = await next.readFile(envFile) + const browser = await next.browser('/env/node') + expect(await browser.elementByCss('p').text()).toBe('mac') + await next.patchFile(envFile, 'MY_DEVICE="ipad"') + + try { + await check(async () => { + expect(await browser.elementByCss('p').text()).toBe('ipad') + return 'success' + }, /success/) + } finally { + await next.patchFile(envFile, envContent) + } + }) + + it('should update server components pages when env files is changed (edge)', async () => { + const envContent = await next.readFile(envFile) + const browser = await next.browser('/env/edge') + expect(await browser.elementByCss('p').text()).toBe('mac') + await next.patchFile(envFile, 'MY_DEVICE="ipad"') + + try { + await check(async () => { + expect(await browser.elementByCss('p').text()).toBe('ipad') + return 'success' + }, /success/) + } finally { + await next.patchFile(envFile, envContent) + } + }) + } + + it('should have no unexpected action error for hmr', async () => { + expect(next.cliOutput).not.toContain('Unexpected action') + }) }) } )