Skip to content

Commit 962fd9b

Browse files
authored
Merge pull request #581 from dev-five-git/fix-hot-update-issue
Fix hot update issue
2 parents 35e864f + dc50871 commit 962fd9b

3 files changed

Lines changed: 167 additions & 1 deletion

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"changes":{"packages/next-plugin/package.json":"Patch"},"note":"Fix hot update issue","date":"2026-02-20T09:53:49.433693700Z"}

packages/next-plugin/src/__tests__/coordinator.test.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,152 @@ describe('coordinator', () => {
554554
coordinator.close()
555555
})
556556

557+
it('should touch devup-ui.css to invalidate Turbopack cache when singleCss=false and new CSS collected', async () => {
558+
codeExtractSpy.mockReturnValue({
559+
code: 'import "./../../df/devup-ui/devup-ui-5.css";\nconst x = 1;',
560+
map: undefined,
561+
cssFile: 'devup-ui-5.css',
562+
updatedBaseStyle: false,
563+
css: '.a{color:yellow}',
564+
free: mock(),
565+
[Symbol.dispose]: mock(),
566+
})
567+
getCssSpy.mockImplementation(
568+
(fileNum: number | null, _importMainCss: boolean) => {
569+
if (fileNum === null) return 'base-css'
570+
return `file-css-${fileNum}`
571+
},
572+
)
573+
exportSheetSpy.mockReturnValue('{}')
574+
exportClassMapSpy.mockReturnValue('{}')
575+
exportFileMapSpy.mockReturnValue('{}')
576+
577+
const options = makeOptions({ singleCss: false })
578+
const coordinator = startCoordinator(options)
579+
580+
await new Promise((r) => setTimeout(r, 100))
581+
582+
const portStr = (writeFileSyncSpy.mock.calls[0] as [string, string])[1]
583+
const port = parseInt(portStr)
584+
585+
const res = await httpRequest(
586+
port,
587+
'POST',
588+
'/extract',
589+
JSON.stringify({
590+
filename: 'src/App.tsx',
591+
code: 'const x = <Box color="yellow" />',
592+
resourcePath: join(process.cwd(), 'src', 'App.tsx'),
593+
}),
594+
)
595+
596+
expect(res.status).toBe(200)
597+
598+
// 5 writes: per-file CSS + sheet + classmap + filemap + devup-ui.css invalidation
599+
expect(writeFileSpy).toHaveBeenCalledTimes(5)
600+
601+
// Verify devup-ui.css was written to trigger Turbopack invalidation
602+
const devupUiCssWrite = writeFileSpy.mock.calls.find(
603+
(call: unknown[]) =>
604+
typeof call[0] === 'string' && call[0].endsWith('devup-ui.css'),
605+
)
606+
expect(devupUiCssWrite).toBeTruthy()
607+
// Content should include base CSS + timestamp nonce
608+
const content = devupUiCssWrite![1] as string
609+
expect(content).toContain('base-css')
610+
expect(content).toMatch(/\/\* \d+ \*\//)
611+
612+
coordinator.close()
613+
})
614+
615+
it('should NOT touch devup-ui.css when singleCss=false but no new CSS collected', async () => {
616+
codeExtractSpy.mockReturnValue({
617+
code: 'const x = 1;',
618+
map: undefined,
619+
cssFile: 'devup-ui-5.css',
620+
updatedBaseStyle: false,
621+
css: undefined, // no new CSS collected
622+
free: mock(),
623+
[Symbol.dispose]: mock(),
624+
})
625+
getCssSpy.mockReturnValue('file-css')
626+
exportSheetSpy.mockReturnValue('{}')
627+
exportClassMapSpy.mockReturnValue('{}')
628+
exportFileMapSpy.mockReturnValue('{}')
629+
630+
const options = makeOptions({ singleCss: false })
631+
const coordinator = startCoordinator(options)
632+
633+
await new Promise((r) => setTimeout(r, 100))
634+
635+
const portStr = (writeFileSyncSpy.mock.calls[0] as [string, string])[1]
636+
const port = parseInt(portStr)
637+
638+
await httpRequest(
639+
port,
640+
'POST',
641+
'/extract',
642+
JSON.stringify({
643+
filename: 'src/App.tsx',
644+
code: 'const x = 1',
645+
resourcePath: join(process.cwd(), 'src', 'App.tsx'),
646+
}),
647+
)
648+
649+
// 4 writes: per-file CSS + sheet + classmap + filemap (NO devup-ui.css touch)
650+
expect(writeFileSpy).toHaveBeenCalledTimes(4)
651+
652+
// Verify NO devup-ui.css write
653+
const devupUiCssWrite = writeFileSpy.mock.calls.find(
654+
(call: unknown[]) =>
655+
typeof call[0] === 'string' && call[0].endsWith('devup-ui.css'),
656+
)
657+
expect(devupUiCssWrite).toBeUndefined()
658+
659+
coordinator.close()
660+
})
661+
662+
it('should NOT touch devup-ui.css for singleCss=true (not needed)', async () => {
663+
codeExtractSpy.mockReturnValue({
664+
code: 'const x = 1;',
665+
map: undefined,
666+
cssFile: 'devup-ui.css',
667+
updatedBaseStyle: false,
668+
css: '.a{color:yellow}',
669+
free: mock(),
670+
[Symbol.dispose]: mock(),
671+
})
672+
getCssSpy.mockReturnValue('all-styles')
673+
exportSheetSpy.mockReturnValue('{}')
674+
exportClassMapSpy.mockReturnValue('{}')
675+
exportFileMapSpy.mockReturnValue('{}')
676+
677+
const options = makeOptions({ singleCss: true })
678+
const coordinator = startCoordinator(options)
679+
680+
await new Promise((r) => setTimeout(r, 100))
681+
682+
const portStr = (writeFileSyncSpy.mock.calls[0] as [string, string])[1]
683+
const port = parseInt(portStr)
684+
685+
await httpRequest(
686+
port,
687+
'POST',
688+
'/extract',
689+
JSON.stringify({
690+
filename: 'src/App.tsx',
691+
code: 'const x = <Box color="yellow" />',
692+
resourcePath: join(process.cwd(), 'src', 'App.tsx'),
693+
}),
694+
)
695+
696+
// 4 writes: CSS file (devup-ui.css via cssFile) + sheet + classmap + filemap
697+
// NO additional devup-ui.css invalidation write (singleCss=true doesn't need it)
698+
expect(writeFileSpy).toHaveBeenCalledTimes(4)
699+
700+
coordinator.close()
701+
})
702+
557703
it('should be reset via resetCoordinator while server is active', async () => {
558704
const options = makeOptions()
559705
startCoordinator(options)

packages/next-plugin/src/coordinator.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export function startCoordinator(options: CoordinatorOptions): {
152152
new Promise<void>((resolve, reject) =>
153153
writeFile(
154154
join(cssDir, 'devup-ui.css'),
155-
getCss(null, false),
155+
`${getCss(null, false)}\n/* ${Date.now()} */`,
156156
'utf-8',
157157
(err) => (err ? reject(err) : resolve()),
158158
),
@@ -187,6 +187,25 @@ export function startCoordinator(options: CoordinatorOptions): {
187187
),
188188
),
189189
)
190+
191+
// In non-singleCss mode, imports are rewritten from devup-ui-N.css to
192+
// devup-ui.css?fileNum=N (line 142). Turbopack watches devup-ui.css for
193+
// all these modules, but above we only write devup-ui-N.css. Without
194+
// updating devup-ui.css, Turbopack never re-runs the css-loader and
195+
// new CSS rules are invisible to the browser.
196+
// When updatedBaseStyle is true, devup-ui.css is already written above.
197+
if (!singleCss && !result.updatedBaseStyle && result.css != null) {
198+
promises.push(
199+
new Promise<void>((resolve, reject) =>
200+
writeFile(
201+
join(cssDir, 'devup-ui.css'),
202+
`${getCss(null, false)}\n/* ${Date.now()} */`,
203+
'utf-8',
204+
(err) => (err ? reject(err) : resolve()),
205+
),
206+
),
207+
)
208+
}
190209
}
191210

192211
await Promise.all(promises)

0 commit comments

Comments
 (0)