@openfairygui/cli 0.2.0-alpha.10 → 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/dist/cli.mjs CHANGED
@@ -1,10 +1,177 @@
1
1
  import { createRequire } from "node:module";
2
+ import { Command } from "commander";
2
3
  import { createNodeBackendRuntime } from "@openfairygui/backend/node";
3
- import * as fs$1 from "node:fs/promises";
4
- import fs from "node:fs/promises";
5
4
  import * as path$1 from "node:path";
6
5
  import path from "node:path";
7
- import { parseArgs } from "node:util";
6
+ import * as fs$1 from "node:fs/promises";
7
+ import fs from "node:fs/promises";
8
+ //#region src/commands/backend-capabilities.ts
9
+ function registerBackendCapabilitiesCommand(program) {
10
+ program.command("backend-capabilities").description("Open a backend session, print runtime capabilities, then close it").argument("<project-dir>", "Project root directory").action(async (projectDir) => {
11
+ const runtime = createNodeBackendRuntime();
12
+ const opened = await runtime.openSession({ projectPath: path.resolve(projectDir) });
13
+ if (!opened.ok) throw new Error(`backend-capabilities: ${opened.error.message}`);
14
+ const capabilities = runtime.getCapabilities();
15
+ if (!capabilities.ok) {
16
+ await runtime.closeSession({ sessionId: opened.data.sessionId });
17
+ throw new Error("backend-capabilities: failed to read capabilities");
18
+ }
19
+ console.log(`Session: ${opened.data.sessionId}`);
20
+ console.log(`Project: ${opened.data.canonicalProjectPath}`);
21
+ console.log(`Revision: ${opened.data.revision}`);
22
+ console.log(`Runtime owner: ${capabilities.data.runtimeOwner}`);
23
+ console.log(`Transaction owner: ${capabilities.data.transactionKernelOwner}`);
24
+ console.log(`App seam owner: ${capabilities.data.appSeamOwner}`);
25
+ const closed = await runtime.closeSession({ sessionId: opened.data.sessionId });
26
+ if (!closed.ok) throw new Error(`backend-capabilities: ${closed.error.message}`);
27
+ });
28
+ }
29
+ //#endregion
30
+ //#region ../functions/src/inspect.ts
31
+ function mapResource(resource) {
32
+ return {
33
+ name: resource.getName(),
34
+ id: resource.getId(),
35
+ path: resource.getPath?.() ?? "/",
36
+ exported: resource.getExported?.() ?? false
37
+ };
38
+ }
39
+ function mapComponentDetail(component, totals) {
40
+ const children = component.listChildren();
41
+ const controllers = component.listControllers();
42
+ const transitions = component.listTransitions();
43
+ totals.displayObjects += children.length;
44
+ totals.controllers += controllers.length;
45
+ totals.transitions += transitions.length;
46
+ for (const child of children) totals.gears += child.listGears().length;
47
+ return {
48
+ name: component.getName(),
49
+ id: component.getId(),
50
+ childCount: children.length,
51
+ controllerCount: controllers.length,
52
+ transitionCount: transitions.length
53
+ };
54
+ }
55
+ /**
56
+ * Generates a detailed report of the project contents.
57
+ *
58
+ * Unlike other transforms, `inspect()` does NOT modify the document —
59
+ * it returns a structured report.
60
+ *
61
+ * ```ts
62
+ * const report = inspect(doc);
63
+ * console.log(`${report.totals.packages} packages, ${report.totals.components} components`);
64
+ * ```
65
+ */
66
+ function inspect(doc) {
67
+ const root = doc.getRoot();
68
+ const totals = {
69
+ packages: 0,
70
+ images: 0,
71
+ sounds: 0,
72
+ fonts: 0,
73
+ movieClips: 0,
74
+ components: 0,
75
+ displayObjects: 0,
76
+ gears: 0,
77
+ controllers: 0,
78
+ transitions: 0
79
+ };
80
+ const packages = root.listPackages().map((pkg) => {
81
+ totals.packages++;
82
+ const resources = pkg.listResources();
83
+ const images = resources.filter((r) => r.propertyType === "ImageResource");
84
+ const sounds = resources.filter((r) => r.propertyType === "SoundResource");
85
+ const fonts = resources.filter((r) => r.propertyType === "FontResource");
86
+ const movieClips = resources.filter((r) => r.propertyType === "MovieClipResource");
87
+ const components = pkg.listComponents();
88
+ totals.images += images.length;
89
+ totals.sounds += sounds.length;
90
+ totals.fonts += fonts.length;
91
+ totals.movieClips += movieClips.length;
92
+ totals.components += components.length;
93
+ const componentDetails = components.map((component) => mapComponentDetail(component, totals));
94
+ return {
95
+ name: pkg.getName(),
96
+ id: pkg.getId(),
97
+ publishName: pkg.getPublishName() || pkg.getName(),
98
+ resources: {
99
+ images: {
100
+ count: images.length,
101
+ details: images.map(mapResource)
102
+ },
103
+ sounds: {
104
+ count: sounds.length,
105
+ details: sounds.map(mapResource)
106
+ },
107
+ fonts: {
108
+ count: fonts.length,
109
+ details: fonts.map(mapResource)
110
+ },
111
+ movieClips: {
112
+ count: movieClips.length,
113
+ details: movieClips.map(mapResource)
114
+ },
115
+ components: {
116
+ count: components.length,
117
+ details: components.map(mapResource)
118
+ }
119
+ },
120
+ componentDetails
121
+ };
122
+ });
123
+ return {
124
+ projectId: root.getProjectId(),
125
+ projectType: root.getProjectType(),
126
+ version: root.getVersion(),
127
+ packages,
128
+ totals
129
+ };
130
+ }
131
+ //#endregion
132
+ //#region ../functions/src/utils.ts
133
+ /**
134
+ * Wraps a transform function, assigning it a name for the transform stack.
135
+ */
136
+ function createTransform(name, fn) {
137
+ Object.defineProperty(fn, "name", { value: name });
138
+ return fn;
139
+ }
140
+ function parseTextureSetMode(value) {
141
+ const raw = value?.trim() ?? "";
142
+ if (!raw) return {
143
+ kind: "auto",
144
+ raw: ""
145
+ };
146
+ if (raw === "alone") return {
147
+ kind: "standalone",
148
+ raw,
149
+ sizeMode: "default"
150
+ };
151
+ if (raw === "alone_npot") return {
152
+ kind: "standalone",
153
+ raw,
154
+ sizeMode: "npot"
155
+ };
156
+ if (raw === "alone_mof") return {
157
+ kind: "standalone",
158
+ raw,
159
+ sizeMode: "multipleOf4"
160
+ };
161
+ if (/^\d+$/.test(raw)) {
162
+ const pageIndex = Number(raw);
163
+ if (pageIndex >= 0 && pageIndex <= 10) return {
164
+ kind: "page",
165
+ raw,
166
+ pageIndex
167
+ };
168
+ }
169
+ return {
170
+ kind: "auto",
171
+ raw
172
+ };
173
+ }
174
+ //#endregion
8
175
  //#region ../../node_modules/.pnpm/property-graph@4.1.0/node_modules/property-graph/dist/index.mjs
9
176
  var EventDispatcher = class {
10
177
  _listeners = {};
@@ -1688,6 +1855,7 @@ var MovieClipResource = class extends ExtensibleProperty {
1688
1855
  highResolutionItemIds: [],
1689
1856
  fileName: "",
1690
1857
  exported: false,
1858
+ textureSetMode: "",
1691
1859
  width: 0,
1692
1860
  height: 0,
1693
1861
  interval: 0,
@@ -1739,6 +1907,12 @@ var MovieClipResource = class extends ExtensibleProperty {
1739
1907
  setExported(v) {
1740
1908
  return this.set("exported", v);
1741
1909
  }
1910
+ getTextureSetMode() {
1911
+ return this.get("textureSetMode");
1912
+ }
1913
+ setTextureSetMode(v) {
1914
+ return this.set("textureSetMode", v);
1915
+ }
1742
1916
  getWidth() {
1743
1917
  return this.get("width");
1744
1918
  }
@@ -4451,6 +4625,8 @@ var GComponent = class extends GObject {
4451
4625
  instanceController: "",
4452
4626
  instancePage: "",
4453
4627
  instanceChecked: false,
4628
+ instanceSound: "",
4629
+ instanceSoundVolumeScale: 1,
4454
4630
  instancePromptText: "",
4455
4631
  instanceSelectionController: "",
4456
4632
  instanceVisibleItemCount: 0,
@@ -4723,6 +4899,18 @@ var GComponent = class extends GObject {
4723
4899
  setInstanceChecked(v) {
4724
4900
  return this.setComponentProp("instanceChecked", v);
4725
4901
  }
4902
+ getInstanceSound() {
4903
+ return firstString$1(this.getComponentProp("instanceSound"));
4904
+ }
4905
+ setInstanceSound(v) {
4906
+ return this.setComponentProp("instanceSound", v);
4907
+ }
4908
+ getInstanceSoundVolumeScale() {
4909
+ return this.getComponentProp("instanceSoundVolumeScale");
4910
+ }
4911
+ setInstanceSoundVolumeScale(v) {
4912
+ return this.setComponentProp("instanceSoundVolumeScale", v);
4913
+ }
4726
4914
  getInstancePromptText() {
4727
4915
  return firstString$1(this.getComponentProp("instancePromptText"));
4728
4916
  }
@@ -9081,6 +9269,7 @@ var Document = class Document {
9081
9269
  _graph = new Graph();
9082
9270
  _root = new Root(this._graph);
9083
9271
  _logger = Logger.DEFAULT_INSTANCE;
9272
+ _projectDir = "";
9084
9273
  static _GRAPH_DOCUMENTS = /* @__PURE__ */ new WeakMap();
9085
9274
  static fromGraph(graph) {
9086
9275
  return Document._GRAPH_DOCUMENTS.get(graph) || null;
@@ -9102,6 +9291,13 @@ var Document = class Document {
9102
9291
  this._logger = logger;
9103
9292
  return this;
9104
9293
  }
9294
+ getProjectDir() {
9295
+ return this._projectDir;
9296
+ }
9297
+ setProjectDir(projectDir) {
9298
+ this._projectDir = projectDir;
9299
+ return this;
9300
+ }
9105
9301
  async transform(...transforms) {
9106
9302
  const stack = transforms.map((fn) => fn.name);
9107
9303
  for (const transform of transforms) await transform(this, { stack });
@@ -9282,6 +9478,7 @@ const PACKAGE_FONT_RESOURCE_ATTRS = {
9282
9478
  renderMode: { canonical: "renderMode" },
9283
9479
  samplePointSize: { canonical: "samplePointSize" }
9284
9480
  };
9481
+ const PACKAGE_MOVIE_CLIP_RESOURCE_ATTRS = { atlas: { canonical: "atlas" } };
9285
9482
  const PACKAGE_SKELETON_RESOURCE_ATTRS = {
9286
9483
  width: { canonical: "width" },
9287
9484
  height: { canonical: "height" },
@@ -9518,7 +9715,10 @@ const LIST_PANEL_ATTRS = {
9518
9715
  const BUTTON_EXTENSION_ATTRS = {
9519
9716
  mode: { canonical: "mode" },
9520
9717
  sound: { canonical: "sound" },
9521
- soundVolumeScale: { canonical: "soundVolumeScale" },
9718
+ soundVolumeScale: {
9719
+ canonical: "soundVolumeScale",
9720
+ aliases: ["volume"]
9721
+ },
9522
9722
  downEffect: { canonical: "downEffect" },
9523
9723
  downEffectValue: { canonical: "downEffectValue" },
9524
9724
  title: { canonical: "title" },
@@ -9644,6 +9844,7 @@ const PACKAGE_PUBLISH_NODE = defineNode(PACKAGE_PUBLISH_ATTRS, { atlas: PACKAGE_
9644
9844
  const PACKAGE_RESOURCE_NODE = defineNode(PACKAGE_RESOURCE_BASE_ATTRS);
9645
9845
  const PACKAGE_IMAGE_RESOURCE_NODE = defineNode(PACKAGE_IMAGE_RESOURCE_ATTRS);
9646
9846
  const PACKAGE_FONT_RESOURCE_NODE = defineNode(PACKAGE_FONT_RESOURCE_ATTRS);
9847
+ const PACKAGE_MOVIE_CLIP_RESOURCE_NODE = defineNode(PACKAGE_MOVIE_CLIP_RESOURCE_ATTRS);
9647
9848
  const PACKAGE_SKELETON_RESOURCE_NODE = defineNode(PACKAGE_SKELETON_RESOURCE_ATTRS);
9648
9849
  const DISPLAY_OBJECT_NODE = defineNode(DISPLAY_OBJECT_IDENTITY_ATTRS);
9649
9850
  const BUTTON_EXTENSION_NODE = defineNode(BUTTON_EXTENSION_ATTRS);
@@ -9734,6 +9935,7 @@ const PROJECT_XML_PROTOCOL = {
9734
9935
  packageResource: PACKAGE_RESOURCE_NODE,
9735
9936
  packageImageResource: PACKAGE_IMAGE_RESOURCE_NODE,
9736
9937
  packageFontResource: PACKAGE_FONT_RESOURCE_NODE,
9938
+ packageMovieClipResource: PACKAGE_MOVIE_CLIP_RESOURCE_NODE,
9737
9939
  packageSkeletonResource: PACKAGE_SKELETON_RESOURCE_NODE,
9738
9940
  displayObject: DISPLAY_OBJECT_NODE,
9739
9941
  image: IMAGE_NODE,
@@ -10177,6 +10379,7 @@ var ProjectReader = class {
10177
10379
  const fs = this._fs;
10178
10380
  const doc = new Document();
10179
10381
  const basePath = getProjectBasePath(fs, projectPath);
10382
+ doc.setProjectDir(basePath);
10180
10383
  const ctx = new ReaderContext(doc, basePath);
10181
10384
  const projDesc = getXmlNode(parseXML(await fs.readFile(projectPath)).projectDescription);
10182
10385
  if (projDesc) {
@@ -10493,6 +10696,8 @@ var ProjectReader = class {
10493
10696
  res.setBranch(branchName);
10494
10697
  res.setFileName(name);
10495
10698
  res.setExported(exported);
10699
+ const textureSetMode = readXmlAttr(attrs, PROJECT_XML_PROTOCOL.packageMovieClipResource.attrs.atlas);
10700
+ if (textureSetMode !== void 0) res.setTextureSetMode(textureSetMode);
10496
10701
  pkg.addResource(res);
10497
10702
  ctx.registerResource(pkg.getId(), id, res);
10498
10703
  return res;
@@ -11752,6 +11957,10 @@ var ProjectReader = class {
11752
11957
  if (page !== void 0) componentObj.setInstancePage?.(page);
11753
11958
  const checked = extSpecs.checked ? readXmlAttr(extAttrs, extSpecs.checked) : void 0;
11754
11959
  if (checked !== void 0) componentObj.setInstanceChecked?.(parseBool(checked));
11960
+ const sound = extSpecs.sound ? readXmlAttr(extAttrs, extSpecs.sound) : void 0;
11961
+ if (sound !== void 0) componentObj.setInstanceSound?.(sound);
11962
+ const soundVolumeScale = extSpecs.soundVolumeScale ? readXmlAttr(extAttrs, extSpecs.soundVolumeScale) : void 0;
11963
+ if (soundVolumeScale !== void 0) componentObj.setInstanceSoundVolumeScale?.(parseFloat2(soundVolumeScale, 1));
11755
11964
  const prompt = extSpecs.prompt ? readXmlAttr(extAttrs, extSpecs.prompt) : void 0;
11756
11965
  if (prompt !== void 0) componentObj.setInstancePromptText?.(prompt);
11757
11966
  const selectionController = extSpecs.selectionController ? readXmlAttr(extAttrs, extSpecs.selectionController) : void 0;
@@ -12418,6 +12627,10 @@ var ProjectWriter = class {
12418
12627
  const samplePointSize = fontRes.getSamplePointSize?.() ?? 0;
12419
12628
  if (samplePointSize !== 0) writeXmlAttr(attrs, PROJECT_XML_PROTOCOL.packageFontResource.attrs.samplePointSize, String(samplePointSize));
12420
12629
  }
12630
+ if (res.propertyType === "MovieClipResource") {
12631
+ const textureSetMode = res.getTextureSetMode?.() ?? "";
12632
+ if (textureSetMode) writeXmlAttr(attrs, PROJECT_XML_PROTOCOL.packageMovieClipResource.attrs.atlas, textureSetMode);
12633
+ }
12421
12634
  if (res.propertyType === "SpineResource" || res.propertyType === "DragonBonesResource") {
12422
12635
  const skeletonRes = res;
12423
12636
  writeXmlAttr(attrs, PROJECT_XML_PROTOCOL.packageSkeletonResource.attrs.width, String(skeletonRes.getWidth?.() ?? 0));
@@ -13099,6 +13312,8 @@ var ProjectWriter = class {
13099
13312
  if (typedObj.getInstanceController?.() && extSpecs.controller) writeXmlAttr(extAttrs, extSpecs.controller, typedObj.getInstanceController?.());
13100
13313
  if (typedObj.getInstancePage?.() && extSpecs.page) writeXmlAttr(extAttrs, extSpecs.page, typedObj.getInstancePage?.());
13101
13314
  if (typedObj.getInstanceChecked?.() && extSpecs.checked) writeXmlAttr(extAttrs, extSpecs.checked, "1");
13315
+ if (typedObj.getInstanceSound?.() && extSpecs.sound) writeXmlAttr(extAttrs, extSpecs.sound, typedObj.getInstanceSound?.());
13316
+ if ((typedObj.getInstanceSoundVolumeScale?.() ?? 1) !== 1 && extSpecs.soundVolumeScale) writeXmlAttr(extAttrs, extSpecs.soundVolumeScale, String(typedObj.getInstanceSoundVolumeScale?.() ?? 1));
13102
13317
  if (typedObj.getInstancePromptText?.() && extSpecs.prompt) writeXmlAttr(extAttrs, extSpecs.prompt, typedObj.getInstancePromptText?.());
13103
13318
  if (typedObj.getInstanceSelectionController?.() && extSpecs.selectionController) writeXmlAttr(extAttrs, extSpecs.selectionController, typedObj.getInstanceSelectionController?.());
13104
13319
  if ((typedObj.getInstanceVisibleItemCount?.() ?? 0) > 0 && extSpecs.visibleItemCount) writeXmlAttr(extAttrs, extSpecs.visibleItemCount, String(typedObj.getInstanceVisibleItemCount?.() ?? 0));
@@ -17569,8 +17784,8 @@ function decodeChildBlock6(resource, child, childBuf) {
17569
17784
  if (relatedControllerIndex >= 0) component.setInstanceController(resource.listControllers()[relatedControllerIndex]?.getName() ?? "");
17570
17785
  }
17571
17786
  component.setInstancePage(childBuf.readS() ?? "");
17572
- childBuf.readS();
17573
- if (childBuf.readBool() && remainingBytes(childBuf) >= 4) childBuf.getFloat32();
17787
+ component.setInstanceSound(childBuf.readS() ?? "");
17788
+ if (childBuf.readBool() && remainingBytes(childBuf) >= 4) component.setInstanceSoundVolumeScale(childBuf.getFloat32());
17574
17789
  if (remainingBytes(childBuf) >= 1) component.setInstanceChecked(childBuf.readBool());
17575
17790
  break;
17576
17791
  case "Label":
@@ -18144,8 +18359,9 @@ const BinItemType$1 = {
18144
18359
  Font: 5,
18145
18360
  Swf: 6,
18146
18361
  Misc: 7,
18147
- Spine: 8,
18148
- DragonBones: 9
18362
+ Unknown: 8,
18363
+ Spine: 9,
18364
+ DragonBones: 10
18149
18365
  };
18150
18366
  function normalizePackageResourcePath(path) {
18151
18367
  const normalized = path.replace(/\\/g, "/").trim();
@@ -19983,8 +20199,13 @@ function _writeExtensionInstanceData(buf, extType, child, comp, pkg, version) {
19983
20199
  buf.writeInt16(ctrlIdx >= 0 ? ctrlIdx : -1);
19984
20200
  } else buf.writeInt16(-1);
19985
20201
  buf.writeS(child.getInstancePage?.() ?? null);
19986
- buf.writeSEx(null, false, false);
19987
- buf.writeBool(false);
20202
+ const sound = child.getInstanceSound?.() ?? null;
20203
+ buf.writeSEx(remapLocalUiUrl(pkg, sound) ?? null, false, false);
20204
+ const soundVolume = child.getInstanceSoundVolumeScale?.();
20205
+ if (soundVolume !== void 0 && soundVolume !== null && soundVolume !== 1) {
20206
+ buf.writeBool(true);
20207
+ buf.writeFloat32(soundVolume);
20208
+ } else buf.writeBool(false);
19988
20209
  buf.writeBool(child.getInstanceChecked?.() ?? false);
19989
20210
  break;
19990
20211
  }
@@ -20165,8 +20386,9 @@ const BinItemType = {
20165
20386
  Atlas: 4,
20166
20387
  Font: 5,
20167
20388
  Misc: 7,
20168
- Spine: 8,
20169
- DragonBones: 9
20389
+ Unknown: 8,
20390
+ Spine: 9,
20391
+ DragonBones: 10
20170
20392
  };
20171
20393
  /**
20172
20394
  * Maps our PropertyType to the editor's type string used for sorting.
@@ -20825,228 +21047,60 @@ var PlatformIO = class {
20825
21047
  }
20826
21048
  };
20827
21049
  //#endregion
20828
- //#region ../core/src/io/node-io.ts
20829
- /**
20830
- * Node.js I/O implementation for reading and writing FairyGUI projects.
20831
- *
20832
- * Usage:
20833
- *
20834
- * ```ts
20835
- * import { NodeIO } from '@openfairygui/core/node';
20836
- *
20837
- * const io = new NodeIO();
20838
- * const doc = await io.readProject('./path/to/project.fairy');
20839
- * await io.writeProject(doc, './path/to/output.fairy');
20840
- * const doc2 = await io.readBinary('./path/to/package_fui.bytes');
20841
- * ```
20842
- *
20843
- * @category I/O
20844
- */
20845
- var NodeIO = class extends PlatformIO {
20846
- createFileSystem() {
20847
- return {
20848
- async readFile(filePath) {
20849
- return fs$1.readFile(filePath, "utf-8");
20850
- },
20851
- async readFileRaw(filePath) {
20852
- const buf = await fs$1.readFile(filePath);
20853
- return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
20854
- },
20855
- async writeFile(filePath, content) {
20856
- await fs$1.writeFile(filePath, content, "utf-8");
20857
- },
20858
- async writeFileRaw(filePath, data) {
20859
- await fs$1.writeFile(filePath, data);
20860
- },
20861
- async mkdir(dirPath) {
20862
- await fs$1.mkdir(dirPath, { recursive: true });
20863
- },
20864
- async readdir(dirPath) {
20865
- return (await fs$1.readdir(dirPath, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
20866
- },
20867
- async exists(filePath) {
20868
- try {
20869
- await fs$1.access(filePath);
20870
- return true;
20871
- } catch {
20872
- return false;
20873
- }
20874
- },
20875
- join(...paths) {
20876
- return path$1.join(...paths);
20877
- },
20878
- dirname(filePath) {
20879
- return path$1.dirname(filePath);
20880
- }
20881
- };
20882
- }
20883
- };
20884
- //#endregion
20885
- //#region ../functions/src/inspect.ts
20886
- function mapResource(resource) {
20887
- return {
20888
- name: resource.getName(),
20889
- id: resource.getId(),
20890
- path: resource.getPath?.() ?? "/",
20891
- exported: resource.getExported?.() ?? false
20892
- };
20893
- }
20894
- function mapComponentDetail(component, totals) {
20895
- const children = component.listChildren();
20896
- const controllers = component.listControllers();
20897
- const transitions = component.listTransitions();
20898
- totals.displayObjects += children.length;
20899
- totals.controllers += controllers.length;
20900
- totals.transitions += transitions.length;
20901
- for (const child of children) totals.gears += child.listGears().length;
20902
- return {
20903
- name: component.getName(),
20904
- id: component.getId(),
20905
- childCount: children.length,
20906
- controllerCount: controllers.length,
20907
- transitionCount: transitions.length
20908
- };
20909
- }
20910
- /**
20911
- * Generates a detailed report of the project contents.
20912
- *
20913
- * Unlike other transforms, `inspect()` does NOT modify the document —
20914
- * it returns a structured report.
20915
- *
20916
- * ```ts
20917
- * const report = inspect(doc);
20918
- * console.log(`${report.totals.packages} packages, ${report.totals.components} components`);
20919
- * ```
20920
- */
20921
- function inspect(doc) {
20922
- const root = doc.getRoot();
20923
- const totals = {
20924
- packages: 0,
20925
- images: 0,
20926
- sounds: 0,
20927
- fonts: 0,
20928
- movieClips: 0,
20929
- components: 0,
20930
- displayObjects: 0,
20931
- gears: 0,
20932
- controllers: 0,
20933
- transitions: 0
20934
- };
20935
- const packages = root.listPackages().map((pkg) => {
20936
- totals.packages++;
20937
- const resources = pkg.listResources();
20938
- const images = resources.filter((r) => r.propertyType === "ImageResource");
20939
- const sounds = resources.filter((r) => r.propertyType === "SoundResource");
20940
- const fonts = resources.filter((r) => r.propertyType === "FontResource");
20941
- const movieClips = resources.filter((r) => r.propertyType === "MovieClipResource");
20942
- const components = pkg.listComponents();
20943
- totals.images += images.length;
20944
- totals.sounds += sounds.length;
20945
- totals.fonts += fonts.length;
20946
- totals.movieClips += movieClips.length;
20947
- totals.components += components.length;
20948
- const componentDetails = components.map((component) => mapComponentDetail(component, totals));
20949
- return {
20950
- name: pkg.getName(),
20951
- id: pkg.getId(),
20952
- publishName: pkg.getPublishName() || pkg.getName(),
20953
- resources: {
20954
- images: {
20955
- count: images.length,
20956
- details: images.map(mapResource)
20957
- },
20958
- sounds: {
20959
- count: sounds.length,
20960
- details: sounds.map(mapResource)
20961
- },
20962
- fonts: {
20963
- count: fonts.length,
20964
- details: fonts.map(mapResource)
20965
- },
20966
- movieClips: {
20967
- count: movieClips.length,
20968
- details: movieClips.map(mapResource)
20969
- },
20970
- components: {
20971
- count: components.length,
20972
- details: components.map(mapResource)
20973
- }
20974
- },
20975
- componentDetails
20976
- };
20977
- });
20978
- return {
20979
- projectId: root.getProjectId(),
20980
- projectType: root.getProjectType(),
20981
- version: root.getVersion(),
20982
- packages,
20983
- totals
20984
- };
20985
- }
20986
- //#endregion
20987
- //#region ../functions/src/utils.ts
20988
- /**
20989
- * Wraps a transform function, assigning it a name for the transform stack.
20990
- */
20991
- function createTransform(name, fn) {
20992
- Object.defineProperty(fn, "name", { value: name });
20993
- return fn;
20994
- }
20995
- //#endregion
20996
- //#region ../functions/src/max-rects-compat.ts
20997
- const NO_ROTATION = 2;
20998
- const MAX_SCORE = 2147483647;
20999
- const MAX_RECTS_METHOD = {
21000
- BestShortSideFit: 0,
21001
- BestLongSideFit: 1,
21002
- BestAreaFit: 2,
21003
- BottomLeftRule: 3,
21004
- ContactPointRule: 4
21005
- };
21006
- const COMPAT_NODE_RECT_FLAGS = {
21007
- DUPLICATE_PADDING: 1,
21008
- NO_ROTATION
21009
- };
21010
- var MaxRectsCompat = class MaxRectsCompat {
21011
- static helperRect = createNodeRect();
21012
- binWidth = 0;
21013
- binHeight = 0;
21014
- allowRotations = false;
21015
- usedRectangles = [];
21016
- freeRectangles = [];
21017
- init(width, height, allowRotations = false) {
21018
- this.binWidth = width;
21019
- this.binHeight = height;
21020
- this.allowRotations = allowRotations;
21021
- this.usedRectangles.length = 0;
21022
- this.freeRectangles.length = 0;
21023
- this.freeRectangles.push({
21024
- ...createNodeRect(),
21025
- x: 0,
21026
- y: 0,
21027
- width,
21028
- height
21029
- });
21030
- }
21031
- insert(rect, method) {
21032
- const newNode = this.scoreRect(rect, method);
21033
- if (newNode.height === 0) return null;
21034
- const placed = cloneNodeRect(newNode);
21035
- this.placeRect(placed);
21036
- return placed;
21037
- }
21038
- pack(rects, method) {
21039
- const remaining = rects.map(cloneNodeRect);
21040
- while (remaining.length > 0) {
21041
- let bestIndex = -1;
21042
- const bestNode = createNodeRect();
21043
- bestNode.score1 = MAX_SCORE;
21044
- bestNode.score2 = MAX_SCORE;
21045
- for (let index = 0; index < remaining.length; index += 1) {
21046
- const candidate = this.scoreRect(remaining[index], method);
21047
- if (candidate.score1 < bestNode.score1 || candidate.score1 === bestNode.score1 && candidate.score2 < bestNode.score2) {
21048
- copyNodeRect(bestNode, candidate);
21049
- bestIndex = index;
21050
+ //#region ../functions/src/max-rects-compat.ts
21051
+ const NO_ROTATION = 2;
21052
+ const MAX_SCORE = 2147483647;
21053
+ const MAX_RECTS_METHOD = {
21054
+ BestShortSideFit: 0,
21055
+ BestLongSideFit: 1,
21056
+ BestAreaFit: 2,
21057
+ BottomLeftRule: 3,
21058
+ ContactPointRule: 4
21059
+ };
21060
+ const COMPAT_NODE_RECT_FLAGS = {
21061
+ DUPLICATE_PADDING: 1,
21062
+ NO_ROTATION
21063
+ };
21064
+ var MaxRectsCompat = class MaxRectsCompat {
21065
+ static helperRect = createNodeRect();
21066
+ binWidth = 0;
21067
+ binHeight = 0;
21068
+ allowRotations = false;
21069
+ usedRectangles = [];
21070
+ freeRectangles = [];
21071
+ init(width, height, allowRotations = false) {
21072
+ this.binWidth = width;
21073
+ this.binHeight = height;
21074
+ this.allowRotations = allowRotations;
21075
+ this.usedRectangles.length = 0;
21076
+ this.freeRectangles.length = 0;
21077
+ this.freeRectangles.push({
21078
+ ...createNodeRect(),
21079
+ x: 0,
21080
+ y: 0,
21081
+ width,
21082
+ height
21083
+ });
21084
+ }
21085
+ insert(rect, method) {
21086
+ const newNode = this.scoreRect(rect, method);
21087
+ if (newNode.height === 0) return null;
21088
+ const placed = cloneNodeRect(newNode);
21089
+ this.placeRect(placed);
21090
+ return placed;
21091
+ }
21092
+ pack(rects, method) {
21093
+ const remaining = rects.map(cloneNodeRect);
21094
+ while (remaining.length > 0) {
21095
+ let bestIndex = -1;
21096
+ const bestNode = createNodeRect();
21097
+ bestNode.score1 = MAX_SCORE;
21098
+ bestNode.score2 = MAX_SCORE;
21099
+ for (let index = 0; index < remaining.length; index += 1) {
21100
+ const candidate = this.scoreRect(remaining[index], method);
21101
+ if (candidate.score1 < bestNode.score1 || candidate.score1 === bestNode.score1 && candidate.score2 < bestNode.score2) {
21102
+ copyNodeRect(bestNode, candidate);
21103
+ bestIndex = index;
21050
21104
  }
21051
21105
  }
21052
21106
  if (bestIndex === -1) break;
@@ -21707,6 +21761,19 @@ const ATLAS_DEFAULTS = {
21707
21761
  function getPublishedItemId(resource) {
21708
21762
  return (resource.getExtras() ?? {})._publishedId ?? resource.getId();
21709
21763
  }
21764
+ function getSelectedSkeletonDependencyImageIds(resources) {
21765
+ const imageIds = /* @__PURE__ */ new Set();
21766
+ const resourcesById = new Map(resources.map((resource) => [resource.getId(), resource]));
21767
+ for (const resource of resources) {
21768
+ if (!isSkeletonResource$1(resource)) continue;
21769
+ for (const requiredId of resource.getRequireIds()) {
21770
+ if (!requiredId) continue;
21771
+ const required = resourcesById.get(requiredId);
21772
+ if (required && isImageResource$1(required)) imageIds.add(requiredId);
21773
+ }
21774
+ }
21775
+ return imageIds;
21776
+ }
21710
21777
  function resolveFontFileName(fontName) {
21711
21778
  return /\.fnt$/i.test(fontName) ? fontName : `${fontName}.fnt`;
21712
21779
  }
@@ -21816,7 +21883,7 @@ async function resolveEditorCompatibleResourceOrder(pkg, allResources, options)
21816
21883
  *
21817
21884
  * This transform performs MaxRects bin-packing on all ImageResource items
21818
21885
  * within each package, creating Atlas and Sprite property nodes. When an
21819
- * `encoder` (sharp) is provided, it also composites the actual PNG files.
21886
+ * a raster backend is provided, it also composites the actual PNG files.
21820
21887
  *
21821
21888
  * When `trimImage` is enabled and encoder is available, transparent pixels
21822
21889
  * at image edges are trimmed before packing. The trimmed offset and original
@@ -21843,12 +21910,18 @@ function atlas(_options = {}) {
21843
21910
  const logger = doc.getLogger();
21844
21911
  const encoder = options.encoder;
21845
21912
  const doTrim = options.trimImage && !!encoder && !!options.basePath;
21913
+ const packageFilter = options.packages ? new Set(options.packages) : null;
21846
21914
  for (const pkg of root.listPackages()) {
21915
+ if (packageFilter && !packageFilter.has(pkg.getName())) continue;
21847
21916
  const selectedPublishIds = new Set((pkg.getExtras() ?? {}).publishedResourceIds ?? []);
21848
21917
  const allResources = selectedPublishIds.size > 0 ? pkg.listResources().filter((resource) => selectedPublishIds.has(resource.getId())) : pkg.listResources();
21918
+ const skeletonDependencyImageIds = getSelectedSkeletonDependencyImageIds(allResources);
21849
21919
  const orderedResources = await resolveEditorCompatibleResourceOrder(pkg, allResources, options);
21850
21920
  const orderedAllResources = sortResourcesByOrder(allResources, new Map(orderedResources.map((resource, index) => [resource.getId(), index])), new Map(allResources.map((resource, index) => [resource.getId(), index])));
21851
- if (!allResources.some((resource) => isPackableResource(resource))) continue;
21921
+ if (!allResources.some((resource) => {
21922
+ if (isImageResource$1(resource) && skeletonDependencyImageIds.has(resource.getId())) return false;
21923
+ return isPackableResource(resource);
21924
+ })) continue;
21852
21925
  const inputs = [];
21853
21926
  const referencedIds = /* @__PURE__ */ new Set();
21854
21927
  const resourceMap = /* @__PURE__ */ new Map();
@@ -21917,6 +21990,7 @@ function atlas(_options = {}) {
21917
21990
  }
21918
21991
  for (const res of orderedAllResources) if (isImageResource$1(res)) {
21919
21992
  const resId = res.getId();
21993
+ if (skeletonDependencyImageIds.has(resId)) continue;
21920
21994
  if (selectedPublishIds.size === 0 && !res.getExported() && referencedIds.size > 0 && !referencedIds.has(resId)) continue;
21921
21995
  await _collectImage(res, pkg, inputs, encoder, options, doTrim, logger);
21922
21996
  } else if (isMovieClipResource$1(res)) {
@@ -21929,137 +22003,59 @@ function atlas(_options = {}) {
21929
22003
  await _collectFontTexture(doc, res, pkg, options);
21930
22004
  }
21931
22005
  if (inputs.length === 0) continue;
21932
- const branchGroups = buildBranchAtlasGroups(doc, inputs, options);
21933
22006
  let totalPageCount = 0;
21934
22007
  let usedDirectOutput = false;
22008
+ const { autoInputs, fixedPageGroups, standaloneGroups, reservedPageIndexes } = groupStandaloneInputs(doc, inputs, options);
22009
+ const branchGroups = buildBranchAtlasGroups(doc, autoInputs, options);
22010
+ const branchPageOffsets = /* @__PURE__ */ new Map();
21935
22011
  for (const group of branchGroups) {
21936
- const directOutput = resolveDirectImageOutput(group.inputs, options);
22012
+ const directOutput = fixedPageGroups.length === 0 && standaloneGroups.length === 0 ? resolveDirectImageOutput(group.inputs, options) : null;
21937
22013
  if (directOutput) {
21938
22014
  await emitDirectImageOutput(doc, pkg, directOutput, encoder, options, logger, group.branchName, group.branchOrdinal);
21939
22015
  usedDirectOutput = true;
21940
22016
  totalPageCount += 1;
21941
22017
  continue;
21942
22018
  }
21943
- const hasDuplicatePadding = group.inputs.some((i) => {
21944
- return isImageResource$1(i.resource) && i.resource.getDuplicatePadding?.() === true;
22019
+ const pageStart = reserveAutoPageStart(branchPageOffsets, group.branchOrdinal, reservedPageIndexes);
22020
+ const emittedPageCount = await emitPagedAtlasGroup(doc, pkg, allResources, group.inputs, {
22021
+ branchName: group.branchName,
22022
+ branchOrdinal: group.branchOrdinal,
22023
+ pageStart,
22024
+ fileNameAt: (pageIndex) => resolveAtlasOutputFileName(pkg, pageIndex, group.branchName),
22025
+ options,
22026
+ encoder,
22027
+ logger
21945
22028
  });
21946
- const pages = new MaxRectsPackerCompat({
21947
- pot: options.powerOfTwo,
21948
- mof: !options.powerOfTwo,
21949
- padding: options.padding,
21950
- rotation: options.allowRotation,
21951
- minWidth: 16,
21952
- minHeight: 16,
21953
- maxWidth: options.maxSize,
21954
- maxHeight: options.maxSize,
21955
- square: options.square,
21956
- fast: options.fast,
21957
- edgePadding: false,
21958
- duplicatePadding: hasDuplicatePadding,
21959
- multiPage: options.multiPage,
21960
- preserveInputOrderOnTie: options.preserveInputOrderOnTie
21961
- }).pack(group.inputs.map((input, index) => inputToCompatRect(input, index)));
21962
- if (!pages || pages.length === 0) continue;
21963
- totalPageCount += pages.length;
21964
- for (let p = 0; p < pages.length; p++) {
21965
- const page = pages[p];
21966
- const atlasNode = doc.createAtlas(`atlas${resolveAtlasIndex(group.branchOrdinal, p)}`);
21967
- atlasNode.setIndex(resolveAtlasIndex(group.branchOrdinal, p));
21968
- atlasNode.setFile(resolveAtlasOutputFileName(pkg, p, group.branchName));
21969
- atlasNode.setWidth(page.width);
21970
- atlasNode.setHeight(page.height);
21971
- pkg.addAtlas(atlasNode);
21972
- for (const pr of page.outputRects) {
21973
- const input = group.inputs[pr.index];
21974
- if (!input) continue;
21975
- const packedSize = resolvePackedRectSize(input, pr.width, pr.height, pr.rotated);
21976
- const rotated = pr.rotated;
21977
- const sprite = doc.createSprite();
21978
- sprite.setItemId(input.id);
21979
- sprite.setRectX(pr.x);
21980
- sprite.setRectY(pr.y);
21981
- sprite.setRectWidth(packedSize.width);
21982
- sprite.setRectHeight(packedSize.height);
21983
- sprite.setRotated(rotated);
21984
- sprite.setOffsetX(input.offsetX);
21985
- sprite.setOffsetY(input.offsetY);
21986
- sprite.setOriginalWidth(input.originalWidth);
21987
- sprite.setOriginalHeight(input.originalHeight);
21988
- sprite.setAtlas(atlasNode);
21989
- atlasNode.addSprite(sprite);
21990
- }
21991
- for (const res of allResources) {
21992
- if (!isFontResource$1(res)) continue;
21993
- const alias = res.getExtras()?._fontSpriteAlias;
21994
- if (!alias) continue;
21995
- const imgSprite = page.outputRects.find((result) => group.inputs[result.index]?.id === alias.textureId);
21996
- if (!imgSprite) continue;
21997
- const imgInput = group.inputs[imgSprite.index];
21998
- const fontSprite = doc.createSprite();
21999
- fontSprite.setItemId(alias.fontId);
22000
- fontSprite.setRectX(imgSprite.x);
22001
- fontSprite.setRectY(imgSprite.y);
22002
- fontSprite.setRectWidth(imgSprite.width);
22003
- fontSprite.setRectHeight(imgSprite.height);
22004
- fontSprite.setRotated(imgSprite.rotated);
22005
- if (imgInput) {
22006
- fontSprite.setOffsetX(imgInput.offsetX);
22007
- fontSprite.setOffsetY(imgInput.offsetY);
22008
- fontSprite.setOriginalWidth(imgInput.originalWidth);
22009
- fontSprite.setOriginalHeight(imgInput.originalHeight);
22010
- }
22011
- fontSprite.setAtlas(atlasNode);
22012
- atlasNode.addSprite(fontSprite);
22013
- }
22014
- }
22015
- if (encoder && options.outputPath) {
22016
- if (options.mkdir) await options.mkdir(options.outputPath);
22017
- for (let p = 0; p < pages.length; p++) {
22018
- const page = pages[p];
22019
- const compositeInputs = [];
22020
- for (const pr of page.outputRects) {
22021
- const input = group.inputs[pr.index];
22022
- if (!input) continue;
22023
- if (pr.width <= 0 || pr.height <= 0 || input.width <= 0 || input.height <= 0) continue;
22024
- try {
22025
- let imgBuffer;
22026
- if (input.trimBuffer) {
22027
- imgBuffer = input.trimBuffer;
22028
- if (imgBuffer.length === 0) continue;
22029
- } else if (input.rasterizedBuffer) imgBuffer = input.rasterizedBuffer;
22030
- else {
22031
- if (!isImageResource$1(input.resource)) {
22032
- logger.warn(`atlas: Non-image input "${input.id}" is missing inline buffer, skipping compositing.`);
22033
- continue;
22034
- }
22035
- imgBuffer = await encoder(_resolveImagePath(input.resource, pkg, options.basePath)).toBuffer();
22036
- }
22037
- if (pr.rotated) imgBuffer = await encoder(imgBuffer).rotate(270).toBuffer();
22038
- compositeInputs.push({
22039
- input: imgBuffer,
22040
- left: pr.x,
22041
- top: pr.y
22042
- });
22043
- } catch {
22044
- logger.warn(`atlas: Could not read image "${input.id}" for compositing.`);
22045
- }
22046
- }
22047
- const atlasFileName = resolveAtlasOutputFileName(pkg, p, group.branchName);
22048
- const outputFile = `${options.outputPath}/${atlasFileName}`;
22049
- await encoder({ create: {
22050
- width: page.width,
22051
- height: page.height,
22052
- channels: 4,
22053
- background: {
22054
- r: 0,
22055
- g: 0,
22056
- b: 0,
22057
- alpha: 0
22058
- }
22059
- } }).composite(compositeInputs).png().toFile(outputFile);
22060
- logger.info(`atlas: Generated ${atlasFileName} (${page.width}x${page.height}, ${page.outputRects.length} sprites)`);
22061
- }
22062
- }
22029
+ totalPageCount += emittedPageCount;
22030
+ branchPageOffsets.set(group.branchOrdinal, pageStart + emittedPageCount);
22031
+ }
22032
+ for (const group of fixedPageGroups) {
22033
+ const emittedPageCount = await emitPagedAtlasGroup(doc, pkg, allResources, group.inputs, {
22034
+ branchName: group.branchName,
22035
+ branchOrdinal: group.branchOrdinal,
22036
+ pageStart: group.pageIndex,
22037
+ forceSinglePage: true,
22038
+ fileNameAt: () => resolveAtlasOutputFileName(pkg, group.pageIndex, group.branchName),
22039
+ options,
22040
+ encoder,
22041
+ logger
22042
+ });
22043
+ totalPageCount += emittedPageCount;
22044
+ }
22045
+ const standalonePageOffsets = new Map(branchPageOffsets);
22046
+ for (const group of fixedPageGroups) {
22047
+ const nextPageIndex = group.pageIndex + 1;
22048
+ if (nextPageIndex > (standalonePageOffsets.get(group.branchOrdinal) ?? 0)) standalonePageOffsets.set(group.branchOrdinal, nextPageIndex);
22049
+ }
22050
+ for (const group of standaloneGroups) {
22051
+ const emittedPageCount = await emitStandaloneAtlasGroup(doc, pkg, group, {
22052
+ atlasIndexStart: standalonePageOffsets.get(group.branchOrdinal) ?? 0,
22053
+ options,
22054
+ encoder,
22055
+ logger
22056
+ });
22057
+ totalPageCount += emittedPageCount;
22058
+ standalonePageOffsets.set(group.branchOrdinal, (standalonePageOffsets.get(group.branchOrdinal) ?? 0) + emittedPageCount);
22063
22059
  }
22064
22060
  if (usedDirectOutput) logger.info(`atlas: Direct output for single image package "${pkg.getName()}".`);
22065
22061
  logger.info(`atlas: Packed ${inputs.length} images into ${totalPageCount} atlas(es) for package "${pkg.getName()}".`);
@@ -22096,6 +22092,171 @@ function buildBranchAtlasGroups(doc, inputs, options) {
22096
22092
  inputs: groups.get(branchName) ?? []
22097
22093
  }));
22098
22094
  }
22095
+ function reserveAutoPageStart(branchPageOffsets, branchOrdinal, reservedPageIndexes) {
22096
+ let pageIndex = branchPageOffsets.get(branchOrdinal) ?? 0;
22097
+ while (branchOrdinal === 0 && reservedPageIndexes.has(pageIndex)) pageIndex += 1;
22098
+ return pageIndex;
22099
+ }
22100
+ async function emitPagedAtlasGroup(doc, pkg, allResources, inputs, context) {
22101
+ if (inputs.length === 0) return 0;
22102
+ const pages = packAtlasPages(inputs, context.options, context.forceSinglePage === true);
22103
+ if (pages.length === 0) return 0;
22104
+ for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
22105
+ const page = pages[pageOffset];
22106
+ const pageIndex = context.pageStart + pageOffset;
22107
+ const atlasNode = doc.createAtlas(`atlas${resolveAtlasIndex(context.branchOrdinal, pageIndex)}`);
22108
+ atlasNode.setIndex(resolveAtlasIndex(context.branchOrdinal, pageIndex));
22109
+ atlasNode.setFile(context.fileNameAt(pageIndex));
22110
+ atlasNode.setWidth(page.width);
22111
+ atlasNode.setHeight(page.height);
22112
+ pkg.addAtlas(atlasNode);
22113
+ attachSpritesToAtlas(doc, allResources, inputs, page.outputRects, atlasNode);
22114
+ await writeAtlasPageImage(pkg, inputs, page, atlasNode.getFile(), context.encoder, context.options, context.logger);
22115
+ }
22116
+ return pages.length;
22117
+ }
22118
+ async function emitStandaloneAtlasGroup(doc, pkg, group, context) {
22119
+ if (group.inputs.length === 0) return 0;
22120
+ const pages = packAtlasPages(group.inputs, context.options, true, group.sizeMode === "npot" ? {
22121
+ powerOfTwo: false,
22122
+ multipleOfFour: false,
22123
+ square: false
22124
+ } : group.sizeMode === "multipleOf4" ? {
22125
+ powerOfTwo: false,
22126
+ multipleOfFour: true,
22127
+ square: false
22128
+ } : void 0);
22129
+ if (pages.length === 0) return 0;
22130
+ for (let pageOffset = 0; pageOffset < pages.length; pageOffset += 1) {
22131
+ const page = pages[pageOffset];
22132
+ const baseFileName = resolveStandaloneAtlasOutputFileName(pkg, group.resource, group.branchName);
22133
+ const atlasFileName = pages.length <= 1 ? baseFileName : insertFileNameSuffix(baseFileName, `_${pageOffset}`);
22134
+ const atlasIndex = context.atlasIndexStart + pageOffset;
22135
+ const atlasNode = doc.createAtlas(`atlas${resolveAtlasIndex(group.branchOrdinal, atlasIndex)}`);
22136
+ atlasNode.setIndex(resolveAtlasIndex(group.branchOrdinal, atlasIndex));
22137
+ atlasNode.setFile(atlasFileName);
22138
+ const standaloneSize = resolveStandaloneAtlasSize(page.width, page.height, group.sizeMode, context.options);
22139
+ atlasNode.setWidth(standaloneSize.width);
22140
+ atlasNode.setHeight(standaloneSize.height);
22141
+ pkg.addAtlas(atlasNode);
22142
+ attachSpritesToAtlas(doc, [], group.inputs, page.outputRects, atlasNode);
22143
+ await writeAtlasPageImage(pkg, group.inputs, {
22144
+ ...page,
22145
+ width: standaloneSize.width,
22146
+ height: standaloneSize.height
22147
+ }, atlasFileName, context.encoder, context.options, context.logger);
22148
+ }
22149
+ return pages.length;
22150
+ }
22151
+ function packAtlasPages(inputs, options, forceSinglePage, sizeOverrides) {
22152
+ const hasDuplicatePadding = inputs.some((input) => {
22153
+ return isImageResource$1(input.resource) && input.resource.getDuplicatePadding?.() === true;
22154
+ });
22155
+ return new MaxRectsPackerCompat({
22156
+ pot: sizeOverrides?.powerOfTwo ?? options.powerOfTwo,
22157
+ mof: sizeOverrides?.multipleOfFour ?? !options.powerOfTwo,
22158
+ padding: options.padding,
22159
+ rotation: options.allowRotation,
22160
+ minWidth: 16,
22161
+ minHeight: 16,
22162
+ maxWidth: options.maxSize,
22163
+ maxHeight: options.maxSize,
22164
+ square: sizeOverrides?.square ?? options.square,
22165
+ fast: options.fast,
22166
+ edgePadding: false,
22167
+ duplicatePadding: hasDuplicatePadding,
22168
+ multiPage: forceSinglePage ? false : options.multiPage,
22169
+ preserveInputOrderOnTie: options.preserveInputOrderOnTie
22170
+ }).pack(inputs.map((input, index) => inputToCompatRect(input, index))) ?? [];
22171
+ }
22172
+ function attachSpritesToAtlas(doc, allResources, inputs, outputRects, atlasNode) {
22173
+ for (const packedRect of outputRects) {
22174
+ const input = inputs[packedRect.index];
22175
+ if (!input) continue;
22176
+ const packedSize = resolvePackedRectSize(input, packedRect.width, packedRect.height, packedRect.rotated);
22177
+ const sprite = doc.createSprite();
22178
+ sprite.setItemId(input.id);
22179
+ sprite.setRectX(packedRect.x);
22180
+ sprite.setRectY(packedRect.y);
22181
+ sprite.setRectWidth(packedSize.width);
22182
+ sprite.setRectHeight(packedSize.height);
22183
+ sprite.setRotated(packedRect.rotated);
22184
+ sprite.setOffsetX(input.offsetX);
22185
+ sprite.setOffsetY(input.offsetY);
22186
+ sprite.setOriginalWidth(input.originalWidth);
22187
+ sprite.setOriginalHeight(input.originalHeight);
22188
+ sprite.setAtlas(atlasNode);
22189
+ atlasNode.addSprite(sprite);
22190
+ }
22191
+ for (const resource of allResources) {
22192
+ if (!isFontResource$1(resource)) continue;
22193
+ const alias = resource.getExtras()?._fontSpriteAlias;
22194
+ if (!alias) continue;
22195
+ const imageSprite = outputRects.find((result) => inputs[result.index]?.id === alias.textureId);
22196
+ if (!imageSprite) continue;
22197
+ const imageInput = inputs[imageSprite.index];
22198
+ const fontSprite = doc.createSprite();
22199
+ fontSprite.setItemId(alias.fontId);
22200
+ fontSprite.setRectX(imageSprite.x);
22201
+ fontSprite.setRectY(imageSprite.y);
22202
+ fontSprite.setRectWidth(imageSprite.width);
22203
+ fontSprite.setRectHeight(imageSprite.height);
22204
+ fontSprite.setRotated(imageSprite.rotated);
22205
+ if (imageInput) {
22206
+ fontSprite.setOffsetX(imageInput.offsetX);
22207
+ fontSprite.setOffsetY(imageInput.offsetY);
22208
+ fontSprite.setOriginalWidth(imageInput.originalWidth);
22209
+ fontSprite.setOriginalHeight(imageInput.originalHeight);
22210
+ }
22211
+ fontSprite.setAtlas(atlasNode);
22212
+ atlasNode.addSprite(fontSprite);
22213
+ }
22214
+ }
22215
+ async function writeAtlasPageImage(pkg, inputs, page, atlasFileName, encoder, options, logger) {
22216
+ if (!encoder || !options.outputPath) return;
22217
+ if (options.mkdir) await options.mkdir(options.outputPath);
22218
+ const compositeInputs = [];
22219
+ for (const packedRect of page.outputRects) {
22220
+ const input = inputs[packedRect.index];
22221
+ if (!input) continue;
22222
+ if (packedRect.width <= 0 || packedRect.height <= 0 || input.width <= 0 || input.height <= 0) continue;
22223
+ try {
22224
+ let imageBuffer;
22225
+ if (input.trimBuffer) {
22226
+ imageBuffer = input.trimBuffer;
22227
+ if (imageBuffer.length === 0) continue;
22228
+ } else if (input.rasterizedBuffer) imageBuffer = input.rasterizedBuffer;
22229
+ else {
22230
+ if (!isImageResource$1(input.resource)) {
22231
+ logger.warn(`atlas: Non-image input "${input.id}" is missing inline buffer, skipping compositing.`);
22232
+ continue;
22233
+ }
22234
+ imageBuffer = await encoder(_resolveImagePath(input.resource, pkg, options.basePath)).toBuffer();
22235
+ }
22236
+ if (packedRect.rotated) imageBuffer = await encoder(imageBuffer).rotate(270).toBuffer();
22237
+ compositeInputs.push({
22238
+ input: imageBuffer,
22239
+ left: packedRect.x,
22240
+ top: packedRect.y
22241
+ });
22242
+ } catch {
22243
+ logger.warn(`atlas: Could not read image "${input.id}" for compositing.`);
22244
+ }
22245
+ }
22246
+ const outputFile = `${options.outputPath}/${atlasFileName}`;
22247
+ await encoder({ create: {
22248
+ width: page.width,
22249
+ height: page.height,
22250
+ channels: 4,
22251
+ background: {
22252
+ r: 0,
22253
+ g: 0,
22254
+ b: 0,
22255
+ alpha: 0
22256
+ }
22257
+ } }).composite(compositeInputs).toFile(outputFile);
22258
+ logger.info(`atlas: Generated ${atlasFileName} (${page.width}x${page.height}, ${page.outputRects.length} sprites)`);
22259
+ }
22099
22260
  function inputToCompatRect(input, index) {
22100
22261
  const duplicatePadding = isImageResource$1(input.resource) && input.resource.getDuplicatePadding?.() === true;
22101
22262
  return {
@@ -22210,14 +22371,47 @@ function resolveAtlasOutputFileName(pkg, pageIndex, branchName) {
22210
22371
  const suffix = branchName ? `_${branchName}` : "";
22211
22372
  return `${pkg.getPublishName() || pkg.getName()}_atlas${pageIndex}${suffix}.png`;
22212
22373
  }
22374
+ function resolveStandaloneAtlasOutputFileName(pkg, resource, branchName) {
22375
+ const baseName = `${pkg.getPublishName() || pkg.getName()}_atlas_${getPublishedItemId(resource)}`;
22376
+ const suffix = branchName ? `_${branchName}` : "";
22377
+ if (isImageResource$1(resource)) return `${baseName}${suffix}${extname$1(resolveImageFileName$1(resource)) || ".png"}`;
22378
+ return `${baseName}${suffix}.png`;
22379
+ }
22380
+ function resolveStandaloneAtlasSize(width, height, sizeMode, options) {
22381
+ if (sizeMode === "npot") return {
22382
+ width,
22383
+ height
22384
+ };
22385
+ if (sizeMode === "multipleOf4") return {
22386
+ width: roundUpToMultiple(width, 4),
22387
+ height: roundUpToMultiple(height, 4)
22388
+ };
22389
+ return resolveDirectOutputAtlasSize(width, height, options);
22390
+ }
22213
22391
  function resolveImageFileName$1(resource) {
22214
22392
  const extras = resource.getExtras();
22215
22393
  return resource.getFileName() || extras._fileName || resource.getName();
22216
22394
  }
22395
+ function extname$1(fileName) {
22396
+ const normalized = fileName.replace(/\\/g, "/");
22397
+ const lastSlash = normalized.lastIndexOf("/");
22398
+ const lastDot = normalized.lastIndexOf(".");
22399
+ if (lastDot <= lastSlash) return "";
22400
+ return normalized.slice(lastDot);
22401
+ }
22402
+ function insertFileNameSuffix(fileName, suffix) {
22403
+ const extension = extname$1(fileName);
22404
+ if (!extension) return `${fileName}${suffix}`;
22405
+ return `${fileName.slice(0, -extension.length)}${suffix}${extension}`;
22406
+ }
22217
22407
  function nextPow2(value) {
22218
22408
  if (value <= 1) return 1;
22219
22409
  return 2 ** Math.ceil(Math.log2(value));
22220
22410
  }
22411
+ function roundUpToMultiple(value, base) {
22412
+ if (value <= 0) return 0;
22413
+ return Math.ceil(value / base) * base;
22414
+ }
22221
22415
  function sortResourcesByOrder(resources, orderMap, inputOrderMap) {
22222
22416
  const ordered = [...resources];
22223
22417
  ordered.sort((left, right) => {
@@ -22233,8 +22427,65 @@ function sortResourcesByOrder(resources, orderMap, inputOrderMap) {
22233
22427
  });
22234
22428
  return ordered;
22235
22429
  }
22430
+ function getResourceTextureSetMode(resource) {
22431
+ if (isImageResource$1(resource)) return parseTextureSetMode(resource.getTextureSetMode?.());
22432
+ return parseTextureSetMode(resource.getTextureSetMode?.());
22433
+ }
22434
+ function groupStandaloneInputs(doc, inputs, options) {
22435
+ const autoInputs = [];
22436
+ const fixedInputsByPage = /* @__PURE__ */ new Map();
22437
+ const standaloneGroups = /* @__PURE__ */ new Map();
22438
+ const reservedPageIndexes = /* @__PURE__ */ new Set();
22439
+ const discoveredBranchNames = [...new Set(inputs.map((input) => getInputBranchName(input)).filter((branchName) => !!branchName))];
22440
+ const orderedBranchNames = doc.getRoot().listBranches().filter((branchName) => discoveredBranchNames.includes(branchName));
22441
+ for (const branchName of discoveredBranchNames) if (!orderedBranchNames.includes(branchName)) orderedBranchNames.push(branchName);
22442
+ const branchOrdinalByName = /* @__PURE__ */ new Map();
22443
+ branchOrdinalByName.set("", 0);
22444
+ if (options.separatedAtlasForBranch) {
22445
+ let ordinal = 1;
22446
+ for (const branchName of orderedBranchNames) branchOrdinalByName.set(branchName, ordinal++);
22447
+ } else for (const branchName of orderedBranchNames) branchOrdinalByName.set(branchName, 0);
22448
+ for (const input of inputs) {
22449
+ const branchName = getInputBranchName(input);
22450
+ const branchOrdinal = branchOrdinalByName.get(branchName) ?? 0;
22451
+ const mode = getResourceTextureSetMode(input.resource);
22452
+ if (mode.kind === "standalone") {
22453
+ const key = `${branchName}\u0000${getPublishedItemId(input.resource)}`;
22454
+ const existing = standaloneGroups.get(key);
22455
+ if (existing) existing.inputs.push(input);
22456
+ else standaloneGroups.set(key, {
22457
+ resource: input.resource,
22458
+ branchName,
22459
+ branchOrdinal,
22460
+ sizeMode: mode.sizeMode,
22461
+ inputs: [input]
22462
+ });
22463
+ continue;
22464
+ }
22465
+ if (mode.kind === "page") {
22466
+ reservedPageIndexes.add(mode.pageIndex);
22467
+ const key = `${branchName}\u0000${mode.pageIndex}`;
22468
+ const existing = fixedInputsByPage.get(key);
22469
+ if (existing) existing.inputs.push(input);
22470
+ else fixedInputsByPage.set(key, {
22471
+ pageIndex: mode.pageIndex,
22472
+ branchName,
22473
+ branchOrdinal,
22474
+ inputs: [input]
22475
+ });
22476
+ continue;
22477
+ }
22478
+ autoInputs.push(input);
22479
+ }
22480
+ return {
22481
+ autoInputs,
22482
+ fixedPageGroups: [...fixedInputsByPage.values()].sort((left, right) => left.branchOrdinal - right.branchOrdinal || left.pageIndex - right.pageIndex),
22483
+ standaloneGroups: [...standaloneGroups.values()].sort((left, right) => left.branchOrdinal - right.branchOrdinal || getPublishedItemId(left.resource).localeCompare(getPublishedItemId(right.resource))),
22484
+ reservedPageIndexes
22485
+ };
22486
+ }
22236
22487
  /**
22237
- * Trim transparent edges from an image using sharp.
22488
+ * Trim transparent edges from an image using the host raster backend.
22238
22489
  * Returns the trimmed buffer, dimensions, and offsets.
22239
22490
  * Falls back to the original image if trim fails (e.g. no alpha channel, no transparent edges).
22240
22491
  */
@@ -22324,7 +22575,11 @@ async function _collectImage(resource, pkg, inputs, encoder, options, doTrim, lo
22324
22575
  }
22325
22576
  sourceHasAlpha = metadata.hasAlpha === true || metadata.channels === 4;
22326
22577
  if (/\.svg$/i.test(resolveImageFileName$1(resource)) && declaredWidth > 0 && declaredHeight > 0) {
22327
- rasterizedBuffer = await encoder(filePath).resize(declaredWidth, declaredHeight, { fit: "fill" }).png().toBuffer();
22578
+ rasterizedBuffer = await encoder(filePath).resize({
22579
+ width: declaredWidth,
22580
+ height: declaredHeight,
22581
+ fit: "fill"
22582
+ }).png().toBuffer();
22328
22583
  sourceHasAlpha = true;
22329
22584
  }
22330
22585
  } catch {
@@ -22793,6 +23048,11 @@ const FGUI_TYPESCRIPT_BINDER_TEMPLATE = `{{generatedMark}}
22793
23048
  }
22794
23049
  `;
22795
23050
  //#endregion
23051
+ //#region ../functions/src/plugins/types.ts
23052
+ function formatPluginError(error) {
23053
+ return error instanceof Error ? error.message : String(error);
23054
+ }
23055
+ //#endregion
22796
23056
  //#region ../functions/src/codegen.ts
22797
23057
  const AUTO_GENERATED_CODE_MARK = "/** This is an automatically generated class by FairyGUI. Please do not modify it. **/";
22798
23058
  const DEFAULT_CLASS_NAME_PREFIX = "UI_";
@@ -22828,6 +23088,18 @@ async function publishCodeGeneration(doc, options) {
22828
23088
  const logger = doc.getLogger();
22829
23089
  const settings = resolveCodeGenerationSettings(doc);
22830
23090
  if (!settings.allowGenCode) return;
23091
+ const plugins = options.plugins?.filter((plugin) => typeof plugin.plugin.genCode === "function") ?? [];
23092
+ if (plugins.length > 0) {
23093
+ let handled = false;
23094
+ for (const plugin of plugins) try {
23095
+ await plugin.plugin.genCode(doc, settings, options);
23096
+ handled = true;
23097
+ logger.info(`publish: Generated code using plugin "${plugin.name}"`);
23098
+ } catch (error) {
23099
+ logger.warn(`publish: Code generation plugin "${plugin.name}" failed: ${formatPluginError(error)}`);
23100
+ }
23101
+ if (handled) return;
23102
+ }
22831
23103
  for (const pkg of options.packages) {
22832
23104
  if (!pkg.getGenCode()) continue;
22833
23105
  const plan = resolvePackageCodegenPlan(pkg, settings, options);
@@ -22926,9 +23198,9 @@ async function cleanupGeneratedFiles(directory, fs, extension = ".cs") {
22926
23198
  }
22927
23199
  }
22928
23200
  function buildCodegenClasses(doc, pkg, plan) {
22929
- const exportedComponents = pkg.listComponents().filter((component) => component.getExported()).sort((left, right) => left.getId().localeCompare(right.getId()));
23201
+ const codegenComponents = pkg.listComponents().sort((left, right) => left.getId().localeCompare(right.getId()));
22930
23202
  const generatedById = /* @__PURE__ */ new Map();
22931
- for (const component of exportedComponents) {
23203
+ for (const component of codegenComponents) {
22932
23204
  const encodedClassName = `${plan.settings.classNamePrefix}${normalizeTypeName(component.getName()) || "Component"}`;
22933
23205
  generatedById.set(component.getId(), {
22934
23206
  classId: component.getId(),
@@ -22941,7 +23213,13 @@ function buildCodegenClasses(doc, pkg, plan) {
22941
23213
  members: []
22942
23214
  });
22943
23215
  }
22944
- for (const component of exportedComponents) {
23216
+ for (const component of codegenComponents) {
23217
+ const classInfo = generatedById.get(component.getId());
23218
+ if (!classInfo) continue;
23219
+ classInfo.members = buildCodegenMembers(doc, pkg, component, plan, generatedById);
23220
+ }
23221
+ for (const [componentId, classInfo] of generatedById) if (classInfo.members.every((member) => member.ignored)) generatedById.delete(componentId);
23222
+ for (const component of codegenComponents) {
22945
23223
  const classInfo = generatedById.get(component.getId());
22946
23224
  if (!classInfo) continue;
22947
23225
  classInfo.members = buildCodegenMembers(doc, pkg, component, plan, generatedById);
@@ -22955,7 +23233,12 @@ function buildCodegenMembers(doc, pkg, component, plan, generatedById) {
22955
23233
  let childIndex = 0;
22956
23234
  let transitionIndex = 0;
22957
23235
  for (const controller of component.listControllers()) members.push(createMember(ownerType, "controller", "Controller", controller.getName(), controllerIndex++, plan));
22958
- for (const child of component.listChildren()) members.push(createMember(ownerType, "child", resolveChildType(doc, pkg, child, generatedById), child.getName(), childIndex++, plan));
23236
+ for (const child of component.listChildren()) {
23237
+ if (!isRuntimeChild(child)) continue;
23238
+ const index = childIndex++;
23239
+ const resolvedChild = resolveChildType(doc, pkg, child, generatedById);
23240
+ members.push(createMember(ownerType, "child", resolvedChild.type, child.getName(), index, plan, resolvedChild.referencedComponent));
23241
+ }
22959
23242
  for (const transition of component.listTransitions()) members.push(createMember(ownerType, "transition", "Transition", transition.getName(), transitionIndex++, plan));
22960
23243
  const usedNames = /* @__PURE__ */ new Map();
22961
23244
  for (const member of members) {
@@ -22967,7 +23250,10 @@ function buildCodegenMembers(doc, pkg, component, plan, generatedById) {
22967
23250
  }
22968
23251
  return members;
22969
23252
  }
22970
- function createMember(ownerType, kind, type, originalName, index, plan) {
23253
+ function isRuntimeChild(child) {
23254
+ return child.propertyType !== "GGroup" || child.getAdvanced?.() === true;
23255
+ }
23256
+ function createMember(ownerType, kind, type, originalName, index, plan, referencedComponent) {
22971
23257
  const ignored = plan.settings.ignoreNoname && isDefaultMemberName(ownerType, kind, originalName);
22972
23258
  return {
22973
23259
  index,
@@ -22975,30 +23261,41 @@ function createMember(ownerType, kind, type, originalName, index, plan) {
22975
23261
  name: applyMemberNamePrefix(originalName, plan.settings.memberNamePrefix),
22976
23262
  originalName,
22977
23263
  type,
22978
- ignored
23264
+ ignored,
23265
+ referencedComponent
22979
23266
  };
22980
23267
  }
22981
23268
  function resolveChildType(doc, pkg, child, generatedById) {
22982
23269
  const src = child.getSrc?.();
22983
23270
  if (src) {
22984
- const localResource = resolveChildSourceComponent(doc, pkg, src);
22985
- if (localResource) return generatedById.get(localResource.getId())?.encodedClassName ?? resolveComponentBaseType(localResource);
23271
+ let referencedComponent = null;
23272
+ if (src.startsWith("ui://")) {
23273
+ const rest = src.slice(5);
23274
+ const pkgId = rest.slice(0, 8);
23275
+ const resourceId = rest.slice(8);
23276
+ const targetPackage = doc.getRoot().listPackages().find((candidate) => candidate.getId() === pkgId);
23277
+ const targetResource = targetPackage?.getResourceById(resourceId);
23278
+ if (targetPackage && targetResource?.propertyType === "Component") referencedComponent = {
23279
+ component: targetResource,
23280
+ package: targetPackage
23281
+ };
23282
+ } else {
23283
+ const packageId = child.getPackageId?.();
23284
+ const targetPackage = packageId ? doc.getRoot().listPackages().find((candidate) => candidate.getId() === packageId) : pkg;
23285
+ const targetResource = targetPackage?.getResourceById(src);
23286
+ if (targetPackage && targetResource?.propertyType === "Component") referencedComponent = {
23287
+ component: targetResource,
23288
+ package: targetPackage
23289
+ };
23290
+ }
23291
+ if (referencedComponent) return {
23292
+ type: (referencedComponent.package === pkg ? generatedById.get(referencedComponent.component.getId()) : void 0)?.encodedClassName ?? resolveComponentBaseType(referencedComponent.component),
23293
+ referencedComponent
23294
+ };
22986
23295
  }
22987
23296
  const instanceExtType = child.getInstanceExtType?.();
22988
- if (instanceExtType) return `G${instanceExtType}`;
22989
- return child.propertyType;
22990
- }
22991
- function resolveChildSourceComponent(doc, pkg, src) {
22992
- if (!src) return null;
22993
- if (src.startsWith("ui://")) {
22994
- const rest = src.slice(5);
22995
- const pkgId = rest.slice(0, 8);
22996
- const resourceId = rest.slice(8);
22997
- const targetResource = doc.getRoot().listPackages().find((candidate) => candidate.getId() === pkgId)?.getResourceById(resourceId);
22998
- return targetResource?.propertyType === "Component" ? targetResource : null;
22999
- }
23000
- const localResource = pkg.getResourceById(src);
23001
- return localResource?.propertyType === "Component" ? localResource : null;
23297
+ if (instanceExtType) return { type: `G${instanceExtType}` };
23298
+ return { type: child.propertyType };
23002
23299
  }
23003
23300
  function resolveComponentBaseType(component) {
23004
23301
  const extensionType = component.getExtensionType();
@@ -23069,21 +23366,21 @@ function renderFguiTypescriptMemberAssignment(member, getMemberByName, variant)
23069
23366
  return getMemberByName ? `\t\tthis.${member.name} = <${translatedType}><any>(this.getChild("${escapeTypeScriptString(member.originalName)}"));` : `\t\tthis.${member.name} = <${translatedType}><any>(this.getChildAt(${member.index}));`;
23070
23367
  }
23071
23368
  function resolveCodePath(codePath, basePath, fs) {
23072
- if (isAbsolutePath(codePath)) return trimTrailingSlashes$1(codePath);
23369
+ if (isAbsolutePath(codePath)) return trimTrailingSlashes$2(codePath);
23073
23370
  const projectBasePath = resolveProjectBasePath(basePath);
23074
- return projectBasePath ? trimTrailingSlashes$1(fs.join(projectBasePath, codePath)) : trimTrailingSlashes$1(codePath);
23371
+ return projectBasePath ? trimTrailingSlashes$2(fs.join(projectBasePath, codePath)) : trimTrailingSlashes$2(codePath);
23075
23372
  }
23076
23373
  function resolveProjectBasePath(basePath) {
23077
23374
  if (!basePath) return "";
23078
- const normalized = trimTrailingSlashes$1(basePath);
23375
+ const normalized = trimTrailingSlashes$2(basePath);
23079
23376
  const assetsMatch = normalized.match(/^(.*)[/\\]assets(?:_[^/\\]+)?$/i);
23080
23377
  if (assetsMatch?.[1]) return assetsMatch[1];
23081
23378
  return dirname$2(normalized);
23082
23379
  }
23083
23380
  function dirname$2(filePath) {
23084
- return trimTrailingSlashes$1(filePath).match(/^(.*)[/\\][^/\\]+$/)?.[1] ?? "";
23381
+ return trimTrailingSlashes$2(filePath).match(/^(.*)[/\\][^/\\]+$/)?.[1] ?? "";
23085
23382
  }
23086
- function trimTrailingSlashes$1(value) {
23383
+ function trimTrailingSlashes$2(value) {
23087
23384
  return value.replace(/[/\\]+$/, "");
23088
23385
  }
23089
23386
  function isAbsolutePath(value) {
@@ -23092,9 +23389,15 @@ function isAbsolutePath(value) {
23092
23389
  function isDefaultMemberName(ownerType, kind, name) {
23093
23390
  if (kind === "controller") return (ownerType === "GButton" || ownerType === "GComboBox") && name === "button";
23094
23391
  if (kind === "transition") return false;
23095
- if (ownerType === "GButton" || ownerType === "GLabel" || ownerType === "GComboBox") return name === "title" || name === "icon";
23096
- if (ownerType === "GProgressBar") return name === "bar" || name === "bar_v" || name === "title" || name === "ani";
23097
- if (ownerType === "GSlider") return name === "bar" || name === "bar_v" || name === "grip" || name === "title" || name === "ani";
23392
+ if (ownerType === "GButton" || ownerType === "GLabel" || ownerType === "GComboBox") {
23393
+ if (name === "title" || name === "icon") return true;
23394
+ }
23395
+ if (ownerType === "GProgressBar") {
23396
+ if (name === "bar" || name === "bar_v" || name === "title" || name === "ani") return true;
23397
+ }
23398
+ if (ownerType === "GSlider") {
23399
+ if (name === "bar" || name === "bar_v" || name === "grip" || name === "title" || name === "ani") return true;
23400
+ }
23098
23401
  return /^n\d+(?:_.*)?$/i.test(name);
23099
23402
  }
23100
23403
  function applyMemberNamePrefix(name, prefix) {
@@ -23383,11 +23686,11 @@ function inferPackageName(fileName) {
23383
23686
  if (/\.fui$/i.test(fileName)) return fileName.replace(/\.fui$/i, "");
23384
23687
  return fileName.replace(/\.bin$/i, "");
23385
23688
  }
23386
- function trimTrailingSlashes(value) {
23689
+ function trimTrailingSlashes$1(value) {
23387
23690
  return value.replace(/[/\\]+$/, "");
23388
23691
  }
23389
23692
  function normalizeComparablePath(value) {
23390
- const normalized = trimTrailingSlashes(value).replace(/\\/g, "/");
23693
+ const normalized = trimTrailingSlashes$1(value).replace(/\\/g, "/");
23391
23694
  const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
23392
23695
  const drivePrefix = driveMatch?.[1].toLowerCase() ?? "";
23393
23696
  const remainder = driveMatch ? driveMatch[2] ?? "" : normalized;
@@ -23407,14 +23710,14 @@ function normalizeComparablePath(value) {
23407
23710
  return (drivePrefix ? `${drivePrefix}/${joined}`.replace(/\/$/, "") : hasRoot ? `/${joined}`.replace(/\/$/, "") : joined || ".").toLowerCase();
23408
23711
  }
23409
23712
  function dirname$1(filePath) {
23410
- return trimTrailingSlashes(filePath).match(/^(.*)[/\\][^/\\]+$/)?.[1] ?? "";
23713
+ return trimTrailingSlashes$1(filePath).match(/^(.*)[/\\][^/\\]+$/)?.[1] ?? "";
23411
23714
  }
23412
23715
  function basename(filePath) {
23413
- return trimTrailingSlashes(filePath).match(/([^/\\]+)$/)?.[1] ?? "";
23716
+ return trimTrailingSlashes$1(filePath).match(/([^/\\]+)$/)?.[1] ?? "";
23414
23717
  }
23415
23718
  function resolveOutputProjectPath(output, fs) {
23416
23719
  if (/\.fairy$/i.test(output)) return output;
23417
- const normalizedOutput = trimTrailingSlashes(output);
23720
+ const normalizedOutput = trimTrailingSlashes$1(output);
23418
23721
  const projectName = basename(normalizedOutput) || "Restored";
23419
23722
  return fs.join(normalizedOutput, `${projectName}.fairy`);
23420
23723
  }
@@ -23460,7 +23763,7 @@ async function prepareRestoreOutputDir(inputDir, outputDir, outputProjectPath, f
23460
23763
  await fs.mkdir(outputDir);
23461
23764
  }
23462
23765
  async function restore(options) {
23463
- const sourceDir = trimTrailingSlashes(options.inputDir);
23766
+ const sourceDir = trimTrailingSlashes$1(options.inputDir);
23464
23767
  const outputIsProjectFile = /\.fairy$/i.test(options.output);
23465
23768
  const outputProjectPath = resolveOutputProjectPath(options.output, options.fs);
23466
23769
  await prepareRestoreOutputDir(sourceDir, dirname$1(outputProjectPath) || ".", outputProjectPath, options.fs, options.force === true, outputIsProjectFile);
@@ -23602,16 +23905,23 @@ var RestoreWorkflow = class {
23602
23905
  async _ensureLooseImageResource(doc, pkg, owner, sourceDir, fileName) {
23603
23906
  const resources = pkg.listResources();
23604
23907
  const existing = this._findResourceByFile(resources, owner, "ImageResource", fileName);
23605
- if (existing) return existing;
23606
23908
  const sourcePath = await this._resolveLooseSourceFile(pkg, sourceDir, fileName);
23607
- if (!sourcePath) return null;
23909
+ if (!sourcePath) return existing ?? null;
23910
+ if (existing) {
23911
+ existing.setExtras?.({
23912
+ ...existing.getExtras?.() ?? {},
23913
+ _publishedFile: fileBaseName(sourcePath),
23914
+ _restoreAsLooseImage: true
23915
+ });
23916
+ return existing;
23917
+ }
23608
23918
  const resource = doc.createImageResource(stripExtension(fileName));
23609
23919
  resource.setId(generateId()).setPath(owner.getPath?.() ?? "/").setBranch(owner.getBranch?.() ?? "").setBranchItemIds(owner.getBranchItemIds?.() ?? []).setExported(false).setFileName(fileName);
23610
23920
  resource.setExtras?.({
23611
23921
  ...resource.getExtras?.() ?? {},
23612
23922
  _publishedFile: fileBaseName(sourcePath),
23613
23923
  _suppressPackageSize: true,
23614
- _syntheticLooseImage: true
23924
+ _restoreAsLooseImage: true
23615
23925
  });
23616
23926
  pkg.addResource(resource);
23617
23927
  return resource;
@@ -23785,13 +24095,13 @@ var RestoreWorkflow = class {
23785
24095
  }
23786
24096
  async _copyLooseResources(pkg, options, warnings) {
23787
24097
  for (const resource of pkg.listResources()) {
23788
- const syntheticLooseImage = resource.getExtras?.()?._syntheticLooseImage === true;
24098
+ const restoreAsLooseImage = resource.getExtras?.()?._restoreAsLooseImage === true;
23789
24099
  if (![
23790
24100
  "SoundResource",
23791
24101
  "MiscResource",
23792
24102
  "SpineResource",
23793
24103
  "DragonBonesResource"
23794
- ].includes(resource.propertyType) && !syntheticLooseImage) continue;
24104
+ ].includes(resource.propertyType) && !restoreAsLooseImage) continue;
23795
24105
  const fileName = resourceFileName(resource);
23796
24106
  if (!fileName) continue;
23797
24107
  const sourcePath = await this._resolveSourceFile(options.sourceDir, this._sourceFileCandidates(pkg, resourcePublishedFileName(resource), fileName));
@@ -23961,6 +24271,18 @@ var RestoreWorkflow = class {
23961
24271
  };
23962
24272
  //#endregion
23963
24273
  //#region ../functions/src/publish.ts
24274
+ async function runPublishPluginHook(plugins, hook, doc, options) {
24275
+ const logger = doc.getLogger();
24276
+ for (const plugin of plugins) {
24277
+ const fn = plugin.plugin[hook];
24278
+ if (typeof fn !== "function") continue;
24279
+ try {
24280
+ await fn(doc, options);
24281
+ } catch (error) {
24282
+ logger.warn(`publish: Plugin "${plugin.name}" ${hook} failed: ${formatPluginError(error)}`);
24283
+ }
24284
+ }
24285
+ }
23964
24286
  const UNITY_PROJECT_TYPE = ProjectType.Unity;
23965
24287
  const COCOS_CREATOR_PROJECT_TYPE = ProjectType.CocosCreator;
23966
24288
  function resolveDefaultPublishFileExtension(projectType, publishSettings) {
@@ -24010,6 +24332,19 @@ function resolvePublishOptions(doc, overrides = {}) {
24010
24332
  atlas: atlasOptions
24011
24333
  };
24012
24334
  }
24335
+ function trimTrailingSlashes(value) {
24336
+ return value.replace(/[/\\]+$/, "");
24337
+ }
24338
+ function isAbsolutePathLike(value) {
24339
+ return /^(?:[a-zA-Z]:[/\\]|[/\\]{1,2})/u.test(value);
24340
+ }
24341
+ function joinPathSegments(left, right) {
24342
+ const normalizedLeft = trimTrailingSlashes(left);
24343
+ const normalizedRight = right.replace(/^[/\\]+/, "");
24344
+ if (!normalizedLeft) return normalizedRight;
24345
+ if (!normalizedRight) return normalizedLeft;
24346
+ return `${normalizedLeft}${normalizedLeft.includes("\\") ? "\\" : "/"}${normalizedRight}`;
24347
+ }
24013
24348
  function dirname(filePath) {
24014
24349
  return filePath.replace(/[/\\]+$/, "").match(/^(.*)[/\\][^/\\]+$/)?.[1] ?? "";
24015
24350
  }
@@ -24125,13 +24460,14 @@ function extname(fileName) {
24125
24460
  if (lastDot <= lastSlash) return "";
24126
24461
  return normalized.slice(lastDot);
24127
24462
  }
24128
- function resolvePublishedMiscFileName(resource) {
24463
+ function resolvePublishedMiscFileName(resource, projectType) {
24129
24464
  const file = resource.getFile();
24465
+ if (projectType !== UNITY_PROJECT_TYPE) return file;
24130
24466
  if (file.toLowerCase().endsWith(".atlas")) return `${file}.txt`;
24131
24467
  return file;
24132
24468
  }
24133
- function resolvePublishedSkeletonFileName(resource) {
24134
- if (isSpineResource(resource) && resource.getFile().toLowerCase().endsWith(".skel")) return `${resource.getFile()}.bytes`;
24469
+ function resolvePublishedSkeletonFileName(resource, projectType) {
24470
+ if (projectType === UNITY_PROJECT_TYPE && isSpineResource(resource) && resource.getFile().toLowerCase().endsWith(".skel")) return `${resource.getFile()}.bytes`;
24135
24471
  return resource.getFile();
24136
24472
  }
24137
24473
  function setPublishedFileExtra(resource, fileName) {
@@ -24234,6 +24570,24 @@ function collectPackagePublishContext(pkg, options) {
24234
24570
  const referencedIds = /* @__PURE__ */ new Set();
24235
24571
  const pixelHitTestImageIds = /* @__PURE__ */ new Set();
24236
24572
  const spriteItemIds = /* @__PURE__ */ new Set();
24573
+ const collectExportedResourceIds = (sourceResources, sourcePublishedResourceIds) => {
24574
+ const exportedResourceIds = new Set(sourcePublishedResourceIds);
24575
+ const resourcesById = new Map(sourceResources.map((resource) => [resource.getId(), resource]));
24576
+ let changed = true;
24577
+ while (changed) {
24578
+ changed = false;
24579
+ for (const resourceId of [...exportedResourceIds]) {
24580
+ const resource = resourcesById.get(resourceId);
24581
+ if (!resource || !isSkeletonResource(resource)) continue;
24582
+ for (const requiredId of resource.getRequireIds()) {
24583
+ if (!requiredId || exportedResourceIds.has(requiredId)) continue;
24584
+ exportedResourceIds.add(requiredId);
24585
+ changed = true;
24586
+ }
24587
+ }
24588
+ }
24589
+ return exportedResourceIds;
24590
+ };
24237
24591
  for (const atlas of pkg.listAtlases()) for (const sprite of atlas.listSprites()) spriteItemIds.add(sprite.getItemId());
24238
24592
  for (const resource of resources) {
24239
24593
  if (!isComponentResource(resource)) continue;
@@ -24260,6 +24614,7 @@ function collectPackagePublishContext(pkg, options) {
24260
24614
  child.getSelectedIcon?.(),
24261
24615
  child.getDropdown?.(),
24262
24616
  child.getSound?.(),
24617
+ child.getInstanceSound?.(),
24263
24618
  child.getInstanceIcon?.(),
24264
24619
  child.getInstanceSelectedIcon?.(),
24265
24620
  child.getVtScrollBarRes?.(),
@@ -24317,19 +24672,7 @@ function collectPackagePublishContext(pkg, options) {
24317
24672
  }
24318
24673
  if (resource.getExported() || referencedIds.has(resourceId)) publishedResourceIds.add(resourceId);
24319
24674
  }
24320
- let changed = true;
24321
- while (changed) {
24322
- changed = false;
24323
- for (const resource of resources) {
24324
- if (!isSkeletonResource(resource)) continue;
24325
- if (!publishedResourceIds.has(resource.getId())) continue;
24326
- for (const requiredId of resource.getRequireIds()) {
24327
- if (!requiredId || publishedResourceIds.has(requiredId)) continue;
24328
- publishedResourceIds.add(requiredId);
24329
- changed = true;
24330
- }
24331
- }
24332
- }
24675
+ for (const resourceId of collectExportedResourceIds(resources, publishedResourceIds)) publishedResourceIds.add(resourceId);
24333
24676
  const highResolutionItemIds = collectHighResolutionItemIds(resources, publishedResourceIds, options.includeHighResolution);
24334
24677
  if (!options.includeBranches) {
24335
24678
  const mainByKey = /* @__PURE__ */ new Map();
@@ -24378,6 +24721,7 @@ function collectPackagePublishContext(pkg, options) {
24378
24721
  return {
24379
24722
  referencedIds,
24380
24723
  publishedResourceIds,
24724
+ exportedResourceIds: collectExportedResourceIds(resources, publishedResourceIds),
24381
24725
  pixelHitTestImageIds,
24382
24726
  highResolutionItemIds,
24383
24727
  effectiveResourceIds,
@@ -24387,6 +24731,7 @@ function collectPackagePublishContext(pkg, options) {
24387
24731
  return {
24388
24732
  referencedIds,
24389
24733
  publishedResourceIds,
24734
+ exportedResourceIds: collectExportedResourceIds(resources, publishedResourceIds),
24390
24735
  pixelHitTestImageIds,
24391
24736
  highResolutionItemIds,
24392
24737
  effectiveResourceIds: new Map([...publishedResourceIds].map((resourceId) => [resourceId, resourceId])),
@@ -24437,7 +24782,7 @@ async function applyPixelHitTests(pkg, imageIds, basePath, encoder) {
24437
24782
  }
24438
24783
  }
24439
24784
  async function annotatePackagePublishArtifacts(pkg, basePath, encoder, options) {
24440
- const { publishedResourceIds, pixelHitTestImageIds, highResolutionItemIds, effectiveResourceIds, includeBranches } = collectPackagePublishContext(pkg, options);
24785
+ const { publishedResourceIds, exportedResourceIds, pixelHitTestImageIds, highResolutionItemIds, effectiveResourceIds, includeBranches } = collectPackagePublishContext(pkg, options);
24441
24786
  for (const resource of pkg.listResources()) {
24442
24787
  setPublishedIdExtra(resource, effectiveResourceIds.get(resource.getId()) ?? null);
24443
24788
  if (isHighResolutionResource(resource)) resource.setHighResolutionItemIds(highResolutionItemIds.get(resource.getId()) ?? []);
@@ -24447,21 +24792,26 @@ async function annotatePackagePublishArtifacts(pkg, basePath, encoder, options)
24447
24792
  pkg.setExtras({
24448
24793
  ...extras,
24449
24794
  publishedResourceIds: [...publishedResourceIds].sort((a, b) => a.localeCompare(b)),
24795
+ exportedResourceIds: [...exportedResourceIds].sort((a, b) => a.localeCompare(b)),
24450
24796
  publishedIncludeBranches: includeBranches,
24451
24797
  publishedEffectiveResourceIds: Object.fromEntries(effectiveResourceIds)
24452
24798
  });
24453
24799
  for (const resource of pkg.listResources()) {
24454
24800
  if (isMiscResource(resource)) {
24455
- setPublishedFileExtra(resource, resolvePublishedMiscFileName(resource));
24801
+ setPublishedFileExtra(resource, resolvePublishedMiscFileName(resource, options.projectType));
24456
24802
  continue;
24457
24803
  }
24458
- if (isSkeletonResource(resource)) setPublishedFileExtra(resource, resolvePublishedSkeletonFileName(resource));
24804
+ if (isSkeletonResource(resource)) setPublishedFileExtra(resource, resolvePublishedSkeletonFileName(resource, options.projectType));
24459
24805
  }
24460
24806
  }
24461
24807
  function getAnnotatedPublishedResourceIds(pkg) {
24462
24808
  const extras = pkg.getExtras() ?? {};
24463
24809
  return new Set(extras.publishedResourceIds ?? []);
24464
24810
  }
24811
+ function getAnnotatedExportedResourceIds(pkg) {
24812
+ const extras = pkg.getExtras() ?? {};
24813
+ return new Set(extras.exportedResourceIds ?? []);
24814
+ }
24465
24815
  function getPublishedSkeletonDependencyImageIds(pkg, publishedResourceIds) {
24466
24816
  const imageIds = /* @__PURE__ */ new Set();
24467
24817
  const resourcesById = new Map(pkg.listResources().map((resource) => [resource.getId(), resource]));
@@ -24500,18 +24850,18 @@ async function exportPackageSounds(pkg, outputDir, basePath, fs, readFileRaw, lo
24500
24850
  }
24501
24851
  }
24502
24852
  async function exportPackageExternalResources(pkg, outputDir, basePath, fs, readFileRaw, logger) {
24503
- const publishedResourceIds = getAnnotatedPublishedResourceIds(pkg);
24504
- const skeletonDependencyImageIds = getPublishedSkeletonDependencyImageIds(pkg, publishedResourceIds);
24505
- if (publishedResourceIds.size === 0) return;
24853
+ const exportedResourceIds = getAnnotatedExportedResourceIds(pkg);
24854
+ const skeletonDependencyImageIds = getPublishedSkeletonDependencyImageIds(pkg, exportedResourceIds);
24855
+ if (exportedResourceIds.size === 0) return;
24506
24856
  if (!basePath || !readFileRaw) {
24507
24857
  if (pkg.listResources().some((resource) => {
24508
- return (isMiscResource(resource) || isSkeletonResource(resource)) && publishedResourceIds.has(resource.getId()) || skeletonDependencyImageIds.has(resource.getId());
24858
+ return (isMiscResource(resource) || isSkeletonResource(resource)) && exportedResourceIds.has(resource.getId()) || skeletonDependencyImageIds.has(resource.getId());
24509
24859
  })) logger.warn(`publish: External resources in package "${pkg.getName()}" were not exported because basePath/readFileRaw is unavailable.`);
24510
24860
  return;
24511
24861
  }
24512
24862
  for (const resource of pkg.listResources()) {
24513
24863
  const resourceId = resource.getId();
24514
- const isSkeletonExternal = publishedResourceIds.has(resourceId) && (isMiscResource(resource) || isSkeletonResource(resource));
24864
+ const isSkeletonExternal = exportedResourceIds.has(resourceId) && (isMiscResource(resource) || isSkeletonResource(resource));
24515
24865
  const isSkeletonImageDependency = skeletonDependencyImageIds.has(resourceId) && isImageResource(resource);
24516
24866
  if (!isSkeletonExternal && !isSkeletonImageDependency) continue;
24517
24867
  let sourcePath;
@@ -24536,10 +24886,13 @@ async function exportPackageExternalResources(pkg, outputDir, basePath, fs, read
24536
24886
  * Publishes a FairyGUI project.
24537
24887
  *
24538
24888
  * Orchestrates:
24539
- * 1. Atlas packing (MaxRects layout + optional sharp compositing)
24889
+ * 1. Atlas packing (MaxRects layout + optional raster compositing)
24540
24890
  * 2. Per-package .fui binary serialization
24541
24891
  * 3. File writing to the output directory
24542
24892
  *
24893
+ * This is the capability-injected core. Standard hosts should use
24894
+ * `publishNode()` or `publishBrowser()` through their dedicated entries.
24895
+ *
24543
24896
  * ```ts
24544
24897
  * import sharp from 'sharp';
24545
24898
  * const io = new NodeIO();
@@ -24557,16 +24910,101 @@ async function exportPackageExternalResources(pkg, outputDir, basePath, fs, read
24557
24910
  */
24558
24911
  function publish(options) {
24559
24912
  return createTransform("publish", async (doc) => {
24913
+ const resolveConfiguredOutputPath = (value, projectBasePath) => {
24914
+ const trimmed = value?.trim();
24915
+ if (!trimmed) return void 0;
24916
+ if (isAbsolutePathLike(trimmed) || !projectBasePath) return trimTrailingSlashes(trimmed);
24917
+ return trimTrailingSlashes(options.fs ? options.fs.join(projectBasePath, trimmed) : joinPathSegments(projectBasePath, trimmed));
24918
+ };
24919
+ const resolveProjectPublishConfig = () => {
24920
+ const publishSettings = (doc.getRoot().getSettings?.() ?? {}).publish ?? {};
24921
+ const resolved = resolvePublishOptions(doc, {
24922
+ compressed: options.compressed,
24923
+ fileExtension: options.fileExtension,
24924
+ packages: options.packages,
24925
+ atlas: options.atlas
24926
+ });
24927
+ const includeBranches = (publishSettings.branchProcessing ?? 0) === 0;
24928
+ return {
24929
+ ...resolved,
24930
+ projectType: doc.getRoot().getProjectType(),
24931
+ includeBranches,
24932
+ activeBranch: includeBranches ? "" : options.branch ?? "",
24933
+ includeHighResolution: publishSettings.includeHighResolution ?? 0,
24934
+ separatedAtlasForBranch: includeBranches && publishSettings.seperatedAtlasForBranch === true,
24935
+ globalOutputPath: publishSettings.path?.trim() ?? "",
24936
+ globalBranchOutputPath: publishSettings.branchPath?.trim() ?? ""
24937
+ };
24938
+ };
24939
+ const resolvePackagePublishPlan = (pkg, config, projectBasePath) => {
24940
+ let outputDir;
24941
+ if (options.output) outputDir = trimTrailingSlashes(options.output);
24942
+ else {
24943
+ const candidates = [];
24944
+ if (!config.includeBranches && config.activeBranch) candidates.push(pkg.getPublishBranchPath(), config.globalBranchOutputPath);
24945
+ candidates.push(pkg.getPublishPath(), config.globalOutputPath);
24946
+ for (const candidate of candidates) {
24947
+ const resolved = resolveConfiguredOutputPath(candidate, projectBasePath);
24948
+ if (!resolved) continue;
24949
+ outputDir = resolved;
24950
+ break;
24951
+ }
24952
+ }
24953
+ const publishName = pkg.getPublishName() || pkg.getName();
24954
+ return {
24955
+ pkg,
24956
+ outputDir,
24957
+ publishName,
24958
+ fileName: resolvePublishFileName(publishName, config.fileExtension),
24959
+ compressed: config.compressed,
24960
+ fileExtension: config.fileExtension,
24961
+ includeBranches: config.includeBranches,
24962
+ activeBranch: config.activeBranch,
24963
+ includeHighResolution: config.includeHighResolution,
24964
+ separatedAtlasForBranch: config.separatedAtlasForBranch,
24965
+ atlas: config.atlas
24966
+ };
24967
+ };
24968
+ const createNoopPublishFs = () => ({
24969
+ async writeFileRaw() {},
24970
+ async mkdir() {},
24971
+ join(...paths) {
24972
+ return paths.join("/");
24973
+ }
24974
+ });
24975
+ const publishPackage = async (plan, writerFs, packageIndex) => {
24976
+ const atlasRuntimeOptions = resolvePublishAtlasRuntimeOptions(plan.fileExtension);
24977
+ await atlas({
24978
+ ...plan.atlas,
24979
+ ...options.atlas ?? {},
24980
+ separatedAtlasForBranch: plan.separatedAtlasForBranch,
24981
+ encoder: options.encoder,
24982
+ basePath: options.basePath,
24983
+ outputPath: options.fs ? plan.outputDir : void 0,
24984
+ mkdir: options.fs ? options.fs.mkdir : void 0,
24985
+ readFileRaw: options.atlas?.readFileRaw ?? options.fs?.readFileRaw,
24986
+ packages: [plan.pkg.getName()],
24987
+ ...atlasRuntimeOptions
24988
+ })(doc);
24989
+ if (!options.fs) return;
24990
+ if (!plan.outputDir) throw new Error("publish: no output directory resolved. Provide --output, or configure global publish.path / package publishPath.");
24991
+ await options.fs.mkdir(plan.outputDir);
24992
+ const filePath = options.fs.join(plan.outputDir, plan.fileName);
24993
+ const bwOptions = {
24994
+ compressed: plan.compressed,
24995
+ packageIndex
24996
+ };
24997
+ await new BinaryWriter(writerFs).write(doc, filePath, bwOptions);
24998
+ await exportPackageSounds(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw, logger);
24999
+ await exportPackageExternalResources(plan.pkg, plan.outputDir, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw, logger);
25000
+ logger.info(`publish: Written ${plan.fileName}`);
25001
+ };
24560
25002
  const root = doc.getRoot();
24561
25003
  const logger = doc.getLogger();
24562
- const publishSettings = (root.getSettings?.() ?? {}).publish ?? {};
24563
- const resolved = resolvePublishOptions(doc, {
24564
- compressed: options.compressed,
24565
- fileExtension: options.fileExtension,
24566
- packages: options.packages,
24567
- atlas: options.atlas
24568
- });
24569
- const ext = resolved.fileExtension;
25004
+ const projectBasePath = resolveProjectBasePath(options.basePath) || doc.getProjectDir?.() || "";
25005
+ const plugins = options.plugins ?? [];
25006
+ await runPublishPluginHook(plugins, "onPublishStart", doc, options);
25007
+ const resolved = resolveProjectPublishConfig();
24570
25008
  let allPackages = root.listPackages();
24571
25009
  if (resolved.packages && resolved.packages.length > 0) {
24572
25010
  const names = new Set(resolved.packages);
@@ -24574,59 +25012,42 @@ function publish(options) {
24574
25012
  }
24575
25013
  if (allPackages.length === 0) {
24576
25014
  logger.warn("publish: No packages to publish.");
25015
+ await runPublishPluginHook(plugins, "onPublishEnd", doc, options);
24577
25016
  return;
24578
25017
  }
24579
- const includeBranches = (publishSettings.branchProcessing ?? 0) === 0;
24580
- const activeBranch = includeBranches ? "" : options.branch ?? "";
24581
- const includeHighResolution = publishSettings.includeHighResolution ?? 0;
24582
- const atlasRuntimeOptions = resolvePublishAtlasRuntimeOptions(ext);
24583
25018
  const allDocPackages = root.listPackages();
24584
25019
  const pkgMap = /* @__PURE__ */ new Map();
24585
25020
  for (const p of allDocPackages) pkgMap.set(p.getId(), p);
24586
25021
  for (const pkg of allPackages) {
24587
- _computeDependencies(pkg, pkgMap);
25022
+ _computeDependencies(doc, pkg, pkgMap);
24588
25023
  await annotatePackagePublishArtifacts(pkg, options.basePath, options.encoder, {
24589
- includeBranches,
24590
- activeBranch,
24591
- includeHighResolution
25024
+ projectType: resolved.projectType,
25025
+ includeBranches: resolved.includeBranches,
25026
+ activeBranch: resolved.activeBranch,
25027
+ includeHighResolution: resolved.includeHighResolution
24592
25028
  });
24593
25029
  }
24594
- await atlas({
24595
- ...resolved.atlas,
24596
- ...options.atlas ?? {},
24597
- separatedAtlasForBranch: includeBranches && publishSettings.seperatedAtlasForBranch === true,
24598
- encoder: options.encoder,
24599
- basePath: options.basePath,
24600
- outputPath: options.fs ? options.output : void 0,
24601
- mkdir: options.fs ? options.fs.mkdir : void 0,
24602
- readFileRaw: options.atlas?.readFileRaw ?? options.fs?.readFileRaw,
24603
- ...atlasRuntimeOptions
24604
- })(doc);
25030
+ const plans = allPackages.map((pkg) => resolvePackagePublishPlan(pkg, resolved, projectBasePath));
24605
25031
  if (!options.fs) {
24606
25032
  logger.info(`publish: No fs provided — layout computed for ${allPackages.length} package(s), skipping file output.`);
25033
+ const noopWriterFs = toBinaryWriterFileSystem(createNoopPublishFs());
25034
+ for (const plan of plans) await publishPackage(plan, noopWriterFs, allDocPackages.indexOf(plan.pkg));
25035
+ await runPublishPluginHook(plugins, "onPublishEnd", doc, options);
24607
25036
  return;
24608
25037
  }
24609
- await options.fs.mkdir(options.output);
25038
+ const unresolvedPlan = plans.find((plan) => !plan.outputDir);
25039
+ if (unresolvedPlan) throw new Error(`publish: no output directory resolved for package "${unresolvedPlan.pkg.getName()}". Provide --output, or configure global publish.path / package publishPath.`);
24610
25040
  const writerFs = toBinaryWriterFileSystem(options.fs);
24611
- for (const pkg of allPackages) {
24612
- const pkgIndex = allDocPackages.indexOf(pkg);
24613
- const fileName = resolvePublishFileName(pkg.getPublishName() || pkg.getName(), ext);
24614
- const filePath = options.fs.join(options.output, fileName);
24615
- const bwOptions = {
24616
- compressed: resolved.compressed,
24617
- packageIndex: pkgIndex
24618
- };
24619
- await new BinaryWriter(writerFs).write(doc, filePath, bwOptions);
24620
- await exportPackageSounds(pkg, options.output, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw, logger);
24621
- await exportPackageExternalResources(pkg, options.output, options.basePath, options.fs, options.atlas?.readFileRaw ?? options.fs.readFileRaw, logger);
24622
- logger.info(`publish: Written ${fileName}`);
24623
- }
24624
- await publishCodeGeneration(doc, {
25041
+ for (const plan of plans) await publishPackage(plan, writerFs, allDocPackages.indexOf(plan.pkg));
25042
+ if (options.codeGeneration !== false) await publishCodeGeneration(doc, {
24625
25043
  basePath: options.basePath,
24626
25044
  fs: options.fs,
24627
- packages: allPackages
25045
+ packages: allPackages,
25046
+ plugins
24628
25047
  });
24629
- logger.info(`publish: Published ${allPackages.length} package(s) to ${options.output}`);
25048
+ const publishedTargets = [...new Set(plans.map((plan) => plan.outputDir).filter((value) => Boolean(value)))];
25049
+ logger.info(publishedTargets.length > 0 ? `publish: Published ${allPackages.length} package(s) to ${publishedTargets.join(", ")}` : `publish: Published ${allPackages.length} package(s)`);
25050
+ await runPublishPluginHook(plugins, "onPublishEnd", doc, options);
24630
25051
  });
24631
25052
  }
24632
25053
  /**
@@ -24634,25 +25055,104 @@ function publish(options) {
24634
25055
  * The editor only adds dependencies for packages referenced via bitmap font URLs.
24635
25056
  * @internal
24636
25057
  */
24637
- function _computeDependencies(pkg, pkgMap) {
25058
+ function _computeDependencies(doc, pkg, pkgMap) {
24638
25059
  const referencedPkgIds = /* @__PURE__ */ new Set();
24639
- function scanFontUrl(font) {
24640
- if (!font) return;
24641
- const fontStr = Array.isArray(font) ? font[0] : String(font);
24642
- if (typeof fontStr !== "string" || !fontStr.startsWith("ui://")) return;
24643
- const rest = fontStr.slice(5);
24644
- if (rest.length >= 8) {
24645
- const depPkgId = rest.slice(0, 8);
24646
- if (depPkgId !== pkg.getId()) referencedPkgIds.add(depPkgId);
25060
+ const pkgId = pkg.getId();
25061
+ const packageOrder = new Map(doc.getRoot().listPackages().map((entry, index) => [entry.getId(), index]));
25062
+ const addDependencyPackageId = (dependencyPkgId) => {
25063
+ const normalized = dependencyPkgId?.trim() ?? "";
25064
+ if (!normalized || normalized === pkgId) return;
25065
+ referencedPkgIds.add(normalized);
25066
+ };
25067
+ const extractPackageIdFromUiUrl = (value) => {
25068
+ if (!value.startsWith("ui://")) return null;
25069
+ const rest = value.slice(5);
25070
+ if (!rest) return null;
25071
+ const slashIndex = rest.indexOf("/");
25072
+ if (slashIndex >= 0) return rest.slice(0, slashIndex) || null;
25073
+ if (rest.length >= 8) return rest.slice(0, 8);
25074
+ return null;
25075
+ };
25076
+ const addDependencyPackageIdFromUiValue = (value) => {
25077
+ if (!value || typeof value !== "string") return;
25078
+ addDependencyPackageId(extractPackageIdFromUiUrl(value));
25079
+ };
25080
+ const addDependencyPackageIdsFromText = (value) => {
25081
+ if (!value || typeof value !== "string") return;
25082
+ const matches = value.matchAll(/ui:\/\/([0-9a-z]{8})/giu);
25083
+ for (const match of matches) addDependencyPackageId(match[1] ?? "");
25084
+ };
25085
+ const addDependencyPackageIdsFromUnknown = (value) => {
25086
+ if (Array.isArray(value)) {
25087
+ for (const entry of value) addDependencyPackageIdsFromUnknown(entry);
25088
+ return;
24647
25089
  }
24648
- }
25090
+ if (typeof value === "string") {
25091
+ addDependencyPackageIdFromUiValue(value);
25092
+ addDependencyPackageIdsFromText(value);
25093
+ }
25094
+ };
25095
+ const addDependencyFontRef = (value) => {
25096
+ if (Array.isArray(value)) {
25097
+ for (const entry of value) addDependencyPackageIdFromUiValue(entry);
25098
+ return;
25099
+ }
25100
+ addDependencyPackageIdFromUiValue(value ?? void 0);
25101
+ };
24649
25102
  for (const res of pkg.listResources()) {
24650
25103
  if (res.propertyType !== "Component") continue;
24651
- for (const child of res.listChildren?.() ?? []) scanFontUrl(child.getFont?.());
25104
+ const component = res;
25105
+ for (const child of component.listChildren?.() ?? []) {
25106
+ addDependencyPackageId(child.getPackageId?.());
25107
+ addDependencyFontRef(child.getFont?.());
25108
+ addDependencyPackageIdsFromText(child.getText?.());
25109
+ for (const ref of [
25110
+ child.getUrl?.(),
25111
+ child.getDefaultItem?.(),
25112
+ child.getIcon?.(),
25113
+ child.getSelectedIcon?.(),
25114
+ child.getDropdown?.(),
25115
+ child.getSound?.(),
25116
+ child.getInstanceSound?.(),
25117
+ child.getInstanceIcon?.(),
25118
+ child.getInstanceSelectedIcon?.(),
25119
+ child.getVtScrollBarRes?.(),
25120
+ child.getHzScrollBarRes?.(),
25121
+ child.getHeaderRes?.(),
25122
+ child.getFooterRes?.()
25123
+ ]) addDependencyPackageIdFromUiValue(ref);
25124
+ for (const item of child.getInstanceComboItems?.() ?? []) addDependencyPackageIdFromUiValue(item.icon ?? void 0);
25125
+ for (const item of child.getListItems?.() ?? []) {
25126
+ addDependencyPackageIdFromUiValue(item.icon ?? void 0);
25127
+ addDependencyPackageIdFromUiValue(item.url ?? void 0);
25128
+ }
25129
+ for (const gear of child.listGears?.() ?? []) {
25130
+ addDependencyPackageIdsFromUnknown(gear.getValues?.());
25131
+ addDependencyPackageIdsFromUnknown(gear.getDefaultValue?.());
25132
+ }
25133
+ }
25134
+ addDependencyFontRef(component.getFont?.());
25135
+ for (const ref of [
25136
+ component.getDropdown?.(),
25137
+ component.getHeaderRes?.(),
25138
+ component.getFooterRes?.(),
25139
+ component.getVtScrollBarRes?.(),
25140
+ component.getHzScrollBarRes?.(),
25141
+ component.getSound?.()
25142
+ ]) addDependencyPackageIdFromUiValue(ref);
25143
+ for (const transition of component.listTransitions?.() ?? []) for (const item of transition.listItems?.() ?? []) {
25144
+ addDependencyPackageIdsFromUnknown(item.getStartValue?.());
25145
+ addDependencyPackageIdsFromUnknown(item.getEndValue?.());
25146
+ }
24652
25147
  }
24653
25148
  for (const dep of pkg.listDependencies()) pkg.removeDependency(dep);
24654
25149
  if (referencedPkgIds.size > 0) {
24655
- const sortedIds = [...referencedPkgIds].sort((a, b) => a.localeCompare(b));
25150
+ const sortedIds = [...referencedPkgIds].sort((a, b) => {
25151
+ const orderA = packageOrder.get(a) ?? Number.MAX_SAFE_INTEGER;
25152
+ const orderB = packageOrder.get(b) ?? Number.MAX_SAFE_INTEGER;
25153
+ if (orderA !== orderB) return orderA - orderB;
25154
+ return a.localeCompare(b);
25155
+ });
24656
25156
  for (const refId of sortedIds) {
24657
25157
  const depPkg = pkgMap.get(refId);
24658
25158
  if (depPkg) pkg.addDependency(depPkg);
@@ -24660,98 +25160,325 @@ function _computeDependencies(pkg, pkgMap) {
24660
25160
  }
24661
25161
  }
24662
25162
  //#endregion
24663
- //#region src/cli.ts
24664
- const HELP = `
24665
- ofgui FairyGUI Headless Authoring CLI
24666
-
24667
- Alias:
24668
- openfairygui
24669
-
24670
- Commands:
24671
- inspect <project-dir> Show project contents report
24672
- publish <project-dir> --output <dir> [options] Publish project to binary outputs and configured generated code
24673
- restore <release-dir> --output <dir> [options] Restore a FairyGUI project from published binaries
24674
- backend-capabilities <project-dir> Open a backend session, print runtime capabilities, then close it
24675
-
24676
- Publish options:
24677
- --output, -o <dir> Output directory (required)
24678
- --compressed Compress binary data (overrides project setting)
24679
- --packages <a,b,c> Only publish specific packages (comma-separated)
24680
- --branch <name> Active branch used by "主干合并活跃分支"; omit for main branch
24681
- --project-type <name|id> Override project type (for example: unity, layabox, cocoscreator, 0, 4, 3)
24682
-
24683
- Restore options:
24684
- --output, -o <dir> Output project directory (required)
24685
- --packages <a,b,c> Only restore specific packages (comma-separated)
24686
- --force Overwrite a non-empty output directory
24687
- --project-type <name|id> Override restored project type; default is unity
24688
-
24689
- Options:
24690
- --help, -h Show this help
24691
- --version, -v Show version
24692
-
24693
- Input can be a .fairy file or a project root directory (auto-discovers .fairy file).
24694
- File extension and binary format are read from project settings.
24695
- `;
24696
- const require = createRequire(import.meta.url);
24697
- function getInjectedPackageVersion() {
24698
- const version = import.meta.env?.PACKAGE_VERSION;
24699
- return typeof version === "string" && version.length > 0 ? version : null;
24700
- }
24701
- function readPackageVersion() {
24702
- const injectedVersion = getInjectedPackageVersion();
24703
- if (injectedVersion) return injectedVersion;
24704
- try {
24705
- const pkg = require("../package.json");
24706
- if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
24707
- } catch {}
24708
- return "0.0.0-dev";
24709
- }
24710
- const PACKAGE_VERSION = readPackageVersion();
25163
+ //#region ../core/src/io/node-io.ts
25164
+ /**
25165
+ * Node.js I/O implementation for reading and writing FairyGUI projects.
25166
+ *
25167
+ * Usage:
25168
+ *
25169
+ * ```ts
25170
+ * import { NodeIO } from '@openfairygui/core/node';
25171
+ *
25172
+ * const io = new NodeIO();
25173
+ * const doc = await io.readProject('./path/to/project.fairy');
25174
+ * await io.writeProject(doc, './path/to/output.fairy');
25175
+ * const doc2 = await io.readBinary('./path/to/package_fui.bytes');
25176
+ * ```
25177
+ *
25178
+ * @category I/O
25179
+ */
25180
+ var NodeIO = class extends PlatformIO {
25181
+ createFileSystem() {
25182
+ return {
25183
+ async readFile(filePath) {
25184
+ return fs$1.readFile(filePath, "utf-8");
25185
+ },
25186
+ async readFileRaw(filePath) {
25187
+ const buf = await fs$1.readFile(filePath);
25188
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
25189
+ },
25190
+ async writeFile(filePath, content) {
25191
+ await fs$1.writeFile(filePath, content, "utf-8");
25192
+ },
25193
+ async writeFileRaw(filePath, data) {
25194
+ await fs$1.writeFile(filePath, data);
25195
+ },
25196
+ async mkdir(dirPath) {
25197
+ await fs$1.mkdir(dirPath, { recursive: true });
25198
+ },
25199
+ async readdir(dirPath) {
25200
+ const entries = await fs$1.readdir(dirPath, { withFileTypes: true });
25201
+ return (await Promise.all(entries.map(async (entry) => {
25202
+ if (entry.isDirectory()) return entry.name;
25203
+ if (!entry.isSymbolicLink()) return null;
25204
+ try {
25205
+ return (await fs$1.stat(path$1.join(dirPath, entry.name))).isDirectory() ? entry.name : null;
25206
+ } catch {
25207
+ return null;
25208
+ }
25209
+ }))).filter((entry) => entry !== null);
25210
+ },
25211
+ async exists(filePath) {
25212
+ try {
25213
+ await fs$1.access(filePath);
25214
+ return true;
25215
+ } catch {
25216
+ return false;
25217
+ }
25218
+ },
25219
+ join(...paths) {
25220
+ return path$1.join(...paths);
25221
+ },
25222
+ dirname(filePath) {
25223
+ return path$1.dirname(filePath);
25224
+ }
25225
+ };
25226
+ }
25227
+ };
25228
+ //#endregion
25229
+ //#region src/utils/project-input.ts
24711
25230
  /** Resolve input to a .fairy file path. Accepts a directory or a .fairy file. */
24712
25231
  async function resolveFairyPath(input) {
24713
25232
  const resolved = path.resolve(input);
24714
25233
  const stat = await fs.stat(resolved);
24715
25234
  if (stat.isFile() && resolved.endsWith(".fairy")) return resolved;
24716
25235
  if (stat.isDirectory()) {
24717
- const fairyFiles = (await fs.readdir(resolved)).filter((e) => e.endsWith(".fairy"));
25236
+ const fairyFiles = (await fs.readdir(resolved)).filter((entry) => entry.endsWith(".fairy"));
24718
25237
  if (fairyFiles.length === 1) return path.join(resolved, fairyFiles[0]);
24719
25238
  if (fairyFiles.length > 1) throw new Error(`Multiple .fairy files found in ${resolved}: ${fairyFiles.join(", ")}. Please specify one.`);
24720
25239
  throw new Error(`No .fairy file found in ${resolved}`);
24721
25240
  }
24722
25241
  throw new Error(`Input is not a .fairy file or directory: ${resolved}`);
24723
25242
  }
24724
- async function main() {
24725
- const args = process.argv.slice(2);
24726
- if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
24727
- console.log(HELP);
24728
- return;
25243
+ //#endregion
25244
+ //#region src/commands/inspect.ts
25245
+ function registerInspectCommand(program) {
25246
+ program.command("inspect").description("Show project contents report").argument("<project-dir>", "Project root directory or .fairy file").action(async (projectDir) => {
25247
+ const fairyPath = await resolveFairyPath(projectDir);
25248
+ console.log(`Project: ${fairyPath}\n`);
25249
+ printReport(inspect(await new NodeIO().readProject(fairyPath)));
25250
+ });
25251
+ }
25252
+ function printReport(report) {
25253
+ console.log(`ID: ${report.projectId}`);
25254
+ console.log(`Type: ${report.projectType}, Version: ${report.version}`);
25255
+ console.log(`\nPackages: ${report.totals.packages}`);
25256
+ console.log(` Images: ${report.totals.images}`);
25257
+ console.log(` Sounds: ${report.totals.sounds}`);
25258
+ console.log(` Fonts: ${report.totals.fonts}`);
25259
+ console.log(` MovieClips: ${report.totals.movieClips}`);
25260
+ console.log(` Components: ${report.totals.components}`);
25261
+ console.log(` DisplayObjs: ${report.totals.displayObjects}`);
25262
+ console.log(` Gears: ${report.totals.gears}`);
25263
+ console.log(` Controllers: ${report.totals.controllers}`);
25264
+ console.log(` Transitions: ${report.totals.transitions}`);
25265
+ console.log("\nPackage details:");
25266
+ for (const pkg of report.packages) {
25267
+ const res = pkg.resources;
25268
+ console.log(` ${pkg.name} (${pkg.id}): ${res.images.count} img, ${res.sounds.count} snd, ${res.fonts.count} font, ${res.components.count} comp`);
24729
25269
  }
24730
- if (args.includes("--version") || args.includes("-v")) {
24731
- console.log(PACKAGE_VERSION);
24732
- return;
25270
+ }
25271
+ //#endregion
25272
+ //#region ../functions/src/adapters/node/plugins.ts
25273
+ const importNative$1 = new Function("id", "return import(id)");
25274
+ async function loadPlugins(doc, pluginsDir) {
25275
+ if (!pluginsDir) return [];
25276
+ const fs = await importNative$1("node:fs/promises");
25277
+ const path = await importNative$1("node:path");
25278
+ let entries;
25279
+ try {
25280
+ entries = await fs.readdir(pluginsDir, { withFileTypes: true });
25281
+ } catch {
25282
+ return [];
24733
25283
  }
24734
- const command = args[0];
24735
- const rest = args.slice(1);
24736
- switch (command) {
24737
- case "inspect":
24738
- await cmdInspect(rest);
24739
- break;
24740
- case "publish":
24741
- await cmdPublish(rest);
24742
- break;
24743
- case "restore":
24744
- await cmdRestore(rest);
24745
- break;
24746
- case "backend-capabilities":
24747
- await cmdBackendCapabilities(rest);
24748
- break;
24749
- default:
24750
- console.error(`Unknown command: ${command}\n`);
24751
- console.log(HELP);
24752
- process.exit(1);
25284
+ const plugins = [];
25285
+ for (const entry of entries) {
25286
+ if (!entry.isDirectory()) continue;
25287
+ const pluginDir = path.join(pluginsDir, entry.name);
25288
+ try {
25289
+ const manifest = await readPluginManifest(fs, path, pluginDir);
25290
+ if (!manifest) continue;
25291
+ const plugin = await loadPlugin(resolvePluginMain(path, pluginDir, manifest));
25292
+ plugins.push({
25293
+ name: manifest.name,
25294
+ plugin
25295
+ });
25296
+ } catch (error) {
25297
+ doc.getLogger().warn(`publish: Plugin "${entry.name}" was skipped: ${formatPluginError(error)}`);
25298
+ }
25299
+ }
25300
+ return plugins;
25301
+ }
25302
+ async function readPluginManifest(fs, path, pluginDir) {
25303
+ const manifestPath = path.join(pluginDir, "package.json");
25304
+ const content = await fs.readFile(manifestPath, "utf-8");
25305
+ const manifest = JSON.parse(content);
25306
+ if (!manifest.name) throw new Error(`Codegen plugin at ${pluginDir} is missing package.json name.`);
25307
+ if (!manifest.main) throw new Error(`Codegen plugin "${manifest.name}" is missing package.json main.`);
25308
+ return manifest;
25309
+ }
25310
+ function resolvePluginMain(path, pluginDir, manifest) {
25311
+ const mainPath = path.resolve(pluginDir, manifest.main);
25312
+ const relative = path.relative(pluginDir, mainPath);
25313
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Codegen plugin "${manifest.name}" main must resolve inside its plugin directory.`);
25314
+ return mainPath;
25315
+ }
25316
+ async function loadPlugin(mainPath) {
25317
+ const { createJiti } = await importNative$1("jiti");
25318
+ const mod = await createJiti(import.meta.url).import(mainPath);
25319
+ const defaultExport = mod.default;
25320
+ return isObject(defaultExport) ? defaultExport : mod;
25321
+ }
25322
+ function isObject(value) {
25323
+ return value !== null && typeof value === "object";
25324
+ }
25325
+ //#endregion
25326
+ //#region ../functions/src/adapters/node/publish.ts
25327
+ const importNative = new Function("id", "return import(id)");
25328
+ async function createNodePublishFileSystem() {
25329
+ const [fs, path] = await Promise.all([importNative("node:fs/promises"), importNative("node:path")]);
25330
+ return {
25331
+ async readFileRaw(filePath) {
25332
+ const data = await fs.readFile(filePath);
25333
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
25334
+ },
25335
+ async writeFileRaw(filePath, data) {
25336
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
25337
+ await fs.writeFile(filePath, data);
25338
+ },
25339
+ async mkdir(dirPath) {
25340
+ await fs.mkdir(dirPath, { recursive: true });
25341
+ },
25342
+ async readdir(dirPath) {
25343
+ return fs.readdir(dirPath);
25344
+ },
25345
+ async deleteFile(filePath) {
25346
+ await fs.rm(filePath, { force: true });
25347
+ },
25348
+ join(...paths) {
25349
+ return path.join(...paths);
25350
+ }
25351
+ };
25352
+ }
25353
+ async function resolveNodeAssetsPath(document, assetsPath) {
25354
+ if (assetsPath) return assetsPath;
25355
+ const projectDir = document.getProjectDir?.() ?? "";
25356
+ if (!projectDir) return void 0;
25357
+ return (await importNative("node:path")).join(projectDir, "assets");
25358
+ }
25359
+ async function loadSharpBackend() {
25360
+ try {
25361
+ const sharp = await importNative("sharp");
25362
+ return sharp.default ?? sharp;
25363
+ } catch {
25364
+ return;
24753
25365
  }
24754
25366
  }
25367
+ async function loadNodePublishPlugins(document, assetsPath) {
25368
+ const projectDir = document.getProjectDir?.() || (assetsPath ? resolveProjectBasePath(assetsPath) : "");
25369
+ if (!projectDir) return [];
25370
+ return loadPlugins(document, (await importNative("node:path")).join(projectDir, "plugins"));
25371
+ }
25372
+ /**
25373
+ * Publish a FairyGUI project through the standard Node host adapter.
25374
+ *
25375
+ * The adapter owns Node filesystem, Sharp, and project plugin discovery.
25376
+ * For custom environments, use the lower-level `publish()` core with explicit
25377
+ * capabilities instead.
25378
+ */
25379
+ async function publishNode(options) {
25380
+ const { document, assetsPath: configuredAssetsPath, atlas, encoder: configuredEncoder, plugins: configuredPlugins, ...publishOptions } = options;
25381
+ const [fileSystem, assetsPath] = await Promise.all([createNodePublishFileSystem(), resolveNodeAssetsPath(document, configuredAssetsPath)]);
25382
+ const [encoder, plugins] = await Promise.all([configuredEncoder === void 0 ? loadSharpBackend() : Promise.resolve(configuredEncoder), configuredPlugins === void 0 ? loadNodePublishPlugins(document, assetsPath) : Promise.resolve(configuredPlugins)]);
25383
+ if (!encoder) document.getLogger().warn("publish: Sharp is unavailable; atlas layout will be generated without PNG output.");
25384
+ await document.transform(publish({
25385
+ ...publishOptions,
25386
+ basePath: assetsPath,
25387
+ encoder,
25388
+ atlas: {
25389
+ ...atlas,
25390
+ readFileRaw: fileSystem.readFileRaw
25391
+ },
25392
+ fs: fileSystem,
25393
+ plugins
25394
+ }));
25395
+ }
25396
+ //#endregion
25397
+ //#region src/utils/project-type.ts
25398
+ function parseProjectType(value) {
25399
+ if (!value) return void 0;
25400
+ const trimmed = value.trim();
25401
+ if (trimmed === "") return void 0;
25402
+ if (/^\d+$/u.test(trimmed)) return Number(trimmed);
25403
+ const normalized = trimmed.toLowerCase();
25404
+ const map = {
25405
+ unity: ProjectType.Unity,
25406
+ flash: ProjectType.Flash,
25407
+ starling: ProjectType.Starling,
25408
+ cocoscreator: ProjectType.CocosCreator,
25409
+ cocos: ProjectType.CocosCreator,
25410
+ layabox: ProjectType.LayaBox,
25411
+ laya: ProjectType.LayaBox,
25412
+ egret: ProjectType.Egret,
25413
+ haxe: ProjectType.Haxe,
25414
+ pixi: ProjectType.Pixi,
25415
+ libgdx: ProjectType.LibGDX,
25416
+ unreal: ProjectType.Unreal,
25417
+ cryengine: ProjectType.CryEngine,
25418
+ monogame: ProjectType.MonoGame,
25419
+ vision: ProjectType.Vision
25420
+ };
25421
+ const resolved = map[normalized];
25422
+ if (resolved === void 0) throw new Error(`Unknown project type: ${value}. Use a numeric id or one of: ${Object.keys(map).join(", ")}`);
25423
+ return resolved;
25424
+ }
25425
+ //#endregion
25426
+ //#region src/commands/publish.ts
25427
+ function registerPublishCommand(program) {
25428
+ program.command("publish").description("Publish project to binary outputs and configured generated code").argument("<project-dir>", "Project root directory or .fairy file").option("-o, --output <dir>", "Override project or package publish output directory").option("-c, --compressed", "Compress binary data (overrides project setting)").option("-p, --packages <a,b,c>", "Only publish specific packages (comma-separated)").option("-b, --branch <name>", "Active branch used by \"主干合并活跃分支\"; omit for main branch").option("-t, --project-type <name|id>", "Override project type (for example: unity, layabox, cocoscreator, 0, 4, 3)").action(async (projectDir, options) => {
25429
+ const fairyPath = await resolveFairyPath(projectDir);
25430
+ const projectRootDir = path.dirname(fairyPath);
25431
+ const outputDir = options.output ? path.resolve(options.output) : void 0;
25432
+ console.log(`Reading project: ${fairyPath}`);
25433
+ const doc = await new NodeIO().readProject(fairyPath);
25434
+ const projectType = parseProjectType(options.projectType);
25435
+ if (projectType !== void 0) doc.getRoot().setProjectType(projectType);
25436
+ const pkgFilter = options.packages?.split(",").map((value) => value.trim());
25437
+ const resolved = resolvePublishOptions(doc, {
25438
+ compressed: options.compressed,
25439
+ packages: pkgFilter
25440
+ });
25441
+ console.log(`Settings: ext=${resolved.fileExtension}, compressed=${resolved.compressed}`);
25442
+ if (options.branch) console.log(`Active branch: ${options.branch}`);
25443
+ await publishNode({
25444
+ document: doc,
25445
+ output: outputDir,
25446
+ compressed: resolved.compressed,
25447
+ fileExtension: resolved.fileExtension,
25448
+ packages: resolved.packages,
25449
+ assetsPath: path.join(projectRootDir, "assets"),
25450
+ atlas: resolved.atlas,
25451
+ branch: options.branch
25452
+ });
25453
+ console.log(`\nDone!${outputDir ? ` Output override: ${outputDir}` : ""}`);
25454
+ });
25455
+ }
25456
+ //#endregion
25457
+ //#region src/commands/restore.ts
25458
+ function registerRestoreCommand(program) {
25459
+ program.command("restore").description("Restore a FairyGUI project from published binaries").argument("<release-dir>", "Published release directory").requiredOption("-o, --output <dir>", "Output project directory").option("-p, --packages <a,b,c>", "Only restore specific packages (comma-separated)").option("-f, --force", "Overwrite a non-empty output directory").option("-t, --project-type <name|id>", "Override restored project type; default is unity").action(async (releaseDir, options) => {
25460
+ const inputDir = path.resolve(releaseDir);
25461
+ const outputDir = path.resolve(options.output);
25462
+ const pkgFilter = options.packages ? options.packages.split(",").map((value) => value.trim()).filter(Boolean) : void 0;
25463
+ const projectType = parseProjectType(options.projectType);
25464
+ const { cropImage, extractImage } = await createRestoreImageProcessors();
25465
+ console.log(`Restoring published FairyGUI project: ${inputDir}`);
25466
+ const result = await restore({
25467
+ inputDir,
25468
+ output: outputDir,
25469
+ fs: createNodeRestoreFs(),
25470
+ packages: pkgFilter,
25471
+ force: options.force,
25472
+ projectType,
25473
+ cropImage,
25474
+ extractImage
25475
+ });
25476
+ const packages = result.document.getRoot().listPackages();
25477
+ console.log(`\nDone! Output: ${result.projectPath}`);
25478
+ console.log(`Packages: ${packages.map((pkg) => pkg.getName()).join(", ")}`);
25479
+ for (const warning of result.warnings) console.warn(`Warning: ${warning}`);
25480
+ });
25481
+ }
24755
25482
  function createNodeRestoreFs() {
24756
25483
  return {
24757
25484
  async readFile(filePath) {
@@ -24860,214 +25587,47 @@ async function createRestoreImageProcessors() {
24860
25587
  }
24861
25588
  };
24862
25589
  }
24863
- async function cmdInspect(args) {
24864
- if (args.length === 0) {
24865
- console.error("Usage: ofgui inspect <project-dir>");
24866
- process.exit(1);
24867
- }
24868
- const fairyPath = await resolveFairyPath(args[0]);
24869
- console.log(`Project: ${fairyPath}\n`);
24870
- printReport(inspect(await new NodeIO().readProject(fairyPath)));
25590
+ //#endregion
25591
+ //#region src/utils/package-version.ts
25592
+ const require = createRequire(import.meta.url);
25593
+ function getInjectedPackageVersion() {
25594
+ const version = import.meta.env?.PACKAGE_VERSION;
25595
+ return typeof version === "string" && version.length > 0 ? version : null;
24871
25596
  }
24872
- function printReport(report) {
24873
- console.log(`ID: ${report.projectId}`);
24874
- console.log(`Type: ${report.projectType}, Version: ${report.version}`);
24875
- console.log(`\nPackages: ${report.totals.packages}`);
24876
- console.log(` Images: ${report.totals.images}`);
24877
- console.log(` Sounds: ${report.totals.sounds}`);
24878
- console.log(` Fonts: ${report.totals.fonts}`);
24879
- console.log(` MovieClips: ${report.totals.movieClips}`);
24880
- console.log(` Components: ${report.totals.components}`);
24881
- console.log(` DisplayObjs: ${report.totals.displayObjects}`);
24882
- console.log(` Gears: ${report.totals.gears}`);
24883
- console.log(` Controllers: ${report.totals.controllers}`);
24884
- console.log(` Transitions: ${report.totals.transitions}`);
24885
- console.log("\nPackage details:");
24886
- for (const pkg of report.packages) {
24887
- const res = pkg.resources;
24888
- console.log(` ${pkg.name} (${pkg.id}): ${res.images.count} img, ${res.sounds.count} snd, ${res.fonts.count} font, ${res.components.count} comp`);
24889
- }
25597
+ function readPackageVersion() {
25598
+ const injectedVersion = getInjectedPackageVersion();
25599
+ if (injectedVersion) return injectedVersion;
25600
+ try {
25601
+ const pkg = require("../../package.json");
25602
+ if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
25603
+ } catch {}
25604
+ return "0.0.0-dev";
24890
25605
  }
24891
- function parseProjectType(value) {
24892
- if (!value) return void 0;
24893
- const trimmed = value.trim();
24894
- if (trimmed === "") return void 0;
24895
- if (/^\d+$/u.test(trimmed)) return Number(trimmed);
24896
- const normalized = trimmed.toLowerCase();
24897
- const map = {
24898
- unity: ProjectType.Unity,
24899
- flash: ProjectType.Flash,
24900
- starling: ProjectType.Starling,
24901
- cocoscreator: ProjectType.CocosCreator,
24902
- cocos: ProjectType.CocosCreator,
24903
- layabox: ProjectType.LayaBox,
24904
- laya: ProjectType.LayaBox,
24905
- egret: ProjectType.Egret,
24906
- haxe: ProjectType.Haxe,
24907
- pixi: ProjectType.Pixi,
24908
- libgdx: ProjectType.LibGDX,
24909
- unreal: ProjectType.Unreal,
24910
- cryengine: ProjectType.CryEngine,
24911
- monogame: ProjectType.MonoGame,
24912
- vision: ProjectType.Vision
24913
- };
24914
- const resolved = map[normalized];
24915
- if (resolved === void 0) throw new Error(`Unknown project type: ${value}. Use a numeric id or one of: ${Object.keys(map).join(", ")}`);
24916
- return resolved;
25606
+ //#endregion
25607
+ //#region src/cli.ts
25608
+ const PACKAGE_VERSION = readPackageVersion();
25609
+ function createProgram() {
25610
+ const program = new Command("ofgui");
25611
+ program.description("FairyGUI Headless Authoring CLI").version(PACKAGE_VERSION).showHelpAfterError();
25612
+ registerInspectCommand(program);
25613
+ registerPublishCommand(program);
25614
+ registerRestoreCommand(program);
25615
+ registerBackendCapabilitiesCommand(program);
25616
+ program.addHelpText("after", [
25617
+ "",
25618
+ "Alias:",
25619
+ " openfairygui",
25620
+ "",
25621
+ "Input can be a .fairy file or a project root directory (auto-discovers .fairy file).",
25622
+ "File extension and binary format are read from project settings."
25623
+ ].join("\n"));
25624
+ return program;
24917
25625
  }
24918
- async function cmdRestore(args) {
24919
- const { values, positionals } = parseArgs({
24920
- args,
24921
- options: {
24922
- output: {
24923
- type: "string",
24924
- short: "o"
24925
- },
24926
- packages: { type: "string" },
24927
- force: { type: "boolean" },
24928
- "project-type": { type: "string" }
24929
- },
24930
- allowPositionals: true
24931
- });
24932
- if (positionals.length === 0 || !values.output) {
24933
- console.error("Usage: ofgui restore <release-dir> --output <dir> [--packages a,b,c] [--force]");
24934
- process.exit(1);
24935
- }
24936
- const releaseDir = path.resolve(positionals[0]);
24937
- const outputDir = path.resolve(values.output);
24938
- const pkgFilter = values.packages ? values.packages.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
24939
- const projectType = parseProjectType(values["project-type"]);
24940
- const { cropImage, extractImage } = await createRestoreImageProcessors();
24941
- console.log(`Restoring published FairyGUI project: ${releaseDir}`);
24942
- const result = await restore({
24943
- inputDir: releaseDir,
24944
- output: outputDir,
24945
- fs: createNodeRestoreFs(),
24946
- packages: pkgFilter,
24947
- force: values.force,
24948
- projectType,
24949
- cropImage,
24950
- extractImage
24951
- });
24952
- const packages = result.document.getRoot().listPackages();
24953
- console.log(`\nDone! Output: ${result.projectPath}`);
24954
- console.log(`Packages: ${packages.map((pkg) => pkg.getName()).join(", ")}`);
24955
- for (const warning of result.warnings) console.warn(`Warning: ${warning}`);
24956
- }
24957
- async function cmdPublish(args) {
24958
- const { values, positionals } = parseArgs({
24959
- args,
24960
- options: {
24961
- output: {
24962
- type: "string",
24963
- short: "o"
24964
- },
24965
- compressed: { type: "boolean" },
24966
- packages: { type: "string" },
24967
- branch: { type: "string" },
24968
- "project-type": { type: "string" }
24969
- },
24970
- allowPositionals: true
24971
- });
24972
- if (positionals.length === 0 || !values.output) {
24973
- console.error("Usage: ofgui publish <project-dir> --output <dir> [--compressed] [--packages a,b,c] [--branch name]");
24974
- process.exit(1);
24975
- }
24976
- const fairyPath = await resolveFairyPath(positionals[0]);
24977
- const projectDir = path.dirname(fairyPath);
24978
- const outputDir = path.resolve(values.output);
24979
- console.log(`Reading project: ${fairyPath}`);
24980
- const doc = await new NodeIO().readProject(fairyPath);
24981
- const projectType = parseProjectType(values["project-type"]);
24982
- if (projectType !== void 0) doc.getRoot().setProjectType(projectType);
24983
- const pkgFilter = values.packages ? values.packages.split(",").map((s) => s.trim()) : void 0;
24984
- const resolved = resolvePublishOptions(doc, {
24985
- compressed: values.compressed,
24986
- packages: pkgFilter
24987
- });
24988
- console.log(`Settings: ext=${resolved.fileExtension}, compressed=${resolved.compressed}`);
24989
- if (values.branch) console.log(`Active branch: ${values.branch}`);
24990
- const atlasConfig = {
24991
- ...resolved.atlas,
24992
- readFileRaw: async (filePath) => {
24993
- const buf = await fs.readFile(filePath);
24994
- return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
24995
- }
24996
- };
24997
- let encoder;
24998
- try {
24999
- const sharp = await import("sharp");
25000
- encoder = sharp.default ?? sharp;
25001
- console.log("Sharp loaded — atlas PNGs will be generated.");
25002
- } catch {
25003
- console.log("Sharp not available — atlas PNGs will NOT be generated (layout only).");
25004
- console.log(" Install sharp to enable: pnpm add sharp");
25005
- }
25006
- await doc.transform(publish({
25007
- output: outputDir,
25008
- compressed: resolved.compressed,
25009
- fileExtension: resolved.fileExtension,
25010
- packages: resolved.packages,
25011
- fs: {
25012
- async readFileRaw(filePath) {
25013
- const buf = await fs.readFile(filePath);
25014
- return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
25015
- },
25016
- async writeFileRaw(filePath, data) {
25017
- await fs.mkdir(path.dirname(filePath), { recursive: true });
25018
- await fs.writeFile(filePath, data);
25019
- },
25020
- async mkdir(dirPath) {
25021
- await fs.mkdir(dirPath, { recursive: true });
25022
- },
25023
- async readdir(dirPath) {
25024
- return fs.readdir(dirPath);
25025
- },
25026
- async deleteFile(filePath) {
25027
- await fs.rm(filePath, { force: true });
25028
- },
25029
- join(...paths) {
25030
- return path.join(...paths);
25031
- }
25032
- },
25033
- encoder,
25034
- basePath: path.join(projectDir, "assets"),
25035
- atlas: atlasConfig,
25036
- branch: values.branch
25037
- }));
25038
- console.log(`\nDone! Output: ${outputDir}`);
25039
- }
25040
- async function cmdBackendCapabilities(args) {
25041
- if (args.length === 0) {
25042
- console.error("Usage: ofgui backend-capabilities <project-dir>");
25043
- process.exit(1);
25044
- }
25045
- const runtime = createNodeBackendRuntime();
25046
- const opened = await runtime.openSession({ projectPath: path.resolve(args[0]) });
25047
- if (!opened.ok) {
25048
- console.error(`backend-capabilities: ${opened.error.message}`);
25049
- process.exit(1);
25050
- }
25051
- const capabilities = runtime.getCapabilities();
25052
- if (!capabilities.ok) {
25053
- console.error("backend-capabilities: failed to read capabilities");
25054
- await runtime.closeSession({ sessionId: opened.data.sessionId });
25055
- process.exit(1);
25056
- }
25057
- console.log(`Session: ${opened.data.sessionId}`);
25058
- console.log(`Project: ${opened.data.canonicalProjectPath}`);
25059
- console.log(`Revision: ${opened.data.revision}`);
25060
- console.log(`Runtime owner: ${capabilities.data.runtimeOwner}`);
25061
- console.log(`Transaction owner: ${capabilities.data.transactionKernelOwner}`);
25062
- console.log(`App seam owner: ${capabilities.data.appSeamOwner}`);
25063
- const closed = await runtime.closeSession({ sessionId: opened.data.sessionId });
25064
- if (!closed.ok) {
25065
- console.error(`backend-capabilities: ${closed.error.message}`);
25066
- process.exit(1);
25067
- }
25626
+ async function main() {
25627
+ await createProgram().parseAsync(process.argv);
25068
25628
  }
25069
25629
  main().catch((err) => {
25070
- console.error(err);
25630
+ console.error(err instanceof Error ? err.message : String(err));
25071
25631
  process.exit(1);
25072
25632
  });
25073
25633
  //#endregion