From e9b481716ba4af44317f5e582569f1f14f198400 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 19 Feb 2025 16:42:52 +0900 Subject: [PATCH] Refactor file handling in DevupUI plugin to use async writeFile and improve chunk management --- .changeset/upset-coins-unite.md | 5 + .../vite-plugin/src/__tests__/plugin.test.ts | 101 +++++++++++++--- packages/vite-plugin/src/plugin.ts | 113 +++++++++--------- 3 files changed, 150 insertions(+), 69 deletions(-) create mode 100644 .changeset/upset-coins-unite.md diff --git a/.changeset/upset-coins-unite.md b/.changeset/upset-coins-unite.md new file mode 100644 index 00000000..2af83e85 --- /dev/null +++ b/.changeset/upset-coins-unite.md @@ -0,0 +1,5 @@ +--- +"@devup-ui/vite-plugin": patch +--- + +Fix vite build, dev issue diff --git a/packages/vite-plugin/src/__tests__/plugin.test.ts b/packages/vite-plugin/src/__tests__/plugin.test.ts index f781d2e5..03264ccd 100644 --- a/packages/vite-plugin/src/__tests__/plugin.test.ts +++ b/packages/vite-plugin/src/__tests__/plugin.test.ts @@ -1,14 +1,16 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { writeFile } from 'node:fs/promises' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { codeExtract, getThemeInterface } from '@devup-ui/wasm' -import { expect } from 'vitest' +import { describe } from 'vitest' import { DevupUI } from '../plugin' vi.mock('@devup-ui/wasm') vi.mock('node:fs') +vi.mock('node:fs/promises') const _filename = fileURLToPath(import.meta.url) const _dirname = resolve(dirname(_filename), '..') @@ -23,7 +25,7 @@ describe('devupUIPlugin', () => { const interfacePath = '.df' const cssFile = join(_dirname, 'devup-ui.css') const libPackage = '@devup-ui/react' - vi.mocked(existsSync).mockReturnValueOnce(true).mockReturnValueOnce(false) + vi.mocked(existsSync).mockReturnValueOnce(false).mockReturnValueOnce(true) vi.mocked(getThemeInterface).mockReturnValue('interface code') vi.mocked(readFileSync).mockReturnValueOnce('{"theme": {}}') const options = { @@ -35,6 +37,8 @@ describe('devupUIPlugin', () => { const plugin = DevupUI(options) expect(plugin).toEqual({ name: 'devup-ui', + load: expect.any(Function), + resolveId: expect.any(Function), config: expect.any(Function), watchChange: expect.any(Function), enforce: 'pre', @@ -56,6 +60,13 @@ describe('devupUIPlugin', () => { ignored: [`!${devupPath}`], }, }, + build: { + rollupOptions: { + output: { + manualChunks: expect.any(Function), + }, + }, + }, }) vi.clearAllMocks() vi.mocked(existsSync).mockReturnValue(true) @@ -99,7 +110,7 @@ describe('devupUIPlugin', () => { code: 'code', } as any) ;(plugin as any).transform('code', 'correct.ts') - expect(writeFileSync).toBeCalledTimes(1) + expect(writeFile).toBeCalledTimes(1) vi.clearAllMocks() vi.mocked(codeExtract).mockReturnValueOnce({ @@ -107,22 +118,22 @@ describe('devupUIPlugin', () => { code: 'code', } as any) ;(plugin as any).transform('code', 'correct.ts') - expect(writeFileSync).toBeCalledTimes(0) + expect(writeFile).toBeCalledTimes(0) ;(plugin as any).apply({}, { command: 'serve' }) vi.clearAllMocks() vi.mocked(codeExtract).mockReturnValueOnce({ - css: 'css code', + css: 'css code next', code: 'code', } as any) ;(plugin as any).transform('code', 'correct.ts') - expect(writeFileSync).toBeCalledTimes(1) + expect(writeFile).toBeCalledTimes(1) }) it('should transform code', () => { const devupPath = 'devup.json' const interfacePath = '.df' const cssFile = join(_dirname, 'devup-ui.css') const libPackage = '@devup-ui/react' - vi.mocked(existsSync).mockReturnValueOnce(true).mockReturnValueOnce(false) + vi.mocked(existsSync).mockReturnValueOnce(false).mockReturnValueOnce(true) vi.mocked(getThemeInterface).mockReturnValue('interface code') vi.mocked(readFileSync).mockReturnValueOnce('{"theme": {}}') const options = { @@ -134,6 +145,8 @@ describe('devupUIPlugin', () => { const plugin = DevupUI(options) expect(plugin).toEqual({ name: 'devup-ui', + load: expect.any(Function), + resolveId: expect.any(Function), config: expect.any(Function), watchChange: expect.any(Function), enforce: 'pre', @@ -151,7 +164,7 @@ describe('devupUIPlugin', () => { expect(existsSync).toHaveBeenCalledWith(interfacePath) vi.clearAllMocks() vi.mocked(codeExtract).mockReturnValueOnce({ - css: 'css code', + css: 'css code 1223444', code: 'code', } as any) // eslint-disable-next-line prefer-spread @@ -159,17 +172,15 @@ describe('devupUIPlugin', () => { command: 'serve', }) vi.stubEnv('NODE_ENV', 'development') - expect((plugin as any).transform('code', 'correct.ts').code).toContain( - 'document.head.appendChild', - ) - expect(writeFileSync).toBeCalledTimes(1) + ;(plugin as any).transform('code', 'correct.ts') + expect(writeFile).toBeCalledTimes(1) }) it('should not extract code', () => { const devupPath = 'devup.json' const interfacePath = '.df' const cssFile = join(_dirname, 'devup-ui.css') const libPackage = '@devup-ui/react' - vi.mocked(existsSync).mockReturnValueOnce(true).mockReturnValueOnce(false) + vi.mocked(existsSync).mockReturnValueOnce(false).mockReturnValueOnce(true) vi.mocked(getThemeInterface).mockReturnValue('interface code') vi.mocked(readFileSync).mockReturnValueOnce('{"theme": {}}') const options = { @@ -182,6 +193,8 @@ describe('devupUIPlugin', () => { const plugin = DevupUI(options) expect(plugin).toEqual({ name: 'devup-ui', + load: expect.any(Function), + resolveId: expect.any(Function), config: expect.any(Function), watchChange: expect.any(Function), enforce: 'pre', @@ -206,7 +219,7 @@ describe('devupUIPlugin', () => { const interfacePath = '.df' const cssFile = join(_dirname, 'devup-ui.css') const libPackage = '@devup-ui/react' - vi.mocked(existsSync).mockReturnValueOnce(true).mockReturnValueOnce(false) + vi.mocked(existsSync).mockReturnValueOnce(false).mockReturnValueOnce(true) vi.mocked(getThemeInterface).mockReturnValue('interface code') vi.mocked(readFileSync).mockReturnValueOnce('{"theme": {}}') vi.mocked(writeFileSync).mockImplementation(() => { @@ -221,6 +234,8 @@ describe('devupUIPlugin', () => { const plugin = DevupUI(options) expect(plugin).toEqual({ name: 'devup-ui', + load: expect.any(Function), + resolveId: expect.any(Function), config: expect.any(Function), watchChange: expect.any(Function), enforce: 'pre', @@ -243,7 +258,7 @@ describe('devupUIPlugin', () => { const interfacePath = '.df' const cssFile = join(_dirname, 'devup-ui.css') const libPackage = '@devup-ui/react' - vi.mocked(existsSync).mockReturnValueOnce(true).mockReturnValueOnce(false) + vi.mocked(existsSync).mockReturnValueOnce(false).mockReturnValueOnce(true) vi.mocked(getThemeInterface).mockReturnValue('interface code') vi.mocked(readFileSync).mockReturnValueOnce('{"theme": {}}') const options = { @@ -256,6 +271,8 @@ describe('devupUIPlugin', () => { expect(plugin).toEqual({ name: 'devup-ui', config: expect.any(Function), + load: expect.any(Function), + resolveId: expect.any(Function), watchChange: expect.any(Function), enforce: 'pre', transform: expect.any(Function), @@ -272,4 +289,58 @@ describe('devupUIPlugin', () => { expect(existsSync).toHaveBeenCalledWith(interfacePath) expect((plugin as any).apply({}, { command: 'build' })).toBe(true) }) + + describe('basic', () => { + const devupPath = 'devup.json' + const interfacePath = '.df' + const cssFile = join(_dirname, 'devup-ui.css') + const libPackage = '@devup-ui/react' + vi.mocked(existsSync).mockReturnValueOnce(false).mockReturnValueOnce(true) + vi.mocked(getThemeInterface).mockReturnValue('interface code') + vi.mocked(readFileSync).mockReturnValueOnce('{"theme": {}}') + const options = { + package: libPackage, + cssFile, + devupPath, + interfacePath, + } + const plugin = DevupUI(options) + it('should merge chunks', () => { + expect( + (plugin as any) + .config() + .build.rollupOptions.output.manualChunks('code', 'code'), + ).toBeUndefined() + + expect( + (plugin as any) + .config() + .build.rollupOptions.output.manualChunks('devup-ui.css', 'code'), + ).toEqual('devup-ui.css') + expect( + (plugin as any) + .config() + .build.rollupOptions.output.manualChunks('devup-ui.css?v=1', 'code'), + ).toEqual('devup-ui.css') + }) + it('should resolveId', () => { + expect((plugin as any).resolveId('code', 'code')).toBeUndefined() + expect( + (plugin as any) + .resolveId(cssFile, 'code') + .startsWith('devup-ui.css?v='), + ).toBe(true) + }) + it('should load', () => { + expect((plugin as any).load('code')).toBeUndefined() + expect((plugin as any).load(cssFile)).toBeUndefined() + expect( + (plugin as any).load('devup-ui.css?v=some').length.toString(), + ).toBe( + (plugin as any) + .resolveId(cssFile, 'code') + .substring('devup-ui.css?v='.length), + ) + }) + }) }) diff --git a/packages/vite-plugin/src/plugin.ts b/packages/vite-plugin/src/plugin.ts index 381d1ec3..8b97df68 100644 --- a/packages/vite-plugin/src/plugin.ts +++ b/packages/vite-plugin/src/plugin.ts @@ -1,18 +1,14 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' -import { dirname, join, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' +import { writeFile } from 'node:fs/promises' +import { join, resolve } from 'node:path' import { codeExtract, - getCss, getThemeInterface, registerTheme, setDebug, } from '@devup-ui/wasm' -import { type PluginOption } from 'vite' - -const _filename = fileURLToPath(import.meta.url) -const _dirname = dirname(_filename) +import { normalizePath, type PluginOption } from 'vite' export interface DevupUIPluginOptions { package: string @@ -26,46 +22,50 @@ export interface DevupUIPluginOptions { function writeDataFiles( options: Omit, ) { - registerTheme(JSON.parse(readFileSync(options.devupPath, 'utf-8'))?.['theme']) - const interfaceCode = getThemeInterface( - options.package, - 'DevupThemeColors', - 'DevupThemeTypography', - 'DevupTheme', - ) - if (interfaceCode) { - if (!existsSync(options.interfacePath)) mkdirSync(options.interfacePath) - writeFileSync(join(options.interfacePath, 'theme.d.ts'), interfaceCode, { + if (!existsSync(options.interfacePath)) mkdirSync(options.interfacePath) + if (existsSync(options.devupPath)) { + registerTheme( + JSON.parse(readFileSync(options.devupPath, 'utf-8'))?.['theme'], + ) + const interfaceCode = getThemeInterface( + options.package, + 'DevupThemeColors', + 'DevupThemeTypography', + 'DevupTheme', + ) + if (interfaceCode) { + writeFileSync(join(options.interfacePath, 'theme.d.ts'), interfaceCode, { + encoding: 'utf-8', + }) + } + } + if (!existsSync(options.cssFile)) + writeFileSync(options.cssFile, '', { encoding: 'utf-8', }) - } - writeFileSync(options.cssFile, getCss(), { - encoding: 'utf-8', - }) } +let globalCss = '' + export function DevupUI({ package: libPackage = '@devup-ui/react', - cssFile = join(_dirname, 'devup-ui.css'), devupPath = 'devup.json', interfacePath = '.df', + cssFile = resolve(interfacePath, 'devup-ui.css'), extractCss = true, debug = false, }: Partial = {}): PluginOption { setDebug(debug) - if (existsSync(devupPath)) { - try { - writeDataFiles({ - package: libPackage, - cssFile, - devupPath, - interfacePath, - }) - } catch (error) { - console.error(error) - } + try { + writeDataFiles({ + package: libPackage, + cssFile, + devupPath, + interfacePath, + }) + } catch (error) { + console.error(error) } - let command: null | 'build' | 'serve' = null return { name: 'devup-ui', config() { @@ -75,10 +75,21 @@ export function DevupUI({ ignored: [`!${devupPath}`], }, }, + build: { + rollupOptions: { + output: { + manualChunks(id) { + // merge devup css files + if (id.startsWith('devup-ui.css')) { + return `devup-ui.css` + } + }, + }, + }, + }, } }, - apply(_, env) { - command = env.command + apply() { return true }, watchChange(id) { @@ -96,33 +107,27 @@ export function DevupUI({ } } }, + resolveId(id) { + if (normalizePath(id) === normalizePath(cssFile)) { + return 'devup-ui.css?v=' + globalCss.length + } + }, + load(id) { + if (id.split('?')[0] === 'devup-ui.css') return globalCss + }, enforce: 'pre', - transform(code, id) { + async transform(code, id) { if (!extractCss) return if (id.includes('node_modules')) return if (!/\.(tsx|ts|js|mjs|jsx)$/i.test(id)) return const { code: retCode, css } = codeExtract(id, code, libPackage, cssFile) - if (css) { - // should be reset css - writeFileSync(cssFile, css, { + if (css && globalCss.length < css.length) { + globalCss = css + await writeFile(cssFile, `/* ${id} ${Date.now()} */`, { encoding: 'utf-8', }) - if (command === 'serve' && process.env.NODE_ENV !== 'test') - return { - code: `${retCode} - if(typeof window !== 'undefined'){ - const exists = !!document.getElementById('devup-ui'); - const style = document.getElementById('devup-ui') || document.createElement('style'); - style.id = 'devup-ui'; - style.textContent = \` - ${css} - \`; - if (!exists) document.head.appendChild(style); - } - `, - } } return { code: retCode,