@openfairygui/functions 0.2.0-alpha.1 → 0.2.0-alpha.12

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/src/publish.ts CHANGED
@@ -17,21 +17,22 @@ import {
17
17
  } from '@openfairygui/core';
18
18
  import { createTransform } from './utils.js';
19
19
  import { atlas, type AtlasOptions } from './atlas.js';
20
- import { publishCodeGeneration } from './codegen.js';
20
+ import { publishCodeGeneration, resolveProjectBasePath } from './codegen.js';
21
+ import { formatPluginError, type LoadedPlugin } from './plugins/types.js';
22
+ import type { AtlasRasterBackend, PublishFileSystem } from './publish/contracts.js';
21
23
  import type {
22
24
  CliPublishSettings,
23
25
  HasOptionalFont,
24
26
  PackagePublishArtifactsExtras,
25
- PublishFileSystem,
26
27
  RootProjectSettings,
27
28
  } from './shared-types.js';
28
29
 
29
30
  export interface PublishOptions {
30
31
  /**
31
- * Output directory for published files (.fui + atlas PNGs).
32
- * Required.
32
+ * Output directory override for published files (.fui + atlas PNGs).
33
+ * When omitted, publish uses package-level or project-level publish paths.
33
34
  */
34
- output: string;
35
+ output?: string;
35
36
 
36
37
  /**
37
38
  * Compress the binary data with zlib raw deflate. Default: false.
@@ -45,10 +46,10 @@ export interface PublishOptions {
45
46
  fileExtension?: string;
46
47
 
47
48
  /**
48
- * Sharp module instance for atlas image compositing.
49
+ * Raster backend for atlas image compositing.
49
50
  * If not provided, atlas packing only computes layout (no PNGs generated).
50
51
  */
51
- encoder?: unknown;
52
+ encoder?: AtlasRasterBackend;
52
53
 
53
54
  /**
54
55
  * Base path for reading source images (project assets root).
@@ -78,9 +79,33 @@ export interface PublishOptions {
78
79
  * Empty or omitted means publishing the main branch.
79
80
  */
80
81
  branch?: string;
82
+
83
+ /**
84
+ * Publish hooks supplied by the host adapter.
85
+ *
86
+ * Node adapters load project plugins. Browser adapters pass an empty list.
87
+ */
88
+ plugins?: LoadedPlugin[];
89
+
90
+ /**
91
+ * Run generic code generation after runtime artifacts. Default: true.
92
+ */
93
+ codeGeneration?: boolean;
81
94
  }
82
95
 
83
- export interface ResolvedPublishAtlasOptions extends Pick<AtlasOptions, 'maxSize' | 'fast' | 'allowRotation' | 'padding' | 'powerOfTwo' | 'square' | 'multiPage' | 'trimImage' | 'extractAlpha'> {}
96
+ export interface ResolvedPublishAtlasOptions
97
+ extends Pick<
98
+ AtlasOptions,
99
+ | 'maxSize'
100
+ | 'fast'
101
+ | 'allowRotation'
102
+ | 'padding'
103
+ | 'powerOfTwo'
104
+ | 'square'
105
+ | 'multiPage'
106
+ | 'trimImage'
107
+ | 'extractAlpha'
108
+ > {}
84
109
 
85
110
  export interface ResolvePublishOptionsOverrides {
86
111
  compressed?: boolean;
@@ -96,6 +121,48 @@ export interface ResolvedPublishOptions {
96
121
  atlas: ResolvedPublishAtlasOptions;
97
122
  }
98
123
 
124
+ interface ResolvedProjectPublishConfig extends ResolvedPublishOptions {
125
+ projectType: number;
126
+ includeBranches: boolean;
127
+ activeBranch: string;
128
+ includeHighResolution: number;
129
+ separatedAtlasForBranch: boolean;
130
+ globalOutputPath: string;
131
+ globalBranchOutputPath: string;
132
+ }
133
+
134
+ interface ResolvedPackagePublishPlan {
135
+ pkg: Package;
136
+ outputDir?: string;
137
+ publishName: string;
138
+ fileName: string;
139
+ compressed: boolean;
140
+ fileExtension: string;
141
+ includeBranches: boolean;
142
+ activeBranch: string;
143
+ includeHighResolution: number;
144
+ separatedAtlasForBranch: boolean;
145
+ atlas: ResolvedPublishAtlasOptions;
146
+ }
147
+
148
+ async function runPublishPluginHook(
149
+ plugins: LoadedPlugin[],
150
+ hook: 'onPublishStart' | 'onPublishEnd',
151
+ doc: Document,
152
+ options: PublishOptions,
153
+ ): Promise<void> {
154
+ const logger = doc.getLogger();
155
+ for (const plugin of plugins) {
156
+ const fn = plugin.plugin[hook];
157
+ if (typeof fn !== 'function') continue;
158
+ try {
159
+ await fn(doc, options);
160
+ } catch (error) {
161
+ logger.warn(`publish: Plugin "${plugin.name}" ${hook} failed: ${formatPluginError(error)}`);
162
+ }
163
+ }
164
+ }
165
+
99
166
  interface ImageResourceExtras extends Record<string, unknown> {
100
167
  _fileName?: string;
101
168
  }
@@ -112,7 +179,9 @@ interface BranchAwarePublishedResource {
112
179
  interface PackagePublishContext {
113
180
  referencedIds: Set<string>;
114
181
  publishedResourceIds: Set<string>;
182
+ exportedResourceIds: Set<string>;
115
183
  pixelHitTestImageIds: Set<string>;
184
+ highResolutionItemIds: Map<string, Array<string | null>>;
116
185
  effectiveResourceIds: Map<string, string>;
117
186
  includeBranches: boolean;
118
187
  }
@@ -138,8 +207,10 @@ interface TransitionWithPublishRefs {
138
207
 
139
208
  interface ChildWithPublishRefs extends HasOptionalFont {
140
209
  getId?(): string;
210
+ getPackageId?(): string;
141
211
  getSrc?(): string;
142
212
  getUrl?(): string;
213
+ getInstanceSound?(): string;
143
214
  getDefaultItem?(): string;
144
215
  getIcon?(): string;
145
216
  getSelectedIcon?(): string;
@@ -172,27 +243,6 @@ interface ComponentWithPublishRefs {
172
243
  listTransitions?(): TransitionWithPublishRefs[];
173
244
  }
174
245
 
175
- interface PublishEncoderMetadata {
176
- width?: number;
177
- height?: number;
178
- channels?: number;
179
- }
180
-
181
- interface PublishEncoderResolvedBuffer {
182
- data: Uint8Array;
183
- info: Required<Pick<PublishEncoderMetadata, 'width' | 'height' | 'channels'>> & PublishEncoderMetadata;
184
- }
185
-
186
- interface PublishEncoderPipeline {
187
- ensureAlpha(): PublishEncoderPipeline;
188
- resize(options: { width: number; height: number; fit: 'fill' }): PublishEncoderPipeline;
189
- raw(): PublishEncoderPipeline;
190
- toBuffer(options: { resolveWithObject: true }): Promise<PublishEncoderResolvedBuffer>;
191
- metadata(): Promise<PublishEncoderMetadata>;
192
- }
193
-
194
- type PublishEncoder = (input: string | Uint8Array) => PublishEncoderPipeline;
195
-
196
246
  const UNITY_PROJECT_TYPE = ProjectType.Unity;
197
247
  const COCOS_CREATOR_PROJECT_TYPE = ProjectType.CocosCreator;
198
248
 
@@ -245,8 +295,7 @@ export function resolvePublishOptions(
245
295
  const atlasSetting = publishSettings.atlasSetting ?? {};
246
296
  const projectType = root.getProjectType();
247
297
 
248
- const fileExtension = overrides.fileExtension
249
- ?? resolveDefaultPublishFileExtension(projectType, publishSettings);
298
+ const fileExtension = overrides.fileExtension ?? resolveDefaultPublishFileExtension(projectType, publishSettings);
250
299
 
251
300
  let compressed = overrides.compressed ?? publishSettings.compressDesc ?? false;
252
301
  if (projectType === UNITY_PROJECT_TYPE) {
@@ -273,6 +322,23 @@ export function resolvePublishOptions(
273
322
  };
274
323
  }
275
324
 
325
+ function trimTrailingSlashes(value: string): string {
326
+ return value.replace(/[/\\]+$/, '');
327
+ }
328
+
329
+ function isAbsolutePathLike(value: string): boolean {
330
+ return /^(?:[a-zA-Z]:[/\\]|[/\\]{1,2})/u.test(value);
331
+ }
332
+
333
+ function joinPathSegments(left: string, right: string): string {
334
+ const normalizedLeft = trimTrailingSlashes(left);
335
+ const normalizedRight = right.replace(/^[/\\]+/, '');
336
+ if (!normalizedLeft) return normalizedRight;
337
+ if (!normalizedRight) return normalizedLeft;
338
+ const separator = normalizedLeft.includes('\\') ? '\\' : '/';
339
+ return `${normalizedLeft}${separator}${normalizedRight}`;
340
+ }
341
+
276
342
  function dirname(filePath: string): string {
277
343
  const trimmed = filePath.replace(/[/\\]+$/, '');
278
344
  const match = trimmed.match(/^(.*)[/\\][^/\\]+$/);
@@ -311,6 +377,12 @@ function isMovieClipResource(resource: ReturnType<Package['listResources']>[numb
311
377
  return resource.propertyType === 'MovieClipResource';
312
378
  }
313
379
 
380
+ function isHighResolutionResource(
381
+ resource: ReturnType<Package['listResources']>[number],
382
+ ): resource is ImageResource | MovieClipResource {
383
+ return isImageResource(resource) || isMovieClipResource(resource);
384
+ }
385
+
314
386
  function isMiscResource(resource: ReturnType<Package['listResources']>[number]): resource is MiscResource {
315
387
  return resource.propertyType === 'MiscResource';
316
388
  }
@@ -327,7 +399,9 @@ function isSpineResource(resource: ReturnType<Package['listResources']>[number])
327
399
  return resource.propertyType === 'SpineResource';
328
400
  }
329
401
 
330
- function isDragonBonesResource(resource: ReturnType<Package['listResources']>[number]): resource is DragonBonesResource {
402
+ function isDragonBonesResource(
403
+ resource: ReturnType<Package['listResources']>[number],
404
+ ): resource is DragonBonesResource {
331
405
  return resource.propertyType === 'DragonBonesResource';
332
406
  }
333
407
 
@@ -374,10 +448,7 @@ function addLocalFontRef(target: Set<string>, pkgId: string, value: string | str
374
448
  addLocalUiResourceRef(target, pkgId, value ?? undefined);
375
449
  }
376
450
 
377
- function resolvePackageAssetsBasePath(
378
- basePath: string,
379
- resource: BranchAwarePublishedResource | undefined,
380
- ): string {
451
+ function resolvePackageAssetsBasePath(basePath: string, resource: BranchAwarePublishedResource | undefined): string {
381
452
  const branchName = resource?.getBranch?.() ?? '';
382
453
  if (!branchName) return basePath;
383
454
  const normalized = basePath.replace(/[/\\]+$/, '');
@@ -423,14 +494,19 @@ function extname(fileName: string): string {
423
494
  return normalized.slice(lastDot);
424
495
  }
425
496
 
426
- function resolvePublishedMiscFileName(resource: MiscResource): string {
497
+ function resolvePublishedMiscFileName(resource: MiscResource, projectType: number): string {
427
498
  const file = resource.getFile();
499
+ if (projectType !== UNITY_PROJECT_TYPE) return file;
428
500
  if (file.toLowerCase().endsWith('.atlas')) return `${file}.txt`;
429
501
  return file;
430
502
  }
431
503
 
432
- function resolvePublishedSkeletonFileName(resource: SpineResource | DragonBonesResource): string {
433
- if (isSpineResource(resource) && resource.getFile().toLowerCase().endsWith('.skel')) {
504
+ function resolvePublishedSkeletonFileName(resource: SpineResource | DragonBonesResource, projectType: number): string {
505
+ if (
506
+ projectType === UNITY_PROJECT_TYPE &&
507
+ isSpineResource(resource) &&
508
+ resource.getFile().toLowerCase().endsWith('.skel')
509
+ ) {
434
510
  return `${resource.getFile()}.bytes`;
435
511
  }
436
512
  return resource.getFile();
@@ -448,7 +524,11 @@ function setPublishedFileExtra(
448
524
  }
449
525
 
450
526
  function setPublishedIdExtra(
451
- resource: { getId(): string; getExtras(): Record<string, unknown> | undefined; setExtras(value: Record<string, unknown>): unknown },
527
+ resource: {
528
+ getId(): string;
529
+ getExtras(): Record<string, unknown> | undefined;
530
+ setExtras(value: Record<string, unknown>): unknown;
531
+ },
452
532
  effectiveId: string | null,
453
533
  ): void {
454
534
  const extras = (resource.getExtras() as PublishFileExtras | undefined) ?? {};
@@ -473,19 +553,103 @@ function getBranchName(resource: BranchAwarePublishedResource | undefined): stri
473
553
  return resource?.getBranch?.() ?? '';
474
554
  }
475
555
 
476
- function buildBranchResourceKey(resource: {
477
- propertyType: string;
478
- getPath(): string;
479
- getName(): string;
480
- }): string {
556
+ function buildBranchResourceKey(resource: { propertyType: string; getPath(): string; getName(): string }): string {
481
557
  return `${resource.propertyType}|${resource.getPath() ?? ''}|${resource.getName() ?? ''}`;
482
558
  }
483
559
 
560
+ const HIGH_RESOLUTION_LEVELS = [
561
+ { scale: 2, bit: 1, slot: 0 },
562
+ { scale: 3, bit: 2, slot: 1 },
563
+ { scale: 4, bit: 4, slot: 2 },
564
+ ] as const;
565
+
566
+ function buildHighResolutionResourceKey(
567
+ resource: {
568
+ propertyType: string;
569
+ getPath(): string;
570
+ getName(): string;
571
+ getBranch?(): string;
572
+ },
573
+ name = resource.getName(),
574
+ ): string {
575
+ return `${resource.propertyType}|${resource.getBranch?.() ?? ''}|${resource.getPath() ?? ''}|${name}`;
576
+ }
577
+
578
+ function isHighResolutionVariantName(name: string): boolean {
579
+ return /@(?:2|3|4)x(?:\.[^./\\]+)?$/iu.test(name);
580
+ }
581
+
582
+ function appendHighResolutionScaleToName(name: string, scale: number): string {
583
+ const extensionIndex = name.lastIndexOf('.');
584
+ if (extensionIndex > 0) {
585
+ return `${name.slice(0, extensionIndex)}@${scale}x${name.slice(extensionIndex)}`;
586
+ }
587
+ return `${name}@${scale}x`;
588
+ }
589
+
590
+ function trimTrailingMissingHighResolutionIds(ids: Array<string | null>): Array<string | null> {
591
+ while (ids.length > 0 && !ids[ids.length - 1]) {
592
+ ids.pop();
593
+ }
594
+ return ids;
595
+ }
596
+
597
+ function collectHighResolutionItemIds(
598
+ resources: ReturnType<Package['listResources']>,
599
+ publishedResourceIds: Set<string>,
600
+ includeHighResolution: number,
601
+ ): Map<string, Array<string | null>> {
602
+ const result = new Map<string, Array<string | null>>();
603
+ if (includeHighResolution <= 0) return result;
604
+
605
+ const highResolutionResourceByKey = new Map<string, ImageResource | MovieClipResource>();
606
+ for (const resource of resources) {
607
+ if (!isHighResolutionResource(resource)) continue;
608
+ highResolutionResourceByKey.set(buildHighResolutionResourceKey(resource), resource);
609
+ }
610
+
611
+ for (const resource of resources) {
612
+ if (!isHighResolutionResource(resource)) continue;
613
+ if (!publishedResourceIds.has(resource.getId())) continue;
614
+ if (isHighResolutionVariantName(resource.getName())) continue;
615
+
616
+ const ids: Array<string | null> = [];
617
+ for (const level of HIGH_RESOLUTION_LEVELS) {
618
+ if ((includeHighResolution & level.bit) === 0) {
619
+ ids[level.slot] = null;
620
+ continue;
621
+ }
622
+
623
+ const highResolutionResource = highResolutionResourceByKey.get(
624
+ buildHighResolutionResourceKey(
625
+ resource,
626
+ appendHighResolutionScaleToName(resource.getName(), level.scale),
627
+ ),
628
+ );
629
+ if (!highResolutionResource) {
630
+ ids[level.slot] = null;
631
+ continue;
632
+ }
633
+
634
+ const highResolutionId = highResolutionResource.getId();
635
+ publishedResourceIds.add(highResolutionId);
636
+ ids[level.slot] = highResolutionId;
637
+ }
638
+
639
+ trimTrailingMissingHighResolutionIds(ids);
640
+ if (ids.length > 0) result.set(resource.getId(), ids);
641
+ }
642
+
643
+ return result;
644
+ }
645
+
484
646
  function collectPackagePublishContext(
485
647
  pkg: Package,
486
648
  options: {
649
+ projectType: number;
487
650
  includeBranches: boolean;
488
651
  activeBranch: string;
652
+ includeHighResolution: number;
489
653
  },
490
654
  ): PackagePublishContext {
491
655
  const pkgId = pkg.getId();
@@ -494,6 +658,27 @@ function collectPackagePublishContext(
494
658
  const referencedIds = new Set<string>();
495
659
  const pixelHitTestImageIds = new Set<string>();
496
660
  const spriteItemIds = new Set<string>();
661
+ const collectExportedResourceIds = (
662
+ sourceResources: ReturnType<Package['listResources']>,
663
+ sourcePublishedResourceIds: Set<string>,
664
+ ): Set<string> => {
665
+ const exportedResourceIds = new Set<string>(sourcePublishedResourceIds);
666
+ const resourcesById = new Map(sourceResources.map((resource) => [resource.getId(), resource] as const));
667
+ let changed = true;
668
+ while (changed) {
669
+ changed = false;
670
+ for (const resourceId of [...exportedResourceIds]) {
671
+ const resource = resourcesById.get(resourceId);
672
+ if (!resource || !isSkeletonResource(resource)) continue;
673
+ for (const requiredId of resource.getRequireIds()) {
674
+ if (!requiredId || exportedResourceIds.has(requiredId)) continue;
675
+ exportedResourceIds.add(requiredId);
676
+ changed = true;
677
+ }
678
+ }
679
+ }
680
+ return exportedResourceIds;
681
+ };
497
682
 
498
683
  for (const atlas of pkg.listAtlases()) {
499
684
  for (const sprite of atlas.listSprites()) {
@@ -531,6 +716,7 @@ function collectPackagePublishContext(
531
716
  child.getSelectedIcon?.(),
532
717
  child.getDropdown?.(),
533
718
  child.getSound?.(),
719
+ child.getInstanceSound?.(),
534
720
  child.getInstanceIcon?.(),
535
721
  child.getInstanceSelectedIcon?.(),
536
722
  child.getVtScrollBarRes?.(),
@@ -583,7 +769,12 @@ function collectPackagePublishContext(
583
769
  continue;
584
770
  }
585
771
  if (isImageResource(resource)) {
586
- if (resource.getExported() || referencedIds.has(resourceId) || spriteItemIds.has(resourceId) || pixelHitTestImageIds.has(resourceId)) {
772
+ if (
773
+ resource.getExported() ||
774
+ referencedIds.has(resourceId) ||
775
+ spriteItemIds.has(resourceId) ||
776
+ pixelHitTestImageIds.has(resourceId)
777
+ ) {
587
778
  publishedResourceIds.add(resourceId);
588
779
  }
589
780
  continue;
@@ -608,20 +799,16 @@ function collectPackagePublishContext(
608
799
  }
609
800
  }
610
801
 
611
- let changed = true;
612
- while (changed) {
613
- changed = false;
614
- for (const resource of resources) {
615
- if (!isSkeletonResource(resource)) continue;
616
- if (!publishedResourceIds.has(resource.getId())) continue;
617
- for (const requiredId of resource.getRequireIds()) {
618
- if (!requiredId || publishedResourceIds.has(requiredId)) continue;
619
- publishedResourceIds.add(requiredId);
620
- changed = true;
621
- }
622
- }
802
+ for (const resourceId of collectExportedResourceIds(resources, publishedResourceIds)) {
803
+ publishedResourceIds.add(resourceId);
623
804
  }
624
805
 
806
+ const highResolutionItemIds = collectHighResolutionItemIds(
807
+ resources,
808
+ publishedResourceIds,
809
+ options.includeHighResolution,
810
+ );
811
+
625
812
  if (!options.includeBranches) {
626
813
  const mainByKey = new Map<string, ReturnType<Package['listResources']>[number]>();
627
814
  const activeBranchByKey = new Map<string, ReturnType<Package['listResources']>[number]>();
@@ -685,7 +872,9 @@ function collectPackagePublishContext(
685
872
  return {
686
873
  referencedIds,
687
874
  publishedResourceIds,
875
+ exportedResourceIds: collectExportedResourceIds(resources, publishedResourceIds),
688
876
  pixelHitTestImageIds,
877
+ highResolutionItemIds,
689
878
  effectiveResourceIds,
690
879
  includeBranches: false,
691
880
  };
@@ -694,7 +883,9 @@ function collectPackagePublishContext(
694
883
  return {
695
884
  referencedIds,
696
885
  publishedResourceIds,
886
+ exportedResourceIds: collectExportedResourceIds(resources, publishedResourceIds),
697
887
  pixelHitTestImageIds,
888
+ highResolutionItemIds,
698
889
  effectiveResourceIds: new Map([...publishedResourceIds].map((resourceId) => [resourceId, resourceId])),
699
890
  includeBranches: true,
700
891
  };
@@ -704,7 +895,7 @@ async function applyPixelHitTests(
704
895
  pkg: Package,
705
896
  imageIds: Set<string>,
706
897
  basePath: string | undefined,
707
- encoder: PublishEncoder | undefined,
898
+ encoder: AtlasRasterBackend | undefined,
708
899
  ): Promise<void> {
709
900
  const images = pkg.listImageResources();
710
901
  for (const image of images) {
@@ -766,31 +957,44 @@ async function applyPixelHitTests(
766
957
  async function annotatePackagePublishArtifacts(
767
958
  pkg: Package,
768
959
  basePath: string | undefined,
769
- encoder: PublishEncoder | undefined,
960
+ encoder: AtlasRasterBackend | undefined,
770
961
  options: {
962
+ projectType: number;
771
963
  includeBranches: boolean;
772
964
  activeBranch: string;
965
+ includeHighResolution: number;
773
966
  },
774
967
  ): Promise<void> {
775
- const { publishedResourceIds, pixelHitTestImageIds, effectiveResourceIds, includeBranches } = collectPackagePublishContext(pkg, options);
968
+ const {
969
+ publishedResourceIds,
970
+ exportedResourceIds,
971
+ pixelHitTestImageIds,
972
+ highResolutionItemIds,
973
+ effectiveResourceIds,
974
+ includeBranches,
975
+ } = collectPackagePublishContext(pkg, options);
776
976
  for (const resource of pkg.listResources()) {
777
977
  setPublishedIdExtra(resource, effectiveResourceIds.get(resource.getId()) ?? null);
978
+ if (isHighResolutionResource(resource)) {
979
+ resource.setHighResolutionItemIds(highResolutionItemIds.get(resource.getId()) ?? []);
980
+ }
778
981
  }
779
982
  await applyPixelHitTests(pkg, pixelHitTestImageIds, basePath, encoder);
780
983
  const extras = (pkg.getExtras() as PackagePublishArtifactsExtras | undefined) ?? {};
781
984
  pkg.setExtras({
782
985
  ...extras,
783
986
  publishedResourceIds: [...publishedResourceIds].sort((a, b) => a.localeCompare(b)),
987
+ exportedResourceIds: [...exportedResourceIds].sort((a, b) => a.localeCompare(b)),
784
988
  publishedIncludeBranches: includeBranches,
785
989
  publishedEffectiveResourceIds: Object.fromEntries(effectiveResourceIds),
786
990
  });
787
991
  for (const resource of pkg.listResources()) {
788
992
  if (isMiscResource(resource)) {
789
- setPublishedFileExtra(resource, resolvePublishedMiscFileName(resource));
993
+ setPublishedFileExtra(resource, resolvePublishedMiscFileName(resource, options.projectType));
790
994
  continue;
791
995
  }
792
996
  if (isSkeletonResource(resource)) {
793
- setPublishedFileExtra(resource, resolvePublishedSkeletonFileName(resource));
997
+ setPublishedFileExtra(resource, resolvePublishedSkeletonFileName(resource, options.projectType));
794
998
  }
795
999
  }
796
1000
  }
@@ -800,10 +1004,12 @@ function getAnnotatedPublishedResourceIds(pkg: Package): Set<string> {
800
1004
  return new Set(extras.publishedResourceIds ?? []);
801
1005
  }
802
1006
 
803
- function getPublishedSkeletonDependencyImageIds(
804
- pkg: Package,
805
- publishedResourceIds: Set<string>,
806
- ): Set<string> {
1007
+ function getAnnotatedExportedResourceIds(pkg: Package): Set<string> {
1008
+ const extras = (pkg.getExtras() as PackagePublishArtifactsExtras | undefined) ?? {};
1009
+ return new Set(extras.exportedResourceIds ?? []);
1010
+ }
1011
+
1012
+ function getPublishedSkeletonDependencyImageIds(pkg: Package, publishedResourceIds: Set<string>): Set<string> {
807
1013
  const imageIds = new Set<string>();
808
1014
  const resourcesById = new Map(pkg.listResources().map((resource) => [resource.getId(), resource] as const));
809
1015
  for (const resource of pkg.listResources()) {
@@ -833,7 +1039,9 @@ async function exportPackageSounds(
833
1039
  return isSoundResource(resource) && publishedResourceIds.has(resource.getId());
834
1040
  });
835
1041
  if (hasPublishedSound) {
836
- logger.warn(`publish: Sound resources in package "${pkg.getName()}" were not exported because basePath/readFileRaw is unavailable.`);
1042
+ logger.warn(
1043
+ `publish: Sound resources in package "${pkg.getName()}" were not exported because basePath/readFileRaw is unavailable.`,
1044
+ );
837
1045
  }
838
1046
  return;
839
1047
  }
@@ -863,25 +1071,29 @@ async function exportPackageExternalResources(
863
1071
  readFileRaw: PublishFileSystem['readFileRaw'] | undefined,
864
1072
  logger: Document['getLogger'] extends () => infer T ? T : never,
865
1073
  ): Promise<void> {
866
- const publishedResourceIds = getAnnotatedPublishedResourceIds(pkg);
867
- const skeletonDependencyImageIds = getPublishedSkeletonDependencyImageIds(pkg, publishedResourceIds);
868
- if (publishedResourceIds.size === 0) return;
1074
+ const exportedResourceIds = getAnnotatedExportedResourceIds(pkg);
1075
+ const skeletonDependencyImageIds = getPublishedSkeletonDependencyImageIds(pkg, exportedResourceIds);
1076
+ if (exportedResourceIds.size === 0) return;
869
1077
  if (!basePath || !readFileRaw) {
870
1078
  const hasPublishedExternal = pkg.listResources().some((resource) => {
871
1079
  return (
872
- (isMiscResource(resource) || isSkeletonResource(resource))
873
- && publishedResourceIds.has(resource.getId())
874
- ) || skeletonDependencyImageIds.has(resource.getId());
1080
+ ((isMiscResource(resource) || isSkeletonResource(resource)) &&
1081
+ exportedResourceIds.has(resource.getId())) ||
1082
+ skeletonDependencyImageIds.has(resource.getId())
1083
+ );
875
1084
  });
876
1085
  if (hasPublishedExternal) {
877
- logger.warn(`publish: External resources in package "${pkg.getName()}" were not exported because basePath/readFileRaw is unavailable.`);
1086
+ logger.warn(
1087
+ `publish: External resources in package "${pkg.getName()}" were not exported because basePath/readFileRaw is unavailable.`,
1088
+ );
878
1089
  }
879
1090
  return;
880
1091
  }
881
1092
 
882
1093
  for (const resource of pkg.listResources()) {
883
1094
  const resourceId = resource.getId();
884
- const isSkeletonExternal = publishedResourceIds.has(resourceId) && (isMiscResource(resource) || isSkeletonResource(resource));
1095
+ const isSkeletonExternal =
1096
+ exportedResourceIds.has(resourceId) && (isMiscResource(resource) || isSkeletonResource(resource));
885
1097
  const isSkeletonImageDependency = skeletonDependencyImageIds.has(resourceId) && isImageResource(resource);
886
1098
  if (!isSkeletonExternal && !isSkeletonImageDependency) continue;
887
1099
 
@@ -892,7 +1104,8 @@ async function exportPackageExternalResources(
892
1104
  targetName = resolveImageFileName(resource);
893
1105
  } else if (isMiscResource(resource) || isSkeletonResource(resource)) {
894
1106
  sourcePath = resolveGenericResourcePath(resource, pkg, basePath);
895
- targetName = ((resource.getExtras() as PublishFileExtras | undefined) ?? {})._publishedFile ?? resource.getFile();
1107
+ targetName =
1108
+ ((resource.getExtras() as PublishFileExtras | undefined) ?? {})._publishedFile ?? resource.getFile();
896
1109
  } else {
897
1110
  continue;
898
1111
  }
@@ -902,7 +1115,9 @@ async function exportPackageExternalResources(
902
1115
  const data = await readFileRaw(sourcePath);
903
1116
  await fs.writeFileRaw(targetPath, data);
904
1117
  } catch {
905
- logger.warn(`publish: Could not export external resource "${resource.getId()}" from package "${pkg.getName()}".`);
1118
+ logger.warn(
1119
+ `publish: Could not export external resource "${resource.getId()}" from package "${pkg.getName()}".`,
1120
+ );
906
1121
  }
907
1122
  }
908
1123
  }
@@ -911,10 +1126,13 @@ async function exportPackageExternalResources(
911
1126
  * Publishes a FairyGUI project.
912
1127
  *
913
1128
  * Orchestrates:
914
- * 1. Atlas packing (MaxRects layout + optional sharp compositing)
1129
+ * 1. Atlas packing (MaxRects layout + optional raster compositing)
915
1130
  * 2. Per-package .fui binary serialization
916
1131
  * 3. File writing to the output directory
917
1132
  *
1133
+ * This is the capability-injected core. Standard hosts should use
1134
+ * `publishNode()` or `publishBrowser()` through their dedicated entries.
1135
+ *
918
1136
  * ```ts
919
1137
  * import sharp from 'sharp';
920
1138
  * const io = new NodeIO();
@@ -932,17 +1150,152 @@ async function exportPackageExternalResources(
932
1150
  */
933
1151
  export function publish(options: PublishOptions): Transform {
934
1152
  return createTransform('publish', async (doc: Document): Promise<void> => {
1153
+ const resolveConfiguredOutputPath = (value?: string, projectBasePath?: string): string | undefined => {
1154
+ const trimmed = value?.trim();
1155
+ if (!trimmed) return undefined;
1156
+ if (isAbsolutePathLike(trimmed) || !projectBasePath) {
1157
+ return trimTrailingSlashes(trimmed);
1158
+ }
1159
+ return trimTrailingSlashes(
1160
+ options.fs ? options.fs.join(projectBasePath, trimmed) : joinPathSegments(projectBasePath, trimmed),
1161
+ );
1162
+ };
1163
+
1164
+ const resolveProjectPublishConfig = (): ResolvedProjectPublishConfig => {
1165
+ const settings = (doc.getRoot().getSettings?.() ?? {}) as RootProjectSettings;
1166
+ const publishSettings: CliPublishSettings = settings.publish ?? {};
1167
+ const resolved = resolvePublishOptions(doc, {
1168
+ compressed: options.compressed,
1169
+ fileExtension: options.fileExtension,
1170
+ packages: options.packages,
1171
+ atlas: options.atlas,
1172
+ });
1173
+ const branchProcessing = publishSettings.branchProcessing ?? 0;
1174
+ const includeBranches = branchProcessing === 0;
1175
+
1176
+ return {
1177
+ ...resolved,
1178
+ projectType: doc.getRoot().getProjectType(),
1179
+ includeBranches,
1180
+ activeBranch: includeBranches ? '' : (options.branch ?? ''),
1181
+ includeHighResolution: publishSettings.includeHighResolution ?? 0,
1182
+ separatedAtlasForBranch: includeBranches && publishSettings.seperatedAtlasForBranch === true,
1183
+ globalOutputPath: publishSettings.path?.trim() ?? '',
1184
+ globalBranchOutputPath: publishSettings.branchPath?.trim() ?? '',
1185
+ };
1186
+ };
1187
+
1188
+ const resolvePackagePublishPlan = (
1189
+ pkg: Package,
1190
+ config: ResolvedProjectPublishConfig,
1191
+ projectBasePath?: string,
1192
+ ): ResolvedPackagePublishPlan => {
1193
+ let outputDir: string | undefined;
1194
+
1195
+ if (options.output) {
1196
+ outputDir = trimTrailingSlashes(options.output);
1197
+ } else {
1198
+ const candidates: Array<string | undefined> = [];
1199
+ if (!config.includeBranches && config.activeBranch) {
1200
+ candidates.push(pkg.getPublishBranchPath(), config.globalBranchOutputPath);
1201
+ }
1202
+ candidates.push(pkg.getPublishPath(), config.globalOutputPath);
1203
+
1204
+ for (const candidate of candidates) {
1205
+ const resolved = resolveConfiguredOutputPath(candidate, projectBasePath);
1206
+ if (!resolved) continue;
1207
+ outputDir = resolved;
1208
+ break;
1209
+ }
1210
+ }
1211
+ const publishName = pkg.getPublishName() || pkg.getName();
1212
+
1213
+ return {
1214
+ pkg,
1215
+ outputDir,
1216
+ publishName,
1217
+ fileName: resolvePublishFileName(publishName, config.fileExtension),
1218
+ compressed: config.compressed,
1219
+ fileExtension: config.fileExtension,
1220
+ includeBranches: config.includeBranches,
1221
+ activeBranch: config.activeBranch,
1222
+ includeHighResolution: config.includeHighResolution,
1223
+ separatedAtlasForBranch: config.separatedAtlasForBranch,
1224
+ atlas: config.atlas,
1225
+ };
1226
+ };
1227
+
1228
+ const createNoopPublishFs = (): PublishFileSystem => ({
1229
+ async writeFileRaw(): Promise<void> {
1230
+ // No-op for layout-only publish flows.
1231
+ },
1232
+ async mkdir(): Promise<void> {
1233
+ // No-op for layout-only publish flows.
1234
+ },
1235
+ join(...paths: string[]): string {
1236
+ return paths.join('/');
1237
+ },
1238
+ });
1239
+
1240
+ const publishPackage = async (plan: ResolvedPackagePublishPlan, writerFs: FileSystem, packageIndex: number) => {
1241
+ const atlasRuntimeOptions = resolvePublishAtlasRuntimeOptions(plan.fileExtension);
1242
+ await atlas({
1243
+ ...plan.atlas,
1244
+ ...(options.atlas ?? {}),
1245
+ separatedAtlasForBranch: plan.separatedAtlasForBranch,
1246
+ encoder: options.encoder,
1247
+ basePath: options.basePath,
1248
+ outputPath: options.fs ? plan.outputDir : undefined,
1249
+ mkdir: options.fs ? options.fs.mkdir : undefined,
1250
+ readFileRaw: options.atlas?.readFileRaw ?? options.fs?.readFileRaw,
1251
+ packages: [plan.pkg.getName()],
1252
+ ...atlasRuntimeOptions,
1253
+ })(doc);
1254
+
1255
+ if (!options.fs) return;
1256
+ if (!plan.outputDir) {
1257
+ throw new Error(
1258
+ 'publish: no output directory resolved. Provide --output, or configure global publish.path / package publishPath.',
1259
+ );
1260
+ }
1261
+
1262
+ await options.fs.mkdir(plan.outputDir);
1263
+
1264
+ const filePath = options.fs.join(plan.outputDir, plan.fileName);
1265
+ const bwOptions: BinaryWriterOptions = {
1266
+ compressed: plan.compressed,
1267
+ packageIndex,
1268
+ };
1269
+
1270
+ const bw = new BinaryWriter(writerFs);
1271
+ await bw.write(doc, filePath, bwOptions);
1272
+ await exportPackageSounds(
1273
+ plan.pkg,
1274
+ plan.outputDir,
1275
+ options.basePath,
1276
+ options.fs,
1277
+ options.atlas?.readFileRaw ?? options.fs.readFileRaw,
1278
+ logger,
1279
+ );
1280
+ await exportPackageExternalResources(
1281
+ plan.pkg,
1282
+ plan.outputDir,
1283
+ options.basePath,
1284
+ options.fs,
1285
+ options.atlas?.readFileRaw ?? options.fs.readFileRaw,
1286
+ logger,
1287
+ );
1288
+
1289
+ logger.info(`publish: Written ${plan.fileName}`);
1290
+ };
1291
+
935
1292
  const root = doc.getRoot();
936
1293
  const logger = doc.getLogger();
937
- const settings = (root.getSettings?.() ?? {}) as RootProjectSettings;
938
- const publishSettings: CliPublishSettings = settings.publish ?? {};
939
- const resolved = resolvePublishOptions(doc, {
940
- compressed: options.compressed,
941
- fileExtension: options.fileExtension,
942
- packages: options.packages,
943
- atlas: options.atlas,
944
- });
945
- const ext = resolved.fileExtension;
1294
+ const projectBasePath = resolveProjectBasePath(options.basePath) || doc.getProjectDir?.() || '';
1295
+ const plugins = options.plugins ?? [];
1296
+ await runPublishPluginHook(plugins, 'onPublishStart', doc, options);
1297
+
1298
+ const resolved = resolveProjectPublishConfig();
946
1299
 
947
1300
  // Step 1: Determine which packages to publish
948
1301
  let allPackages = root.listPackages();
@@ -953,14 +1306,10 @@ export function publish(options: PublishOptions): Transform {
953
1306
 
954
1307
  if (allPackages.length === 0) {
955
1308
  logger.warn('publish: No packages to publish.');
1309
+ await runPublishPluginHook(plugins, 'onPublishEnd', doc, options);
956
1310
  return;
957
1311
  }
958
1312
 
959
- const branchProcessing = publishSettings.branchProcessing ?? 0;
960
- const includeBranches = branchProcessing === 0;
961
- const activeBranch = includeBranches ? '' : (options.branch ?? '');
962
- const atlasRuntimeOptions = resolvePublishAtlasRuntimeOptions(ext);
963
-
964
1313
  const allDocPackages = root.listPackages();
965
1314
  // Build a pkgId→name map for dependency resolution
966
1315
  const pkgMap = new Map<string, Package>();
@@ -971,82 +1320,62 @@ export function publish(options: PublishOptions): Transform {
971
1320
  for (const pkg of allPackages) {
972
1321
  // Compute dependency list and selected publish artifacts before atlas packing,
973
1322
  // so merged-branch publishes can pack the overridden resources with main IDs.
974
- _computeDependencies(pkg, pkgMap);
975
- await annotatePackagePublishArtifacts(
976
- pkg,
977
- options.basePath,
978
- options.encoder as PublishEncoder | undefined,
979
- {
980
- includeBranches,
981
- activeBranch,
982
- },
983
- );
1323
+ _computeDependencies(doc, pkg, pkgMap);
1324
+ await annotatePackagePublishArtifacts(pkg, options.basePath, options.encoder, {
1325
+ projectType: resolved.projectType,
1326
+ includeBranches: resolved.includeBranches,
1327
+ activeBranch: resolved.activeBranch,
1328
+ includeHighResolution: resolved.includeHighResolution,
1329
+ });
984
1330
  }
985
1331
 
986
- // Step 2: Atlas packing
987
- const atlasOpts: AtlasOptions = {
988
- ...resolved.atlas,
989
- ...(options.atlas ?? {}),
990
- separatedAtlasForBranch: includeBranches && publishSettings.seperatedAtlasForBranch === true,
991
- encoder: options.encoder,
992
- basePath: options.basePath,
993
- outputPath: options.fs ? options.output : undefined,
994
- mkdir: options.fs ? options.fs.mkdir : undefined,
995
- readFileRaw: options.atlas?.readFileRaw ?? options.fs?.readFileRaw,
996
- ...atlasRuntimeOptions,
997
- };
998
- await atlas(atlasOpts)(doc);
1332
+ const plans = allPackages.map((pkg) => resolvePackagePublishPlan(pkg, resolved, projectBasePath));
999
1333
 
1000
- // Step 3: Write .fui binary per package
1001
1334
  if (!options.fs) {
1002
- logger.info(`publish: No fs provided — layout computed for ${allPackages.length} package(s), skipping file output.`);
1335
+ logger.info(
1336
+ `publish: No fs provided — layout computed for ${allPackages.length} package(s), skipping file output.`,
1337
+ );
1338
+ const noopWriterFs = toBinaryWriterFileSystem(createNoopPublishFs());
1339
+ for (const plan of plans) {
1340
+ await publishPackage(plan, noopWriterFs, allDocPackages.indexOf(plan.pkg));
1341
+ }
1342
+ await runPublishPluginHook(plugins, 'onPublishEnd', doc, options);
1003
1343
  return;
1004
1344
  }
1005
1345
 
1006
- await options.fs.mkdir(options.output);
1346
+ const unresolvedPlan = plans.find((plan) => !plan.outputDir);
1347
+ if (unresolvedPlan) {
1348
+ throw new Error(
1349
+ `publish: no output directory resolved for package "${unresolvedPlan.pkg.getName()}". ` +
1350
+ 'Provide --output, or configure global publish.path / package publishPath.',
1351
+ );
1352
+ }
1007
1353
 
1008
1354
  const writerFs = toBinaryWriterFileSystem(options.fs);
1009
1355
 
1010
- for (const pkg of allPackages) {
1011
- const pkgIndex = allDocPackages.indexOf(pkg);
1012
- const publishName = pkg.getPublishName() || pkg.getName();
1013
- const fileName = resolvePublishFileName(publishName, ext);
1014
- const filePath = options.fs.join(options.output, fileName);
1015
-
1016
- const bwOptions: BinaryWriterOptions = {
1017
- compressed: resolved.compressed,
1018
- packageIndex: pkgIndex,
1019
- };
1020
-
1021
- const bw = new BinaryWriter(writerFs);
1022
- await bw.write(doc, filePath, bwOptions);
1023
- await exportPackageSounds(
1024
- pkg,
1025
- options.output,
1026
- options.basePath,
1027
- options.fs,
1028
- options.atlas?.readFileRaw ?? options.fs.readFileRaw,
1029
- logger,
1030
- );
1031
- await exportPackageExternalResources(
1032
- pkg,
1033
- options.output,
1034
- options.basePath,
1035
- options.fs,
1036
- options.atlas?.readFileRaw ?? options.fs.readFileRaw,
1037
- logger,
1038
- );
1039
-
1040
- logger.info(`publish: Written ${fileName}`);
1356
+ for (const plan of plans) {
1357
+ const pkgIndex = allDocPackages.indexOf(plan.pkg);
1358
+ await publishPackage(plan, writerFs, pkgIndex);
1041
1359
  }
1042
1360
 
1043
- await publishCodeGeneration(doc, {
1044
- basePath: options.basePath,
1045
- fs: options.fs,
1046
- packages: allPackages,
1047
- });
1361
+ if (options.codeGeneration !== false) {
1362
+ await publishCodeGeneration(doc, {
1363
+ basePath: options.basePath,
1364
+ fs: options.fs,
1365
+ packages: allPackages,
1366
+ plugins,
1367
+ });
1368
+ }
1048
1369
 
1049
- logger.info(`publish: Published ${allPackages.length} package(s) to ${options.output}`);
1370
+ const publishedTargets = [
1371
+ ...new Set(plans.map((plan) => plan.outputDir).filter((value): value is string => Boolean(value))),
1372
+ ];
1373
+ logger.info(
1374
+ publishedTargets.length > 0
1375
+ ? `publish: Published ${allPackages.length} package(s) to ${publishedTargets.join(', ')}`
1376
+ : `publish: Published ${allPackages.length} package(s)`,
1377
+ );
1378
+ await runPublishPluginHook(plugins, 'onPublishEnd', doc, options);
1050
1379
  });
1051
1380
  }
1052
1381
 
@@ -1055,25 +1384,114 @@ export function publish(options: PublishOptions): Transform {
1055
1384
  * The editor only adds dependencies for packages referenced via bitmap font URLs.
1056
1385
  * @internal
1057
1386
  */
1058
- function _computeDependencies(pkg: Package, pkgMap: Map<string, Package>): void {
1387
+ function _computeDependencies(doc: Document, pkg: Package, pkgMap: Map<string, Package>): void {
1059
1388
  const referencedPkgIds = new Set<string>();
1060
-
1061
- function scanFontUrl(font: string | string[] | null | undefined): void {
1062
- if (!font) return;
1063
- const fontStr = Array.isArray(font) ? font[0] : String(font);
1064
- if (typeof fontStr !== 'string' || !fontStr.startsWith('ui://')) return;
1065
- const rest = fontStr.slice(5);
1389
+ const pkgId = pkg.getId();
1390
+ const packageOrder = new Map(
1391
+ doc
1392
+ .getRoot()
1393
+ .listPackages()
1394
+ .map((entry, index) => [entry.getId(), index] as const),
1395
+ );
1396
+ const addDependencyPackageId = (dependencyPkgId: string | null | undefined): void => {
1397
+ const normalized = dependencyPkgId?.trim() ?? '';
1398
+ if (!normalized || normalized === pkgId) return;
1399
+ referencedPkgIds.add(normalized);
1400
+ };
1401
+ const extractPackageIdFromUiUrl = (value: string): string | null => {
1402
+ if (!value.startsWith('ui://')) return null;
1403
+ const rest = value.slice(5);
1404
+ if (!rest) return null;
1405
+ const slashIndex = rest.indexOf('/');
1406
+ if (slashIndex >= 0) {
1407
+ return rest.slice(0, slashIndex) || null;
1408
+ }
1066
1409
  if (rest.length >= 8) {
1067
- const depPkgId = rest.slice(0, 8);
1068
- if (depPkgId !== pkg.getId()) referencedPkgIds.add(depPkgId);
1410
+ return rest.slice(0, 8);
1069
1411
  }
1070
- }
1412
+ return null;
1413
+ };
1414
+ const addDependencyPackageIdFromUiValue = (value: string | null | undefined): void => {
1415
+ if (!value || typeof value !== 'string') return;
1416
+ addDependencyPackageId(extractPackageIdFromUiUrl(value));
1417
+ };
1418
+ const addDependencyPackageIdsFromText = (value: string | null | undefined): void => {
1419
+ if (!value || typeof value !== 'string') return;
1420
+ const matches = value.matchAll(/ui:\/\/([0-9a-z]{8})/giu);
1421
+ for (const match of matches) {
1422
+ addDependencyPackageId(match[1] ?? '');
1423
+ }
1424
+ };
1425
+ const addDependencyPackageIdsFromUnknown = (value: unknown): void => {
1426
+ if (Array.isArray(value)) {
1427
+ for (const entry of value) addDependencyPackageIdsFromUnknown(entry);
1428
+ return;
1429
+ }
1430
+ if (typeof value === 'string') {
1431
+ addDependencyPackageIdFromUiValue(value);
1432
+ addDependencyPackageIdsFromText(value);
1433
+ }
1434
+ };
1435
+ const addDependencyFontRef = (value: string | string[] | null | undefined): void => {
1436
+ if (Array.isArray(value)) {
1437
+ for (const entry of value) addDependencyPackageIdFromUiValue(entry);
1438
+ return;
1439
+ }
1440
+ addDependencyPackageIdFromUiValue(value ?? undefined);
1441
+ };
1071
1442
 
1072
1443
  for (const res of pkg.listResources()) {
1073
1444
  if (res.propertyType !== 'Component') continue;
1074
- for (const child of res.listChildren?.() ?? []) {
1075
- // Only font="ui://..." references generate dependencies
1076
- scanFontUrl((child as HasOptionalFont).getFont?.());
1445
+ const component = res as ComponentWithPublishRefs;
1446
+ for (const child of component.listChildren?.() ?? []) {
1447
+ addDependencyPackageId(child.getPackageId?.());
1448
+ addDependencyFontRef(child.getFont?.());
1449
+ addDependencyPackageIdsFromText(child.getText?.());
1450
+ for (const ref of [
1451
+ child.getUrl?.(),
1452
+ child.getDefaultItem?.(),
1453
+ child.getIcon?.(),
1454
+ child.getSelectedIcon?.(),
1455
+ child.getDropdown?.(),
1456
+ child.getSound?.(),
1457
+ child.getInstanceSound?.(),
1458
+ child.getInstanceIcon?.(),
1459
+ child.getInstanceSelectedIcon?.(),
1460
+ child.getVtScrollBarRes?.(),
1461
+ child.getHzScrollBarRes?.(),
1462
+ child.getHeaderRes?.(),
1463
+ child.getFooterRes?.(),
1464
+ ]) {
1465
+ addDependencyPackageIdFromUiValue(ref);
1466
+ }
1467
+ for (const item of child.getInstanceComboItems?.() ?? []) {
1468
+ addDependencyPackageIdFromUiValue(item.icon ?? undefined);
1469
+ }
1470
+ for (const item of child.getListItems?.() ?? []) {
1471
+ addDependencyPackageIdFromUiValue(item.icon ?? undefined);
1472
+ addDependencyPackageIdFromUiValue(item.url ?? undefined);
1473
+ }
1474
+ for (const gear of child.listGears?.() ?? []) {
1475
+ addDependencyPackageIdsFromUnknown(gear.getValues?.());
1476
+ addDependencyPackageIdsFromUnknown(gear.getDefaultValue?.());
1477
+ }
1478
+ }
1479
+ addDependencyFontRef(component.getFont?.());
1480
+ for (const ref of [
1481
+ component.getDropdown?.(),
1482
+ component.getHeaderRes?.(),
1483
+ component.getFooterRes?.(),
1484
+ component.getVtScrollBarRes?.(),
1485
+ component.getHzScrollBarRes?.(),
1486
+ component.getSound?.(),
1487
+ ]) {
1488
+ addDependencyPackageIdFromUiValue(ref);
1489
+ }
1490
+ for (const transition of component.listTransitions?.() ?? []) {
1491
+ for (const item of transition.listItems?.() ?? []) {
1492
+ addDependencyPackageIdsFromUnknown(item.getStartValue?.());
1493
+ addDependencyPackageIdsFromUnknown(item.getEndValue?.());
1494
+ }
1077
1495
  }
1078
1496
  }
1079
1497
 
@@ -1082,7 +1500,12 @@ function _computeDependencies(pkg: Package, pkgMap: Map<string, Package>): void
1082
1500
  }
1083
1501
 
1084
1502
  if (referencedPkgIds.size > 0) {
1085
- const sortedIds = [...referencedPkgIds].sort((a, b) => a.localeCompare(b));
1503
+ const sortedIds = [...referencedPkgIds].sort((a, b) => {
1504
+ const orderA = packageOrder.get(a) ?? Number.MAX_SAFE_INTEGER;
1505
+ const orderB = packageOrder.get(b) ?? Number.MAX_SAFE_INTEGER;
1506
+ if (orderA !== orderB) return orderA - orderB;
1507
+ return a.localeCompare(b);
1508
+ });
1086
1509
  for (const refId of sortedIds) {
1087
1510
  const depPkg = pkgMap.get(refId);
1088
1511
  if (depPkg) {