@openfairygui/functions 0.3.0 → 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/index.cjs +2 -2
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/node.cjs +113 -18
- package/dist/node.d.cts +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +113 -18
- package/dist/{publish-BBoK-yeR.js → publish-Cy09yjfn.js} +9 -2
- package/dist/{publish-CxKatE3e.cjs → publish-DhMv_lEP.cjs} +9 -2
- package/dist/{restore-BOfsCM47.d.ts → restore-CbscKkNw.d.ts} +3 -0
- package/dist/{restore-DOcyfc0b.js → restore-DB_L_xq4.js} +1 -1
- package/dist/{restore-D6F1ELdM.d.cts → restore-k3oX26Fs.d.cts} +3 -0
- package/dist/{restore-QScbHJ7Y.cjs → restore-n61oZ4nw.cjs} +1 -1
- package/dist/web.cjs +7 -64
- package/dist/web.js +9 -66
- package/package.json +5 -2
- package/src/adapters/node/plugins.ts +25 -9
- package/src/adapters/node/publish.ts +74 -4
- package/src/adapters/web/raster.ts +9 -72
- package/src/codegen.ts +4 -2
- package/src/plugins/types.ts +7 -0
- package/src/publish.ts +4 -2
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_publish = require("./publish-
|
|
3
|
-
const require_restore = require("./restore-
|
|
2
|
+
const require_publish = require("./publish-DhMv_lEP.cjs");
|
|
3
|
+
const require_restore = require("./restore-n61oZ4nw.cjs");
|
|
4
4
|
const require_uam_transaction = require("./uam-transaction.cjs");
|
|
5
5
|
//#region src/inspect.ts
|
|
6
6
|
function mapResource(resource) {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as AtlasRasterInput, c as AtlasRasterResolvedBuffer, d as PublishSourceFileSystem, i as AtlasRasterCompositeInput, l as PublishFileSystem, n as atlas, o as AtlasRasterMetadata, r as AtlasRasterBackend, s as AtlasRasterPipeline, t as AtlasOptions, u as PublishOutputFileSystem } from "./atlas-CSqHsG0X.cjs";
|
|
2
|
-
import { A as ExtrasMap, B as ResolvedPublishOptions, C as MaybePromise, D as CliAtlasSettings, E as PluginModule, F as RootProjectSettings, I as publish, L as PublishOptions, M as HasOptionalSrc, N as HasOptionalUrl, O as CliCodeGenerationSettings, P as PublishDependency, R as ResolvePublishOptionsOverrides, S as LoadedPlugin, T as PluginManifest, V as resolvePublishOptions, _ as encodeText, a as RestoreImageExtractor, b as CodeWriter, c as restore, d as CodegenMember, f as CodegenReferencedComponent, g as decodeText, h as buildCodegenClasses, i as RestoreImageExtractInput, j as HasOptionalFont, k as CliPublishSettings, l as AUTO_GENERATED_CODE_MARK, m as ResolvedPackageCodegenPlan, n as RestoreImageCropInput, o as RestoreOptions, p as PublishCodeGenerationOptions, r as RestoreImageCropper, s as RestoreResult, t as RestoreFileSystem, u as CodegenClass, v as publishCodeGeneration, w as Plugin, x as ICodeWriterConfig, y as resolvePackageCodegenPlan, z as ResolvedPublishAtlasOptions } from "./restore-
|
|
2
|
+
import { A as ExtrasMap, B as ResolvedPublishOptions, C as MaybePromise, D as CliAtlasSettings, E as PluginModule, F as RootProjectSettings, I as publish, L as PublishOptions, M as HasOptionalSrc, N as HasOptionalUrl, O as CliCodeGenerationSettings, P as PublishDependency, R as ResolvePublishOptionsOverrides, S as LoadedPlugin, T as PluginManifest, V as resolvePublishOptions, _ as encodeText, a as RestoreImageExtractor, b as CodeWriter, c as restore, d as CodegenMember, f as CodegenReferencedComponent, g as decodeText, h as buildCodegenClasses, i as RestoreImageExtractInput, j as HasOptionalFont, k as CliPublishSettings, l as AUTO_GENERATED_CODE_MARK, m as ResolvedPackageCodegenPlan, n as RestoreImageCropInput, o as RestoreOptions, p as PublishCodeGenerationOptions, r as RestoreImageCropper, s as RestoreResult, t as RestoreFileSystem, u as CodegenClass, v as publishCodeGeneration, w as Plugin, x as ICodeWriterConfig, y as resolvePackageCodegenPlan, z as ResolvedPublishAtlasOptions } from "./restore-k3oX26Fs.cjs";
|
|
3
3
|
import { ApplyUamTransactionAppDiagnostic, ApplyUamTransactionAppError, ApplyUamTransactionAppInput, ApplyUamTransactionAppResult, applyUamTransactionApp, applyUamTransactionAppAsync } from "./uam-transaction.cjs";
|
|
4
4
|
import { Document, ProjectDiagnostic, ProjectValidationReport, Transform, UamProject } from "@openfairygui/core";
|
|
5
5
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as AtlasRasterInput, c as AtlasRasterResolvedBuffer, d as PublishSourceFileSystem, i as AtlasRasterCompositeInput, l as PublishFileSystem, n as atlas, o as AtlasRasterMetadata, r as AtlasRasterBackend, s as AtlasRasterPipeline, t as AtlasOptions, u as PublishOutputFileSystem } from "./atlas-BtWa1DwO.js";
|
|
2
|
-
import { A as ExtrasMap, B as ResolvedPublishOptions, C as MaybePromise, D as CliAtlasSettings, E as PluginModule, F as RootProjectSettings, I as publish, L as PublishOptions, M as HasOptionalSrc, N as HasOptionalUrl, O as CliCodeGenerationSettings, P as PublishDependency, R as ResolvePublishOptionsOverrides, S as LoadedPlugin, T as PluginManifest, V as resolvePublishOptions, _ as encodeText, a as RestoreImageExtractor, b as CodeWriter, c as restore, d as CodegenMember, f as CodegenReferencedComponent, g as decodeText, h as buildCodegenClasses, i as RestoreImageExtractInput, j as HasOptionalFont, k as CliPublishSettings, l as AUTO_GENERATED_CODE_MARK, m as ResolvedPackageCodegenPlan, n as RestoreImageCropInput, o as RestoreOptions, p as PublishCodeGenerationOptions, r as RestoreImageCropper, s as RestoreResult, t as RestoreFileSystem, u as CodegenClass, v as publishCodeGeneration, w as Plugin, x as ICodeWriterConfig, y as resolvePackageCodegenPlan, z as ResolvedPublishAtlasOptions } from "./restore-
|
|
2
|
+
import { A as ExtrasMap, B as ResolvedPublishOptions, C as MaybePromise, D as CliAtlasSettings, E as PluginModule, F as RootProjectSettings, I as publish, L as PublishOptions, M as HasOptionalSrc, N as HasOptionalUrl, O as CliCodeGenerationSettings, P as PublishDependency, R as ResolvePublishOptionsOverrides, S as LoadedPlugin, T as PluginManifest, V as resolvePublishOptions, _ as encodeText, a as RestoreImageExtractor, b as CodeWriter, c as restore, d as CodegenMember, f as CodegenReferencedComponent, g as decodeText, h as buildCodegenClasses, i as RestoreImageExtractInput, j as HasOptionalFont, k as CliPublishSettings, l as AUTO_GENERATED_CODE_MARK, m as ResolvedPackageCodegenPlan, n as RestoreImageCropInput, o as RestoreOptions, p as PublishCodeGenerationOptions, r as RestoreImageCropper, s as RestoreResult, t as RestoreFileSystem, u as CodegenClass, v as publishCodeGeneration, w as Plugin, x as ICodeWriterConfig, y as resolvePackageCodegenPlan, z as ResolvedPublishAtlasOptions } from "./restore-CbscKkNw.js";
|
|
3
3
|
import { ApplyUamTransactionAppDiagnostic, ApplyUamTransactionAppError, ApplyUamTransactionAppInput, ApplyUamTransactionAppResult, applyUamTransactionApp, applyUamTransactionAppAsync } from "./uam-transaction.js";
|
|
4
4
|
import { Document, ProjectDiagnostic, ProjectValidationReport, Transform, UamProject } from "@openfairygui/core";
|
|
5
5
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { _ as validateProject, a as decodeText, g as createTransform, h as atlas, i as buildCodegenClasses, l as resolvePackageCodegenPlan, n as resolvePublishOptions, o as encodeText, r as AUTO_GENERATED_CODE_MARK, s as publishCodeGeneration, t as publish } from "./publish-
|
|
2
|
-
import { t as restore } from "./restore-
|
|
1
|
+
import { _ as validateProject, a as decodeText, g as createTransform, h as atlas, i as buildCodegenClasses, l as resolvePackageCodegenPlan, n as resolvePublishOptions, o as encodeText, r as AUTO_GENERATED_CODE_MARK, s as publishCodeGeneration, t as publish } from "./publish-Cy09yjfn.js";
|
|
2
|
+
import { t as restore } from "./restore-DB_L_xq4.js";
|
|
3
3
|
import { applyUamTransactionApp, applyUamTransactionAppAsync } from "./uam-transaction.js";
|
|
4
4
|
//#region src/inspect.ts
|
|
5
5
|
function mapResource(resource) {
|
package/dist/node.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_publish = require("./publish-
|
|
3
|
-
const require_restore = require("./restore-
|
|
2
|
+
const require_publish = require("./publish-DhMv_lEP.cjs");
|
|
3
|
+
const require_restore = require("./restore-n61oZ4nw.cjs");
|
|
4
4
|
let _openfairygui_core = require("@openfairygui/core");
|
|
5
5
|
let _openfairygui_core_node = require("@openfairygui/core/node");
|
|
6
6
|
//#region src/adapters/node/plugins.ts
|
|
@@ -19,16 +19,33 @@ async function loadPlugins(doc, pluginsDir) {
|
|
|
19
19
|
for (const entry of entries) {
|
|
20
20
|
if (!entry.isDirectory()) continue;
|
|
21
21
|
const pluginDir = path.join(pluginsDir, entry.name);
|
|
22
|
+
let manifest;
|
|
23
|
+
try {
|
|
24
|
+
manifest = await readPluginManifest(fs, path, pluginDir);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
doc.getLogger().warn(`publish: Plugin "${entry.name}" was skipped: ${require_publish.formatPluginError(error)}`);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (!manifest) continue;
|
|
30
|
+
if (!manifest.main) {
|
|
31
|
+
const error = /* @__PURE__ */ new Error(`Codegen plugin "${manifest.name}" is missing package.json main.`);
|
|
32
|
+
if (manifest.required) throw error;
|
|
33
|
+
doc.getLogger().warn(`publish: Plugin "${manifest.name}" was skipped: ${error.message}`);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
22
36
|
try {
|
|
23
|
-
const manifest = await readPluginManifest(fs, path, pluginDir);
|
|
24
|
-
if (!manifest) continue;
|
|
25
37
|
const plugin = await loadPlugin(resolvePluginMain(path, pluginDir, manifest));
|
|
26
38
|
plugins.push({
|
|
27
39
|
name: manifest.name,
|
|
28
|
-
plugin
|
|
40
|
+
plugin,
|
|
41
|
+
failureMode: manifest.required ? "abort" : manifest.failureMode
|
|
29
42
|
});
|
|
30
43
|
} catch (error) {
|
|
31
|
-
|
|
44
|
+
if (!manifest.required && manifest.failureMode === "warn") {
|
|
45
|
+
doc.getLogger().warn(`publish: Plugin "${manifest.name}" was skipped: ${require_publish.formatPluginError(error)}`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`publish: Failed to load plugin "${manifest.name}": ${require_publish.formatPluginError(error)}`);
|
|
32
49
|
}
|
|
33
50
|
}
|
|
34
51
|
return plugins;
|
|
@@ -38,7 +55,6 @@ async function readPluginManifest(fs, path, pluginDir) {
|
|
|
38
55
|
const content = await fs.readFile(manifestPath, "utf-8");
|
|
39
56
|
const manifest = JSON.parse(content);
|
|
40
57
|
if (!manifest.name) throw new Error(`Codegen plugin at ${pluginDir} is missing package.json name.`);
|
|
41
|
-
if (!manifest.main) throw new Error(`Codegen plugin "${manifest.name}" is missing package.json main.`);
|
|
42
58
|
return manifest;
|
|
43
59
|
}
|
|
44
60
|
function resolvePluginMain(path, pluginDir, manifest) {
|
|
@@ -103,6 +119,77 @@ async function loadNodePublishPlugins(document, assetsPath) {
|
|
|
103
119
|
if (!projectDir) return [];
|
|
104
120
|
return loadPlugins(document, (await importNative$2("node:path")).join(projectDir, "plugins"));
|
|
105
121
|
}
|
|
122
|
+
async function publishToStagedOutput(output, run) {
|
|
123
|
+
const [fs, path, { randomUUID }] = await Promise.all([
|
|
124
|
+
importNative$2("node:fs/promises"),
|
|
125
|
+
importNative$2("node:path"),
|
|
126
|
+
importNative$2("node:crypto")
|
|
127
|
+
]);
|
|
128
|
+
const target = path.resolve(output);
|
|
129
|
+
const parent = path.dirname(target);
|
|
130
|
+
const name = path.basename(target);
|
|
131
|
+
const staging = path.join(parent, `.${name}.publish-${randomUUID()}`);
|
|
132
|
+
const backup = path.join(parent, `.${name}.publish-backup-${randomUUID()}`);
|
|
133
|
+
await fs.mkdir(parent, { recursive: true });
|
|
134
|
+
let existed = false;
|
|
135
|
+
try {
|
|
136
|
+
const stat = await fs.lstat(target);
|
|
137
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`publishNode: output must be a regular directory: ${target}`);
|
|
138
|
+
await assertNoSymlinks(fs, path, target);
|
|
139
|
+
existed = true;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error.code !== "ENOENT") throw error;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
if (existed) await fs.cp(target, staging, {
|
|
145
|
+
recursive: true,
|
|
146
|
+
errorOnExist: true,
|
|
147
|
+
force: false
|
|
148
|
+
});
|
|
149
|
+
else await fs.mkdir(staging);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
await fs.rm(staging, {
|
|
152
|
+
recursive: true,
|
|
153
|
+
force: true
|
|
154
|
+
});
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
await run(staging);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
await fs.rm(staging, {
|
|
161
|
+
recursive: true,
|
|
162
|
+
force: true
|
|
163
|
+
});
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
if (!existed) {
|
|
167
|
+
await fs.rename(staging, target);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await fs.rename(target, backup);
|
|
171
|
+
try {
|
|
172
|
+
await fs.rename(staging, target);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
await fs.rename(backup, target);
|
|
175
|
+
await fs.rm(staging, {
|
|
176
|
+
recursive: true,
|
|
177
|
+
force: true
|
|
178
|
+
});
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
await fs.rm(backup, {
|
|
182
|
+
recursive: true,
|
|
183
|
+
force: true
|
|
184
|
+
}).catch(() => void 0);
|
|
185
|
+
}
|
|
186
|
+
async function assertNoSymlinks(fs, path, directory) {
|
|
187
|
+
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
188
|
+
const entryPath = path.join(directory, entry.name);
|
|
189
|
+
if (entry.isSymbolicLink()) throw new Error(`publishNode: symbolic links are not supported in output directories: ${entryPath}`);
|
|
190
|
+
if (entry.isDirectory()) await assertNoSymlinks(fs, path, entryPath);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
106
193
|
/**
|
|
107
194
|
* Publish a FairyGUI project through the standard Node host adapter.
|
|
108
195
|
*
|
|
@@ -115,17 +202,25 @@ async function publishNode(options) {
|
|
|
115
202
|
const [fileSystem, assetsPath] = await Promise.all([createNodePublishFileSystem(), resolveNodeAssetsPath(document, configuredAssetsPath)]);
|
|
116
203
|
const [encoder, plugins] = await Promise.all([configuredEncoder === void 0 ? loadSharpBackend() : Promise.resolve(configuredEncoder), configuredPlugins === void 0 ? loadNodePublishPlugins(document, assetsPath) : Promise.resolve(configuredPlugins)]);
|
|
117
204
|
if (!encoder) throw new Error("publishNode: Sharp is required for a complete publish. Install sharp or provide an encoder.");
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
205
|
+
const run = async (output) => {
|
|
206
|
+
await document.transform(require_publish.publish({
|
|
207
|
+
...publishOptions,
|
|
208
|
+
output,
|
|
209
|
+
basePath: assetsPath,
|
|
210
|
+
encoder,
|
|
211
|
+
atlas: {
|
|
212
|
+
...atlas,
|
|
213
|
+
readFileRaw: fileSystem.readFileRaw
|
|
214
|
+
},
|
|
215
|
+
fs: fileSystem,
|
|
216
|
+
plugins
|
|
217
|
+
}));
|
|
218
|
+
};
|
|
219
|
+
if (publishOptions.output) {
|
|
220
|
+
await publishToStagedOutput(publishOptions.output, run);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
await run(void 0);
|
|
129
224
|
}
|
|
130
225
|
//#endregion
|
|
131
226
|
//#region src/adapters/node/restore.ts
|
package/dist/node.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as AtlasRasterBackend } from "./atlas-CSqHsG0X.cjs";
|
|
2
|
-
import { L as PublishOptions, S as LoadedPlugin, o as RestoreOptions, s as RestoreResult } from "./restore-
|
|
2
|
+
import { L as PublishOptions, S as LoadedPlugin, o as RestoreOptions, s as RestoreResult } from "./restore-k3oX26Fs.cjs";
|
|
3
3
|
import { Document, ProjectValidationReport } from "@openfairygui/core";
|
|
4
4
|
|
|
5
5
|
//#region src/adapters/node/publish.d.ts
|
package/dist/node.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as AtlasRasterBackend } from "./atlas-BtWa1DwO.js";
|
|
2
|
-
import { L as PublishOptions, S as LoadedPlugin, o as RestoreOptions, s as RestoreResult } from "./restore-
|
|
2
|
+
import { L as PublishOptions, S as LoadedPlugin, o as RestoreOptions, s as RestoreResult } from "./restore-CbscKkNw.js";
|
|
3
3
|
import { Document, ProjectValidationReport } from "@openfairygui/core";
|
|
4
4
|
|
|
5
5
|
//#region src/adapters/node/publish.d.ts
|
package/dist/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { _ as validateProject, m as formatPluginError, t as publish, u as resolveProjectBasePath } from "./publish-
|
|
2
|
-
import { t as restore } from "./restore-
|
|
1
|
+
import { _ as validateProject, m as formatPluginError, t as publish, u as resolveProjectBasePath } from "./publish-Cy09yjfn.js";
|
|
2
|
+
import { t as restore } from "./restore-DB_L_xq4.js";
|
|
3
3
|
import { createProjectValidationReport, liftDocumentToUamProject } from "@openfairygui/core";
|
|
4
4
|
import { NodeIO } from "@openfairygui/core/node";
|
|
5
5
|
//#region src/adapters/node/plugins.ts
|
|
@@ -18,16 +18,33 @@ async function loadPlugins(doc, pluginsDir) {
|
|
|
18
18
|
for (const entry of entries) {
|
|
19
19
|
if (!entry.isDirectory()) continue;
|
|
20
20
|
const pluginDir = path.join(pluginsDir, entry.name);
|
|
21
|
+
let manifest;
|
|
22
|
+
try {
|
|
23
|
+
manifest = await readPluginManifest(fs, path, pluginDir);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
doc.getLogger().warn(`publish: Plugin "${entry.name}" was skipped: ${formatPluginError(error)}`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!manifest) continue;
|
|
29
|
+
if (!manifest.main) {
|
|
30
|
+
const error = /* @__PURE__ */ new Error(`Codegen plugin "${manifest.name}" is missing package.json main.`);
|
|
31
|
+
if (manifest.required) throw error;
|
|
32
|
+
doc.getLogger().warn(`publish: Plugin "${manifest.name}" was skipped: ${error.message}`);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
21
35
|
try {
|
|
22
|
-
const manifest = await readPluginManifest(fs, path, pluginDir);
|
|
23
|
-
if (!manifest) continue;
|
|
24
36
|
const plugin = await loadPlugin(resolvePluginMain(path, pluginDir, manifest));
|
|
25
37
|
plugins.push({
|
|
26
38
|
name: manifest.name,
|
|
27
|
-
plugin
|
|
39
|
+
plugin,
|
|
40
|
+
failureMode: manifest.required ? "abort" : manifest.failureMode
|
|
28
41
|
});
|
|
29
42
|
} catch (error) {
|
|
30
|
-
|
|
43
|
+
if (!manifest.required && manifest.failureMode === "warn") {
|
|
44
|
+
doc.getLogger().warn(`publish: Plugin "${manifest.name}" was skipped: ${formatPluginError(error)}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`publish: Failed to load plugin "${manifest.name}": ${formatPluginError(error)}`);
|
|
31
48
|
}
|
|
32
49
|
}
|
|
33
50
|
return plugins;
|
|
@@ -37,7 +54,6 @@ async function readPluginManifest(fs, path, pluginDir) {
|
|
|
37
54
|
const content = await fs.readFile(manifestPath, "utf-8");
|
|
38
55
|
const manifest = JSON.parse(content);
|
|
39
56
|
if (!manifest.name) throw new Error(`Codegen plugin at ${pluginDir} is missing package.json name.`);
|
|
40
|
-
if (!manifest.main) throw new Error(`Codegen plugin "${manifest.name}" is missing package.json main.`);
|
|
41
57
|
return manifest;
|
|
42
58
|
}
|
|
43
59
|
function resolvePluginMain(path, pluginDir, manifest) {
|
|
@@ -102,6 +118,77 @@ async function loadNodePublishPlugins(document, assetsPath) {
|
|
|
102
118
|
if (!projectDir) return [];
|
|
103
119
|
return loadPlugins(document, (await importNative$2("node:path")).join(projectDir, "plugins"));
|
|
104
120
|
}
|
|
121
|
+
async function publishToStagedOutput(output, run) {
|
|
122
|
+
const [fs, path, { randomUUID }] = await Promise.all([
|
|
123
|
+
importNative$2("node:fs/promises"),
|
|
124
|
+
importNative$2("node:path"),
|
|
125
|
+
importNative$2("node:crypto")
|
|
126
|
+
]);
|
|
127
|
+
const target = path.resolve(output);
|
|
128
|
+
const parent = path.dirname(target);
|
|
129
|
+
const name = path.basename(target);
|
|
130
|
+
const staging = path.join(parent, `.${name}.publish-${randomUUID()}`);
|
|
131
|
+
const backup = path.join(parent, `.${name}.publish-backup-${randomUUID()}`);
|
|
132
|
+
await fs.mkdir(parent, { recursive: true });
|
|
133
|
+
let existed = false;
|
|
134
|
+
try {
|
|
135
|
+
const stat = await fs.lstat(target);
|
|
136
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`publishNode: output must be a regular directory: ${target}`);
|
|
137
|
+
await assertNoSymlinks(fs, path, target);
|
|
138
|
+
existed = true;
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error.code !== "ENOENT") throw error;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
if (existed) await fs.cp(target, staging, {
|
|
144
|
+
recursive: true,
|
|
145
|
+
errorOnExist: true,
|
|
146
|
+
force: false
|
|
147
|
+
});
|
|
148
|
+
else await fs.mkdir(staging);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
await fs.rm(staging, {
|
|
151
|
+
recursive: true,
|
|
152
|
+
force: true
|
|
153
|
+
});
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
await run(staging);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
await fs.rm(staging, {
|
|
160
|
+
recursive: true,
|
|
161
|
+
force: true
|
|
162
|
+
});
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
if (!existed) {
|
|
166
|
+
await fs.rename(staging, target);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await fs.rename(target, backup);
|
|
170
|
+
try {
|
|
171
|
+
await fs.rename(staging, target);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
await fs.rename(backup, target);
|
|
174
|
+
await fs.rm(staging, {
|
|
175
|
+
recursive: true,
|
|
176
|
+
force: true
|
|
177
|
+
});
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
await fs.rm(backup, {
|
|
181
|
+
recursive: true,
|
|
182
|
+
force: true
|
|
183
|
+
}).catch(() => void 0);
|
|
184
|
+
}
|
|
185
|
+
async function assertNoSymlinks(fs, path, directory) {
|
|
186
|
+
for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
|
|
187
|
+
const entryPath = path.join(directory, entry.name);
|
|
188
|
+
if (entry.isSymbolicLink()) throw new Error(`publishNode: symbolic links are not supported in output directories: ${entryPath}`);
|
|
189
|
+
if (entry.isDirectory()) await assertNoSymlinks(fs, path, entryPath);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
105
192
|
/**
|
|
106
193
|
* Publish a FairyGUI project through the standard Node host adapter.
|
|
107
194
|
*
|
|
@@ -114,17 +201,25 @@ async function publishNode(options) {
|
|
|
114
201
|
const [fileSystem, assetsPath] = await Promise.all([createNodePublishFileSystem(), resolveNodeAssetsPath(document, configuredAssetsPath)]);
|
|
115
202
|
const [encoder, plugins] = await Promise.all([configuredEncoder === void 0 ? loadSharpBackend() : Promise.resolve(configuredEncoder), configuredPlugins === void 0 ? loadNodePublishPlugins(document, assetsPath) : Promise.resolve(configuredPlugins)]);
|
|
116
203
|
if (!encoder) throw new Error("publishNode: Sharp is required for a complete publish. Install sharp or provide an encoder.");
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
204
|
+
const run = async (output) => {
|
|
205
|
+
await document.transform(publish({
|
|
206
|
+
...publishOptions,
|
|
207
|
+
output,
|
|
208
|
+
basePath: assetsPath,
|
|
209
|
+
encoder,
|
|
210
|
+
atlas: {
|
|
211
|
+
...atlas,
|
|
212
|
+
readFileRaw: fileSystem.readFileRaw
|
|
213
|
+
},
|
|
214
|
+
fs: fileSystem,
|
|
215
|
+
plugins
|
|
216
|
+
}));
|
|
217
|
+
};
|
|
218
|
+
if (publishOptions.output) {
|
|
219
|
+
await publishToStagedOutput(publishOptions.output, run);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
await run(void 0);
|
|
128
223
|
}
|
|
129
224
|
//#endregion
|
|
130
225
|
//#region src/adapters/node/restore.ts
|
|
@@ -2439,6 +2439,9 @@ const FGUI_TYPESCRIPT_BINDER_TEMPLATE = `{{generatedMark}}
|
|
|
2439
2439
|
function formatPluginError(error) {
|
|
2440
2440
|
return error instanceof Error ? error.message : String(error);
|
|
2441
2441
|
}
|
|
2442
|
+
function shouldAbortPluginFailure(plugin) {
|
|
2443
|
+
return plugin.failureMode !== "warn";
|
|
2444
|
+
}
|
|
2442
2445
|
//#endregion
|
|
2443
2446
|
//#region src/path-utils.ts
|
|
2444
2447
|
function trimTrailingSlashes(value) {
|
|
@@ -2524,7 +2527,9 @@ async function publishCodeGeneration(doc, options) {
|
|
|
2524
2527
|
handled = true;
|
|
2525
2528
|
logger.info(`publish: Generated code using plugin "${plugin.name}"`);
|
|
2526
2529
|
} catch (error) {
|
|
2527
|
-
|
|
2530
|
+
const message = `publish: Code generation plugin "${plugin.name}" failed: ${formatPluginError(error)}`;
|
|
2531
|
+
if (shouldAbortPluginFailure(plugin)) throw new Error(message);
|
|
2532
|
+
logger.warn(message);
|
|
2528
2533
|
}
|
|
2529
2534
|
}
|
|
2530
2535
|
if (handled) return;
|
|
@@ -2995,7 +3000,9 @@ async function runPublishPluginHook(plugins, hook, doc, options) {
|
|
|
2995
3000
|
try {
|
|
2996
3001
|
await fn(doc, options);
|
|
2997
3002
|
} catch (error) {
|
|
2998
|
-
|
|
3003
|
+
const message = `publish: Plugin "${plugin.name}" ${hook} failed: ${formatPluginError(error)}`;
|
|
3004
|
+
if (shouldAbortPluginFailure(plugin)) throw new Error(message);
|
|
3005
|
+
logger.warn(message);
|
|
2999
3006
|
}
|
|
3000
3007
|
}
|
|
3001
3008
|
}
|
|
@@ -2439,6 +2439,9 @@ const FGUI_TYPESCRIPT_BINDER_TEMPLATE = `{{generatedMark}}
|
|
|
2439
2439
|
function formatPluginError(error) {
|
|
2440
2440
|
return error instanceof Error ? error.message : String(error);
|
|
2441
2441
|
}
|
|
2442
|
+
function shouldAbortPluginFailure(plugin) {
|
|
2443
|
+
return plugin.failureMode !== "warn";
|
|
2444
|
+
}
|
|
2442
2445
|
//#endregion
|
|
2443
2446
|
//#region src/path-utils.ts
|
|
2444
2447
|
function trimTrailingSlashes(value) {
|
|
@@ -2524,7 +2527,9 @@ async function publishCodeGeneration(doc, options) {
|
|
|
2524
2527
|
handled = true;
|
|
2525
2528
|
logger.info(`publish: Generated code using plugin "${plugin.name}"`);
|
|
2526
2529
|
} catch (error) {
|
|
2527
|
-
|
|
2530
|
+
const message = `publish: Code generation plugin "${plugin.name}" failed: ${formatPluginError(error)}`;
|
|
2531
|
+
if (shouldAbortPluginFailure(plugin)) throw new Error(message);
|
|
2532
|
+
logger.warn(message);
|
|
2528
2533
|
}
|
|
2529
2534
|
}
|
|
2530
2535
|
if (handled) return;
|
|
@@ -2995,7 +3000,9 @@ async function runPublishPluginHook(plugins, hook, doc, options) {
|
|
|
2995
3000
|
try {
|
|
2996
3001
|
await fn(doc, options);
|
|
2997
3002
|
} catch (error) {
|
|
2998
|
-
|
|
3003
|
+
const message = `publish: Plugin "${plugin.name}" ${hook} failed: ${formatPluginError(error)}`;
|
|
3004
|
+
if (shouldAbortPluginFailure(plugin)) throw new Error(message);
|
|
3005
|
+
logger.warn(message);
|
|
2999
3006
|
}
|
|
3000
3007
|
}
|
|
3001
3008
|
}
|
|
@@ -166,6 +166,8 @@ interface PluginManifest {
|
|
|
166
166
|
};
|
|
167
167
|
icon?: string;
|
|
168
168
|
main: string;
|
|
169
|
+
required?: boolean;
|
|
170
|
+
failureMode?: 'abort' | 'warn';
|
|
169
171
|
}
|
|
170
172
|
interface ICodeWriterConfig {
|
|
171
173
|
blockStart?: string;
|
|
@@ -194,6 +196,7 @@ interface Plugin {
|
|
|
194
196
|
interface LoadedPlugin {
|
|
195
197
|
name: string;
|
|
196
198
|
plugin: Plugin;
|
|
199
|
+
failureMode?: 'abort' | 'warn';
|
|
197
200
|
}
|
|
198
201
|
type PluginModule = Plugin & {
|
|
199
202
|
default?: Plugin;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as basename, f as normalizeComparablePath, p as trimTrailingSlashes } from "./publish-
|
|
1
|
+
import { d as basename, f as normalizeComparablePath, p as trimTrailingSlashes } from "./publish-Cy09yjfn.js";
|
|
2
2
|
import { BinaryReader, ProjectType, ProjectWriter, generateId } from "@openfairygui/core";
|
|
3
3
|
//#region src/restore-internals/output-transaction.ts
|
|
4
4
|
function isPathWithin(root, candidate) {
|
|
@@ -166,6 +166,8 @@ interface PluginManifest {
|
|
|
166
166
|
};
|
|
167
167
|
icon?: string;
|
|
168
168
|
main: string;
|
|
169
|
+
required?: boolean;
|
|
170
|
+
failureMode?: 'abort' | 'warn';
|
|
169
171
|
}
|
|
170
172
|
interface ICodeWriterConfig {
|
|
171
173
|
blockStart?: string;
|
|
@@ -194,6 +196,7 @@ interface Plugin {
|
|
|
194
196
|
interface LoadedPlugin {
|
|
195
197
|
name: string;
|
|
196
198
|
plugin: Plugin;
|
|
199
|
+
failureMode?: 'abort' | 'warn';
|
|
197
200
|
}
|
|
198
201
|
type PluginModule = Plugin & {
|
|
199
202
|
default?: Plugin;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_publish = require("./publish-
|
|
1
|
+
const require_publish = require("./publish-DhMv_lEP.cjs");
|
|
2
2
|
let _openfairygui_core = require("@openfairygui/core");
|
|
3
3
|
//#region src/restore-internals/output-transaction.ts
|
|
4
4
|
function isPathWithin(root, candidate) {
|
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;
|
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;
|
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
|
|
|
@@ -55,7 +72,6 @@ async function readPluginManifest(
|
|
|
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
|
-
if (!manifest.main) throw new Error(`Codegen plugin "${manifest.name}" is missing package.json main.`);
|
|
59
75
|
return manifest as PluginManifest;
|
|
60
76
|
}
|
|
61
77
|
|
|
@@ -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
|
}
|
|
@@ -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
|
|
|
@@ -41,30 +42,8 @@ interface BrowserRaster {
|
|
|
41
42
|
height: number;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
const MAX_SVG_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
45
45
|
const MAX_SVG_DIMENSION = 16_384;
|
|
46
46
|
const MAX_SVG_PIXELS = 64 * 1024 * 1024;
|
|
47
|
-
const MAX_SVG_NODES = 50_000;
|
|
48
|
-
const UNSAFE_SVG_ELEMENTS = new Set([
|
|
49
|
-
'a',
|
|
50
|
-
'animate',
|
|
51
|
-
'animatecolor',
|
|
52
|
-
'animatemotion',
|
|
53
|
-
'animatetransform',
|
|
54
|
-
'audio',
|
|
55
|
-
'canvas',
|
|
56
|
-
'discard',
|
|
57
|
-
'embed',
|
|
58
|
-
'feimage',
|
|
59
|
-
'foreignobject',
|
|
60
|
-
'iframe',
|
|
61
|
-
'image',
|
|
62
|
-
'object',
|
|
63
|
-
'script',
|
|
64
|
-
'set',
|
|
65
|
-
'style',
|
|
66
|
-
'video',
|
|
67
|
-
]);
|
|
68
47
|
|
|
69
48
|
type ParsedSvgEntry = Record<string, unknown> & { ':@'?: Record<string, unknown> };
|
|
70
49
|
|
|
@@ -87,57 +66,16 @@ function parseSvgLength(value: unknown, name: string): number | undefined {
|
|
|
87
66
|
return parsed;
|
|
88
67
|
}
|
|
89
68
|
|
|
90
|
-
function validateSvgAttribute(name: string, value: unknown): void {
|
|
91
|
-
const normalizedName = name.toLowerCase();
|
|
92
|
-
if (normalizedName === 'xmlns' || normalizedName.startsWith('xmlns:')) return;
|
|
93
|
-
const localName = svgLocalName(name);
|
|
94
|
-
const text = String(value);
|
|
95
|
-
if (localName.startsWith('on')) unsafeSvg(`event attribute "${name}" is not allowed`);
|
|
96
|
-
if (localName === 'style' || localName === 'src') unsafeSvg(`attribute "${name}" is not allowed`);
|
|
97
|
-
if (localName === 'href' && !/^#[A-Za-z_][\w:.-]*$/u.test(text)) {
|
|
98
|
-
unsafeSvg(`external reference in "${name}" is not allowed`);
|
|
99
|
-
}
|
|
100
|
-
if (/(?:^|[\s("'=])(?:https?:|file:|javascript:|data:|\/\/)/iu.test(text)) {
|
|
101
|
-
unsafeSvg(`external URL in "${name}" is not allowed`);
|
|
102
|
-
}
|
|
103
|
-
for (const match of text.matchAll(/url\s*\(([^)]*)\)/giu)) {
|
|
104
|
-
const reference = (match[1] ?? '').trim().replace(/^(['"])(.*)\1$/u, '$2');
|
|
105
|
-
if (!/^#[A-Za-z_][\w:.-]*$/u.test(reference)) unsafeSvg(`external url() in "${name}" is not allowed`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function visitSvgEntry(entry: ParsedSvgEntry): void {
|
|
110
|
-
const pending = [entry];
|
|
111
|
-
let nodeCount = 0;
|
|
112
|
-
while (pending.length > 0) {
|
|
113
|
-
const current = pending.pop()!;
|
|
114
|
-
for (const [name, value] of Object.entries(current)) {
|
|
115
|
-
if (name === ':@' || name.startsWith('#') || name.startsWith('?')) continue;
|
|
116
|
-
if (++nodeCount > MAX_SVG_NODES) unsafeSvg('node count exceeds the supported limit');
|
|
117
|
-
const localName = svgLocalName(name);
|
|
118
|
-
if (UNSAFE_SVG_ELEMENTS.has(localName)) unsafeSvg(`element <${name}> is not allowed`);
|
|
119
|
-
for (const [attributeName, attributeValue] of Object.entries(current[':@'] ?? {})) {
|
|
120
|
-
validateSvgAttribute(attributeName, attributeValue);
|
|
121
|
-
}
|
|
122
|
-
if (Array.isArray(value)) {
|
|
123
|
-
for (const child of value) {
|
|
124
|
-
if (child && typeof child === 'object' && !Array.isArray(child)) pending.push(child as ParsedSvgEntry);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
69
|
function validateSvg(bytes: Uint8Array): void {
|
|
132
|
-
if (bytes.byteLength === 0 || bytes.byteLength > MAX_SVG_SOURCE_BYTES) unsafeSvg('source size is unsupported');
|
|
133
|
-
let source: string;
|
|
134
70
|
try {
|
|
135
|
-
source =
|
|
136
|
-
|
|
137
|
-
|
|
71
|
+
const source = validateSafeSvgSource(bytes);
|
|
72
|
+
validateSvgDimensions(source);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
unsafeSvg(error instanceof Error ? error.message : String(error));
|
|
138
75
|
}
|
|
139
|
-
|
|
140
|
-
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validateSvgDimensions(source: string): void {
|
|
141
79
|
const parsed = new XMLParser({
|
|
142
80
|
preserveOrder: true,
|
|
143
81
|
ignoreAttributes: false,
|
|
@@ -152,7 +90,6 @@ function validateSvg(bytes: Uint8Array): void {
|
|
|
152
90
|
.map((name) => ({ entry, name })));
|
|
153
91
|
if (roots.length !== 1 || svgLocalName(roots[0]!.name) !== 'svg') unsafeSvg('a single <svg> root is required');
|
|
154
92
|
const root = roots[0]!.entry;
|
|
155
|
-
visitSvgEntry(root);
|
|
156
93
|
const attributes = root[':@'] ?? {};
|
|
157
94
|
const width = parseSvgLength(attributes.width, 'width');
|
|
158
95
|
const height = parseSvgLength(attributes.height, 'height');
|
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';
|
|
@@ -119,7 +119,9 @@ export async function publishCodeGeneration(doc: Document, options: PublishCodeG
|
|
|
119
119
|
handled = true;
|
|
120
120
|
logger.info(`publish: Generated code using plugin "${plugin.name}"`);
|
|
121
121
|
} catch (error) {
|
|
122
|
-
|
|
122
|
+
const message = `publish: Code generation plugin "${plugin.name}" failed: ${formatPluginError(error)}`;
|
|
123
|
+
if (shouldAbortPluginFailure(plugin)) throw new Error(message);
|
|
124
|
+
logger.warn(message);
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
if (handled) {
|
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.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { prepareMovieClipResource } from './atlas/inputs.js';
|
|
|
13
13
|
import type { PreparedJtaData } from './atlas/jta.js';
|
|
14
14
|
import { publishCodeGeneration, resolveProjectBasePath } from './codegen.js';
|
|
15
15
|
import { dirname, isAbsolutePathLike, trimTrailingSlashes } from './path-utils.js';
|
|
16
|
-
import { formatPluginError, type LoadedPlugin } from './plugins/types.js';
|
|
16
|
+
import { formatPluginError, type LoadedPlugin, shouldAbortPluginFailure } from './plugins/types.js';
|
|
17
17
|
import type { PublishFileSystem } from './publish/contracts.js';
|
|
18
18
|
import {
|
|
19
19
|
annotatePackagePublishArtifacts,
|
|
@@ -75,7 +75,9 @@ async function runPublishPluginHook(
|
|
|
75
75
|
try {
|
|
76
76
|
await fn(doc, options);
|
|
77
77
|
} catch (error) {
|
|
78
|
-
|
|
78
|
+
const message = `publish: Plugin "${plugin.name}" ${hook} failed: ${formatPluginError(error)}`;
|
|
79
|
+
if (shouldAbortPluginFailure(plugin)) throw new Error(message);
|
|
80
|
+
logger.warn(message);
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
}
|