@aiot-toolkit/aiotpack 2.0.6-beta.9 → 2.1.0-prender.2

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.
Files changed (31) hide show
  1. package/lib/afterCompile/ux/UxAfterCompile.js +25 -2
  2. package/lib/compiler/javascript/JavascriptCompiler.js +11 -4
  3. package/lib/compiler/javascript/TemplateCompiler.d.ts +29 -0
  4. package/lib/compiler/javascript/TemplateCompiler.js +564 -0
  5. package/lib/compiler/javascript/ViteCompiler.d.ts +13 -0
  6. package/lib/compiler/javascript/ViteCompiler.js +449 -0
  7. package/lib/compiler/javascript/interface/IJavascriptCompileOption.d.ts +9 -0
  8. package/lib/compiler/javascript/vela/VelaWebpackConfigurator.d.ts +3 -1
  9. package/lib/compiler/javascript/vela/VelaWebpackConfigurator.js +16 -1
  10. package/lib/compiler/javascript/vela/interface/IManifest.d.ts +12 -0
  11. package/lib/compiler/javascript/vela/plugin/WrapPlugin.d.ts +10 -1
  12. package/lib/compiler/javascript/vela/plugin/WrapPlugin.js +241 -57
  13. package/lib/compiler/javascript/vela/utils/UxCompileUtil.d.ts +3 -2
  14. package/lib/compiler/javascript/vela/utils/UxCompileUtil.js +12 -4
  15. package/lib/compiler/javascript/vela/utils/VruUtil.d.ts +50 -0
  16. package/lib/compiler/javascript/vela/utils/VruUtil.js +128 -0
  17. package/lib/compiler/javascript/vela/utils/ZipUtil.d.ts +9 -0
  18. package/lib/compiler/javascript/vela/utils/ZipUtil.js +112 -6
  19. package/lib/compiler/javascript/vela/utils/webpackLoader/WebpackJsLoader.js +1 -1
  20. package/lib/config/UxConfig.d.ts +12 -5
  21. package/lib/config/UxConfig.js +4 -6
  22. package/lib/loader/ux/JsLoader.d.ts +7 -0
  23. package/lib/loader/ux/JsLoader.js +38 -8
  24. package/lib/loader/ux/vela/HmlLoader.d.ts +6 -6
  25. package/lib/loader/ux/vela/HmlLoader.js +30 -13
  26. package/lib/utils/BeforeCompileUtils.d.ts +1 -1
  27. package/lib/utils/BeforeCompileUtils.js +52 -9
  28. package/lib/utils/ux/ManifestSchema.js +0 -1
  29. package/lib/utils/ux/UxFileUtils.js +1 -1
  30. package/lib/utils/ux/UxLoaderUtils.js +8 -3
  31. package/package.json +9 -6
@@ -9,7 +9,7 @@ var _fsExtra = _interopRequireDefault(require("fs-extra"));
9
9
  var _lodash = _interopRequireDefault(require("lodash"));
10
10
  var _path = _interopRequireDefault(require("path"));
11
11
  var _CompileMode = _interopRequireDefault(require("../../compiler/enum/CompileMode"));
12
- var _JavascriptCompiler = _interopRequireDefault(require("../../compiler/javascript/JavascriptCompiler"));
12
+ var _ViteCompiler = _interopRequireDefault(require("../../compiler/javascript/ViteCompiler"));
13
13
  var _JavascriptDefaultCompileOption = _interopRequireDefault(require("../../compiler/javascript/JavascriptDefaultCompileOption"));
14
14
  var _Jsc = _interopRequireDefault(require("../../compiler/javascript/vela/utils/Jsc"));
15
15
  var _ZipUtil = _interopRequireDefault(require("../../compiler/javascript/vela/utils/ZipUtil"));
@@ -18,6 +18,7 @@ var _FileLaneTriggerType = _interopRequireDefault(require("file-lane/lib/enum/Fi
18
18
  var _ZipFileUtil = _interopRequireDefault(require("@aiot-toolkit/shared-utils/lib/utils/ZipFileUtil"));
19
19
  var _ImageIcu = _interopRequireDefault(require("../../compiler/tools/icu/ImageIcu"));
20
20
  var _LiteCard = _interopRequireDefault(require("../../compiler/javascript/vela/utils/LiteCard"));
21
+ var _UxUtil = _interopRequireDefault(require("@aiot-toolkit/parser/lib/ux/utils/UxUtil"));
21
22
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
22
23
  const BinaryPlugin = require('@aiot-toolkit/parser/lib/ux/translate/vela/protobuf/BinaryPlugin');
23
24
 
@@ -31,7 +32,7 @@ class UxAfterCompile {
31
32
  compilerOption,
32
33
  onLog
33
34
  } = params;
34
- return new _JavascriptCompiler.default(context, onLog).compile({
35
+ return new _ViteCompiler.default(context, onLog).compile({
35
36
  projectPath: _path.default.join(context.projectPath, context.output),
36
37
  mode: _CompileMode.default.DEVELOPMENT,
37
38
  devtool: false,
@@ -239,6 +240,28 @@ class UxAfterCompile {
239
240
  });
240
241
  }
241
242
  }
243
+
244
+ // e2e
245
+ if (compilerOption.e2eConfigPath && compilerOption.enableE2e) {
246
+ const testConfig = _UxUtil.default.getE2eConfig({
247
+ projectPath: compilerOption.projectPath,
248
+ e2eConfigPath: compilerOption.e2eConfigPath
249
+ });
250
+ const testPagePath = _path.default.parse(_path.default.relative(_path.default.join(compilerOption.projectPath, sourceRoot), _path.default.join(testConfig.dir, testConfig.entry.path)).replaceAll(_path.default.sep, _path.default.posix.sep));
251
+ // 1. 修改entry
252
+ const entryName = testPagePath.dir;
253
+ const launchMode = testConfig.entry?.launchMode;
254
+ if (entryName) {
255
+ content.router.entry = entryName;
256
+ }
257
+ // 2. 添加测试页到router.pages
258
+ if (content.router.pages) {
259
+ content.router.pages[entryName] = {
260
+ component: testPagePath.name,
261
+ launchMode
262
+ };
263
+ }
264
+ }
242
265
  _fsExtra.default.writeJSONSync(_path.default.join(projectPath, outputPath, _UxFileUtils.default.CONFIG_FILE_NAME), content, {
243
266
  spaces: 2
244
267
  });
@@ -135,12 +135,16 @@ class JavascriptCompiler {
135
135
  let result = {
136
136
  context: projectPath,
137
137
  mode,
138
- devtool: devtool,
138
+ devtool: devtool || false,
139
139
  output: {
140
- globalObject: 'window',
140
+ globalObject: 'global',
141
141
  filename: '[name].js',
142
142
  publicPath: './',
143
- path: buildPath
143
+ path: buildPath,
144
+ iife: false,
145
+ library: {
146
+ type: 'commonjs2'
147
+ }
144
148
  },
145
149
  module: {
146
150
  rules: [{
@@ -178,7 +182,10 @@ class JavascriptCompiler {
178
182
  },
179
183
  optimization: {
180
184
  minimize: true,
181
- minimizer: [this.createMinimizerRspackPlugin(param)]
185
+ minimizer: [this.createMinimizerRspackPlugin(param)],
186
+ splitChunks: false,
187
+ runtimeChunk: false,
188
+ concatenateModules: true
182
189
  },
183
190
  resolve: {
184
191
  extensions: ['.js', '.ts', '.ux']
@@ -0,0 +1,29 @@
1
+ declare function uxToTemplateJson(templateNode: any, styleObjectId?: number, styleSheet?: any, importMap?: Record<string, string>, pageDir?: string): any;
2
+ /**
3
+ * Normalize a media query condition text from CSS Level 4 syntax to Level 3.
4
+ *
5
+ * Level 4 → Level 3:
6
+ * `(width <= 30)` → `(max-width: 30)`
7
+ * `(width >= 80)` → `(min-width: 80)`
8
+ * `(400 <= width <= 700)` → `(min-width: 400) and (max-width: 700)`
9
+ * `(width < 30)` → `(max-width: 29)` // strict → off-by-one
10
+ * Same for height/aspect-ratio. Other features pass through.
11
+ *
12
+ * `screen and ` is preserved; `not` / `only` are preserved as-is.
13
+ */
14
+ declare function normalizeMediaQuery(condition: string): string;
15
+ /**
16
+ * Parse compiled $app_style$ array. Each rule is either:
17
+ * [selectors, props] — base rule
18
+ * [{condition: "<media-query>"}, selectors, props] — conditional rule (@media or @import with query)
19
+ *
20
+ * Returns a flat map: selectors → props for base rules, plus
21
+ * `"@media <normalizedConditionText>"` keys whose values are the per-condition
22
+ * `{selector: props}` map. The condition text is normalized to Level 3 so
23
+ * runtime only needs a Level-3 matcher.
24
+ */
25
+ declare function parseStyleArray(code: string): Record<string, any>;
26
+ /** Convert CSS text to css.json format */
27
+ export { uxToTemplateJson, parseStyleArray, normalizeMediaQuery };
28
+ /** Compute styleObjectId hash from a string (same algorithm as blueos-pack) */
29
+ export declare function getStyleObjectId(name: string): number;
@@ -0,0 +1,564 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.getStyleObjectId = getStyleObjectId;
7
+ exports.normalizeMediaQuery = normalizeMediaQuery;
8
+ exports.parseStyleArray = parseStyleArray;
9
+ exports.uxToTemplateJson = uxToTemplateJson;
10
+ var acorn = _interopRequireWildcard(require("acorn"));
11
+ var _astring = require("astring");
12
+ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
13
+ function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
14
+ // Template.json compilation from parse5 AST
15
+
16
+ function uxToTemplateJson(templateNode, styleObjectId, styleSheet, importMap, pageDir) {
17
+ const children = templateNode.content?.childNodes || templateNode.childNodes || [];
18
+ const rootEl = children.find(n => n.tagName && n.nodeName !== '#text');
19
+ if (!rootEl) return {};
20
+ const result = elementToJson(rootEl, styleSheet, importMap, undefined, pageDir);
21
+ // Insert styleObjectId before children (vivo format)
22
+ if (styleObjectId !== undefined) {
23
+ const ordered = {};
24
+ for (const [k, v] of Object.entries(result)) {
25
+ if (k === 'children') {
26
+ ordered.styleObjectId = styleObjectId;
27
+ }
28
+ ordered[k] = v;
29
+ }
30
+ if (!ordered.styleObjectId) ordered.styleObjectId = styleObjectId;
31
+ return ordered;
32
+ }
33
+ return result;
34
+ }
35
+ /** Add this. prefix to variable references in an expression */
36
+ // blueos-pack's pathTestRE: matches simple paths like a.b.c, a['key'], a[0], a[$idx]
37
+ const pathTestRE = /^[A-Za-z_$][\w$]*(?:\.[\w$]+|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/;
38
+ /**
39
+ * Normalize a media query condition text from CSS Level 4 syntax to Level 3.
40
+ *
41
+ * Level 4 → Level 3:
42
+ * `(width <= 30)` → `(max-width: 30)`
43
+ * `(width >= 80)` → `(min-width: 80)`
44
+ * `(400 <= width <= 700)` → `(min-width: 400) and (max-width: 700)`
45
+ * `(width < 30)` → `(max-width: 29)` // strict → off-by-one
46
+ * Same for height/aspect-ratio. Other features pass through.
47
+ *
48
+ * `screen and ` is preserved; `not` / `only` are preserved as-is.
49
+ */
50
+ function normalizeMediaQuery(condition) {
51
+ if (!condition) return condition;
52
+ // Only operate on what's inside parens; leave operators (and/or/not/only) alone.
53
+ return condition.replace(/\(([^)]+)\)/g, (_match, inner) => {
54
+ const expr = String(inner).trim();
55
+ // Range form: <num> <op1> <feature> <op2> <num>, e.g. "400 <= width <= 700"
56
+ const range = expr.match(/^(-?\d+(?:\.\d+)?)\s*(<=?|>=?)\s*([a-zA-Z-]+)\s*(<=?|>=?)\s*(-?\d+(?:\.\d+)?)$/);
57
+ if (range) {
58
+ const [, lhs, op1, feat, op2, rhs] = range;
59
+ const lo = op1 === '<=' || op1 === '<' ? lhs : rhs;
60
+ const hi = op1 === '<=' || op1 === '<' ? rhs : lhs;
61
+ // Both operators must be the same direction; otherwise fall through.
62
+ if ((op1 === '<=' || op1 === '<') && (op2 === '<=' || op2 === '<') || (op1 === '>=' || op1 === '>') && (op2 === '>=' || op2 === '>')) {
63
+ const loNum = op1 === '<' || op2 === '<' ? +lo + 1 : +lo;
64
+ const hiNum = op1 === '<' || op2 === '<' ? +hi - 1 : +hi;
65
+ return `(min-${feat}: ${loNum}) and (max-${feat}: ${hiNum})`;
66
+ }
67
+ }
68
+ // Comparison form: <feature> <op> <num> or <num> <op> <feature>
69
+ const cmp = expr.match(/^(?:([a-zA-Z-]+)\s*(<=?|>=?)\s*(-?\d+(?:\.\d+)?)|(-?\d+(?:\.\d+)?)\s*(<=?|>=?)\s*([a-zA-Z-]+))$/);
70
+ if (cmp) {
71
+ let feat, op, num;
72
+ if (cmp[1]) {
73
+ feat = cmp[1];
74
+ op = cmp[2];
75
+ num = +cmp[3];
76
+ } else {
77
+ feat = cmp[6];
78
+ op = cmp[5];
79
+ num = +cmp[4];
80
+ // Flip operator since we're moving feature to the left
81
+ op = op === '<=' ? '>=' : op === '>=' ? '<=' : op === '<' ? '>' : '<';
82
+ }
83
+ // Strict comparisons get off-by-one (CSS spec: (width < 30) means width is at most 29 in integer dp)
84
+ if (op === '<') return `(max-${feat}: ${num - 1})`;
85
+ if (op === '>') return `(min-${feat}: ${num + 1})`;
86
+ if (op === '<=') return `(max-${feat}: ${num})`;
87
+ if (op === '>=') return `(min-${feat}: ${num})`;
88
+ }
89
+ return `(${expr})`;
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Parse compiled $app_style$ array. Each rule is either:
95
+ * [selectors, props] — base rule
96
+ * [{condition: "<media-query>"}, selectors, props] — conditional rule (@media or @import with query)
97
+ *
98
+ * Returns a flat map: selectors → props for base rules, plus
99
+ * `"@media <normalizedConditionText>"` keys whose values are the per-condition
100
+ * `{selector: props}` map. The condition text is normalized to Level 3 so
101
+ * runtime only needs a Level-3 matcher.
102
+ */
103
+ function parseStyleArray(code) {
104
+ const styleSheet = {};
105
+ const ingest = parsed => {
106
+ if (!Array.isArray(parsed)) return;
107
+ for (const rule of parsed) {
108
+ if (!Array.isArray(rule) || rule.length < 2) continue;
109
+ let selectors, props, conditionText;
110
+ if (rule.length >= 3 && rule[0] && typeof rule[0] === 'object' && !Array.isArray(rule[0]) && typeof rule[0].condition === 'string') {
111
+ conditionText = rule[0].condition;
112
+ selectors = rule[1];
113
+ props = rule[2];
114
+ } else {
115
+ selectors = rule[0];
116
+ props = rule[1];
117
+ }
118
+ if (!Array.isArray(selectors)) continue;
119
+ let target = styleSheet;
120
+ if (conditionText) {
121
+ const key = `@media ${normalizeMediaQuery(conditionText)}`;
122
+ target = styleSheet[key] = styleSheet[key] || {};
123
+ }
124
+ for (const sel of selectors) {
125
+ if (Array.isArray(sel) && sel.length >= 2) {
126
+ target[sel[0] === 0 ? `.${sel[1]}` : sel[1]] = props;
127
+ }
128
+ }
129
+ }
130
+ };
131
+ try {
132
+ const jsonLike = code.replace(/'/g, '"').replace(/([{,]\s*)([a-zA-Z_$][\w$]*)\s*:/g, '$1"$2":').replace(/,\s*([}\]])/g, '$1');
133
+ ingest(JSON.parse(jsonLike));
134
+ } catch {
135
+ try {
136
+ ingest(new Function(`return ${code}`)());
137
+ } catch {}
138
+ }
139
+ return styleSheet;
140
+ }
141
+ function addThisPrefix(expr, extraReserved) {
142
+ const reserved = new Set(['true', 'false', 'null', 'undefined', 'NaN', 'Infinity', 'typeof', 'instanceof', 'new', 'delete', 'void', 'in', 'of', 'if', 'else', 'return', 'this', 'function', 'var', 'let', 'const', '$event', 'evt', 'event', 'Math', 'JSON', 'Object', 'Array', 'String', 'Number', 'Boolean', 'Date', 'RegExp', 'Error', 'Promise', 'parseInt', 'parseFloat', 'console']);
143
+ try {
144
+ const generate = _astring.generate;
145
+ const ast = acorn.parseExpressionAt(expr, 0, {
146
+ ecmaVersion: 2020
147
+ });
148
+ // Walk AST and add this. prefix to free identifiers
149
+ function walk(node, parent) {
150
+ if (!node || typeof node !== 'object') return;
151
+ if (node.type === 'Identifier' && !reserved.has(node.name) && !extraReserved?.has(node.name)) {
152
+ if (parent?.type === 'MemberExpression' && parent.property === node && !parent.computed) return;
153
+ if (parent?.type === 'Property' && parent.key === node && !parent.computed) return;
154
+ const origName = node.name;
155
+ node.type = 'MemberExpression';
156
+ node.object = {
157
+ type: 'ThisExpression'
158
+ };
159
+ node.property = {
160
+ type: 'Identifier',
161
+ name: origName
162
+ };
163
+ node.computed = false;
164
+ delete node.name;
165
+ }
166
+ for (const key of Object.keys(node)) {
167
+ if (key === 'type' || key === 'start' || key === 'end') continue;
168
+ const child = node[key];
169
+ if (Array.isArray(child)) child.forEach(c => c?.type && walk(c, node));else if (child?.type) walk(child, node);
170
+ }
171
+ }
172
+ walk(ast, null);
173
+ return generate(ast, {
174
+ indent: '',
175
+ lineEnd: ' '
176
+ }).trim();
177
+ } catch {
178
+ // Fallback to regex for unparseable expressions
179
+ const parts = expr.split(/('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")/);
180
+ return parts.map((part, i) => {
181
+ if (i % 2 === 1) return part;
182
+ return part.replace(/(?<![.'"\w$/])([a-zA-Z_$][a-zA-Z0-9_$]*)/g, (match, id) => {
183
+ if (reserved.has(id) || extraReserved?.has(id)) return match;
184
+ return `this.${id}`;
185
+ });
186
+ }).join('');
187
+ }
188
+ }
189
+ function elementToJson(el, styleSheet, importMap, scopeVars, pageDir) {
190
+ const tagName = el.tagName === 'img' ? 'image' : el.tagName;
191
+ const node = {
192
+ type: tagName
193
+ };
194
+ const attrs = el.attrs || [];
195
+ const attrObj = {};
196
+ const events = {};
197
+ let className = '';
198
+ let hasBind = false;
199
+ // Static inline style values (from style="key: val") are merged into node.style alongside
200
+ // tag/class selector styles to match vivo's behavior — only dynamic values stay in inlineStyle
201
+ const staticInlineStyle = {};
202
+ // Check if this is an imported sub-component
203
+ const isComponent = importMap && importMap[el.tagName];
204
+ for (const attr of attrs) {
205
+ let {
206
+ name,
207
+ value
208
+ } = attr;
209
+ // :attr shorthand for dynamic binding
210
+ if (name.startsWith(':') && !name.startsWith('::')) {
211
+ name = name.slice(1);
212
+ if (!value.includes('{{')) value = `{{${value}}}`;
213
+ }
214
+ if (name === 'is' && tagName === 'component') {
215
+ continue; // is attribute handled at runtime
216
+ } else if (name === 'class') {
217
+ className = value.trim();
218
+ } else if (name.startsWith('@') || name.startsWith('on') && name.length > 2) {
219
+ const rawEvtName = name.startsWith('@') ? name.slice(1) : name.replace(/^on/, '');
220
+ const evtName = rawEvtName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
221
+ // Events with parameters use $event format with function wrapper
222
+ const evtVal = value.trim();
223
+ if (evtVal.includes('(')) {
224
+ // Add evt as last parameter
225
+ const withEvt = evtVal.replace(/\)$/, evtVal.includes('()') ? 'evt)' : ', evt)');
226
+ events[`$${evtName}`] = `function (evt) { return ${addThisPrefix(withEvt)}; }`;
227
+ } else {
228
+ events[evtName] = evtVal;
229
+ }
230
+ hasBind = true;
231
+ } else if (name === 'if') {
232
+ const ifVal = value.replace(/\{\{|\}\}/g, '').trim();
233
+ if (ifVal === 'true') {
234
+ node['shown'] = true;
235
+ } else if (ifVal === 'false') {
236
+ node['shown'] = false;
237
+ } else {
238
+ node['__cond__'] = {
239
+ kind: 'if',
240
+ expr: ifVal
241
+ };
242
+ hasBind = true;
243
+ }
244
+ } else if (name === 'elif') {
245
+ const elifVal = value.replace(/\{\{|\}\}/g, '').trim();
246
+ node['__cond__'] = {
247
+ kind: 'elif',
248
+ expr: elifVal
249
+ };
250
+ hasBind = true;
251
+ } else if (name === 'else') {
252
+ node['__cond__'] = {
253
+ kind: 'else'
254
+ };
255
+ hasBind = true;
256
+ } else if (name === 'show') {
257
+ const showVal = value.replace(/\{\{|\}\}/g, '').trim();
258
+ if (showVal === 'true') attrObj['show'] = true;else if (showVal === 'false') attrObj['show'] = false;else {
259
+ // Try to evaluate as literal
260
+ try {
261
+ const v = JSON.parse(showVal);
262
+ attrObj["show"] = v;
263
+ } catch {
264
+ attrObj['$show'] = showVal;
265
+ hasBind = true;
266
+ }
267
+ }
268
+ } else if (name === 'for') {
269
+ const forVal = value.replace(/\{\{|\}\}/g, '').trim();
270
+ const inMatch = forVal.match(/^\((\w+)\s*,\s*(\w+)\)\s+in\s+(.+)$/);
271
+ const simpleInMatch = !inMatch && forVal.match(/^(\w+)\s+in\s+(.+)$/);
272
+ if (inMatch) {
273
+ // for="(key, value) in expr" syntax
274
+ const expr = inMatch[3].trim();
275
+ if (/^[a-zA-Z_$][\w$.]*$/.test(expr)) {
276
+ node['repeat'] = {
277
+ '$exp': expr,
278
+ key: inMatch[1],
279
+ value: inMatch[2]
280
+ };
281
+ } else {
282
+ node['$repeat'] = `function () { return ${addThisPrefix(expr)}; }`;
283
+ node['repeat'] = [];
284
+ }
285
+ } else if (simpleInMatch) {
286
+ // for="value in expr" syntax
287
+ const expr = simpleInMatch[2].trim();
288
+ if (/^[a-zA-Z_$][\w$.]*$/.test(expr)) {
289
+ node['repeat'] = {
290
+ '$exp': expr,
291
+ value: simpleInMatch[1]
292
+ };
293
+ } else {
294
+ node['$repeat'] = `function () { return ${addThisPrefix(expr)}; }`;
295
+ node['repeat'] = [];
296
+ }
297
+ } else if (pathTestRE.test(forVal)) {
298
+ node['$repeat'] = forVal;
299
+ node['repeat'] = [];
300
+ } else {
301
+ node['$repeat'] = `function () { return ${addThisPrefix(forVal)}; }`;
302
+ node['repeat'] = [];
303
+ }
304
+ hasBind = true;
305
+ // For-loop variables should NOT get this. prefix
306
+ if (inMatch) {
307
+ scopeVars = new Set(scopeVars);
308
+ scopeVars.add(inMatch[1]); // key
309
+ scopeVars.add(inMatch[2]); // value
310
+ } else if (simpleInMatch) {
311
+ scopeVars = new Set(scopeVars);
312
+ scopeVars.add(simpleInMatch[1]); // value
313
+ }
314
+ } else if (name === 'style') {
315
+ if (/^\{\{[^}]+\}\}$/.test(value.trim())) {
316
+ // Pure dynamic style binding: style="{{variable}}"
317
+ const inner = value.replace(/\{\{|\}\}/g, '').trim();
318
+ if (/^[a-zA-Z_$][\w$.]*$/.test(inner)) {
319
+ node['$style'] = inner;
320
+ } else {
321
+ node['$style'] = `function () { return ${addThisPrefix(inner)}; }`;
322
+ }
323
+ hasBind = true;
324
+ } else {
325
+ // Static or per-property dynamic inline style
326
+ // Static values get merged into node.style (alongside tag/class selectors)
327
+ // Dynamic values ($key: function) stay in inlineStyle
328
+ // Only set hasBind if there are dynamic values
329
+ const inlineStyle = {};
330
+ let hasDynamicInlineStyle = false;
331
+ value.split(';').forEach(decl => {
332
+ const [prop, ...vals] = decl.split(':');
333
+ if (prop && vals.length) {
334
+ const key = prop.trim().replace(/-([a-z])/g, (_, c) => c.toUpperCase());
335
+ const propVal = vals.join(':').trim();
336
+ if (/\{\{[\s\S]*?\}\}/.test(propVal)) {
337
+ // Dynamic value — emit as $key: function() {...}
338
+ const parts = propVal.split(/(\{\{[\s\S]*?\}\})/);
339
+ const exprs = parts.filter(Boolean).map(p => {
340
+ const m = p.match(/^\{\{([\s\S]*?)\}\}$/);
341
+ if (m) return addThisPrefix(m[1].trim());
342
+ return `'${p.replace(/'/g, "\\'")}'`;
343
+ });
344
+ const fullExpr = exprs.length === 1 ? exprs[0] : `'' + ${exprs.join(' + ')}`;
345
+ inlineStyle['$' + key] = `function () { return ${fullExpr}; }`;
346
+ hasDynamicInlineStyle = true;
347
+ } else {
348
+ // Static value — accumulate to staticInlineStyle for merging into node.style later
349
+ staticInlineStyle[key] = propVal;
350
+ }
351
+ }
352
+ });
353
+ if (Object.keys(inlineStyle).length) node['inlineStyle'] = inlineStyle;
354
+ if (hasDynamicInlineStyle) hasBind = true;
355
+ }
356
+ } else if (/\{\{.*\}\}/.test(value)) {
357
+ // Dynamic binding - build expression from mixed static/dynamic parts
358
+ const parts = value.split(/(\{\{[\s\S]*?\}\})/);
359
+ const exprs = parts.filter(Boolean).map(p => {
360
+ const m = p.match(/^\{\{([\s\S]*?)\}\}$/);
361
+ if (m) return addThisPrefix(m[1].trim());
362
+ return `'${p.replace(/'/g, "\\'")}'`;
363
+ });
364
+ const camelName = '$' + name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
365
+ const plainName = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
366
+ // Handle literal values (null, true, false, numbers)
367
+ if (exprs.length === 1) {
368
+ const raw = exprs[0].replace(/^this\./, '');
369
+ if (raw === 'null') {
370
+ attrObj[plainName] = null;
371
+ hasBind = true;
372
+ } else if (raw === 'undefined') {
373
+ attrObj[plainName] = undefined;
374
+ hasBind = true;
375
+ } else if (raw === 'true') {
376
+ attrObj[plainName] = true;
377
+ hasBind = true;
378
+ } else if (raw === 'false') {
379
+ attrObj[plainName] = false;
380
+ hasBind = true;
381
+ } else if (pathTestRE.test(raw)) {
382
+ // Simple variable reference - just the name without this. prefix
383
+ attrObj[camelName] = raw;
384
+ } else {
385
+ attrObj[camelName] = `function () { return ${exprs[0]}; }`;
386
+ }
387
+ } else {
388
+ const wrappedExprs = exprs.map(e => {
389
+ if (e.startsWith("'") || e.startsWith('"')) return e; // string literal
390
+ if (/(\|\||&&|\?\s|\+)/.test(e) && !e.startsWith('(')) return `(${e})`;
391
+ return e;
392
+ });
393
+ const fullExpr = `'' + ${wrappedExprs.join(' + ')}`;
394
+ attrObj[camelName] = `function () { return ${fullExpr}; }`;
395
+ }
396
+ hasBind = true;
397
+ } else {
398
+ let staticVal = value ? value.replace(/ {2,}/g, ' ') : '';
399
+ // Normalize relative path attrs (src, href) to absolute paths from src root
400
+ if (staticVal && (name === 'src' || name === 'href') && !staticVal.startsWith('/') && !/^[a-z]+:/.test(staticVal) && pageDir) {
401
+ // Resolve relative to pageDir, then make absolute from src root
402
+ const segments = pageDir.split('/').filter(Boolean).concat(staticVal.split('/'));
403
+ const stack = [];
404
+ for (const seg of segments) {
405
+ if (seg === '..') stack.pop();else if (seg && seg !== '.') stack.push(seg);
406
+ }
407
+ staticVal = '/' + stack.join('/');
408
+ }
409
+ attrObj[name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = staticVal;
410
+ if (name === 'id') hasBind = true;
411
+ }
412
+ }
413
+ // Handle text content
414
+ for (const child of el.childNodes || []) {
415
+ const textElements = new Set(['text', 'span', 'a', 'input', 'qrcode', 'barcode', 'marquee', 'arc-text', 'option', 'button', 'label', 'textarea', 'richtext']);
416
+ if (child.value?.trim() && !child.tagName && !(el.childNodes || []).some(c => c.tagName) && textElements.has(tagName)) {
417
+ // Collapse whitespace for static text; dynamic text is trimmed separately
418
+ const text = child.value;
419
+ if (/\{\{.*\}\}/.test(text)) {
420
+ // Build function expression from template literal
421
+ // Trim surrounding whitespace before splitting (vivo ignores indentation whitespace)
422
+ const trimmedText = text.trim();
423
+ const parts = trimmedText.split(/(\{\{[\s\S]*?\}\})/);
424
+ const exprs = parts.filter(Boolean).map(p => {
425
+ const m = p.match(/^\{\{([\s\S]*?)\}\}$/);
426
+ if (m) return addThisPrefix(m[1].trim());
427
+ return `'${p.replace(/'/g, "\\'")}'`;
428
+ });
429
+ if (exprs.length === 1 && pathTestRE.test(exprs[0].replace(/^this\./, ''))) {
430
+ attrObj['$value'] = exprs[0].replace(/^this\./, '');
431
+ } else if (exprs.length === 1) {
432
+ attrObj['$value'] = `function () { return ${exprs[0]}; }`;
433
+ } else {
434
+ const wrappedExprs = exprs.map(e => {
435
+ if (e.startsWith("'") || e.startsWith('"')) return e;
436
+ if (/(\|\||&&|\?\s|\+)/.test(e) && !e.startsWith('(')) return `(${e})`;
437
+ return e;
438
+ });
439
+ attrObj['$value'] = `function () { return '' + ${wrappedExprs.join(' + ')}; }`;
440
+ }
441
+ hasBind = true;
442
+ } else {
443
+ attrObj['value'] = text.replace(/\s*\n\s*/g, ' ');
444
+ }
445
+ }
446
+ }
447
+ // Output in vivo order: attr → bind → class → events → import → style → children
448
+ if (Object.keys(attrObj).length) node.attr = attrObj;
449
+ // Handle dynamic class before bind assignment
450
+ let hasDynClass = false;
451
+ if (className && /\{\{.*\}\}/.test(className)) {
452
+ const parts = className.split(/(\{\{[\s\S]*?\}\})/).filter(Boolean);
453
+ const arrayParts = parts.map(p => {
454
+ const m = p.match(/^\{\{([\s\S]*?)\}\}$/);
455
+ if (m) return addThisPrefix(m[1].trim());
456
+ return p.trim().split(/\s+/).map(c => `'${c}'`).join(', ');
457
+ });
458
+ // Single dynamic variable → just the name; otherwise array format
459
+ if (arrayParts.length === 1 && pathTestRE.test(arrayParts[0].replace(/^this\./, ''))) {
460
+ node['$class'] = arrayParts[0].replace(/^this\./, '');
461
+ } else {
462
+ node['$class'] = `function () { return [${arrayParts.join(', ')}]; }`;
463
+ }
464
+ hasBind = true;
465
+ hasDynClass = true;
466
+ }
467
+ if (isComponent) {
468
+ node.bind = 3;
469
+ node['import'] = importMap[el.tagName];
470
+ } else if (node['$shown'] !== undefined || node['$repeat'] || node['repeat']?.['$exp']) {
471
+ node.bind = 2;
472
+ } else if (hasBind) {
473
+ node.bind = 1;
474
+ }
475
+ if (!hasDynClass && className) {
476
+ node.class = className;
477
+ }
478
+ if (Object.keys(events).length) node.events = events;
479
+ // Inline style from styleSheet (class selector + tag selector) + static portion of style="..."
480
+ // Only base (non-@media) selectors are inlined; @media rules are applied at runtime by SDK.
481
+ if (styleSheet) {
482
+ const merged = {};
483
+ if (styleSheet[el.tagName]) Object.assign(merged, styleSheet[el.tagName]);
484
+ if (className && !hasDynClass) {
485
+ for (const cls of className.split(/\s+/)) {
486
+ if (cls && styleSheet[`.${cls}`]) Object.assign(merged, styleSheet[`.${cls}`]);
487
+ }
488
+ }
489
+ Object.assign(merged, staticInlineStyle);
490
+ if (Object.keys(merged).length) node.style = merged;
491
+ } else if (Object.keys(staticInlineStyle).length) {
492
+ node.style = {
493
+ ...staticInlineStyle
494
+ };
495
+ }
496
+ // Process children (skip text nodes already handled above)
497
+ const childNodes = [];
498
+ const hasElementChildren = (el.childNodes || []).some(c => c.tagName);
499
+ const hasTextChildren = (el.childNodes || []).some(c => !c.tagName && c.value?.trim());
500
+ const isMixedContent = hasElementChildren && hasTextChildren && (tagName === 'text' || tagName === 'span' || tagName === 'a' || tagName === 'marquee');
501
+ for (const child of el.childNodes || []) {
502
+ if (child.tagName) {
503
+ childNodes.push(elementToJson(child, styleSheet, importMap, scopeVars, pageDir));
504
+ } else if (isMixedContent && child.value?.trim()) {
505
+ // Wrap text nodes in <span> for mixed content
506
+ const text = child.value.replace(/[ \t]*\n[ \t]*/g, ' ').replace(/ {2,}/g, ' ');
507
+ const spanNode = {
508
+ type: 'span',
509
+ attr: {
510
+ value: text
511
+ }
512
+ };
513
+ childNodes.push(spanNode);
514
+ }
515
+ }
516
+ // Resolve if/elif/else chain — emit $shown with composite negations.
517
+ // if → $shown = self
518
+ // elif → $shown = self && !prev1 && !prev2 ...
519
+ // else → $shown = !prev1 && !prev2 ...
520
+ // Each chain participant gets bind=2 (virtual fragment); else terminates the chain.
521
+ const negate = e => /^this\.[\w$.[\]'"]+$/.test(e) ? `!${e}` : `!(${e})`;
522
+ let prevConds = [];
523
+ for (const child of childNodes) {
524
+ const cond = child['__cond__'];
525
+ if (!cond) {
526
+ prevConds = [];
527
+ continue;
528
+ }
529
+ delete child['__cond__'];
530
+ if (cond.kind === 'if') {
531
+ const expr = cond.expr;
532
+ child['$shown'] = pathTestRE.test(expr) ? expr : `function () { return ${addThisPrefix(expr)}; }`;
533
+ child['shown'] = false;
534
+ child['bind'] = 2;
535
+ prevConds = [addThisPrefix(expr)];
536
+ } else if (cond.kind === 'elif') {
537
+ const selfExpr = addThisPrefix(cond.expr);
538
+ const negs = prevConds.map(negate).join(' && ');
539
+ child['$shown'] = `function () { return ${selfExpr}${negs ? ' && ' + negs : ''}; }`;
540
+ child['shown'] = false;
541
+ child['bind'] = 2;
542
+ prevConds.push(selfExpr);
543
+ } else if (cond.kind === 'else') {
544
+ const negs = prevConds.map(negate).join(' && ');
545
+ child['$shown'] = `function () { return ${negs || 'true'}; }`;
546
+ child['shown'] = false;
547
+ child['bind'] = 2;
548
+ prevConds = [];
549
+ }
550
+ }
551
+ if (childNodes.length) node.children = childNodes;
552
+ return node;
553
+ }
554
+ /** Convert CSS text to css.json format */
555
+
556
+ /** Compute styleObjectId hash from a string (same algorithm as blueos-pack) */
557
+ function getStyleObjectId(name) {
558
+ let h = 0;
559
+ for (let i = 0; i < name.length; i++) {
560
+ h = (h << 5) - h + name.charCodeAt(i);
561
+ h = h & h;
562
+ }
563
+ return Math.abs(h);
564
+ }