From 0006dd3855c884df7a236386c78b17f9bbf65cfc Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Sun, 20 Apr 2025 14:07:45 -0400 Subject: [PATCH] [Refactor] Split custom vite plugins to files under build/plugins (#3531) --- build/plugins/addElementVnodeExportPlugin.ts | 59 +++++ build/plugins/comfyAPIPlugin.ts | 82 ++++++ build/plugins/generateImportMapPlugin.ts | 103 ++++++++ build/plugins/index.ts | 3 + vite.config.mts | 250 +------------------ 5 files changed, 257 insertions(+), 240 deletions(-) create mode 100644 build/plugins/addElementVnodeExportPlugin.ts create mode 100644 build/plugins/comfyAPIPlugin.ts create mode 100644 build/plugins/generateImportMapPlugin.ts create mode 100644 build/plugins/index.ts diff --git a/build/plugins/addElementVnodeExportPlugin.ts b/build/plugins/addElementVnodeExportPlugin.ts new file mode 100644 index 000000000..1266d13e2 --- /dev/null +++ b/build/plugins/addElementVnodeExportPlugin.ts @@ -0,0 +1,59 @@ +import { Plugin } from 'vite' + +/** + * Vite plugin that adds an alias export for Vue's createBaseVNode as createElementVNode. + * + * This plugin addresses compatibility issues where some components or libraries + * might be using the older createElementVNode function name instead of createBaseVNode. + * It modifies the Vue vendor chunk during build to add the alias export. + * + * @returns {Plugin} A Vite plugin that modifies the Vue vendor chunk exports + */ +export function addElementVnodeExportPlugin(): Plugin { + return { + name: 'add-element-vnode-export-plugin', + + renderChunk(code, chunk, _options) { + if (chunk.name.startsWith('vendor-vue')) { + const exportRegex = /(export\s*\{)([^}]*)(\}\s*;?\s*)$/ + const match = code.match(exportRegex) + + if (match) { + const existingExports = match[2].trim() + const exportsArray = existingExports + .split(',') + .map((e) => e.trim()) + .filter(Boolean) + + const hasCreateBaseVNode = exportsArray.some((e) => + e.startsWith('createBaseVNode') + ) + const hasCreateElementVNode = exportsArray.some((e) => + e.includes('createElementVNode') + ) + + if (hasCreateBaseVNode && !hasCreateElementVNode) { + const newExportStatement = `${match[1]} ${existingExports ? existingExports + ',' : ''} createBaseVNode as createElementVNode ${match[3]}` + const newCode = code.replace(exportRegex, newExportStatement) + + console.log( + `[add-element-vnode-export-plugin] Added 'createBaseVNode as createElementVNode' export to vendor-vue chunk.` + ) + + return { code: newCode, map: null } + } else if (!hasCreateBaseVNode) { + console.warn( + `[add-element-vnode-export-plugin] Warning: 'createBaseVNode' not found in exports of vendor-vue chunk. Cannot add alias.` + ) + } + } else { + console.warn( + `[add-element-vnode-export-plugin] Warning: Could not find expected export block format in vendor-vue chunk.` + ) + } + } + + return null + } + } +} diff --git a/build/plugins/comfyAPIPlugin.ts b/build/plugins/comfyAPIPlugin.ts new file mode 100644 index 000000000..3f795a219 --- /dev/null +++ b/build/plugins/comfyAPIPlugin.ts @@ -0,0 +1,82 @@ +import path from 'path' +import { Plugin } from 'vite' + +interface ShimResult { + code: string + exports: string[] +} + +function isLegacyFile(id: string): boolean { + return ( + id.endsWith('.ts') && + (id.includes('src/extensions/core') || id.includes('src/scripts')) + ) +} + +function transformExports(code: string, id: string): ShimResult { + const moduleName = getModuleName(id) + const exports: string[] = [] + let newCode = code + + // Regex to match different types of exports + const regex = + /export\s+(const|let|var|function|class|async function)\s+([a-zA-Z$_][a-zA-Z\d$_]*)(\s|\()/g + let match + + while ((match = regex.exec(code)) !== null) { + const name = match[2] + // All exports should be bind to the window object as new API endpoint. + if (exports.length == 0) { + newCode += `\nwindow.comfyAPI = window.comfyAPI || {};` + newCode += `\nwindow.comfyAPI.${moduleName} = window.comfyAPI.${moduleName} || {};` + } + newCode += `\nwindow.comfyAPI.${moduleName}.${name} = ${name};` + exports.push( + `export const ${name} = window.comfyAPI.${moduleName}.${name};\n` + ) + } + + return { + code: newCode, + exports + } +} + +function getModuleName(id: string): string { + // Simple example to derive a module name from the file path + const parts = id.split('/') + const fileName = parts[parts.length - 1] + return fileName.replace(/\.\w+$/, '') // Remove file extension +} + +export function comfyAPIPlugin(isDev: boolean): Plugin { + return { + name: 'comfy-api-plugin', + transform(code: string, id: string) { + if (isDev) return null + + if (isLegacyFile(id)) { + const result = transformExports(code, id) + + if (result.exports.length > 0) { + const projectRoot = process.cwd() + const relativePath = path.relative(path.join(projectRoot, 'src'), id) + const shimFileName = relativePath.replace(/\.ts$/, '.js') + + const shimComment = `// Shim for ${relativePath}\n` + + this.emitFile({ + type: 'asset', + fileName: shimFileName, + source: shimComment + result.exports.join('') + }) + } + + return { + code: result.code, + map: null // If you're not modifying the source map, return null + } + } + } + } +} diff --git a/build/plugins/generateImportMapPlugin.ts b/build/plugins/generateImportMapPlugin.ts new file mode 100644 index 000000000..c6661a811 --- /dev/null +++ b/build/plugins/generateImportMapPlugin.ts @@ -0,0 +1,103 @@ +import type { OutputOptions } from 'rollup' +import { HtmlTagDescriptor, Plugin } from 'vite' + +interface VendorLibrary { + name: string + pattern: RegExp +} + +/** + * Vite plugin that generates an import map for vendor chunks. + * + * This plugin creates a browser-compatible import map that maps module specifiers + * (like 'vue' or 'primevue') to their actual file locations in the build output. + * This improves module loading in modern browsers and enables better caching. + * + * The plugin: + * 1. Tracks vendor chunks during bundle generation + * 2. Creates mappings between module names and their file paths + * 3. Injects an import map script tag into the HTML head + * 4. Configures manual chunk splitting for vendor libraries + * + * @param vendorLibraries - An array of vendor libraries to split into separate chunks + * @returns {Plugin} A Vite plugin that generates and injects an import map + */ +export function generateImportMapPlugin( + vendorLibraries: VendorLibrary[] +): Plugin { + const importMapEntries: Record = {} + + return { + name: 'generate-import-map-plugin', + + // Configure manual chunks during the build process + configResolved(config) { + if (config.build) { + // Ensure rollupOptions exists + if (!config.build.rollupOptions) { + config.build.rollupOptions = {} + } + + const outputOptions: OutputOptions = { + manualChunks: (id: string) => { + for (const lib of vendorLibraries) { + if (lib.pattern.test(id)) { + return `vendor-${lib.name}` + } + } + return null + }, + // Disable minification of internal exports to preserve function names + minifyInternalExports: false + } + config.build.rollupOptions.output = outputOptions + } + }, + + generateBundle(_options, bundle) { + for (const fileName in bundle) { + const chunk = bundle[fileName] + if (chunk.type === 'chunk' && !chunk.isEntry) { + // Find matching vendor library by chunk name + const vendorLib = vendorLibraries.find( + (lib) => chunk.name === `vendor-${lib.name}` + ) + + if (vendorLib) { + const relativePath = `./${chunk.fileName.replace(/\\/g, '/')}` + importMapEntries[vendorLib.name] = relativePath + + console.log( + `[ImportMap Plugin] Found chunk: ${chunk.name} -> Mapped '${vendorLib.name}' to '${relativePath}'` + ) + } + } + } + }, + + transformIndexHtml(html) { + if (Object.keys(importMapEntries).length === 0) { + console.warn( + '[ImportMap Plugin] No vendor chunks found to create import map.' + ) + return html + } + + const importMap = { + imports: importMapEntries + } + + const importMapTag: HtmlTagDescriptor = { + tag: 'script', + attrs: { type: 'importmap' }, + children: JSON.stringify(importMap, null, 2), + injectTo: 'head' + } + + return { + html, + tags: [importMapTag] + } + } + } +} diff --git a/build/plugins/index.ts b/build/plugins/index.ts new file mode 100644 index 000000000..c67473f7a --- /dev/null +++ b/build/plugins/index.ts @@ -0,0 +1,3 @@ +export { addElementVnodeExportPlugin } from './addElementVnodeExportPlugin' +export { comfyAPIPlugin } from './comfyAPIPlugin' +export { generateImportMapPlugin } from './generateImportMapPlugin' diff --git a/vite.config.mts b/vite.config.mts index 2b9d4c1a6..56e5182d6 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -1,13 +1,17 @@ import vue from '@vitejs/plugin-vue' import dotenv from 'dotenv' -import path from 'path' -import type { OutputOptions } from 'rollup' import IconsResolver from 'unplugin-icons/resolver' import Icons from 'unplugin-icons/vite' import Components from 'unplugin-vue-components/vite' -import { HtmlTagDescriptor, Plugin, defineConfig } from 'vite' +import { defineConfig } from 'vite' import type { UserConfigExport } from 'vitest/config' +import { + addElementVnodeExportPlugin, + comfyAPIPlugin, + generateImportMapPlugin +} from './build/plugins' + dotenv.config() const IS_DEV = process.env.NODE_ENV === 'development' @@ -15,240 +19,6 @@ const SHOULD_MINIFY = process.env.ENABLE_MINIFY === 'true' // vite dev server will listen on all addresses, including LAN and public addresses const VITE_REMOTE_DEV = process.env.VITE_REMOTE_DEV === 'true' -interface ShimResult { - code: string - exports: string[] -} - -function isLegacyFile(id: string): boolean { - return ( - id.endsWith('.ts') && - (id.includes('src/extensions/core') || id.includes('src/scripts')) - ) -} - -/** - * Vite plugin that adds an alias export for Vue's createBaseVNode as createElementVNode. - * - * This plugin addresses compatibility issues where some components or libraries - * might be using the older createElementVNode function name instead of createBaseVNode. - * It modifies the Vue vendor chunk during build to add the alias export. - * - * @returns {Plugin} A Vite plugin that modifies the Vue vendor chunk exports - */ -function addElementVnodeExportPlugin(): Plugin { - return { - name: 'add-element-vnode-export-plugin', - - renderChunk(code, chunk, _options) { - if (chunk.name.startsWith('vendor-vue')) { - const exportRegex = /(export\s*\{)([^}]*)(\}\s*;?\s*)$/ - const match = code.match(exportRegex) - - if (match) { - const existingExports = match[2].trim() - const exportsArray = existingExports - .split(',') - .map((e) => e.trim()) - .filter(Boolean) - - const hasCreateBaseVNode = exportsArray.some((e) => - e.startsWith('createBaseVNode') - ) - const hasCreateElementVNode = exportsArray.some((e) => - e.includes('createElementVNode') - ) - - if (hasCreateBaseVNode && !hasCreateElementVNode) { - const newExportStatement = `${match[1]} ${existingExports ? existingExports + ',' : ''} createBaseVNode as createElementVNode ${match[3]}` - const newCode = code.replace(exportRegex, newExportStatement) - - console.log( - `[add-element-vnode-export-plugin] Added 'createBaseVNode as createElementVNode' export to vendor-vue chunk.` - ) - - return { code: newCode, map: null } - } else if (!hasCreateBaseVNode) { - console.warn( - `[add-element-vnode-export-plugin] Warning: 'createBaseVNode' not found in exports of vendor-vue chunk. Cannot add alias.` - ) - } - } else { - console.warn( - `[add-element-vnode-export-plugin] Warning: Could not find expected export block format in vendor-vue chunk.` - ) - } - } - - return null - } - } -} - -/** - * Vite plugin that generates an import map for vendor chunks. - * - * This plugin creates a browser-compatible import map that maps module specifiers - * (like 'vue' or 'primevue') to their actual file locations in the build output. - * This improves module loading in modern browsers and enables better caching. - * - * The plugin: - * 1. Tracks vendor chunks during bundle generation - * 2. Creates mappings between module names and their file paths - * 3. Injects an import map script tag into the HTML head - * 4. Configures manual chunk splitting for vendor libraries - * - * @param vendorLibraries - An array of vendor libraries to split into separate chunks - * @returns {Plugin} A Vite plugin that generates and injects an import map - */ -function generateImportMapPlugin( - vendorLibraries: { name: string; pattern: string }[] -): Plugin { - const importMapEntries: Record = {} - - return { - name: 'generate-import-map-plugin', - - // Configure manual chunks during the build process - configResolved(config) { - if (config.build) { - // Ensure rollupOptions exists - if (!config.build.rollupOptions) { - config.build.rollupOptions = {} - } - - const outputOptions: OutputOptions = { - manualChunks: (id: string) => { - for (const lib of vendorLibraries) { - if (id.includes(lib.pattern)) { - return `vendor-${lib.name}` - } - } - return null - }, - // Disable minification of internal exports to preserve function names - minifyInternalExports: false - } - config.build.rollupOptions.output = outputOptions - } - }, - - generateBundle(_options, bundle) { - for (const fileName in bundle) { - const chunk = bundle[fileName] - if (chunk.type === 'chunk' && !chunk.isEntry) { - // Find matching vendor library by chunk name - const vendorLib = vendorLibraries.find( - (lib) => chunk.name === `vendor-${lib.name}` - ) - - if (vendorLib) { - const relativePath = `./${chunk.fileName.replace(/\\/g, '/')}` - importMapEntries[vendorLib.name] = relativePath - - console.log( - `[ImportMap Plugin] Found chunk: ${chunk.name} -> Mapped '${vendorLib.name}' to '${relativePath}'` - ) - } - } - } - }, - - transformIndexHtml(html) { - if (Object.keys(importMapEntries).length === 0) { - console.warn( - '[ImportMap Plugin] No vendor chunks found to create import map.' - ) - return html - } - - const importMap = { - imports: importMapEntries - } - - const importMapTag: HtmlTagDescriptor = { - tag: 'script', - attrs: { type: 'importmap' }, - children: JSON.stringify(importMap, null, 2), - injectTo: 'head' - } - - return { - html, - tags: [importMapTag] - } - } - } -} - -function comfyAPIPlugin(): Plugin { - return { - name: 'comfy-api-plugin', - transform(code: string, id: string) { - if (IS_DEV) return null - - if (isLegacyFile(id)) { - const result = transformExports(code, id) - - if (result.exports.length > 0) { - const projectRoot = process.cwd() - const relativePath = path.relative(path.join(projectRoot, 'src'), id) - const shimFileName = relativePath.replace(/\.ts$/, '.js') - - const shimComment = `// Shim for ${relativePath}\n` - - this.emitFile({ - type: 'asset', - fileName: shimFileName, - source: shimComment + result.exports.join('') - }) - } - - return { - code: result.code, - map: null // If you're not modifying the source map, return null - } - } - } - } -} - -function transformExports(code: string, id: string): ShimResult { - const moduleName = getModuleName(id) - const exports: string[] = [] - let newCode = code - - // Regex to match different types of exports - const regex = - /export\s+(const|let|var|function|class|async function)\s+([a-zA-Z$_][a-zA-Z\d$_]*)(\s|\()/g - let match - - while ((match = regex.exec(code)) !== null) { - const name = match[2] - // All exports should be bind to the window object as new API endpoint. - if (exports.length == 0) { - newCode += `\nwindow.comfyAPI = window.comfyAPI || {};` - newCode += `\nwindow.comfyAPI.${moduleName} = window.comfyAPI.${moduleName} || {};` - } - newCode += `\nwindow.comfyAPI.${moduleName}.${name} = ${name};` - exports.push( - `export const ${name} = window.comfyAPI.${moduleName}.${name};\n` - ) - } - - return { - code: newCode, - exports - } -} - -function getModuleName(id: string): string { - // Simple example to derive a module name from the file path - const parts = id.split('/') - const fileName = parts[parts.length - 1] - return fileName.replace(/\.\w+$/, '') // Remove file extension -} - const DEV_SERVER_COMFYUI_URL = process.env.DEV_SERVER_COMFYUI_URL || 'http://127.0.0.1:8188' @@ -291,10 +61,10 @@ export default defineConfig({ plugins: [ vue(), - comfyAPIPlugin(), + comfyAPIPlugin(IS_DEV), generateImportMapPlugin([ - { name: 'vue', pattern: 'node_modules/vue/' }, - { name: 'primevue', pattern: 'node_modules/primevue/' } + { name: 'vue', pattern: /[\\/]node_modules[\\/]vue[\\/]/ }, + { name: 'primevue', pattern: /[\\/]node_modules[\\/]primevue[\\/]/ } ]), addElementVnodeExportPlugin(),