@openfairygui/functions 0.2.0-alpha.1 → 0.2.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/web.js ADDED
@@ -0,0 +1,271 @@
1
+ import { t as publish } from "./publish-Bl-GK9kt.js";
2
+ import { ProjectType } from "@openfairygui/core";
3
+ //#region src/adapters/web/publish.ts
4
+ function getBrowserContext(canvas) {
5
+ const context = canvas.getContext("2d");
6
+ if (!context) throw new Error("publishBrowser: a 2D canvas context is unavailable.");
7
+ return context;
8
+ }
9
+ function createBrowserCanvas(width, height) {
10
+ if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(width, height);
11
+ if (typeof globalThis.document === "undefined") throw new Error("publishBrowser: OffscreenCanvas or a DOM canvas is required for atlas PNG generation.");
12
+ const canvas = globalThis.document.createElement("canvas");
13
+ canvas.width = width;
14
+ canvas.height = height;
15
+ return canvas;
16
+ }
17
+ function assertBrowserImageSupport() {
18
+ if (typeof createImageBitmap !== "function") throw new Error("publishBrowser: createImageBitmap is required for atlas PNG generation.");
19
+ if (typeof OffscreenCanvas === "undefined" && typeof globalThis.document === "undefined") throw new Error("publishBrowser: OffscreenCanvas or a DOM canvas is required for atlas PNG generation.");
20
+ }
21
+ function createRaster(width, height, background) {
22
+ const canvas = createBrowserCanvas(width, height);
23
+ const context = getBrowserContext(canvas);
24
+ context.clearRect(0, 0, width, height);
25
+ if (background && background.alpha > 0) {
26
+ context.fillStyle = `rgba(${background.r}, ${background.g}, ${background.b}, ${background.alpha})`;
27
+ context.fillRect(0, 0, width, height);
28
+ }
29
+ return {
30
+ canvas,
31
+ width,
32
+ height
33
+ };
34
+ }
35
+ function imageMimeType(path) {
36
+ if (/\.svg$/iu.test(path)) return "image/svg+xml";
37
+ if (/\.jpe?g$/iu.test(path)) return "image/jpeg";
38
+ if (/\.webp$/iu.test(path)) return "image/webp";
39
+ if (/\.gif$/iu.test(path)) return "image/gif";
40
+ return "image/png";
41
+ }
42
+ function imageMimeTypeFromBytes(bytes) {
43
+ if (bytes[0] === 255 && bytes[1] === 216) return "image/jpeg";
44
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) return "image/gif";
45
+ if (bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70) return "image/webp";
46
+ return "image/png";
47
+ }
48
+ async function canvasToPng(canvas) {
49
+ let blob;
50
+ if ("convertToBlob" in canvas && typeof canvas.convertToBlob === "function") blob = await canvas.convertToBlob({ type: "image/png" });
51
+ else blob = await new Promise((resolve, reject) => {
52
+ canvas.toBlob((value) => {
53
+ if (value) resolve(value);
54
+ else reject(/* @__PURE__ */ new Error("publishBrowser: canvas PNG encoding failed."));
55
+ }, "image/png");
56
+ });
57
+ return new Uint8Array(await blob.arrayBuffer());
58
+ }
59
+ async function decodeRaster(bytes, mimeType) {
60
+ if (typeof createImageBitmap !== "function") throw new Error("publishBrowser: createImageBitmap is required for atlas PNG generation.");
61
+ const copy = bytes.slice();
62
+ const bitmap = await createImageBitmap(new Blob([copy.buffer], { type: mimeType }));
63
+ try {
64
+ const raster = createRaster(bitmap.width, bitmap.height);
65
+ getBrowserContext(raster.canvas).drawImage(bitmap, 0, 0);
66
+ return raster;
67
+ } finally {
68
+ bitmap.close();
69
+ }
70
+ }
71
+ var BrowserImagePipeline = class {
72
+ rawOutput = false;
73
+ constructor(raster, decode, write) {
74
+ this.raster = raster;
75
+ this.decode = decode;
76
+ this.write = write;
77
+ }
78
+ ensureAlpha() {
79
+ return this;
80
+ }
81
+ resize(options) {
82
+ this.raster = this.raster.then((source) => {
83
+ const target = createRaster(options.width, options.height);
84
+ getBrowserContext(target.canvas).drawImage(source.canvas, 0, 0, options.width, options.height);
85
+ return target;
86
+ });
87
+ return this;
88
+ }
89
+ raw() {
90
+ this.rawOutput = true;
91
+ return this;
92
+ }
93
+ extract(options) {
94
+ this.raster = this.raster.then((source) => {
95
+ const target = createRaster(options.width, options.height);
96
+ getBrowserContext(target.canvas).drawImage(source.canvas, options.left, options.top, options.width, options.height, 0, 0, options.width, options.height);
97
+ return target;
98
+ });
99
+ return this;
100
+ }
101
+ png() {
102
+ this.rawOutput = false;
103
+ return this;
104
+ }
105
+ rotate(angle) {
106
+ this.raster = this.raster.then((source) => {
107
+ if (angle % 180 === 0) return source;
108
+ const target = createRaster(source.height, source.width);
109
+ const context = getBrowserContext(target.canvas);
110
+ context.save();
111
+ if (angle === 270 || angle === -90) {
112
+ context.translate(0, source.width);
113
+ context.rotate(-Math.PI / 2);
114
+ } else {
115
+ context.translate(source.height, 0);
116
+ context.rotate(Math.PI / 2);
117
+ }
118
+ context.drawImage(source.canvas, 0, 0);
119
+ context.restore();
120
+ return target;
121
+ });
122
+ return this;
123
+ }
124
+ composite(inputs) {
125
+ this.raster = this.raster.then(async (target) => {
126
+ const context = getBrowserContext(target.canvas);
127
+ for (const input of inputs) {
128
+ const source = await this.decode(input.input);
129
+ context.drawImage(source.canvas, input.left, input.top);
130
+ }
131
+ return target;
132
+ });
133
+ return this;
134
+ }
135
+ async metadata() {
136
+ const raster = await this.raster;
137
+ return {
138
+ width: raster.width,
139
+ height: raster.height,
140
+ channels: 4,
141
+ hasAlpha: true
142
+ };
143
+ }
144
+ async toBuffer(options) {
145
+ const raster = await this.raster;
146
+ if (options?.resolveWithObject) {
147
+ const data = getBrowserContext(raster.canvas).getImageData(0, 0, raster.width, raster.height).data;
148
+ return {
149
+ data: new Uint8Array(data),
150
+ info: {
151
+ width: raster.width,
152
+ height: raster.height,
153
+ channels: 4
154
+ }
155
+ };
156
+ }
157
+ if (this.rawOutput) return new Uint8Array(getBrowserContext(raster.canvas).getImageData(0, 0, raster.width, raster.height).data);
158
+ return canvasToPng(raster.canvas);
159
+ }
160
+ async toFile(path) {
161
+ const raster = await this.raster;
162
+ await this.write(path, await canvasToPng(raster.canvas));
163
+ }
164
+ };
165
+ function createBrowserImageEncoder(sourceFileSystem, outputFileSystem) {
166
+ const decode = (bytes) => decodeRaster(bytes, imageMimeTypeFromBytes(bytes));
167
+ return (input) => {
168
+ return new BrowserImagePipeline(typeof input === "string" ? sourceFileSystem.readFileRaw(input).then((bytes) => decodeRaster(bytes, imageMimeType(input))) : input instanceof Uint8Array ? decode(input) : Promise.resolve(createRaster(input.create.width, input.create.height, input.create.background)), decode, outputFileSystem.writeFileRaw);
169
+ };
170
+ }
171
+ function createTrackingFileSystem(fileSystem, files) {
172
+ return {
173
+ join: (...paths) => fileSystem.join(...paths),
174
+ mkdir: (path) => fileSystem.mkdir(path),
175
+ writeFileRaw: async (path, data) => {
176
+ await fileSystem.writeFileRaw(path, data);
177
+ files.set(path, data.byteLength);
178
+ }
179
+ };
180
+ }
181
+ function createDiagnosticLogger(logger, diagnostics) {
182
+ return {
183
+ debug(message) {
184
+ diagnostics.push({
185
+ level: "debug",
186
+ message
187
+ });
188
+ logger.debug(message);
189
+ },
190
+ info(message) {
191
+ diagnostics.push({
192
+ level: "info",
193
+ message
194
+ });
195
+ logger.info(message);
196
+ },
197
+ warn(message) {
198
+ diagnostics.push({
199
+ level: "warning",
200
+ message
201
+ });
202
+ logger.warn(message);
203
+ },
204
+ error(message) {
205
+ diagnostics.push({
206
+ level: "error",
207
+ message
208
+ });
209
+ logger.error(message);
210
+ }
211
+ };
212
+ }
213
+ function toResult(success, files, diagnostics) {
214
+ return {
215
+ success,
216
+ files: [...files].map(([path, size]) => ({
217
+ path,
218
+ size
219
+ })),
220
+ diagnostics
221
+ };
222
+ }
223
+ /**
224
+ * Publish a loaded FairyGUI project to browser-provided storage.
225
+ *
226
+ * The adapter uses browser Canvas APIs for atlas composition, writes only through
227
+ * the supplied output filesystem, and intentionally skips Node publish plugins.
228
+ */
229
+ async function publishBrowser(options) {
230
+ const files = /* @__PURE__ */ new Map();
231
+ const diagnostics = [];
232
+ const root = options.document.getRoot();
233
+ const previousProjectType = root.getProjectType();
234
+ const previousLogger = options.document.getLogger();
235
+ options.document.setLogger(createDiagnosticLogger(previousLogger, diagnostics));
236
+ try {
237
+ if (options.projectType !== "layabox") throw new Error(`publishBrowser: unsupported project type "${String(options.projectType)}".`);
238
+ assertBrowserImageSupport();
239
+ root.setProjectType(ProjectType.LayaBox);
240
+ const outputFileSystem = createTrackingFileSystem(options.outputFileSystem, files);
241
+ const sourceAssetsPath = options.sourceFileSystem.join(options.document.getProjectDir(), "assets");
242
+ await options.document.transform(publish({
243
+ output: options.output,
244
+ compressed: options.compressed,
245
+ fileExtension: "fui",
246
+ packages: options.packages,
247
+ branch: options.branch,
248
+ basePath: sourceAssetsPath,
249
+ encoder: createBrowserImageEncoder(options.sourceFileSystem, outputFileSystem),
250
+ atlas: {
251
+ ...options.atlas,
252
+ readFileRaw: (path) => options.sourceFileSystem.readFileRaw(path)
253
+ },
254
+ fs: outputFileSystem,
255
+ plugins: [],
256
+ codeGeneration: false
257
+ }));
258
+ return toResult(true, files, diagnostics);
259
+ } catch (error) {
260
+ diagnostics.push({
261
+ level: "error",
262
+ message: error instanceof Error ? error.message : String(error)
263
+ });
264
+ return toResult(false, files, diagnostics);
265
+ } finally {
266
+ root.setProjectType(previousProjectType);
267
+ options.document.setLogger(previousLogger);
268
+ }
269
+ }
270
+ //#endregion
271
+ export { publishBrowser };
package/package.json CHANGED
@@ -1,54 +1,87 @@
1
1
  {
2
- "name": "@openfairygui/functions",
3
- "version": "0.2.0-alpha.1",
4
- "description": "FairyGUI Headless Authoring SDK — composable transform functions.",
5
- "author": "OpenFairyGUI Contributors",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/OpenFairyGUI/OpenFairyGUI.git",
10
- "directory": "packages/functions"
11
- },
12
- "homepage": "https://github.com/OpenFairyGUI/OpenFairyGUI#readme",
13
- "bugs": {
14
- "url": "https://github.com/OpenFairyGUI/OpenFairyGUI/issues"
15
- },
16
- "type": "module",
17
- "sideEffects": false,
18
- "main": "./dist/index.cjs",
19
- "module": "./dist/index.js",
20
- "types": "./dist/index.d.ts",
21
- "exports": {
22
- "require": {
23
- "types": "./dist/index.d.cts",
24
- "default": "./dist/index.cjs"
25
- },
26
- "default": {
27
- "types": "./dist/index.d.ts",
28
- "default": "./dist/index.js"
29
- }
30
- },
31
- "scripts": {
32
- "build": "tsdown --format esm,cjs --platform neutral --env.PACKAGE_VERSION=$npm_package_version",
33
- "build:watch": "tsdown --watch --format esm,cjs --platform neutral --env.PACKAGE_VERSION=$npm_package_version"
34
- },
35
- "files": [
36
- "dist/",
37
- "src/"
38
- ],
39
- "keywords": [
40
- "fairygui",
41
- "ui",
42
- "headless",
43
- "authoring",
44
- "transform",
45
- "publish"
46
- ],
47
- "dependencies": {
48
- "@openfairygui/core": "workspace:*"
49
- },
50
- "devDependencies": {
51
- "ava": "^7.0.0",
52
- "tsx": "^4.0.0"
53
- }
54
- }
2
+ "name": "@openfairygui/functions",
3
+ "version": "0.2.0-alpha.12",
4
+ "description": "FairyGUI Headless Authoring SDK — composable transform functions.",
5
+ "author": "OpenFairyGUI Contributors",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/OpenFairyGUI/OpenFairyGUI.git",
10
+ "directory": "packages/functions"
11
+ },
12
+ "homepage": "https://github.com/OpenFairyGUI/OpenFairyGUI#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/OpenFairyGUI/OpenFairyGUI/issues"
15
+ },
16
+ "type": "module",
17
+ "sideEffects": false,
18
+ "main": "./dist/index.cjs",
19
+ "module": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "require": {
24
+ "types": "./dist/index.d.cts",
25
+ "default": "./dist/index.cjs"
26
+ },
27
+ "default": {
28
+ "types": "./dist/index.d.ts",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "./uam": {
33
+ "require": {
34
+ "types": "./dist/uam-transaction.d.cts",
35
+ "default": "./dist/uam-transaction.cjs"
36
+ },
37
+ "default": {
38
+ "types": "./dist/uam-transaction.d.ts",
39
+ "default": "./dist/uam-transaction.js"
40
+ }
41
+ },
42
+ "./node": {
43
+ "require": {
44
+ "types": "./dist/node.d.cts",
45
+ "default": "./dist/node.cjs"
46
+ },
47
+ "default": {
48
+ "types": "./dist/node.d.ts",
49
+ "default": "./dist/node.js"
50
+ }
51
+ },
52
+ "./web": {
53
+ "require": {
54
+ "types": "./dist/web.d.cts",
55
+ "default": "./dist/web.cjs"
56
+ },
57
+ "default": {
58
+ "types": "./dist/web.d.ts",
59
+ "default": "./dist/web.js"
60
+ }
61
+ }
62
+ },
63
+ "files": [
64
+ "dist/",
65
+ "src/"
66
+ ],
67
+ "keywords": [
68
+ "fairygui",
69
+ "ui",
70
+ "headless",
71
+ "authoring",
72
+ "transform",
73
+ "publish"
74
+ ],
75
+ "dependencies": {
76
+ "jiti": "^2.6.1",
77
+ "@openfairygui/core": "0.2.0-alpha.11"
78
+ },
79
+ "devDependencies": {
80
+ "ava": "^7.0.0",
81
+ "tsx": "^4.0.0"
82
+ },
83
+ "scripts": {
84
+ "build": "tsdown src/index.ts src/uam-transaction.ts src/node.ts src/web.ts --format esm,cjs --platform neutral --env.PACKAGE_VERSION=$npm_package_version",
85
+ "build:watch": "tsdown src/index.ts src/uam-transaction.ts src/node.ts src/web.ts --watch --format esm,cjs --platform neutral --env.PACKAGE_VERSION=$npm_package_version"
86
+ }
87
+ }
@@ -0,0 +1,82 @@
1
+ import type { Document } from '@openfairygui/core';
2
+ import {
3
+ formatPluginError,
4
+ type LoadedPlugin,
5
+ type Plugin,
6
+ type PluginManifest,
7
+ type PluginModule,
8
+ } from '../../plugins/types.js';
9
+
10
+ interface PluginPackageJson extends Partial<PluginManifest> {
11
+ name?: string;
12
+ main?: string;
13
+ }
14
+
15
+ // Keep Node builtins out of the neutral bundle resolver while still loading plugins in Node.
16
+ const importNative = new Function('id', 'return import(id)') as <T>(id: string) => Promise<T>;
17
+
18
+ export async function loadPlugins(doc: Document, pluginsDir: string): Promise<LoadedPlugin[]> {
19
+ if (!pluginsDir) return [];
20
+
21
+ const fs = await importNative<typeof import('node:fs/promises')>('node:fs/promises');
22
+ const path = await importNative<typeof import('node:path')>('node:path');
23
+ let entries: Array<{ name: string; isDirectory(): boolean }>;
24
+ try {
25
+ entries = await fs.readdir(pluginsDir, { withFileTypes: true });
26
+ } catch {
27
+ return [];
28
+ }
29
+
30
+ const plugins: LoadedPlugin[] = [];
31
+ for (const entry of entries) {
32
+ if (!entry.isDirectory()) continue;
33
+ const pluginDir = path.join(pluginsDir, entry.name);
34
+ try {
35
+ const manifest = await readPluginManifest(fs, path, pluginDir);
36
+ if (!manifest) continue;
37
+
38
+ const mainPath = resolvePluginMain(path, pluginDir, manifest);
39
+ const plugin = await loadPlugin(mainPath);
40
+ plugins.push({ name: manifest.name, plugin });
41
+ } catch (error) {
42
+ doc.getLogger().warn(`publish: Plugin "${entry.name}" was skipped: ${formatPluginError(error)}`);
43
+ }
44
+ }
45
+
46
+ return plugins;
47
+ }
48
+
49
+ async function readPluginManifest(
50
+ fs: typeof import('node:fs/promises'),
51
+ path: typeof import('node:path'),
52
+ pluginDir: string,
53
+ ): Promise<PluginPackageJson | null> {
54
+ const manifestPath = path.join(pluginDir, 'package.json');
55
+ const content = await fs.readFile(manifestPath, 'utf-8');
56
+ const manifest = JSON.parse(content) as PluginPackageJson;
57
+ if (!manifest.name) throw new Error(`Codegen plugin at ${pluginDir} is missing package.json name.`);
58
+ if (!manifest.main) throw new Error(`Codegen plugin "${manifest.name}" is missing package.json main.`);
59
+ return manifest;
60
+ }
61
+
62
+ function resolvePluginMain(path: typeof import('node:path'), pluginDir: string, manifest: PluginPackageJson): string {
63
+ const mainPath = path.resolve(pluginDir, manifest.main!);
64
+ const relative = path.relative(pluginDir, mainPath);
65
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
66
+ throw new Error(`Codegen plugin "${manifest.name}" main must resolve inside its plugin directory.`);
67
+ }
68
+ return mainPath;
69
+ }
70
+
71
+ async function loadPlugin(mainPath: string): Promise<Plugin> {
72
+ const { createJiti } = await importNative<typeof import('jiti')>('jiti');
73
+ const jiti = createJiti(import.meta.url);
74
+ const mod = await jiti.import<PluginModule>(mainPath);
75
+ const defaultExport = mod.default;
76
+ const plugin = isObject(defaultExport) ? defaultExport : mod;
77
+ return plugin as Plugin;
78
+ }
79
+
80
+ function isObject(value: unknown): value is Record<string, unknown> {
81
+ return value !== null && typeof value === 'object';
82
+ }
@@ -0,0 +1,129 @@
1
+ import type { Document } from '@openfairygui/core';
2
+ import { resolveProjectBasePath } from '../../codegen.js';
3
+ import { publish, type PublishOptions } from '../../publish.js';
4
+ import type { AtlasRasterBackend, PublishFileSystem } from '../../publish/contracts.js';
5
+ import type { LoadedPlugin } from '../../plugins/types.js';
6
+ import { loadPlugins } from './plugins.js';
7
+
8
+ const importNative = new Function('id', 'return import(id)') as <T>(id: string) => Promise<T>;
9
+
10
+ interface NodePublishFileSystem extends PublishFileSystem {
11
+ readFileRaw(path: string): Promise<Uint8Array>;
12
+ }
13
+
14
+ export interface PublishNodeOptions extends Omit<PublishOptions, 'atlas' | 'basePath' | 'encoder' | 'fs' | 'plugins'> {
15
+ document: Document;
16
+ /**
17
+ * Assets directory. Defaults to `<document project dir>/assets` when available.
18
+ */
19
+ assetsPath?: string;
20
+ /**
21
+ * Override the standard Sharp raster backend.
22
+ */
23
+ encoder?: AtlasRasterBackend;
24
+ /**
25
+ * Supply already-loaded hooks. Pass an empty array to skip project plugin discovery.
26
+ */
27
+ plugins?: LoadedPlugin[];
28
+ atlas?: Omit<NonNullable<PublishOptions['atlas']>, 'readFileRaw'>;
29
+ }
30
+
31
+ async function createNodePublishFileSystem(): Promise<NodePublishFileSystem> {
32
+ const [fs, path] = await Promise.all([
33
+ importNative<typeof import('node:fs/promises')>('node:fs/promises'),
34
+ importNative<typeof import('node:path')>('node:path'),
35
+ ]);
36
+
37
+ return {
38
+ async readFileRaw(filePath: string): Promise<Uint8Array> {
39
+ const data = await fs.readFile(filePath);
40
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
41
+ },
42
+ async writeFileRaw(filePath: string, data: Uint8Array): Promise<void> {
43
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
44
+ await fs.writeFile(filePath, data);
45
+ },
46
+ async mkdir(dirPath: string): Promise<void> {
47
+ await fs.mkdir(dirPath, { recursive: true });
48
+ },
49
+ async readdir(dirPath: string): Promise<string[]> {
50
+ return fs.readdir(dirPath);
51
+ },
52
+ async deleteFile(filePath: string): Promise<void> {
53
+ await fs.rm(filePath, { force: true });
54
+ },
55
+ join(...paths: string[]): string {
56
+ return path.join(...paths);
57
+ },
58
+ };
59
+ }
60
+
61
+ async function resolveNodeAssetsPath(document: Document, assetsPath: string | undefined): Promise<string | undefined> {
62
+ if (assetsPath) return assetsPath;
63
+ const projectDir = document.getProjectDir?.() ?? '';
64
+ if (!projectDir) return undefined;
65
+ const path = await importNative<typeof import('node:path')>('node:path');
66
+ return path.join(projectDir, 'assets');
67
+ }
68
+
69
+ async function loadSharpBackend(): Promise<AtlasRasterBackend | undefined> {
70
+ try {
71
+ const sharp = await importNative<typeof import('sharp')>('sharp');
72
+ return (sharp.default ?? sharp) as unknown as AtlasRasterBackend;
73
+ } catch {
74
+ return undefined;
75
+ }
76
+ }
77
+
78
+ async function loadNodePublishPlugins(document: Document, assetsPath: string | undefined): Promise<LoadedPlugin[]> {
79
+ const projectDir = document.getProjectDir?.() || (assetsPath ? resolveProjectBasePath(assetsPath) : '');
80
+ if (!projectDir) return [];
81
+ const path = await importNative<typeof import('node:path')>('node:path');
82
+ return loadPlugins(document, path.join(projectDir, 'plugins'));
83
+ }
84
+
85
+ /**
86
+ * Publish a FairyGUI project through the standard Node host adapter.
87
+ *
88
+ * The adapter owns Node filesystem, Sharp, and project plugin discovery.
89
+ * For custom environments, use the lower-level `publish()` core with explicit
90
+ * capabilities instead.
91
+ */
92
+ export async function publishNode(options: PublishNodeOptions): Promise<void> {
93
+ const {
94
+ document,
95
+ assetsPath: configuredAssetsPath,
96
+ atlas,
97
+ encoder: configuredEncoder,
98
+ plugins: configuredPlugins,
99
+ ...publishOptions
100
+ } = options;
101
+ const [fileSystem, assetsPath] = await Promise.all([
102
+ createNodePublishFileSystem(),
103
+ resolveNodeAssetsPath(document, configuredAssetsPath),
104
+ ]);
105
+ const [encoder, plugins] = await Promise.all([
106
+ configuredEncoder === undefined ? loadSharpBackend() : Promise.resolve(configuredEncoder),
107
+ configuredPlugins === undefined
108
+ ? loadNodePublishPlugins(document, assetsPath)
109
+ : Promise.resolve(configuredPlugins),
110
+ ]);
111
+
112
+ if (!encoder) {
113
+ document.getLogger().warn('publish: Sharp is unavailable; atlas layout will be generated without PNG output.');
114
+ }
115
+
116
+ await document.transform(
117
+ publish({
118
+ ...publishOptions,
119
+ basePath: assetsPath,
120
+ encoder,
121
+ atlas: {
122
+ ...atlas,
123
+ readFileRaw: fileSystem.readFileRaw,
124
+ },
125
+ fs: fileSystem,
126
+ plugins,
127
+ }),
128
+ );
129
+ }