@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.
- package/lib/compiler/javascript/TemplateCompiler.d.ts +8 -1
- package/lib/compiler/javascript/TemplateCompiler.js +236 -34
- package/lib/compiler/javascript/ViteCompiler.d.ts +13 -0
- package/lib/compiler/javascript/ViteCompiler.js +55 -11
- package/lib/compiler/javascript/vela/utils/Jsc.d.ts +1 -0
- package/lib/compiler/javascript/vela/utils/Jsc.js +28 -2
- package/package.json +5 -5
|
@@ -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
|
|
592
|
-
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
797
|
-
|
|
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
|
-
|
|
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,
|
|
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;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
23
|
-
"@aiot-toolkit/parser": "2.1.0-prender.
|
|
24
|
-
"@aiot-toolkit/shared-utils": "2.1.0-prender.
|
|
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.
|
|
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",
|