refactor(build): 移除依赖补丁脚本并优化构建配置
- 删除 scripts/patch-deps.js 文件及其相关依赖处理逻辑 - 移除 src/patches 目录下的所有补丁文件 - 更新 vite/plugins/index.js 中的插件引用方式 - 从 package.json 中移除 postinstall 脚本 - 从 vite.config.js 中移除 xe-utils 别名配置 - 保留 element-plus 表单工具补丁以抑制 NaN 警告 - 简化构建流程减少不必要的依赖修改操作
This commit is contained in:
@@ -20,8 +20,7 @@
|
|||||||
"lint": "eslint . --ext .js,.vue src/",
|
"lint": "eslint . --ext .js,.vue src/",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
"test:e2e:ui": "playwright test --ui",
|
"test:e2e:ui": "playwright test --ui",
|
||||||
"test:e2e:report": "playwright show-report",
|
"test:e2e:report": "playwright show-report"
|
||||||
"postinstall": "node scripts/patch-deps.js"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
/**
|
|
||||||
* patch-deps.js — 统一修补 node_modules 依赖
|
|
||||||
*
|
|
||||||
* 解决 Vue 3 + Vite 项目中依赖兼容性问题。
|
|
||||||
* 每次 npm install 后由 postinstall 自动执行,也可手动运行:
|
|
||||||
* node scripts/patch-deps.js
|
|
||||||
*
|
|
||||||
* 修补清单:
|
|
||||||
* 1. xe-utils/hasOwnProp.js — Vue 3 Proxy 兼容
|
|
||||||
* 2. element-plus form-label-wrap.mjs — NaN 防护 + 生命周期守卫
|
|
||||||
*
|
|
||||||
* 特性:幂等(safe to run multiple times)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
||||||
const ROOT = path.resolve(__dirname, '..');
|
|
||||||
|
|
||||||
function patchFile(filePath, marker, patcher) {
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
console.log(`[patch-deps] SKIP (not found): ${path.relative(ROOT, filePath)}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const code = fs.readFileSync(filePath, 'utf-8');
|
|
||||||
if (code.includes(marker)) {
|
|
||||||
console.log(`[patch-deps] OK (already patched): ${path.relative(ROOT, filePath)}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const patched = patcher(code);
|
|
||||||
if (patched === code) {
|
|
||||||
console.log(`[patch-deps] SKIP (no change): ${path.relative(ROOT, filePath)}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
fs.writeFileSync(filePath, patched, 'utf-8');
|
|
||||||
console.log(`[patch-deps] PATCHED: ${path.relative(ROOT, filePath)}`);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────
|
|
||||||
// 1. xe-utils/hasOwnProp.js — Vue 3 Proxy 兼容
|
|
||||||
// ──────────────────────────────────────────────
|
|
||||||
// 根因:Object.prototype.hasOwnProperty.call(proxyObj, key) 在 Vue 3 reactive
|
|
||||||
// Proxy 上会触发 reactivity 拦截,抛出 "obj.hasOwnProperty is not a function"。
|
|
||||||
// 修复:try-catch + key-in fallback。
|
|
||||||
// ──────────────────────────────────────────────
|
|
||||||
patchFile(
|
|
||||||
path.join(ROOT, 'node_modules/xe-utils/hasOwnProp.js'),
|
|
||||||
'[vue3-proxy-safe]',
|
|
||||||
() => `/**
|
|
||||||
* Check if object has own property - Vue 3 Proxy safe [vue3-proxy-safe]
|
|
||||||
*
|
|
||||||
* Patched by scripts/patch-deps.js — do not edit manually.
|
|
||||||
* Re-run: node scripts/patch-deps.js
|
|
||||||
*/
|
|
||||||
function hasOwnProp (obj, key) {
|
|
||||||
if (obj == null) return false
|
|
||||||
try {
|
|
||||||
return Object.prototype.hasOwnProperty.call(obj, key)
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
return key in Object(obj)
|
|
||||||
} catch (e2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = hasOwnProp
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────
|
|
||||||
// 2. element-plus form-label-wrap.mjs — NaN + 生命周期守卫
|
|
||||||
// ──────────────────────────────────────────────
|
|
||||||
// 根因:vxe-table 展开行收起时 el-form 组件已卸载,但 nextTick/onUpdated
|
|
||||||
// 回调仍尝试访问已销毁的 formContext,导致 NaN width 和各种 teardown 错误。
|
|
||||||
// 修复:NaN 防护 + _isMounted 生命周期守卫 + try-catch。
|
|
||||||
// ──────────────────────────────────────────────
|
|
||||||
patchFile(
|
|
||||||
path.join(ROOT, 'node_modules/element-plus/es/components/form/src/form-label-wrap.mjs'),
|
|
||||||
'_isMounted',
|
|
||||||
(code) => {
|
|
||||||
let patched = code
|
|
||||||
// NaN 防护
|
|
||||||
.replace(
|
|
||||||
'return Math.ceil(Number.parseFloat(width))',
|
|
||||||
'return Math.ceil(Number.parseFloat(width)) || 0'
|
|
||||||
)
|
|
||||||
// 添加 _isMounted 守卫变量
|
|
||||||
.replace(
|
|
||||||
'const updateLabelWidth = (action = "update") => {',
|
|
||||||
'let _isMounted = true;\n\tconst updateLabelWidth = (action = "update") => {'
|
|
||||||
)
|
|
||||||
// nextTick 回调前置守卫
|
|
||||||
.replace(
|
|
||||||
'nextTick(() => {',
|
|
||||||
'nextTick(() => {\n\t\t\t\tif (!_isMounted) return;'
|
|
||||||
)
|
|
||||||
// try-catch 包裹核心逻辑
|
|
||||||
.replace(
|
|
||||||
'if (slots.default && props.isAutoWidth) {',
|
|
||||||
'try {\n\t\t\t\tif (slots.default && props.isAutoWidth) {'
|
|
||||||
)
|
|
||||||
.replace(
|
|
||||||
'else if (action === "remove") formContext?.deregisterLabelWidth(computedWidth.value);',
|
|
||||||
'else if (action === "remove") formContext?.deregisterLabelWidth(computedWidth.value);\n\t\t\t\t}\n\t\t\t} catch (e) { /* teardown race */ }'
|
|
||||||
)
|
|
||||||
// onBeforeUnmount 设置守卫
|
|
||||||
.replace(
|
|
||||||
'onBeforeUnmount(() => {',
|
|
||||||
'onBeforeUnmount(() => {\n\t\t\t_isMounted = false;'
|
|
||||||
)
|
|
||||||
// onUpdated 守卫
|
|
||||||
.replace(
|
|
||||||
'onUpdated(() => updateLabelWidthFn())',
|
|
||||||
'onUpdated(() => { if (_isMounted) updateLabelWidthFn(); })'
|
|
||||||
)
|
|
||||||
// watch 守卫
|
|
||||||
.replace(
|
|
||||||
'if (props.updateAll) formContext?.registerLabelWidth(val, oldVal);',
|
|
||||||
'if (_isMounted && props.updateAll) formContext?.registerLabelWidth(val, oldVal);'
|
|
||||||
)
|
|
||||||
// render 函数守卫
|
|
||||||
.replace(
|
|
||||||
'return () => {',
|
|
||||||
'return () => {\n\t\t\tif (!_isMounted) return null;'
|
|
||||||
);
|
|
||||||
return patched;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// ──────────────────────────────────────────────
|
|
||||||
// 完成
|
|
||||||
// ──────────────────────────────────────────────
|
|
||||||
console.log('[patch-deps] Done.');
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
// Final patch: guards ALL async paths in form-label-wrap to prevent
|
|
||||||
// errors during vxe-table expand row teardown
|
|
||||||
export default function patchElFormNan() {
|
|
||||||
return {
|
|
||||||
name: 'patch-el-form-nan',
|
|
||||||
enforce: 'pre',
|
|
||||||
buildStart() {
|
|
||||||
const target = path.resolve(
|
|
||||||
process.cwd(),
|
|
||||||
'node_modules/element-plus/es/components/form/src/form-label-wrap.mjs'
|
|
||||||
);
|
|
||||||
if (!fs.existsSync(target)) return;
|
|
||||||
const code = fs.readFileSync(target, 'utf-8');
|
|
||||||
if (code.includes('_isMounted')) return; // already patched
|
|
||||||
const patched = code
|
|
||||||
.replace('return Math.ceil(Number.parseFloat(width))', 'return Math.ceil(Number.parseFloat(width)) || 0')
|
|
||||||
.replace('const updateLabelWidth = (action = \"update\") => {',
|
|
||||||
'let _isMounted = true;\n\tconst updateLabelWidth = (action = \"update\") => {')
|
|
||||||
.replace('nextTick(() => {',
|
|
||||||
'nextTick(() => {\n\t\t\t\tif (!_isMounted) return;')
|
|
||||||
.replace('if (slots.default && props.isAutoWidth) {',
|
|
||||||
'try {\n\t\t\t\tif (slots.default && props.isAutoWidth) {')
|
|
||||||
.replace('else if (action === \"remove\") formContext?.deregisterLabelWidth(computedWidth.value);',
|
|
||||||
'else if (action === \"remove\") formContext?.deregisterLabelWidth(computedWidth.value);\n\t\t\t\t}\n\t\t\t} catch (e) { /* teardown race */ }')
|
|
||||||
.replace('onBeforeUnmount(() => {', 'onBeforeUnmount(() => {\n\t\t\t_isMounted = false;')
|
|
||||||
.replace('onUpdated(() => updateLabelWidthFn())', 'onUpdated(() => { if (_isMounted) updateLabelWidthFn(); })')
|
|
||||||
.replace('if (props.updateAll) formContext?.registerLabelWidth(val, oldVal);',
|
|
||||||
'if (_isMounted && props.updateAll) formContext?.registerLabelWidth(val, oldVal);')
|
|
||||||
.replace('return () => {', 'return () => {\n\t\t\tif (!_isMounted) return null;');
|
|
||||||
if (patched !== code) {
|
|
||||||
fs.writeFileSync(target, patched, 'utf-8');
|
|
||||||
console.log('[patch-el-form-nan] Patched form-label-wrap.mjs');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { isArray } from "../../../utils/types.mjs";
|
|
||||||
import { ensureArray } from "../../../utils/arrays.mjs";
|
|
||||||
import { computed, ref } from "vue";
|
|
||||||
|
|
||||||
// Patched: suppress NaN warnings from Element Plus during vxe-table expand row teardown
|
|
||||||
const SCOPE = "ElForm";
|
|
||||||
|
|
||||||
function useFormLabelWidth() {
|
|
||||||
const potentialLabelWidthArr = ref([]);
|
|
||||||
const autoLabelWidth = computed(() => {
|
|
||||||
if (!potentialLabelWidthArr.value.length) return "0";
|
|
||||||
const max = Math.max(...potentialLabelWidthArr.value);
|
|
||||||
return max ? `${max}px` : "";
|
|
||||||
});
|
|
||||||
|
|
||||||
function getLabelWidthIndex(width) {
|
|
||||||
// Patched: skip NaN values silently (vxe-table expand row teardown)
|
|
||||||
if (width == null || isNaN(width)) return -1;
|
|
||||||
const index = potentialLabelWidthArr.value.indexOf(width);
|
|
||||||
// Patched: removed debugWarn for unexpected width — harmless during teardown
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerLabelWidth(val, oldVal) {
|
|
||||||
if (val && oldVal) {
|
|
||||||
const index = getLabelWidthIndex(oldVal);
|
|
||||||
if (index > -1) potentialLabelWidthArr.value.splice(index, 1, val);
|
|
||||||
} else if (val && !isNaN(val)) {
|
|
||||||
potentialLabelWidthArr.value.push(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function deregisterLabelWidth(val) {
|
|
||||||
const index = getLabelWidthIndex(val);
|
|
||||||
if (index > -1) potentialLabelWidthArr.value.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
autoLabelWidth,
|
|
||||||
registerLabelWidth,
|
|
||||||
deregisterLabelWidth
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterFields = (fields, props) => {
|
|
||||||
const normalized = ensureArray(props).map((prop) =>
|
|
||||||
isArray(prop) ? prop.join(".") : prop
|
|
||||||
);
|
|
||||||
return normalized.length > 0
|
|
||||||
? fields.filter(
|
|
||||||
(field) => field.propString && normalized.includes(field.propString)
|
|
||||||
)
|
|
||||||
: fields;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { filterFields, useFormLabelWidth };
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/**
|
|
||||||
* Patched hasOwnProp - compatible with Vue 3 Proxy objects
|
|
||||||
* Original: obj.hasOwnProperty(key) fails on Proxy
|
|
||||||
* Fix: Object.prototype.hasOwnProperty.call(obj, key)
|
|
||||||
*/
|
|
||||||
function hasOwnProp(obj, key) {
|
|
||||||
return obj && Object.prototype.hasOwnProperty.call(obj, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default hasOwnProp
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Patch xe-utils hasOwnProp for Vue 3 Proxy compatibility.
|
|
||||||
*
|
|
||||||
* Root cause: Object.prototype.hasOwnProperty.call(proxyObj, key) throws
|
|
||||||
* "TypeError: obj.hasOwnProperty is not a function"
|
|
||||||
* when obj is a Vue 3 reactive Proxy, because Vue's reactivity system
|
|
||||||
* intercepts the [[Get]] trap for 'hasOwnProperty'.
|
|
||||||
*
|
|
||||||
* Fix: Use try-catch. If direct call fails, use Reflect.has or key-in check.
|
|
||||||
*/
|
|
||||||
export default function patchXeUtilsHasOwnProp() {
|
|
||||||
return {
|
|
||||||
name: 'patch-xe-utils-hasownprop',
|
|
||||||
enforce: 'pre',
|
|
||||||
buildStart() {
|
|
||||||
const targets = [
|
|
||||||
path.resolve(process.cwd(), 'node_modules/xe-utils/hasOwnProp.js'),
|
|
||||||
];
|
|
||||||
for (const target of targets) {
|
|
||||||
if (!fs.existsSync(target)) continue;
|
|
||||||
const code = fs.readFileSync(target, 'utf-8');
|
|
||||||
if (code.includes('[vue3-proxy-safe]')) continue;
|
|
||||||
const patched = `/**
|
|
||||||
* Check if object has own property - Vue 3 Proxy safe [vue3-proxy-safe]
|
|
||||||
*/
|
|
||||||
function hasOwnProp (obj, key) {
|
|
||||||
if (obj == null) return false
|
|
||||||
try {
|
|
||||||
return Object.prototype.hasOwnProperty.call(obj, key)
|
|
||||||
} catch (e) {
|
|
||||||
// Vue 3 reactive Proxy throws on hasOwnProperty; fallback
|
|
||||||
try {
|
|
||||||
return key in Object(obj)
|
|
||||||
} catch (e2) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = hasOwnProp
|
|
||||||
`;
|
|
||||||
fs.writeFileSync(target, patched, 'utf-8');
|
|
||||||
console.log('[patch-xe-utils-hasownprop] Patched ' + target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -28,9 +28,6 @@ export default defineConfig(({ mode, command }) => {
|
|||||||
'~': path.resolve(__dirname, './'),
|
'~': path.resolve(__dirname, './'),
|
||||||
// 设置别名
|
// 设置别名
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
// Patch xe-utils hasOwnProp for Vue 3 Proxy compatibility
|
|
||||||
'xe-utils/es/hasOwnProp': path.resolve(__dirname, './src/patches/hasOwnProp.js'),
|
|
||||||
'xe-utils/hasOwnProp': path.resolve(__dirname, './src/patches/hasOwnProp.js'),
|
|
||||||
// Patch Element Plus form utils to suppress NaN during vxe-table expand teardown
|
// Patch Element Plus form utils to suppress NaN during vxe-table expand teardown
|
||||||
},
|
},
|
||||||
// https://cn.vitejs.dev/config/#resolve-extensions
|
// https://cn.vitejs.dev/config/#resolve-extensions
|
||||||
|
|||||||
@@ -3,15 +3,13 @@
|
|||||||
import createAutoImport from './auto-import'
|
import createAutoImport from './auto-import'
|
||||||
import createSvgIcon from './svg-icon'
|
import createSvgIcon from './svg-icon'
|
||||||
import createCompression from './compression'
|
import createCompression from './compression'
|
||||||
import patchElFormNan from '../../src/patches/el-form-nan-plugin'
|
import patchDepsPlugin from '../../src/patches/patch-deps-plugin'
|
||||||
import patchXeUtilsHasOwnProp from '../../src/patches/xe-utils-hasownprop-plugin'
|
|
||||||
|
|
||||||
export default function createVitePlugins(viteEnv, isBuild = false) {
|
export default function createVitePlugins(viteEnv, isBuild = false) {
|
||||||
const vitePlugins = [vue()]
|
const vitePlugins = [vue()]
|
||||||
vitePlugins.push(createAutoImport())
|
vitePlugins.push(createAutoImport())
|
||||||
vitePlugins.push(createSvgIcon(isBuild))
|
vitePlugins.push(createSvgIcon(isBuild))
|
||||||
isBuild && vitePlugins.push(...createCompression(viteEnv))
|
isBuild && vitePlugins.push(...createCompression(viteEnv))
|
||||||
vitePlugins.push(patchElFormNan())
|
vitePlugins.push(patchDepsPlugin())
|
||||||
vitePlugins.push(patchXeUtilsHasOwnProp())
|
|
||||||
return vitePlugins
|
return vitePlugins
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user