@aiot-toolkit/aiotpack 2.1.0-prender.13 → 2.1.0-prender.15

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.
@@ -1,4 +1,11 @@
1
1
  declare function uxToTemplateJson(templateNode: any, styleObjectId?: number, styleSheet?: any, importMap?: Record<string, string>, pageDir?: string, componentsMap?: Record<string, string>): any;
2
+ /**
3
+ * `for="x in expr"` / `for="(k, v) in expr"` declare loop variable names that the
4
+ * children reference verbatim (no `this.` prefix). If `repeat` loses `value`, runtime
5
+ * falls back to naming the item `$item` and every `x.*` reference in the subtree
6
+ * resolves to undefined — the rows render empty with no error.
7
+ */
8
+ declare function assertRepeatVariablePreserved(root: any, pageDir?: string): void;
2
9
  /**
3
10
  * Normalize a media query condition text from CSS Level 4 syntax to Level 3.
4
11
  *
@@ -14,6 +21,6 @@ declare function uxToTemplateJson(templateNode: any, styleObjectId?: number, sty
14
21
  declare function normalizeMediaQuery(condition: string): string;
15
22
  declare function parseStyleArray(code: string): Record<string, any>;
16
23
  /** Convert CSS text to css.json format */
17
- export { uxToTemplateJson, parseStyleArray, normalizeMediaQuery };
24
+ export { uxToTemplateJson, parseStyleArray, normalizeMediaQuery, assertRepeatVariablePreserved };
18
25
  /** Compute styleObjectId hash from a string (same algorithm as blueos-pack) */
19
26
  export declare function getStyleObjectId(name: string): number;
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.assertRepeatVariablePreserved = assertRepeatVariablePreserved;
6
7
  exports.getStyleObjectId = getStyleObjectId;
7
8
  exports.normalizeMediaQuery = normalizeMediaQuery;
8
9
  exports.parseStyleArray = parseStyleArray;
@@ -236,6 +237,9 @@ function uxToTemplateJson(templateNode, styleObjectId, styleSheet, importMap, pa
236
237
  const rootEl = children.find(n => n.tagName && n.nodeName !== '#text');
237
238
  if (!rootEl) return {};
238
239
  const result = elementToJson(rootEl, styleSheet, importMap, undefined, pageDir);
240
+ assertNoRelativeAssetPaths(result, pageDir);
241
+ assertNoDelimitersInEvents(result, pageDir);
242
+ assertRepeatVariablePreserved(result, pageDir);
239
243
  const componentsForRoot = componentsMap && Object.keys(componentsMap).length ? componentsMap : importMap;
240
244
  if (componentsForRoot && Object.keys(componentsForRoot).length) {
241
245
  result.components = {
@@ -259,6 +263,209 @@ function uxToTemplateJson(templateNode, styleObjectId, styleSheet, importMap, pa
259
263
  /** Add this. prefix to variable references in an expression */
260
264
  // blueos-pack's pathTestRE: matches simple paths like a.b.c, a['key'], a[0], a[$idx]
261
265
  const pathTestRE = /^[A-Za-z_$][\w$]*(?:\.[\w$]+|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/;
266
+
267
+ // `for="x in expr"` declares a loop variable, so the variable names must survive in
268
+ // `repeat.{key,value}`. The whole binding therefore stays inside `repeat.$exp`, which
269
+ // holds a dotted path verbatim and anything else as a function string. `$repeat` is
270
+ // reserved for the variable-less `for="{{expr}}"` form, where runtime names the item
271
+ // `$item` itself.
272
+ const dottedPathRE = /^[a-zA-Z_$][\w$.]*$/;
273
+ function forRepeatExp(expr) {
274
+ return dottedPathRE.test(expr) ? expr : `function () { return ${addThisPrefix(expr)}; }`;
275
+ }
276
+
277
+ // Attributes whose value is a resource path and therefore must be rewritten from
278
+ // `./x` / `../x` (relative to the owning .ux file) to an rpk-root absolute path.
279
+ // Runtime resolves <image src> against the rpk root, so a relative path emitted
280
+ // from a nested component directory never resolves and falls back to a placeholder.
281
+ const PATH_ATTRS = new Set(['src', 'href', 'alt']);
282
+ const ARROW_RE = /=>/;
283
+
284
+ /**
285
+ * Canonical event key for an `@x` / `onx` attribute.
286
+ *
287
+ * Every event name the platform defines is a lowercase single word
288
+ * (`click`, `longpress`, `touchstart`, `animationiteration`, ... — 72 of them
289
+ * across vela and android, none camelCase or hyphenated), and the parser's own
290
+ * `translateEventName` just drops the prefix. Camel-casing the remainder turned
291
+ * `on-long-press` into `LongPress`, which matches no event and silently never
292
+ * fires, so separators are removed and the name lowercased instead.
293
+ */
294
+ function normalizeEventName(name) {
295
+ const stripped = name.startsWith('@') ? name.slice(1) : name.replace(/^on/, '');
296
+ return stripped.replace(/[-_]/g, '').toLowerCase();
297
+ }
298
+
299
+ /**
300
+ * Strip one wrapping `{{ ... }}` from an attribute value.
301
+ *
302
+ * Only the outer pair is removed, and only when it actually wraps the whole
303
+ * value — `{{()=>{ x() }}}` must yield `()=>{ x() }`, so a global
304
+ * `replace(/\{\{|\}\}/g, '')` is wrong: it would also eat the arrow body's
305
+ * braces. A value that merely contains an interpolation (`a {{b}} c`) is left
306
+ * alone; callers that need that do their own splitting.
307
+ */
308
+ function stripTemplateDelimiters(value) {
309
+ let v = (value ?? '').trim();
310
+ if (!v.startsWith('{{') || !v.endsWith('}}')) return v;
311
+ const inner = v.slice(2, -2);
312
+ // Reject `{{a}} + {{b}}`, where the opening pair closes before the end.
313
+ let depth = 0;
314
+ for (let i = 0; i < inner.length; i++) {
315
+ if (inner[i] === '{') depth++;else if (inner[i] === '}') {
316
+ depth--;
317
+ if (depth < 0) return v;
318
+ }
319
+ }
320
+ return depth === 0 ? inner.trim() : v;
321
+ }
322
+ function isRelAssetPath(v) {
323
+ return v.startsWith('./') || v.startsWith('../');
324
+ }
325
+ function resolveRelFromPageDir(rel, pageDir) {
326
+ // A trailing slash is significant when the path is a prefix fragment of a
327
+ // concatenated expression ('./assets/full/' + lang + '.png') — dropping it
328
+ // would yield '/.../fullcn.png'.
329
+ const trailing = rel.endsWith('/') ? '/' : '';
330
+ const segments = pageDir.split('/').filter(Boolean).concat(rel.split('/'));
331
+ const stack = [];
332
+ for (const seg of segments) {
333
+ if (seg === '..') stack.pop();else if (seg && seg !== '.') stack.push(seg);
334
+ }
335
+ return '/' + stack.join('/') + trailing;
336
+ }
337
+
338
+ /**
339
+ * Rewrite relative resource-path string literals inside a generated expression.
340
+ * Dynamic `src="{{'./assets/' + lang + '.png'}}"` compiles to a `$src` function
341
+ * body, so the path lives in a literal rather than a plain attribute value — the
342
+ * static-attribute rewrite never sees it. Only literals starting with ./ or ../
343
+ * are touched, so suffix fragments like '.png' pass through untouched.
344
+ */
345
+ function rewriteRelPathLiterals(exprSrc, pageDir) {
346
+ let out = '';
347
+ let i = 0;
348
+ while (i < exprSrc.length) {
349
+ const ch = exprSrc[i];
350
+ if (ch !== "'" && ch !== '"') {
351
+ out += ch;
352
+ i++;
353
+ continue;
354
+ }
355
+ const quote = ch;
356
+ let j = i + 1;
357
+ let raw = '';
358
+ let closed = false;
359
+ while (j < exprSrc.length) {
360
+ const c = exprSrc[j];
361
+ if (c === '\\') {
362
+ raw += c + (exprSrc[j + 1] ?? '');
363
+ j += 2;
364
+ continue;
365
+ }
366
+ if (c === quote) {
367
+ closed = true;
368
+ break;
369
+ }
370
+ raw += c;
371
+ j++;
372
+ }
373
+ if (!closed) {
374
+ out += exprSrc.slice(i);
375
+ break;
376
+ }
377
+ const unescaped = raw.replace(/\\(['"\\])/g, '$1');
378
+ if (isRelAssetPath(unescaped)) {
379
+ const abs = resolveRelFromPageDir(unescaped, pageDir);
380
+ out += quote + abs.replace(/\\/g, '\\\\').replace(new RegExp(quote, 'g'), '\\' + quote) + quote;
381
+ } else {
382
+ out += quote + raw + quote;
383
+ }
384
+ i = j + 1;
385
+ }
386
+ return out;
387
+ }
388
+ const REL_LITERAL_IN_EXPR_RE = /(['"])\.\.?\//;
389
+
390
+ /**
391
+ * Fail the build when a template still carries a relative resource path, in a
392
+ * static attr or inside a `$`-bound expression's literals. Runtime resolves
393
+ * these against the rpk root, silently falling back to a placeholder image —
394
+ * far harder to diagnose on-device than a pack-time error.
395
+ */
396
+ function assertNoRelativeAssetPaths(root, pageDir) {
397
+ const offenders = [];
398
+ const walk = node => {
399
+ if (!node || typeof node !== 'object') return;
400
+ const attr = node.attr;
401
+ if (attr && typeof attr === 'object') {
402
+ for (const [key, val] of Object.entries(attr)) {
403
+ if (typeof val !== 'string') continue;
404
+ const bare = key.startsWith('$') ? key.slice(1) : key;
405
+ if (!PATH_ATTRS.has(bare)) continue;
406
+ const bad = key.startsWith('$') ? REL_LITERAL_IN_EXPR_RE.test(val) : isRelAssetPath(val);
407
+ if (bad) offenders.push(`<${node.type ?? '?'} ${key}="${val}">`);
408
+ }
409
+ }
410
+ if (Array.isArray(node.children)) node.children.forEach(walk);
411
+ };
412
+ walk(root);
413
+ if (offenders.length) {
414
+ throw new Error(`[template] ${offenders.length} relative resource path(s) survived rewriting` + `${pageDir ? ` in ${pageDir}` : ' (no pageDir supplied)'}. Runtime resolves src/href ` + `against the rpk root, so these render a placeholder instead of the asset:\n` + offenders.map(o => ` ${o}`).join('\n'));
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Fail the build when an event value still carries template delimiters. Runtime
420
+ * cannot recover: the `$`-prefixed form fails to compile (logged, event skipped)
421
+ * and the bare form is looked up as a method literally named `{{fn}}` (silent).
422
+ * Either way the handler is dead, so catching it at pack time is the only way
423
+ * the breakage is visible before the device.
424
+ */
425
+ function assertNoDelimitersInEvents(root, pageDir) {
426
+ const offenders = [];
427
+ const walk = node => {
428
+ if (!node || typeof node !== 'object') return;
429
+ const events = node.events;
430
+ if (events && typeof events === 'object') {
431
+ for (const [key, val] of Object.entries(events)) {
432
+ if (typeof val !== 'string') continue;
433
+ if (val.includes('{{') || val.includes('}}')) {
434
+ offenders.push(`<${node.type ?? '?'} ${key}="${val}">`);
435
+ }
436
+ }
437
+ }
438
+ if (Array.isArray(node.children)) node.children.forEach(walk);
439
+ };
440
+ walk(root);
441
+ if (offenders.length) {
442
+ throw new Error(`[template] ${offenders.length} event handler(s) still contain template delimiters` + `${pageDir ? ` in ${pageDir}` : ''}. Runtime cannot compile or resolve these, so the ` + `events are silently dead:\n` + offenders.map(o => ` ${o}`).join('\n'));
443
+ }
444
+ }
445
+
446
+ /**
447
+ * `for="x in expr"` / `for="(k, v) in expr"` declare loop variable names that the
448
+ * children reference verbatim (no `this.` prefix). If `repeat` loses `value`, runtime
449
+ * falls back to naming the item `$item` and every `x.*` reference in the subtree
450
+ * resolves to undefined — the rows render empty with no error.
451
+ */
452
+ function assertRepeatVariablePreserved(root, pageDir) {
453
+ const offenders = [];
454
+ const walk = node => {
455
+ if (!node || typeof node !== 'object') return;
456
+ const repeat = node.repeat;
457
+ if (repeat && !Array.isArray(repeat) && typeof repeat === 'object') {
458
+ if (typeof repeat.value !== 'string' || !repeat.value) {
459
+ offenders.push(`<${node.type ?? '?'} repeat.$exp="${repeat['$exp']}">`);
460
+ }
461
+ }
462
+ if (Array.isArray(node.children)) node.children.forEach(walk);
463
+ };
464
+ walk(root);
465
+ if (offenders.length) {
466
+ throw new Error(`[template] ${offenders.length} repeat binding(s) lost the declared loop variable` + `${pageDir ? ` in ${pageDir}` : ''}:\n` + offenders.map(o => ` ${o}`).join('\n'));
467
+ }
468
+ }
262
469
  function tryEvalStaticShowLiteral(expr) {
263
470
  if (/^'[^']*'$/.test(expr) || /^"[^"]*"$/.test(expr)) {
264
471
  const inner = expr.slice(1, -1);
@@ -588,11 +795,18 @@ function elementToJson(el, styleSheet, importMap, scopeVars, pageDir) {
588
795
  if (name === 'class') {
589
796
  className = value.replace(/\s+/g, ' ').trim();
590
797
  } else if (name.startsWith('@') || name.startsWith('on') && name.length > 2) {
591
- const rawEvtName = name.startsWith('@') ? name.slice(1) : name.replace(/^on/, '');
592
- const evtName = rawEvtName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
593
- // Events with parameters use $event format with function wrapper
594
- const evtVal = value.trim();
595
- if (evtVal.includes('(')) {
798
+ const evtName = normalizeEventName(name);
799
+ // Event values may be written bare (@click="fn") or wrapped in template
800
+ // delimiters (@click="{{fn}}"); both mean the same thing, so strip the
801
+ // delimiters before classifying. Leaving them in emits `{{fn}}`, which
802
+ // runtime either fails to compile ($-form) or looks up as a method name
803
+ // literally called "{{fn}}" (bare form) — a silent dead event either way.
804
+ const evtVal = stripTemplateDelimiters(value);
805
+ // An arrow function is already a handler; wrapping it in `function (evt)`
806
+ // and appending evt to its call would both mis-bind and drop the event arg.
807
+ if (ARROW_RE.test(evtVal)) {
808
+ events[`$${evtName}`] = `function (evt) { return (${addThisPrefix(evtVal)})(evt); }`;
809
+ } else if (evtVal.includes('(')) {
596
810
  // Add evt as last parameter
597
811
  const withEvt = evtVal.replace(/\)$/, evtVal.includes('()') ? 'evt)' : ', evt)');
598
812
  events[`$${evtName}`] = `function (evt) { return ${addThisPrefix(withEvt)}; }`;
@@ -651,28 +865,18 @@ function elementToJson(el, styleSheet, importMap, scopeVars, pageDir) {
651
865
  if (inMatch) {
652
866
  // for="(key, value) in expr" syntax
653
867
  const expr = inMatch[3].trim();
654
- if (/^[a-zA-Z_$][\w$.]*$/.test(expr)) {
655
- node['repeat'] = {
656
- '$exp': expr,
657
- key: inMatch[1],
658
- value: inMatch[2]
659
- };
660
- } else {
661
- node['$repeat'] = `function () { return ${addThisPrefix(expr)}; }`;
662
- node['repeat'] = [];
663
- }
868
+ node['repeat'] = {
869
+ '$exp': forRepeatExp(expr),
870
+ key: inMatch[1],
871
+ value: inMatch[2]
872
+ };
664
873
  } else if (simpleInMatch) {
665
874
  // for="value in expr" syntax
666
875
  const expr = simpleInMatch[2].trim();
667
- if (/^[a-zA-Z_$][\w$.]*$/.test(expr)) {
668
- node['repeat'] = {
669
- '$exp': expr,
670
- value: simpleInMatch[1]
671
- };
672
- } else {
673
- node['$repeat'] = `function () { return ${addThisPrefix(expr)}; }`;
674
- node['repeat'] = [];
675
- }
876
+ node['repeat'] = {
877
+ '$exp': forRepeatExp(expr),
878
+ value: simpleInMatch[1]
879
+ };
676
880
  } else if (pathTestRE.test(forVal)) {
677
881
  node['$repeat'] = forVal;
678
882
  node['repeat'] = [];
@@ -750,11 +954,16 @@ function elementToJson(el, styleSheet, importMap, scopeVars, pageDir) {
750
954
  } else if (/\{\{.*\}\}/.test(value)) {
751
955
  // Dynamic binding - build expression from mixed static/dynamic parts
752
956
  const parts = value.split(/(\{\{[\s\S]*?\}\})/);
753
- const exprs = parts.filter(Boolean).map(p => {
957
+ let exprs = parts.filter(Boolean).map(p => {
754
958
  const m = p.match(/^\{\{([\s\S]*?)\}\}$/);
755
959
  if (m) return addThisPrefix(m[1].trim());
756
960
  return `'${p.replace(/'/g, "\\'")}'`;
757
961
  });
962
+ // Resource-path attrs carry their path inside the expression's string
963
+ // literals; rewrite those too so dynamic src matches static src form.
964
+ if (PATH_ATTRS.has(name) && pageDir) {
965
+ exprs = exprs.map(e => rewriteRelPathLiterals(e, pageDir));
966
+ }
758
967
  const camelName = '$' + name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
759
968
  const plainName = name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
760
969
  // Handle literal values (null, true, false, numbers)
@@ -793,15 +1002,8 @@ function elementToJson(el, styleSheet, importMap, scopeVars, pageDir) {
793
1002
  // Normalize relative path attrs (src, href, alt) to absolute paths from src root.
794
1003
  // alt can also carry prose (e.g. "blank") — only rewrite when the value starts with
795
1004
  // ./ or ../ so image alt-text prose passes through untouched.
796
- const isRelPath = staticVal.startsWith('./') || staticVal.startsWith('../');
797
- if (staticVal && isRelPath && (name === 'src' || name === 'href' || name === 'alt') && pageDir) {
798
- // Resolve relative to pageDir, then make absolute from src root
799
- const segments = pageDir.split('/').filter(Boolean).concat(staticVal.split('/'));
800
- const stack = [];
801
- for (const seg of segments) {
802
- if (seg === '..') stack.pop();else if (seg && seg !== '.') stack.push(seg);
803
- }
804
- staticVal = '/' + stack.join('/');
1005
+ if (staticVal && isRelAssetPath(staticVal) && PATH_ATTRS.has(name) && pageDir) {
1006
+ staticVal = resolveRelFromPageDir(staticVal, pageDir);
805
1007
  }
806
1008
  attrObj[name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = staticVal;
807
1009
  if (name === 'id') hasBind = true;
@@ -11,9 +11,22 @@ declare class ViteCompiler implements ICompiler {
11
11
  clean(param: ICompileParam & IJavascriptCompileOption): Promise<void>;
12
12
  }
13
13
  declare function stripDeadDefProp(code: string): string;
14
+ /**
15
+ * Build a sub-component's standalone `.js`.
16
+ *
17
+ * A component is registered as its own `$app_define$` module and loaded
18
+ * independently at runtime — the host page bundle does not carry its code. So
19
+ * everything the component's `<script>` declares at module level has to survive:
20
+ * feature imports (`import vibrator from "@system.vibrator"`), consts, helpers.
21
+ * Keeping only the `export default` object literal strips the feature binding and
22
+ * leaves `vibrator` as a free identifier, which is why the 咪咕/百度地图 keyboard
23
+ * could no longer vibrate.
24
+ */
25
+ declare function buildComponentJs(compCode: string, compName: string, compSid: number, relPath: string): string;
14
26
  declare function mergeStyleSheets(app: any, page: any): any;
15
27
  export declare const __test: {
16
28
  stripDeadDefProp: typeof stripDeadDefProp;
17
29
  mergeStyleSheets: typeof mergeStyleSheets;
30
+ buildComponentJs: typeof buildComponentJs;
18
31
  };
19
32
  export default ViteCompiler;
@@ -554,11 +554,7 @@ class ViteCompiler {
554
554
  spaces: 2
555
555
  });
556
556
  const compJsPath = _path.default.join(buildPath, `${compPath}.js`);
557
- const exportMatch = compCode.match(/export\s+default\s+([\s\S]*?);?\s*$/);
558
- const compObj = exportMatch ? exportMatch[1].trim() : '{}';
559
- const compVarName = compName.replace(/-/g, '_') + '_default';
560
- const compJsContent = `let $style$${compSid} = {"@info":{"styleObjectId":${compSid}}};\nconst $app_style$${compSid} = $style$${compSid};\nvar ${compVarName} = ${compObj};\n$app_define$("@app-component/${compName}", [], function($app_require$, $app_exports$, $app_module$) {\n $app_module$.exports = ${compVarName}.default || ${compVarName};\n $app_module$.exports.style = $app_style$${compSid};\n});\n`;
561
- _fsExtra.default.writeFileSync(compJsPath, compJsContent);
557
+ _fsExtra.default.writeFileSync(compJsPath, buildComponentJs(compCode, compName, compSid, compPath));
562
558
  }
563
559
  }
564
560
  }
@@ -685,13 +681,15 @@ class ViteCompiler {
685
681
  spaces: 2
686
682
  });
687
683
  // Generate js
688
- const exportMatch = compCode.match(/export\s+default\s+([\s\S]*?);?\s*$/);
689
- const compObj = exportMatch ? exportMatch[1].trim() : '{}';
690
684
  const compName = _path.default.basename(relPath).toLowerCase();
691
- const varName = compName.replace(/-/g, '_') + '_default';
692
685
  _fsExtra.default.ensureDirSync(_path.default.dirname(outputJs));
693
- _fsExtra.default.writeFileSync(outputJs, `let $style$${sid} = {"@info":{"styleObjectId":${sid}}};\nconst $app_style$${sid} = $style$${sid};\nvar ${varName} = ${compObj};\n$app_define$("@app-component/${compName}", [], function($app_require$, $app_exports$, $app_module$) {\n $app_module$.exports = ${varName}.default || ${varName};\n $app_module$.exports.style = $app_style$${sid};\n});\n`);
694
- } catch {}
686
+ _fsExtra.default.writeFileSync(outputJs, buildComponentJs(compCode, compName, sid, relPath));
687
+ } catch (e) {
688
+ // A component that fails to emit renders but has no behaviour, so don't
689
+ // swallow the guard errors that report exactly why.
690
+ if (e?.message?.startsWith('[component]')) throw e;
691
+ console.warn(`vite: failed to emit component ${relPath}:`, e?.message ?? e);
692
+ }
695
693
  }
696
694
  }
697
695
  async clean(param) {
@@ -707,6 +705,51 @@ function stripDeadDefProp(code) {
707
705
  const withoutDefine = code.replace(/var __defProp = Object\.defineProperty;\n?/g, '');
708
706
  return /\b__defProp\s*\(/.test(withoutDefine) ? code : withoutDefine;
709
707
  }
708
+
709
+ /**
710
+ * Build a sub-component's standalone `.js`.
711
+ *
712
+ * A component is registered as its own `$app_define$` module and loaded
713
+ * independently at runtime — the host page bundle does not carry its code. So
714
+ * everything the component's `<script>` declares at module level has to survive:
715
+ * feature imports (`import vibrator from "@system.vibrator"`), consts, helpers.
716
+ * Keeping only the `export default` object literal strips the feature binding and
717
+ * leaves `vibrator` as a free identifier, which is why the 咪咕/百度地图 keyboard
718
+ * could no longer vibrate.
719
+ */
720
+ function buildComponentJs(compCode, compName, compSid, relPath) {
721
+ let code = compCode;
722
+ // The raw `var $app_style$ = [...]` array is superseded by the $app_style$<sid>
723
+ // @info reference emitted below; css.json already carries the real style data.
724
+ const styleLit = extractAppStyleLiteral(code);
725
+ if (styleLit) {
726
+ const decl = code.match(/var\s+\$app_style\$\s*=\s*/);
727
+ if (decl) {
728
+ const start = decl.index;
729
+ let end = code.indexOf(styleLit, start) + styleLit.length;
730
+ if (code[end] === ';') end++;
731
+ code = code.slice(0, start) + code.slice(end);
732
+ }
733
+ }
734
+ // Turn @system.*/@app-module imports into $app_require$ calls, same as entries.
735
+ code = rewriteSystemImports(code);
736
+ let defaultExpr = '{}';
737
+ const edIdx = code.search(/export\s+default\s+/);
738
+ if (edIdx !== -1) {
739
+ const kw = code.slice(edIdx).match(/^export\s+default\s+/);
740
+ defaultExpr = code.slice(edIdx + kw[0].length).trim().replace(/;\s*$/, '') || '{}';
741
+ code = code.slice(0, edIdx);
742
+ }
743
+ // Named exports have no consumer in a $app_define$ module; keep the declaration.
744
+ code = code.replace(/\bexport\s+(?=(?:const|let|var|function|class|async)\b)/g, '');
745
+ const leftoverImport = code.match(/^\s*import\s[^\n]*/m);
746
+ if (leftoverImport) {
747
+ throw new Error(`[component] ${relPath} has an unresolved ESM import that cannot be emitted into a ` + `standalone $app_define$ module: ${leftoverImport[0].trim()}\n` + `Only @system.*/@service.*/@android.*/@hap.*/@app-module imports are rewritten to ` + `$app_require$. Inline the dependency into the component or move it behind a feature module.`);
748
+ }
749
+ const varName = compName.replace(/-/g, '_') + '_default';
750
+ const moduleCode = code.trim();
751
+ return [`let $style$${compSid} = {"@info":{"styleObjectId":${compSid}}};`, `const $app_style$${compSid} = $style$${compSid};`, ...(moduleCode ? [moduleCode] : []), `var ${varName} = ${defaultExpr};`, `$app_define$("@app-component/${compName}", [], function($app_require$, $app_exports$, $app_module$) {`, ` $app_module$.exports = ${varName}.default || ${varName};`, ` $app_module$.exports.style = $app_style$${compSid};`, `});`, ''].join('\n');
752
+ }
710
753
  function wrapOutput(code, entryName, isApp) {
711
754
  const componentName = _path.default.parse(entryName).name;
712
755
  const defineId = isApp ? '@app-component/app' : `@app-component/${componentName}`;
@@ -831,6 +874,7 @@ function mergeStyleSheets(app, page) {
831
874
  }
832
875
  const __test = exports.__test = {
833
876
  stripDeadDefProp,
834
- mergeStyleSheets
877
+ mergeStyleSheets,
878
+ buildComponentJs
835
879
  };
836
880
  var _default = exports.default = ViteCompiler;
@@ -8,6 +8,7 @@ declare class Jsc {
8
8
  private static listFiles;
9
9
  private assertJscVersion;
10
10
  static wrapAllJsWithIife(buildPath: string): void;
11
+ private static assertLegacyGlobalBinding;
11
12
  private static compareSemver;
12
13
  }
13
14
  export default Jsc;
@@ -10,7 +10,15 @@ var _sharedUtils = require("@aiot-toolkit/shared-utils");
10
10
  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); }
11
11
  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; }
12
12
  const REQUIRED_JSC_VERSION = '1.0.9';
13
- const IIFE_WRAP_PREFIX = 'export default (function (__ctx) {\n' + ' let { requireModule, unRequireModule, $app_define$, $app_bootstrap$, $app_require$,\n' + ' console = global.console } = __ctx;\n' + ' let { setTimeout, clearTimeout, setInterval, clearInterval } = __ctx;\n\n';
13
+
14
+ // `__ctx` is the page scope whose prototype is `ferryGlobal` (where the runtime
15
+ // registers `runGC` and friends). A bare `global` in legacy page source resolves
16
+ // to QuickJS's real global object, which does NOT carry those APIs — hence
17
+ // `global.runGC()` throws `TypeError: not a function`. Bind `global`/`window`/`self`
18
+ // to `__ctx` so `global.xxx` walks `__ctx → ferryGlobal`. `globalThis` is left
19
+ // untouched (still the real global), so `console` must default from `globalThis`,
20
+ // not `global` — after the rebind `global.console` would look up ferryGlobal.
21
+ const IIFE_WRAP_PREFIX = 'export default (function (__ctx) {\n' + ' const global = __ctx, window = __ctx, self = __ctx;\n' + ' let { requireModule, unRequireModule, $app_define$, $app_bootstrap$, $app_require$,\n' + ' console = globalThis.console } = __ctx;\n' + ' let { setTimeout, clearTimeout, setInterval, clearInterval } = __ctx;\n\n';
14
22
  const IIFE_WRAP_SUFFIX = '\n});\n';
15
23
  class Jsc {
16
24
  constructor(projectPath, buildPath) {
@@ -150,7 +158,25 @@ class Jsc {
150
158
  const abs = path.join(buildPath, rel);
151
159
  const body = fs.readFileSync(abs, 'utf-8');
152
160
  if (body.startsWith('export default (function (__ctx)')) continue;
153
- fs.writeFileSync(abs, IIFE_WRAP_PREFIX + body + IIFE_WRAP_SUFFIX, 'utf-8');
161
+ const wrapped = IIFE_WRAP_PREFIX + body + IIFE_WRAP_SUFFIX;
162
+ Jsc.assertLegacyGlobalBinding(rel, wrapped);
163
+ fs.writeFileSync(abs, wrapped, 'utf-8');
164
+ }
165
+ }
166
+
167
+ // Fail the build if a Component-JS page keeps a bare `global`/`window`/`self`
168
+ // member access but the wrapper lost its compat alias — otherwise the page
169
+ // ships and only blows up at runtime (`global.runGC is not a function` →
170
+ // unhandled promise rejection stuck on the loading page). The alias is emitted
171
+ // unconditionally by IIFE_WRAP_PREFIX, so this trips only if that prefix
172
+ // regresses, surfacing the break at pack time instead of on-device.
173
+ static assertLegacyGlobalBinding(rel, wrapped) {
174
+ const usesLegacyGlobal = /\b(global|window|self)\s*\./.test(wrapped);
175
+ if (!usesLegacyGlobal) return;
176
+ const hasAlias = /\bconst global = __ctx, window = __ctx, self = __ctx;/.test(wrapped);
177
+ if (!hasAlias) {
178
+ _sharedUtils.ColorConsole.throw(`[jsc] ${rel} references bare global/window/self but the IIFE wrapper is ` + `missing the \`const global = __ctx, window = __ctx, self = __ctx\` compat ` + `alias. Such a page would throw at runtime (e.g. global.runGC is not a ` + `function) and hang on the loading page.`);
179
+ throw new Error(`[jsc] ${rel}: missing legacy global compat alias in wrapper`);
154
180
  }
155
181
  }
156
182
  static compareSemver(a, b) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiot-toolkit/aiotpack",
3
- "version": "2.1.0-prender.13",
3
+ "version": "2.1.0-prender.15",
4
4
  "description": "The process tool for packaging aiot projects.",
5
5
  "keywords": [
6
6
  "aiotpack"
@@ -19,16 +19,16 @@
19
19
  "test": "node ./__tests__/aiotpack.test.js"
20
20
  },
21
21
  "dependencies": {
22
- "@aiot-toolkit/generator": "2.1.0-prender.13",
23
- "@aiot-toolkit/parser": "2.1.0-prender.13",
24
- "@aiot-toolkit/shared-utils": "2.1.0-prender.13",
22
+ "@aiot-toolkit/generator": "2.1.0-prender.15",
23
+ "@aiot-toolkit/parser": "2.1.0-prender.15",
24
+ "@aiot-toolkit/shared-utils": "2.1.0-prender.15",
25
25
  "@hap-toolkit/aaptjs": "^2.0.0",
26
26
  "@rspack/core": "^1.3.9",
27
27
  "acorn": "^8.16.0",
28
28
  "aiot-parse5": "^1.0.2",
29
29
  "astring": "^1.9.0",
30
30
  "babel-loader": "^9.1.3",
31
- "file-lane": "2.1.0-prender.13",
31
+ "file-lane": "2.1.0-prender.15",
32
32
  "file-loader": "^6.2.0",
33
33
  "fs-extra": "^11.2.0",
34
34
  "jsrsasign": "^11.1.0",