update 增加 Transition 标签下 节点错误检测功能

This commit is contained in:
疯狂的狮子Li 2026-06-25 10:16:17 +08:00
parent d54c058085
commit 5e2f228e64
2 changed files with 119 additions and 0 deletions

View File

@ -0,0 +1,117 @@
import type { PluginOption } from 'vite';
import { normalizePath } from 'vite';
import { parse } from 'vue/compiler-sfc';
/** 模板 AST 元素节点类型 */
const NODE_TYPE_ELEMENT = 1;
/** 模板 AST 注释节点类型 */
const NODE_TYPE_COMMENT = 3;
/** 模板 AST 指令属性类型 */
const NODE_TYPE_DIRECTIVE = 7;
/**
*
*/
function isElement(node: any): boolean {
return node?.type === NODE_TYPE_ELEMENT;
}
/**
*
* @description transition
*/
function isComment(node: any): boolean {
return node?.type === NODE_TYPE_COMMENT;
}
/**
*
*/
function nodeLabel(node: any): string {
return isComment(node) ? '<!--注释-->' : `<${node.tag}>`;
}
/**
* v-if / v-else-if / v-else
*/
function hasDirective(node: any, name: string): boolean {
return (node.props || []).some(
(prop: any) => prop.type === NODE_TYPE_DIRECTIVE && prop.name === name,
);
}
/**
* v-if / v-else-if / v-else
* @description transition
*/
function isConditionalChain(elements: any[]): boolean {
// 首个节点必须是 v-if后续节点必须都是 v-else-if 或 v-else
if (!hasDirective(elements[0], 'if')) {
return false;
}
return elements
.slice(1)
.every((el) => hasDirective(el, 'else-if') || hasDirective(el, 'else'));
}
/**
* vite
* @description / <transition> transition
* Vue <transition>
* src/views
*/
export const viteCheckTransitionPlugin = (): PluginOption => {
// 仅检测 views 目录下的页面
const targetDir = normalizePath('/src/views/');
return {
enforce: 'pre',
name: 'vite:check-transition',
transform(code, id) {
const normalizedId = normalizePath(id);
// 跳过非 views 下的文件、非 .vue 文件以及带 query 的子请求
if (
normalizedId.includes('?') ||
!normalizedId.endsWith('.vue') ||
!normalizedId.includes(targetDir)
) {
return null;
}
const { descriptor, errors: parseErrors } = parse(code, {
filename: id,
});
// 解析失败交由 vue 插件处理,这里不重复报错
if (parseErrors.length > 0 || !descriptor.template?.ast) {
return null;
}
// 统计根级元素节点与注释节点,二者都是 transition 的子节点,忽略空白文本
const rootNodes = (descriptor.template.ast.children || []).filter(
(child: any) => isElement(child) || isComment(child),
);
// 仅元素节点参与 v-if 分支链判断(注释不属于分支链)
const rootElements = rootNodes.filter((node: any) => isElement(node));
const isChain =
rootNodes.length === rootElements.length &&
isConditionalChain(rootElements);
if (rootNodes.length > 1 && !isChain) {
const relativePath = normalizedId.slice(
normalizedId.indexOf('/src/') + 1,
);
const tags = rootNodes.map((node: any) => nodeLabel(node)).join(' ');
throw new Error(
`[check-transition] ${relativePath} 存在多个根节点: ${tags}\n 页面外层已包裹 <transition>,模板必须只有单个根节点,请用一个容器元素(如 div)包裹,并删除多余的注释/空标签。`,
);
}
return null;
},
};
};

View File

@ -6,6 +6,7 @@ import createComponents from './components';
import createSvgIconsPlugin from './svg-icon';
import createCompression from './compression';
import createSetupExtend from './setup-extend';
import { viteCheckTransitionPlugin } from "./check-transition";
export default (viteEnv: any, isBuild = false): [] => {
const vitePlugins: any = [];
@ -16,5 +17,6 @@ export default (viteEnv: any, isBuild = false): [] => {
vitePlugins.push(createCompression(viteEnv));
vitePlugins.push(createSvgIconsPlugin());
vitePlugins.push(createSetupExtend());
vitePlugins.push(viteCheckTransitionPlugin())
return vitePlugins;
};