From ac2cdd1a21dfff65d4454899aac20d1232174ada Mon Sep 17 00:00:00 2001 From: alex8088 <244096523@qq.com> Date: Thu, 24 Sep 2026 19:30:27 +0800 Subject: [PATCH] fix: clone config without plugins #902 --- src/config.ts | 4 ++-- src/utils.ts | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index 446d42a..2a76f86 100644 --- a/src/config.ts +++ b/src/config.ts @@ -28,7 +28,7 @@ import modulePathPlugin from './plugins/modulePath' import isolateEntriesPlugin from './plugins/isolateEntries' import { type ExternalOptions, externalizeDepsPlugin } from './plugins/externalizeDeps' import { type BytecodeOptions, bytecodePlugin } from './plugins/bytecode' -import { isObject, isFilePathESM, deepClone, asyncFlatten } from './utils' +import { isObject, isFilePathESM, cloneConfig, asyncFlatten } from './utils' import { findConfigFile, bundleConfigFile, loadConfigFormBundledFile } from './load' export { defineConfig as defineViteConfig } from 'vite' @@ -229,7 +229,7 @@ export abstract class ConfigFactory { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - const config = mergeConfig(deepClone(this.baseConfig) as any, deepClone(this.inlineConfig)) as T + const config = mergeConfig(cloneConfig(this.baseConfig) as any, cloneConfig(this.inlineConfig)) as T config.mode = this.inlineConfig.mode || config.mode || process.env.NODE_ENV diff --git a/src/utils.ts b/src/utils.ts index a87a0c6..dd81879 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -112,6 +112,59 @@ export function deepClone(value: T): DeepWritable { return value as DeepWritable } +/** + * Isolate configuration data without cloning plugins or runtime instances. + * Plugins may capture their own object in hooks, so their identity must survive. + */ +export function cloneConfig(config: T): T { + const seen = new WeakMap() + const pluginArrays = new WeakMap() + + function clone(value: unknown, plugins = false): unknown { + if (value === null || typeof value !== 'object') return value + + const cache = plugins ? pluginArrays : seen + if (cache.has(value)) return cache.get(value) + + if (Array.isArray(value)) { + const result: unknown[] = new Array(value.length) + cache.set(value, result) + value.forEach((item, index) => { + result[index] = clone(item, plugins) + }) + return result + } + + // Preserve plugin objects, promises and other opaque plugin options. + if (plugins) return value + + if (value instanceof RegExp) { + const result = new RegExp(value) + result.lastIndex = value.lastIndex + cache.set(value, result) + return result + } + + const prototype = Object.getPrototypeOf(value) + if (prototype !== Object.prototype && prototype !== null) return value + + const result = Object.create(prototype) as Record + cache.set(value, result) + for (const key of Reflect.ownKeys(value)) { + if (!Object.prototype.propertyIsEnumerable.call(value, key)) continue + Object.defineProperty(result, key, { + value: clone((value as Record)[key], key === 'plugins'), + enumerable: true, + configurable: true, + writable: true + }) + } + return result + } + + return clone(config) as T +} + type AsyncFlatten = T extends (infer U)[] ? Exclude, U[]>[] : never export async function asyncFlatten(arr: T): Promise> {