@dongmingzhixiu/vuexir-compiler 0.5.7 → 0.7.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/bindings.d.ts +11 -0
- package/dist/codegen/bindings.js +32 -0
- package/dist/codegen/compose.d.ts +10 -1
- package/dist/codegen/compose.js +297 -65
- package/dist/codegen/dynamic-vector.d.ts +3 -0
- package/dist/codegen/dynamic-vector.js +81 -0
- package/dist/codegen/page-js-android.d.ts +2 -0
- package/dist/codegen/page-js-android.js +59 -0
- package/dist/codegen/project-assets.d.ts +5 -0
- package/dist/codegen/project-assets.js +31 -0
- package/dist/codegen/project-styles.d.ts +2 -0
- package/dist/codegen/project-styles.js +41 -0
- package/dist/codegen/swiftui.d.ts +4 -25
- package/dist/codegen/swiftui.js +251 -71
- package/dist/codegen/vector.d.ts +3 -0
- package/dist/codegen/vector.js +165 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +8 -2
- package/dist/ir/schema.d.ts +41 -1
- package/dist/ir/schema.js +39 -20
- package/dist/transform/css.d.ts +21 -1
- package/dist/transform/css.js +217 -14
- package/dist/transform/dynamic-svg.d.ts +3 -0
- package/dist/transform/dynamic-svg.js +53 -0
- package/dist/transform/svg.d.ts +16 -0
- package/dist/transform/svg.js +96 -0
- package/dist/transform/tab-bar.d.ts +17 -0
- package/dist/transform/tab-bar.js +44 -0
- package/dist/transform/visuals.d.ts +17 -0
- package/dist/transform/visuals.js +91 -0
- package/dist/transform/vue2ir.d.ts +12 -1
- package/dist/transform/vue2ir.js +581 -188
- package/package.json +4 -2
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** 动态 SVG 仅传递绘图数据,双端仍由原生 Canvas 绘制。 */
|
|
2
|
+
export const dynamicVectorSwiftHelpers = String.raw `
|
|
3
|
+
func vrnSvgPath(_ source: String) -> Path {
|
|
4
|
+
var path = Path()
|
|
5
|
+
let pattern = "[MmLlHhVvCcQqZz]|[-+]?(?:[0-9]*\\.)?[0-9]+(?:[eE][-+]?[0-9]+)?"
|
|
6
|
+
guard let regex = try? NSRegularExpression(pattern: pattern) else { return path }
|
|
7
|
+
let ns = source as NSString
|
|
8
|
+
let tokens = regex.matches(in: source, range: NSRange(location: 0, length: ns.length)).map { ns.substring(with: $0.range) }
|
|
9
|
+
var i = 0; var command = "M"; var x = 0.0; var y = 0.0; var sx = 0.0; var sy = 0.0
|
|
10
|
+
func number() -> Double { let value = Double(tokens[i]) ?? 0; i += 1; return value }
|
|
11
|
+
while i < tokens.count {
|
|
12
|
+
if tokens[i].first?.isLetter == true { command = tokens[i]; i += 1 }
|
|
13
|
+
let relative = command == command.lowercased()
|
|
14
|
+
let upper = command.uppercased()
|
|
15
|
+
let needed = ["M":2,"L":2,"H":1,"V":1,"C":6,"Q":4,"Z":0][upper] ?? -1
|
|
16
|
+
if needed < 0 || i + needed > tokens.count { break }
|
|
17
|
+
let bx = relative ? x : 0; let by = relative ? y : 0
|
|
18
|
+
switch upper {
|
|
19
|
+
case "M", "L":
|
|
20
|
+
x = number() + bx; y = number() + by
|
|
21
|
+
if upper == "M" { path.move(to: CGPoint(x:x,y:y)); sx=x;sy=y;command=relative ? "l":"L" }
|
|
22
|
+
else { path.addLine(to: CGPoint(x:x,y:y)) }
|
|
23
|
+
case "H": x = number() + bx; path.addLine(to: CGPoint(x:x,y:y))
|
|
24
|
+
case "V": y = number() + by; path.addLine(to: CGPoint(x:x,y:y))
|
|
25
|
+
case "C":
|
|
26
|
+
let a=number()+bx,b=number()+by,c=number()+bx,d=number()+by
|
|
27
|
+
x=number()+bx;y=number()+by
|
|
28
|
+
path.addCurve(to:CGPoint(x:x,y:y),control1:CGPoint(x:a,y:b),control2:CGPoint(x:c,y:d))
|
|
29
|
+
case "Q":
|
|
30
|
+
let a=number()+bx,b=number()+by;x=number()+bx;y=number()+by
|
|
31
|
+
path.addQuadCurve(to:CGPoint(x:x,y:y),control:CGPoint(x:a,y:b))
|
|
32
|
+
case "Z": path.closeSubpath();x=sx;y=sy;command="M"
|
|
33
|
+
default: break
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return path
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
export const wrapLayoutSwiftHelpers = String.raw `
|
|
40
|
+
struct VrnWrapFlex: LayoutValueKey { static let defaultValue = 0.0 }
|
|
41
|
+
struct VrnWrapMin: LayoutValueKey { static let defaultValue = 0.0 }
|
|
42
|
+
struct VrnWrapBreak: LayoutValueKey { static let defaultValue = false }
|
|
43
|
+
struct VrnWrapLayout: Layout {
|
|
44
|
+
var spacing: Double
|
|
45
|
+
private func plan(_ available: CGFloat, _ views: Subviews) -> ([CGRect], CGSize) {
|
|
46
|
+
let width = max(1, available)
|
|
47
|
+
var rows: [[Int]] = []; var row: [Int] = []; var used = 0.0
|
|
48
|
+
var widths: [Double] = Array(repeating: 0, count: views.count)
|
|
49
|
+
for i in views.indices {
|
|
50
|
+
let item = views[i]
|
|
51
|
+
let full = item[VrnWrapBreak.self]
|
|
52
|
+
let w = full ? width : max(item[VrnWrapMin.self], item[VrnWrapFlex.self] > 0 ? item[VrnWrapMin.self] : min(width, item.sizeThatFits(.unspecified).width))
|
|
53
|
+
if !row.isEmpty && (full || used + spacing + w > width) { rows.append(row);row=[];used=0 }
|
|
54
|
+
if !row.isEmpty { used += spacing }
|
|
55
|
+
row.append(i);used += w;widths[i]=w
|
|
56
|
+
if full {rows.append(row);row=[];used=0}
|
|
57
|
+
}
|
|
58
|
+
if !row.isEmpty {rows.append(row)}
|
|
59
|
+
var frames=Array(repeating:CGRect.zero,count:views.count);var y=0.0
|
|
60
|
+
for items in rows {
|
|
61
|
+
let occupied=items.reduce(0.0){$0+widths[$1]}+Double(max(0,items.count-1))*spacing
|
|
62
|
+
let flex=items.reduce(0.0){$0+views[$1][VrnWrapFlex.self]}
|
|
63
|
+
let extra=max(0,width-occupied);var x=0.0;var height=0.0
|
|
64
|
+
for i in items {
|
|
65
|
+
let w=widths[i]+(flex > 0 ? extra*views[i][VrnWrapFlex.self]/flex : 0)
|
|
66
|
+
let h=views[i].sizeThatFits(ProposedViewSize(width:w,height:nil)).height
|
|
67
|
+
frames[i]=CGRect(x:x,y:y,width:w,height:h);x+=w+spacing;height=max(height,h)
|
|
68
|
+
}
|
|
69
|
+
y+=height+spacing
|
|
70
|
+
}
|
|
71
|
+
return (frames,CGSize(width:width,height:max(0,y-spacing)))
|
|
72
|
+
}
|
|
73
|
+
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
|
74
|
+
plan(proposal.width ?? 390,subviews).1
|
|
75
|
+
}
|
|
76
|
+
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
|
77
|
+
let frames=plan(bounds.width,subviews).0
|
|
78
|
+
for i in subviews.indices {let f=frames[i];subviews[i].place(at:CGPoint(x:bounds.minX+f.minX,y:bounds.minY+f.minY),anchor:.topLeading,proposal:ProposedViewSize(width:f.width,height:f.height))}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** 每页独立的 QuickJS 引擎:业务状态通过 JSON 回写 Compose,避免跨页全局变量互相覆盖。 */
|
|
2
|
+
export const pageJsAndroid = (bundleId) => `package ${bundleId}
|
|
3
|
+
|
|
4
|
+
class VrnPageJs(context: android.content.Context, asset: String) {
|
|
5
|
+
private val engine = app.cash.quickjs.QuickJs.create()
|
|
6
|
+
private val prefs = context.getSharedPreferences("vuexir.runtime", android.content.Context.MODE_PRIVATE)
|
|
7
|
+
private var closed = false
|
|
8
|
+
init {
|
|
9
|
+
val stored = org.json.JSONObject()
|
|
10
|
+
prefs.all.forEach { (key, value) -> stored.put(key, value.toString()) }
|
|
11
|
+
eval("var __vrnPersist = " + stored.toString() + ";")
|
|
12
|
+
eval("""
|
|
13
|
+
var __vrnOut = {};
|
|
14
|
+
function __routerPush(route) { __vrnOut.__nav=String(route); }
|
|
15
|
+
function __routerBack() { __vrnOut.__navBack=true; }
|
|
16
|
+
function __setState(k,v) { __vrnOut[k] = v; }
|
|
17
|
+
function __lsGet(k) { return __vrnPersist[k] === undefined ? null : __vrnPersist[k]; }
|
|
18
|
+
function __lsSet(k,v) { if(v === null) delete __vrnPersist[k]; else __vrnPersist[k]=String(v); __vrnOut['__ls:'+k]=v; }
|
|
19
|
+
var console = {log:function(){},warn:function(){},error:function(){}};
|
|
20
|
+
var __vrnNativeTimers = {}, __vrnNativeTimerSeq = 0;
|
|
21
|
+
function setTimeout(fn,ms) { var id=++__vrnNativeTimerSeq; __vrnNativeTimers[id]={fn:fn,at:Date.now()+(Number(ms)||0),repeat:0};return id; }
|
|
22
|
+
function clearTimeout(id) { delete __vrnNativeTimers[id]; }
|
|
23
|
+
function setInterval(fn,ms) {var id=setTimeout(fn,ms);__vrnNativeTimers[id].repeat=Math.max(1,Number(ms)||0);return id;}
|
|
24
|
+
function clearInterval(id){clearTimeout(id);}
|
|
25
|
+
function __vrnNativeTick(){
|
|
26
|
+
var now=Date.now();
|
|
27
|
+
Object.keys(__vrnNativeTimers).forEach(function(id){var t=__vrnNativeTimers[id];if(!t||t.at>now)return;
|
|
28
|
+
if(t.repeat)t.at=now+t.repeat;else delete __vrnNativeTimers[id];try{t.fn();}catch(e){console.error(e);}});
|
|
29
|
+
if(typeof __vrnRecompute==='function')__vrnRecompute();
|
|
30
|
+
if(typeof __vrnSyncPage==='function')__vrnSyncPage();
|
|
31
|
+
}
|
|
32
|
+
""")
|
|
33
|
+
eval(context.assets.open(asset).bufferedReader().use { it.readText() })
|
|
34
|
+
eval("if(typeof __onMounted==='function')__onMounted(); void 0;")
|
|
35
|
+
}
|
|
36
|
+
fun eval(code: String): String? {
|
|
37
|
+
if (closed) return null
|
|
38
|
+
return try { engine.evaluate(code)?.toString() } catch (e: Throwable) {
|
|
39
|
+
android.util.Log.e("VrnPageJs", "JavaScript: " + e.message); null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
fun template(expression: String, bindings: Map<String, Any?>): String {
|
|
43
|
+
val expr = org.json.JSONObject.quote(expression)
|
|
44
|
+
val json = org.json.JSONObject.quote(org.json.JSONObject(bindings).toString())
|
|
45
|
+
return eval("__vrnEvalTemplate(" + expr + "," + json + ")") ?: ""
|
|
46
|
+
}
|
|
47
|
+
fun drain(): org.json.JSONObject {
|
|
48
|
+
val json = org.json.JSONObject(eval("JSON.stringify(__vrnOut)") ?: "{}")
|
|
49
|
+
eval("__vrnOut = {}; void 0;")
|
|
50
|
+
json.keys().asSequence().filter { it.startsWith("__ls:") }.forEach { key ->
|
|
51
|
+
val edit = prefs.edit()
|
|
52
|
+
if (json.isNull(key)) edit.remove(key.removePrefix("__ls:")) else edit.putString(key.removePrefix("__ls:"), json.optString(key))
|
|
53
|
+
edit.apply()
|
|
54
|
+
}
|
|
55
|
+
return json
|
|
56
|
+
}
|
|
57
|
+
fun close() { if (!closed) { closed = true; engine.close() } }
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IrNode } from '../ir/schema.js';
|
|
2
|
+
/** 把页面显式导入的图片交给纯编译器;数据 URI 让生成的源码不依赖开发机路径。 */
|
|
3
|
+
export declare function loadProjectAssets(pageFile: string, root: string): Record<string, string>;
|
|
4
|
+
/** 工程输出阶段把 data URI 落成资源,避免巨型字符串进入 Swift/Kotlin 的类型检查器。 */
|
|
5
|
+
export declare function materializeProjectAssets(ui: IrNode, directory: string, platform: 'ios' | 'android'): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve, extname } from 'node:path';
|
|
4
|
+
/** 把页面显式导入的图片交给纯编译器;数据 URI 让生成的源码不依赖开发机路径。 */
|
|
5
|
+
export function loadProjectAssets(pageFile, root) {
|
|
6
|
+
const out = {};
|
|
7
|
+
const source = readFileSync(pageFile, 'utf8');
|
|
8
|
+
for (const m of source.matchAll(/import\s+([A-Za-z_$][\w$]*)\s+from\s*['"]([^'"]+\.(?:png|jpe?g|webp|gif))['"]/g)) {
|
|
9
|
+
const path = m[2].startsWith('@/') ? join(root, 'src', m[2].slice(2)) : resolve(dirname(pageFile), m[2]);
|
|
10
|
+
if (!existsSync(path))
|
|
11
|
+
continue;
|
|
12
|
+
const ext = extname(path).slice(1).replace('jpg', 'jpeg');
|
|
13
|
+
out[m[1]] = `data:image/${ext};base64,${readFileSync(path).toString('base64')}`;
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
/** 工程输出阶段把 data URI 落成资源,避免巨型字符串进入 Swift/Kotlin 的类型检查器。 */
|
|
18
|
+
export function materializeProjectAssets(ui, directory, platform) {
|
|
19
|
+
const walk = (node) => {
|
|
20
|
+
const match = node.props?.src?.match(/^data:image\/(png|jpeg|gif|webp);base64,([\s\S]+)$/);
|
|
21
|
+
if (match && node.props) {
|
|
22
|
+
const bytes = Buffer.from(match[2], 'base64');
|
|
23
|
+
const name = 'vrn-' + createHash('sha256').update(bytes).digest('hex').slice(0, 16) + '.' + (match[1] === 'jpeg' ? 'jpg' : match[1]);
|
|
24
|
+
mkdirSync(directory, { recursive: true });
|
|
25
|
+
writeFileSync(join(directory, name), bytes);
|
|
26
|
+
node.props.src = (platform === 'ios' ? 'bundle://' : 'asset://') + name;
|
|
27
|
+
}
|
|
28
|
+
node.children?.forEach(walk);
|
|
29
|
+
};
|
|
30
|
+
walk(ui);
|
|
31
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname, resolve } from 'node:path';
|
|
3
|
+
import postcss from 'postcss';
|
|
4
|
+
/** 工程生成层的文件读取:只追踪入口显式导入的 CSS,不扫描或误用未启用的样式文件。 */
|
|
5
|
+
export function loadProjectStyles(projectRoot) {
|
|
6
|
+
const styles = [];
|
|
7
|
+
const seen = new Set();
|
|
8
|
+
const load = (file) => {
|
|
9
|
+
if (seen.has(file) || !existsSync(file))
|
|
10
|
+
return;
|
|
11
|
+
seen.add(file);
|
|
12
|
+
const root = postcss.parse(readFileSync(file, 'utf8'), { from: file });
|
|
13
|
+
// 在 @import 出现的位置展开,保留导入与本地声明之间的级联顺序。
|
|
14
|
+
for (const node of [...root.nodes]) {
|
|
15
|
+
if (node.type !== 'atrule' || node.name !== 'import')
|
|
16
|
+
continue;
|
|
17
|
+
const match = node.params.match(/^(?:url\(\s*)?['"]([^'"]+\.css)['"]\s*\)?\s*$/);
|
|
18
|
+
if (!match || !match[1].startsWith('.'))
|
|
19
|
+
continue;
|
|
20
|
+
const imported = [];
|
|
21
|
+
const start = styles.length;
|
|
22
|
+
load(resolve(dirname(file), match[1]));
|
|
23
|
+
imported.push(...styles.splice(start));
|
|
24
|
+
node.replaceWith(...postcss.parse(imported.join('\n')).nodes);
|
|
25
|
+
}
|
|
26
|
+
styles.push(root.toString());
|
|
27
|
+
};
|
|
28
|
+
const entry = ['src/main.ts', 'src/main.js', 'src/main.mts', 'src/index.ts', 'src/index.js']
|
|
29
|
+
.map((name) => join(projectRoot, name)).find(existsSync);
|
|
30
|
+
if (!entry)
|
|
31
|
+
return styles;
|
|
32
|
+
const source = readFileSync(entry, 'utf8');
|
|
33
|
+
for (const match of source.matchAll(/\bimport\s+(?:[^'";\n]*?\s+from\s*)?['"]([^'"]+\.css)['"]/g)) {
|
|
34
|
+
const spec = match[1];
|
|
35
|
+
if (spec.startsWith('.'))
|
|
36
|
+
load(resolve(dirname(entry), spec));
|
|
37
|
+
else if (spec.startsWith('@/'))
|
|
38
|
+
load(join(projectRoot, 'src', spec.slice(2)));
|
|
39
|
+
}
|
|
40
|
+
return styles;
|
|
41
|
+
}
|
|
@@ -1,27 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* 职责:IR 节点树 → 自包含 SwiftUI 源码字符串(可直接 swiftc -parse / 进 Xcode 工程)
|
|
5
|
-
*
|
|
6
|
-
* 遵循 RULES.md:
|
|
7
|
-
* - R14:生成器产出必须可直接过 swiftc 编译
|
|
8
|
-
* - R16:fill → .frame(maxWidth/maxHeight:.infinity);禁绝对像素(固定物除外)
|
|
9
|
-
* - R26:颜色直接生成 Color(red:green:blue:opacity:) 数值表达式
|
|
10
|
-
*
|
|
11
|
-
* 映射表(R25 薄抽象层):
|
|
12
|
-
* root → ZStack + 全屏 frame + 背景
|
|
13
|
-
* group → VStack / HStack(flexDirection)+ spacing(gap) + padding + 背景/圆角/描边
|
|
14
|
-
* text → Text + font/foregroundColor/multilineTextAlignment/lineSpacing
|
|
15
|
-
* button → Button(label:) + 尺寸/背景/圆角
|
|
16
|
-
* scroll → ScrollView(.vertical) + VStack(flex:1 → maxHeight:.infinity)
|
|
17
|
-
* spacer → Spacer(minLength: 0)
|
|
18
|
-
* divider → Rectangle().fill().frame(height:)
|
|
19
|
-
* image → 占位 RoundedRectangle(P0 无真实图片链路)
|
|
20
|
-
*
|
|
21
|
-
* P0 简化声明:justifyContent 仅 start/center/spaceBetween;breakpoints 未实现;
|
|
22
|
-
* flex 仅支持容器主轴方向的 maxHeight/maxWidth:.infinity。
|
|
23
|
-
*/
|
|
24
|
-
import type { IrAction, IrNode, IrScreen } from '../ir/schema.js';
|
|
1
|
+
import type { IrAction, IrNode, IrProps, IrScreen } from '../ir/schema.js';
|
|
2
|
+
export declare function colorExpr(c: string): string;
|
|
25
3
|
/** R42 收集 IR 树中的颜色与字体组合(生成 Theme.swift 的输入) */
|
|
26
4
|
export declare function collectTheme(...nodes: (IrNode | undefined)[]): {
|
|
27
5
|
colors: Set<string>;
|
|
@@ -36,13 +14,14 @@ export declare function buildTheme(tokens: {
|
|
|
36
14
|
fontNames: Map<string, string>;
|
|
37
15
|
};
|
|
38
16
|
/** R42 生成 Theme.swift(颜色/字体去重常量) */
|
|
39
|
-
export declare const VRN_HELPERS_SWIFT
|
|
17
|
+
export declare const VRN_HELPERS_SWIFT: string;
|
|
40
18
|
export declare function generateThemeSource(palette: {
|
|
41
19
|
colorNames: Map<string, string>;
|
|
42
20
|
fontNames: Map<string, string>;
|
|
43
21
|
}): string;
|
|
44
22
|
/** R43/R44 View 扩展源码(Theme.swift 内;theme 模式引用) */
|
|
45
23
|
export declare function generateViewExtensions(): string;
|
|
24
|
+
export declare function boxModifiers(props: IrProps): string[];
|
|
46
25
|
export declare function generateScreenSource(screen: IrScreen, opts?: {
|
|
47
26
|
warnings?: string[];
|
|
48
27
|
theme?: {
|