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

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.
@@ -3,6 +3,48 @@ import ICompileParam from '../interface/ICompileParam';
3
3
  import ICompiler from '../interface/ICompiler';
4
4
  import { ILog } from '@aiot-toolkit/shared-utils';
5
5
  import { IFileLaneContext } from 'file-lane';
6
+ /**
7
+ * Resolve an `<import src>` value to an absolute `.ux` path, accepting the
8
+ * extensionless and directory forms the loader also accepts.
9
+ */
10
+ declare function resolveImportSrc(fromUxFile: string, src: string): string | null;
11
+ /**
12
+ * `.ux` files reachable from the routed pages, following `<import src>` chains.
13
+ *
14
+ * aiot-toolkit 1.x only compiled routed pages (components were bundled into the
15
+ * host page), so a `.ux` that no route reaches never produced output. 2.x walks
16
+ * every `.ux` on disk, which ships dead files and surfaces their defects as
17
+ * build failures — prerender-apps/idiom has an unrouted `pages/index` whose
18
+ * leftover `import page from './index.js'` failed the build for a page no route
19
+ * can open.
20
+ */
21
+ declare function collectReachableUx(entryUxFiles: string[]): Set<string>;
22
+ /**
23
+ * Relative ESM imports a sub-component declares that nothing downstream can
24
+ * resolve: `.ux` imports are loader-generated and dropped later, and
25
+ * `@system.*`/`@app-module` imports become `$app_require$` calls.
26
+ */
27
+ declare function findUnbundledRelativeImports(code: string): string[];
28
+ /**
29
+ * Inline a sub-component's relative `.js`/`.ts` dependencies.
30
+ *
31
+ * An entry page is bundled by rolldown, so `import page from './index.js'`
32
+ * is inlined. A sub-component was instead compiled file-by-file by
33
+ * `UxLoaderUtils.compileUxToJavascript` with no bundler, leaving the statement
34
+ * in place — fatal, because the IIFE wrapper nests it in a function body. Run
35
+ * the same rolldown pipeline over the compiled component so such a dependency
36
+ * is resolved the way a page's is.
37
+ *
38
+ * The entry is a virtual module placed in the component's own directory, so
39
+ * relative specifiers resolve against the component without writing to disk.
40
+ * Components with no such import return unchanged, keeping the common case free.
41
+ */
42
+ declare function bundleComponentCode(compCode: string, uxFile: string, srcPath: string, param: IJavascriptCompileOption, userVite: {
43
+ define?: Record<string, string>;
44
+ plugins?: any[];
45
+ resolve?: any;
46
+ build?: any;
47
+ }): Promise<string>;
6
48
  declare class ViteCompiler implements ICompiler {
7
49
  private readonly context;
8
50
  private readonly onLog?;
@@ -22,11 +64,57 @@ declare function stripDeadDefProp(code: string): string;
22
64
  * leaves `vibrator` as a free identifier, which is why the 咪咕/百度地图 keyboard
23
65
  * could no longer vibrate.
24
66
  */
67
+ /**
68
+ * `<import name="comp-score" src="../Score/index.ux">` is compiled by the ux
69
+ * loader into `import comp_score_comp from '../Score/index.ux'`, but a
70
+ * sub-component is resolved purely through data: template.json carries
71
+ * `{type, import}` and the component's own `.js` registers
72
+ * `$app_define$("@app-component/<name>")`. The binding is never read, and
73
+ * leaving the statement in place is a hard error — the IIFE wrapper nests it
74
+ * inside a function body, where `import` is a syntax error.
75
+ *
76
+ * Only drop it when the binding is provably unused; a referenced binding means
77
+ * the loader contract changed and silently dropping it would reintroduce the
78
+ * free-identifier class of bug, so leave it for the caller's guard to report.
79
+ */
80
+ declare function dropUnusedUxImports(code: string): string;
25
81
  declare function buildComponentJs(compCode: string, compName: string, compSid: number, relPath: string): string;
82
+ /**
83
+ * Normalize rolldown output: drop its region markers and runtime helpers, and
84
+ * turn surviving `require("@system.*")` calls into `$app_require$`.
85
+ */
86
+ declare function stripRolldownArtifacts(code: string): string;
87
+ /**
88
+ * Move synthesized namespace objects to the top of the flattened scope.
89
+ *
90
+ * The `import * as ns` interop bridge reads `ns_exports` from a top-level statement,
91
+ * so with an import cycle DFS post-order can leave the bridge ahead of the object:
92
+ * `var` assignment is not hoisted, so it reads undefined and throws before any
93
+ * `$app_define$` runs. Only getter-only literals move — they have no side effects and
94
+ * resolve lazily, so relocating one cannot snapshot an undefined or reorder an effect.
95
+ */
96
+ declare function hoistNamespaceObjects(code: string): string;
97
+ /** Calls through a surviving thunk table are silently dropped, so refuse to emit one. */
98
+ declare function assertNamespaceObjectsInlined(code: string): void;
99
+ /**
100
+ * Reject a top-level `var` whose eager initializer reads an identifier assigned by a
101
+ * later top-level `var` — the read yields undefined, which is how an import cycle
102
+ * surfaces once module bodies are flattened into one scope.
103
+ */
104
+ declare function assertNoEagerForwardRead(code: string): void;
26
105
  declare function mergeStyleSheets(app: any, page: any): any;
27
106
  export declare const __test: {
28
107
  stripDeadDefProp: typeof stripDeadDefProp;
29
108
  mergeStyleSheets: typeof mergeStyleSheets;
30
109
  buildComponentJs: typeof buildComponentJs;
110
+ dropUnusedUxImports: typeof dropUnusedUxImports;
111
+ collectReachableUx: typeof collectReachableUx;
112
+ resolveImportSrc: typeof resolveImportSrc;
113
+ findUnbundledRelativeImports: typeof findUnbundledRelativeImports;
114
+ bundleComponentCode: typeof bundleComponentCode;
115
+ stripRolldownArtifacts: typeof stripRolldownArtifacts;
116
+ hoistNamespaceObjects: typeof hoistNamespaceObjects;
117
+ assertNamespaceObjectsInlined: typeof assertNamespaceObjectsInlined;
118
+ assertNoEagerForwardRead: typeof assertNoEagerForwardRead;
31
119
  };
32
120
  export default ViteCompiler;
@@ -216,6 +216,57 @@ function extractLegacyDefinePluginEntries(projectPath, configPath, mode) {
216
216
  } catch {/* swallow config load errors */}
217
217
  return captured;
218
218
  }
219
+
220
+ /**
221
+ * Resolve an `<import src>` value to an absolute `.ux` path, accepting the
222
+ * extensionless and directory forms the loader also accepts.
223
+ */
224
+ function resolveImportSrc(fromUxFile, src) {
225
+ const base = _path.default.resolve(_path.default.dirname(fromUxFile), src);
226
+ for (const cand of [base, `${base}.ux`, _path.default.join(base, 'index.ux')]) {
227
+ if (cand.endsWith('.ux') && _fsExtra.default.existsSync(cand)) return cand;
228
+ }
229
+ return null;
230
+ }
231
+
232
+ /**
233
+ * `.ux` files reachable from the routed pages, following `<import src>` chains.
234
+ *
235
+ * aiot-toolkit 1.x only compiled routed pages (components were bundled into the
236
+ * host page), so a `.ux` that no route reaches never produced output. 2.x walks
237
+ * every `.ux` on disk, which ships dead files and surfaces their defects as
238
+ * build failures — prerender-apps/idiom has an unrouted `pages/index` whose
239
+ * leftover `import page from './index.js'` failed the build for a page no route
240
+ * can open.
241
+ */
242
+ function collectReachableUx(entryUxFiles) {
243
+ const parse5 = require('aiot-parse5');
244
+ const reachable = new Set();
245
+ const worklist = [...entryUxFiles];
246
+ while (worklist.length) {
247
+ const uxFile = worklist.pop();
248
+ const key = _path.default.resolve(uxFile);
249
+ if (reachable.has(key)) continue;
250
+ if (!_fsExtra.default.existsSync(key)) continue;
251
+ reachable.add(key);
252
+ let doc;
253
+ try {
254
+ doc = parse5.parseFragment(_fsExtra.default.readFileSync(key, 'utf-8'), {
255
+ scriptingEnabled: false
256
+ });
257
+ } catch {
258
+ continue;
259
+ }
260
+ for (const node of doc.childNodes ?? []) {
261
+ if (node.nodeName !== 'import') continue;
262
+ const srcAttr = node.attrs?.find(a => a.name === 'src');
263
+ if (!srcAttr) continue;
264
+ const resolved = resolveImportSrc(key, srcAttr.value);
265
+ if (resolved) worklist.push(resolved);
266
+ }
267
+ }
268
+ return reachable;
269
+ }
219
270
  function extractAppStyleLiteral(code) {
220
271
  const m = code.match(/var\s+\$app_style\$\s*=\s*\[/);
221
272
  if (!m) return null;
@@ -243,6 +294,161 @@ function extractAppStyleLiteral(code) {
243
294
  }
244
295
  return null;
245
296
  }
297
+ /** Single dynamic-import site for vite, whose types this tsconfig cannot see. */
298
+ async function loadViteBuild() {
299
+ const {
300
+ build
301
+ } = await import('vite');
302
+ return build;
303
+ }
304
+
305
+ /**
306
+ * Plugins shared by the page-entry build and the sub-component bundler, so a
307
+ * component's dependencies are transformed exactly like a page's.
308
+ */
309
+ function makeSharedPlugins(srcPath) {
310
+ return [{
311
+ name: 'image-asset-string',
312
+ enforce: 'pre',
313
+ load(id) {
314
+ // require("./x.png") in .ux <script> must resolve to a path string,
315
+ // matching webpack file-loader / vivo blueos-pack semantics. Without
316
+ // this hook rolldown treats each image as an ES module and emits
317
+ // `(init_x(), __toCommonJS(x_exports))` — the site assigns
318
+ // `{ default: fn }` to src and runtime cannot render.
319
+ const match = id.match(/\.(png|jpe?g|gif|svg|webp|bmp|ico)(\?.*)?$/i);
320
+ if (!match) return null;
321
+ const filePath = id.replace(/\?.*$/, '');
322
+ if (!_fsExtra.default.existsSync(filePath)) return null;
323
+ const relFromSrc = _path.default.relative(srcPath, filePath).split(_path.default.sep).join('/');
324
+ // Assets outside src/ (typically node_modules) fall through to the
325
+ // default asset handler — the caller is responsible for their path
326
+ // resolution. Vivo blueos-pack + webpack file-loader use rpk-root
327
+ // absolute paths (`/common/...`, `/dynamicAssets/name-hash.png`);
328
+ // runtime <image src> resolves against the rpk root, so relative
329
+ // `./common/...` from a nested page directory would misdirect.
330
+ if (relFromSrc.startsWith('..')) return null;
331
+ const runtimePath = '/' + relFromSrc;
332
+ return `module.exports = ${JSON.stringify(runtimePath)};`;
333
+ }
334
+ }, {
335
+ name: "cjs-shim",
336
+ enforce: "pre",
337
+ load(id) {
338
+ if (id.includes('node_modules') && !id.endsWith('.ux')) {
339
+ const content = _fsExtra.default.readFileSync(id, 'utf-8');
340
+ if (content.includes('module.exports') || content.includes('exports.')) {
341
+ return `var module = {exports:{}}; var exports = module.exports;\n${content}\nexport default module.exports;`;
342
+ }
343
+ }
344
+ return null;
345
+ }
346
+ }, {
347
+ name: 'system-to-app-require',
348
+ enforce: 'pre',
349
+ transform(code, id) {
350
+ const out = rewriteSystemImports(code);
351
+ return out !== code ? {
352
+ code: out,
353
+ map: null
354
+ } : null;
355
+ }
356
+ }, {
357
+ name: 'namespace-preserve',
358
+ enforce: 'pre',
359
+ transform(code, id) {
360
+ if (!/import\s*\*\s*as\s+\w+\s+from/.test(code)) return null;
361
+ // Rolldown resolves `import * as L from './x'; L.foo()` at compile time —
362
+ // if `foo` isn't exported by `./x` the call collapses to `(void 0)()` and
363
+ // the source identifier is lost. Rewriting the alias to a runtime variable
364
+ // (with webpack-style default-interop) preserves the identifier so runtime
365
+ // errors point at the actual source name.
366
+ const out = code.replace(/import\s*\*\s*as\s+(\w+)\s+from\s+(['"][^'"]+['"])/g, (_m, local, src) => {
367
+ const ns = `__${local}_ns`,
368
+ key = `__${local}_key`;
369
+ return `import * as ${ns} from ${src}; const ${key} = 'default'; const ${local} = ${ns}[${key}] != null ? ${ns}[${key}] : ${ns};`;
370
+ });
371
+ return out !== code ? {
372
+ code: out,
373
+ map: null
374
+ } : null;
375
+ }
376
+ }];
377
+ }
378
+
379
+ /**
380
+ * Relative ESM imports a sub-component declares that nothing downstream can
381
+ * resolve: `.ux` imports are loader-generated and dropped later, and
382
+ * `@system.*`/`@app-module` imports become `$app_require$` calls.
383
+ */
384
+ function findUnbundledRelativeImports(code) {
385
+ const out = [];
386
+ const re = /^[ \t]*import\s[^\n]*?\sfrom\s+(['"])(\.[^'"]*)\1/gm;
387
+ for (const m of code.matchAll(re)) {
388
+ if (!m[2].endsWith('.ux')) out.push(m[2]);
389
+ }
390
+ return out;
391
+ }
392
+
393
+ /**
394
+ * Inline a sub-component's relative `.js`/`.ts` dependencies.
395
+ *
396
+ * An entry page is bundled by rolldown, so `import page from './index.js'`
397
+ * is inlined. A sub-component was instead compiled file-by-file by
398
+ * `UxLoaderUtils.compileUxToJavascript` with no bundler, leaving the statement
399
+ * in place — fatal, because the IIFE wrapper nests it in a function body. Run
400
+ * the same rolldown pipeline over the compiled component so such a dependency
401
+ * is resolved the way a page's is.
402
+ *
403
+ * The entry is a virtual module placed in the component's own directory, so
404
+ * relative specifiers resolve against the component without writing to disk.
405
+ * Components with no such import return unchanged, keeping the common case free.
406
+ */
407
+ async function bundleComponentCode(compCode, uxFile, srcPath, param, userVite) {
408
+ if (findUnbundledRelativeImports(compCode).length === 0) return compCode;
409
+ const virtualId = _path.default.join(_path.default.dirname(uxFile), `__component_entry__${_path.default.basename(uxFile)}.js`);
410
+ const build = await loadViteBuild();
411
+ const result = await build({
412
+ root: _path.default.dirname(uxFile),
413
+ logLevel: 'silent',
414
+ define: userVite.define ?? {},
415
+ resolve: {
416
+ extensions: ['.js', '.ts', '.ux', '.json'],
417
+ ...(userVite.resolve ?? {})
418
+ },
419
+ build: {
420
+ write: false,
421
+ lib: {
422
+ entry: virtualId,
423
+ formats: ['es'],
424
+ fileName: () => 'component.js'
425
+ },
426
+ rollupOptions: {
427
+ external: id => id.startsWith('@app-module/') || /^@(system|service|android|hap)\./.test(id)
428
+ },
429
+ commonjsOptions: {
430
+ transformMixedEsModules: true
431
+ },
432
+ minify: false
433
+ },
434
+ plugins: [{
435
+ name: 'component-virtual-entry',
436
+ enforce: 'pre',
437
+ resolveId(source) {
438
+ return source === virtualId ? virtualId : null;
439
+ },
440
+ load(id) {
441
+ return id === virtualId ? compCode : null;
442
+ }
443
+ }, ...makeSharedPlugins(srcPath), ...(userVite.plugins ?? [])]
444
+ });
445
+ const outputs = Array.isArray(result) ? result[0]?.output : result?.output;
446
+ const code = outputs?.[0]?.code;
447
+ if (!code) {
448
+ throw new Error(`[component] ${_path.default.relative(srcPath, uxFile)} bundling produced no output; ` + `its relative import(s) ${findUnbundledRelativeImports(compCode).join(', ')} could not be inlined.`);
449
+ }
450
+ return code;
451
+ }
246
452
  class ViteCompiler {
247
453
  constructor(context, onLog) {
248
454
  this.context = context;
@@ -290,9 +496,7 @@ class ViteCompiler {
290
496
  entries: allEntryPaths
291
497
  };
292
498
  try {
293
- const {
294
- build
295
- } = await import('vite');
499
+ const build = await loadViteBuild();
296
500
  const result = await build({
297
501
  root: originalProjectPath,
298
502
  logLevel: 'silent',
@@ -341,73 +545,7 @@ class ViteCompiler {
341
545
  }
342
546
  }
343
547
  },
344
- plugins: [{
345
- name: 'image-asset-string',
346
- enforce: 'pre',
347
- load(id) {
348
- // require("./x.png") in .ux <script> must resolve to a path string,
349
- // matching webpack file-loader / vivo blueos-pack semantics. Without
350
- // this hook rolldown treats each image as an ES module and emits
351
- // `(init_x(), __toCommonJS(x_exports))` — the site assigns
352
- // `{ default: fn }` to src and runtime cannot render.
353
- const match = id.match(/\.(png|jpe?g|gif|svg|webp|bmp|ico)(\?.*)?$/i);
354
- if (!match) return null;
355
- const filePath = id.replace(/\?.*$/, '');
356
- if (!_fsExtra.default.existsSync(filePath)) return null;
357
- const relFromSrc = _path.default.relative(srcPath, filePath).split(_path.default.sep).join('/');
358
- // Assets outside src/ (typically node_modules) fall through to the
359
- // default asset handler — the caller is responsible for their path
360
- // resolution. Vivo blueos-pack + webpack file-loader use rpk-root
361
- // absolute paths (`/common/...`, `/dynamicAssets/name-hash.png`);
362
- // runtime <image src> resolves against the rpk root, so relative
363
- // `./common/...` from a nested page directory would misdirect.
364
- if (relFromSrc.startsWith('..')) return null;
365
- const runtimePath = '/' + relFromSrc;
366
- return `module.exports = ${JSON.stringify(runtimePath)};`;
367
- }
368
- }, {
369
- name: "cjs-shim",
370
- enforce: "pre",
371
- load(id) {
372
- if (id.includes('node_modules') && !id.endsWith('.ux')) {
373
- const content = _fsExtra.default.readFileSync(id, 'utf-8');
374
- if (content.includes('module.exports') || content.includes('exports.')) {
375
- return `var module = {exports:{}}; var exports = module.exports;\n${content}\nexport default module.exports;`;
376
- }
377
- }
378
- return null;
379
- }
380
- }, {
381
- name: 'system-to-app-require',
382
- enforce: 'pre',
383
- transform(code, id) {
384
- const out = rewriteSystemImports(code);
385
- return out !== code ? {
386
- code: out,
387
- map: null
388
- } : null;
389
- }
390
- }, {
391
- name: 'namespace-preserve',
392
- enforce: 'pre',
393
- transform(code, id) {
394
- if (!/import\s*\*\s*as\s+\w+\s+from/.test(code)) return null;
395
- // Rolldown resolves `import * as L from './x'; L.foo()` at compile time —
396
- // if `foo` isn't exported by `./x` the call collapses to `(void 0)()` and
397
- // the source identifier is lost. Rewriting the alias to a runtime variable
398
- // (with webpack-style default-interop) preserves the identifier so runtime
399
- // errors point at the actual source name.
400
- const out = code.replace(/import\s*\*\s*as\s+(\w+)\s+from\s+(['"][^'"]+['"])/g, (_m, local, src) => {
401
- const ns = `__${local}_ns`,
402
- key = `__${local}_key`;
403
- return `import * as ${ns} from ${src}; const ${key} = 'default'; const ${local} = ${ns}[${key}] != null ? ${ns}[${key}] : ${ns};`;
404
- });
405
- return out !== code ? {
406
- code: out,
407
- map: null
408
- } : null;
409
- }
410
- }, {
548
+ plugins: [...makeSharedPlugins(srcPath), {
411
549
  name: 'ux-loader',
412
550
  resolveId(source, importer) {
413
551
  if (source.endsWith('.ux')) {
@@ -554,7 +692,9 @@ class ViteCompiler {
554
692
  spaces: 2
555
693
  });
556
694
  const compJsPath = _path.default.join(buildPath, `${compPath}.js`);
557
- _fsExtra.default.writeFileSync(compJsPath, buildComponentJs(compCode, compName, compSid, compPath));
695
+ // Style data was already read off the unbundled code above.
696
+ const compBundled = await bundleComponentCode(compCode, compUxPath, srcPath, param, userVite);
697
+ _fsExtra.default.writeFileSync(compJsPath, buildComponentJs(compBundled, compName, compSid, compPath));
558
698
  }
559
699
  }
560
700
  }
@@ -615,9 +755,15 @@ class ViteCompiler {
615
755
  };
616
756
  findUx(srcPath);
617
757
  const compiledEntries = new Set(Object.values(entries).map(v => _path.default.resolve(originalProjectPath, v.split('?')[0])));
758
+ const reachableUx = collectReachableUx([...compiledEntries]);
759
+ const skippedUx = [];
618
760
  for (const uxFile of allUxFiles) {
619
761
  if (compiledEntries.has(uxFile)) continue; // already compiled as entry
620
762
  if (uxFile.replace(/\\/g, '/').endsWith('/app.ux')) continue; // app.ux handled separately
763
+ if (!reachableUx.has(_path.default.resolve(uxFile))) {
764
+ skippedUx.push(_path.default.relative(srcPath, uxFile).replace(/\\/g, '/'));
765
+ continue;
766
+ }
621
767
  const relPath = _path.default.relative(srcPath, uxFile).replace(/\.ux$/, '');
622
768
  const outputJs = _path.default.join(buildPath, `${relPath}.js`);
623
769
  const outputTpl = _path.default.join(buildPath, `${relPath}.template.json`);
@@ -683,7 +829,8 @@ class ViteCompiler {
683
829
  // Generate js
684
830
  const compName = _path.default.basename(relPath).toLowerCase();
685
831
  _fsExtra.default.ensureDirSync(_path.default.dirname(outputJs));
686
- _fsExtra.default.writeFileSync(outputJs, buildComponentJs(compCode, compName, sid, relPath));
832
+ const bundled = await bundleComponentCode(compCode, uxFile, srcPath, param, userVite);
833
+ _fsExtra.default.writeFileSync(outputJs, buildComponentJs(bundled, compName, sid, relPath));
687
834
  } catch (e) {
688
835
  // A component that fails to emit renders but has no behaviour, so don't
689
836
  // swallow the guard errors that report exactly why.
@@ -691,6 +838,12 @@ class ViteCompiler {
691
838
  console.warn(`vite: failed to emit component ${relPath}:`, e?.message ?? e);
692
839
  }
693
840
  }
841
+ if (skippedUx.length) {
842
+ this.onLog?.([{
843
+ level: 3,
844
+ message: [`Skipped ${skippedUx.length} .ux file(s) not reachable from any route or <import> chain ` + `(matches aiot-toolkit 1.x, which only compiled routed pages): ${skippedUx.join(', ')}. ` + `If one of these is opened by a dynamic router call, add it to manifest.json router.pages.`]
845
+ }]);
846
+ }
694
847
  }
695
848
  async clean(param) {
696
849
  const {
@@ -717,6 +870,27 @@ function stripDeadDefProp(code) {
717
870
  * leaves `vibrator` as a free identifier, which is why the 咪咕/百度地图 keyboard
718
871
  * could no longer vibrate.
719
872
  */
873
+ /**
874
+ * `<import name="comp-score" src="../Score/index.ux">` is compiled by the ux
875
+ * loader into `import comp_score_comp from '../Score/index.ux'`, but a
876
+ * sub-component is resolved purely through data: template.json carries
877
+ * `{type, import}` and the component's own `.js` registers
878
+ * `$app_define$("@app-component/<name>")`. The binding is never read, and
879
+ * leaving the statement in place is a hard error — the IIFE wrapper nests it
880
+ * inside a function body, where `import` is a syntax error.
881
+ *
882
+ * Only drop it when the binding is provably unused; a referenced binding means
883
+ * the loader contract changed and silently dropping it would reintroduce the
884
+ * free-identifier class of bug, so leave it for the caller's guard to report.
885
+ */
886
+ function dropUnusedUxImports(code) {
887
+ const uxImport = /^[ \t]*import\s+(\w+)\s+from\s+(['"])([^'"]*\.ux)\2[ \t]*;?[ \t]*$\n?/gm;
888
+ return code.replace(uxImport, (stmt, binding) => {
889
+ const rest = code.replace(stmt, '');
890
+ const used = new RegExp(`\\b${binding}\\b`).test(rest);
891
+ return used ? stmt : '';
892
+ });
893
+ }
720
894
  function buildComponentJs(compCode, compName, compSid, relPath) {
721
895
  let code = compCode;
722
896
  // The raw `var $app_style$ = [...]` array is superseded by the $app_style$<sid>
@@ -733,7 +907,22 @@ function buildComponentJs(compCode, compName, compSid, relPath) {
733
907
  }
734
908
  // Turn @system.*/@app-module imports into $app_require$ calls, same as entries.
735
909
  code = rewriteSystemImports(code);
736
- let defaultExpr = '{}';
910
+ const hadDefault = /export\s+default\s|\bas\s+(?:default\b|["']default["'])/.test(code);
911
+ // Bundled components come straight from rolldown, which leaves region markers
912
+ // and runtime helpers behind and re-exports the default as
913
+ // `export { X as default }` rather than `export default`.
914
+ code = stripRolldownArtifacts(code);
915
+ // Must run before the `export default` expression is sliced off, otherwise a
916
+ // binding referenced only inside that expression reads as unused.
917
+ code = dropUnusedUxImports(code);
918
+ let defaultExpr = null;
919
+ const asDefault = code.match(/export\s*\{\s*(\w+)\s+as\s+default\s*(?:,[^}]*)?\}\s*;?/);
920
+ if (asDefault) {
921
+ defaultExpr = asDefault[1];
922
+ code = code.replace(asDefault[0], '');
923
+ }
924
+ // Remaining named exports have no consumer in a $app_define$ module.
925
+ code = code.replace(/^[ \t]*export\s*\{[^}]*\}\s*;?[ \t]*$\n?/gm, '');
737
926
  const edIdx = code.search(/export\s+default\s+/);
738
927
  if (edIdx !== -1) {
739
928
  const kw = code.slice(edIdx).match(/^export\s+default\s+/);
@@ -746,37 +935,22 @@ function buildComponentJs(compCode, compName, compSid, relPath) {
746
935
  if (leftoverImport) {
747
936
  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
937
  }
938
+ if (hadDefault && defaultExpr === null) {
939
+ // An unrecognized default-export shape would register an empty component:
940
+ // it renders but has no data, props or handlers.
941
+ throw new Error(`[component] ${relPath} declares a default export but none was extracted, ` + `which would register an empty component.`);
942
+ }
749
943
  const varName = compName.replace(/-/g, '_') + '_default';
750
944
  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');
945
+ const emittedDefault = defaultExpr ?? '{}';
946
+ return [`let $style$${compSid} = {"@info":{"styleObjectId":${compSid}}};`, `const $app_style$${compSid} = $style$${compSid};`, ...(moduleCode ? [moduleCode] : []), `var ${varName} = ${emittedDefault};`, `$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
947
  }
753
- function wrapOutput(code, entryName, isApp) {
754
- const componentName = _path.default.parse(entryName).name;
755
- const defineId = isApp ? '@app-component/app' : `@app-component/${componentName}`;
756
- const bootstrapId = isApp ? '@app-application/app' : `@app-component/${componentName}`;
757
- // Remove IIFE wrapper: var _page_ = (function(...) { ... return xxx; })(...);
758
- // Only remove the FIRST line and the LAST return+closing
759
- if (code.startsWith('var _page_')) {
760
- // Remove first line (the IIFE opening)
761
- const firstNewline = code.indexOf('\n');
762
- code = code.slice(firstNewline + 1);
763
- // Remove the last return statement and closing })();
764
- const lastReturn = code.lastIndexOf('\treturn ');
765
- if (lastReturn !== -1) {
766
- const afterReturn = code.indexOf('\n', lastReturn);
767
- code = code.slice(0, lastReturn) + code.slice(afterReturn + 1);
768
- }
769
- // Remove the closing })(); or })(...)
770
- const lastClose = code.lastIndexOf('})();');
771
- if (lastClose !== -1) {
772
- code = code.slice(0, lastClose);
773
- } else {
774
- const lastClose2 = code.lastIndexOf('})(');
775
- if (lastClose2 !== -1) {
776
- code = code.slice(0, lastClose2);
777
- }
778
- }
779
- }
948
+
949
+ /**
950
+ * Normalize rolldown output: drop its region markers and runtime helpers, and
951
+ * turn surviving `require("@system.*")` calls into `$app_require$`.
952
+ */
953
+ function stripRolldownArtifacts(code) {
780
954
  // Remove rolldown region comments
781
955
  code = code.replace(/\/\/#region[^\n]*\n/g, '');
782
956
  code = code.replace(/\/\/#endregion[^\n]*/g, '');
@@ -788,8 +962,11 @@ function wrapOutput(code, entryName, isApp) {
788
962
  // Inline __exportAll calls: __exportAll({key: () => value, ...}) -> {key: value, ...}
789
963
  // Match __exportAll(...) or /* @__PURE__ */ __exportAll(...)
790
964
  code = code.replace(/(?:\/\* @__PURE__ \*\/\s*)?__exportAll\(\s*\{([\s\S]*?)\}\s*\)/g, (_match, body) => {
791
- // Replace each "key: () => value" with "get key() { return value }" for proper live binding
792
- const inlined = body.replace(/(\w+)\s*:\s*\(\)\s*=>\s*([^,\n}]+?)([,\n])/g, 'get $1() { return $2; }$3');
965
+ // A surviving thunk is not a live binding but a function returning one, so
966
+ // `ns.fn(arg)` drops the argument and returns `fn`. The lookahead (rather than
967
+ // consuming the separator) is what converts the last property, which is followed
968
+ // by the closing brace.
969
+ const inlined = body.replace(/(\w+)\s*:\s*\(\)\s*=>\s*([^,\n}]+?)\s*(?=[,\n]|$)/g, 'get $1() { return $2; }');
793
970
  return `{${inlined}}`;
794
971
  });
795
972
  // Remove Object.defineProperty for Symbol.toStringTag
@@ -817,6 +994,205 @@ function wrapOutput(code, entryName, isApp) {
817
994
  if (!/\b__require\b/.test(code)) {
818
995
  code = code.replace(/var __require\s*=\s*\/\*[^*]*\*\/\s*\(\(x\)\s*=>[\s\S]*?}\);\n?/g, '');
819
996
  }
997
+ assertNamespaceObjectsInlined(code);
998
+ code = hoistNamespaceObjects(code);
999
+ assertNoEagerForwardRead(code);
1000
+ return code;
1001
+ }
1002
+
1003
+ /** A getter-only object literal: `{ get a() { return a; }, get b() { return b; }}`. */
1004
+ function isGetterOnlyLiteral(literal) {
1005
+ const inner = literal.slice(1, -1).trim();
1006
+ if (!inner) return false;
1007
+ return inner.split(/,\s*(?=get\s)/).every(p => /^get\s+\w+\s*\(\)\s*\{\s*return\s+[^;{}]+;?\s*\}\s*,?$/.test(p.trim()));
1008
+ }
1009
+
1010
+ /**
1011
+ * Move synthesized namespace objects to the top of the flattened scope.
1012
+ *
1013
+ * The `import * as ns` interop bridge reads `ns_exports` from a top-level statement,
1014
+ * so with an import cycle DFS post-order can leave the bridge ahead of the object:
1015
+ * `var` assignment is not hoisted, so it reads undefined and throws before any
1016
+ * `$app_define$` runs. Only getter-only literals move — they have no side effects and
1017
+ * resolve lazily, so relocating one cannot snapshot an undefined or reorder an effect.
1018
+ */
1019
+ function hoistNamespaceObjects(code) {
1020
+ const hoisted = [];
1021
+ const out = code.replace(/^[ \t]*var\s+(\w+)\s*=\s*(\{[\s\S]*?\});[ \t]*$\n?/gm, (stmt, name, literal) => {
1022
+ if (!isGetterOnlyLiteral(literal)) return stmt;
1023
+ hoisted.push(`var ${name} = ${literal};`);
1024
+ return '';
1025
+ });
1026
+ return hoisted.length ? `${hoisted.join('\n')}\n${out}` : out;
1027
+ }
1028
+
1029
+ /** Calls through a surviving thunk table are silently dropped, so refuse to emit one. */
1030
+ function assertNamespaceObjectsInlined(code) {
1031
+ if (/\b__exportAll\s*\(/.test(code)) {
1032
+ throw new Error(`[js] a namespace object was emitted through the __exportAll helper instead of being ` + `inlined as getters. Calls through it are silently dropped. Offending code:\n` + code.match(/[^\n]*__exportAll\s*\([^\n]*/)[0]);
1033
+ }
1034
+ const thunk = code.match(/^[ \t]*var\s+\w+_exports\s*=\s*\{[^{}]*?\w+\s*:\s*\(\)\s*=>[^\n]*$/m);
1035
+ if (thunk) {
1036
+ throw new Error(`[js] namespace object still holds a thunk property instead of a getter; calls ` + `through it are silently dropped. Offending code:\n${thunk[0]}`);
1037
+ }
1038
+ }
1039
+
1040
+ /**
1041
+ * Reject a top-level `var` whose eager initializer reads an identifier assigned by a
1042
+ * later top-level `var` — the read yields undefined, which is how an import cycle
1043
+ * surfaces once module bodies are flattened into one scope.
1044
+ */
1045
+ function assertNoEagerForwardRead(code) {
1046
+ const stmts = topLevelVarStatements(code);
1047
+ const assignedAt = new Map();
1048
+ for (const s of stmts) if (!assignedAt.has(s.name)) assignedAt.set(s.name, s.at);
1049
+ for (const {
1050
+ name,
1051
+ init,
1052
+ at
1053
+ } of stmts) {
1054
+ if (isLazyInitializer(init)) continue;
1055
+ for (const ref of maskNonReferences(init).matchAll(/[A-Za-z_$][\w$]*/g)) {
1056
+ const refAt = assignedAt.get(ref[0]);
1057
+ if (ref[0] !== name && refAt !== undefined && refAt > at) {
1058
+ throw new Error(`[js] top-level \`var ${name}\` is evaluated before \`${ref[0]}\` is assigned ` + `(assignment appears later in the flattened scope), so it reads undefined. ` + `This is a module ordering bug, usually an import cycle.\n var ${name} = ${init};`);
1059
+ }
1060
+ }
1061
+ }
1062
+ }
1063
+ function isLazyInitializer(init) {
1064
+ // A `{` means a block or object literal, where the name may sit in a method or getter
1065
+ // body that runs later — or be a property key, which is no reference at all. Skipping
1066
+ // those trades coverage inside object literals for freedom from false positives.
1067
+ return init.includes('{') || /\bfunction\b|=>|\bclass\b/.test(init);
1068
+ }
1069
+
1070
+ /** Blank out what looks like an identifier but cannot be a binding reference. */
1071
+ function maskNonReferences(src) {
1072
+ return src.replace(/'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|`(?:\\.|[^`\\])*`/g, '""').replace(/\.\s*[A-Za-z_$][\w$]*/g, '');
1073
+ }
1074
+
1075
+ /**
1076
+ * Split top-level `var NAME = INIT;` statements.
1077
+ *
1078
+ * Needs a real scan rather than a per-line regex: initializers do span lines in bundler
1079
+ * output (array literals, multi-line template literals), and a line-based split would
1080
+ * read template-literal text as identifiers.
1081
+ */
1082
+ function topLevelVarStatements(code) {
1083
+ const out = [];
1084
+ let depth = 0;
1085
+ let i = 0;
1086
+ while (i < code.length) {
1087
+ const c = code[i];
1088
+ if (c === '{' || c === '(' || c === '[') {
1089
+ depth++;
1090
+ i++;
1091
+ continue;
1092
+ }
1093
+ if (c === '}' || c === ')' || c === ']') {
1094
+ depth--;
1095
+ i++;
1096
+ continue;
1097
+ }
1098
+ if (c === '"' || c === "'" || c === '`') {
1099
+ i = skipString(code, i);
1100
+ continue;
1101
+ }
1102
+ if (c === '/' && code[i + 1] === '/') {
1103
+ i = code.indexOf('\n', i);
1104
+ if (i === -1) break;
1105
+ continue;
1106
+ }
1107
+ if (c === '/' && code[i + 1] === '*') {
1108
+ i = code.indexOf('*/', i);
1109
+ if (i === -1) break;
1110
+ i += 2;
1111
+ continue;
1112
+ }
1113
+ if (depth === 0 && code.startsWith('var ', i) && (i === 0 || /[\s;}]/.test(code[i - 1]))) {
1114
+ const m = /^var\s+([\w$]+)\s*=\s*/.exec(code.slice(i));
1115
+ if (m) {
1116
+ const initStart = i + m[0].length;
1117
+ const end = findStatementEnd(code, initStart);
1118
+ out.push({
1119
+ name: m[1],
1120
+ init: code.slice(initStart, end).trim(),
1121
+ at: i
1122
+ });
1123
+ i = end;
1124
+ continue;
1125
+ }
1126
+ }
1127
+ i++;
1128
+ }
1129
+ return out;
1130
+ }
1131
+ function skipString(code, start) {
1132
+ const quote = code[start];
1133
+ let i = start + 1;
1134
+ while (i < code.length) {
1135
+ if (code[i] === '\\') {
1136
+ i += 2;
1137
+ continue;
1138
+ }
1139
+ if (code[i] === quote) return i + 1;
1140
+ i++;
1141
+ }
1142
+ return i;
1143
+ }
1144
+ function findStatementEnd(code, start) {
1145
+ let depth = 0;
1146
+ let i = start;
1147
+ while (i < code.length) {
1148
+ const c = code[i];
1149
+ if (c === '{' || c === '(' || c === '[') {
1150
+ depth++;
1151
+ i++;
1152
+ continue;
1153
+ }
1154
+ if (c === '}' || c === ')' || c === ']') {
1155
+ depth--;
1156
+ i++;
1157
+ continue;
1158
+ }
1159
+ if (c === '"' || c === "'" || c === '`') {
1160
+ i = skipString(code, i);
1161
+ continue;
1162
+ }
1163
+ if (depth === 0 && (c === ';' || c === '\n')) return i;
1164
+ i++;
1165
+ }
1166
+ return i;
1167
+ }
1168
+ function wrapOutput(code, entryName, isApp) {
1169
+ const componentName = _path.default.parse(entryName).name;
1170
+ const defineId = isApp ? '@app-component/app' : `@app-component/${componentName}`;
1171
+ const bootstrapId = isApp ? '@app-application/app' : `@app-component/${componentName}`;
1172
+ // Remove IIFE wrapper: var _page_ = (function(...) { ... return xxx; })(...);
1173
+ // Only remove the FIRST line and the LAST return+closing
1174
+ if (code.startsWith('var _page_')) {
1175
+ // Remove first line (the IIFE opening)
1176
+ const firstNewline = code.indexOf('\n');
1177
+ code = code.slice(firstNewline + 1);
1178
+ // Remove the last return statement and closing })();
1179
+ const lastReturn = code.lastIndexOf('\treturn ');
1180
+ if (lastReturn !== -1) {
1181
+ const afterReturn = code.indexOf('\n', lastReturn);
1182
+ code = code.slice(0, lastReturn) + code.slice(afterReturn + 1);
1183
+ }
1184
+ // Remove the closing })(); or })(...)
1185
+ const lastClose = code.lastIndexOf('})();');
1186
+ if (lastClose !== -1) {
1187
+ code = code.slice(0, lastClose);
1188
+ } else {
1189
+ const lastClose2 = code.lastIndexOf('})(');
1190
+ if (lastClose2 !== -1) {
1191
+ code = code.slice(0, lastClose2);
1192
+ }
1193
+ }
1194
+ }
1195
+ code = stripRolldownArtifacts(code);
820
1196
  // Extract the default export variable
821
1197
  let exportVar = '_default';
822
1198
  const m1 = code.match(/export\s*\{\s*(\w+)\s+as\s+default\s*\}/);
@@ -875,6 +1251,15 @@ function mergeStyleSheets(app, page) {
875
1251
  const __test = exports.__test = {
876
1252
  stripDeadDefProp,
877
1253
  mergeStyleSheets,
878
- buildComponentJs
1254
+ buildComponentJs,
1255
+ dropUnusedUxImports,
1256
+ collectReachableUx,
1257
+ resolveImportSrc,
1258
+ findUnbundledRelativeImports,
1259
+ bundleComponentCode,
1260
+ stripRolldownArtifacts,
1261
+ hoistNamespaceObjects,
1262
+ assertNamespaceObjectsInlined,
1263
+ assertNoEagerForwardRead
879
1264
  };
880
1265
  var _default = exports.default = ViteCompiler;
@@ -20,6 +20,12 @@ const REQUIRED_JSC_VERSION = '1.0.9';
20
20
  // not `global` — after the rebind `global.console` would look up ferryGlobal.
21
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';
22
22
  const IIFE_WRAP_SUFFIX = '\n});\n';
23
+
24
+ // `-s` strips debug info. `-m` (force module) is deliberately omitted: aiotjsc
25
+ // autodetects module-ness and the IIFE wrapper's `export default` already makes
26
+ // it detect module. Passing an explicit array also suppresses jsc's own
27
+ // `undefined → ["-ms"]` fallback, so this list is the full flag set.
28
+ const JSC_ARGS = ['-s'];
23
29
  class Jsc {
24
30
  constructor(projectPath, buildPath) {
25
31
  this.projectPath = projectPath;
@@ -37,7 +43,7 @@ class Jsc {
37
43
  this.assertJscVersion();
38
44
  Jsc.wrapAllJsWithIife(buildPath);
39
45
  const start = Date.now();
40
- const msg = await module.default(buildPath, []);
46
+ const msg = await module.default(buildPath, JSC_ARGS);
41
47
  const elapsed = Date.now() - start;
42
48
  if (msg) {
43
49
  _sharedUtils.ColorConsole.success({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiot-toolkit/aiotpack",
3
- "version": "2.1.0-prender.15",
3
+ "version": "2.1.0-prender.17",
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.15",
23
- "@aiot-toolkit/parser": "2.1.0-prender.15",
24
- "@aiot-toolkit/shared-utils": "2.1.0-prender.15",
22
+ "@aiot-toolkit/generator": "2.1.0-prender.17",
23
+ "@aiot-toolkit/parser": "2.1.0-prender.17",
24
+ "@aiot-toolkit/shared-utils": "2.1.0-prender.17",
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.15",
31
+ "file-lane": "2.1.0-prender.17",
32
32
  "file-loader": "^6.2.0",
33
33
  "fs-extra": "^11.2.0",
34
34
  "jsrsasign": "^11.1.0",