@dongmingzhixiu/vuexir-compiler 0.1.0 → 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.
@@ -116,6 +116,37 @@ export function buildTheme(tokens) {
116
116
  return { colorNames, fontNames };
117
117
  }
118
118
  /** R42 生成 Theme.swift(颜色/字体去重常量) */
119
+ export const VRN_HELPERS_SWIFT = `import Foundation
120
+
121
+ // vuexir 运行时辅助(R78 对象字段取值)
122
+ /// R78 对象字段取值:状态里存 JSON 字符串时按 key 取值(非 JSON → 返回空串)
123
+ /// R85 点路径取值:key 支持 "a.b.c" 与末段 ".length"(数组/字典/字符串长度)
124
+ func vrnField(_ json: String, _ path: String, _ fallback: String = "") -> String {
125
+ let raw = vrnFieldRaw(json, path)
126
+ return raw.isEmpty ? fallback : raw
127
+ }
128
+
129
+ /// 原始取值(不套兜底):点路径下钻,缺失返回空串
130
+ private func vrnFieldRaw(_ json: String, _ path: String) -> String {
131
+ guard let data = json.data(using: .utf8),
132
+ let root = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else { return "" }
133
+ var cur: Any = root
134
+ for seg in path.split(separator: ".").map(String.init) {
135
+ if seg == "length" || seg == "size" {
136
+ if let a = cur as? [Any] { cur = a.count }
137
+ else if let d = cur as? [String: Any] { cur = d.count }
138
+ else if let s = cur as? String { cur = s.count }
139
+ else { return "" }
140
+ continue
141
+ }
142
+ if let d = cur as? [String: Any], let next = d[seg] { cur = next } else { return "" }
143
+ }
144
+ if let s = cur as? String { return s }
145
+ if let b = cur as? Bool { return b ? "true" : "false" }
146
+ if let n = cur as? NSNumber { return n.stringValue }
147
+ return String(describing: cur)
148
+ }
149
+ `;
119
150
  export function generateThemeSource(palette) {
120
151
  const lines = ['import SwiftUI', '', '// GENERATED by vue-render-native compiler · R42 theme tokens', 'enum Theme {'];
121
152
  const colors = [...palette.colorNames.entries()].map(([hex, name]) => {
@@ -471,6 +502,10 @@ function emitNode(node, ctx) {
471
502
  }
472
503
  /** JS 条件表达式 → Swift 兼容:===/!== 为 JS 严格比较,Swift 用 ==/!=;字符串字面量单引号 → 双引号 */
473
504
  function swiftCondExpr(expr) {
505
+ // R78 续(G12):对象字段条件 → 非空判断
506
+ const f = expr.match(/^__vrnField__:(\w+):(\w+)$/);
507
+ if (f)
508
+ return `!vrnField(${f[1]}, "${f[2]}").isEmpty`;
474
509
  let s = expr.replace(/!==/g, '!=').replace(/===/g, '==');
475
510
  s = s.replace(/'(\\[^]|[^'\\])*'/g, (m) => '"' + m.slice(1, -1).replace(/"/g, '\\"') + '"');
476
511
  return s;
@@ -715,26 +750,30 @@ function emitNodeInner(node, ctx) {
715
750
  ? pad(`Text(${props.ref})`)
716
751
  : props.bindExpr !== undefined
717
752
  ? pad(`Text(${props.bindExpr})`)
718
- : props.dynField !== undefined
719
- ? pad(`Text(${dictFieldExpr(props.dynField.var, props.dynField.field)})`)
720
- : props.bindParts !== undefined
721
- ? pad(`Text("${props.bindParts
722
- .map((p) => 'text' in p
723
- ? swiftStringEscape(p.text)
724
- : 'bind' in p
725
- ? `\\(${p.bind})`
726
- : `\\(${dictFieldExpr(p.dynField.var, p.dynField.field)})`)
727
- .join('')}")`)
728
- : isBind
729
- ? ctx.loop && (bindKey === ctx.loop.itemKey || bindKey === ctx.loop.indexKey)
730
- ? pad(`Text("\\(${bindKey})")`)
731
- : currentStateTypes[bindKey] === 'number'
753
+ : props.fieldOf !== undefined
754
+ ? pad(`Text(vrnField(${props.fieldOf.obj}, "${props.fieldOf.field}"${props.fieldOf.fallback !== undefined ? `, "${swiftStringEscape(props.fieldOf.fallback)}"` : ''}))`)
755
+ : props.dynField !== undefined
756
+ ? pad(`Text(${dictFieldExpr(props.dynField.var, props.dynField.field)})`)
757
+ : props.bindParts !== undefined
758
+ ? pad(`Text("${props.bindParts
759
+ .map((p) => 'text' in p
760
+ ? swiftStringEscape(p.text)
761
+ : 'bind' in p
762
+ ? `\\(${p.bind})`
763
+ : 'fieldOf' in p
764
+ ? `\\(vrnField(${p.fieldOf.obj}, "${p.fieldOf.field}"${p.fieldOf.fallback !== undefined ? `, "${swiftStringEscape(p.fieldOf.fallback)}"` : ''}))`
765
+ : `\\(${dictFieldExpr(p.dynField.var, p.dynField.field)})`)
766
+ .join('')}")`)
767
+ : isBind
768
+ ? ctx.loop && (bindKey === ctx.loop.itemKey || bindKey === ctx.loop.indexKey)
732
769
  ? pad(`Text("\\(${bindKey})")`)
733
- : currentStateTypes[bindKey] === 'boolean'
734
- // 布尔 state → String(describing:)(Text(Bool) 无构造匹配,typecheck 失败)
735
- ? pad(`Text(String(describing: ${bindKey}))`)
736
- : pad(`Text(${bindKey})`)
737
- : pad(`Text("${swiftStringEscape(content)}")`);
770
+ : currentStateTypes[bindKey] === 'number'
771
+ ? pad(`Text("\\(${bindKey})")`)
772
+ : currentStateTypes[bindKey] === 'boolean'
773
+ // 布尔 state → String(describing:)(Text(Bool) 无构造匹配,typecheck 失败)
774
+ ? pad(`Text(String(describing: ${bindKey}))`)
775
+ : pad(`Text(${bindKey})`)
776
+ : pad(`Text("${swiftStringEscape(content)}")`);
738
777
  line += `.font(${fontExpr(fs, props.fontWeight, 'normal')})`;
739
778
  // M2 :style 动态 → 运行时从 state 读取(fontSize 数值 / color hex 字符串)
740
779
  if (props.fontSizeDynamic !== undefined) {
@@ -1060,11 +1099,13 @@ export function generateScreenSource(screen, opts = {}) {
1060
1099
  if (opts.bindingMode === 'external') {
1061
1100
  return ` @Binding var ${s.name}: ${typeof s.init === 'string' ? 'String' : typeof s.init === 'boolean' ? 'Bool' : 'Int'}`;
1062
1101
  }
1063
- const init = typeof s.init === 'string'
1064
- ? `"${swiftStringEscape(s.init)}"`
1065
- : typeof s.init === 'boolean'
1066
- ? (s.init ? 'true' : 'false')
1067
- : String(s.init);
1102
+ const init = s.obj && typeof s.init === 'string'
1103
+ ? `#"${s.init}"#` // R85 对象态:JSON 原文(vrnField 按点路径解析)
1104
+ : typeof s.init === 'string'
1105
+ ? `"${swiftStringEscape(s.init)}"`
1106
+ : typeof s.init === 'boolean'
1107
+ ? (s.init ? 'true' : 'false')
1108
+ : String(s.init);
1068
1109
  return ` @State private var ${s.name} = ${init}`;
1069
1110
  })
1070
1111
  .join('\n');
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';
@@ -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
- /** @click="fn(arg)" → 若 fn 是已提取的 setter,内联为状态赋值 { var, value, kind }(M1 扩展无参表达式 setter)
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 适配基准) */