Skip to content

Commit 6161a71

Browse files
committed
Fix loader issue
1 parent 319fbe9 commit 6161a71

5 files changed

Lines changed: 275 additions & 79 deletions

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 loader issue","date":"2026-01-14T11:16:47.199601600Z"}
Lines changed: 162 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,162 @@
1-
import * as wasm from '@devup-ui/wasm'
2-
import {
3-
afterAll,
4-
beforeAll,
5-
describe,
6-
expect,
7-
it,
8-
mock,
9-
spyOn,
10-
} from 'bun:test'
11-
12-
import devupUICssLoader from '../css-loader'
13-
14-
let getCssSpy: ReturnType<typeof spyOn>
15-
let registerThemeSpy: ReturnType<typeof spyOn>
16-
17-
beforeAll(() => {
18-
getCssSpy = spyOn(wasm, 'getCss').mockReturnValue('get css')
19-
registerThemeSpy = spyOn(wasm, 'registerTheme').mockReturnValue(undefined)
20-
})
21-
22-
afterAll(() => {
23-
getCssSpy.mockRestore()
24-
registerThemeSpy.mockRestore()
25-
})
26-
27-
describe('devupUICssLoader', () => {
28-
it('should return css on no watch', () => {
29-
const callback = mock()
30-
const addContextDependency = mock()
31-
devupUICssLoader.bind({
32-
callback,
33-
addContextDependency,
34-
resourcePath: 'devup-ui.css',
35-
getOptions: () => ({ watch: false }),
36-
} as any)(Buffer.from('data'), '')
37-
expect(callback).toHaveBeenCalledWith(
38-
null,
39-
Buffer.from('data'),
40-
'',
41-
undefined,
42-
)
43-
})
44-
45-
it('should return _compiler hit css on watch', () => {
46-
const callback = mock()
47-
const addContextDependency = mock()
48-
devupUICssLoader.bind({
49-
callback,
50-
addContextDependency,
51-
getOptions: () => ({ watch: true }),
52-
resourcePath: 'devup-ui.css',
53-
} as any)(Buffer.from('data'), '')
54-
expect(callback).toHaveBeenCalledWith(null, 'get css', '', undefined)
55-
expect(getCssSpy).toHaveBeenCalledTimes(1)
56-
getCssSpy.mockClear()
57-
devupUICssLoader.bind({
58-
callback,
59-
addContextDependency,
60-
getOptions: () => ({ watch: true }),
61-
resourcePath: 'devup-ui.css',
62-
} as any)(Buffer.from('data'), '')
63-
64-
expect(getCssSpy).toHaveBeenCalledTimes(1)
65-
66-
getCssSpy.mockClear()
67-
68-
devupUICssLoader.bind({
69-
callback,
70-
addContextDependency,
71-
getOptions: () => ({ watch: true }),
72-
resourcePath: 'devup-ui-10.css',
73-
} as any)(Buffer.from(''), '')
74-
75-
expect(getCssSpy).toHaveBeenCalledTimes(1)
76-
})
77-
})
1+
import * as fs from 'node:fs'
2+
3+
import * as wasm from '@devup-ui/wasm'
4+
import {
5+
afterAll,
6+
afterEach,
7+
beforeAll,
8+
describe,
9+
expect,
10+
it,
11+
mock,
12+
spyOn,
13+
} from 'bun:test'
14+
15+
import devupUICssLoader, { resetInit } from '../css-loader'
16+
17+
let getCssSpy: ReturnType<typeof spyOn>
18+
let registerThemeSpy: ReturnType<typeof spyOn>
19+
let importSheetSpy: ReturnType<typeof spyOn>
20+
let importClassMapSpy: ReturnType<typeof spyOn>
21+
let importFileMapSpy: ReturnType<typeof spyOn>
22+
let existsSyncSpy: ReturnType<typeof spyOn>
23+
let readFileSyncSpy: ReturnType<typeof spyOn>
24+
25+
const defaultOptions = {
26+
watch: false,
27+
sheetFile: 'sheet.json',
28+
classMapFile: 'classMap.json',
29+
fileMapFile: 'fileMap.json',
30+
themeFile: 'devup.json',
31+
theme: {},
32+
defaultSheet: {},
33+
defaultClassMap: {},
34+
defaultFileMap: {},
35+
}
36+
37+
beforeAll(() => {
38+
getCssSpy = spyOn(wasm, 'getCss').mockReturnValue('get css')
39+
registerThemeSpy = spyOn(wasm, 'registerTheme').mockReturnValue(undefined)
40+
importSheetSpy = spyOn(wasm, 'importSheet').mockReturnValue(undefined)
41+
importClassMapSpy = spyOn(wasm, 'importClassMap').mockReturnValue(undefined)
42+
importFileMapSpy = spyOn(wasm, 'importFileMap').mockReturnValue(undefined)
43+
existsSyncSpy = spyOn(fs, 'existsSync').mockReturnValue(false)
44+
readFileSyncSpy = spyOn(fs, 'readFileSync').mockReturnValue('{}')
45+
})
46+
47+
afterEach(() => {
48+
resetInit()
49+
getCssSpy.mockClear()
50+
registerThemeSpy.mockClear()
51+
importSheetSpy.mockClear()
52+
importClassMapSpy.mockClear()
53+
importFileMapSpy.mockClear()
54+
existsSyncSpy.mockClear()
55+
readFileSyncSpy.mockClear()
56+
})
57+
58+
afterAll(() => {
59+
getCssSpy.mockRestore()
60+
registerThemeSpy.mockRestore()
61+
importSheetSpy.mockRestore()
62+
importClassMapSpy.mockRestore()
63+
importFileMapSpy.mockRestore()
64+
existsSyncSpy.mockRestore()
65+
readFileSyncSpy.mockRestore()
66+
})
67+
68+
describe('devupUICssLoader', () => {
69+
it('should return css on no watch', () => {
70+
const callback = mock()
71+
const addContextDependency = mock()
72+
devupUICssLoader.bind({
73+
callback,
74+
addContextDependency,
75+
resourcePath: 'devup-ui.css',
76+
getOptions: () => ({ ...defaultOptions, watch: false }),
77+
} as any)(Buffer.from('data'), '')
78+
expect(callback).toHaveBeenCalledWith(
79+
null,
80+
Buffer.from('data'),
81+
'',
82+
undefined,
83+
)
84+
// Should initialize on first call
85+
expect(importFileMapSpy).toHaveBeenCalledTimes(1)
86+
expect(importClassMapSpy).toHaveBeenCalledTimes(1)
87+
expect(importSheetSpy).toHaveBeenCalledTimes(1)
88+
expect(registerThemeSpy).toHaveBeenCalledTimes(1)
89+
})
90+
91+
it('should return _compiler hit css on watch', () => {
92+
const callback = mock()
93+
const addContextDependency = mock()
94+
devupUICssLoader.bind({
95+
callback,
96+
addContextDependency,
97+
getOptions: () => ({ ...defaultOptions, watch: true }),
98+
resourcePath: 'devup-ui.css',
99+
} as any)(Buffer.from('data'), '')
100+
expect(callback).toHaveBeenCalledTimes(1)
101+
expect(getCssSpy).toHaveBeenCalledTimes(1)
102+
getCssSpy.mockClear()
103+
devupUICssLoader.bind({
104+
callback,
105+
addContextDependency,
106+
getOptions: () => ({ ...defaultOptions, watch: true }),
107+
resourcePath: 'devup-ui.css',
108+
} as any)(Buffer.from('data'), '')
109+
110+
expect(getCssSpy).toHaveBeenCalledTimes(1)
111+
112+
getCssSpy.mockClear()
113+
114+
devupUICssLoader.bind({
115+
callback,
116+
addContextDependency,
117+
getOptions: () => ({ ...defaultOptions, watch: true }),
118+
resourcePath: 'devup-ui-10.css',
119+
} as any)(Buffer.from(''), '')
120+
121+
expect(getCssSpy).toHaveBeenCalledTimes(1)
122+
})
123+
124+
it('should read files from disk in watch mode when files exist', () => {
125+
existsSyncSpy.mockReturnValue(true)
126+
readFileSyncSpy.mockReturnValue(JSON.stringify({ theme: { color: 'red' } }))
127+
128+
const callback = mock()
129+
const addContextDependency = mock()
130+
devupUICssLoader.bind({
131+
callback,
132+
addContextDependency,
133+
getOptions: () => ({ ...defaultOptions, watch: true }),
134+
resourcePath: 'devup-ui.css',
135+
} as any)(Buffer.from('data'), '')
136+
137+
// Should read files from disk
138+
expect(existsSyncSpy).toHaveBeenCalledTimes(4)
139+
expect(readFileSyncSpy).toHaveBeenCalledTimes(4)
140+
expect(importSheetSpy).toHaveBeenCalledTimes(1)
141+
expect(importClassMapSpy).toHaveBeenCalledTimes(1)
142+
expect(importFileMapSpy).toHaveBeenCalledTimes(1)
143+
expect(registerThemeSpy).toHaveBeenCalledWith({ color: 'red' })
144+
})
145+
146+
it('should handle missing theme in devup.json', () => {
147+
existsSyncSpy.mockReturnValue(true)
148+
readFileSyncSpy.mockReturnValue(JSON.stringify({}))
149+
150+
const callback = mock()
151+
const addContextDependency = mock()
152+
devupUICssLoader.bind({
153+
callback,
154+
addContextDependency,
155+
getOptions: () => ({ ...defaultOptions, watch: true }),
156+
resourcePath: 'devup-ui.css',
157+
} as any)(Buffer.from('data'), '')
158+
159+
// Should call registerTheme with empty object when theme is missing
160+
expect(registerThemeSpy).toHaveBeenCalledWith({})
161+
})
162+
})

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,21 @@ describe('DevupUINextPlugin', () => {
170170
loader: '@devup-ui/next-plugin/css-loader',
171171
options: {
172172
watch: false,
173+
sheetFile: join('df', 'sheet.json'),
174+
classMapFile: join('df', 'classMap.json'),
175+
fileMapFile: join('df', 'fileMap.json'),
176+
themeFile: 'devup.json',
177+
theme: {},
178+
defaultClassMap: {},
179+
defaultFileMap: {},
180+
defaultSheet: {
181+
css: {},
182+
font_faces: {},
183+
global_css_files: [],
184+
imports: {},
185+
keyframes: {},
186+
properties: {},
187+
},
173188
},
174189
},
175190
],
@@ -237,6 +252,21 @@ describe('DevupUINextPlugin', () => {
237252
loader: '@devup-ui/next-plugin/css-loader',
238253
options: {
239254
watch: false,
255+
sheetFile: join('df', 'sheet.json'),
256+
classMapFile: join('df', 'classMap.json'),
257+
fileMapFile: join('df', 'fileMap.json'),
258+
themeFile: 'devup.json',
259+
theme: {},
260+
defaultClassMap: {},
261+
defaultFileMap: {},
262+
defaultSheet: {
263+
css: {},
264+
font_faces: {},
265+
global_css_files: [],
266+
imports: {},
267+
keyframes: {},
268+
properties: {},
269+
},
240270
},
241271
},
242272
],
@@ -312,6 +342,21 @@ describe('DevupUINextPlugin', () => {
312342
loader: '@devup-ui/next-plugin/css-loader',
313343
options: {
314344
watch: false,
345+
sheetFile: join('df', 'sheet.json'),
346+
classMapFile: join('df', 'classMap.json'),
347+
fileMapFile: join('df', 'fileMap.json'),
348+
themeFile: 'devup.json',
349+
theme: 'theme',
350+
defaultClassMap: {},
351+
defaultFileMap: {},
352+
defaultSheet: {
353+
css: {},
354+
font_faces: {},
355+
global_css_files: [],
356+
imports: {},
357+
keyframes: {},
358+
properties: {},
359+
},
315360
},
316361
},
317362
],

packages/next-plugin/src/css-loader.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { getCss } from '@devup-ui/wasm'
1+
import { existsSync, readFileSync } from 'node:fs'
2+
3+
import {
4+
getCss,
5+
importClassMap,
6+
importFileMap,
7+
importSheet,
8+
registerTheme,
9+
} from '@devup-ui/wasm'
210
import type { RawLoaderDefinitionFunction } from 'webpack'
311

412
function getFileNumByFilename(filename: string) {
@@ -9,11 +17,55 @@ function getFileNumByFilename(filename: string) {
917
export interface DevupUICssLoaderOptions {
1018
// turbo
1119
watch: boolean
20+
sheetFile: string
21+
classMapFile: string
22+
fileMapFile: string
23+
themeFile: string
24+
theme?: object
25+
defaultSheet: object
26+
defaultClassMap: object
27+
defaultFileMap: object
1228
}
1329

30+
let init = false
31+
1432
const devupUICssLoader: RawLoaderDefinitionFunction<DevupUICssLoaderOptions> =
1533
function (source, map, meta) {
16-
const { watch } = this.getOptions()
34+
const {
35+
watch,
36+
sheetFile,
37+
classMapFile,
38+
fileMapFile,
39+
themeFile,
40+
theme,
41+
defaultClassMap,
42+
defaultFileMap,
43+
defaultSheet,
44+
} = this.getOptions()
45+
46+
if (!init) {
47+
init = true
48+
if (watch) {
49+
// restart loader issue
50+
// loader should read files when they exist in watch mode
51+
if (existsSync(sheetFile))
52+
importSheet(JSON.parse(readFileSync(sheetFile, 'utf-8')))
53+
if (existsSync(classMapFile))
54+
importClassMap(JSON.parse(readFileSync(classMapFile, 'utf-8')))
55+
if (existsSync(fileMapFile))
56+
importFileMap(JSON.parse(readFileSync(fileMapFile, 'utf-8')))
57+
if (existsSync(themeFile))
58+
registerTheme(
59+
JSON.parse(readFileSync(themeFile, 'utf-8'))?.['theme'] ?? {},
60+
)
61+
} else {
62+
importFileMap(defaultFileMap)
63+
importClassMap(defaultClassMap)
64+
importSheet(defaultSheet)
65+
registerTheme(theme)
66+
}
67+
}
68+
1769
this.callback(
1870
null,
1971
!watch ? source : getCss(getFileNumByFilename(this.resourcePath), true),
@@ -22,3 +74,8 @@ const devupUICssLoader: RawLoaderDefinitionFunction<DevupUICssLoaderOptions> =
2274
)
2375
}
2476
export default devupUICssLoader
77+
78+
/** @internal Reset init state for testing purposes only */
79+
export const resetInit = () => {
80+
init = false
81+
}

0 commit comments

Comments
 (0)