Skip to content

Commit 991d54e

Browse files
authored
Merge pull request #579 from dev-five-git/load-cache
Implement import data
2 parents 3ac1c45 + 157be4d commit 991d54e

3 files changed

Lines changed: 106 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":"Implement import data","date":"2026-02-19T12:19:27.261637500Z"}

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@ let existsSyncSpy: ReturnType<typeof spyOn>
2020
let mkdirSyncSpy: ReturnType<typeof spyOn>
2121
let readFileSyncSpy: ReturnType<typeof spyOn>
2222
let writeFileSyncSpy: ReturnType<typeof spyOn>
23+
let unlinkSyncSpy: ReturnType<typeof spyOn>
2324
let getDefaultThemeSpy: ReturnType<typeof spyOn>
2425
let getThemeInterfaceSpy: ReturnType<typeof spyOn>
2526
let setPrefixSpy: ReturnType<typeof spyOn>
2627
let registerThemeSpy: ReturnType<typeof spyOn>
2728
let getCssSpy: ReturnType<typeof spyOn>
29+
let importSheetSpy: ReturnType<typeof spyOn>
30+
let importClassMapSpy: ReturnType<typeof spyOn>
31+
let importFileMapSpy: ReturnType<typeof spyOn>
2832
let exportSheetSpy: ReturnType<typeof spyOn>
2933
let exportClassMapSpy: ReturnType<typeof spyOn>
3034
let exportFileMapSpy: ReturnType<typeof spyOn>
@@ -40,11 +44,15 @@ beforeEach(() => {
4044
mkdirSyncSpy = spyOn(fs, 'mkdirSync').mockReturnValue('' as any)
4145
readFileSyncSpy = spyOn(fs, 'readFileSync').mockReturnValue('{}')
4246
writeFileSyncSpy = spyOn(fs, 'writeFileSync').mockReturnValue(undefined)
47+
unlinkSyncSpy = spyOn(fs, 'unlinkSync').mockReturnValue(undefined)
4348
getDefaultThemeSpy = spyOn(wasm, 'getDefaultTheme').mockReturnValue(undefined)
4449
getThemeInterfaceSpy = spyOn(wasm, 'getThemeInterface').mockReturnValue('')
4550
setPrefixSpy = spyOn(wasm, 'setPrefix').mockReturnValue(undefined)
4651
registerThemeSpy = spyOn(wasm, 'registerTheme').mockReturnValue(undefined)
4752
getCssSpy = spyOn(wasm, 'getCss').mockReturnValue('')
53+
importSheetSpy = spyOn(wasm, 'importSheet').mockReturnValue(undefined)
54+
importClassMapSpy = spyOn(wasm, 'importClassMap').mockReturnValue(undefined)
55+
importFileMapSpy = spyOn(wasm, 'importFileMap').mockReturnValue(undefined)
4856
exportSheetSpy = spyOn(wasm, 'exportSheet').mockReturnValue(
4957
JSON.stringify({
5058
css: {},
@@ -84,11 +92,15 @@ afterEach(() => {
8492
mkdirSyncSpy.mockRestore()
8593
readFileSyncSpy.mockRestore()
8694
writeFileSyncSpy.mockRestore()
95+
unlinkSyncSpy.mockRestore()
8796
getDefaultThemeSpy.mockRestore()
8897
getThemeInterfaceSpy.mockRestore()
8998
setPrefixSpy.mockRestore()
9099
registerThemeSpy.mockRestore()
91100
getCssSpy.mockRestore()
101+
importSheetSpy.mockRestore()
102+
importClassMapSpy.mockRestore()
103+
importFileMapSpy.mockRestore()
92104
exportSheetSpy.mockRestore()
93105
exportClassMapSpy.mockRestore()
94106
exportFileMapSpy.mockRestore()
@@ -541,6 +553,66 @@ describe('DevupUINextPlugin', () => {
541553
DevupUI({}, { prefix: 'my-prefix' })
542554
expect(setPrefixSpy).toHaveBeenCalledWith('my-prefix')
543555
})
556+
it('should import previous session state on restart', () => {
557+
process.env.TURBOPACK = '1'
558+
existsSyncSpy
559+
.mockReturnValueOnce(true) // distDir
560+
.mockReturnValueOnce(true) // cssDir
561+
.mockReturnValueOnce(true) // gitignoreFile
562+
.mockReturnValueOnce(false) // devupFile in loadDevupConfigSync
563+
564+
// Simulate previous session state files on disk
565+
const prevSheet = {
566+
css: { a: 'color:red' },
567+
font_faces: {},
568+
global_css_files: [],
569+
imports: {},
570+
keyframes: {},
571+
properties: {},
572+
}
573+
const prevClassMap = { a: 0 }
574+
const prevFileMap = { 'src/App.tsx': 0 }
575+
576+
readFileSyncSpy
577+
.mockReturnValueOnce(JSON.stringify(prevSheet)) // sheetFile
578+
.mockReturnValueOnce(JSON.stringify(prevClassMap)) // classMapFile
579+
.mockReturnValueOnce(JSON.stringify(prevFileMap)) // fileMapFile
580+
581+
DevupUI({})
582+
583+
// Verify previous state was imported before registerTheme
584+
expect(importSheetSpy).toHaveBeenCalledWith(prevSheet)
585+
expect(importClassMapSpy).toHaveBeenCalledWith(prevClassMap)
586+
expect(importFileMapSpy).toHaveBeenCalledWith(prevFileMap)
587+
expect(registerThemeSpy).toHaveBeenCalledWith({})
588+
589+
// Verify stale port file was deleted before starting coordinator
590+
expect(unlinkSyncSpy).toHaveBeenCalledWith(join('df', 'coordinator.port'))
591+
})
592+
it('should handle missing state files gracefully on first run', () => {
593+
process.env.TURBOPACK = '1'
594+
existsSyncSpy
595+
.mockReturnValueOnce(false) // distDir — doesn't exist
596+
.mockReturnValueOnce(false) // cssDir
597+
.mockReturnValueOnce(false) // gitignoreFile
598+
.mockReturnValueOnce(false) // devupFile
599+
600+
// readFileSync throws for state files (they don't exist)
601+
readFileSyncSpy.mockImplementation((path: string) => {
602+
throw new Error(`ENOENT: no such file or directory, open '${path}'`)
603+
})
604+
605+
// Should not throw — try-catch handles missing files
606+
DevupUI({})
607+
608+
// importSheet should NOT have been called (readFileSync threw)
609+
expect(importSheetSpy).not.toHaveBeenCalled()
610+
expect(importClassMapSpy).not.toHaveBeenCalled()
611+
expect(importFileMapSpy).not.toHaveBeenCalled()
612+
613+
// registerTheme should still be called with empty theme
614+
expect(registerThemeSpy).toHaveBeenCalledWith({})
615+
})
544616
it('should start coordinator in development mode', async () => {
545617
process.env.TURBOPACK = '1'
546618
;(process.env as any).NODE_ENV = 'development'

packages/next-plugin/src/plugin.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
1+
import {
2+
existsSync,
3+
mkdirSync,
4+
readFileSync,
5+
unlinkSync,
6+
writeFileSync,
7+
} from 'node:fs'
28
import { join, relative, resolve } from 'node:path'
39

410
import {
@@ -13,6 +19,9 @@ import {
1319
getCss,
1420
getDefaultTheme,
1521
getThemeInterface,
22+
importClassMap,
23+
importFileMap,
24+
importSheet,
1625
registerTheme,
1726
setPrefix,
1827
} from '@devup-ui/wasm'
@@ -76,9 +85,23 @@ export function DevupUI(
7685
recursive: true,
7786
})
7887
if (!existsSync(gitignoreFile)) writeFileSync(gitignoreFile, '*')
88+
// Import previous session state to handle Turbopack persistent cache.
89+
// When the dev server restarts, Turbopack may skip re-running loaders for
90+
// unchanged files. Without importing previous state, the coordinator's WASM
91+
// starts empty and CSS for cached files would be missing.
92+
try {
93+
importSheet(JSON.parse(readFileSync(sheetFile, 'utf-8')))
94+
importClassMap(JSON.parse(readFileSync(classMapFile, 'utf-8')))
95+
importFileMap(JSON.parse(readFileSync(fileMapFile, 'utf-8')))
96+
} catch {
97+
// No previous session state (first run) or corrupt files — start fresh
98+
}
99+
79100
const devupConfig = loadDevupConfigSync(devupFile)
80101

81102
const theme: any = devupConfig.theme ?? {}
103+
// Register current theme after importing previous state,
104+
// since importSheet replaces the entire sheet including its theme.
82105
registerTheme(theme)
83106
const themeInterface = getThemeInterface(
84107
libPackage,
@@ -97,6 +120,15 @@ export function DevupUI(
97120
// create devup-ui.css file
98121
writeFileSync(join(cssDir, 'devup-ui.css'), getCss(null, false))
99122

123+
// Delete stale port file from previous session so loaders don't connect
124+
// to a dead coordinator port. The new coordinator writes a fresh port file
125+
// once it starts listening.
126+
try {
127+
unlinkSync(coordinatorPortFile)
128+
} catch {
129+
// Port file doesn't exist (first run) — safe to ignore
130+
}
131+
100132
const coordinator = startCoordinator({
101133
package: libPackage,
102134
cssDir,

0 commit comments

Comments
 (0)