Skip to content

Commit 2c8736a

Browse files
authored
Turbopack: Implement HMR for module-scoped environment variable changes (#68209)
While we currently refetch RSC updates on changes to the environment (e.g. `.env` files), we never cleared the require cache to re-evaluate RSCs with the new environment. This implements that. Test Plan: Added tests. `TURBOPACK=1 pnpm test-dev test/development/app-hmr/hmr.test.ts`
1 parent c66f7ed commit 2c8736a

File tree

4 files changed

+94
-109
lines changed

4 files changed

+94
-109
lines changed

packages/next/src/server/dev/hot-reloader-turbopack.ts

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export async function createHotReloaderTurbopack(
157157
opts.onCleanup(() => project.onExit())
158158
const entrypointsSubscription = project.entrypointsSubscribe()
159159

160+
const currentWrittenEntrypoints: Map<EntryKey, WrittenEndpoint> = new Map()
160161
const currentEntrypoints: Entrypoints = {
161162
global: {
162163
app: undefined,
@@ -193,35 +194,47 @@ export async function createHotReloaderTurbopack(
193194

194195
function clearRequireCache(
195196
key: EntryKey,
196-
writtenEndpoint: WrittenEndpoint
197+
writtenEndpoint: WrittenEndpoint,
198+
{
199+
force,
200+
}: {
201+
// Always clear the cache, don't check if files have changed
202+
force?: boolean
203+
} = {}
197204
): void {
198-
// Figure out if the server files have changed
199-
let hasChange = false
200-
for (const { path, contentHash } of writtenEndpoint.serverPaths) {
201-
// We ignore source maps
202-
if (path.endsWith('.map')) continue
203-
const localKey = `${key}:${path}`
204-
const localHash = serverPathState.get(localKey)
205-
const globalHash = serverPathState.get(path)
206-
if (
207-
(localHash && localHash !== contentHash) ||
208-
(globalHash && globalHash !== contentHash)
209-
) {
210-
hasChange = true
211-
serverPathState.set(key, contentHash)
205+
if (force) {
206+
for (const { path, contentHash } of writtenEndpoint.serverPaths) {
212207
serverPathState.set(path, contentHash)
213-
} else {
214-
if (!localHash) {
208+
}
209+
} else {
210+
// Figure out if the server files have changed
211+
let hasChange = false
212+
for (const { path, contentHash } of writtenEndpoint.serverPaths) {
213+
// We ignore source maps
214+
if (path.endsWith('.map')) continue
215+
const localKey = `${key}:${path}`
216+
const localHash = serverPathState.get(localKey)
217+
const globalHash = serverPathState.get(path)
218+
if (
219+
(localHash && localHash !== contentHash) ||
220+
(globalHash && globalHash !== contentHash)
221+
) {
222+
hasChange = true
215223
serverPathState.set(key, contentHash)
216-
}
217-
if (!globalHash) {
218224
serverPathState.set(path, contentHash)
225+
} else {
226+
if (!localHash) {
227+
serverPathState.set(key, contentHash)
228+
}
229+
if (!globalHash) {
230+
serverPathState.set(path, contentHash)
231+
}
219232
}
220233
}
221-
}
222234

223-
if (!hasChange) {
224-
return
235+
if (!hasChange) {
236+
return
237+
}
225238
}
226239

227240
const hasAppPaths = writtenEndpoint.serverPaths.some(({ path: p }) =>
@@ -477,6 +490,7 @@ export async function createHotReloaderTurbopack(
477490

478491
hooks: {
479492
handleWrittenEndpoint: (id, result) => {
493+
currentWrittenEntrypoints.set(id, result)
480494
clearRequireCache(id, result)
481495
},
482496
propagateServerField: propagateServerField.bind(null, opts),
@@ -754,6 +768,10 @@ export async function createHotReloaderTurbopack(
754768
reloadAfterInvalidation,
755769
}) {
756770
if (reloadAfterInvalidation) {
771+
for (const [key, entrypoint] of currentWrittenEntrypoints) {
772+
clearRequireCache(key, entrypoint, { force: true })
773+
}
774+
757775
await clearAllModuleContexts()
758776
this.send({
759777
action: HMR_ACTIONS_SENT_TO_BROWSER.SERVER_COMPONENT_CHANGES,
@@ -822,6 +840,7 @@ export async function createHotReloaderTurbopack(
822840
subscribeToChanges,
823841
handleWrittenEndpoint: (id, result) => {
824842
clearRequireCache(id, result)
843+
currentWrittenEntrypoints.set(id, result)
825844
assetMapper.setPathsForKey(id, result.clientPaths)
826845
},
827846
},
@@ -879,6 +898,7 @@ export async function createHotReloaderTurbopack(
879898
hooks: {
880899
subscribeToChanges,
881900
handleWrittenEndpoint: (id, result) => {
901+
currentWrittenEntrypoints.set(id, result)
882902
clearRequireCache(id, result)
883903
assetMapper.setPathsForKey(id, result.clientPaths)
884904
},
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const MY_DEVICE = process.env.MY_DEVICE?.slice()
2+
3+
export default function Page() {
4+
return <p>{MY_DEVICE}</p>
5+
}
6+
7+
export const runtime = 'edge'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const MY_DEVICE = process.env.MY_DEVICE?.slice()
2+
3+
export default function Page() {
4+
return <p>{MY_DEVICE}</p>
5+
}

test/development/app-hmr/hmr.test.ts

Lines changed: 40 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -140,107 +140,60 @@ describe(`app-dir-hmr`, () => {
140140
}
141141
})
142142

143-
it('should update server components pages when env files is changed (nodejs)', async () => {
144-
const browser = await next.browser('/env/node')
145-
expect(await browser.elementByCss('p').text()).toBe('mac')
146-
await next.patchFile(envFile, 'MY_DEVICE="ipad"')
147-
148-
const logs = await browser.log()
149-
await retry(async () => {
150-
expect(logs).toEqual(
151-
expect.arrayContaining([
152-
expect.objectContaining({
153-
message: '[Fast Refresh] rebuilding',
154-
source: 'log',
155-
}),
156-
])
157-
)
158-
})
143+
it.each(['node', 'node-module-var', 'edge', 'edge-module-var'])(
144+
'should update server components pages when env files is changed (%s)',
145+
async (page) => {
146+
const browser = await next.browser(`/env/${page}`)
147+
expect(await browser.elementByCss('p').text()).toBe('mac')
148+
await next.patchFile(envFile, 'MY_DEVICE="ipad"')
159149

160-
try {
150+
const logs = await browser.log()
161151
await retry(async () => {
162-
expect(await browser.elementByCss('p').text()).toBe('ipad')
163-
})
164-
165-
if (process.env.TURBOPACK) {
166-
// FIXME: Turbopack should have matching "done in" for each "rebuilding"
167-
expect(logs).not.toEqual(
168-
expect.arrayContaining([
169-
expect.objectContaining({
170-
message: expect.stringContaining('[Fast Refresh] done in'),
171-
source: 'log',
172-
}),
173-
])
174-
)
175-
} else {
176152
expect(logs).toEqual(
177153
expect.arrayContaining([
178154
expect.objectContaining({
179-
message: expect.stringContaining('[Fast Refresh] done in'),
155+
message: '[Fast Refresh] rebuilding',
180156
source: 'log',
181157
}),
182158
])
183159
)
184-
}
185-
} finally {
186-
// TOOD: use sandbox instead
187-
await next.patchFile(envFile, 'MY_DEVICE="mac"')
188-
await retry(async () => {
189-
expect(await browser.elementByCss('p').text()).toBe('mac')
190160
})
191-
}
192-
})
193161

194-
it('should update server components pages when env files is changed (edge)', async () => {
195-
const browser = await next.browser('/env/edge')
196-
expect(await browser.elementByCss('p').text()).toBe('mac')
197-
await next.patchFile(envFile, 'MY_DEVICE="ipad"')
198-
199-
const logs = await browser.log()
200-
await retry(async () => {
201-
expect(logs).toEqual(
202-
expect.arrayContaining([
203-
expect.objectContaining({
204-
message: '[Fast Refresh] rebuilding',
205-
source: 'log',
206-
}),
207-
])
208-
)
209-
})
210-
211-
try {
212-
await retry(async () => {
213-
expect(await browser.elementByCss('p').text()).toBe('ipad')
214-
})
162+
try {
163+
await retry(async () => {
164+
expect(await browser.elementByCss('p').text()).toBe('ipad')
165+
})
215166

216-
if (process.env.TURBOPACK) {
217-
// FIXME: Turbopack should have matching "done in" for each "rebuilding"
218-
expect(logs).not.toEqual(
219-
expect.arrayContaining([
220-
expect.objectContaining({
221-
message: expect.stringContaining('[Fast Refresh] done in'),
222-
source: 'log',
223-
}),
224-
])
225-
)
226-
} else {
227-
expect(logs).toEqual(
228-
expect.arrayContaining([
229-
expect.objectContaining({
230-
message: expect.stringContaining('[Fast Refresh] done in'),
231-
source: 'log',
232-
}),
233-
])
234-
)
167+
if (process.env.TURBOPACK) {
168+
// FIXME: Turbopack should have matching "done in" for each "rebuilding"
169+
expect(logs).not.toEqual(
170+
expect.arrayContaining([
171+
expect.objectContaining({
172+
message: expect.stringContaining('[Fast Refresh] done in'),
173+
source: 'log',
174+
}),
175+
])
176+
)
177+
} else {
178+
expect(logs).toEqual(
179+
expect.arrayContaining([
180+
expect.objectContaining({
181+
message: expect.stringContaining('[Fast Refresh] done in'),
182+
source: 'log',
183+
}),
184+
])
185+
)
186+
}
187+
} finally {
188+
// TOOD: use sandbox instead
189+
await next.patchFile(envFile, 'MY_DEVICE="mac"')
190+
await retry(async () => {
191+
console.log('checking...', await browser.elementByCss('p').text())
192+
expect(await browser.elementByCss('p').text()).toBe('mac')
193+
})
235194
}
236-
} finally {
237-
// TOOD: use sandbox instead
238-
await next.patchFile(envFile, 'MY_DEVICE="mac"')
239-
await retry(async () => {
240-
expect(await browser.elementByCss('p').text()).toBe('mac')
241-
})
242195
}
243-
})
196+
)
244197

245198
it('should have no unexpected action error for hmr', async () => {
246199
expect(next.cliOutput).not.toContain('Unexpected action')

0 commit comments

Comments
 (0)