@openfairygui/functions 0.2.0-alpha.34 → 0.2.0-alpha.36

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/web.cjs CHANGED
@@ -1,7 +1,116 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_publish = require("./publish-DCP0AYx2.cjs");
2
+ const require_publish = require("./publish-CykUJfVa.cjs");
3
3
  let _openfairygui_core = require("@openfairygui/core");
4
+ let fast_xml_parser = require("fast-xml-parser");
4
5
  //#region src/adapters/web/raster.ts
6
+ const MAX_SVG_SOURCE_BYTES = 8 * 1024 * 1024;
7
+ const MAX_SVG_DIMENSION = 16384;
8
+ const MAX_SVG_PIXELS = 64 * 1024 * 1024;
9
+ const MAX_SVG_NODES = 5e4;
10
+ const UNSAFE_SVG_ELEMENTS = new Set([
11
+ "a",
12
+ "animate",
13
+ "animatecolor",
14
+ "animatemotion",
15
+ "animatetransform",
16
+ "audio",
17
+ "canvas",
18
+ "discard",
19
+ "embed",
20
+ "feimage",
21
+ "foreignobject",
22
+ "iframe",
23
+ "image",
24
+ "object",
25
+ "script",
26
+ "set",
27
+ "style",
28
+ "video"
29
+ ]);
30
+ function unsafeSvg(message) {
31
+ throw new Error(`publishBrowser: unsafe SVG input (${message}).`);
32
+ }
33
+ function svgLocalName(name) {
34
+ return name.split(":").at(-1).toLowerCase();
35
+ }
36
+ function parseSvgLength(value, name) {
37
+ if (value === void 0) return void 0;
38
+ const match = String(value).match(/^\s*(?:\d+(?:\.\d+)?|\.\d+)(?:px)?\s*$/iu);
39
+ if (!match) unsafeSvg(`${name} must use a finite pixel value`);
40
+ const parsed = Number.parseFloat(match[0]);
41
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > MAX_SVG_DIMENSION) unsafeSvg(`${name} exceeds the supported dimensions`);
42
+ return parsed;
43
+ }
44
+ function validateSvgAttribute(name, value) {
45
+ const normalizedName = name.toLowerCase();
46
+ if (normalizedName === "xmlns" || normalizedName.startsWith("xmlns:")) return;
47
+ const localName = svgLocalName(name);
48
+ const text = String(value);
49
+ if (localName.startsWith("on")) unsafeSvg(`event attribute "${name}" is not allowed`);
50
+ if (localName === "style" || localName === "src") unsafeSvg(`attribute "${name}" is not allowed`);
51
+ if (localName === "href" && !/^#[A-Za-z_][\w:.-]*$/u.test(text)) unsafeSvg(`external reference in "${name}" is not allowed`);
52
+ if (/(?:^|[\s("'=])(?:https?:|file:|javascript:|data:|\/\/)/iu.test(text)) unsafeSvg(`external URL in "${name}" is not allowed`);
53
+ for (const match of text.matchAll(/url\s*\(([^)]*)\)/giu)) {
54
+ const reference = (match[1] ?? "").trim().replace(/^(['"])(.*)\1$/u, "$2");
55
+ if (!/^#[A-Za-z_][\w:.-]*$/u.test(reference)) unsafeSvg(`external url() in "${name}" is not allowed`);
56
+ }
57
+ }
58
+ function visitSvgEntry(entry) {
59
+ const pending = [entry];
60
+ let nodeCount = 0;
61
+ while (pending.length > 0) {
62
+ const current = pending.pop();
63
+ for (const [name, value] of Object.entries(current)) {
64
+ if (name === ":@" || name.startsWith("#") || name.startsWith("?")) continue;
65
+ if (++nodeCount > MAX_SVG_NODES) unsafeSvg("node count exceeds the supported limit");
66
+ const localName = svgLocalName(name);
67
+ if (UNSAFE_SVG_ELEMENTS.has(localName)) unsafeSvg(`element <${name}> is not allowed`);
68
+ for (const [attributeName, attributeValue] of Object.entries(current[":@"] ?? {})) validateSvgAttribute(attributeName, attributeValue);
69
+ if (Array.isArray(value)) {
70
+ for (const child of value) if (child && typeof child === "object" && !Array.isArray(child)) pending.push(child);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ function validateSvg(bytes) {
76
+ if (bytes.byteLength === 0 || bytes.byteLength > MAX_SVG_SOURCE_BYTES) unsafeSvg("source size is unsupported");
77
+ let source;
78
+ try {
79
+ source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
80
+ } catch {
81
+ unsafeSvg("source is not valid UTF-8");
82
+ }
83
+ if (/<!\s*(?:doctype|entity)\b|<\?xml-stylesheet\b/iu.test(source)) unsafeSvg("DTD, entities, and stylesheets are not allowed");
84
+ if (fast_xml_parser.XMLValidator.validate(source, { allowBooleanAttributes: false }) !== true) unsafeSvg("source is not well-formed XML");
85
+ const roots = new fast_xml_parser.XMLParser({
86
+ preserveOrder: true,
87
+ ignoreAttributes: false,
88
+ attributeNamePrefix: "",
89
+ parseAttributeValue: false,
90
+ parseTagValue: false,
91
+ processEntities: false,
92
+ trimValues: false
93
+ }).parse(source).flatMap((entry) => Object.keys(entry).filter((name) => name !== ":@" && !name.startsWith("#") && !name.startsWith("?")).map((name) => ({
94
+ entry,
95
+ name
96
+ })));
97
+ if (roots.length !== 1 || svgLocalName(roots[0].name) !== "svg") unsafeSvg("a single <svg> root is required");
98
+ const root = roots[0].entry;
99
+ visitSvgEntry(root);
100
+ const attributes = root[":@"] ?? {};
101
+ const width = parseSvgLength(attributes.width, "width");
102
+ const height = parseSvgLength(attributes.height, "height");
103
+ let viewBoxWidth;
104
+ let viewBoxHeight;
105
+ if (attributes.viewBox !== void 0) {
106
+ const viewBox = String(attributes.viewBox).trim().split(/[\s,]+/u).map(Number);
107
+ if (viewBox.length !== 4 || viewBox.some((value) => !Number.isFinite(value)) || viewBox[2] <= 0 || viewBox[3] <= 0) unsafeSvg("viewBox must contain four finite values with positive dimensions");
108
+ viewBoxWidth = viewBox[2];
109
+ viewBoxHeight = viewBox[3];
110
+ if (viewBoxWidth > MAX_SVG_DIMENSION || viewBoxHeight > MAX_SVG_DIMENSION) unsafeSvg("viewBox exceeds the supported dimensions");
111
+ }
112
+ if ((width ?? viewBoxWidth ?? 300) * (height ?? viewBoxHeight ?? 150) > MAX_SVG_PIXELS) unsafeSvg("pixel count exceeds the supported limit");
113
+ }
5
114
  function getBrowserContext(canvas) {
6
115
  const context = canvas.getContext("2d");
7
116
  if (!context) throw new Error("publishBrowser: a 2D canvas context is unavailable.");
@@ -60,7 +169,15 @@ async function canvasToPng(canvas) {
60
169
  async function decodeRaster(bytes, mimeType) {
61
170
  if (typeof createImageBitmap !== "function") throw new Error("publishBrowser: createImageBitmap is required for atlas PNG generation.");
62
171
  const copy = bytes.slice();
63
- const bitmap = await createImageBitmap(new Blob([copy.buffer], { type: mimeType }));
172
+ if (mimeType === "image/svg+xml") validateSvg(copy);
173
+ const blob = new Blob([copy.buffer], { type: mimeType });
174
+ let bitmap;
175
+ try {
176
+ bitmap = await createImageBitmap(blob);
177
+ } catch (error) {
178
+ if (mimeType !== "image/svg+xml") throw error;
179
+ return decodeSvgWithDom(blob);
180
+ }
64
181
  try {
65
182
  const raster = createRaster(bitmap.width, bitmap.height);
66
183
  getBrowserContext(raster.canvas).drawImage(bitmap, 0, 0);
@@ -69,6 +186,26 @@ async function decodeRaster(bytes, mimeType) {
69
186
  bitmap.close();
70
187
  }
71
188
  }
189
+ async function decodeSvgWithDom(blob) {
190
+ if (typeof globalThis.Image !== "function" || typeof globalThis.URL?.createObjectURL !== "function" || typeof globalThis.URL?.revokeObjectURL !== "function") throw new Error("publishBrowser: createImageBitmap rejected SVG and DOM image decoding is unavailable.");
191
+ const url = globalThis.URL.createObjectURL(blob);
192
+ try {
193
+ const image = new globalThis.Image();
194
+ await new Promise((resolve, reject) => {
195
+ image.onload = () => resolve();
196
+ image.onerror = () => reject(/* @__PURE__ */ new Error("publishBrowser: DOM image decoding failed for SVG."));
197
+ image.src = url;
198
+ });
199
+ const width = image.naturalWidth || image.width;
200
+ const height = image.naturalHeight || image.height;
201
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_SVG_DIMENSION || height > MAX_SVG_DIMENSION || width * height > MAX_SVG_PIXELS) unsafeSvg("decoded dimensions exceed the supported limit");
202
+ const raster = createRaster(width, height);
203
+ getBrowserContext(raster.canvas).drawImage(image, 0, 0);
204
+ return raster;
205
+ } finally {
206
+ globalThis.URL.revokeObjectURL(url);
207
+ }
208
+ }
72
209
  var BrowserImagePipeline = class {
73
210
  rawOutput = false;
74
211
  constructor(raster, decode, write) {
@@ -223,6 +360,15 @@ function toResult(success, files, diagnostics) {
223
360
  diagnostics
224
361
  };
225
362
  }
363
+ function unsupportedSetting(setting, path, message) {
364
+ return {
365
+ level: "error",
366
+ code: "unsupported_publish_setting",
367
+ setting,
368
+ path,
369
+ message
370
+ };
371
+ }
226
372
  /**
227
373
  * Publish a loaded FairyGUI project to browser-provided storage.
228
374
  *
@@ -238,14 +384,33 @@ async function publishBrowser(options) {
238
384
  options.document.setLogger(createDiagnosticLogger(previousLogger, diagnostics));
239
385
  try {
240
386
  if (options.projectType !== "layabox") throw new Error(`publishBrowser: unsupported project type "${String(options.projectType)}".`);
241
- assertBrowserImageSupport();
242
387
  root.setProjectType(_openfairygui_core.ProjectType.LayaBox);
388
+ const resolved = require_publish.resolvePublishOptions(options.document, {
389
+ compressed: options.compressed,
390
+ packages: options.packages,
391
+ atlas: options.atlas
392
+ });
393
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(resolved.fileExtension)) {
394
+ diagnostics.push(unsupportedSetting("fileExtension", "settings.publish.fileExtension", `publishBrowser: unsupported fileExtension "${resolved.fileExtension}".`));
395
+ return toResult(false, files, diagnostics);
396
+ }
397
+ const selectedPackageNames = options.packages?.length ? new Set(options.packages) : null;
398
+ const selectedPackages = root.listPackages().filter((pkg) => !selectedPackageNames || selectedPackageNames.has(pkg.getName()));
399
+ if (require_publish.resolveCodeGenerationSettings(options.document).allowGenCode) {
400
+ const packageIndex = selectedPackages.findIndex((pkg) => pkg.getGenCode());
401
+ if (packageIndex >= 0) {
402
+ const pkg = selectedPackages[packageIndex];
403
+ diagnostics.push(unsupportedSetting("codeGeneration", `packages[${root.listPackages().indexOf(pkg)}].publish.genCode`, `publishBrowser: code generation requested by package "${pkg.getName()}" is not supported.`));
404
+ return toResult(false, files, diagnostics);
405
+ }
406
+ }
407
+ assertBrowserImageSupport();
243
408
  const outputFileSystem = createTrackingFileSystem(options.outputFileSystem, files);
244
409
  const sourceAssetsPath = options.sourceFileSystem.join(options.document.getProjectDir(), "assets");
245
410
  await options.document.transform(require_publish.publish({
246
411
  output: options.output,
247
- compressed: options.compressed,
248
- fileExtension: "fui",
412
+ compressed: resolved.compressed,
413
+ fileExtension: resolved.fileExtension,
249
414
  packages: options.packages,
250
415
  branch: options.branch,
251
416
  basePath: sourceAssetsPath,
@@ -262,6 +427,7 @@ async function publishBrowser(options) {
262
427
  } catch (error) {
263
428
  diagnostics.push({
264
429
  level: "error",
430
+ code: "publish_failed",
265
431
  message: error instanceof Error ? error.message : String(error)
266
432
  });
267
433
  return toResult(false, files, diagnostics);
package/dist/web.d.cts CHANGED
@@ -20,6 +20,9 @@ interface BrowserPublishOptions {
20
20
  interface BrowserPublishDiagnostic {
21
21
  level: 'debug' | 'info' | 'warning' | 'error';
22
22
  message: string;
23
+ code?: 'unsupported_publish_setting' | 'publish_failed';
24
+ setting?: string;
25
+ path?: string;
23
26
  }
24
27
  interface BrowserPublishedFile {
25
28
  path: string;
package/dist/web.d.ts CHANGED
@@ -20,6 +20,9 @@ interface BrowserPublishOptions {
20
20
  interface BrowserPublishDiagnostic {
21
21
  level: 'debug' | 'info' | 'warning' | 'error';
22
22
  message: string;
23
+ code?: 'unsupported_publish_setting' | 'publish_failed';
24
+ setting?: string;
25
+ path?: string;
23
26
  }
24
27
  interface BrowserPublishedFile {
25
28
  path: string;
package/dist/web.js CHANGED
@@ -1,6 +1,115 @@
1
- import { t as publish } from "./publish-BJk8UzRP.js";
1
+ import { c as resolveCodeGenerationSettings, n as resolvePublishOptions, t as publish } from "./publish-DXoaC1Nl.js";
2
2
  import { ProjectType } from "@openfairygui/core";
3
+ import { XMLParser, XMLValidator } from "fast-xml-parser";
3
4
  //#region src/adapters/web/raster.ts
5
+ const MAX_SVG_SOURCE_BYTES = 8 * 1024 * 1024;
6
+ const MAX_SVG_DIMENSION = 16384;
7
+ const MAX_SVG_PIXELS = 64 * 1024 * 1024;
8
+ const MAX_SVG_NODES = 5e4;
9
+ const UNSAFE_SVG_ELEMENTS = new Set([
10
+ "a",
11
+ "animate",
12
+ "animatecolor",
13
+ "animatemotion",
14
+ "animatetransform",
15
+ "audio",
16
+ "canvas",
17
+ "discard",
18
+ "embed",
19
+ "feimage",
20
+ "foreignobject",
21
+ "iframe",
22
+ "image",
23
+ "object",
24
+ "script",
25
+ "set",
26
+ "style",
27
+ "video"
28
+ ]);
29
+ function unsafeSvg(message) {
30
+ throw new Error(`publishBrowser: unsafe SVG input (${message}).`);
31
+ }
32
+ function svgLocalName(name) {
33
+ return name.split(":").at(-1).toLowerCase();
34
+ }
35
+ function parseSvgLength(value, name) {
36
+ if (value === void 0) return void 0;
37
+ const match = String(value).match(/^\s*(?:\d+(?:\.\d+)?|\.\d+)(?:px)?\s*$/iu);
38
+ if (!match) unsafeSvg(`${name} must use a finite pixel value`);
39
+ const parsed = Number.parseFloat(match[0]);
40
+ if (!Number.isFinite(parsed) || parsed <= 0 || parsed > MAX_SVG_DIMENSION) unsafeSvg(`${name} exceeds the supported dimensions`);
41
+ return parsed;
42
+ }
43
+ function validateSvgAttribute(name, value) {
44
+ const normalizedName = name.toLowerCase();
45
+ if (normalizedName === "xmlns" || normalizedName.startsWith("xmlns:")) return;
46
+ const localName = svgLocalName(name);
47
+ const text = String(value);
48
+ if (localName.startsWith("on")) unsafeSvg(`event attribute "${name}" is not allowed`);
49
+ if (localName === "style" || localName === "src") unsafeSvg(`attribute "${name}" is not allowed`);
50
+ if (localName === "href" && !/^#[A-Za-z_][\w:.-]*$/u.test(text)) unsafeSvg(`external reference in "${name}" is not allowed`);
51
+ if (/(?:^|[\s("'=])(?:https?:|file:|javascript:|data:|\/\/)/iu.test(text)) unsafeSvg(`external URL in "${name}" is not allowed`);
52
+ for (const match of text.matchAll(/url\s*\(([^)]*)\)/giu)) {
53
+ const reference = (match[1] ?? "").trim().replace(/^(['"])(.*)\1$/u, "$2");
54
+ if (!/^#[A-Za-z_][\w:.-]*$/u.test(reference)) unsafeSvg(`external url() in "${name}" is not allowed`);
55
+ }
56
+ }
57
+ function visitSvgEntry(entry) {
58
+ const pending = [entry];
59
+ let nodeCount = 0;
60
+ while (pending.length > 0) {
61
+ const current = pending.pop();
62
+ for (const [name, value] of Object.entries(current)) {
63
+ if (name === ":@" || name.startsWith("#") || name.startsWith("?")) continue;
64
+ if (++nodeCount > MAX_SVG_NODES) unsafeSvg("node count exceeds the supported limit");
65
+ const localName = svgLocalName(name);
66
+ if (UNSAFE_SVG_ELEMENTS.has(localName)) unsafeSvg(`element <${name}> is not allowed`);
67
+ for (const [attributeName, attributeValue] of Object.entries(current[":@"] ?? {})) validateSvgAttribute(attributeName, attributeValue);
68
+ if (Array.isArray(value)) {
69
+ for (const child of value) if (child && typeof child === "object" && !Array.isArray(child)) pending.push(child);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ function validateSvg(bytes) {
75
+ if (bytes.byteLength === 0 || bytes.byteLength > MAX_SVG_SOURCE_BYTES) unsafeSvg("source size is unsupported");
76
+ let source;
77
+ try {
78
+ source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
79
+ } catch {
80
+ unsafeSvg("source is not valid UTF-8");
81
+ }
82
+ if (/<!\s*(?:doctype|entity)\b|<\?xml-stylesheet\b/iu.test(source)) unsafeSvg("DTD, entities, and stylesheets are not allowed");
83
+ if (XMLValidator.validate(source, { allowBooleanAttributes: false }) !== true) unsafeSvg("source is not well-formed XML");
84
+ const roots = new XMLParser({
85
+ preserveOrder: true,
86
+ ignoreAttributes: false,
87
+ attributeNamePrefix: "",
88
+ parseAttributeValue: false,
89
+ parseTagValue: false,
90
+ processEntities: false,
91
+ trimValues: false
92
+ }).parse(source).flatMap((entry) => Object.keys(entry).filter((name) => name !== ":@" && !name.startsWith("#") && !name.startsWith("?")).map((name) => ({
93
+ entry,
94
+ name
95
+ })));
96
+ if (roots.length !== 1 || svgLocalName(roots[0].name) !== "svg") unsafeSvg("a single <svg> root is required");
97
+ const root = roots[0].entry;
98
+ visitSvgEntry(root);
99
+ const attributes = root[":@"] ?? {};
100
+ const width = parseSvgLength(attributes.width, "width");
101
+ const height = parseSvgLength(attributes.height, "height");
102
+ let viewBoxWidth;
103
+ let viewBoxHeight;
104
+ if (attributes.viewBox !== void 0) {
105
+ const viewBox = String(attributes.viewBox).trim().split(/[\s,]+/u).map(Number);
106
+ if (viewBox.length !== 4 || viewBox.some((value) => !Number.isFinite(value)) || viewBox[2] <= 0 || viewBox[3] <= 0) unsafeSvg("viewBox must contain four finite values with positive dimensions");
107
+ viewBoxWidth = viewBox[2];
108
+ viewBoxHeight = viewBox[3];
109
+ if (viewBoxWidth > MAX_SVG_DIMENSION || viewBoxHeight > MAX_SVG_DIMENSION) unsafeSvg("viewBox exceeds the supported dimensions");
110
+ }
111
+ if ((width ?? viewBoxWidth ?? 300) * (height ?? viewBoxHeight ?? 150) > MAX_SVG_PIXELS) unsafeSvg("pixel count exceeds the supported limit");
112
+ }
4
113
  function getBrowserContext(canvas) {
5
114
  const context = canvas.getContext("2d");
6
115
  if (!context) throw new Error("publishBrowser: a 2D canvas context is unavailable.");
@@ -59,7 +168,15 @@ async function canvasToPng(canvas) {
59
168
  async function decodeRaster(bytes, mimeType) {
60
169
  if (typeof createImageBitmap !== "function") throw new Error("publishBrowser: createImageBitmap is required for atlas PNG generation.");
61
170
  const copy = bytes.slice();
62
- const bitmap = await createImageBitmap(new Blob([copy.buffer], { type: mimeType }));
171
+ if (mimeType === "image/svg+xml") validateSvg(copy);
172
+ const blob = new Blob([copy.buffer], { type: mimeType });
173
+ let bitmap;
174
+ try {
175
+ bitmap = await createImageBitmap(blob);
176
+ } catch (error) {
177
+ if (mimeType !== "image/svg+xml") throw error;
178
+ return decodeSvgWithDom(blob);
179
+ }
63
180
  try {
64
181
  const raster = createRaster(bitmap.width, bitmap.height);
65
182
  getBrowserContext(raster.canvas).drawImage(bitmap, 0, 0);
@@ -68,6 +185,26 @@ async function decodeRaster(bytes, mimeType) {
68
185
  bitmap.close();
69
186
  }
70
187
  }
188
+ async function decodeSvgWithDom(blob) {
189
+ if (typeof globalThis.Image !== "function" || typeof globalThis.URL?.createObjectURL !== "function" || typeof globalThis.URL?.revokeObjectURL !== "function") throw new Error("publishBrowser: createImageBitmap rejected SVG and DOM image decoding is unavailable.");
190
+ const url = globalThis.URL.createObjectURL(blob);
191
+ try {
192
+ const image = new globalThis.Image();
193
+ await new Promise((resolve, reject) => {
194
+ image.onload = () => resolve();
195
+ image.onerror = () => reject(/* @__PURE__ */ new Error("publishBrowser: DOM image decoding failed for SVG."));
196
+ image.src = url;
197
+ });
198
+ const width = image.naturalWidth || image.width;
199
+ const height = image.naturalHeight || image.height;
200
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_SVG_DIMENSION || height > MAX_SVG_DIMENSION || width * height > MAX_SVG_PIXELS) unsafeSvg("decoded dimensions exceed the supported limit");
201
+ const raster = createRaster(width, height);
202
+ getBrowserContext(raster.canvas).drawImage(image, 0, 0);
203
+ return raster;
204
+ } finally {
205
+ globalThis.URL.revokeObjectURL(url);
206
+ }
207
+ }
71
208
  var BrowserImagePipeline = class {
72
209
  rawOutput = false;
73
210
  constructor(raster, decode, write) {
@@ -222,6 +359,15 @@ function toResult(success, files, diagnostics) {
222
359
  diagnostics
223
360
  };
224
361
  }
362
+ function unsupportedSetting(setting, path, message) {
363
+ return {
364
+ level: "error",
365
+ code: "unsupported_publish_setting",
366
+ setting,
367
+ path,
368
+ message
369
+ };
370
+ }
225
371
  /**
226
372
  * Publish a loaded FairyGUI project to browser-provided storage.
227
373
  *
@@ -237,14 +383,33 @@ async function publishBrowser(options) {
237
383
  options.document.setLogger(createDiagnosticLogger(previousLogger, diagnostics));
238
384
  try {
239
385
  if (options.projectType !== "layabox") throw new Error(`publishBrowser: unsupported project type "${String(options.projectType)}".`);
240
- assertBrowserImageSupport();
241
386
  root.setProjectType(ProjectType.LayaBox);
387
+ const resolved = resolvePublishOptions(options.document, {
388
+ compressed: options.compressed,
389
+ packages: options.packages,
390
+ atlas: options.atlas
391
+ });
392
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(resolved.fileExtension)) {
393
+ diagnostics.push(unsupportedSetting("fileExtension", "settings.publish.fileExtension", `publishBrowser: unsupported fileExtension "${resolved.fileExtension}".`));
394
+ return toResult(false, files, diagnostics);
395
+ }
396
+ const selectedPackageNames = options.packages?.length ? new Set(options.packages) : null;
397
+ const selectedPackages = root.listPackages().filter((pkg) => !selectedPackageNames || selectedPackageNames.has(pkg.getName()));
398
+ if (resolveCodeGenerationSettings(options.document).allowGenCode) {
399
+ const packageIndex = selectedPackages.findIndex((pkg) => pkg.getGenCode());
400
+ if (packageIndex >= 0) {
401
+ const pkg = selectedPackages[packageIndex];
402
+ diagnostics.push(unsupportedSetting("codeGeneration", `packages[${root.listPackages().indexOf(pkg)}].publish.genCode`, `publishBrowser: code generation requested by package "${pkg.getName()}" is not supported.`));
403
+ return toResult(false, files, diagnostics);
404
+ }
405
+ }
406
+ assertBrowserImageSupport();
242
407
  const outputFileSystem = createTrackingFileSystem(options.outputFileSystem, files);
243
408
  const sourceAssetsPath = options.sourceFileSystem.join(options.document.getProjectDir(), "assets");
244
409
  await options.document.transform(publish({
245
410
  output: options.output,
246
- compressed: options.compressed,
247
- fileExtension: "fui",
411
+ compressed: resolved.compressed,
412
+ fileExtension: resolved.fileExtension,
248
413
  packages: options.packages,
249
414
  branch: options.branch,
250
415
  basePath: sourceAssetsPath,
@@ -261,6 +426,7 @@ async function publishBrowser(options) {
261
426
  } catch (error) {
262
427
  diagnostics.push({
263
428
  level: "error",
429
+ code: "publish_failed",
264
430
  message: error instanceof Error ? error.message : String(error)
265
431
  });
266
432
  return toResult(false, files, diagnostics);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfairygui/functions",
3
- "version": "0.2.0-alpha.34",
3
+ "version": "0.2.0-alpha.36",
4
4
  "description": "FairyGUI Headless Authoring SDK — composable transform functions.",
5
5
  "author": "OpenFairyGUI Contributors",
6
6
  "license": "MIT",
@@ -73,8 +73,9 @@
73
73
  "publish"
74
74
  ],
75
75
  "dependencies": {
76
+ "fast-xml-parser": "^5.0.0",
76
77
  "jiti": "^2.6.1",
77
- "@openfairygui/core": "0.2.0-alpha.34"
78
+ "@openfairygui/core": "0.2.0-alpha.36"
78
79
  },
79
80
  "optionalDependencies": {
80
81
  "sharp": ">=0.33.0"
@@ -1,11 +1,13 @@
1
1
  import { type Document, type ILogger, ProjectType } from '@openfairygui/core';
2
2
  import type { AtlasOptions } from '../../atlas.js';
3
+ import { resolveCodeGenerationSettings } from '../../codegen.js';
3
4
  import { publish } from '../../publish.js';
4
5
  import type {
5
6
  PublishFileSystem,
6
7
  PublishOutputFileSystem,
7
8
  PublishSourceFileSystem,
8
9
  } from '../../publish/contracts.js';
10
+ import { resolvePublishOptions } from '../../publish/options.js';
9
11
  import { assertBrowserImageSupport, createBrowserImageEncoder } from './raster.js';
10
12
 
11
13
  export type BrowserPublishProjectType = 'layabox';
@@ -42,6 +44,9 @@ export interface BrowserPublishOptions {
42
44
  export interface BrowserPublishDiagnostic {
43
45
  level: 'debug' | 'info' | 'warning' | 'error';
44
46
  message: string;
47
+ code?: 'unsupported_publish_setting' | 'publish_failed';
48
+ setting?: string;
49
+ path?: string;
45
50
  }
46
51
 
47
52
  export interface BrowserPublishedFile {
@@ -103,6 +108,10 @@ function toResult(
103
108
  };
104
109
  }
105
110
 
111
+ function unsupportedSetting(setting: string, path: string, message: string): BrowserPublishDiagnostic {
112
+ return { level: 'error', code: 'unsupported_publish_setting', setting, path, message };
113
+ }
114
+
106
115
  /**
107
116
  * Publish a loaded FairyGUI project to browser-provided storage.
108
117
  *
@@ -121,16 +130,43 @@ export async function publishBrowser(options: BrowserPublishOptions): Promise<Br
121
130
  if (options.projectType !== 'layabox') {
122
131
  throw new Error(`publishBrowser: unsupported project type "${String(options.projectType)}".`);
123
132
  }
124
- assertBrowserImageSupport();
125
133
  root.setProjectType(ProjectType.LayaBox);
134
+ const resolved = resolvePublishOptions(options.document, {
135
+ compressed: options.compressed,
136
+ packages: options.packages,
137
+ atlas: options.atlas,
138
+ });
139
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(resolved.fileExtension)) {
140
+ diagnostics.push(unsupportedSetting(
141
+ 'fileExtension',
142
+ 'settings.publish.fileExtension',
143
+ `publishBrowser: unsupported fileExtension "${resolved.fileExtension}".`,
144
+ ));
145
+ return toResult(false, files, diagnostics);
146
+ }
147
+ const selectedPackageNames = options.packages?.length ? new Set(options.packages) : null;
148
+ const selectedPackages = root.listPackages().filter((pkg) => !selectedPackageNames || selectedPackageNames.has(pkg.getName()));
149
+ if (resolveCodeGenerationSettings(options.document).allowGenCode) {
150
+ const packageIndex = selectedPackages.findIndex((pkg) => pkg.getGenCode());
151
+ if (packageIndex >= 0) {
152
+ const pkg = selectedPackages[packageIndex]!;
153
+ diagnostics.push(unsupportedSetting(
154
+ 'codeGeneration',
155
+ `packages[${root.listPackages().indexOf(pkg)}].publish.genCode`,
156
+ `publishBrowser: code generation requested by package "${pkg.getName()}" is not supported.`,
157
+ ));
158
+ return toResult(false, files, diagnostics);
159
+ }
160
+ }
161
+ assertBrowserImageSupport();
126
162
  const outputFileSystem = createTrackingFileSystem(options.outputFileSystem, files);
127
163
  const sourceAssetsPath = options.sourceFileSystem.join(options.document.getProjectDir(), 'assets');
128
164
 
129
165
  await options.document.transform(
130
166
  publish({
131
167
  output: options.output,
132
- compressed: options.compressed,
133
- fileExtension: 'fui',
168
+ compressed: resolved.compressed,
169
+ fileExtension: resolved.fileExtension,
134
170
  packages: options.packages,
135
171
  branch: options.branch,
136
172
  basePath: sourceAssetsPath,
@@ -149,6 +185,7 @@ export async function publishBrowser(options: BrowserPublishOptions): Promise<Br
149
185
  } catch (error) {
150
186
  diagnostics.push({
151
187
  level: 'error',
188
+ code: 'publish_failed',
152
189
  message: error instanceof Error ? error.message : String(error),
153
190
  });
154
191
  return toResult(false, files, diagnostics);