@dongmingzhixiu/vuexir-compiler 0.1.1 → 0.2.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/dist/codegen/compose.d.ts +17 -3
- package/dist/codegen/compose.js +324 -39
- package/dist/codegen/swiftui.d.ts +1 -0
- package/dist/codegen/swiftui.js +65 -24
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/ir/schema.d.ts +16 -0
- package/dist/ir/schema.js +4 -3
- package/dist/transform/stores.d.ts +55 -0
- package/dist/transform/stores.js +303 -0
- package/dist/transform/vue2ir.d.ts +4 -3
- package/dist/transform/vue2ir.js +594 -21
- package/package.json +7 -7
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,6 @@ export { parseCss, parseInlineStyle, mergeIrProps, CssParseError } from './trans
|
|
|
12
12
|
export type { CssParseResult } from './transform/css.js';
|
|
13
13
|
export { validateIr, parseAndValidateIr } from './ir/schema.js';
|
|
14
14
|
export type { IrScreen, IrNode, IrProps, IrValidationIssue } from './ir/schema.js';
|
|
15
|
-
export { generateScreenSource, generateComponentSource, generateThemeSource, generateViewExtensions, collectTheme, buildTheme, } from './codegen/swiftui.js';
|
|
16
|
-
export { emitNode, buildNavHost, parseRouter, resolveInput, runTabs } from './codegen/compose.js';
|
|
15
|
+
export { generateScreenSource, generateComponentSource, generateThemeSource, generateViewExtensions, VRN_HELPERS_SWIFT, collectTheme, buildTheme, } from './codegen/swiftui.js';
|
|
16
|
+
export { emitNode, buildNavHost, parseRouter, resolveInput, runTabs, findProjectRoot } from './codegen/compose.js';
|
|
17
17
|
export type { RunTabsOptions } from './codegen/compose.js';
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
export { vueToIr, VueCompileError, extractScreenState, extractScreenConstArrays, extractScreenNavBar, extractFunctions, extractOnMounted, rewriteStateRefs, generateRuntimeSource, swiftExpr, resolveSetCall, } from './transform/vue2ir.js';
|
|
10
10
|
export { parseCss, parseInlineStyle, mergeIrProps, CssParseError } from './transform/css.js';
|
|
11
11
|
export { validateIr, parseAndValidateIr } from './ir/schema.js';
|
|
12
|
-
export { generateScreenSource, generateComponentSource, generateThemeSource, generateViewExtensions, collectTheme, buildTheme, } from './codegen/swiftui.js';
|
|
12
|
+
export { generateScreenSource, generateComponentSource, generateThemeSource, generateViewExtensions, VRN_HELPERS_SWIFT, collectTheme, buildTheme, } from './codegen/swiftui.js';
|
|
13
13
|
// 工程脚手架入口(runTabs):保留在 core 供 CLI 与旧脚本调用;模板由调用方注入(P2)
|
|
14
|
-
export { emitNode, buildNavHost, parseRouter, resolveInput, runTabs } from './codegen/compose.js';
|
|
14
|
+
export { emitNode, buildNavHost, parseRouter, resolveInput, runTabs, findProjectRoot } from './codegen/compose.js';
|
package/dist/ir/schema.d.ts
CHANGED
|
@@ -66,6 +66,13 @@ export interface IrProps {
|
|
|
66
66
|
var: string;
|
|
67
67
|
field: string;
|
|
68
68
|
};
|
|
69
|
+
/** R78 对象字段插值:{{ obj.field }}(obj 为 JSON 字符串状态;非状态对象编译期警告) */
|
|
70
|
+
/** R92 对象字段取值;fallback 为缺失时的兜底文案(对应 `{{ a.b ?? '兜底' }}`) */
|
|
71
|
+
fieldOf?: {
|
|
72
|
+
obj: string;
|
|
73
|
+
field: string;
|
|
74
|
+
fallback?: string;
|
|
75
|
+
};
|
|
69
76
|
/** M4b 混合文本:静态段 + 插值/动态字段段 → Text("\(bind)静态\(dyn)") */
|
|
70
77
|
bindParts?: ({
|
|
71
78
|
text: string;
|
|
@@ -76,6 +83,12 @@ export interface IrProps {
|
|
|
76
83
|
var: string;
|
|
77
84
|
field: string;
|
|
78
85
|
};
|
|
86
|
+
} | {
|
|
87
|
+
fieldOf: {
|
|
88
|
+
obj: string;
|
|
89
|
+
field: string;
|
|
90
|
+
fallback?: string;
|
|
91
|
+
};
|
|
79
92
|
})[];
|
|
80
93
|
/** group 专属 */
|
|
81
94
|
flexDirection?: FlexDirection;
|
|
@@ -246,9 +259,12 @@ export interface IrScreen {
|
|
|
246
259
|
/** 预留:静态数据表 */
|
|
247
260
|
data?: Record<string, unknown>;
|
|
248
261
|
/** 屏级状态(ref 提取):name=变量名,init=初始值;生成 Swift @State */
|
|
262
|
+
/** 页面状态;obj=true 表示对象态(init 为 JSON 字符串,供 vrnField 点路径读取,R85) */
|
|
249
263
|
state?: {
|
|
250
264
|
name: string;
|
|
251
265
|
init: number | string | boolean;
|
|
266
|
+
list?: boolean;
|
|
267
|
+
obj?: boolean;
|
|
252
268
|
}[];
|
|
253
269
|
/** M3 <script setup> 提取的 handler 函数(@click="fn()" 引用;body 已做 .value → stateStore 重写)
|
|
254
270
|
* R72:async:true 表示 `async function`(JS 侧保留 async 关键字;Android 侧按协程形状生成) */
|
package/dist/ir/schema.js
CHANGED
|
@@ -108,6 +108,7 @@ const PROPS_VALIDATORS = {
|
|
|
108
108
|
v !== null &&
|
|
109
109
|
['scale', 'rotate', 'translateX', 'translateY'].every((k) => v[k] === undefined || typeof v[k] === 'number'),
|
|
110
110
|
show: (v) => typeof v === 'string',
|
|
111
|
+
fieldOf: (v) => typeof v === 'object' && v !== null && typeof v.obj === 'string' && typeof v.field === 'string',
|
|
111
112
|
opacity: (v) => typeof v === 'number' && v >= 0 && v <= 1,
|
|
112
113
|
placeholder: (v) => typeof v === 'string',
|
|
113
114
|
};
|
|
@@ -115,7 +116,7 @@ const PROPS_VALIDATORS = {
|
|
|
115
116
|
const NODE_ALLOWED_PROPS = {
|
|
116
117
|
root: ['width', 'height', 'background'],
|
|
117
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'],
|
|
118
|
-
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'],
|
|
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'],
|
|
119
120
|
image: ['width', 'height', 'src', 'objectFit', 'radius', 'borderSides', 'borderWidth', 'borderColor', 'clip', 'flex', 'marginTop', 'marginBottom', 'marginRight', 'marginLeft', 'position', 'absTop', 'absLeft', 'absRight', 'absBottom', 'transform', 'show', 'opacity'],
|
|
120
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
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'],
|
|
@@ -272,8 +273,8 @@ function validateNode(node, path, issues, seenKeys, stateNames = new Set(), inLo
|
|
|
272
273
|
issues.push({ path, message: `${node.type} 为叶子节点,不允许 children` });
|
|
273
274
|
}
|
|
274
275
|
// 文本内容:text 节点需 props.text / props.ref(组件 prop 引用)/ props.bind(页面 state,M1)或 bind
|
|
275
|
-
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))) {
|
|
276
|
-
issues.push({ path, message: 'text 节点必须有 props.text / props.ref / props.bind / props.bindExpr 或 bind' });
|
|
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
|
+
issues.push({ path, message: 'text 节点必须有 props.text / props.ref / props.bind / props.bindExpr / props.fieldOf 或 bind' });
|
|
277
278
|
}
|
|
278
279
|
if (node.type === 'button' && !node.bind && (!node.props || node.props.label === undefined)) {
|
|
279
280
|
issues.push({ path, message: 'button 节点必须有 props.label 或 bind' });
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R86 Pinia store 编译期折叠(第一步:状态读取)
|
|
3
|
+
*
|
|
4
|
+
* 原生端没有响应式 store 运行时,页面读 store 状态(`ui.dialog.title`)必须变成
|
|
5
|
+
* 「页面自有状态 + 编译期注入的初值」。本模块把 `defineStore` 源码解析成
|
|
6
|
+
* `StoreDef`,再由 vue2ir 把 `alias.field` 拍平成 `alias_field` 状态。
|
|
7
|
+
*
|
|
8
|
+
* 白名单(R13 铁律):只认 state 里的**字面量**初值(数字/布尔/字符串/对象/数组);
|
|
9
|
+
* getter / action 暂不折叠(调用会明确 warning),跨页共享也不保证(各页各一份初值)。
|
|
10
|
+
*/
|
|
11
|
+
export interface StoreStateField {
|
|
12
|
+
name: string;
|
|
13
|
+
/** 规范化后的初值:原始量或 JSON 字符串(obj/list) */
|
|
14
|
+
init: number | string | boolean;
|
|
15
|
+
obj?: boolean;
|
|
16
|
+
list?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface StoreDef {
|
|
19
|
+
/** defineStore 的第一个参数(store id) */
|
|
20
|
+
key: string;
|
|
21
|
+
/** `useXxxStore` 标识符 */
|
|
22
|
+
fnName: string;
|
|
23
|
+
state: StoreStateField[];
|
|
24
|
+
/** getter:name + 可折叠的成员链(`(s) => s.dialog.visible` → stateField='dialog', path='visible') */
|
|
25
|
+
getters: {
|
|
26
|
+
name: string;
|
|
27
|
+
/** 可折叠:从 state 字段出发的纯成员链 */
|
|
28
|
+
stateField?: string;
|
|
29
|
+
path?: string;
|
|
30
|
+
/** 含计算逻辑时的底层 state 字段(降级读取用;筛选/排序语义会丢失) */
|
|
31
|
+
refField?: string;
|
|
32
|
+
/** 多字段依赖(无法降级到单一 state 字段) */
|
|
33
|
+
multi?: boolean;
|
|
34
|
+
}[];
|
|
35
|
+
/** action:name + 形参表 + 函数体(用于折叠成原生赋值;无法折叠则忽略) */
|
|
36
|
+
actions: {
|
|
37
|
+
name: string;
|
|
38
|
+
params: string[];
|
|
39
|
+
body: string;
|
|
40
|
+
}[];
|
|
41
|
+
}
|
|
42
|
+
/** 把对象字面量规范化为 JSON 文本(单引号 → 双引号、裸键补引号) */
|
|
43
|
+
export declare function normalizeJsonish(raw: string): string;
|
|
44
|
+
/** 解析单文件里的所有 `export const useXxxStore = defineStore('key', {...})` */
|
|
45
|
+
export declare function parseStoreSource(src: string): StoreDef[];
|
|
46
|
+
/** 由多文件源码构建 registry:useXxxStore → StoreDef */
|
|
47
|
+
export declare function buildStoreRegistry(sources: Record<string, string>): Map<string, StoreDef>;
|
|
48
|
+
/** 从 `<script setup>` 里找 `const alias = useXxxStore()` 的别名映射 */
|
|
49
|
+
export declare function findStoreAliases(script: string, registry: Map<string, StoreDef>): Map<string, StoreDef>;
|
|
50
|
+
/**
|
|
51
|
+
* R88 解析 store 文件里导出的**字面量**常量数组(`export const SKINS: SkinMeta[] = [{...}, ...]`)。
|
|
52
|
+
* 返回 { 名称 → 数组内部文本 },交给 v-for 做编译期内联(原生端无 JS 模块系统)。
|
|
53
|
+
* 含函数调用/变量引用的元素不做过滤(由下游 JSON 化失败时保持原样并告警)。
|
|
54
|
+
*/
|
|
55
|
+
export declare function parseSharedConstArrays(src: string): Record<string, string>;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* R86 Pinia store 编译期折叠(第一步:状态读取)
|
|
3
|
+
*
|
|
4
|
+
* 原生端没有响应式 store 运行时,页面读 store 状态(`ui.dialog.title`)必须变成
|
|
5
|
+
* 「页面自有状态 + 编译期注入的初值」。本模块把 `defineStore` 源码解析成
|
|
6
|
+
* `StoreDef`,再由 vue2ir 把 `alias.field` 拍平成 `alias_field` 状态。
|
|
7
|
+
*
|
|
8
|
+
* 白名单(R13 铁律):只认 state 里的**字面量**初值(数字/布尔/字符串/对象/数组);
|
|
9
|
+
* getter / action 暂不折叠(调用会明确 warning),跨页共享也不保证(各页各一份初值)。
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* 方法简写的函数体起点:跳过返回类型里的泛型尖括号(`Array<{ ... }>`),
|
|
13
|
+
* 取**尖括号之外**的第一个 `{` —— 这样单行多个方法(`a(){}, b(){}`)也能正确定位。
|
|
14
|
+
*/
|
|
15
|
+
function methodBodyStart(text, matchIndex) {
|
|
16
|
+
const parenAt = text.indexOf(')', matchIndex);
|
|
17
|
+
if (parenAt === -1)
|
|
18
|
+
return -1;
|
|
19
|
+
let angle = 0;
|
|
20
|
+
for (let i = parenAt + 1; i < text.length; i++) {
|
|
21
|
+
const ch = text[i];
|
|
22
|
+
if (ch === '<')
|
|
23
|
+
angle++;
|
|
24
|
+
else if (ch === '>')
|
|
25
|
+
angle = Math.max(0, angle - 1);
|
|
26
|
+
else if (ch === '{' && angle === 0)
|
|
27
|
+
return i;
|
|
28
|
+
else if (ch === '}' && angle === 0)
|
|
29
|
+
return -1;
|
|
30
|
+
}
|
|
31
|
+
return -1;
|
|
32
|
+
}
|
|
33
|
+
/** 找到 src 中从 start(含)开始的花括号配对结束位置;未闭合返回 -1 */
|
|
34
|
+
function matchBrace(src, start) {
|
|
35
|
+
let depth = 0;
|
|
36
|
+
for (let i = start; i < src.length; i++) {
|
|
37
|
+
const ch = src[i];
|
|
38
|
+
if (ch === '{')
|
|
39
|
+
depth++;
|
|
40
|
+
else if (ch === '}') {
|
|
41
|
+
depth--;
|
|
42
|
+
if (depth === 0)
|
|
43
|
+
return i;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return -1;
|
|
47
|
+
}
|
|
48
|
+
/** 把对象字面量规范化为 JSON 文本(单引号 → 双引号、裸键补引号) */
|
|
49
|
+
export function normalizeJsonish(raw) {
|
|
50
|
+
return (raw
|
|
51
|
+
.replace(/'([^']*)'/g, (_a, inner) => `"${String(inner).replace(/"/g, '\\"')}"`)
|
|
52
|
+
.replace(/([{,]\s*)([A-Za-z_$][\w$]*)\s*:/g, (_a, pre, key) => `${pre}"${key}":`)
|
|
53
|
+
// 尾逗号([...,] / {...,})会让 JSONSerialization 解析失败
|
|
54
|
+
.replace(/,(\s*[}\]])/g, '$1')
|
|
55
|
+
// 单行化:Swift 的 raw string #"..."# 不允许多行内容
|
|
56
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
57
|
+
.replace(/\s{2,}/g, ' ')
|
|
58
|
+
.trim());
|
|
59
|
+
}
|
|
60
|
+
/** 分析一层对象的顶层键值(粗粒度:按顶层逗号切分,值保留原文) */
|
|
61
|
+
function splitTopLevel(body) {
|
|
62
|
+
const out = [];
|
|
63
|
+
let depth = 0;
|
|
64
|
+
let cur = '';
|
|
65
|
+
let quote = '';
|
|
66
|
+
for (const ch of body) {
|
|
67
|
+
if (quote) {
|
|
68
|
+
cur += ch;
|
|
69
|
+
if (ch === quote)
|
|
70
|
+
quote = '';
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (ch === '"' || ch === "'") {
|
|
74
|
+
quote = ch;
|
|
75
|
+
cur += ch;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === '{' || ch === '[' || ch === '(')
|
|
79
|
+
depth++;
|
|
80
|
+
if (ch === '}' || ch === ']' || ch === ')')
|
|
81
|
+
depth--;
|
|
82
|
+
if (ch === ',' && depth === 0) {
|
|
83
|
+
out.push({ key: '', value: cur });
|
|
84
|
+
cur = '';
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
cur += ch;
|
|
88
|
+
}
|
|
89
|
+
if (cur.trim())
|
|
90
|
+
out.push({ key: '', value: cur });
|
|
91
|
+
return out
|
|
92
|
+
.map(({ value }) => {
|
|
93
|
+
const m = value.match(/^\s*([A-Za-z_$][\w$]*|"[^"]*"|'[^']*')\s*:\s*([\s\S]*)$/);
|
|
94
|
+
if (!m)
|
|
95
|
+
return null;
|
|
96
|
+
return { key: m[1].replace(/^['"]|['"]$/g, ''), value: m[2].trim() };
|
|
97
|
+
})
|
|
98
|
+
.filter((x) => !!x);
|
|
99
|
+
}
|
|
100
|
+
/** 解析单文件里的所有 `export const useXxxStore = defineStore('key', {...})` */
|
|
101
|
+
export function parseStoreSource(src) {
|
|
102
|
+
const defs = [];
|
|
103
|
+
const re = /const\s+(use[A-Za-z0-9_$]*Store)\s*=\s*defineStore\s*\(\s*(['"])([^'"]+)\2\s*,\s*([\s\S]*)$/m;
|
|
104
|
+
const m = src.match(re);
|
|
105
|
+
if (!m)
|
|
106
|
+
return defs;
|
|
107
|
+
const fnName = m[1];
|
|
108
|
+
const key = m[3];
|
|
109
|
+
let body = m[4].trim();
|
|
110
|
+
const def = { key, fnName, state: [], getters: [], actions: [] };
|
|
111
|
+
if (body.startsWith('{')) {
|
|
112
|
+
// options 写法:{ state: () => ({...}), getters: {...}, actions: {...} }
|
|
113
|
+
const end = matchBrace(body, 0);
|
|
114
|
+
if (end !== -1)
|
|
115
|
+
body = body.slice(0, end + 1);
|
|
116
|
+
// state: () => ({...}) / state: (): UiState => ({...}) / state: {...}
|
|
117
|
+
const stateMatch = body.match(/state\s*:\s*(?:\([^)]*\)\s*(?::\s*[\w$<>\[\], .|]+)?=>\s*)?\(?\s*\{/);
|
|
118
|
+
if (stateMatch) {
|
|
119
|
+
const braceAt = body.indexOf('{', stateMatch.index ?? 0);
|
|
120
|
+
const stateEnd = braceAt === -1 ? -1 : matchBrace(body, braceAt);
|
|
121
|
+
if (stateEnd !== -1) {
|
|
122
|
+
const fields = splitTopLevel(body.slice(braceAt + 1, stateEnd));
|
|
123
|
+
for (const f of fields) {
|
|
124
|
+
const lit = classifyLiteral(f.value);
|
|
125
|
+
if (lit)
|
|
126
|
+
def.state.push({ name: f.key, ...lit });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const gm = body.match(/getters\s*:\s*\{/);
|
|
131
|
+
if (gm) {
|
|
132
|
+
const at = body.indexOf('{', gm.index ?? 0);
|
|
133
|
+
const endIdx = at === -1 ? -1 : matchBrace(body, at);
|
|
134
|
+
if (endIdx !== -1) {
|
|
135
|
+
// getter 是方法简写(`upcoming(state): T { ... }`)或箭头箭头(`isOpen: (s) => s.x.y`),
|
|
136
|
+
// `key: value` 切分对方法简写无效(标识符后面跟的是形参表),必须两路都扫
|
|
137
|
+
const block = body.slice(at + 1, endIdx);
|
|
138
|
+
const found = [];
|
|
139
|
+
const gM = /(?:^|,)\s*(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*(?::[^{]+)?\{/g;
|
|
140
|
+
let gm2;
|
|
141
|
+
let cursor = 0;
|
|
142
|
+
while ((gm2 = gM.exec(block))) {
|
|
143
|
+
const braceAt = methodBodyStart(block, gm2.index);
|
|
144
|
+
const bodyEnd = braceAt === -1 ? -1 : matchBrace(block, braceAt);
|
|
145
|
+
if (bodyEnd === -1)
|
|
146
|
+
break;
|
|
147
|
+
found.push({ name: gm2[1], value: block.slice(braceAt + 1, bodyEnd).trim() });
|
|
148
|
+
gM.lastIndex = bodyEnd + 1;
|
|
149
|
+
cursor = bodyEnd + 1;
|
|
150
|
+
}
|
|
151
|
+
// 箭头写法(没被方法扫描吃掉的部分)
|
|
152
|
+
for (const x of splitTopLevel(block)) {
|
|
153
|
+
if (!x.value || found.some((f) => f.name === x.key))
|
|
154
|
+
continue;
|
|
155
|
+
found.push({ name: x.key, value: x.value });
|
|
156
|
+
}
|
|
157
|
+
void cursor;
|
|
158
|
+
def.getters = found.map((x) => getterShape(x.name, x.value));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const am = body.match(/actions\s*:\s*\{/);
|
|
162
|
+
if (am) {
|
|
163
|
+
const at = body.indexOf('{', am.index ?? 0);
|
|
164
|
+
const endIdx = at === -1 ? -1 : matchBrace(body, at);
|
|
165
|
+
if (endIdx !== -1) {
|
|
166
|
+
// action 是方法简写(`setSkin(skin) { ... }`)而非 `key: value`,必须单独扫描
|
|
167
|
+
const block = body.slice(at + 1, endIdx);
|
|
168
|
+
const ARE = /(?:async\s+)?([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*\{/g;
|
|
169
|
+
let m;
|
|
170
|
+
while ((m = ARE.exec(block))) {
|
|
171
|
+
const braceAt = methodBodyStart(block, m.index);
|
|
172
|
+
const bodyEnd = braceAt === -1 ? -1 : matchBrace(block, braceAt);
|
|
173
|
+
if (bodyEnd === -1)
|
|
174
|
+
break;
|
|
175
|
+
def.actions.push({
|
|
176
|
+
name: m[1],
|
|
177
|
+
params: m[2]
|
|
178
|
+
.split(',')
|
|
179
|
+
.map((t) => t.trim().replace(/:.*$/, '').trim())
|
|
180
|
+
.filter(Boolean),
|
|
181
|
+
body: block.slice(braceAt + 1, bodyEnd).trim(),
|
|
182
|
+
});
|
|
183
|
+
ARE.lastIndex = bodyEnd + 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// setup 写法:defineStore('key', () => { const a = ref(1); ... return { a } })
|
|
190
|
+
for (const rm of src.matchAll(/const\s+(\w+)\s*=\s*ref(?:<[^>]*>)?\(\s*([^)]*)\)/g)) {
|
|
191
|
+
const lit = classifyLiteral(rm[2].trim());
|
|
192
|
+
if (lit)
|
|
193
|
+
def.state.push({ name: rm[1], ...lit });
|
|
194
|
+
}
|
|
195
|
+
for (const fm of src.matchAll(/function\s+(\w+)\s*\(([^)]*)\)\s*\{([\s\S]*?)\n\}/g)) {
|
|
196
|
+
def.actions.push({
|
|
197
|
+
name: fm[1],
|
|
198
|
+
params: fm[2].split(',').map((t) => t.trim()).filter(Boolean),
|
|
199
|
+
body: fm[3].trim(),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
defs.push(def);
|
|
204
|
+
return defs;
|
|
205
|
+
}
|
|
206
|
+
/** 字面量分类:数字/布尔/字符串 → 原始量;对象/数组 → JSON 字符串 */
|
|
207
|
+
function classifyLiteral(raw) {
|
|
208
|
+
const v = raw.trim().replace(/,$/, '');
|
|
209
|
+
if (!v)
|
|
210
|
+
return null;
|
|
211
|
+
if (/^-?\d+(\.\d+)?$/.test(v))
|
|
212
|
+
return { init: Number(v) };
|
|
213
|
+
if (v === 'true' || v === 'false')
|
|
214
|
+
return { init: v === 'true' };
|
|
215
|
+
if (/^'[^']*'$/.test(v) || /^"[^"]*"$/.test(v))
|
|
216
|
+
return { init: v.slice(1, -1) };
|
|
217
|
+
if (v.startsWith('{') && matchBrace(v, 0) === v.length - 1)
|
|
218
|
+
return { init: normalizeJsonish(v), obj: true };
|
|
219
|
+
if (v.startsWith('[') && v.endsWith(']')) {
|
|
220
|
+
const json = normalizeJsonish(v);
|
|
221
|
+
// 元素全为字面量才当列表;含调用/变量则放弃(保持警告,不静默)
|
|
222
|
+
if (!/[A-Za-z_$][\w$]*\s*\(/.test(v))
|
|
223
|
+
return { init: json, list: true };
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
/** 由多文件源码构建 registry:useXxxStore → StoreDef */
|
|
228
|
+
export function buildStoreRegistry(sources) {
|
|
229
|
+
const map = new Map();
|
|
230
|
+
for (const src of Object.values(sources)) {
|
|
231
|
+
for (const def of parseStoreSource(src))
|
|
232
|
+
map.set(def.fnName, def);
|
|
233
|
+
}
|
|
234
|
+
return map;
|
|
235
|
+
}
|
|
236
|
+
/** 从 `<script setup>` 里找 `const alias = useXxxStore()` 的别名映射 */
|
|
237
|
+
export function findStoreAliases(script, registry) {
|
|
238
|
+
const aliases = new Map();
|
|
239
|
+
for (const m of script.matchAll(/const\s+(\w+)\s*=\s*(use[A-Za-z0-9_$]*Store)\s*\(\s*\)/g)) {
|
|
240
|
+
const def = registry.get(m[2]);
|
|
241
|
+
if (def)
|
|
242
|
+
aliases.set(m[1], def);
|
|
243
|
+
}
|
|
244
|
+
return aliases;
|
|
245
|
+
}
|
|
246
|
+
/** 找到 src 中从 start(含)开始的方括号配对结束位置;未闭合返回 -1 */
|
|
247
|
+
function matchBracket(src, start) {
|
|
248
|
+
let depth = 0;
|
|
249
|
+
for (let i = start; i < src.length; i++) {
|
|
250
|
+
const ch = src[i];
|
|
251
|
+
if (ch === '[')
|
|
252
|
+
depth++;
|
|
253
|
+
else if (ch === ']') {
|
|
254
|
+
depth--;
|
|
255
|
+
if (depth === 0)
|
|
256
|
+
return i;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return -1;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* R88 解析 store 文件里导出的**字面量**常量数组(`export const SKINS: SkinMeta[] = [{...}, ...]`)。
|
|
263
|
+
* 返回 { 名称 → 数组内部文本 },交给 v-for 做编译期内联(原生端无 JS 模块系统)。
|
|
264
|
+
* 含函数调用/变量引用的元素不做过滤(由下游 JSON 化失败时保持原样并告警)。
|
|
265
|
+
*/
|
|
266
|
+
export function parseSharedConstArrays(src) {
|
|
267
|
+
const out = {};
|
|
268
|
+
for (const m of src.matchAll(/export\s+const\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*\[/g)) {
|
|
269
|
+
// 注意:类型标注里可能先出现 `[`(`SkinMeta[]`),必须从 `=` 之后找数组起始方括号
|
|
270
|
+
const afterEq = src.indexOf('=', m.index ?? 0);
|
|
271
|
+
const at = afterEq === -1 ? -1 : src.indexOf('[', afterEq);
|
|
272
|
+
if (at === -1)
|
|
273
|
+
continue;
|
|
274
|
+
const end = matchBracket(src, at);
|
|
275
|
+
if (end === -1)
|
|
276
|
+
continue;
|
|
277
|
+
out[m[1]] = src.slice(at + 1, end).trim();
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* R90 getter 形态分析:
|
|
283
|
+
* 纯成员链(`(s) => s.dialog.visible`)→ 可折叠成点路径;
|
|
284
|
+
* 其余(遍历/过滤/排序/调用)→ 记录底层 state 字段供降级读取(语义丢失,调用方必须告警)。
|
|
285
|
+
* state 写法有三种:`(state) => state.list` 形参式、`this.list` 方法体式、`state.list`。
|
|
286
|
+
*/
|
|
287
|
+
function getterShape(name, value) {
|
|
288
|
+
const v = value.trim();
|
|
289
|
+
// 纯成员链
|
|
290
|
+
const m = v.match(/^\s*\(?\s*(\w+)\s*\)?\s*=>\s*([\w$.\[\]']+)\s*$/);
|
|
291
|
+
if (m) {
|
|
292
|
+
const segs = m[2].split('.');
|
|
293
|
+
if (segs.length >= 2 && segs[0] === m[1]) {
|
|
294
|
+
return { name, stateField: segs[1], path: segs.slice(2).join('.') || undefined };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// 降级:找 getter 体里引用到的 state 字段(方法体式 this.x / 形参式 state.x / s.x)
|
|
298
|
+
const refs = [...v.matchAll(/\b(?:this|state|s)\.([A-Za-z_$][\w$]*)\b/g)].map((r) => r[1]);
|
|
299
|
+
const uniq = [...new Set(refs)];
|
|
300
|
+
if (uniq.length === 1)
|
|
301
|
+
return { name, refField: uniq[0] };
|
|
302
|
+
return { name, ...(uniq.length > 1 ? { multi: true } : {}) };
|
|
303
|
+
}
|
|
@@ -7,6 +7,7 @@ export interface ScreenStateInfo {
|
|
|
7
7
|
name: string;
|
|
8
8
|
init: number | string | boolean;
|
|
9
9
|
list?: boolean;
|
|
10
|
+
obj?: boolean;
|
|
10
11
|
}[];
|
|
11
12
|
/** setter 函数名 → { target: 状态变量名, param: 形参名, expr: 无参函数体的赋值表达式 } */
|
|
12
13
|
setters: Record<string, {
|
|
@@ -52,14 +53,14 @@ export declare function generateRuntimeSource(states: {
|
|
|
52
53
|
async?: boolean;
|
|
53
54
|
}[], onMounted: string | undefined, onMountedAsync?: boolean): string;
|
|
54
55
|
export declare function swiftExpr(exp: string): string | null;
|
|
55
|
-
|
|
56
|
-
* M2:直接赋值表达式 `selected = item` / `selected = i`(列表选中)同样内联 */
|
|
57
|
-
export declare function resolveSetCall(handler: string, state: ScreenStateInfo): {
|
|
56
|
+
export declare function resolveSetCall(handler: string, state: ScreenStateInfo, warn?: (msg: string) => void): {
|
|
58
57
|
var: string;
|
|
59
58
|
value: number | string;
|
|
60
59
|
kind: 'literal' | 'expr';
|
|
61
60
|
} | null;
|
|
62
61
|
export interface Vue2IrOptions {
|
|
62
|
+
/** R86 Pinia store 源码(文件名 → 源码);提供后页面可用 `const ui = useUiStore()` 读状态 */
|
|
63
|
+
storeSources?: Record<string, string>;
|
|
63
64
|
screenId?: string;
|
|
64
65
|
title?: string;
|
|
65
66
|
/** 设计稿高度(默认 640,R16 适配基准) */
|