@openfairygui/functions 0.3.0-alpha.4 → 0.3.1
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/{atlas-C6tbl7nn.d.ts → atlas-BtWa1DwO.d.ts} +6 -0
- package/dist/{atlas-CHsu2Y8i.d.cts → atlas-CSqHsG0X.d.cts} +6 -0
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/node.cjs +128 -30
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +128 -30
- package/dist/{publish-CyBj2o7n.js → publish-Cy09yjfn.js} +122 -54
- package/dist/{publish-C7qHwEiP.cjs → publish-DhMv_lEP.cjs} +122 -54
- package/dist/{restore-BeWaJNjR.d.cts → restore-CbscKkNw.d.ts} +9 -4
- package/dist/{restore-DVo1hXpN.js → restore-DB_L_xq4.js} +1 -1
- package/dist/{restore-Dh0-Nvms.d.ts → restore-k3oX26Fs.d.cts} +9 -4
- package/dist/{restore-LXbDQqmT.cjs → restore-n61oZ4nw.cjs} +1 -1
- package/dist/web.cjs +47 -64
- package/dist/web.d.cts +2 -2
- package/dist/web.d.ts +2 -2
- package/dist/web.js +49 -66
- package/package.json +5 -2
- package/src/adapters/node/plugins.ts +29 -13
- package/src/adapters/node/publish.ts +74 -4
- package/src/adapters/node/validate.ts +1 -0
- package/src/adapters/web/publish.ts +2 -0
- package/src/adapters/web/raster.ts +55 -72
- package/src/atlas/packing.ts +22 -13
- package/src/atlas.ts +22 -3
- package/src/codegen.ts +24 -11
- package/src/plugins/types.ts +7 -0
- package/src/publish/contracts.ts +3 -0
- package/src/publish/external-resources.ts +12 -6
- package/src/publish/options.ts +21 -7
- package/src/publish/package-context.ts +35 -11
- package/src/publish/resource-references.ts +21 -6
- package/src/publish.ts +26 -4
- package/src/utils.ts +2 -2
|
@@ -6,7 +6,8 @@ import type {
|
|
|
6
6
|
PublishFileSystem,
|
|
7
7
|
PublishSourceFileSystem,
|
|
8
8
|
} from '../../publish/contracts.js';
|
|
9
|
-
import {
|
|
9
|
+
import { validateSafeSvgSource } from '@openfairygui/core';
|
|
10
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
10
11
|
|
|
11
12
|
type BrowserCanvas = OffscreenCanvas | HTMLCanvasElement;
|
|
12
13
|
|
|
@@ -27,6 +28,7 @@ interface BrowserContext {
|
|
|
27
28
|
): void;
|
|
28
29
|
fillRect(x: number, y: number, width: number, height: number): void;
|
|
29
30
|
getImageData(sx: number, sy: number, sw: number, sh: number): ImageData;
|
|
31
|
+
putImageData(imageData: ImageData, dx: number, dy: number): void;
|
|
30
32
|
rotate(angle: number): void;
|
|
31
33
|
restore(): void;
|
|
32
34
|
save(): void;
|
|
@@ -40,30 +42,8 @@ interface BrowserRaster {
|
|
|
40
42
|
height: number;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
const MAX_SVG_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
44
45
|
const MAX_SVG_DIMENSION = 16_384;
|
|
45
46
|
const MAX_SVG_PIXELS = 64 * 1024 * 1024;
|
|
46
|
-
const MAX_SVG_NODES = 50_000;
|
|
47
|
-
const UNSAFE_SVG_ELEMENTS = new Set([
|
|
48
|
-
'a',
|
|
49
|
-
'animate',
|
|
50
|
-
'animatecolor',
|
|
51
|
-
'animatemotion',
|
|
52
|
-
'animatetransform',
|
|
53
|
-
'audio',
|
|
54
|
-
'canvas',
|
|
55
|
-
'discard',
|
|
56
|
-
'embed',
|
|
57
|
-
'feimage',
|
|
58
|
-
'foreignobject',
|
|
59
|
-
'iframe',
|
|
60
|
-
'image',
|
|
61
|
-
'object',
|
|
62
|
-
'script',
|
|
63
|
-
'set',
|
|
64
|
-
'style',
|
|
65
|
-
'video',
|
|
66
|
-
]);
|
|
67
47
|
|
|
68
48
|
type ParsedSvgEntry = Record<string, unknown> & { ':@'?: Record<string, unknown> };
|
|
69
49
|
|
|
@@ -86,57 +66,16 @@ function parseSvgLength(value: unknown, name: string): number | undefined {
|
|
|
86
66
|
return parsed;
|
|
87
67
|
}
|
|
88
68
|
|
|
89
|
-
function validateSvgAttribute(name: string, value: unknown): void {
|
|
90
|
-
const normalizedName = name.toLowerCase();
|
|
91
|
-
if (normalizedName === 'xmlns' || normalizedName.startsWith('xmlns:')) return;
|
|
92
|
-
const localName = svgLocalName(name);
|
|
93
|
-
const text = String(value);
|
|
94
|
-
if (localName.startsWith('on')) unsafeSvg(`event attribute "${name}" is not allowed`);
|
|
95
|
-
if (localName === 'style' || localName === 'src') unsafeSvg(`attribute "${name}" is not allowed`);
|
|
96
|
-
if (localName === 'href' && !/^#[A-Za-z_][\w:.-]*$/u.test(text)) {
|
|
97
|
-
unsafeSvg(`external reference in "${name}" is not allowed`);
|
|
98
|
-
}
|
|
99
|
-
if (/(?:^|[\s("'=])(?:https?:|file:|javascript:|data:|\/\/)/iu.test(text)) {
|
|
100
|
-
unsafeSvg(`external URL in "${name}" is not allowed`);
|
|
101
|
-
}
|
|
102
|
-
for (const match of text.matchAll(/url\s*\(([^)]*)\)/giu)) {
|
|
103
|
-
const reference = (match[1] ?? '').trim().replace(/^(['"])(.*)\1$/u, '$2');
|
|
104
|
-
if (!/^#[A-Za-z_][\w:.-]*$/u.test(reference)) unsafeSvg(`external url() in "${name}" is not allowed`);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function visitSvgEntry(entry: ParsedSvgEntry): void {
|
|
109
|
-
const pending = [entry];
|
|
110
|
-
let nodeCount = 0;
|
|
111
|
-
while (pending.length > 0) {
|
|
112
|
-
const current = pending.pop()!;
|
|
113
|
-
for (const [name, value] of Object.entries(current)) {
|
|
114
|
-
if (name === ':@' || name.startsWith('#') || name.startsWith('?')) continue;
|
|
115
|
-
if (++nodeCount > MAX_SVG_NODES) unsafeSvg('node count exceeds the supported limit');
|
|
116
|
-
const localName = svgLocalName(name);
|
|
117
|
-
if (UNSAFE_SVG_ELEMENTS.has(localName)) unsafeSvg(`element <${name}> is not allowed`);
|
|
118
|
-
for (const [attributeName, attributeValue] of Object.entries(current[':@'] ?? {})) {
|
|
119
|
-
validateSvgAttribute(attributeName, attributeValue);
|
|
120
|
-
}
|
|
121
|
-
if (Array.isArray(value)) {
|
|
122
|
-
for (const child of value) {
|
|
123
|
-
if (child && typeof child === 'object' && !Array.isArray(child)) pending.push(child as ParsedSvgEntry);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
69
|
function validateSvg(bytes: Uint8Array): void {
|
|
131
|
-
if (bytes.byteLength === 0 || bytes.byteLength > MAX_SVG_SOURCE_BYTES) unsafeSvg('source size is unsupported');
|
|
132
|
-
let source: string;
|
|
133
70
|
try {
|
|
134
|
-
source =
|
|
135
|
-
|
|
136
|
-
|
|
71
|
+
const source = validateSafeSvgSource(bytes);
|
|
72
|
+
validateSvgDimensions(source);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
unsafeSvg(error instanceof Error ? error.message : String(error));
|
|
137
75
|
}
|
|
138
|
-
|
|
139
|
-
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validateSvgDimensions(source: string): void {
|
|
140
79
|
const parsed = new XMLParser({
|
|
141
80
|
preserveOrder: true,
|
|
142
81
|
ignoreAttributes: false,
|
|
@@ -151,7 +90,6 @@ function validateSvg(bytes: Uint8Array): void {
|
|
|
151
90
|
.map((name) => ({ entry, name })));
|
|
152
91
|
if (roots.length !== 1 || svgLocalName(roots[0]!.name) !== 'svg') unsafeSvg('a single <svg> root is required');
|
|
153
92
|
const root = roots[0]!.entry;
|
|
154
|
-
visitSvgEntry(root);
|
|
155
93
|
const attributes = root[':@'] ?? {};
|
|
156
94
|
const width = parseSvgLength(attributes.width, 'width');
|
|
157
95
|
const height = parseSvgLength(attributes.height, 'height');
|
|
@@ -312,6 +250,51 @@ class BrowserImagePipeline implements AtlasRasterPipeline {
|
|
|
312
250
|
return this;
|
|
313
251
|
}
|
|
314
252
|
|
|
253
|
+
removeAlpha(): this {
|
|
254
|
+
this.raster = this.raster.then((source) => {
|
|
255
|
+
const context = getBrowserContext(source.canvas);
|
|
256
|
+
const image = context.getImageData(0, 0, source.width, source.height);
|
|
257
|
+
for (let index = 3; index < image.data.length; index += 4) image.data[index] = 255;
|
|
258
|
+
context.putImageData(image, 0, 0);
|
|
259
|
+
return source;
|
|
260
|
+
});
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
extractChannel(channel: 'alpha'): this {
|
|
265
|
+
if (channel !== 'alpha') throw new Error(`publishBrowser: Unsupported channel "${channel}".`);
|
|
266
|
+
this.raster = this.raster.then((source) => {
|
|
267
|
+
const context = getBrowserContext(source.canvas);
|
|
268
|
+
const image = context.getImageData(0, 0, source.width, source.height);
|
|
269
|
+
for (let index = 0; index < image.data.length; index += 4) {
|
|
270
|
+
const alpha = image.data[index + 3] ?? 0;
|
|
271
|
+
image.data[index] = alpha;
|
|
272
|
+
image.data[index + 1] = alpha;
|
|
273
|
+
image.data[index + 2] = alpha;
|
|
274
|
+
image.data[index + 3] = 255;
|
|
275
|
+
}
|
|
276
|
+
context.putImageData(image, 0, 0);
|
|
277
|
+
return source;
|
|
278
|
+
});
|
|
279
|
+
return this;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
joinChannel(images: Uint8Array[]): this {
|
|
283
|
+
this.raster = Promise.all([this.raster, ...images.map((image) => this.decode(image))]).then(([source, ...channels]) => {
|
|
284
|
+
const context = getBrowserContext(source.canvas);
|
|
285
|
+
const image = context.getImageData(0, 0, source.width, source.height);
|
|
286
|
+
for (const [channelIndex, channel] of channels.slice(0, 2).entries()) {
|
|
287
|
+
const channelData = getBrowserContext(channel.canvas).getImageData(0, 0, channel.width, channel.height).data;
|
|
288
|
+
for (let index = 0; index < image.data.length; index += 4) {
|
|
289
|
+
image.data[index + channelIndex + 1] = channelData[index] ?? 0;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
context.putImageData(image, 0, 0);
|
|
293
|
+
return source;
|
|
294
|
+
});
|
|
295
|
+
return this;
|
|
296
|
+
}
|
|
297
|
+
|
|
315
298
|
resize(options: { width: number; height: number; fit?: 'fill' }): this {
|
|
316
299
|
this.raster = this.raster.then((source) => {
|
|
317
300
|
const target = createRaster(options.width, options.height);
|
package/src/atlas/packing.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Document, ILogger, Package } from '@openfairygui/core';
|
|
2
2
|
import type { AtlasOptions } from '../atlas.js';
|
|
3
|
-
import { COMPAT_NODE_RECT_FLAGS, type CompatNodeRect } from '../max-rects-compat.js';
|
|
3
|
+
import { COMPAT_NODE_RECT_FLAGS, type CompatNodeRect, type CompatPage } from '../max-rects-compat.js';
|
|
4
4
|
import { MaxRectsPackerCompat } from '../max-rects-packer-compat.js';
|
|
5
5
|
import type { AtlasRasterBackend } from '../publish/contracts.js';
|
|
6
6
|
import {
|
|
@@ -289,13 +289,13 @@ function packAtlasPages(
|
|
|
289
289
|
multipleOfFour: boolean;
|
|
290
290
|
square: boolean;
|
|
291
291
|
},
|
|
292
|
-
):
|
|
292
|
+
): CompatPage[] {
|
|
293
293
|
const hasDuplicatePadding = inputs.some((input) => {
|
|
294
294
|
return isImageResource(input.resource) && input.resource.getDuplicatePadding?.() === true;
|
|
295
295
|
});
|
|
296
296
|
const packer = new MaxRectsPackerCompat({
|
|
297
297
|
pot: sizeOverrides?.powerOfTwo ?? options.powerOfTwo,
|
|
298
|
-
mof: sizeOverrides?.multipleOfFour ??
|
|
298
|
+
mof: sizeOverrides?.multipleOfFour ?? options.multipleOfFour,
|
|
299
299
|
padding: options.padding,
|
|
300
300
|
rotation: options.allowRotation,
|
|
301
301
|
minWidth: 16,
|
|
@@ -433,16 +433,25 @@ async function writeAtlasPageImage(
|
|
|
433
433
|
}
|
|
434
434
|
|
|
435
435
|
const outputFile = `${options.outputPath}/${atlasFileName}`;
|
|
436
|
-
|
|
436
|
+
const atlasPipeline = encoder({
|
|
437
437
|
create: {
|
|
438
438
|
width: page.width,
|
|
439
439
|
height: page.height,
|
|
440
440
|
channels: 4 as const,
|
|
441
441
|
background: { r: 0, g: 0, b: 0, alpha: 0 },
|
|
442
442
|
},
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
.
|
|
443
|
+
}).composite(compositeInputs);
|
|
444
|
+
if (options.extractAlpha) {
|
|
445
|
+
const atlasBuffer = await atlasPipeline.png().toBuffer();
|
|
446
|
+
await encoder(atlasBuffer).removeAlpha().png().toFile(outputFile);
|
|
447
|
+
const alphaBuffer = await encoder(atlasBuffer).extractChannel('alpha').png().toBuffer();
|
|
448
|
+
await encoder(alphaBuffer)
|
|
449
|
+
.joinChannel([alphaBuffer, alphaBuffer])
|
|
450
|
+
.png()
|
|
451
|
+
.toFile(`${options.outputPath}/${insertFileNameSuffix(atlasFileName, '!a')}`);
|
|
452
|
+
} else {
|
|
453
|
+
await atlasPipeline.toFile(outputFile);
|
|
454
|
+
}
|
|
446
455
|
|
|
447
456
|
logger.info(`atlas: Generated ${atlasFileName} (${page.width}x${page.height}, ${page.outputRects.length} sprites)`);
|
|
448
457
|
}
|
|
@@ -504,6 +513,9 @@ function resolveDirectOutputAtlasSize(
|
|
|
504
513
|
if (options.powerOfTwo) {
|
|
505
514
|
resolvedWidth = nextPow2(resolvedWidth);
|
|
506
515
|
resolvedHeight = nextPow2(resolvedHeight);
|
|
516
|
+
} else if (options.multipleOfFour) {
|
|
517
|
+
resolvedWidth = roundUpToMultiple(resolvedWidth, 4);
|
|
518
|
+
resolvedHeight = roundUpToMultiple(resolvedHeight, 4);
|
|
507
519
|
}
|
|
508
520
|
return { width: resolvedWidth, height: resolvedHeight };
|
|
509
521
|
}
|
|
@@ -663,11 +675,8 @@ export function sortResourcesByOrder(
|
|
|
663
675
|
return ordered;
|
|
664
676
|
}
|
|
665
677
|
|
|
666
|
-
function getResourceTextureSetMode(resource: PackInputResource): TextureSetMode {
|
|
667
|
-
|
|
668
|
-
return parseTextureSetMode(resource.getTextureSetMode?.());
|
|
669
|
-
}
|
|
670
|
-
return parseTextureSetMode(resource.getTextureSetMode?.());
|
|
678
|
+
function getResourceTextureSetMode(resource: PackInputResource, maxAtlasIndex: number): TextureSetMode {
|
|
679
|
+
return parseTextureSetMode(resource.getTextureSetMode?.(), maxAtlasIndex);
|
|
671
680
|
}
|
|
672
681
|
|
|
673
682
|
function groupStandaloneInputs(
|
|
@@ -710,7 +719,7 @@ function groupStandaloneInputs(
|
|
|
710
719
|
for (const input of inputs) {
|
|
711
720
|
const branchName = getInputBranchName(input);
|
|
712
721
|
const branchOrdinal = branchOrdinalByName.get(branchName) ?? 0;
|
|
713
|
-
const mode = getResourceTextureSetMode(input.resource);
|
|
722
|
+
const mode = getResourceTextureSetMode(input.resource, options.maxAtlasIndex ?? 10);
|
|
714
723
|
if (mode.kind === 'standalone') {
|
|
715
724
|
const resourceId = getPublishedItemId(input.resource);
|
|
716
725
|
const key = `${branchName}\u0000${resourceId}`;
|
package/src/atlas.ts
CHANGED
|
@@ -62,6 +62,9 @@ export interface AtlasOptions {
|
|
|
62
62
|
|
|
63
63
|
/** Constrain atlas dimensions to powers of two. Default: false. */
|
|
64
64
|
powerOfTwo?: boolean;
|
|
65
|
+
/** Highest fixed atlas page index accepted from resource textureSetMode. Default: 10. */
|
|
66
|
+
maxAtlasIndex?: number;
|
|
67
|
+
multipleOfFour?: boolean;
|
|
65
68
|
|
|
66
69
|
/** Force square atlas (width === height). Default: false. */
|
|
67
70
|
square?: boolean;
|
|
@@ -151,6 +154,8 @@ const ATLAS_DEFAULTS: Required<
|
|
|
151
154
|
allowRotation: true,
|
|
152
155
|
padding: 1,
|
|
153
156
|
powerOfTwo: false,
|
|
157
|
+
maxAtlasIndex: 10,
|
|
158
|
+
multipleOfFour: false,
|
|
154
159
|
square: false,
|
|
155
160
|
multiPage: true,
|
|
156
161
|
trimImage: false,
|
|
@@ -163,7 +168,9 @@ const ATLAS_DEFAULTS: Required<
|
|
|
163
168
|
|
|
164
169
|
interface AtlasReferenceItem {
|
|
165
170
|
icon?: string | null;
|
|
171
|
+
selectedIcon?: string | null;
|
|
166
172
|
url?: string | null;
|
|
173
|
+
propertyOverrides?: Array<{ value: string }>;
|
|
167
174
|
}
|
|
168
175
|
|
|
169
176
|
interface GearWithAtlasRefs {
|
|
@@ -183,12 +190,14 @@ interface TransitionWithAtlasRefs {
|
|
|
183
190
|
}
|
|
184
191
|
|
|
185
192
|
interface ChildWithReferenceUrls extends HasOptionalSrc, HasOptionalUrl {
|
|
193
|
+
getClearOnPublish?(): boolean;
|
|
186
194
|
getDefaultItem?(): string;
|
|
187
195
|
getIcon?(): string;
|
|
188
196
|
getSelectedIcon?(): string;
|
|
189
197
|
getDropdown?(): string;
|
|
190
198
|
getSound?(): string;
|
|
191
199
|
getText?(): string;
|
|
200
|
+
getAutoClearText?(): boolean;
|
|
192
201
|
getFont?(): string;
|
|
193
202
|
getInstanceIcon?(): string;
|
|
194
203
|
getInstanceSelectedIcon?(): string;
|
|
@@ -197,7 +206,10 @@ interface ChildWithReferenceUrls extends HasOptionalSrc, HasOptionalUrl {
|
|
|
197
206
|
getHeaderRes?(): string;
|
|
198
207
|
getFooterRes?(): string;
|
|
199
208
|
getInstanceComboItems?(): Array<{ icon: string | null }>;
|
|
209
|
+
getInstanceAutoClearItems?(): boolean;
|
|
200
210
|
getListItems?(): AtlasReferenceItem[];
|
|
211
|
+
getAutoClearItems?(): boolean;
|
|
212
|
+
getPropertyOverrides?(): Array<{ value: string }>;
|
|
201
213
|
listGears?(): GearWithAtlasRefs[];
|
|
202
214
|
}
|
|
203
215
|
|
|
@@ -303,7 +315,7 @@ async function resolveEditorCompatibleResourceOrder(
|
|
|
303
315
|
const refChild = child as ChildWithReferenceUrls;
|
|
304
316
|
await addResource(resourceMap.get(refChild.getSrc?.() ?? ''));
|
|
305
317
|
for (const ref of [
|
|
306
|
-
refChild.getUrl?.(),
|
|
318
|
+
refChild.getClearOnPublish?.() ? undefined : refChild.getUrl?.(),
|
|
307
319
|
refChild.getDefaultItem?.(),
|
|
308
320
|
refChild.getIcon?.(),
|
|
309
321
|
refChild.getSelectedIcon?.(),
|
|
@@ -319,12 +331,19 @@ async function resolveEditorCompatibleResourceOrder(
|
|
|
319
331
|
]) {
|
|
320
332
|
await addResourceByLocalUiUrl(ref);
|
|
321
333
|
}
|
|
322
|
-
for (const item of refChild.getInstanceComboItems?.() ?? []) {
|
|
334
|
+
for (const item of refChild.getInstanceAutoClearItems?.() ? [] : (refChild.getInstanceComboItems?.() ?? [])) {
|
|
323
335
|
await addResourceByLocalUiUrl(item.icon ?? undefined);
|
|
324
336
|
}
|
|
325
|
-
for (const item of refChild.getListItems?.() ?? []) {
|
|
337
|
+
for (const item of refChild.getAutoClearItems?.() ? [] : (refChild.getListItems?.() ?? [])) {
|
|
326
338
|
await addResourceByLocalUiUrl(item.icon ?? undefined);
|
|
339
|
+
await addResourceByLocalUiUrl(item.selectedIcon ?? undefined);
|
|
327
340
|
await addResourceByLocalUiUrl(item.url ?? undefined);
|
|
341
|
+
for (const property of item.propertyOverrides ?? []) {
|
|
342
|
+
await addResourceByLocalUiUrl(property.value);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
for (const property of refChild.getPropertyOverrides?.() ?? []) {
|
|
346
|
+
await addResourceByLocalUiUrl(property.value);
|
|
328
347
|
}
|
|
329
348
|
for (const gear of refChild.listGears?.() ?? []) {
|
|
330
349
|
await addGearIconResources(gear);
|
package/src/codegen.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
UNITY_BINDER_TEMPLATE,
|
|
13
13
|
UNITY_COMPONENT_TEMPLATE,
|
|
14
14
|
} from './codegen-templates.js';
|
|
15
|
-
import { formatPluginError, type LoadedPlugin } from './plugins/types.js';
|
|
15
|
+
import { formatPluginError, type LoadedPlugin, shouldAbortPluginFailure } from './plugins/types.js';
|
|
16
16
|
import { dirname, isAbsolutePathLike, trimTrailingSlashes } from './path-utils.js';
|
|
17
17
|
import type { PublishFileSystem } from './publish/contracts.js';
|
|
18
18
|
import type { CliCodeGenerationSettings, RootProjectSettings } from './shared-types.js';
|
|
@@ -33,12 +33,13 @@ export interface ResolvedPackageCodegenPlan {
|
|
|
33
33
|
packageFolderName: string;
|
|
34
34
|
packageNamespace: string;
|
|
35
35
|
binderClassName: string;
|
|
36
|
-
settings: CliCodeGenerationSettings
|
|
36
|
+
settings: Required<CliCodeGenerationSettings>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
interface FguiTypescriptVariant {
|
|
40
40
|
binderMethod: 'setExtension';
|
|
41
41
|
runtimeNamespace: 'fgui';
|
|
42
|
+
runtimeImport: string;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
const FGUI_TYPESCRIPT_RUNTIME_TYPES = new Set([
|
|
@@ -65,9 +66,15 @@ const FGUI_TYPESCRIPT_RUNTIME_TYPES = new Set([
|
|
|
65
66
|
'Transition',
|
|
66
67
|
]);
|
|
67
68
|
|
|
68
|
-
const
|
|
69
|
+
const LAYABOX_TYPESCRIPT_VARIANT: FguiTypescriptVariant = {
|
|
69
70
|
binderMethod: 'setExtension',
|
|
70
71
|
runtimeNamespace: 'fgui',
|
|
72
|
+
runtimeImport: '',
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const COCOS_CREATOR_TYPESCRIPT_VARIANT: FguiTypescriptVariant = {
|
|
76
|
+
...LAYABOX_TYPESCRIPT_VARIANT,
|
|
77
|
+
runtimeImport: 'import * as fgui from "fairygui-cc";',
|
|
71
78
|
};
|
|
72
79
|
|
|
73
80
|
export interface CodegenMember {
|
|
@@ -101,16 +108,20 @@ export async function publishCodeGeneration(doc: Document, options: PublishCodeG
|
|
|
101
108
|
const settings = resolveCodeGenerationSettings(doc);
|
|
102
109
|
if (!settings.allowGenCode) return;
|
|
103
110
|
|
|
104
|
-
const plugins = options.plugins
|
|
111
|
+
const plugins = options.plugins ?? [];
|
|
105
112
|
if (plugins.length > 0) {
|
|
106
113
|
let handled = false;
|
|
107
114
|
for (const plugin of plugins) {
|
|
115
|
+
const genCode = plugin.plugin.genCode;
|
|
116
|
+
if (!genCode) continue;
|
|
108
117
|
try {
|
|
109
|
-
await
|
|
118
|
+
await genCode(doc, settings, options);
|
|
110
119
|
handled = true;
|
|
111
120
|
logger.info(`publish: Generated code using plugin "${plugin.name}"`);
|
|
112
121
|
} catch (error) {
|
|
113
|
-
|
|
122
|
+
const message = `publish: Code generation plugin "${plugin.name}" failed: ${formatPluginError(error)}`;
|
|
123
|
+
if (shouldAbortPluginFailure(plugin)) throw new Error(message);
|
|
124
|
+
logger.warn(message);
|
|
114
125
|
}
|
|
115
126
|
}
|
|
116
127
|
if (handled) {
|
|
@@ -172,7 +183,7 @@ export function resolveCodeGenerationSettings(doc: Document): Required<CliCodeGe
|
|
|
172
183
|
};
|
|
173
184
|
}
|
|
174
185
|
|
|
175
|
-
export function resolvePackageCodegenPlan(pkg: Package, settings: CliCodeGenerationSettings
|
|
186
|
+
export function resolvePackageCodegenPlan(pkg: Package, settings: Required<CliCodeGenerationSettings>, options: PublishCodeGenerationOptions): ResolvedPackageCodegenPlan | null {
|
|
176
187
|
const rawCodePath = (pkg.getCodePath() || settings.codePath || '').trim();
|
|
177
188
|
if (!rawCodePath) return null;
|
|
178
189
|
|
|
@@ -198,11 +209,11 @@ function supportsCodeGenerationLane(doc: Document, codeType: string): boolean {
|
|
|
198
209
|
return false;
|
|
199
210
|
}
|
|
200
211
|
|
|
201
|
-
// Layabox and Cocos Creator currently share the same modern fgui TypeScript output contract.
|
|
202
212
|
function resolveFguiTypescriptVariant(doc: Document): FguiTypescriptVariant | null {
|
|
203
213
|
const projectType = doc.getRoot().getProjectType();
|
|
204
|
-
if (projectType
|
|
205
|
-
return
|
|
214
|
+
if (projectType === ProjectType.LayaBox) return LAYABOX_TYPESCRIPT_VARIANT;
|
|
215
|
+
if (projectType === ProjectType.CocosCreator) return COCOS_CREATOR_TYPESCRIPT_VARIANT;
|
|
216
|
+
return null;
|
|
206
217
|
}
|
|
207
218
|
|
|
208
219
|
async function generateUnityCode(
|
|
@@ -528,9 +539,10 @@ function renderFguiTypescriptBinder(
|
|
|
528
539
|
const bindLines = classes
|
|
529
540
|
.map((classInfo) => `\t\t${variant.runtimeNamespace}.UIObjectFactory.${variant.binderMethod}(${classInfo.encodedClassName}.URL, ${classInfo.encodedClassName});`)
|
|
530
541
|
.join('\n');
|
|
531
|
-
const
|
|
542
|
+
const classImports = classes
|
|
532
543
|
.map((classInfo) => `import ${classInfo.encodedClassName} from "./${classInfo.encodedClassName}";`)
|
|
533
544
|
.join('\n');
|
|
545
|
+
const importLines = [variant.runtimeImport, classImports].filter(Boolean).join('\n');
|
|
534
546
|
|
|
535
547
|
return renderTemplate(FGUI_TYPESCRIPT_BINDER_TEMPLATE, {
|
|
536
548
|
binderClassName: plan.binderClassName,
|
|
@@ -636,6 +648,7 @@ function normalizeTypeName(value: string): string {
|
|
|
636
648
|
|
|
637
649
|
function collectFguiTypescriptImports(classInfo: CodegenClass, variant: FguiTypescriptVariant): string {
|
|
638
650
|
const imports = new Set<string>();
|
|
651
|
+
if (variant.runtimeImport) imports.add(variant.runtimeImport);
|
|
639
652
|
for (const member of classInfo.members) {
|
|
640
653
|
if (member.ignored) continue;
|
|
641
654
|
const translated = translateFguiTypescriptType(member.type, variant);
|
package/src/plugins/types.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface PluginManifest {
|
|
|
15
15
|
};
|
|
16
16
|
icon?: string;
|
|
17
17
|
main: string;
|
|
18
|
+
required?: boolean;
|
|
19
|
+
failureMode?: 'abort' | 'warn';
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export interface ICodeWriterConfig {
|
|
@@ -47,6 +49,7 @@ export interface Plugin {
|
|
|
47
49
|
export interface LoadedPlugin {
|
|
48
50
|
name: string;
|
|
49
51
|
plugin: Plugin;
|
|
52
|
+
failureMode?: 'abort' | 'warn';
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
export type PluginModule = Plugin & { default?: Plugin };
|
|
@@ -54,3 +57,7 @@ export type PluginModule = Plugin & { default?: Plugin };
|
|
|
54
57
|
export function formatPluginError(error: unknown): string {
|
|
55
58
|
return error instanceof Error ? error.message : String(error);
|
|
56
59
|
}
|
|
60
|
+
|
|
61
|
+
export function shouldAbortPluginFailure(plugin: LoadedPlugin): boolean {
|
|
62
|
+
return plugin.failureMode !== 'warn';
|
|
63
|
+
}
|
package/src/publish/contracts.ts
CHANGED
|
@@ -65,6 +65,9 @@ export type AtlasRasterInput =
|
|
|
65
65
|
*/
|
|
66
66
|
export interface AtlasRasterPipeline {
|
|
67
67
|
ensureAlpha(): AtlasRasterPipeline;
|
|
68
|
+
removeAlpha(): AtlasRasterPipeline;
|
|
69
|
+
extractChannel(channel: 'alpha'): AtlasRasterPipeline;
|
|
70
|
+
joinChannel(images: Uint8Array[]): AtlasRasterPipeline;
|
|
68
71
|
resize(options: { width: number; height: number; fit?: 'fill' }): AtlasRasterPipeline;
|
|
69
72
|
raw(): AtlasRasterPipeline;
|
|
70
73
|
extract(options: { left: number; top: number; width: number; height: number }): AtlasRasterPipeline;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isMiscResource,
|
|
11
11
|
isSkeletonResource,
|
|
12
12
|
isSoundResource,
|
|
13
|
+
isSwfResource,
|
|
13
14
|
resolveGenericResourcePath,
|
|
14
15
|
resolveImageFileName,
|
|
15
16
|
resolveImagePath,
|
|
@@ -71,7 +72,7 @@ export async function exportPackageExternalResources(
|
|
|
71
72
|
if (!basePath || !readFileRaw) {
|
|
72
73
|
const hasPublishedExternal = pkg.listResources().some((resource) => {
|
|
73
74
|
return (
|
|
74
|
-
((isMiscResource(resource) || isSkeletonResource(resource)) &&
|
|
75
|
+
((isMiscResource(resource) || isSwfResource(resource) || isSkeletonResource(resource)) &&
|
|
75
76
|
exportedResourceIds.has(resource.getId())) ||
|
|
76
77
|
skeletonDependencyImageIds.has(resource.getId())
|
|
77
78
|
);
|
|
@@ -86,20 +87,25 @@ export async function exportPackageExternalResources(
|
|
|
86
87
|
|
|
87
88
|
for (const resource of pkg.listResources()) {
|
|
88
89
|
const resourceId = resource.getId();
|
|
89
|
-
const
|
|
90
|
-
exportedResourceIds.has(resourceId) &&
|
|
90
|
+
const isExternal =
|
|
91
|
+
exportedResourceIds.has(resourceId) &&
|
|
92
|
+
(isMiscResource(resource) || isSwfResource(resource) || isSkeletonResource(resource));
|
|
91
93
|
const isSkeletonImageDependency = skeletonDependencyImageIds.has(resourceId) && isImageResource(resource);
|
|
92
|
-
if (!
|
|
94
|
+
if (!isExternal && !isSkeletonImageDependency) continue;
|
|
93
95
|
|
|
94
96
|
let sourcePath: string;
|
|
95
97
|
let targetName: string;
|
|
96
98
|
if (isSkeletonImageDependency) {
|
|
97
99
|
sourcePath = resolveImagePath(resource, pkg, basePath);
|
|
98
100
|
targetName = resolveImageFileName(resource);
|
|
99
|
-
} else if (isMiscResource(resource) || isSkeletonResource(resource)) {
|
|
101
|
+
} else if (isMiscResource(resource) || isSwfResource(resource) || isSkeletonResource(resource)) {
|
|
100
102
|
sourcePath = resolveGenericResourcePath(resource, pkg, basePath);
|
|
101
|
-
|
|
103
|
+
const publishedFile =
|
|
102
104
|
((resource.getExtras() as PublishFileExtras | undefined) ?? {})._publishedFile ?? resource.getFile();
|
|
105
|
+
targetName =
|
|
106
|
+
isMiscResource(resource) || isSwfResource(resource)
|
|
107
|
+
? `${pkg.getPublishName() || pkg.getName()}_${publishedFile}`
|
|
108
|
+
: publishedFile;
|
|
103
109
|
} else {
|
|
104
110
|
continue;
|
|
105
111
|
}
|
package/src/publish/options.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type Document, ProjectType } from '@openfairygui/core';
|
|
2
2
|
import type { AtlasOptions } from '../atlas.js';
|
|
3
3
|
import type { LoadedPlugin } from '../plugins/types.js';
|
|
4
|
-
import type { AtlasRasterBackend, PublishFileSystem } from './contracts.js';
|
|
5
4
|
import type { CliPublishSettings, RootProjectSettings } from '../shared-types.js';
|
|
5
|
+
import type { AtlasRasterBackend, PublishFileSystem } from './contracts.js';
|
|
6
6
|
|
|
7
7
|
export interface PublishOptions {
|
|
8
8
|
/**
|
|
@@ -80,6 +80,8 @@ export interface ResolvedPublishAtlasOptions
|
|
|
80
80
|
| 'allowRotation'
|
|
81
81
|
| 'padding'
|
|
82
82
|
| 'powerOfTwo'
|
|
83
|
+
| 'maxAtlasIndex'
|
|
84
|
+
| 'multipleOfFour'
|
|
83
85
|
| 'square'
|
|
84
86
|
| 'multiPage'
|
|
85
87
|
| 'trimImage'
|
|
@@ -87,6 +89,8 @@ export interface ResolvedPublishAtlasOptions
|
|
|
87
89
|
> {}
|
|
88
90
|
|
|
89
91
|
export interface ResolvePublishOptionsOverrides {
|
|
92
|
+
/** Select a target profile between direct overrides and persisted project settings. */
|
|
93
|
+
targetProjectType?: number;
|
|
90
94
|
compressed?: boolean;
|
|
91
95
|
fileExtension?: string;
|
|
92
96
|
packages?: string[];
|
|
@@ -150,21 +154,31 @@ export function resolvePublishOptions(
|
|
|
150
154
|
const settings = (root.getSettings?.() ?? {}) as RootProjectSettings;
|
|
151
155
|
const publishSettings: CliPublishSettings = settings.publish ?? {};
|
|
152
156
|
const atlasSetting = publishSettings.atlasSetting ?? {};
|
|
153
|
-
const projectType = root.getProjectType();
|
|
157
|
+
const projectType = overrides.targetProjectType ?? root.getProjectType();
|
|
158
|
+
const explicitLayaboxTarget = overrides.targetProjectType === ProjectType.LayaBox;
|
|
154
159
|
|
|
155
|
-
const fileExtension =
|
|
160
|
+
const fileExtension =
|
|
161
|
+
overrides.fileExtension ??
|
|
162
|
+
(explicitLayaboxTarget ? 'fui' : resolveDefaultPublishFileExtension(projectType, publishSettings));
|
|
156
163
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
const runtimeRejectsCompression =
|
|
165
|
+
projectType === UNITY_PROJECT_TYPE || projectType === COCOS_CREATOR_PROJECT_TYPE;
|
|
166
|
+
if (runtimeRejectsCompression && overrides.compressed === true) {
|
|
167
|
+
throw new Error('publish: The selected target runtime does not support compressed package data.');
|
|
160
168
|
}
|
|
169
|
+
const compressed = runtimeRejectsCompression
|
|
170
|
+
? false
|
|
171
|
+
: (overrides.compressed ?? publishSettings.compressDesc ?? false);
|
|
161
172
|
|
|
162
173
|
const atlasOptions: ResolvedPublishAtlasOptions = {
|
|
163
174
|
maxSize: overrides.atlas?.maxSize ?? atlasSetting.maxSize ?? 2048,
|
|
164
175
|
fast: overrides.atlas?.fast ?? atlasSetting.fast ?? true,
|
|
165
|
-
allowRotation:
|
|
176
|
+
allowRotation:
|
|
177
|
+
overrides.atlas?.allowRotation ?? (explicitLayaboxTarget ? false : (atlasSetting.allowRotation ?? false)),
|
|
166
178
|
padding: overrides.atlas?.padding ?? atlasSetting.padding ?? 2,
|
|
167
179
|
powerOfTwo: overrides.atlas?.powerOfTwo ?? atlasSetting.sizeOption === 'pot',
|
|
180
|
+
maxAtlasIndex: overrides.atlas?.maxAtlasIndex ?? 10,
|
|
181
|
+
multipleOfFour: overrides.atlas?.multipleOfFour ?? atlasSetting.sizeOption === 'mof',
|
|
168
182
|
square: overrides.atlas?.square ?? atlasSetting.forceSquare ?? false,
|
|
169
183
|
multiPage: overrides.atlas?.multiPage ?? atlasSetting.paging ?? true,
|
|
170
184
|
trimImage: overrides.atlas?.trimImage ?? atlasSetting.trimImage ?? false,
|