@dongmingzhixiu/vuexir-compiler 0.3.12 → 0.3.14
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/dist/codegen/compose.js +2 -1
- package/dist/codegen/swiftui.js +6 -1
- package/dist/ir/schema.js +5 -2
- package/dist/transform/vue2ir.js +63 -4
- package/package.json +1 -1
package/dist/codegen/compose.js
CHANGED
|
@@ -1176,7 +1176,8 @@ function emitNodeInner(node, ctx, depth, loop, useParamModifier = false, parentS
|
|
|
1176
1176
|
}
|
|
1177
1177
|
case 'button': {
|
|
1178
1178
|
const chain = modChain([...stretchModifiers(parentStretch, node), ...sizeModifiers(p), ...absModifiers(p), ...clipModifiers(p), ...visualModifiers(p), ...borderModifiers(p), ...paddingModifiers(p), ...alphaArr, ...transformModifiers(p)], pad + ' ');
|
|
1179
|
-
|
|
1179
|
+
// R108:动态按钮文案(props.bind)优先;Kotlin 里用字符串模板插值
|
|
1180
|
+
const label = p.bind !== undefined ? '"${' + String(p.bind) + '}"' : (p.label ?? '');
|
|
1180
1181
|
const click = kotlinClickAction(p, ctx, loop);
|
|
1181
1182
|
const labelStyle = [];
|
|
1182
1183
|
if (p.fontSize !== undefined)
|
package/dist/codegen/swiftui.js
CHANGED
|
@@ -824,7 +824,12 @@ function emitNodeInner(node, ctx) {
|
|
|
824
824
|
return line;
|
|
825
825
|
}
|
|
826
826
|
case 'button': {
|
|
827
|
-
|
|
827
|
+
// R108:动态按钮文案(props.bind → @State)优先于静态 label
|
|
828
|
+
const label = props.bind !== undefined
|
|
829
|
+
? props.bind
|
|
830
|
+
: props.label !== undefined
|
|
831
|
+
? props.label
|
|
832
|
+
: '按钮';
|
|
828
833
|
const fs = props.fontSize ?? 15;
|
|
829
834
|
// 事件:onSet(状态直接赋值)优先,其次 onClick(JS 运行时),无事件则空 action
|
|
830
835
|
// M1:onSet.kind==='expr' → 表达式原样嵌入(count + 1);literal → 数字原样/字符串加引号
|
package/dist/ir/schema.js
CHANGED
|
@@ -118,7 +118,7 @@ const NODE_ALLOWED_PROPS = {
|
|
|
118
118
|
group: ['width', 'height', 'padding', 'gap', 'flex', 'flexDirection', 'justifyContent', 'alignItems', 'background', 'radius', 'borderWidth', 'borderColor', 'marginTopAuto', 'marginTop', 'marginLeftAuto', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides', 'clip', 'onClick', 'onSet', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity'],
|
|
119
119
|
text: ['width', 'text', 'ref', 'bind', 'bindExpr', 'dynField', 'bindParts', 'fontSize', 'fontWeight', 'textAlign', 'lineHeight', 'color', 'padding', 'colorActive', 'marginLeftAuto', 'marginTop', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'flex', 'opacity', 'onClick', 'onSet', 'fieldOf'],
|
|
120
120
|
image: ['width', 'height', 'src', 'objectFit', 'radius', 'borderSides', 'borderWidth', 'borderColor', 'clip', 'flex', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity'],
|
|
121
|
-
button: ['width', 'height', 'label', 'background', 'radius', 'borderWidth', 'borderColor', 'fontSize', 'fontWeight', 'color', 'flex', 'padding', 'marginTopAuto', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides', 'clip', 'onClick', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'pressBg', 'opacity'],
|
|
121
|
+
button: ['width', 'height', 'label', 'bind', 'background', 'radius', 'borderWidth', 'borderColor', 'fontSize', 'fontWeight', 'color', 'flex', 'padding', 'marginTopAuto', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'borderSides', 'clip', 'onClick', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'pressBg', 'opacity'],
|
|
122
122
|
input: ['width', 'height', 'value', 'placeholder', 'fontSize', 'color', 'background', 'padding', 'radius', 'borderWidth', 'borderColor', 'borderSides', 'clip', 'flex', 'marginTop', 'marginTopAuto', 'marginBottom', 'marginRight', 'marginLeft', 'onSet', 'colorDynamic', 'fontSizeDynamic', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show'],
|
|
123
123
|
scroll: ['width', 'height', 'padding', 'gap', 'flexDirection', 'background', 'showsIndicators', 'flex'],
|
|
124
124
|
spacer: ['width', 'height', 'flex'],
|
|
@@ -276,7 +276,10 @@ function validateNode(node, path, issues, seenKeys, stateNames = new Set(), inLo
|
|
|
276
276
|
if (node.type === 'text' && !node.bind && (!node.props || (node.props.text === undefined && node.props.ref === undefined && node.props.bind === undefined && node.props.bindExpr === undefined && node.props.dynField === undefined && node.props.bindParts === undefined && node.props.fieldOf === undefined))) {
|
|
277
277
|
issues.push({ path, message: 'text 节点必须有 props.text / props.ref / props.bind / props.bindExpr / props.fieldOf 或 bind' });
|
|
278
278
|
}
|
|
279
|
-
|
|
279
|
+
// R108:按钮文案可以是静态 label,也可以是动态 bind(运行时求值)
|
|
280
|
+
if (node.type === 'button' &&
|
|
281
|
+
!node.bind &&
|
|
282
|
+
(!node.props || (node.props.label === undefined && node.props.bind === undefined))) {
|
|
280
283
|
issues.push({ path, message: 'button 节点必须有 props.label 或 bind' });
|
|
281
284
|
}
|
|
282
285
|
// condition / active
|
package/dist/transform/vue2ir.js
CHANGED
|
@@ -896,6 +896,7 @@ const TAG_MAP = {
|
|
|
896
896
|
select: { type: 'group', defaultProps: { flexDirection: 'column' } },
|
|
897
897
|
option: { type: 'text', defaultProps: { fontSize: 14 } },
|
|
898
898
|
aside: { type: 'group', defaultProps: { flexDirection: 'column' } },
|
|
899
|
+
label: { type: 'text', defaultProps: { fontSize: 13 } },
|
|
899
900
|
figure: { type: 'group', defaultProps: { flexDirection: 'column' } },
|
|
900
901
|
figcaption: { type: 'text', defaultProps: { fontSize: 12 } },
|
|
901
902
|
h1: { type: 'text', defaultProps: { fontSize: 24, fontWeight: 'bold' } },
|
|
@@ -1279,7 +1280,10 @@ export function vueToIr(source, opts = {}) {
|
|
|
1279
1280
|
// 5) <script setup> 逻辑源码 → 运行时 JS(R12:事件逻辑由原生 JS 引擎执行)
|
|
1280
1281
|
// 仅支持 JS 子集(无 TS 类型标注、无 import);超出即编译期警告(后续阶段扩展)
|
|
1281
1282
|
// R98 P3:把模板表达式下沉产生的求值状态并入页面状态,并重建 runtime(含 recompute)
|
|
1282
|
-
if (jsExprStates.length > 0
|
|
1283
|
+
if (jsExprStates.length > 0) {
|
|
1284
|
+
// 无 <script setup> 的页面也要能承载表达式求值状态
|
|
1285
|
+
if (!stateInfo)
|
|
1286
|
+
stateInfo = { states: [], setters: {} };
|
|
1283
1287
|
for (const st of jsExprStates) {
|
|
1284
1288
|
if (!stateInfo.states.some((x) => x.name === st.name))
|
|
1285
1289
|
stateInfo.states.push({ ...st });
|
|
@@ -1630,13 +1634,34 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1630
1634
|
}
|
|
1631
1635
|
}
|
|
1632
1636
|
}
|
|
1633
|
-
|
|
1637
|
+
let vf = parseVFor(forExp, warnings, vforConsts);
|
|
1634
1638
|
void sunkList;
|
|
1635
1639
|
if (!vf) {
|
|
1636
|
-
|
|
1640
|
+
// R108 兜底:任意 `item in <表达式>` 都能下沉——运行时求值成 JSON 数组即可
|
|
1641
|
+
const gm = forExp.trim().match(/^(?:\(([^)]+)\)|([A-Za-z_$][\w$]*))\s+in\s+([\s\S]+)$/);
|
|
1642
|
+
if (gm) {
|
|
1643
|
+
const expr = gm[3].trim();
|
|
1644
|
+
// Vue3 语义:`v-for="i in 3"` 迭代 1..3
|
|
1645
|
+
const rangeM = expr.match(/^(\d+)$/);
|
|
1646
|
+
const sunk = addJsExprState(rangeM
|
|
1647
|
+
? `JSON.stringify(Array.from({ length: ${rangeM[1]} }, function (_, i) { return i + 1; }))`
|
|
1648
|
+
: `JSON.stringify(${expr})`);
|
|
1649
|
+
vf = {
|
|
1650
|
+
listState: sunk,
|
|
1651
|
+
itemKey: (gm[1] ? gm[1].split(',')[0] : gm[2]).trim(),
|
|
1652
|
+
indexKey: gm[1] ? gm[1].split(',')[1]?.trim() : undefined,
|
|
1653
|
+
};
|
|
1654
|
+
warnings.push(`v-for「${forExp.trim().slice(0, 40)}」的数据源下沉为运行时求值(R108)`);
|
|
1655
|
+
}
|
|
1656
|
+
else {
|
|
1657
|
+
throw new VueCompileError('v-for 语法无法解析(需 `item in <表达式>` 形态)');
|
|
1658
|
+
}
|
|
1637
1659
|
}
|
|
1638
1660
|
// M4b:动态列表数据源必须已声明为 ref 数组状态(编译期校验,防悬空引用)
|
|
1639
|
-
|
|
1661
|
+
// 表达式下沉产生的状态(_vrnE*)在 walk 之后才并入页面状态,这里放行
|
|
1662
|
+
if (vf.listState &&
|
|
1663
|
+
!/^_vrnE\d+$/.test(vf.listState) &&
|
|
1664
|
+
!(state && state.states.some((s) => s.name === vf.listState))) {
|
|
1640
1665
|
throw new VueCompileError(`v-for 动态数据源「${vf.listState}」未在 <script setup> 中声明为 ref 数组(M4b)`);
|
|
1641
1666
|
}
|
|
1642
1667
|
const innerNode = { ...node, props: node.props.filter((p) => !(p.type === NodeTypes.DIRECTIVE && p.name === 'for')) };
|
|
@@ -1856,6 +1881,18 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
1856
1881
|
warnings.push(`属性 ${attrName}="${attrValue}" 暂不支持,已忽略`);
|
|
1857
1882
|
}
|
|
1858
1883
|
}
|
|
1884
|
+
// R108:button 只接受 label/bind —— 布局属性丢弃、缺 label 时占位(避免整页失败)
|
|
1885
|
+
if (mapping.type === 'button') {
|
|
1886
|
+
const badProps = ['flexDirection', 'justifyContent', 'alignItems', 'gap', 'flex', 'textAlign', 'lineHeight', 'overflow', 'clip'];
|
|
1887
|
+
for (const k of badProps)
|
|
1888
|
+
delete props[k];
|
|
1889
|
+
}
|
|
1890
|
+
if (mapping.type === 'input') {
|
|
1891
|
+
// R108:input 只接受 value/placeholder 等,文本样式属性一律丢弃
|
|
1892
|
+
for (const k of ['textAlign', 'lineHeight', 'fontWeight', 'flexDirection', 'justifyContent', 'alignItems']) {
|
|
1893
|
+
delete props[k];
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1859
1896
|
// 子节点:
|
|
1860
1897
|
// - button:文本 → label
|
|
1861
1898
|
// - text 类型(p/span/h1~h4):全部文本子节点合并为 props.text;出现元素子节点 → 行内混合报错;
|
|
@@ -2021,6 +2058,13 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2021
2058
|
props.label = ((props.label ?? '') + child.content.trim()).trim();
|
|
2022
2059
|
continue;
|
|
2023
2060
|
}
|
|
2061
|
+
if (child.type === NodeTypes.INTERPOLATION) {
|
|
2062
|
+
// R108:按钮文案是动态表达式 → bind(运行时求值),原生端照样显示
|
|
2063
|
+
const raw = textOf(child.content).trim();
|
|
2064
|
+
if (!props.bind && !props.label)
|
|
2065
|
+
props.bind = addJsExprState(raw);
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2024
2068
|
if (child.type === NodeTypes.ELEMENT) {
|
|
2025
2069
|
// 常见写法兜底:<button><span>文字</span></button> → 内联文本并入 label(否则 IR 校验因缺 label 中断)
|
|
2026
2070
|
const inner = elementInnerText(child).trim();
|
|
@@ -2037,6 +2081,16 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2037
2081
|
children.push(...walk(child, classProps, comboRules, scopedRules, pseudoRules, warnings, state, compTable, propRefs, loopScope, [...ancestorClasses, ...elementClasses]));
|
|
2038
2082
|
}
|
|
2039
2083
|
}
|
|
2084
|
+
// R108:文本节点丢弃布局类属性(原生端 width/height/flexDirection 只对容器有意义)
|
|
2085
|
+
if (mapping.type === 'text') {
|
|
2086
|
+
const LAYOUT_ONLY = ['height', 'width', 'flexDirection', 'justifyContent', 'alignItems', 'flex', 'gap'];
|
|
2087
|
+
const dropped = LAYOUT_ONLY.filter((k) => props[k] !== undefined);
|
|
2088
|
+
if (dropped.length > 0) {
|
|
2089
|
+
for (const k of dropped)
|
|
2090
|
+
delete props[k];
|
|
2091
|
+
warnings.push(`<${node.tag}> 是文本节点,布局属性(${dropped.join('/')})已忽略(R108)`);
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2040
2094
|
// R88 续:容器节点不接受文本专属样式(color/fontSize/fontWeight/textAlign/lineHeight)
|
|
2041
2095
|
// 原生端容器没有「字体继承」,这类属性会直接触发 IR 校验拒收 → 明确降级 + 警告
|
|
2042
2096
|
if (mapping.type === 'group' || mapping.type === 'for') {
|
|
@@ -2048,6 +2102,11 @@ function walkElement(node, classProps, comboRules, scopedRules, pseudoRules, war
|
|
|
2048
2102
|
warnings.push(`<${node.tag}> 是容器,文本样式(${dropped.join('/')})在原生端不继承 → 已忽略;请把样式下沉到具体文本节点(R88)`);
|
|
2049
2103
|
}
|
|
2050
2104
|
}
|
|
2105
|
+
// R108 兜底:按钮最终必须有可见文案(label 或 bind),否则整页校验失败
|
|
2106
|
+
if (mapping.type === 'button' && props.label === undefined && props.bind === undefined) {
|
|
2107
|
+
props.label = ' ';
|
|
2108
|
+
warnings.push(`<button> 无可读文案(可能是纯图标按钮)→ 已占位,建议补 aria-label 或文案(R108)`);
|
|
2109
|
+
}
|
|
2051
2110
|
// R77 空容器(<div class="spacer" /> 等占位写法)→ 降级为 spacer 占位 + 警告,而不是中断整个编译
|
|
2052
2111
|
if (mapping.type === 'group' && children.length === 0) {
|
|
2053
2112
|
warnings.push(`<${node.tag}> 无子节点 → 降级为占位 spacer(R17/R77;可用 padding/height 表达留白)`);
|