From 74a8eec8c98582469be1520b0e5fee2987eae89f Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 7 Jan 2026 03:00:25 +0900 Subject: [PATCH 1/2] Add extends --- .../changepack_log_WzTvxRCALLzG2EJbWR-56.json | 1 + bun.lock | 30 ++- packages/bun-plugin/package.json | 1 + packages/bun-plugin/src/plugin.ts | 12 +- packages/next-plugin/package.json | 1 + packages/next-plugin/src/plugin.ts | 9 +- packages/plugin-utils/package.json | 43 +++ .../src/__tests__/load-config.test.ts | 246 ++++++++++++++++++ packages/plugin-utils/src/index.ts | 8 + packages/plugin-utils/src/load-config.ts | 135 ++++++++++ packages/plugin-utils/src/types.ts | 49 ++++ packages/plugin-utils/tsconfig.json | 29 +++ packages/rsbuild-plugin/package.json | 1 + packages/rsbuild-plugin/src/plugin.ts | 38 ++- packages/vite-plugin/package.json | 1 + packages/vite-plugin/src/plugin.ts | 38 ++- packages/webpack-plugin/package.json | 1 + .../src/__tests__/plugin.test.ts | 57 ++-- packages/webpack-plugin/src/plugin.ts | 36 +-- 19 files changed, 627 insertions(+), 109 deletions(-) create mode 100644 .changepacks/changepack_log_WzTvxRCALLzG2EJbWR-56.json create mode 100644 packages/plugin-utils/package.json create mode 100644 packages/plugin-utils/src/__tests__/load-config.test.ts create mode 100644 packages/plugin-utils/src/index.ts create mode 100644 packages/plugin-utils/src/load-config.ts create mode 100644 packages/plugin-utils/src/types.ts create mode 100644 packages/plugin-utils/tsconfig.json diff --git a/.changepacks/changepack_log_WzTvxRCALLzG2EJbWR-56.json b/.changepacks/changepack_log_WzTvxRCALLzG2EJbWR-56.json new file mode 100644 index 00000000..4d6c1b3e --- /dev/null +++ b/.changepacks/changepack_log_WzTvxRCALLzG2EJbWR-56.json @@ -0,0 +1 @@ +{"changes":{"packages/next-plugin/package.json":"Patch","packages/rsbuild-plugin/package.json":"Patch","packages/vite-plugin/package.json":"Patch","packages/webpack-plugin/package.json":"Patch","packages/bun-plugin/package.json":"Patch"},"note":"Add extends key","date":"2026-01-06T18:00:01.029206300Z"} \ No newline at end of file diff --git a/bun.lock b/bun.lock index a113c836..b445376a 100644 --- a/bun.lock +++ b/bun.lock @@ -329,12 +329,13 @@ }, "bindings/devup-ui-wasm": { "name": "@devup-ui/wasm", - "version": "1.0.55", + "version": "1.0.56", }, "packages/bun-plugin": { "name": "@devup-ui/bun-plugin", - "version": "1.0.1", + "version": "1.0.2", "dependencies": { + "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", }, "devDependencies": { @@ -376,7 +377,7 @@ }, "packages/eslint-plugin": { "name": "@devup-ui/eslint-plugin", - "version": "1.0.7", + "version": "1.0.8", "dependencies": { "@typescript-eslint/utils": "^8.51", "typescript-eslint": "^8.51", @@ -392,8 +393,9 @@ }, "packages/next-plugin": { "name": "@devup-ui/next-plugin", - "version": "1.0.65", + "version": "1.0.66", "dependencies": { + "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", "@devup-ui/webpack-plugin": "workspace:^", "next": "^16.1", @@ -409,6 +411,13 @@ "next": "*", }, }, + "packages/plugin-utils": { + "name": "@devup-ui/plugin-utils", + "version": "1.0.0", + "devDependencies": { + "typescript": "^5.9.3", + }, + }, "packages/react": { "name": "@devup-ui/react", "version": "1.0.31", @@ -431,7 +440,7 @@ }, "packages/reset-css": { "name": "@devup-ui/reset-css", - "version": "1.0.20", + "version": "1.0.21", "dependencies": { "@devup-ui/react": "workspace:^", }, @@ -444,8 +453,9 @@ }, "packages/rsbuild-plugin": { "name": "@devup-ui/rsbuild-plugin", - "version": "1.0.47", + "version": "1.0.48", "dependencies": { + "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", }, "devDependencies": { @@ -459,8 +469,9 @@ }, "packages/vite-plugin": { "name": "@devup-ui/vite-plugin", - "version": "1.0.52", + "version": "1.0.53", "dependencies": { + "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", "vite": "^7.3", }, @@ -474,8 +485,9 @@ }, "packages/webpack-plugin": { "name": "@devup-ui/webpack-plugin", - "version": "1.0.52", + "version": "1.0.53", "dependencies": { + "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", }, "devDependencies": { @@ -718,6 +730,8 @@ "@devup-ui/next-plugin": ["@devup-ui/next-plugin@workspace:packages/next-plugin"], + "@devup-ui/plugin-utils": ["@devup-ui/plugin-utils@workspace:packages/plugin-utils"], + "@devup-ui/react": ["@devup-ui/react@workspace:packages/react"], "@devup-ui/reset-css": ["@devup-ui/reset-css@workspace:packages/reset-css"], diff --git a/packages/bun-plugin/package.json b/packages/bun-plugin/package.json index 19ffae95..7de7dad5 100644 --- a/packages/bun-plugin/package.json +++ b/packages/bun-plugin/package.json @@ -41,6 +41,7 @@ "dist" ], "dependencies": { + "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^" }, "devDependencies": { diff --git a/packages/bun-plugin/src/plugin.ts b/packages/bun-plugin/src/plugin.ts index 93b55286..65f4713c 100644 --- a/packages/bun-plugin/src/plugin.ts +++ b/packages/bun-plugin/src/plugin.ts @@ -1,7 +1,8 @@ import { existsSync } from 'node:fs' -import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { mkdir, writeFile } from 'node:fs/promises' import { basename, dirname, join, relative, resolve } from 'node:path' +import { loadDevupConfig } from '@devup-ui/plugin-utils' import { codeExtract, getThemeInterface, @@ -20,13 +21,8 @@ const singleCss = true async function writeDataFiles() { let theme = {} try { - const content = existsSync(devupFile) - ? await readFile(devupFile, 'utf-8') - : undefined - - if (content) { - theme = JSON.parse(content)?.['theme'] ?? {} - } + const config = await loadDevupConfig(devupFile) + theme = config.theme ?? {} } catch { // Error reading devup.json, use empty theme } diff --git a/packages/next-plugin/package.json b/packages/next-plugin/package.json index c29d1783..a78ab1ad 100644 --- a/packages/next-plugin/package.json +++ b/packages/next-plugin/package.json @@ -52,6 +52,7 @@ ], "types": "./dist/index.d.ts", "dependencies": { + "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/webpack-plugin": "workspace:^", "next": "^16.1", "@devup-ui/wasm": "workspace:^", diff --git a/packages/next-plugin/src/plugin.ts b/packages/next-plugin/src/plugin.ts index 949580c0..618ace67 100644 --- a/packages/next-plugin/src/plugin.ts +++ b/packages/next-plugin/src/plugin.ts @@ -1,6 +1,7 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, writeFileSync } from 'node:fs' import { join, relative, resolve } from 'node:path' +import { loadDevupConfigSync } from '@devup-ui/plugin-utils' import { exportClassMap, exportFileMap, @@ -68,9 +69,9 @@ export function DevupUI( recursive: true, }) if (!existsSync(gitignoreFile)) writeFileSync(gitignoreFile, '*') - const theme = existsSync(devupFile) - ? JSON.parse(readFileSync(devupFile, 'utf-8'))?.['theme'] - : {} + const devupConfig = loadDevupConfigSync(devupFile) + + const theme: any = devupConfig.theme ?? {} registerTheme(theme) const themeInterface = getThemeInterface( libPackage, diff --git a/packages/plugin-utils/package.json b/packages/plugin-utils/package.json new file mode 100644 index 00000000..9940c8ca --- /dev/null +++ b/packages/plugin-utils/package.json @@ -0,0 +1,43 @@ +{ + "name": "@devup-ui/plugin-utils", + "description": "Shared utilities for Devup UI build plugins", + "repository": "https://github.com/dev-five-git/devup-ui", + "author": "devfive", + "license": "Apache-2.0", + "homepage": "https://devup-ui.com", + "bugs": { + "url": "https://github.com/dev-five-git/devup-ui/issues", + "email": "contact@devfive.kr" + }, + "keywords": [ + "devup-ui", + "plugin", + "utils" + ], + "type": "module", + "version": "1.0.0", + "scripts": { + "lint": "eslint", + "build": "tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external" + }, + "publishConfig": { + "access": "public" + }, + "sideEffects": false, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist" + ], + "devDependencies": { + "typescript": "^5.9.3" + } +} diff --git a/packages/plugin-utils/src/__tests__/load-config.test.ts b/packages/plugin-utils/src/__tests__/load-config.test.ts new file mode 100644 index 00000000..0444a5ac --- /dev/null +++ b/packages/plugin-utils/src/__tests__/load-config.test.ts @@ -0,0 +1,246 @@ +import { mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' + +import { afterAll, beforeAll, describe, expect, it } from 'bun:test' + +import { deepMerge, loadDevupConfig, loadDevupConfigSync } from '../load-config' + +const testDir = join(import.meta.dir, 'fixtures') + +beforeAll(() => { + mkdirSync(testDir, { recursive: true }) +}) + +afterAll(() => { + rmSync(testDir, { recursive: true, force: true }) +}) + +describe('deepMerge', () => { + it('should merge two flat objects', () => { + const base = { a: 1, b: 2 } + const override = { b: 3, c: 4 } + expect(deepMerge(base, override) as any).toEqual({ a: 1, b: 3, c: 4 }) + }) + + it('should deep merge nested objects', () => { + const base = { + theme: { + colors: { default: { text: 'black' } }, + }, + } + const override = { + theme: { + colors: { default: { bg: 'white' } }, + }, + } + expect(deepMerge(base, override)).toEqual({ + theme: { + colors: { default: { text: 'black', bg: 'white' } }, + }, + }) + }) + + it('should replace arrays', () => { + const base = { arr: [1, 2, 3] } + const override = { arr: [4, 5] } + expect(deepMerge(base, override)).toEqual({ arr: [4, 5] }) + }) + + it('should handle undefined values in override', () => { + const base = { a: 1, b: 2 } + const override = { a: undefined, c: 3 } + expect(deepMerge(base, override)).toEqual({ a: 1, b: 2, c: 3 }) + }) + + it('should return override when base is not a plain object', () => { + expect(deepMerge([1, 2], { a: 1 })).toEqual({ a: 1 }) + expect(deepMerge(null, { a: 1 })).toEqual({ a: 1 }) + expect(deepMerge('string', { a: 1 })).toEqual({ a: 1 }) + expect(deepMerge(123, { a: 1 })).toEqual({ a: 1 }) + }) + + it('should return override when override is not a plain object', () => { + expect(deepMerge({ a: 1 }, [1, 2])).toEqual([1, 2]) + expect(deepMerge({ a: 1 }, null)).toEqual(null) + expect(deepMerge({ a: 1 }, 'string')).toEqual('string') + }) + + it('should return base when override is undefined', () => { + expect(deepMerge({ a: 1 }, undefined)).toEqual({ a: 1 }) + }) +}) + +describe('loadDevupConfigSync', () => { + it('should return empty object for non-existent file', () => { + const result = loadDevupConfigSync(join(testDir, 'non-existent.json')) + expect(result).toEqual({}) + }) + + it('should load simple config', () => { + const configPath = join(testDir, 'simple.json') + writeFileSync( + configPath, + JSON.stringify({ + theme: { colors: { default: { text: 'red' } } }, + }), + ) + + const result = loadDevupConfigSync(configPath) + expect(result).toEqual({ + theme: { colors: { default: { text: 'red' } } }, + }) + }) + + it('should return empty object for invalid JSON', () => { + const configPath = join(testDir, 'invalid.json') + writeFileSync(configPath, 'not valid json {{{') + + const result = loadDevupConfigSync(configPath) + expect(result).toEqual({}) + }) + + it('should resolve extends', () => { + const baseDir = join(testDir, 'extends') + mkdirSync(baseDir, { recursive: true }) + + // Base config + writeFileSync( + join(baseDir, 'base.json'), + JSON.stringify({ + theme: { + colors: { default: { text: 'black', bg: 'white' } }, + }, + }), + ) + + // Child config that extends base + writeFileSync( + join(baseDir, 'child.json'), + JSON.stringify({ + extends: ['./base.json'], + theme: { + colors: { default: { text: 'red' } }, + }, + }), + ) + + const result = loadDevupConfigSync(join(baseDir, 'child.json')) + expect(result).toEqual({ + theme: { + colors: { default: { text: 'red', bg: 'white' } }, + }, + }) + }) + + it('should resolve multiple extends in order', () => { + const baseDir = join(testDir, 'multi-extends') + mkdirSync(baseDir, { recursive: true }) + + // First base + writeFileSync( + join(baseDir, 'first.json'), + JSON.stringify({ + theme: { colors: { default: { a: '1', b: '2' } } }, + }), + ) + + // Second base (overrides first) + writeFileSync( + join(baseDir, 'second.json'), + JSON.stringify({ + theme: { colors: { default: { b: '3', c: '4' } } }, + }), + ) + + // Child extends both + writeFileSync( + join(baseDir, 'child.json'), + JSON.stringify({ + extends: ['./first.json', './second.json'], + theme: { colors: { default: { c: '5' } } }, + }), + ) + + const result = loadDevupConfigSync(join(baseDir, 'child.json')) + expect(result).toEqual({ + theme: { + colors: { default: { a: '1', b: '3', c: '5' } }, + }, + }) + }) + + it('should handle nested extends', () => { + const baseDir = join(testDir, 'nested-extends') + mkdirSync(baseDir, { recursive: true }) + + // Grandparent + writeFileSync( + join(baseDir, 'grandparent.json'), + JSON.stringify({ + theme: { colors: { default: { x: '1' } } }, + }), + ) + + // Parent extends grandparent + writeFileSync( + join(baseDir, 'parent.json'), + JSON.stringify({ + extends: ['./grandparent.json'], + theme: { colors: { default: { y: '2' } } }, + }), + ) + + // Child extends parent + writeFileSync( + join(baseDir, 'child.json'), + JSON.stringify({ + extends: ['./parent.json'], + theme: { colors: { default: { z: '3' } } }, + }), + ) + + const result = loadDevupConfigSync(join(baseDir, 'child.json')) + expect(result).toEqual({ + theme: { + colors: { default: { x: '1', y: '2', z: '3' } }, + }, + }) + }) +}) + +describe('loadDevupConfig', () => { + it('should return empty object for non-existent file', async () => { + const result = await loadDevupConfig(join(testDir, 'non-existent.json')) + expect(result).toEqual({}) + }) + + it('should load and resolve extends asynchronously', async () => { + const baseDir = join(testDir, 'async-extends') + mkdirSync(baseDir, { recursive: true }) + + writeFileSync( + join(baseDir, 'base.json'), + JSON.stringify({ + theme: { colors: { default: { text: 'blue' } } }, + }), + ) + + writeFileSync( + join(baseDir, 'child.json'), + JSON.stringify({ + extends: ['./base.json'], + theme: { colors: { dark: { text: 'white' } } }, + }), + ) + + const result = await loadDevupConfig(join(baseDir, 'child.json')) + expect(result).toEqual({ + theme: { + colors: { + default: { text: 'blue' }, + dark: { text: 'white' }, + }, + }, + }) + }) +}) diff --git a/packages/plugin-utils/src/index.ts b/packages/plugin-utils/src/index.ts new file mode 100644 index 00000000..0696f782 --- /dev/null +++ b/packages/plugin-utils/src/index.ts @@ -0,0 +1,8 @@ +export { deepMerge, loadDevupConfig, loadDevupConfigSync } from './load-config' +export type { + DevupConfig, + DevupTheme, + ThemeColors, + ThemeTypography, + Typography, +} from './types' diff --git a/packages/plugin-utils/src/load-config.ts b/packages/plugin-utils/src/load-config.ts new file mode 100644 index 00000000..d8bcd351 --- /dev/null +++ b/packages/plugin-utils/src/load-config.ts @@ -0,0 +1,135 @@ +import { existsSync, readFileSync } from 'node:fs' +import { readFile } from 'node:fs/promises' +import { dirname, resolve } from 'node:path' + +import type { DevupConfig } from './types' + +/** + * Check if a value is a plain object + */ +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +/** + * Deep merge two objects + * Arrays are replaced, not merged + * Objects are recursively merged + * The second object (override) takes precedence + */ +export function deepMerge(base: T, override: U): T { + if (!isPlainObject(base) || !isPlainObject(override)) { + return (override !== undefined ? override : base) as T + } + + const result = { ...base } as T + + for (const key in override) { + if (Object.prototype.hasOwnProperty.call(override, key)) { + const baseValue = (base as Record)[key] + const overrideValue = (override as Record)[key] + + if (isPlainObject(baseValue) && isPlainObject(overrideValue)) { + // Recursively merge objects + ;(result as Record)[key] = deepMerge( + baseValue as Record, + overrideValue as Record, + ) + } else if (overrideValue !== undefined) { + // Override with the new value (including arrays) + ;(result as Record)[key] = overrideValue + } + } + } + + return result +} + +/** + * Parse JSON content safely + */ +function parseConfig(content: string): DevupConfig { + try { + return JSON.parse(content) as DevupConfig + } catch { + return {} + } +} + +/** + * Load and resolve a devup.json config file synchronously + * Handles the extends field by loading and merging parent configs + * + * @param configPath - Path to the devup.json file + * @returns Resolved configuration with all extends merged + */ +export function loadDevupConfigSync(configPath: string): DevupConfig { + if (!existsSync(configPath)) { + return {} + } + + const content = readFileSync(configPath, 'utf-8') + const config = parseConfig(content) + + // If no extends, return the config as-is + if (!config.extends || config.extends.length === 0) { + return config + } + + const configDir = dirname(configPath) + + // Start with empty base and merge extends in order + // First extend is the base, subsequent ones override + let mergedConfig: DevupConfig = {} + + for (const extendPath of config.extends) { + const resolvedPath = resolve(configDir, extendPath) + const extendedConfig = loadDevupConfigSync(resolvedPath) + mergedConfig = deepMerge(mergedConfig, extendedConfig) + } + + // Finally, merge the current config (highest priority) + // Remove extends from the result as it's already resolved + const { extends: _, ...currentConfig } = config + return deepMerge(mergedConfig, currentConfig) +} + +/** + * Load and resolve a devup.json config file asynchronously + * Handles the extends field by loading and merging parent configs + * + * @param configPath - Path to the devup.json file + * @returns Resolved configuration with all extends merged + */ +export async function loadDevupConfig( + configPath: string, +): Promise { + if (!existsSync(configPath)) { + return {} + } + + const content = await readFile(configPath, 'utf-8') + const config = parseConfig(content) + + // If no extends, return the config as-is + if (!config.extends || config.extends.length === 0) { + return config + } + + const configDir = dirname(configPath) + + // Start with empty base and merge extends in order + // First extend is the base, subsequent ones override + let mergedConfig: DevupConfig = {} + + for (const extendPath of config.extends) { + const resolvedPath = resolve(configDir, extendPath) + const extendedConfig = await loadDevupConfig(resolvedPath) + mergedConfig = deepMerge(mergedConfig, extendedConfig) + } + + // Finally, merge the current config (highest priority) + // Remove extends from the result as it's already resolved + const { extends: _, ...currentConfig } = config + return deepMerge(mergedConfig, currentConfig) +} diff --git a/packages/plugin-utils/src/types.ts b/packages/plugin-utils/src/types.ts new file mode 100644 index 00000000..3addeb41 --- /dev/null +++ b/packages/plugin-utils/src/types.ts @@ -0,0 +1,49 @@ +/** + * Typography definition for a single breakpoint or non-responsive typography + */ +export interface Typography { + fontFamily?: string + fontStyle?: string + fontWeight?: number | string + fontSize?: string + lineHeight?: number | string + letterSpacing?: string +} + +/** + * Theme colors definition + * Each theme variant (e.g., 'default', 'dark', 'light') maps color names to values + */ +export type ThemeColors = Record> + +/** + * Theme typography definition + * Each typography name maps to either a single Typography or an array for responsive values + */ +export type ThemeTypography = Record + +/** + * Theme configuration + */ +export interface DevupTheme { + colors?: ThemeColors + typography?: ThemeTypography +} + +/** + * Devup configuration file structure (devup.json) + */ +export interface DevupConfig { + /** + * Array of paths to extend from + * Paths are resolved relative to the config file + * First item is the base, subsequent items override in order + * The current config is applied last (highest priority) + */ + extends?: string[] + + /** + * Theme configuration + */ + theme?: DevupTheme +} diff --git a/packages/plugin-utils/tsconfig.json b/packages/plugin-utils/tsconfig.json new file mode 100644 index 00000000..c5d4da3c --- /dev/null +++ b/packages/plugin-utils/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "types": ["bun"], + "strict": true, + "target": "ESNext", + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "dist", + "declarationMap": true, + "removeComments": true, + "sourceMap": true, + "useDefineForClassFields": true, + "allowJs": false, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strictFunctionTypes": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "baseUrl": ".", + "jsx": "react-jsx" + }, + "include": ["src"], + "exclude": ["node_modules", "dist", "src/**/__tests__"] +} diff --git a/packages/rsbuild-plugin/package.json b/packages/rsbuild-plugin/package.json index dcb265cd..3f66f8cd 100644 --- a/packages/rsbuild-plugin/package.json +++ b/packages/rsbuild-plugin/package.json @@ -41,6 +41,7 @@ "dist" ], "dependencies": { + "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^" }, "peerDependencies": { diff --git a/packages/rsbuild-plugin/src/plugin.ts b/packages/rsbuild-plugin/src/plugin.ts index 5712d537..39d08283 100644 --- a/packages/rsbuild-plugin/src/plugin.ts +++ b/packages/rsbuild-plugin/src/plugin.ts @@ -1,7 +1,8 @@ import { existsSync } from 'node:fs' -import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { mkdir, writeFile } from 'node:fs/promises' import { basename, join, resolve } from 'node:path' +import { loadDevupConfig } from '@devup-ui/plugin-utils' import { codeExtract, getCss, @@ -34,28 +35,23 @@ async function writeDataFiles( >, ) { try { - const content = existsSync(options.devupFile) - ? await readFile(options.devupFile, 'utf-8') - : undefined + const config = await loadDevupConfig(options.devupFile) + const theme = config.theme ?? {} - if (content) { - registerTheme(JSON.parse(content)?.['theme'] ?? {}) - const interfaceCode = getThemeInterface( - options.package, - 'CustomColors', - 'DevupThemeTypography', - 'DevupTheme', - ) + registerTheme(theme) + const interfaceCode = getThemeInterface( + options.package, + 'CustomColors', + 'DevupThemeTypography', + 'DevupTheme', + ) - if (interfaceCode) { - await writeFile( - join(options.distDir, 'theme.d.ts'), - interfaceCode, - 'utf-8', - ) - } - } else { - registerTheme({}) + if (interfaceCode) { + await writeFile( + join(options.distDir, 'theme.d.ts'), + interfaceCode, + 'utf-8', + ) } } catch (error) { console.error(error) diff --git a/packages/vite-plugin/package.json b/packages/vite-plugin/package.json index 4aa28c1f..691be675 100644 --- a/packages/vite-plugin/package.json +++ b/packages/vite-plugin/package.json @@ -41,6 +41,7 @@ "dist" ], "dependencies": { + "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", "vite": "^7.3" }, diff --git a/packages/vite-plugin/src/plugin.ts b/packages/vite-plugin/src/plugin.ts index 4c51a559..7b940c47 100644 --- a/packages/vite-plugin/src/plugin.ts +++ b/packages/vite-plugin/src/plugin.ts @@ -1,7 +1,8 @@ import { existsSync } from 'node:fs' -import { mkdir, readFile, writeFile } from 'node:fs/promises' +import { mkdir, writeFile } from 'node:fs/promises' import { basename, dirname, join, relative, resolve } from 'node:path' +import { loadDevupConfig } from '@devup-ui/plugin-utils' import { codeExtract, getCss, @@ -34,28 +35,23 @@ async function writeDataFiles( options: Omit, ) { try { - const content = existsSync(options.devupFile) - ? await readFile(options.devupFile, 'utf-8') - : undefined + const config = await loadDevupConfig(options.devupFile) + const theme = config.theme ?? {} - if (content) { - registerTheme(JSON.parse(content)?.['theme'] ?? {}) - const interfaceCode = getThemeInterface( - options.package, - 'CustomColors', - 'DevupThemeTypography', - 'DevupTheme', - ) + registerTheme(theme) + const interfaceCode = getThemeInterface( + options.package, + 'CustomColors', + 'DevupThemeTypography', + 'DevupTheme', + ) - if (interfaceCode) { - await writeFile( - join(options.distDir, 'theme.d.ts'), - interfaceCode, - 'utf-8', - ) - } - } else { - registerTheme({}) + if (interfaceCode) { + await writeFile( + join(options.distDir, 'theme.d.ts'), + interfaceCode, + 'utf-8', + ) } } catch (error) { console.error(error) diff --git a/packages/webpack-plugin/package.json b/packages/webpack-plugin/package.json index 56ee36c9..4b28177e 100644 --- a/packages/webpack-plugin/package.json +++ b/packages/webpack-plugin/package.json @@ -50,6 +50,7 @@ "dist" ], "dependencies": { + "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^" }, "peerDependencies": { diff --git a/packages/webpack-plugin/src/__tests__/plugin.test.ts b/packages/webpack-plugin/src/__tests__/plugin.test.ts index 64caa2b6..d7efd3e1 100644 --- a/packages/webpack-plugin/src/__tests__/plugin.test.ts +++ b/packages/webpack-plugin/src/__tests__/plugin.test.ts @@ -2,6 +2,7 @@ import * as fs from 'node:fs' import * as fsPromises from 'node:fs/promises' import { join, resolve } from 'node:path' +import * as pluginUtils from '@devup-ui/plugin-utils' import * as wasm from '@devup-ui/wasm' import { afterEach, @@ -25,6 +26,7 @@ let importSheetSpy: ReturnType let registerThemeSpy: ReturnType let setDebugSpy: ReturnType let setPrefixSpy: ReturnType +let loadDevupConfigSyncSpy: ReturnType let existsSyncSpy: ReturnType let mkdirSyncSpy: ReturnType let readFileSyncSpy: ReturnType @@ -56,6 +58,10 @@ beforeEach(() => { registerThemeSpy = spyOn(wasm, 'registerTheme').mockReturnValue(undefined) setDebugSpy = spyOn(wasm, 'setDebug').mockReturnValue(undefined) setPrefixSpy = spyOn(wasm, 'setPrefix').mockReturnValue(undefined) + loadDevupConfigSyncSpy = spyOn( + pluginUtils, + 'loadDevupConfigSync', + ).mockReturnValue({}) existsSyncSpy = spyOn(fs, 'existsSync').mockReturnValue(false) mkdirSyncSpy = spyOn(fs, 'mkdirSync').mockReturnValue(undefined) readFileSyncSpy = spyOn(fs, 'readFileSync').mockReturnValue('{}') @@ -77,6 +83,7 @@ afterEach(() => { registerThemeSpy.mockRestore() setDebugSpy.mockRestore() setPrefixSpy.mockRestore() + loadDevupConfigSyncSpy.mockRestore() existsSyncSpy.mockRestore() mkdirSyncSpy.mockRestore() readFileSyncSpy.mockRestore() @@ -154,38 +161,38 @@ describe('devupUIWebpackPlugin', () => { getCss: ['css', 'css-core'], }), )('should write data files', async (_options) => { - readFileSyncSpy.mockReturnValue(JSON.stringify(_options.readFile)) + loadDevupConfigSyncSpy.mockReturnValue( + _options.readFile !== undefined + ? { theme: _options.readFile.theme } + : {}, + ) getThemeInterfaceSpy.mockReturnValue(_options.getThemeInterface) getCssSpy.mockReturnValue(_options.getCss) - existsSyncSpy.mockReturnValueOnce(_options.readFile !== undefined) writeFileSyncSpy.mockReturnValue(undefined) const plugin = new DevupUIWebpackPlugin(options) await plugin.writeDataFiles() - if (_options.readFile !== undefined) { - expect(readFileSyncSpy).toHaveBeenCalledWith(options.devupFile, 'utf-8') - - expect(registerThemeSpy).toHaveBeenCalledWith( - _options.readFile?.theme ?? {}, - ) - expect(getThemeInterfaceSpy).toHaveBeenCalledWith( - options.package, - 'CustomColors', - 'DevupThemeTypography', - 'DevupTheme', + expect(loadDevupConfigSyncSpy).toHaveBeenCalledWith(options.devupFile) + expect(registerThemeSpy).toHaveBeenCalledWith( + _options.readFile?.theme ?? {}, + ) + expect(getThemeInterfaceSpy).toHaveBeenCalledWith( + options.package, + 'CustomColors', + 'DevupThemeTypography', + 'DevupTheme', + ) + if (_options.getThemeInterface) + expect(writeFileSyncSpy).toHaveBeenCalledWith( + join(options.distDir, 'theme.d.ts'), + _options.getThemeInterface, + { + encoding: 'utf-8', + }, ) - if (_options.getThemeInterface) - expect(writeFileSyncSpy).toHaveBeenCalledWith( - join(options.distDir, 'theme.d.ts'), - _options.getThemeInterface, - { - encoding: 'utf-8', - }, - ) - else - expect(writeFileSyncSpy).toHaveBeenCalledTimes(options.watch ? 1 : 0) - } else expect(readFileSpy).not.toHaveBeenCalled() + else expect(writeFileSyncSpy).toHaveBeenCalledTimes(options.watch ? 1 : 0) + if (options.watch) expect(writeFileSyncSpy).toHaveBeenCalledWith( join(options.cssDir, 'devup-ui.css'), @@ -193,7 +200,7 @@ describe('devupUIWebpackPlugin', () => { ) else expect(writeFileSyncSpy).toHaveBeenCalledTimes( - _options.getThemeInterface && _options.readFile !== undefined ? 1 : 0, + _options.getThemeInterface ? 1 : 0, ) }) }) diff --git a/packages/webpack-plugin/src/plugin.ts b/packages/webpack-plugin/src/plugin.ts index 7e72af5d..542de11a 100644 --- a/packages/webpack-plugin/src/plugin.ts +++ b/packages/webpack-plugin/src/plugin.ts @@ -3,6 +3,7 @@ import { stat, writeFile } from 'node:fs/promises' import { createRequire } from 'node:module' import { join, resolve } from 'node:path' +import { loadDevupConfigSync } from '@devup-ui/plugin-utils' import { getCss, getDefaultTheme, @@ -64,30 +65,21 @@ export class DevupUIWebpackPlugin { writeDataFiles() { try { - const content = existsSync(this.options.devupFile) - ? readFileSync(this.options.devupFile, 'utf-8') - : undefined + const config = loadDevupConfigSync(this.options.devupFile) + const theme = config.theme ?? {} - if (content) { - registerTheme(JSON.parse(content)?.['theme'] ?? {}) - const interfaceCode = getThemeInterface( - this.options.package, - 'CustomColors', - 'DevupThemeTypography', - 'DevupTheme', - ) + registerTheme(theme) + const interfaceCode = getThemeInterface( + this.options.package, + 'CustomColors', + 'DevupThemeTypography', + 'DevupTheme', + ) - if (interfaceCode) { - writeFileSync( - join(this.options.distDir, 'theme.d.ts'), - interfaceCode, - { - encoding: 'utf-8', - }, - ) - } - } else { - registerTheme({}) + if (interfaceCode) { + writeFileSync(join(this.options.distDir, 'theme.d.ts'), interfaceCode, { + encoding: 'utf-8', + }) } } catch (error) { console.error(error) From 59f9904f9f59b71816e5eacd9f70ffe6d6a980cc Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Wed, 7 Jan 2026 03:12:49 +0900 Subject: [PATCH 2/2] Add build script --- .changepacks/changepack_log_8qVhFYHtH7lswOCkOv6fH.json | 1 + package.json | 2 +- packages/plugin-utils/package.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 .changepacks/changepack_log_8qVhFYHtH7lswOCkOv6fH.json diff --git a/.changepacks/changepack_log_8qVhFYHtH7lswOCkOv6fH.json b/.changepacks/changepack_log_8qVhFYHtH7lswOCkOv6fH.json new file mode 100644 index 00000000..9914ab41 --- /dev/null +++ b/.changepacks/changepack_log_8qVhFYHtH7lswOCkOv6fH.json @@ -0,0 +1 @@ +{"changes":{"packages/plugin-utils/package.json":"Major"},"note":"Release Plugin utils","date":"2026-01-06T18:10:29.522327400Z"} \ No newline at end of file diff --git a/package.json b/package.json index 2726abbc..43e003dc 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "lint:fix": "eslint --fix", "pretest": "bun run --filter @devup-ui/vite-plugin build", "test": "cargo tarpaulin --out xml --out stdout --out html --all-targets && bun test", - "build": "bun run --filter @devup-ui/wasm build && bun run --filter @devup-ui/react --filter @devup-ui/webpack-plugin build && bun run --filter @devup-ui/eslint-plugin --filter @devup-ui/vite-plugin --filter @devup-ui/next-plugin --filter @devup-ui/rsbuild-plugin --filter @devup-ui/bun-plugin --filter @devup-ui/components --filter @devup-ui/reset-css build", + "build": "bun run --filter @devup-ui/wasm --filter @devup-ui/plugin-utils build && bun run --filter @devup-ui/react --filter @devup-ui/webpack-plugin build && bun run --filter @devup-ui/eslint-plugin --filter @devup-ui/vite-plugin --filter @devup-ui/next-plugin --filter @devup-ui/rsbuild-plugin --filter @devup-ui/bun-plugin --filter @devup-ui/components --filter @devup-ui/reset-css build", "dev": "bun run --filter '*' dev", "benchmark": "bun benchmark.js", "prepare": "husky", diff --git a/packages/plugin-utils/package.json b/packages/plugin-utils/package.json index 9940c8ca..0fce307e 100644 --- a/packages/plugin-utils/package.json +++ b/packages/plugin-utils/package.json @@ -15,7 +15,7 @@ "utils" ], "type": "module", - "version": "1.0.0", + "version": "0.0.1", "scripts": { "lint": "eslint", "build": "tsc && bun build --target node src/index.ts --production --env=disable --outfile dist/index.cjs --format cjs --packages external && bun build --target node src/index.ts --production --env=disable --outfile dist/index.mjs --format esm --packages external"