@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
package/dist/web.cjs
CHANGED
|
@@ -1,32 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_publish = require("./publish-
|
|
2
|
+
const require_publish = require("./publish-DhMv_lEP.cjs");
|
|
3
3
|
let _openfairygui_core = require("@openfairygui/core");
|
|
4
4
|
let fast_xml_parser = require("fast-xml-parser");
|
|
5
5
|
//#region src/adapters/web/raster.ts
|
|
6
|
-
const MAX_SVG_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
7
6
|
const MAX_SVG_DIMENSION = 16384;
|
|
8
7
|
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
8
|
function unsafeSvg(message) {
|
|
31
9
|
throw new Error(`publishBrowser: unsafe SVG input (${message}).`);
|
|
32
10
|
}
|
|
@@ -41,47 +19,14 @@ function parseSvgLength(value, name) {
|
|
|
41
19
|
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > MAX_SVG_DIMENSION) unsafeSvg(`${name} exceeds the supported dimensions`);
|
|
42
20
|
return parsed;
|
|
43
21
|
}
|
|
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
22
|
function validateSvg(bytes) {
|
|
76
|
-
if (bytes.byteLength === 0 || bytes.byteLength > MAX_SVG_SOURCE_BYTES) unsafeSvg("source size is unsupported");
|
|
77
|
-
let source;
|
|
78
23
|
try {
|
|
79
|
-
|
|
80
|
-
} catch {
|
|
81
|
-
unsafeSvg(
|
|
24
|
+
validateSvgDimensions((0, _openfairygui_core.validateSafeSvgSource)(bytes));
|
|
25
|
+
} catch (error) {
|
|
26
|
+
unsafeSvg(error instanceof Error ? error.message : String(error));
|
|
82
27
|
}
|
|
83
|
-
|
|
84
|
-
|
|
28
|
+
}
|
|
29
|
+
function validateSvgDimensions(source) {
|
|
85
30
|
const roots = new fast_xml_parser.XMLParser({
|
|
86
31
|
preserveOrder: true,
|
|
87
32
|
ignoreAttributes: false,
|
|
@@ -95,9 +40,7 @@ function validateSvg(bytes) {
|
|
|
95
40
|
name
|
|
96
41
|
})));
|
|
97
42
|
if (roots.length !== 1 || svgLocalName(roots[0].name) !== "svg") unsafeSvg("a single <svg> root is required");
|
|
98
|
-
const
|
|
99
|
-
visitSvgEntry(root);
|
|
100
|
-
const attributes = root[":@"] ?? {};
|
|
43
|
+
const attributes = roots[0].entry[":@"] ?? {};
|
|
101
44
|
const width = parseSvgLength(attributes.width, "width");
|
|
102
45
|
const height = parseSvgLength(attributes.height, "height");
|
|
103
46
|
let viewBoxWidth;
|
|
@@ -219,6 +162,46 @@ var BrowserImagePipeline = class {
|
|
|
219
162
|
ensureAlpha() {
|
|
220
163
|
return this;
|
|
221
164
|
}
|
|
165
|
+
removeAlpha() {
|
|
166
|
+
this.raster = this.raster.then((source) => {
|
|
167
|
+
const context = getBrowserContext(source.canvas);
|
|
168
|
+
const image = context.getImageData(0, 0, source.width, source.height);
|
|
169
|
+
for (let index = 3; index < image.data.length; index += 4) image.data[index] = 255;
|
|
170
|
+
context.putImageData(image, 0, 0);
|
|
171
|
+
return source;
|
|
172
|
+
});
|
|
173
|
+
return this;
|
|
174
|
+
}
|
|
175
|
+
extractChannel(channel) {
|
|
176
|
+
if (channel !== "alpha") throw new Error(`publishBrowser: Unsupported channel "${channel}".`);
|
|
177
|
+
this.raster = this.raster.then((source) => {
|
|
178
|
+
const context = getBrowserContext(source.canvas);
|
|
179
|
+
const image = context.getImageData(0, 0, source.width, source.height);
|
|
180
|
+
for (let index = 0; index < image.data.length; index += 4) {
|
|
181
|
+
const alpha = image.data[index + 3] ?? 0;
|
|
182
|
+
image.data[index] = alpha;
|
|
183
|
+
image.data[index + 1] = alpha;
|
|
184
|
+
image.data[index + 2] = alpha;
|
|
185
|
+
image.data[index + 3] = 255;
|
|
186
|
+
}
|
|
187
|
+
context.putImageData(image, 0, 0);
|
|
188
|
+
return source;
|
|
189
|
+
});
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
joinChannel(images) {
|
|
193
|
+
this.raster = Promise.all([this.raster, ...images.map((image) => this.decode(image))]).then(([source, ...channels]) => {
|
|
194
|
+
const context = getBrowserContext(source.canvas);
|
|
195
|
+
const image = context.getImageData(0, 0, source.width, source.height);
|
|
196
|
+
for (const [channelIndex, channel] of channels.slice(0, 2).entries()) {
|
|
197
|
+
const channelData = getBrowserContext(channel.canvas).getImageData(0, 0, channel.width, channel.height).data;
|
|
198
|
+
for (let index = 0; index < image.data.length; index += 4) image.data[index + channelIndex + 1] = channelData[index] ?? 0;
|
|
199
|
+
}
|
|
200
|
+
context.putImageData(image, 0, 0);
|
|
201
|
+
return source;
|
|
202
|
+
});
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
222
205
|
resize(options) {
|
|
223
206
|
this.raster = this.raster.then((source) => {
|
|
224
207
|
const target = createRaster(options.width, options.height);
|
package/dist/web.d.cts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { d as PublishSourceFileSystem, t as AtlasOptions, u as PublishOutputFileSystem } from "./atlas-
|
|
1
|
+
import { d as PublishSourceFileSystem, t as AtlasOptions, u as PublishOutputFileSystem } from "./atlas-CSqHsG0X.cjs";
|
|
2
2
|
import { Document, ProjectValidationReport, UamProject } from "@openfairygui/core";
|
|
3
3
|
|
|
4
4
|
//#region src/adapters/web/publish.d.ts
|
|
5
5
|
type BrowserPublishProjectType = 'layabox';
|
|
6
|
-
type BrowserPublishAtlasOptions = Pick<AtlasOptions, 'maxSize' | 'fast' | 'allowRotation' | 'padding' | 'powerOfTwo' | 'square' | 'multiPage' | 'trimImage' | 'extractAlpha'>;
|
|
6
|
+
type BrowserPublishAtlasOptions = Pick<AtlasOptions, 'maxSize' | 'fast' | 'allowRotation' | 'padding' | 'powerOfTwo' | 'maxAtlasIndex' | 'multipleOfFour' | 'square' | 'multiPage' | 'trimImage' | 'extractAlpha'>;
|
|
7
7
|
type BrowserPublishSourceFileSystem = PublishSourceFileSystem;
|
|
8
8
|
type BrowserPublishOutputFileSystem = PublishOutputFileSystem;
|
|
9
9
|
interface BrowserPublishOptions {
|
package/dist/web.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { d as PublishSourceFileSystem, t as AtlasOptions, u as PublishOutputFileSystem } from "./atlas-
|
|
1
|
+
import { d as PublishSourceFileSystem, t as AtlasOptions, u as PublishOutputFileSystem } from "./atlas-BtWa1DwO.js";
|
|
2
2
|
import { Document, ProjectValidationReport, UamProject } from "@openfairygui/core";
|
|
3
3
|
|
|
4
4
|
//#region src/adapters/web/publish.d.ts
|
|
5
5
|
type BrowserPublishProjectType = 'layabox';
|
|
6
|
-
type BrowserPublishAtlasOptions = Pick<AtlasOptions, 'maxSize' | 'fast' | 'allowRotation' | 'padding' | 'powerOfTwo' | 'square' | 'multiPage' | 'trimImage' | 'extractAlpha'>;
|
|
6
|
+
type BrowserPublishAtlasOptions = Pick<AtlasOptions, 'maxSize' | 'fast' | 'allowRotation' | 'padding' | 'powerOfTwo' | 'maxAtlasIndex' | 'multipleOfFour' | 'square' | 'multiPage' | 'trimImage' | 'extractAlpha'>;
|
|
7
7
|
type BrowserPublishSourceFileSystem = PublishSourceFileSystem;
|
|
8
8
|
type BrowserPublishOutputFileSystem = PublishOutputFileSystem;
|
|
9
9
|
interface BrowserPublishOptions {
|
package/dist/web.js
CHANGED
|
@@ -1,31 +1,9 @@
|
|
|
1
|
-
import { _ as validateProject, c as resolveCodeGenerationSettings, n as resolvePublishOptions, t as publish } from "./publish-
|
|
2
|
-
import { ProjectType, createProjectValidationReport } from "@openfairygui/core";
|
|
3
|
-
import { XMLParser
|
|
1
|
+
import { _ as validateProject, c as resolveCodeGenerationSettings, n as resolvePublishOptions, t as publish } from "./publish-Cy09yjfn.js";
|
|
2
|
+
import { ProjectType, createProjectValidationReport, validateSafeSvgSource } from "@openfairygui/core";
|
|
3
|
+
import { XMLParser } from "fast-xml-parser";
|
|
4
4
|
//#region src/adapters/web/raster.ts
|
|
5
|
-
const MAX_SVG_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
6
5
|
const MAX_SVG_DIMENSION = 16384;
|
|
7
6
|
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
7
|
function unsafeSvg(message) {
|
|
30
8
|
throw new Error(`publishBrowser: unsafe SVG input (${message}).`);
|
|
31
9
|
}
|
|
@@ -40,47 +18,14 @@ function parseSvgLength(value, name) {
|
|
|
40
18
|
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > MAX_SVG_DIMENSION) unsafeSvg(`${name} exceeds the supported dimensions`);
|
|
41
19
|
return parsed;
|
|
42
20
|
}
|
|
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
21
|
function validateSvg(bytes) {
|
|
75
|
-
if (bytes.byteLength === 0 || bytes.byteLength > MAX_SVG_SOURCE_BYTES) unsafeSvg("source size is unsupported");
|
|
76
|
-
let source;
|
|
77
22
|
try {
|
|
78
|
-
|
|
79
|
-
} catch {
|
|
80
|
-
unsafeSvg(
|
|
23
|
+
validateSvgDimensions(validateSafeSvgSource(bytes));
|
|
24
|
+
} catch (error) {
|
|
25
|
+
unsafeSvg(error instanceof Error ? error.message : String(error));
|
|
81
26
|
}
|
|
82
|
-
|
|
83
|
-
|
|
27
|
+
}
|
|
28
|
+
function validateSvgDimensions(source) {
|
|
84
29
|
const roots = new XMLParser({
|
|
85
30
|
preserveOrder: true,
|
|
86
31
|
ignoreAttributes: false,
|
|
@@ -94,9 +39,7 @@ function validateSvg(bytes) {
|
|
|
94
39
|
name
|
|
95
40
|
})));
|
|
96
41
|
if (roots.length !== 1 || svgLocalName(roots[0].name) !== "svg") unsafeSvg("a single <svg> root is required");
|
|
97
|
-
const
|
|
98
|
-
visitSvgEntry(root);
|
|
99
|
-
const attributes = root[":@"] ?? {};
|
|
42
|
+
const attributes = roots[0].entry[":@"] ?? {};
|
|
100
43
|
const width = parseSvgLength(attributes.width, "width");
|
|
101
44
|
const height = parseSvgLength(attributes.height, "height");
|
|
102
45
|
let viewBoxWidth;
|
|
@@ -218,6 +161,46 @@ var BrowserImagePipeline = class {
|
|
|
218
161
|
ensureAlpha() {
|
|
219
162
|
return this;
|
|
220
163
|
}
|
|
164
|
+
removeAlpha() {
|
|
165
|
+
this.raster = this.raster.then((source) => {
|
|
166
|
+
const context = getBrowserContext(source.canvas);
|
|
167
|
+
const image = context.getImageData(0, 0, source.width, source.height);
|
|
168
|
+
for (let index = 3; index < image.data.length; index += 4) image.data[index] = 255;
|
|
169
|
+
context.putImageData(image, 0, 0);
|
|
170
|
+
return source;
|
|
171
|
+
});
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
extractChannel(channel) {
|
|
175
|
+
if (channel !== "alpha") throw new Error(`publishBrowser: Unsupported channel "${channel}".`);
|
|
176
|
+
this.raster = this.raster.then((source) => {
|
|
177
|
+
const context = getBrowserContext(source.canvas);
|
|
178
|
+
const image = context.getImageData(0, 0, source.width, source.height);
|
|
179
|
+
for (let index = 0; index < image.data.length; index += 4) {
|
|
180
|
+
const alpha = image.data[index + 3] ?? 0;
|
|
181
|
+
image.data[index] = alpha;
|
|
182
|
+
image.data[index + 1] = alpha;
|
|
183
|
+
image.data[index + 2] = alpha;
|
|
184
|
+
image.data[index + 3] = 255;
|
|
185
|
+
}
|
|
186
|
+
context.putImageData(image, 0, 0);
|
|
187
|
+
return source;
|
|
188
|
+
});
|
|
189
|
+
return this;
|
|
190
|
+
}
|
|
191
|
+
joinChannel(images) {
|
|
192
|
+
this.raster = Promise.all([this.raster, ...images.map((image) => this.decode(image))]).then(([source, ...channels]) => {
|
|
193
|
+
const context = getBrowserContext(source.canvas);
|
|
194
|
+
const image = context.getImageData(0, 0, source.width, source.height);
|
|
195
|
+
for (const [channelIndex, channel] of channels.slice(0, 2).entries()) {
|
|
196
|
+
const channelData = getBrowserContext(channel.canvas).getImageData(0, 0, channel.width, channel.height).data;
|
|
197
|
+
for (let index = 0; index < image.data.length; index += 4) image.data[index + channelIndex + 1] = channelData[index] ?? 0;
|
|
198
|
+
}
|
|
199
|
+
context.putImageData(image, 0, 0);
|
|
200
|
+
return source;
|
|
201
|
+
});
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
221
204
|
resize(options) {
|
|
222
205
|
this.raster = this.raster.then((source) => {
|
|
223
206
|
const target = createRaster(options.width, options.height);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfairygui/functions",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "FairyGUI Headless Authoring SDK — composable transform functions.",
|
|
5
5
|
"author": "OpenFairyGUI Contributors",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/OpenFairyGUI/OpenFairyGUI/issues"
|
|
15
15
|
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
16
19
|
"type": "module",
|
|
17
20
|
"sideEffects": false,
|
|
18
21
|
"main": "./dist/index.cjs",
|
|
@@ -75,7 +78,7 @@
|
|
|
75
78
|
"dependencies": {
|
|
76
79
|
"fast-xml-parser": "^5.0.0",
|
|
77
80
|
"jiti": "^2.6.1",
|
|
78
|
-
"@openfairygui/core": "0.3.
|
|
81
|
+
"@openfairygui/core": "0.3.1"
|
|
79
82
|
},
|
|
80
83
|
"optionalDependencies": {
|
|
81
84
|
"sharp": ">=0.33.0"
|
|
@@ -7,10 +7,7 @@ import {
|
|
|
7
7
|
type PluginModule,
|
|
8
8
|
} from '../../plugins/types.js';
|
|
9
9
|
|
|
10
|
-
interface PluginPackageJson extends Partial<PluginManifest> {
|
|
11
|
-
name?: string;
|
|
12
|
-
main?: string;
|
|
13
|
-
}
|
|
10
|
+
interface PluginPackageJson extends Partial<PluginManifest> {}
|
|
14
11
|
|
|
15
12
|
// Keep Node builtins out of the neutral bundle resolver while still loading plugins in Node.
|
|
16
13
|
const importNative = new Function('id', 'return import(id)') as <T>(id: string) => Promise<T>;
|
|
@@ -31,15 +28,35 @@ export async function loadPlugins(doc: Document, pluginsDir: string): Promise<Lo
|
|
|
31
28
|
for (const entry of entries) {
|
|
32
29
|
if (!entry.isDirectory()) continue;
|
|
33
30
|
const pluginDir = path.join(pluginsDir, entry.name);
|
|
31
|
+
let manifest: PluginManifest | null;
|
|
34
32
|
try {
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
manifest = await readPluginManifest(fs, path, pluginDir);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
doc.getLogger().warn(`publish: Plugin "${entry.name}" was skipped: ${formatPluginError(error)}`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (!manifest) continue;
|
|
39
|
+
if (!manifest.main) {
|
|
40
|
+
const error = new Error(`Codegen plugin "${manifest.name}" is missing package.json main.`);
|
|
41
|
+
if (manifest.required) throw error;
|
|
42
|
+
doc.getLogger().warn(`publish: Plugin "${manifest.name}" was skipped: ${error.message}`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
37
45
|
|
|
46
|
+
try {
|
|
38
47
|
const mainPath = resolvePluginMain(path, pluginDir, manifest);
|
|
39
48
|
const plugin = await loadPlugin(mainPath);
|
|
40
|
-
plugins.push({
|
|
49
|
+
plugins.push({
|
|
50
|
+
name: manifest.name,
|
|
51
|
+
plugin,
|
|
52
|
+
failureMode: manifest.required ? 'abort' : manifest.failureMode,
|
|
53
|
+
});
|
|
41
54
|
} catch (error) {
|
|
42
|
-
|
|
55
|
+
if (!manifest.required && manifest.failureMode === 'warn') {
|
|
56
|
+
doc.getLogger().warn(`publish: Plugin "${manifest.name}" was skipped: ${formatPluginError(error)}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`publish: Failed to load plugin "${manifest.name}": ${formatPluginError(error)}`);
|
|
43
60
|
}
|
|
44
61
|
}
|
|
45
62
|
|
|
@@ -50,17 +67,16 @@ async function readPluginManifest(
|
|
|
50
67
|
fs: typeof import('node:fs/promises'),
|
|
51
68
|
path: typeof import('node:path'),
|
|
52
69
|
pluginDir: string,
|
|
53
|
-
): Promise<
|
|
70
|
+
): Promise<PluginManifest | null> {
|
|
54
71
|
const manifestPath = path.join(pluginDir, 'package.json');
|
|
55
72
|
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
56
73
|
const manifest = JSON.parse(content) as PluginPackageJson;
|
|
57
74
|
if (!manifest.name) throw new Error(`Codegen plugin at ${pluginDir} is missing package.json name.`);
|
|
58
|
-
|
|
59
|
-
return manifest;
|
|
75
|
+
return manifest as PluginManifest;
|
|
60
76
|
}
|
|
61
77
|
|
|
62
|
-
function resolvePluginMain(path: typeof import('node:path'), pluginDir: string, manifest:
|
|
63
|
-
const mainPath = path.resolve(pluginDir, manifest.main
|
|
78
|
+
function resolvePluginMain(path: typeof import('node:path'), pluginDir: string, manifest: PluginManifest): string {
|
|
79
|
+
const mainPath = path.resolve(pluginDir, manifest.main);
|
|
64
80
|
const relative = path.relative(pluginDir, mainPath);
|
|
65
81
|
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
66
82
|
throw new Error(`Codegen plugin "${manifest.name}" main must resolve inside its plugin directory.`);
|
|
@@ -83,6 +83,70 @@ async function loadNodePublishPlugins(document: Document, assetsPath: string | u
|
|
|
83
83
|
return loadPlugins(document, path.join(projectDir, 'plugins'));
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
async function publishToStagedOutput(output: string, run: (staging: string) => Promise<void>): Promise<void> {
|
|
87
|
+
const [fs, path, { randomUUID }] = await Promise.all([
|
|
88
|
+
importNative<typeof import('node:fs/promises')>('node:fs/promises'),
|
|
89
|
+
importNative<typeof import('node:path')>('node:path'),
|
|
90
|
+
importNative<typeof import('node:crypto')>('node:crypto'),
|
|
91
|
+
]);
|
|
92
|
+
const target = path.resolve(output);
|
|
93
|
+
const parent = path.dirname(target);
|
|
94
|
+
const name = path.basename(target);
|
|
95
|
+
const staging = path.join(parent, `.${name}.publish-${randomUUID()}`);
|
|
96
|
+
const backup = path.join(parent, `.${name}.publish-backup-${randomUUID()}`);
|
|
97
|
+
await fs.mkdir(parent, { recursive: true });
|
|
98
|
+
let existed = false;
|
|
99
|
+
try {
|
|
100
|
+
const stat = await fs.lstat(target);
|
|
101
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`publishNode: output must be a regular directory: ${target}`);
|
|
102
|
+
await assertNoSymlinks(fs, path, target);
|
|
103
|
+
existed = true;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
if (existed) await fs.cp(target, staging, { recursive: true, errorOnExist: true, force: false });
|
|
109
|
+
else await fs.mkdir(staging);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
await fs.rm(staging, { recursive: true, force: true });
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await run(staging);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
await fs.rm(staging, { recursive: true, force: true });
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
if (!existed) {
|
|
122
|
+
await fs.rename(staging, target);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ponytail: two-step rename preserves rollback; use directory exchange if zero reader gap becomes required.
|
|
127
|
+
await fs.rename(target, backup);
|
|
128
|
+
try {
|
|
129
|
+
await fs.rename(staging, target);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
await fs.rename(backup, target);
|
|
132
|
+
await fs.rm(staging, { recursive: true, force: true });
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
await fs.rm(backup, { recursive: true, force: true }).catch(() => undefined);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function assertNoSymlinks(
|
|
139
|
+
fs: typeof import('node:fs/promises'),
|
|
140
|
+
path: typeof import('node:path'),
|
|
141
|
+
directory: string,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
144
|
+
const entryPath = path.join(directory, entry.name);
|
|
145
|
+
if (entry.isSymbolicLink()) throw new Error(`publishNode: symbolic links are not supported in output directories: ${entryPath}`);
|
|
146
|
+
if (entry.isDirectory()) await assertNoSymlinks(fs, path, entryPath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
86
150
|
/**
|
|
87
151
|
* Publish a FairyGUI project through the standard Node host adapter.
|
|
88
152
|
*
|
|
@@ -114,9 +178,10 @@ export async function publishNode(options: PublishNodeOptions): Promise<void> {
|
|
|
114
178
|
throw new Error('publishNode: Sharp is required for a complete publish. Install sharp or provide an encoder.');
|
|
115
179
|
}
|
|
116
180
|
|
|
117
|
-
|
|
118
|
-
publish({
|
|
181
|
+
const run = async (output: string | undefined): Promise<void> => {
|
|
182
|
+
await document.transform(publish({
|
|
119
183
|
...publishOptions,
|
|
184
|
+
output,
|
|
120
185
|
basePath: assetsPath,
|
|
121
186
|
encoder,
|
|
122
187
|
atlas: {
|
|
@@ -125,6 +190,11 @@ export async function publishNode(options: PublishNodeOptions): Promise<void> {
|
|
|
125
190
|
},
|
|
126
191
|
fs: fileSystem,
|
|
127
192
|
plugins,
|
|
128
|
-
})
|
|
129
|
-
|
|
193
|
+
}));
|
|
194
|
+
};
|
|
195
|
+
if (publishOptions.output) {
|
|
196
|
+
await publishToStagedOutput(publishOptions.output, run);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
await run(undefined);
|
|
130
200
|
}
|
|
@@ -72,6 +72,7 @@ export async function validateProjectNode(projectPath: string): Promise<ProjectV
|
|
|
72
72
|
));
|
|
73
73
|
|
|
74
74
|
for (const { pkg, packageIndex, resource, resourceIndex } of images) {
|
|
75
|
+
if (resource.kind !== 'image') continue;
|
|
75
76
|
try {
|
|
76
77
|
await sharp(resource.sourceBytes!).raw().toBuffer();
|
|
77
78
|
} catch (error) {
|