@aiot-toolkit/aiotpack 2.1.0-prender.1 → 2.1.0-prender.10

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.
@@ -6,10 +6,6 @@ import IJavascriptCompileOption from '../../compiler/javascript/interface/IJavas
6
6
  declare class UxAfterCompile {
7
7
  static compileJavascript: FollowWork<IJavascriptCompileOption>;
8
8
  static protobuf: FollowWork<IJavascriptCompileOption>;
9
- /**
10
- * 预渲染:在 VM 沙箱中执行编译后的模板函数,生成 .template.json + .css.json
11
- */
12
- static prerender: FollowWork<IJavascriptCompileOption>;
13
9
  static jsc: FollowWork<IJavascriptCompileOption>;
14
10
  static toRpk: FollowWork<IJavascriptCompileOption>;
15
11
  /**
@@ -19,7 +19,6 @@ var _ZipFileUtil = _interopRequireDefault(require("@aiot-toolkit/shared-utils/li
19
19
  var _ImageIcu = _interopRequireDefault(require("../../compiler/tools/icu/ImageIcu"));
20
20
  var _LiteCard = _interopRequireDefault(require("../../compiler/javascript/vela/utils/LiteCard"));
21
21
  var _UxUtil = _interopRequireDefault(require("@aiot-toolkit/parser/lib/ux/utils/UxUtil"));
22
- var _prerender = require("../../prerender");
23
22
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
24
23
  const BinaryPlugin = require('@aiot-toolkit/parser/lib/ux/translate/vela/protobuf/BinaryPlugin');
25
24
 
@@ -45,11 +44,16 @@ class UxAfterCompile {
45
44
  level: _sharedUtils.Loglevel.SUCCESS,
46
45
  message: ['javascript compile success']
47
46
  }]);
48
- }).catch(_ref => {
49
- let {
50
- errors,
51
- warnings
52
- } = _ref;
47
+ }).catch(err => {
48
+ const errors = err?.errors;
49
+ const warnings = err?.warnings;
50
+ if (!errors?.length && !warnings?.length) {
51
+ onLog?.([{
52
+ level: _sharedUtils.Loglevel.THROW,
53
+ message: [`javascript compile failed: ${err?.message ?? err}`]
54
+ }]);
55
+ throw err instanceof Error ? err : new Error(String(err));
56
+ }
53
57
  const errorLength = errors?.length || 0;
54
58
  const messages = [`webpack error:\r\n`];
55
59
  if (errors?.length) {
@@ -90,70 +94,6 @@ class UxAfterCompile {
90
94
  BinaryPlugin.createBinFiles();
91
95
  }
92
96
  };
93
-
94
- /**
95
- * 预渲染:在 VM 沙箱中执行编译后的模板函数,生成 .template.json + .css.json
96
- */
97
- static prerender = async params => {
98
- const {
99
- compilerOption,
100
- onLog,
101
- compilation
102
- } = params;
103
- if (!compilerOption || compilerOption.enablePrerender === false) {
104
- return;
105
- }
106
-
107
- // In watch mode, clear prerender caches for changed pages
108
- if (compilation && compilation.trigger !== _FileLaneTriggerType.default.START) {
109
- const {
110
- sourceRoot
111
- } = compilerOption;
112
- const srcPath = _path.default.join(compilerOption.projectPath, sourceRoot);
113
- const changedPages = (compilation.changedFiles || []).filter(f => compilation.entries?.includes(f)).map(f => {
114
- const rel = _path.default.relative(srcPath, f).replace(/\\/g, '/');
115
- return rel.replace(/\.[^.]+$/, '');
116
- });
117
- (0, _prerender.watchChange)(changedPages.length > 0 ? changedPages : undefined);
118
- }
119
- const {
120
- projectPath,
121
- outputPath,
122
- sourceRoot
123
- } = compilerOption;
124
- const buildPath = _path.default.join(projectPath, outputPath);
125
- const manifestPath = _path.default.join(projectPath, sourceRoot, 'manifest.json');
126
- if (!_fsExtra.default.existsSync(manifestPath)) {
127
- return;
128
- }
129
- const excludeList = compilerOption.prerenderExclude || [];
130
- const manifest = _fsExtra.default.readJSONSync(manifestPath);
131
- const pages = Object.keys(manifest.router?.pages || {}).filter(pageName => {
132
- if (excludeList.some(pattern => pageName.startsWith(pattern))) {
133
- onLog?.([{
134
- level: _sharedUtils.Loglevel.INFO,
135
- message: [`prerender: skip excluded ${pageName}`]
136
- }]);
137
- return false;
138
- }
139
- return true;
140
- }).map(pageName => {
141
- const pageConf = manifest.router.pages[pageName];
142
- const component = pageConf.component || 'index';
143
- return `${pageName}/${component}`;
144
- });
145
- if (!pages.length) {
146
- return;
147
- }
148
- const deviceTypes = manifest.deviceTypeList?.length ? manifest.deviceTypeList : ['default'];
149
- await (0, _prerender.prerender)({
150
- buildPath,
151
- sourcePath: _path.default.join(projectPath, sourceRoot),
152
- pages,
153
- prerenderSubComp: compilerOption.prerenderSubComp !== false,
154
- deviceTypes
155
- }, onLog);
156
- };
157
97
  static jsc = async params => {
158
98
  const {
159
99
  context,
@@ -12,16 +12,6 @@ declare function uxToTemplateJson(templateNode: any, styleObjectId?: number, sty
12
12
  * `screen and ` is preserved; `not` / `only` are preserved as-is.
13
13
  */
14
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
15
  declare function parseStyleArray(code: string): Record<string, any>;
26
16
  /** Convert CSS text to css.json format */
27
17
  export { uxToTemplateJson, parseStyleArray, normalizeMediaQuery };
@@ -13,6 +13,149 @@ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return
13
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
14
  // Template.json compilation from parse5 AST
15
15
 
16
+ // A stylesheet has an applicable @MEDIA rule for a node when any @MEDIA entry
17
+ // carries a selector that matches the node's tag or one of its static class
18
+ // tokens (either directly or as a component of a descendant/combinator sel).
19
+ function hasApplicableMediaRule(styleSheet, tagName, classTokens) {
20
+ const media = styleSheet?.['@MEDIA'];
21
+ if (!Array.isArray(media)) return false;
22
+ const classSet = new Set(classTokens);
23
+ for (const entry of media) {
24
+ if (!entry || typeof entry !== 'object') continue;
25
+ for (const sel of Object.keys(entry)) {
26
+ if (sel === 'condition') continue;
27
+ for (const part of sel.split(/[\s>+~]/).map(s => s.trim()).filter(Boolean)) {
28
+ if (part.startsWith('.')) {
29
+ if (classSet.has(part.slice(1))) return true;
30
+ } else if (!part.startsWith('#')) {
31
+ if (part === tagName) return true;
32
+ }
33
+ }
34
+ }
35
+ }
36
+ return false;
37
+ }
38
+
39
+ // vivo blueos-pack's aggressive camelCase applied to transition-property values.
40
+ // Every letter that follows a non-letter character (separator or digit) is upper-cased,
41
+ // then all non-alphanumeric characters are dropped. This intentionally covers both the
42
+ // simple case (`background-position` → `backgroundPosition`) and the odd shorthand-inside-
43
+ // transition-property form we see in inline styles (`transform-origin 0.2s ease-in-out .5s`
44
+ // → `transformOrigin02SEaseInOut5S`).
45
+ function normalizeTransitionPropertyValue(s) {
46
+ let out = '';
47
+ let capNext = false;
48
+ for (const ch of s) {
49
+ if (/[A-Za-z]/.test(ch)) {
50
+ out += capNext ? ch.toUpperCase() : ch;
51
+ capNext = false;
52
+ } else if (/[0-9]/.test(ch)) {
53
+ out += ch;
54
+ capNext = true;
55
+ } else {
56
+ capNext = true;
57
+ }
58
+ }
59
+ return out;
60
+ }
61
+
62
+ // vivo blueos-pack's inline `animation-timing-function` normalization.
63
+ // Returns:
64
+ // - string → the normalized value to emit
65
+ // - null (drop) → invalid; caller should skip this inline decl so the class-level
66
+ // value (or default) applies
67
+ // Rules (traced empirically from build-vivo/watch-round baseline):
68
+ // - Predefined keywords (`linear`, `ease`, `ease-in`, `ease-out`, `ease-in-out`,
69
+ // `step-start`, `step-end`) → keep as-is
70
+ // - `steps(N)` → normalize to `steps(N, end)` (fill default dir)
71
+ // - `steps(N, start|end)` → keep as-is
72
+ // - `cubic-bezier(a, b, c, d)`
73
+ // * all 4 numeric and x1,x2 ∈ [0,1] → keep (spec-compliant)
74
+ // * otherwise → drop (invalid per vivo validator)
75
+ // - Any other unrecognized function → drop
76
+ function normalizeAnimationTimingFunction(s) {
77
+ const trimmed = s.trim();
78
+ if (['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'step-start', 'step-end'].includes(trimmed)) return trimmed;
79
+ const stepsMatch = /^steps\s*\(\s*([^,)]+?)\s*(?:,\s*(start|end)\s*)?\)$/.exec(trimmed);
80
+ if (stepsMatch) {
81
+ const n = stepsMatch[1];
82
+ const dir = stepsMatch[2] || 'end';
83
+ return `steps(${n}, ${dir})`;
84
+ }
85
+ const cubicMatch = /^cubic-bezier\s*\(\s*([^)]+)\)$/.exec(trimmed);
86
+ if (cubicMatch) {
87
+ const args = cubicMatch[1].split(',').map(a => a.trim());
88
+ if (args.length !== 4) return null;
89
+ const nums = args.map(a => Number(a));
90
+ if (nums.some(n => !isFinite(n))) return null;
91
+ // spec: x1 and x2 must be in [0, 1]; y1 and y2 unbounded. Reproduce vivo behavior:
92
+ // any component outside [0, 1] is treated as invalid (vivo is stricter than spec).
93
+ if (nums.some(n => n < 0 || n > 1)) return null;
94
+ return `cubic-bezier(${args.join(', ')})`;
95
+ }
96
+ return null;
97
+ }
98
+
99
+ // `transform: translate(40px) scale(0.5) ...` (CSS function-list from inline style="")
100
+ // → `{"translateX":"40px","translateY":"0px","scaleX":0.5,"scaleY":0.5}`.
101
+ // Returns null when input isn't a plain CSS function list (already JSON, dynamic
102
+ // binding, or unknown function), so callers can fall back to the raw string.
103
+ function normalizeCssTransform(input) {
104
+ if (typeof input !== 'string') return null;
105
+ const s = input.trim();
106
+ if (!s || s.startsWith('{') || /\{\{/.test(s)) return null;
107
+ const out = {};
108
+ const funcRe = /(\w+)\s*\(([^)]*)\)/g;
109
+ let matched = false;
110
+ let m;
111
+ const toNum = x => /^-?\d+(?:\.\d+)?$/.test(x) ? Number(x) : x;
112
+ // Add a CSS unit to bare numeric arguments; keep existing unit / percent forms.
113
+ const withUnit = (x, defaultUnit) => {
114
+ if (x === undefined) return '0' + defaultUnit;
115
+ return /^-?\d+(?:\.\d+)?$/.test(x) ? x + defaultUnit : x;
116
+ };
117
+ while ((m = funcRe.exec(s)) !== null) {
118
+ matched = true;
119
+ const fn = m[1];
120
+ const args = m[2].split(',').map(a => a.trim()).filter(Boolean);
121
+ switch (fn) {
122
+ case 'translate':
123
+ out.translateX = withUnit(args[0], 'px');
124
+ out.translateY = withUnit(args[1], 'px');
125
+ break;
126
+ case 'scale':
127
+ {
128
+ const x = args[0] ?? '1';
129
+ out.scaleX = toNum(x);
130
+ out.scaleY = args[1] !== undefined ? toNum(args[1]) : toNum(x);
131
+ break;
132
+ }
133
+ case 'translateX':
134
+ case 'translateY':
135
+ case 'translateZ':
136
+ out[fn] = withUnit(args[0], 'px');
137
+ break;
138
+ case 'scaleX':
139
+ case 'scaleY':
140
+ out[fn] = toNum(args[0] ?? '1');
141
+ break;
142
+ case 'rotate':
143
+ case 'rotateX':
144
+ case 'rotateY':
145
+ case 'rotateZ':
146
+ case 'skew':
147
+ case 'skewX':
148
+ case 'skewY':
149
+ // Only the first arg is meaningful for single-axis rotate/skew; vivo silently
150
+ // discards extras (e.g. `rotate(90deg,120deg)` → `rotate: "90deg"`). We follow suit.
151
+ out[fn] = withUnit(args[0], 'deg');
152
+ break;
153
+ default:
154
+ return null;
155
+ }
156
+ }
157
+ return matched ? JSON.stringify(out) : null;
158
+ }
16
159
  function uxToTemplateJson(templateNode, styleObjectId, styleSheet, importMap, pageDir) {
17
160
  const children = templateNode.content?.childNodes || templateNode.childNodes || [];
18
161
  const rootEl = children.find(n => n.tagName && n.nodeName !== '#text');
@@ -100,10 +243,74 @@ function normalizeMediaQuery(condition) {
100
243
  * `{selector: props}` map. The condition text is normalized to Level 3 so
101
244
  * runtime only needs a Level-3 matcher.
102
245
  */
246
+ const NAMED_COLORS = new Set(['transparent', 'aliceblue', 'antiquewhite', 'aqua', 'aquamarine', 'azure', 'beige', 'bisque', 'black', 'blanchedalmond', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chartreuse', 'chocolate', 'coral', 'cornflowerblue', 'cornsilk', 'crimson', 'cyan', 'darkblue', 'darkcyan', 'darkgoldenrod', 'darkgray', 'darkgreen', 'darkgrey', 'darkkhaki', 'darkmagenta', 'darkolivegreen', 'darkorange', 'darkorchid', 'darkred', 'darksalmon', 'darkseagreen', 'darkslateblue', 'darkslategray', 'darkslategrey', 'darkturquoise', 'darkviolet', 'deeppink', 'deepskyblue', 'dimgray', 'dimgrey', 'dodgerblue', 'firebrick', 'floralwhite', 'forestgreen', 'fuchsia', 'gainsboro', 'ghostwhite', 'gold', 'goldenrod', 'gray', 'green', 'greenyellow', 'grey', 'honeydew', 'hotpink', 'indianred', 'indigo', 'ivory', 'khaki', 'lavender', 'lavenderblush', 'lawngreen', 'lemonchiffon', 'lightblue', 'lightcoral', 'lightcyan', 'lightgoldenrodyellow', 'lightgray', 'lightgreen', 'lightgrey', 'lightpink', 'lightsalmon', 'lightseagreen', 'lightskyblue', 'lightslategray', 'lightslategrey', 'lightsteelblue', 'lightyellow', 'lime', 'limegreen', 'linen', 'magenta', 'maroon', 'mediumaquamarine', 'mediumblue', 'mediumorchid', 'mediumpurple', 'mediumseagreen', 'mediumslateblue', 'mediumspringgreen', 'mediumturquoise', 'mediumvioletred', 'midnightblue', 'mintcream', 'mistyrose', 'moccasin', 'navajowhite', 'navy', 'oldlace', 'olive', 'olivedrab', 'orange', 'orangered', 'orchid', 'palegoldenrod', 'palegreen', 'paleturquoise', 'palevioletred', 'papayawhip', 'peachpuff', 'peru', 'pink', 'plum', 'powderblue', 'purple', 'rebeccapurple', 'red', 'rosybrown', 'royalblue', 'saddlebrown', 'salmon', 'sandybrown', 'seagreen', 'seashell', 'sienna', 'silver', 'skyblue', 'slateblue', 'slategray', 'slategrey', 'snow', 'springgreen', 'steelblue', 'tan', 'teal', 'thistle', 'tomato', 'turquoise', 'violet', 'wheat', 'white', 'whitesmoke', 'yellow', 'yellowgreen']);
247
+ const COLOR_PROP_RE = /(?:^|[a-z])[Cc]olor$/;
248
+ const NON_NAMED_COLOR_VALUE_RE = /^(?:#[0-9a-fA-F]{3,8}|rgba?\(|hsla?\(|currentColor)/;
103
249
  function parseStyleArray(code) {
104
250
  const styleSheet = {};
251
+ const expandShortHex = value => {
252
+ const short = /^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])?$/.exec(value);
253
+ if (!short) return value;
254
+ const r = short[1] + short[1];
255
+ const g = short[2] + short[2];
256
+ const b = short[3] + short[3];
257
+ const a = short[4] ? short[4] + short[4] : '';
258
+ return ('#' + r + g + b + a).toLowerCase();
259
+ };
260
+ const normalizeTransform = obj => {
261
+ if (!obj || typeof obj !== 'object') return typeof obj === 'string' ? obj : JSON.stringify(obj);
262
+ const out = {};
263
+ for (const [k, v] of Object.entries(obj)) {
264
+ out[k] = typeof v === 'number' && /^translate[XYZ]?$/.test(k) ? `${v}px` : v;
265
+ }
266
+ return JSON.stringify(out);
267
+ };
268
+ const expandKeyframeSteps = value => {
269
+ let arr = [];
270
+ if (typeof value === 'string') {
271
+ try {
272
+ arr = JSON.parse(value);
273
+ } catch {
274
+ return [];
275
+ }
276
+ } else if (Array.isArray(value)) {
277
+ arr = value;
278
+ } else {
279
+ return [];
280
+ }
281
+ return arr.map(step => {
282
+ const out = {};
283
+ const entries = Object.entries(step ?? {});
284
+ for (const [k, v] of entries) {
285
+ if (k === 'time') continue;
286
+ out[k] = k === 'transform' && v && typeof v === 'object' ? normalizeTransform(v) : v;
287
+ }
288
+ if ('time' in (step ?? {})) out.time = step.time;
289
+ return out;
290
+ });
291
+ };
292
+ const normalizeStyleProps = props => {
293
+ if (!props || typeof props !== 'object') return props;
294
+ const out = {};
295
+ for (const [k, v] of Object.entries(props)) {
296
+ if ((k === 'animationIterationCount' || k === 'iterationCount') && v === 'infinite') {
297
+ out[k] = -1;
298
+ } else if (typeof v === 'string' && COLOR_PROP_RE.test(k)) {
299
+ if (NAMED_COLORS.has(v.toLowerCase())) {
300
+ out[k] = v;
301
+ } else if (NON_NAMED_COLOR_VALUE_RE.test(v)) {
302
+ out[k] = expandShortHex(v);
303
+ }
304
+ } else {
305
+ out[k] = v;
306
+ }
307
+ }
308
+ return out;
309
+ };
105
310
  const ingest = parsed => {
106
311
  if (!Array.isArray(parsed)) return;
312
+ let currentMediaCondition = null;
313
+ let currentMediaEntry = null;
107
314
  for (const rule of parsed) {
108
315
  if (!Array.isArray(rule) || rule.length < 2) continue;
109
316
  let selectors, props, conditionText;
@@ -115,16 +322,47 @@ function parseStyleArray(code) {
115
322
  selectors = rule[0];
116
323
  props = rule[1];
117
324
  }
118
- if (!Array.isArray(selectors)) continue;
119
- let target = styleSheet;
325
+ if (!Array.isArray(selectors) || selectors.length === 0) continue;
326
+ let target;
120
327
  if (conditionText) {
121
- const key = `@media ${normalizeMediaQuery(conditionText)}`;
122
- target = styleSheet[key] = styleSheet[key] || {};
328
+ const normalized = normalizeMediaQuery(conditionText);
329
+ if (currentMediaCondition !== normalized) {
330
+ const list = styleSheet['@MEDIA'] = styleSheet['@MEDIA'] || [];
331
+ currentMediaEntry = {
332
+ condition: normalized
333
+ };
334
+ list.push(currentMediaEntry);
335
+ currentMediaCondition = normalized;
336
+ }
337
+ target = currentMediaEntry;
338
+ } else {
339
+ target = styleSheet;
340
+ currentMediaCondition = null;
341
+ currentMediaEntry = null;
123
342
  }
124
- for (const sel of selectors) {
125
- if (Array.isArray(sel) && sel.length >= 2) {
126
- target[sel[0] === 0 ? `.${sel[1]}` : sel[1]] = props;
343
+ const normalizedProps = normalizeStyleProps(props);
344
+ const firstSel = selectors[0];
345
+ if (Array.isArray(firstSel) && firstSel.length >= 2 && firstSel[0] === 3) {
346
+ const bucket = target['@KEYFRAMES'] = target['@KEYFRAMES'] || {};
347
+ bucket[firstSel[1]] = expandKeyframeSteps(props?.keyframes ?? '[]');
348
+ } else if (Array.isArray(firstSel) && firstSel.length >= 2 && firstSel[0] === 4) {
349
+ const bucket = target['@FONT-FACE'] = target['@FONT-FACE'] || {};
350
+ bucket[firstSel[1]] = props?.fontface ?? normalizedProps;
351
+ } else {
352
+ const pieces = [];
353
+ for (let i = 0; i < selectors.length; i++) {
354
+ const sel = selectors[i];
355
+ if (!Array.isArray(sel) || sel.length < 2) continue;
356
+ if (sel[0] === 5) {
357
+ pieces.push(String(sel[1]));
358
+ continue;
359
+ }
360
+ if (pieces.length && pieces[pieces.length - 1] !== '>' && pieces[pieces.length - 1] !== '~' && pieces[pieces.length - 1] !== '+') {
361
+ pieces.push(' ');
362
+ }
363
+ if (sel[0] === 0) pieces.push(`.${sel[1]}`);else if (sel[0] === 1) pieces.push(`#${sel[1]}`);else pieces.push(String(sel[1]));
127
364
  }
365
+ if (pieces.length) target[pieces.join('')] = normalizedProps;
128
366
  }
129
367
  }
130
368
  };
@@ -186,6 +424,50 @@ function addThisPrefix(expr, extraReserved) {
186
424
  }).join('');
187
425
  }
188
426
  }
427
+ function emitTextValueAttr(rawText) {
428
+ const text = rawText.trim();
429
+ if (!/\{\{[\s\S]*?\}\}/.test(text)) {
430
+ return {
431
+ attr: {
432
+ value: text.replace(/\s*\n\s*/g, ' ')
433
+ },
434
+ needsBind: false
435
+ };
436
+ }
437
+ const parts = text.split(/(\{\{[\s\S]*?\}\})/);
438
+ const exprs = parts.filter(Boolean).map(p => {
439
+ const m = p.match(/^\{\{([\s\S]*?)\}\}$/);
440
+ if (m) return addThisPrefix(m[1].trim());
441
+ return `'${p.replace(/'/g, "\\'")}'`;
442
+ });
443
+ if (exprs.length === 1 && pathTestRE.test(exprs[0].replace(/^this\./, ''))) {
444
+ return {
445
+ attr: {
446
+ $value: exprs[0].replace(/^this\./, '')
447
+ },
448
+ needsBind: true
449
+ };
450
+ }
451
+ if (exprs.length === 1) {
452
+ return {
453
+ attr: {
454
+ $value: `function () { return ${exprs[0]}; }`
455
+ },
456
+ needsBind: true
457
+ };
458
+ }
459
+ const wrapped = exprs.map(e => {
460
+ if (e.startsWith("'") || e.startsWith('"')) return e;
461
+ if (/(\|\||&&|\?\s|\+)/.test(e) && !e.startsWith('(')) return `(${e})`;
462
+ return e;
463
+ });
464
+ return {
465
+ attr: {
466
+ $value: `function () { return '' + ${wrapped.join(' + ')}; }`
467
+ },
468
+ needsBind: true
469
+ };
470
+ }
189
471
  function elementToJson(el, styleSheet, importMap, scopeVars, pageDir) {
190
472
  const tagName = el.tagName === 'img' ? 'image' : el.tagName;
191
473
  const node = {
@@ -214,7 +496,7 @@ function elementToJson(el, styleSheet, importMap, scopeVars, pageDir) {
214
496
  if (name === 'is' && tagName === 'component') {
215
497
  continue; // is attribute handled at runtime
216
498
  } else if (name === 'class') {
217
- className = value.trim();
499
+ className = value.replace(/\s+/g, ' ').trim();
218
500
  } else if (name.startsWith('@') || name.startsWith('on') && name.length > 2) {
219
501
  const rawEvtName = name.startsWith('@') ? name.slice(1) : name.replace(/^on/, '');
220
502
  const evtName = rawEvtName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
@@ -346,7 +628,17 @@ function elementToJson(el, styleSheet, importMap, scopeVars, pageDir) {
346
628
  hasDynamicInlineStyle = true;
347
629
  } else {
348
630
  // Static value — accumulate to staticInlineStyle for merging into node.style later
349
- staticInlineStyle[key] = propVal;
631
+ if (key === 'transform') {
632
+ staticInlineStyle[key] = normalizeCssTransform(propVal) ?? propVal;
633
+ } else if (key === 'transitionProperty') {
634
+ staticInlineStyle[key] = normalizeTransitionPropertyValue(propVal);
635
+ } else if (key === 'animationTimingFunction' || key === 'transitionTimingFunction') {
636
+ const normalized = normalizeAnimationTimingFunction(propVal);
637
+ if (normalized !== null) staticInlineStyle[key] = normalized;
638
+ // else drop — invalid inline value; class-level rule (or default) applies
639
+ } else {
640
+ staticInlineStyle[key] = propVal;
641
+ }
350
642
  }
351
643
  }
352
644
  });
@@ -396,8 +688,11 @@ function elementToJson(el, styleSheet, importMap, scopeVars, pageDir) {
396
688
  hasBind = true;
397
689
  } else {
398
690
  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) {
691
+ // Normalize relative path attrs (src, href, alt) to absolute paths from src root.
692
+ // alt can also carry prose (e.g. "blank") — only rewrite when the value starts with
693
+ // ./ or ../ so image alt-text prose passes through untouched.
694
+ const isRelPath = staticVal.startsWith('./') || staticVal.startsWith('../');
695
+ if (staticVal && isRelPath && (name === 'src' || name === 'href' || name === 'alt') && pageDir) {
401
696
  // Resolve relative to pageDir, then make absolute from src root
402
697
  const segments = pageDir.split('/').filter(Boolean).concat(staticVal.split('/'));
403
698
  const stack = [];
@@ -414,34 +709,9 @@ function elementToJson(el, styleSheet, importMap, scopeVars, pageDir) {
414
709
  for (const child of el.childNodes || []) {
415
710
  const textElements = new Set(['text', 'span', 'a', 'input', 'qrcode', 'barcode', 'marquee', 'arc-text', 'option', 'button', 'label', 'textarea', 'richtext']);
416
711
  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
- }
712
+ const emitted = emitTextValueAttr(child.value);
713
+ Object.assign(attrObj, emitted.attr);
714
+ if (emitted.needsBind) hasBind = true;
445
715
  }
446
716
  }
447
717
  // Output in vivo order: attr → bind → class → events → import → style → children
@@ -464,30 +734,88 @@ function elementToJson(el, styleSheet, importMap, scopeVars, pageDir) {
464
734
  hasBind = true;
465
735
  hasDynClass = true;
466
736
  }
467
- if (isComponent) {
468
- node.bind = 3;
469
- node['import'] = importMap[el.tagName];
470
- } else if (node['$shown'] !== undefined || node['$repeat'] || node['repeat']?.['$exp']) {
737
+ if (node['$shown'] !== undefined || node['$repeat'] !== undefined || node['repeat']?.['$exp']) {
471
738
  node.bind = 2;
739
+ } else if (isComponent) {
740
+ node.bind = 3;
472
741
  } else if (hasBind) {
473
742
  node.bind = 1;
474
743
  }
744
+ if (isComponent) {
745
+ node['import'] = importMap[el.tagName];
746
+ }
475
747
  if (!hasDynClass && className) {
476
748
  node.class = className;
477
749
  }
478
750
  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.
751
+ // Inline style from styleSheet (class selector + tag selector) + static portion of style="...".
752
+ // - Static class only: merge tag + class rules + staticInlineStyle → node.style
753
+ // - Dynamic class (partly {{}}): vivo emits merged tag + STATIC class rules into
754
+ // node.inlineStyle (alongside per-key $foo bindings),
755
+ // so the runtime still applies them even though the
756
+ // class list is only known at runtime.
757
+ // - Fully dynamic (only {{...}}): no static class tokens → tag rules only, still into
758
+ // inlineStyle.
759
+ // @media rules are applied at runtime by SDK, not inlined here.
760
+ // Class rules apply in stylesheet declaration order (later declarations override earlier),
761
+ // NOT in class-attribute order.
481
762
  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}`]);
763
+ const staticClassTokens = [];
764
+ if (className) {
765
+ const parts = className.split(/(\{\{[\s\S]*?\}\})/).filter(Boolean);
766
+ for (const p of parts) {
767
+ if (/^\{\{[\s\S]*?\}\}$/.test(p)) continue;
768
+ for (const tok of p.trim().split(/\s+/)) if (tok) staticClassTokens.push(tok);
487
769
  }
488
770
  }
489
- Object.assign(merged, staticInlineStyle);
490
- if (Object.keys(merged).length) node.style = merged;
771
+ const hasMedia = hasApplicableMediaRule(styleSheet, el.tagName, staticClassTokens);
772
+ // Skip class-rule merge when the node has any dynamic binding OR any @media
773
+ // rule that applies to it — runtime must resolve class chain against the
774
+ // stylesheet for correct priority + media evaluation. Only the user-authored
775
+ // `style="..."` static portion stays inlined on the node.
776
+ const skipClassMerge = hasBind || hasMedia;
777
+ // @media-only presence with no other dynamic binding still needs runtime
778
+ // re-evaluation, mark bind=1 (vivo convention).
779
+ if (!node.bind && hasMedia) node.bind = 1;
780
+ if (skipClassMerge) {
781
+ const inlined = {
782
+ ...staticInlineStyle
783
+ };
784
+ if (inlined.animationName && styleSheet['@KEYFRAMES']?.[inlined.animationName]) {
785
+ inlined.animationKeyframes = {
786
+ [inlined.animationName]: styleSheet['@KEYFRAMES'][inlined.animationName]
787
+ };
788
+ }
789
+ if (Object.keys(inlined).length) {
790
+ if (hasDynClass) {
791
+ const existing = node['inlineStyle'] || {};
792
+ node['inlineStyle'] = {
793
+ ...inlined,
794
+ ...existing
795
+ };
796
+ } else {
797
+ node.style = inlined;
798
+ }
799
+ }
800
+ } else {
801
+ const merged = {};
802
+ if (styleSheet[el.tagName]) Object.assign(merged, styleSheet[el.tagName]);
803
+ if (staticClassTokens.length) {
804
+ const classSet = new Set(staticClassTokens);
805
+ for (const key of Object.keys(styleSheet)) {
806
+ if (key.length > 1 && key.startsWith('.') && classSet.has(key.slice(1))) {
807
+ Object.assign(merged, styleSheet[key]);
808
+ }
809
+ }
810
+ }
811
+ Object.assign(merged, staticInlineStyle);
812
+ if (merged.animationName && styleSheet['@KEYFRAMES']?.[merged.animationName]) {
813
+ merged.animationKeyframes = {
814
+ [merged.animationName]: styleSheet['@KEYFRAMES'][merged.animationName]
815
+ };
816
+ }
817
+ if (Object.keys(merged).length) node.style = merged;
818
+ }
491
819
  } else if (Object.keys(staticInlineStyle).length) {
492
820
  node.style = {
493
821
  ...staticInlineStyle
@@ -502,14 +830,12 @@ function elementToJson(el, styleSheet, importMap, scopeVars, pageDir) {
502
830
  if (child.tagName) {
503
831
  childNodes.push(elementToJson(child, styleSheet, importMap, scopeVars, pageDir));
504
832
  } 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, ' ');
833
+ const emitted = emitTextValueAttr(child.value);
507
834
  const spanNode = {
508
835
  type: 'span',
509
- attr: {
510
- value: text
511
- }
836
+ attr: emitted.attr
512
837
  };
838
+ if (emitted.needsBind) spanNode.bind = 1;
513
839
  childNodes.push(spanNode);
514
840
  }
515
841
  }
@@ -10,4 +10,8 @@ declare class ViteCompiler implements ICompiler {
10
10
  compile(param: IJavascriptCompileOption): Promise<void>;
11
11
  clean(param: ICompileParam & IJavascriptCompileOption): Promise<void>;
12
12
  }
13
+ declare function stripDeadDefProp(code: string): string;
14
+ export declare const __test: {
15
+ stripDeadDefProp: typeof stripDeadDefProp;
16
+ };
13
17
  export default ViteCompiler;