@vitarx/plugin-vite 0.0.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -0
- package/README.md +298 -0
- package/dist/components.js +1 -0
- package/dist/constants/index.js +59 -0
- package/dist/context.js +78 -0
- package/dist/error.js +156 -0
- package/dist/hmr-client/index.js +143 -0
- package/dist/hmr-client/update.js +53 -0
- package/dist/hmr-client/utils.js +125 -0
- package/dist/index.js +65 -0
- package/dist/passes/components/ifBlock.js +42 -0
- package/dist/passes/components/index.js +24 -0
- package/dist/passes/components/switch.js +102 -0
- package/dist/passes/directives/index.js +6 -0
- package/dist/passes/directives/processDirectives.js +46 -0
- package/dist/passes/directives/vIf.js +64 -0
- package/dist/passes/hmr/index.js +5 -0
- package/dist/passes/hmr/inject.js +189 -0
- package/dist/passes/imports/collectImports.js +56 -0
- package/dist/passes/imports/collectRefVariables.js +95 -0
- package/dist/passes/imports/index.js +7 -0
- package/dist/passes/imports/injectImports.js +96 -0
- package/dist/passes/index.js +16 -0
- package/dist/passes/jsx/index.js +7 -0
- package/dist/passes/jsx/processChildren.js +114 -0
- package/dist/passes/jsx/processJSXElement.js +173 -0
- package/dist/passes/jsx/processJSXFragment.js +37 -0
- package/dist/passes/props/attribute.js +165 -0
- package/dist/passes/props/index.js +94 -0
- package/dist/passes/props/types.js +1 -0
- package/dist/passes/props/vmodel.js +115 -0
- package/dist/transform.js +144 -0
- package/dist/utils/ast-builders.js +142 -0
- package/dist/utils/ast-guards.js +16 -0
- package/dist/utils/branch-factory.js +107 -0
- package/dist/utils/component-collect.js +233 -0
- package/dist/utils/generate.js +11 -0
- package/dist/utils/index.js +16 -0
- package/dist/utils/jsx-helpers.js +168 -0
- package/dist/utils/pattern-helpers.js +47 -0
- package/dist/utils/vif-helpers.js +127 -0
- package/package.json +63 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import { isJSXElement, isJSXExpressionContainer, isJSXText } from '@babel/types';
|
|
3
|
+
import { addWarning, markImport } from '../../context.js';
|
|
4
|
+
import { createError, createWarning } from '../../error.js';
|
|
5
|
+
import { addPureComment, createArrowFunction, createBranch, createCreateViewCall, createLocationObject, getAlias, getDirectiveValue, getJSXAttributeByName, getJSXElementName, isNativeElement, isVElse, isVIf, isVIfChain, removeVDirectives } from '../../utils/index.js';
|
|
6
|
+
import { processDirectives } from '../directives/index.js';
|
|
7
|
+
import { processProps } from '../props/index.js';
|
|
8
|
+
import { processChildren } from './processChildren.js';
|
|
9
|
+
/**
|
|
10
|
+
* 处理 JSX 元素
|
|
11
|
+
* @param path - JSX 元素路径
|
|
12
|
+
* @param ctx - 转换上下文
|
|
13
|
+
*/
|
|
14
|
+
export function processJSXElement(path, ctx) {
|
|
15
|
+
const name = getJSXElementName(path.node);
|
|
16
|
+
// 校验 Match 必须在 Switch 内使用
|
|
17
|
+
if (name === 'Match') {
|
|
18
|
+
validateMatchInSwitch(path);
|
|
19
|
+
}
|
|
20
|
+
const result = transformJSXElement(path.node, ctx, true);
|
|
21
|
+
if (result) {
|
|
22
|
+
path.replaceWith(result);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 校验 Match 组件必须在 Switch 内使用
|
|
27
|
+
*/
|
|
28
|
+
function validateMatchInSwitch(path) {
|
|
29
|
+
const parent = path.parentPath;
|
|
30
|
+
if (!parent) {
|
|
31
|
+
throw createError('E012', path.node);
|
|
32
|
+
}
|
|
33
|
+
// 检查父元素是否是 Switch
|
|
34
|
+
if (parent.node.type === 'JSXElement') {
|
|
35
|
+
const parentName = getJSXElementName(parent.node);
|
|
36
|
+
if (parentName === 'Switch') {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// 检查是否在 Fragment 内且 Fragment 的父元素是 Switch
|
|
41
|
+
if (parent.node.type === 'JSXFragment' || parent.node.type === 'JSXElement') {
|
|
42
|
+
const grandParent = parent.parentPath;
|
|
43
|
+
if (grandParent?.node.type === 'JSXElement') {
|
|
44
|
+
const grandParentName = getJSXElementName(grandParent.node);
|
|
45
|
+
if (grandParentName === 'Switch') {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
throw createError('E012', path.node);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 转换 JSX 元素为表达式
|
|
54
|
+
* @param node - JSX 元素节点
|
|
55
|
+
* @param ctx - 转换上下文
|
|
56
|
+
* @param handleVIf - 是否处理 v-if 指令
|
|
57
|
+
* @returns 转换后的表达式
|
|
58
|
+
*/
|
|
59
|
+
export function transformJSXElement(node, ctx, handleVIf = false) {
|
|
60
|
+
const name = getJSXElementName(node);
|
|
61
|
+
if (!name)
|
|
62
|
+
return null;
|
|
63
|
+
// 处理 v-if 链
|
|
64
|
+
if (handleVIf && isVIfChain(node)) {
|
|
65
|
+
if (isVIf(node)) {
|
|
66
|
+
return transformSingleVIf(node, ctx);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
throw createError(isVElse(node) ? 'E003' : 'E004', node);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 确定元素类型
|
|
73
|
+
const type = isNativeElement(name) ? t.stringLiteral(name) : t.identifier(name);
|
|
74
|
+
// 检测是否有有效子元素
|
|
75
|
+
const hasChildren = node.children.some(child => {
|
|
76
|
+
if (isJSXText(child)) {
|
|
77
|
+
return child.value.trim().length > 0;
|
|
78
|
+
}
|
|
79
|
+
if (isJSXExpressionContainer(child)) {
|
|
80
|
+
return child.expression.type !== 'JSXEmptyExpression';
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
84
|
+
// 处理属性(有子元素时跳过 children 属性)
|
|
85
|
+
const { props, directives } = processProps(node, ctx, hasChildren);
|
|
86
|
+
// 处理子元素
|
|
87
|
+
const finalProps = processElementChildren(node, props, ctx);
|
|
88
|
+
// 生成 createView 调用
|
|
89
|
+
markImport(ctx, 'createView');
|
|
90
|
+
const createViewAlias = getAlias(ctx.vitarxAliases, 'createView');
|
|
91
|
+
const locInfo = ctx.options.dev && node.loc ? createLocationObject(ctx.filename, node.loc) : null;
|
|
92
|
+
let viewCall = createCreateViewCall(type, finalProps, locInfo, createViewAlias);
|
|
93
|
+
// 处理指令
|
|
94
|
+
if (directives.size > 0) {
|
|
95
|
+
viewCall = processDirectives(viewCall, directives, ctx);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
viewCall = addPureComment(viewCall);
|
|
99
|
+
}
|
|
100
|
+
if (node.loc) {
|
|
101
|
+
viewCall.loc = node.loc;
|
|
102
|
+
}
|
|
103
|
+
return viewCall;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 处理元素的子元素
|
|
107
|
+
* 子元素优先于 children 属性,同时发出警告
|
|
108
|
+
*/
|
|
109
|
+
function processElementChildren(node, props, ctx) {
|
|
110
|
+
// 过滤有效子元素
|
|
111
|
+
const children = node.children.filter(child => {
|
|
112
|
+
if (isJSXText(child)) {
|
|
113
|
+
return child.value.trim().length > 0;
|
|
114
|
+
}
|
|
115
|
+
if (isJSXExpressionContainer(child)) {
|
|
116
|
+
return child.expression.type !== 'JSXEmptyExpression';
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
});
|
|
120
|
+
if (children.length === 0) {
|
|
121
|
+
return props;
|
|
122
|
+
}
|
|
123
|
+
// 检查是否存在 children 属性
|
|
124
|
+
const childrenAttr = getJSXAttributeByName(node, 'children');
|
|
125
|
+
if (childrenAttr) {
|
|
126
|
+
// 发出警告:children 属性和子元素同时存在
|
|
127
|
+
addWarning(ctx, createWarning('W001', childrenAttr));
|
|
128
|
+
}
|
|
129
|
+
// 校验 Match 组件必须在 Switch 内使用
|
|
130
|
+
// 当前元素不是 Switch 时,检查子元素中是否有 Match
|
|
131
|
+
const currentName = getJSXElementName(node);
|
|
132
|
+
if (currentName !== 'Switch') {
|
|
133
|
+
for (const child of children) {
|
|
134
|
+
if (isJSXElement(child)) {
|
|
135
|
+
const childName = getJSXElementName(child);
|
|
136
|
+
if (childName === 'Match') {
|
|
137
|
+
throw createError('E012', child);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// 处理子元素
|
|
143
|
+
const processedChildren = processChildren(children, ctx);
|
|
144
|
+
const childrenValue = processedChildren.length === 1 ? processedChildren[0] : t.arrayExpression(processedChildren);
|
|
145
|
+
// 添加 children 属性
|
|
146
|
+
if (props) {
|
|
147
|
+
props.properties.push(t.objectProperty(t.identifier('children'), childrenValue));
|
|
148
|
+
return props;
|
|
149
|
+
}
|
|
150
|
+
return t.objectExpression([t.objectProperty(t.identifier('children'), childrenValue)]);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* 转换单个 v-if 元素
|
|
154
|
+
*/
|
|
155
|
+
function transformSingleVIf(node, ctx) {
|
|
156
|
+
if (!isVIf(node))
|
|
157
|
+
return null;
|
|
158
|
+
const condition = getDirectiveValue(node, 'v-if');
|
|
159
|
+
if (!condition)
|
|
160
|
+
return null;
|
|
161
|
+
// 移除 v- 指令
|
|
162
|
+
removeVDirectives(node);
|
|
163
|
+
// 转换元素
|
|
164
|
+
const transformedNode = transformJSXElement(node, ctx, false);
|
|
165
|
+
if (!transformedNode)
|
|
166
|
+
return null;
|
|
167
|
+
// 生成 branch 调用
|
|
168
|
+
const branchCall = createBranch({ conditions: [condition], branches: [createArrowFunction(transformedNode)] }, ctx);
|
|
169
|
+
if (node.loc) {
|
|
170
|
+
branchCall.loc = node.loc;
|
|
171
|
+
}
|
|
172
|
+
return branchCall;
|
|
173
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import { addPureComment, createCreateViewCall, createLocationObject, filterWhitespaceChildren, getAlias, validateMatchInSwitch } from '../../utils/index.js';
|
|
3
|
+
import { processChildren } from './processChildren.js';
|
|
4
|
+
/**
|
|
5
|
+
* 处理 JSX Fragment
|
|
6
|
+
* @param path - JSX Fragment 路径
|
|
7
|
+
* @param ctx - 转换上下文
|
|
8
|
+
*/
|
|
9
|
+
export function processJSXFragment(path, ctx) {
|
|
10
|
+
const node = path.node;
|
|
11
|
+
// 过滤空白子节点
|
|
12
|
+
const children = filterWhitespaceChildren(node.children);
|
|
13
|
+
// 校验 Match 组件
|
|
14
|
+
validateMatchInSwitch(children);
|
|
15
|
+
// 标记需要的导入
|
|
16
|
+
ctx.imports.Fragment = true;
|
|
17
|
+
ctx.imports.createView = true;
|
|
18
|
+
const fragmentAlias = getAlias(ctx.vitarxAliases, 'Fragment');
|
|
19
|
+
const createViewAlias = getAlias(ctx.vitarxAliases, 'createView');
|
|
20
|
+
const locInfo = ctx.options.dev && node.loc ? createLocationObject(ctx.filename, node.loc) : null;
|
|
21
|
+
// 无子元素
|
|
22
|
+
if (children.length === 0) {
|
|
23
|
+
const viewCall = addPureComment(createCreateViewCall(t.identifier(fragmentAlias), null, locInfo, createViewAlias));
|
|
24
|
+
if (node.loc)
|
|
25
|
+
viewCall.loc = node.loc;
|
|
26
|
+
path.replaceWith(viewCall);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// 处理子元素
|
|
30
|
+
const processedChildren = processChildren(children, ctx);
|
|
31
|
+
const childrenValue = processedChildren.length === 1 ? processedChildren[0] : t.arrayExpression(processedChildren);
|
|
32
|
+
const props = t.objectExpression([t.objectProperty(t.identifier('children'), childrenValue)]);
|
|
33
|
+
const viewCall = addPureComment(createCreateViewCall(t.identifier(fragmentAlias), props, locInfo, createViewAlias));
|
|
34
|
+
if (node.loc)
|
|
35
|
+
viewCall.loc = node.loc;
|
|
36
|
+
path.replaceWith(viewCall);
|
|
37
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 属性处理模块
|
|
3
|
+
* 负责处理 JSX 元素的各类属性,包括普通属性、展开属性、命名空间属性等
|
|
4
|
+
* @module passes/props/attribute
|
|
5
|
+
*/
|
|
6
|
+
import * as t from '@babel/types';
|
|
7
|
+
import { isBooleanLiteral, isIdentifier, isJSXExpressionContainer, isNumericLiteral, isStringLiteral } from '@babel/types';
|
|
8
|
+
import { markImport } from '../../context.js';
|
|
9
|
+
import { createError } from '../../error.js';
|
|
10
|
+
import { getAlias } from '../../utils/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* 处理展开属性 {...props}
|
|
13
|
+
* 将展开属性转换为 v-bind 特殊属性格式
|
|
14
|
+
* @param attr - JSX 展开属性节点
|
|
15
|
+
* @param hasVBind - 是否已存在 v-bind
|
|
16
|
+
* @param node - JSX 元素节点(用于错误定位)
|
|
17
|
+
* @returns 转换后的属性对象,或 null(如果已存在 v-bind)
|
|
18
|
+
* @throws {Error} E001 - 当已存在 v-bind 时抛出
|
|
19
|
+
*/
|
|
20
|
+
export function processSpreadAttribute(attr, hasVBind, node) {
|
|
21
|
+
// 不允许多个 v-bind
|
|
22
|
+
if (hasVBind) {
|
|
23
|
+
throw createError('E001', node);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
property: t.objectProperty(t.stringLiteral('v-bind'), attr.argument),
|
|
27
|
+
hasVBind: true
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 处理单个 JSX 属性
|
|
32
|
+
* 根据属性名类型分发到不同的处理逻辑
|
|
33
|
+
* @param attr - JSX 属性节点
|
|
34
|
+
* @param existingPropNames - 已存在的属性名集合
|
|
35
|
+
* @param ctx - 转换上下文
|
|
36
|
+
* @returns 属性处理结果
|
|
37
|
+
*/
|
|
38
|
+
export function processAttribute(attr, existingPropNames, ctx) {
|
|
39
|
+
const attrName = attr.name;
|
|
40
|
+
// 处理命名空间属性 (v:xxx)
|
|
41
|
+
if (attrName.type === 'JSXNamespacedName') {
|
|
42
|
+
return processNamespacedAttribute(attr, attrName, existingPropNames, ctx);
|
|
43
|
+
}
|
|
44
|
+
// 处理标识符属性
|
|
45
|
+
if (attrName.type === 'JSXIdentifier') {
|
|
46
|
+
const name = attrName.name;
|
|
47
|
+
// v-bind 指令
|
|
48
|
+
if (name === 'v-bind') {
|
|
49
|
+
const value = getAttributeValue(attr.value);
|
|
50
|
+
return { type: 'directive', name: 'v-bind', value, isVBind: true, isVModel: false };
|
|
51
|
+
}
|
|
52
|
+
// v-model 指令
|
|
53
|
+
if (name === 'v-model') {
|
|
54
|
+
const value = getAttributeValue(attr.value);
|
|
55
|
+
return { type: 'directive', name: 'v-model', value, isVBind: false, isVModel: true };
|
|
56
|
+
}
|
|
57
|
+
// 其他 v- 指令(排除 v-if 系列,它们在单独的处理流程中)
|
|
58
|
+
if (name.startsWith('v-') && !['v-if', 'v-else-if', 'v-else'].includes(name)) {
|
|
59
|
+
const value = getAttributeValue(attr.value);
|
|
60
|
+
return { type: 'directive', name, value, isVBind: false, isVModel: false };
|
|
61
|
+
}
|
|
62
|
+
// 普通属性
|
|
63
|
+
existingPropNames.add(name);
|
|
64
|
+
const value = getAttributeValue(attr.value);
|
|
65
|
+
const property = createProperty(name, value, ctx);
|
|
66
|
+
return { type: 'property', property };
|
|
67
|
+
}
|
|
68
|
+
// 未知属性类型,作为普通属性处理
|
|
69
|
+
const value = getAttributeValue(attr.value);
|
|
70
|
+
return { type: 'property', property: t.objectProperty(t.identifier('unknown'), value) };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 处理命名空间属性
|
|
74
|
+
* 支持 v:xxx 格式的指令语法
|
|
75
|
+
* @param attr - JSX 属性节点
|
|
76
|
+
* @param attrName - 命名空间名称
|
|
77
|
+
* @param existingPropNames - 已存在的属性名集合
|
|
78
|
+
* @param ctx - 转换上下文
|
|
79
|
+
* @returns 属性处理结果
|
|
80
|
+
*/
|
|
81
|
+
function processNamespacedAttribute(attr, attrName, existingPropNames, ctx) {
|
|
82
|
+
const namespace = attrName.namespace.name;
|
|
83
|
+
const name = attrName.name.name;
|
|
84
|
+
const fullName = `${namespace}:${name}`;
|
|
85
|
+
// v:xxx 指令
|
|
86
|
+
if (namespace === 'v') {
|
|
87
|
+
const directiveName = `v-${name}`;
|
|
88
|
+
const value = getAttributeValue(attr.value);
|
|
89
|
+
// v:bind 等同于 v-bind
|
|
90
|
+
if (directiveName === 'v-bind') {
|
|
91
|
+
return { type: 'directive', name: 'v-bind', value, isVBind: true, isVModel: false };
|
|
92
|
+
}
|
|
93
|
+
// 其他指令(排除 v-if 系列和 v-model)
|
|
94
|
+
if (!['v-if', 'v-else-if', 'v-else', 'v-model'].includes(directiveName)) {
|
|
95
|
+
return { type: 'directive', name: directiveName, value, isVBind: false, isVModel: false };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 其他命名空间属性作为普通属性处理
|
|
99
|
+
existingPropNames.add(fullName);
|
|
100
|
+
const value = getAttributeValue(attr.value);
|
|
101
|
+
const property = createProperty(fullName, value, ctx);
|
|
102
|
+
return { type: 'property', property };
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 获取属性值表达式
|
|
106
|
+
* 处理不同类型的属性值,统一返回表达式
|
|
107
|
+
* @param value - JSX 属性值
|
|
108
|
+
* @returns 表达式节点
|
|
109
|
+
*/
|
|
110
|
+
function getAttributeValue(value) {
|
|
111
|
+
// 无值属性默认为 true
|
|
112
|
+
if (!value) {
|
|
113
|
+
return t.booleanLiteral(true);
|
|
114
|
+
}
|
|
115
|
+
// 字符串字面量直接返回
|
|
116
|
+
if (isStringLiteral(value)) {
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
// JSX 表达式容器
|
|
120
|
+
if (isJSXExpressionContainer(value)) {
|
|
121
|
+
// 空表达式 {} 视为 true
|
|
122
|
+
if (value.expression.type === 'JSXEmptyExpression') {
|
|
123
|
+
return t.booleanLiteral(true);
|
|
124
|
+
}
|
|
125
|
+
return value.expression;
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 创建对象属性节点
|
|
131
|
+
* 静态值直接赋值,动态值生成 getter 方法
|
|
132
|
+
*
|
|
133
|
+
* 特殊处理:
|
|
134
|
+
* - children 属性不使用 unref,因为 ref 作为 children 是合法的可变渲染源
|
|
135
|
+
* - 其他标识符属性使用 unref 解包
|
|
136
|
+
*
|
|
137
|
+
* @param key - 属性名
|
|
138
|
+
* @param value - 属性值表达式
|
|
139
|
+
* @param ctx - 转换上下文
|
|
140
|
+
* @returns 对象属性或方法节点
|
|
141
|
+
*/
|
|
142
|
+
function createProperty(key, value, ctx) {
|
|
143
|
+
// 静态值:直接赋值
|
|
144
|
+
if (isStringLiteral(value) || isNumericLiteral(value) || isBooleanLiteral(value)) {
|
|
145
|
+
return t.objectProperty(t.identifier(key), value);
|
|
146
|
+
}
|
|
147
|
+
// Identifier: 检查是否为已知的 ref 变量
|
|
148
|
+
if (isIdentifier(value)) {
|
|
149
|
+
// 已知 ref 变量:直接访问 .value
|
|
150
|
+
if (ctx.refVariables.has(value.name)) {
|
|
151
|
+
return t.objectMethod('get', t.identifier(key), [], t.blockStatement([t.returnStatement(t.memberExpression(value, t.identifier('value')))]));
|
|
152
|
+
}
|
|
153
|
+
// children 属性:直接引用,不使用 unref
|
|
154
|
+
// 原因:children 中的 ref 是合法的可变渲染源
|
|
155
|
+
if (key === 'children') {
|
|
156
|
+
return t.objectProperty(t.identifier(key), value);
|
|
157
|
+
}
|
|
158
|
+
// 其他未知变量:使用 unref 进行解包
|
|
159
|
+
markImport(ctx, 'unref');
|
|
160
|
+
const unrefAlias = getAlias(ctx.vitarxAliases, 'unref');
|
|
161
|
+
return t.objectMethod('get', t.identifier(key), [], t.blockStatement([t.returnStatement(t.callExpression(t.identifier(unrefAlias), [value]))]));
|
|
162
|
+
}
|
|
163
|
+
// 其他表达式:直接返回(保持响应性)
|
|
164
|
+
return t.objectMethod('get', t.identifier(key), [], t.blockStatement([t.returnStatement(value)]));
|
|
165
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props 处理模块入口
|
|
3
|
+
* 负责处理 JSX 元素的属性,包括 v-model 特殊指令
|
|
4
|
+
* @module passes/props
|
|
5
|
+
*/
|
|
6
|
+
import * as t from '@babel/types';
|
|
7
|
+
import { processAttribute, processSpreadAttribute } from './attribute.js';
|
|
8
|
+
import { createVModelProps, extractVModelState } from './vmodel.js';
|
|
9
|
+
/**
|
|
10
|
+
* 处理 JSX 元素的属性
|
|
11
|
+
* 将 JSX 属性转换为运行时 props 对象,并提取指令
|
|
12
|
+
*
|
|
13
|
+
* 处理流程:
|
|
14
|
+
* 1. 遍历所有属性,分类处理
|
|
15
|
+
* 2. 展开属性转换为 v-bind 格式
|
|
16
|
+
* 3. 普通属性转换为对象属性或 getter
|
|
17
|
+
* 4. 指令属性提取到 directives Map
|
|
18
|
+
* 5. v-model 特殊处理,生成 modelValue 和 onUpdate:modelValue
|
|
19
|
+
*
|
|
20
|
+
* @param node - JSX 元素节点
|
|
21
|
+
* @param ctx - 转换上下文
|
|
22
|
+
* @param hasChildren - 是否有子元素(有子元素时跳过 children 属性)
|
|
23
|
+
* @returns 处理结果,包含 props 对象、指令映射和 v-bind 标记
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```tsx
|
|
27
|
+
* // 输入
|
|
28
|
+
* <Input v-model={value} placeholder="text" disabled />
|
|
29
|
+
*
|
|
30
|
+
* // 输出 props
|
|
31
|
+
* {
|
|
32
|
+
* modelValue: get() { return value.value },
|
|
33
|
+
* 'onUpdate:modelValue': v => value.value = v,
|
|
34
|
+
* placeholder: "text",
|
|
35
|
+
* disabled: true
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function processProps(node, ctx, hasChildren = false) {
|
|
40
|
+
const attributes = node.openingElement.attributes;
|
|
41
|
+
const properties = [];
|
|
42
|
+
const directives = new Map();
|
|
43
|
+
let hasVBind = false;
|
|
44
|
+
// 追踪已存在的属性名(用于 v-model 冲突检测)
|
|
45
|
+
const existingPropNames = new Set();
|
|
46
|
+
// 提取初始 v-model 状态
|
|
47
|
+
let vModelState = extractVModelState(attributes);
|
|
48
|
+
// 遍历处理所有属性
|
|
49
|
+
for (const attr of attributes) {
|
|
50
|
+
// 处理展开属性 {...props}
|
|
51
|
+
if (attr.type === 'JSXSpreadAttribute') {
|
|
52
|
+
const result = processSpreadAttribute(attr, hasVBind, node);
|
|
53
|
+
if (result) {
|
|
54
|
+
properties.push(result.property);
|
|
55
|
+
hasVBind = result.hasVBind;
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// 处理普通属性
|
|
60
|
+
if (attr.type === 'JSXAttribute') {
|
|
61
|
+
// 如果有子元素,跳过 children 属性
|
|
62
|
+
if (hasChildren && attr.name.type === 'JSXIdentifier' && attr.name.name === 'children') {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const result = processAttribute(attr, existingPropNames, ctx);
|
|
66
|
+
if (result.type === 'directive') {
|
|
67
|
+
// v-model 指令:更新状态
|
|
68
|
+
if (result.isVModel) {
|
|
69
|
+
vModelState = { hasVModel: true, value: result.value, node: attr };
|
|
70
|
+
}
|
|
71
|
+
// v-bind 指令:添加到属性列表
|
|
72
|
+
else if (result.isVBind) {
|
|
73
|
+
properties.push(t.objectProperty(t.stringLiteral('v-bind'), result.value));
|
|
74
|
+
hasVBind = true;
|
|
75
|
+
}
|
|
76
|
+
// 其他指令:添加到指令映射
|
|
77
|
+
else {
|
|
78
|
+
directives.set(result.name, result.value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (result.type === 'property') {
|
|
82
|
+
properties.push(result.property);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// 处理 v-model:生成 modelValue 和 onUpdate:modelValue
|
|
87
|
+
if (vModelState.hasVModel && vModelState.value) {
|
|
88
|
+
const vModelProps = createVModelProps(vModelState.value, vModelState.node, existingPropNames, node, ctx);
|
|
89
|
+
properties.push(...vModelProps);
|
|
90
|
+
}
|
|
91
|
+
// 构建最终结果
|
|
92
|
+
const props = properties.length > 0 ? t.objectExpression(properties) : null;
|
|
93
|
+
return { props, directives, hasVBind };
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v-model 指令处理模块
|
|
3
|
+
* 负责处理 JSX 元素的 v-model 双向绑定指令
|
|
4
|
+
* @module passes/props/vmodel
|
|
5
|
+
*/
|
|
6
|
+
import * as t from '@babel/types';
|
|
7
|
+
import { isIdentifier, isMemberExpression } from '@babel/types';
|
|
8
|
+
import { markImport } from '../../context.js';
|
|
9
|
+
import { createError } from '../../error.js';
|
|
10
|
+
import { getAlias } from '../../utils/index.js';
|
|
11
|
+
/**
|
|
12
|
+
* 从属性列表中提取 v-model 状态
|
|
13
|
+
* @param attributes - JSX 元素属性列表
|
|
14
|
+
* @returns v-model 状态对象
|
|
15
|
+
*/
|
|
16
|
+
export function extractVModelState(attributes) {
|
|
17
|
+
for (const attr of attributes) {
|
|
18
|
+
if (attr.type === 'JSXAttribute' && attr.name.type === 'JSXIdentifier') {
|
|
19
|
+
if (attr.name.name === 'v-model') {
|
|
20
|
+
const value = attr.value?.type === 'JSXExpressionContainer'
|
|
21
|
+
? attr.value.expression
|
|
22
|
+
: null;
|
|
23
|
+
return { hasVModel: true, value, node: attr };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { hasVModel: false, value: null, node: null };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 创建 v-model 相关的 props
|
|
31
|
+
* 生成 modelValue getter 和 onUpdate:modelValue 回调
|
|
32
|
+
* @param value - v-model 绑定的值表达式
|
|
33
|
+
* @param attrNode - v-model 属性节点(用于错误定位)
|
|
34
|
+
* @param existingPropNames - 已存在的属性名集合(用于冲突检测)
|
|
35
|
+
* @param node - JSX 元素节点
|
|
36
|
+
* @param ctx - 转换上下文
|
|
37
|
+
* @returns 包含 modelValue 和 onUpdate:modelValue 的属性数组
|
|
38
|
+
* @throws {Error} E009 - 当 modelValue 或 onUpdate:modelValue 已存在时
|
|
39
|
+
* @throws {Error} E010 - 当 v-model 绑定值不是 Identifier 或 MemberExpression 时
|
|
40
|
+
*/
|
|
41
|
+
export function createVModelProps(value, attrNode, existingPropNames, node, ctx) {
|
|
42
|
+
// 检查属性名冲突
|
|
43
|
+
if (existingPropNames.has('modelValue') || existingPropNames.has('onUpdate:modelValue')) {
|
|
44
|
+
throw createError('E009', attrNode || node);
|
|
45
|
+
}
|
|
46
|
+
// v-model 只支持 Identifier 和 MemberExpression
|
|
47
|
+
if (!isIdentifier(value) && !isMemberExpression(value)) {
|
|
48
|
+
throw createError('E010', attrNode || node);
|
|
49
|
+
}
|
|
50
|
+
// Identifier: 可能是 ref 变量
|
|
51
|
+
if (isIdentifier(value)) {
|
|
52
|
+
return createIdentifierVModelProps(value, attrNode || node, ctx);
|
|
53
|
+
}
|
|
54
|
+
// MemberExpression: 直接访问属性
|
|
55
|
+
return createMemberExpressionVModelProps(value);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 为 Identifier 类型的 v-model 值创建 props
|
|
59
|
+
* 支持已知 ref 变量和未知变量两种情况
|
|
60
|
+
* @param value - Identifier 值
|
|
61
|
+
* @param node - AST 节点(用于错误定位)
|
|
62
|
+
* @param ctx - 转换上下文
|
|
63
|
+
* @returns 属性数组
|
|
64
|
+
*/
|
|
65
|
+
function createIdentifierVModelProps(value, node, ctx) {
|
|
66
|
+
const isKnownRef = ctx.refVariables.has(value.name);
|
|
67
|
+
// 已知 ref 变量:直接访问 .value
|
|
68
|
+
if (isKnownRef) {
|
|
69
|
+
return [
|
|
70
|
+
// modelValue getter: 返回 value.value
|
|
71
|
+
t.objectMethod('get', t.identifier('modelValue'), [], t.blockStatement([t.returnStatement(t.memberExpression(value, t.identifier('value')))])),
|
|
72
|
+
// onUpdate:modelValue: v => value.value = v
|
|
73
|
+
t.objectProperty(t.stringLiteral('onUpdate:modelValue'), t.arrowFunctionExpression([t.identifier('v')], t.assignmentExpression('=', t.memberExpression(value, t.identifier('value')), t.identifier('v'))))
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
// 未知变量:使用 unref 进行解包
|
|
77
|
+
markImport(ctx, 'unref');
|
|
78
|
+
const unrefAlias = getAlias(ctx.vitarxAliases, 'unref');
|
|
79
|
+
const updateBody = [];
|
|
80
|
+
// 开发模式:添加 isRef 检查
|
|
81
|
+
if (ctx.options.dev) {
|
|
82
|
+
markImport(ctx, 'isRef');
|
|
83
|
+
const isRefAlias = getAlias(ctx.vitarxAliases, 'isRef');
|
|
84
|
+
const locInfo = node.loc
|
|
85
|
+
? ` at ${ctx.filename}:${node.loc.start.line}:${node.loc.start.column + 1}`
|
|
86
|
+
: '';
|
|
87
|
+
updateBody.push(t.ifStatement(t.unaryExpression('!', t.callExpression(t.identifier(isRefAlias), [value])), t.blockStatement([
|
|
88
|
+
t.throwStatement(t.newExpression(t.identifier('Error'), [
|
|
89
|
+
t.stringLiteral(`[v-model] Identifier must be a ref. Invalid usage${locInfo}`)
|
|
90
|
+
]))
|
|
91
|
+
])));
|
|
92
|
+
}
|
|
93
|
+
// 更新逻辑:value.value = v
|
|
94
|
+
updateBody.push(t.expressionStatement(t.assignmentExpression('=', t.memberExpression(value, t.identifier('value')), t.identifier('v'))));
|
|
95
|
+
return [
|
|
96
|
+
// modelValue getter: 返回 unref(value)
|
|
97
|
+
t.objectMethod('get', t.identifier('modelValue'), [], t.blockStatement([t.returnStatement(t.callExpression(t.identifier(unrefAlias), [value]))])),
|
|
98
|
+
// onUpdate:modelValue: v => { [isRef检查]; value.value = v }
|
|
99
|
+
t.objectProperty(t.stringLiteral('onUpdate:modelValue'), t.arrowFunctionExpression([t.identifier('v')], t.blockStatement(updateBody)))
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 为 MemberExpression 类型的 v-model 值创建 props
|
|
104
|
+
* 直接访问对象属性,无需 ref 处理
|
|
105
|
+
* @param value - MemberExpression 值
|
|
106
|
+
* @returns 属性数组
|
|
107
|
+
*/
|
|
108
|
+
function createMemberExpressionVModelProps(value) {
|
|
109
|
+
return [
|
|
110
|
+
// modelValue getter: 直接返回表达式
|
|
111
|
+
t.objectMethod('get', t.identifier('modelValue'), [], t.blockStatement([t.returnStatement(value)])),
|
|
112
|
+
// onUpdate:modelValue: v => obj.prop = v
|
|
113
|
+
t.objectProperty(t.stringLiteral('onUpdate:modelValue'), t.arrowFunctionExpression([t.identifier('v')], t.assignmentExpression('=', value, t.identifier('v'))))
|
|
114
|
+
];
|
|
115
|
+
}
|