@openfairygui/functions 0.2.0-alpha.7 → 0.2.0
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/README.md +52 -6
- package/dist/atlas-C6tbl7nn.d.ts +193 -0
- package/dist/atlas-CHsu2Y8i.d.cts +193 -0
- package/dist/index.cjs +17 -3603
- package/dist/index.d.cts +5 -294
- package/dist/index.d.ts +5 -294
- package/dist/index.js +4 -3595
- package/dist/node.cjs +256 -0
- package/dist/node.d.cts +36 -0
- package/dist/node.d.ts +36 -0
- package/dist/node.js +254 -0
- package/dist/publish-CykUJfVa.cjs +3269 -0
- package/dist/publish-DXoaC1Nl.js +3174 -0
- package/dist/restore-BQp01WY3.js +914 -0
- package/dist/restore-BeWaJNjR.d.cts +288 -0
- package/dist/restore-CEywQUHz.cjs +919 -0
- package/dist/restore-Dh0-Nvms.d.ts +288 -0
- package/dist/uam-transaction.cjs +29 -14
- package/dist/uam-transaction.d.cts +2 -1
- package/dist/uam-transaction.d.ts +2 -1
- package/dist/uam-transaction.js +30 -16
- package/dist/web.cjs +440 -0
- package/dist/web.d.cts +44 -0
- package/dist/web.d.ts +44 -0
- package/dist/web.js +439 -0
- package/package.json +29 -4
- package/src/adapters/node/plugins.ts +82 -0
- package/src/adapters/node/publish.ts +130 -0
- package/src/adapters/node/restore.ts +187 -0
- package/src/adapters/web/publish.ts +196 -0
- package/src/adapters/web/raster.ts +421 -0
- package/src/atlas/font.ts +95 -0
- package/src/atlas/inputs.ts +445 -0
- package/src/atlas/jta.ts +157 -0
- package/src/atlas/packing.ts +762 -0
- package/src/atlas.ts +129 -1221
- package/src/codegen.ts +108 -82
- package/src/index.ts +43 -3
- package/src/node.ts +8 -0
- package/src/path-utils.ts +40 -0
- package/src/plugins/types.ts +56 -0
- package/src/publish/contracts.ts +80 -0
- package/src/publish/external-resources.ts +117 -0
- package/src/publish/options.ts +180 -0
- package/src/publish/package-context.ts +608 -0
- package/src/publish/resource-references.ts +210 -0
- package/src/publish.ts +327 -975
- package/src/restore-internals/font.ts +100 -0
- package/src/restore-internals/movie-clip.ts +104 -0
- package/src/restore-internals/output-transaction.ts +124 -0
- package/src/restore.ts +122 -311
- package/src/shared-types.ts +4 -8
- package/src/uam-transaction.ts +34 -17
- package/src/utils.ts +28 -0
- package/src/web.ts +11 -0
package/dist/web.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { c as resolveCodeGenerationSettings, n as resolvePublishOptions, t as publish } from "./publish-DXoaC1Nl.js";
|
|
2
|
+
import { ProjectType } from "@openfairygui/core";
|
|
3
|
+
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
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
|
+
}
|
|
113
|
+
function getBrowserContext(canvas) {
|
|
114
|
+
const context = canvas.getContext("2d");
|
|
115
|
+
if (!context) throw new Error("publishBrowser: a 2D canvas context is unavailable.");
|
|
116
|
+
return context;
|
|
117
|
+
}
|
|
118
|
+
function createBrowserCanvas(width, height) {
|
|
119
|
+
if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(width, height);
|
|
120
|
+
if (typeof globalThis.document === "undefined") throw new Error("publishBrowser: OffscreenCanvas or a DOM canvas is required for atlas PNG generation.");
|
|
121
|
+
const canvas = globalThis.document.createElement("canvas");
|
|
122
|
+
canvas.width = width;
|
|
123
|
+
canvas.height = height;
|
|
124
|
+
return canvas;
|
|
125
|
+
}
|
|
126
|
+
function assertBrowserImageSupport() {
|
|
127
|
+
if (typeof createImageBitmap !== "function") throw new Error("publishBrowser: createImageBitmap is required for atlas PNG generation.");
|
|
128
|
+
if (typeof OffscreenCanvas === "undefined" && typeof globalThis.document === "undefined") throw new Error("publishBrowser: OffscreenCanvas or a DOM canvas is required for atlas PNG generation.");
|
|
129
|
+
}
|
|
130
|
+
function createRaster(width, height, background) {
|
|
131
|
+
const canvas = createBrowserCanvas(width, height);
|
|
132
|
+
const context = getBrowserContext(canvas);
|
|
133
|
+
context.clearRect(0, 0, width, height);
|
|
134
|
+
if (background && background.alpha > 0) {
|
|
135
|
+
context.fillStyle = `rgba(${background.r}, ${background.g}, ${background.b}, ${background.alpha})`;
|
|
136
|
+
context.fillRect(0, 0, width, height);
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
canvas,
|
|
140
|
+
width,
|
|
141
|
+
height
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function imageMimeType(path) {
|
|
145
|
+
if (/\.svg$/iu.test(path)) return "image/svg+xml";
|
|
146
|
+
if (/\.jpe?g$/iu.test(path)) return "image/jpeg";
|
|
147
|
+
if (/\.webp$/iu.test(path)) return "image/webp";
|
|
148
|
+
if (/\.gif$/iu.test(path)) return "image/gif";
|
|
149
|
+
return "image/png";
|
|
150
|
+
}
|
|
151
|
+
function imageMimeTypeFromBytes(bytes) {
|
|
152
|
+
if (bytes[0] === 255 && bytes[1] === 216) return "image/jpeg";
|
|
153
|
+
if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) return "image/gif";
|
|
154
|
+
if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70) return "image/webp";
|
|
155
|
+
return "image/png";
|
|
156
|
+
}
|
|
157
|
+
async function canvasToPng(canvas) {
|
|
158
|
+
let blob;
|
|
159
|
+
if ("convertToBlob" in canvas && typeof canvas.convertToBlob === "function") blob = await canvas.convertToBlob({ type: "image/png" });
|
|
160
|
+
else blob = await new Promise((resolve, reject) => {
|
|
161
|
+
canvas.toBlob((value) => {
|
|
162
|
+
if (value) resolve(value);
|
|
163
|
+
else reject(/* @__PURE__ */ new Error("publishBrowser: canvas PNG encoding failed."));
|
|
164
|
+
}, "image/png");
|
|
165
|
+
});
|
|
166
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
167
|
+
}
|
|
168
|
+
async function decodeRaster(bytes, mimeType) {
|
|
169
|
+
if (typeof createImageBitmap !== "function") throw new Error("publishBrowser: createImageBitmap is required for atlas PNG generation.");
|
|
170
|
+
const copy = bytes.slice();
|
|
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
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const raster = createRaster(bitmap.width, bitmap.height);
|
|
182
|
+
getBrowserContext(raster.canvas).drawImage(bitmap, 0, 0);
|
|
183
|
+
return raster;
|
|
184
|
+
} finally {
|
|
185
|
+
bitmap.close();
|
|
186
|
+
}
|
|
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
|
+
}
|
|
208
|
+
var BrowserImagePipeline = class {
|
|
209
|
+
rawOutput = false;
|
|
210
|
+
constructor(raster, decode, write) {
|
|
211
|
+
this.raster = raster;
|
|
212
|
+
this.decode = decode;
|
|
213
|
+
this.write = write;
|
|
214
|
+
}
|
|
215
|
+
ensureAlpha() {
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
resize(options) {
|
|
219
|
+
this.raster = this.raster.then((source) => {
|
|
220
|
+
const target = createRaster(options.width, options.height);
|
|
221
|
+
getBrowserContext(target.canvas).drawImage(source.canvas, 0, 0, options.width, options.height);
|
|
222
|
+
return target;
|
|
223
|
+
});
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
raw() {
|
|
227
|
+
this.rawOutput = true;
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
extract(options) {
|
|
231
|
+
this.raster = this.raster.then((source) => {
|
|
232
|
+
const target = createRaster(options.width, options.height);
|
|
233
|
+
getBrowserContext(target.canvas).drawImage(source.canvas, options.left, options.top, options.width, options.height, 0, 0, options.width, options.height);
|
|
234
|
+
return target;
|
|
235
|
+
});
|
|
236
|
+
return this;
|
|
237
|
+
}
|
|
238
|
+
png() {
|
|
239
|
+
this.rawOutput = false;
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
rotate(angle) {
|
|
243
|
+
this.raster = this.raster.then((source) => {
|
|
244
|
+
if (angle % 180 === 0) return source;
|
|
245
|
+
const target = createRaster(source.height, source.width);
|
|
246
|
+
const context = getBrowserContext(target.canvas);
|
|
247
|
+
context.save();
|
|
248
|
+
if (angle === 270 || angle === -90) {
|
|
249
|
+
context.translate(0, source.width);
|
|
250
|
+
context.rotate(-Math.PI / 2);
|
|
251
|
+
} else {
|
|
252
|
+
context.translate(source.height, 0);
|
|
253
|
+
context.rotate(Math.PI / 2);
|
|
254
|
+
}
|
|
255
|
+
context.drawImage(source.canvas, 0, 0);
|
|
256
|
+
context.restore();
|
|
257
|
+
return target;
|
|
258
|
+
});
|
|
259
|
+
return this;
|
|
260
|
+
}
|
|
261
|
+
composite(inputs) {
|
|
262
|
+
this.raster = this.raster.then(async (target) => {
|
|
263
|
+
const context = getBrowserContext(target.canvas);
|
|
264
|
+
for (const input of inputs) {
|
|
265
|
+
const source = await this.decode(input.input);
|
|
266
|
+
context.drawImage(source.canvas, input.left, input.top);
|
|
267
|
+
}
|
|
268
|
+
return target;
|
|
269
|
+
});
|
|
270
|
+
return this;
|
|
271
|
+
}
|
|
272
|
+
async metadata() {
|
|
273
|
+
const raster = await this.raster;
|
|
274
|
+
return {
|
|
275
|
+
width: raster.width,
|
|
276
|
+
height: raster.height,
|
|
277
|
+
channels: 4,
|
|
278
|
+
hasAlpha: true
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
async toBuffer(options) {
|
|
282
|
+
const raster = await this.raster;
|
|
283
|
+
if (options?.resolveWithObject) {
|
|
284
|
+
const data = getBrowserContext(raster.canvas).getImageData(0, 0, raster.width, raster.height).data;
|
|
285
|
+
return {
|
|
286
|
+
data: new Uint8Array(data),
|
|
287
|
+
info: {
|
|
288
|
+
width: raster.width,
|
|
289
|
+
height: raster.height,
|
|
290
|
+
channels: 4
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (this.rawOutput) return new Uint8Array(getBrowserContext(raster.canvas).getImageData(0, 0, raster.width, raster.height).data);
|
|
295
|
+
return canvasToPng(raster.canvas);
|
|
296
|
+
}
|
|
297
|
+
async toFile(path) {
|
|
298
|
+
const raster = await this.raster;
|
|
299
|
+
await this.write(path, await canvasToPng(raster.canvas));
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
function createBrowserImageEncoder(sourceFileSystem, outputFileSystem) {
|
|
303
|
+
const decode = (bytes) => decodeRaster(bytes, imageMimeTypeFromBytes(bytes));
|
|
304
|
+
return (input) => {
|
|
305
|
+
return new BrowserImagePipeline(typeof input === "string" ? sourceFileSystem.readFileRaw(input).then((bytes) => decodeRaster(bytes, imageMimeType(input))) : input instanceof Uint8Array ? decode(input) : Promise.resolve(createRaster(input.create.width, input.create.height, input.create.background)), decode, outputFileSystem.writeFileRaw);
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/adapters/web/publish.ts
|
|
310
|
+
function createTrackingFileSystem(fileSystem, files) {
|
|
311
|
+
return {
|
|
312
|
+
join: (...paths) => fileSystem.join(...paths),
|
|
313
|
+
mkdir: (path) => fileSystem.mkdir(path),
|
|
314
|
+
writeFileRaw: async (path, data) => {
|
|
315
|
+
await fileSystem.writeFileRaw(path, data);
|
|
316
|
+
files.set(path, data.byteLength);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function createDiagnosticLogger(logger, diagnostics) {
|
|
321
|
+
return {
|
|
322
|
+
debug(message) {
|
|
323
|
+
diagnostics.push({
|
|
324
|
+
level: "debug",
|
|
325
|
+
message
|
|
326
|
+
});
|
|
327
|
+
logger.debug(message);
|
|
328
|
+
},
|
|
329
|
+
info(message) {
|
|
330
|
+
diagnostics.push({
|
|
331
|
+
level: "info",
|
|
332
|
+
message
|
|
333
|
+
});
|
|
334
|
+
logger.info(message);
|
|
335
|
+
},
|
|
336
|
+
warn(message) {
|
|
337
|
+
diagnostics.push({
|
|
338
|
+
level: "warning",
|
|
339
|
+
message
|
|
340
|
+
});
|
|
341
|
+
logger.warn(message);
|
|
342
|
+
},
|
|
343
|
+
error(message) {
|
|
344
|
+
diagnostics.push({
|
|
345
|
+
level: "error",
|
|
346
|
+
message
|
|
347
|
+
});
|
|
348
|
+
logger.error(message);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function toResult(success, files, diagnostics) {
|
|
353
|
+
return {
|
|
354
|
+
success,
|
|
355
|
+
files: [...files].map(([path, size]) => ({
|
|
356
|
+
path,
|
|
357
|
+
size
|
|
358
|
+
})),
|
|
359
|
+
diagnostics
|
|
360
|
+
};
|
|
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
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Publish a loaded FairyGUI project to browser-provided storage.
|
|
373
|
+
*
|
|
374
|
+
* The adapter uses browser Canvas APIs for atlas composition, writes only through
|
|
375
|
+
* the supplied output filesystem, and intentionally skips Node publish plugins.
|
|
376
|
+
*/
|
|
377
|
+
async function publishBrowser(options) {
|
|
378
|
+
const files = /* @__PURE__ */ new Map();
|
|
379
|
+
const diagnostics = [];
|
|
380
|
+
const root = options.document.getRoot();
|
|
381
|
+
const previousProjectType = root.getProjectType();
|
|
382
|
+
const previousLogger = options.document.getLogger();
|
|
383
|
+
options.document.setLogger(createDiagnosticLogger(previousLogger, diagnostics));
|
|
384
|
+
try {
|
|
385
|
+
if (options.projectType !== "layabox") throw new Error(`publishBrowser: unsupported project type "${String(options.projectType)}".`);
|
|
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();
|
|
407
|
+
const outputFileSystem = createTrackingFileSystem(options.outputFileSystem, files);
|
|
408
|
+
const sourceAssetsPath = options.sourceFileSystem.join(options.document.getProjectDir(), "assets");
|
|
409
|
+
await options.document.transform(publish({
|
|
410
|
+
output: options.output,
|
|
411
|
+
compressed: resolved.compressed,
|
|
412
|
+
fileExtension: resolved.fileExtension,
|
|
413
|
+
packages: options.packages,
|
|
414
|
+
branch: options.branch,
|
|
415
|
+
basePath: sourceAssetsPath,
|
|
416
|
+
encoder: createBrowserImageEncoder(options.sourceFileSystem, outputFileSystem),
|
|
417
|
+
atlas: {
|
|
418
|
+
...options.atlas,
|
|
419
|
+
readFileRaw: (path) => options.sourceFileSystem.readFileRaw(path)
|
|
420
|
+
},
|
|
421
|
+
fs: outputFileSystem,
|
|
422
|
+
plugins: [],
|
|
423
|
+
codeGeneration: false
|
|
424
|
+
}));
|
|
425
|
+
return toResult(true, files, diagnostics);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
diagnostics.push({
|
|
428
|
+
level: "error",
|
|
429
|
+
code: "publish_failed",
|
|
430
|
+
message: error instanceof Error ? error.message : String(error)
|
|
431
|
+
});
|
|
432
|
+
return toResult(false, files, diagnostics);
|
|
433
|
+
} finally {
|
|
434
|
+
root.setProjectType(previousProjectType);
|
|
435
|
+
options.document.setLogger(previousLogger);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
//#endregion
|
|
439
|
+
export { publishBrowser };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfairygui/functions",
|
|
3
|
-
"version": "0.2.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "FairyGUI Headless Authoring SDK — composable transform functions.",
|
|
5
5
|
"author": "OpenFairyGUI Contributors",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,26 @@
|
|
|
38
38
|
"types": "./dist/uam-transaction.d.ts",
|
|
39
39
|
"default": "./dist/uam-transaction.js"
|
|
40
40
|
}
|
|
41
|
+
},
|
|
42
|
+
"./node": {
|
|
43
|
+
"require": {
|
|
44
|
+
"types": "./dist/node.d.cts",
|
|
45
|
+
"default": "./dist/node.cjs"
|
|
46
|
+
},
|
|
47
|
+
"default": {
|
|
48
|
+
"types": "./dist/node.d.ts",
|
|
49
|
+
"default": "./dist/node.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"./web": {
|
|
53
|
+
"require": {
|
|
54
|
+
"types": "./dist/web.d.cts",
|
|
55
|
+
"default": "./dist/web.cjs"
|
|
56
|
+
},
|
|
57
|
+
"default": {
|
|
58
|
+
"types": "./dist/web.d.ts",
|
|
59
|
+
"default": "./dist/web.js"
|
|
60
|
+
}
|
|
41
61
|
}
|
|
42
62
|
},
|
|
43
63
|
"files": [
|
|
@@ -53,14 +73,19 @@
|
|
|
53
73
|
"publish"
|
|
54
74
|
],
|
|
55
75
|
"dependencies": {
|
|
56
|
-
"
|
|
76
|
+
"fast-xml-parser": "^5.0.0",
|
|
77
|
+
"jiti": "^2.6.1",
|
|
78
|
+
"@openfairygui/core": "0.2.0"
|
|
79
|
+
},
|
|
80
|
+
"optionalDependencies": {
|
|
81
|
+
"sharp": ">=0.33.0"
|
|
57
82
|
},
|
|
58
83
|
"devDependencies": {
|
|
59
84
|
"ava": "^7.0.0",
|
|
60
85
|
"tsx": "^4.0.0"
|
|
61
86
|
},
|
|
62
87
|
"scripts": {
|
|
63
|
-
"build": "tsdown src/index.ts src/uam-transaction.ts --format esm,cjs --platform neutral
|
|
64
|
-
"build:watch": "tsdown src/index.ts src/uam-transaction.ts --watch --format esm,cjs --platform neutral
|
|
88
|
+
"build": "tsdown src/index.ts src/uam-transaction.ts src/node.ts src/web.ts --format esm,cjs --platform neutral",
|
|
89
|
+
"build:watch": "tsdown src/index.ts src/uam-transaction.ts src/node.ts src/web.ts --watch --format esm,cjs --platform neutral"
|
|
65
90
|
}
|
|
66
91
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Document } from '@openfairygui/core';
|
|
2
|
+
import {
|
|
3
|
+
formatPluginError,
|
|
4
|
+
type LoadedPlugin,
|
|
5
|
+
type Plugin,
|
|
6
|
+
type PluginManifest,
|
|
7
|
+
type PluginModule,
|
|
8
|
+
} from '../../plugins/types.js';
|
|
9
|
+
|
|
10
|
+
interface PluginPackageJson extends Partial<PluginManifest> {
|
|
11
|
+
name?: string;
|
|
12
|
+
main?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Keep Node builtins out of the neutral bundle resolver while still loading plugins in Node.
|
|
16
|
+
const importNative = new Function('id', 'return import(id)') as <T>(id: string) => Promise<T>;
|
|
17
|
+
|
|
18
|
+
export async function loadPlugins(doc: Document, pluginsDir: string): Promise<LoadedPlugin[]> {
|
|
19
|
+
if (!pluginsDir) return [];
|
|
20
|
+
|
|
21
|
+
const fs = await importNative<typeof import('node:fs/promises')>('node:fs/promises');
|
|
22
|
+
const path = await importNative<typeof import('node:path')>('node:path');
|
|
23
|
+
let entries: Array<{ name: string; isDirectory(): boolean }>;
|
|
24
|
+
try {
|
|
25
|
+
entries = await fs.readdir(pluginsDir, { withFileTypes: true });
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const plugins: LoadedPlugin[] = [];
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (!entry.isDirectory()) continue;
|
|
33
|
+
const pluginDir = path.join(pluginsDir, entry.name);
|
|
34
|
+
try {
|
|
35
|
+
const manifest = await readPluginManifest(fs, path, pluginDir);
|
|
36
|
+
if (!manifest) continue;
|
|
37
|
+
|
|
38
|
+
const mainPath = resolvePluginMain(path, pluginDir, manifest);
|
|
39
|
+
const plugin = await loadPlugin(mainPath);
|
|
40
|
+
plugins.push({ name: manifest.name, plugin });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
doc.getLogger().warn(`publish: Plugin "${entry.name}" was skipped: ${formatPluginError(error)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return plugins;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readPluginManifest(
|
|
50
|
+
fs: typeof import('node:fs/promises'),
|
|
51
|
+
path: typeof import('node:path'),
|
|
52
|
+
pluginDir: string,
|
|
53
|
+
): Promise<PluginPackageJson | null> {
|
|
54
|
+
const manifestPath = path.join(pluginDir, 'package.json');
|
|
55
|
+
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
56
|
+
const manifest = JSON.parse(content) as PluginPackageJson;
|
|
57
|
+
if (!manifest.name) throw new Error(`Codegen plugin at ${pluginDir} is missing package.json name.`);
|
|
58
|
+
if (!manifest.main) throw new Error(`Codegen plugin "${manifest.name}" is missing package.json main.`);
|
|
59
|
+
return manifest;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolvePluginMain(path: typeof import('node:path'), pluginDir: string, manifest: PluginPackageJson): string {
|
|
63
|
+
const mainPath = path.resolve(pluginDir, manifest.main!);
|
|
64
|
+
const relative = path.relative(pluginDir, mainPath);
|
|
65
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
66
|
+
throw new Error(`Codegen plugin "${manifest.name}" main must resolve inside its plugin directory.`);
|
|
67
|
+
}
|
|
68
|
+
return mainPath;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function loadPlugin(mainPath: string): Promise<Plugin> {
|
|
72
|
+
const { createJiti } = await importNative<typeof import('jiti')>('jiti');
|
|
73
|
+
const jiti = createJiti(import.meta.url);
|
|
74
|
+
const mod = await jiti.import<PluginModule>(mainPath);
|
|
75
|
+
const defaultExport = mod.default;
|
|
76
|
+
const plugin = isObject(defaultExport) ? defaultExport : mod;
|
|
77
|
+
return plugin as Plugin;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
81
|
+
return value !== null && typeof value === 'object';
|
|
82
|
+
}
|