@openfairygui/functions 0.2.0-alpha.2 → 0.2.0-alpha.21

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.
Files changed (54) hide show
  1. package/README.md +48 -6
  2. package/dist/atlas-C6tbl7nn.d.ts +193 -0
  3. package/dist/atlas-CHsu2Y8i.d.cts +193 -0
  4. package/dist/index.cjs +16 -3603
  5. package/dist/index.d.cts +5 -294
  6. package/dist/index.d.ts +5 -294
  7. package/dist/index.js +3 -3594
  8. package/dist/node.cjs +256 -0
  9. package/dist/node.d.cts +36 -0
  10. package/dist/node.d.ts +36 -0
  11. package/dist/node.js +254 -0
  12. package/dist/publish-BJ_eelME.js +3267 -0
  13. package/dist/publish-xFWT9Slz.cjs +3338 -0
  14. package/dist/restore-BW2xacB3.cjs +936 -0
  15. package/dist/restore-BeWaJNjR.d.cts +288 -0
  16. package/dist/restore-Clk62n0O.js +931 -0
  17. package/dist/restore-Dh0-Nvms.d.ts +288 -0
  18. package/dist/uam-transaction.cjs +44 -1
  19. package/dist/uam-transaction.d.cts +16 -1
  20. package/dist/uam-transaction.d.ts +16 -1
  21. package/dist/uam-transaction.js +44 -1
  22. package/dist/web.cjs +274 -0
  23. package/dist/web.d.cts +41 -0
  24. package/dist/web.d.ts +41 -0
  25. package/dist/web.js +273 -0
  26. package/package.json +28 -4
  27. package/src/adapters/node/plugins.ts +82 -0
  28. package/src/adapters/node/publish.ts +130 -0
  29. package/src/adapters/node/restore.ts +187 -0
  30. package/src/adapters/web/publish.ts +159 -0
  31. package/src/adapters/web/raster.ts +251 -0
  32. package/src/atlas/font.ts +95 -0
  33. package/src/atlas/inputs.ts +515 -0
  34. package/src/atlas/jta.ts +211 -0
  35. package/src/atlas/packing.ts +767 -0
  36. package/src/atlas.ts +116 -1221
  37. package/src/codegen.ts +106 -67
  38. package/src/index.ts +43 -3
  39. package/src/node.ts +8 -0
  40. package/src/plugins/types.ts +56 -0
  41. package/src/publish/contracts.ts +80 -0
  42. package/src/publish/external-resources.ts +117 -0
  43. package/src/publish/options.ts +180 -0
  44. package/src/publish/package-context.ts +608 -0
  45. package/src/publish/resource-references.ts +210 -0
  46. package/src/publish.ts +290 -968
  47. package/src/restore-internals/font.ts +100 -0
  48. package/src/restore-internals/movie-clip.ts +104 -0
  49. package/src/restore-internals/output-transaction.ts +164 -0
  50. package/src/restore.ts +112 -311
  51. package/src/shared-types.ts +4 -8
  52. package/src/uam-transaction.ts +68 -0
  53. package/src/utils.ts +28 -0
  54. package/src/web.ts +11 -0
package/src/codegen.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  type Component,
3
- type GComponent,
4
3
  type Document,
4
+ type GComponent,
5
5
  type GObject,
6
6
  type Package,
7
7
  ProjectType,
@@ -12,7 +12,9 @@ import {
12
12
  UNITY_BINDER_TEMPLATE,
13
13
  UNITY_COMPONENT_TEMPLATE,
14
14
  } from './codegen-templates.js';
15
- import type { CliCodeGenerationSettings, PublishFileSystem, RootProjectSettings } from './shared-types.js';
15
+ import { formatPluginError, type LoadedPlugin } from './plugins/types.js';
16
+ import type { PublishFileSystem } from './publish/contracts.js';
17
+ import type { CliCodeGenerationSettings, RootProjectSettings } from './shared-types.js';
16
18
 
17
19
  export const AUTO_GENERATED_CODE_MARK = '/** This is an automatically generated class by FairyGUI. Please do not modify it. **/';
18
20
  const DEFAULT_CLASS_NAME_PREFIX = 'UI_';
@@ -22,25 +24,15 @@ export interface PublishCodeGenerationOptions {
22
24
  basePath?: string;
23
25
  fs: PublishFileSystem;
24
26
  packages: Package[];
27
+ plugins?: LoadedPlugin[];
25
28
  }
26
29
 
27
- interface ResolvedCodeGenerationSettings {
28
- allowGenCode: boolean;
29
- classNamePrefix: string;
30
- memberNamePrefix: string;
31
- packageName: string;
32
- ignoreNoname: boolean;
33
- getMemberByName: boolean;
34
- codePath: string;
35
- codeType: string;
36
- }
37
-
38
- interface ResolvedPackageCodegenPlan {
30
+ export interface ResolvedPackageCodegenPlan {
39
31
  outputDir: string;
40
32
  packageFolderName: string;
41
33
  packageNamespace: string;
42
34
  binderClassName: string;
43
- settings: ResolvedCodeGenerationSettings;
35
+ settings: CliCodeGenerationSettings;
44
36
  }
45
37
 
46
38
  interface FguiTypescriptVariant {
@@ -77,16 +69,22 @@ const SHARED_FGUI_TYPESCRIPT_VARIANT: FguiTypescriptVariant = {
77
69
  runtimeNamespace: 'fgui',
78
70
  };
79
71
 
80
- interface CodegenMember {
72
+ export interface CodegenMember {
81
73
  index: number;
82
74
  kind: 'child' | 'controller' | 'transition';
83
75
  name: string;
84
76
  originalName: string;
85
77
  type: string;
86
78
  ignored: boolean;
79
+ referencedComponent?: CodegenReferencedComponent;
87
80
  }
88
81
 
89
- interface CodegenClass {
82
+ export interface CodegenReferencedComponent {
83
+ component: Component;
84
+ package: Package;
85
+ }
86
+
87
+ export interface CodegenClass {
90
88
  classId: string;
91
89
  className: string;
92
90
  encodedClassName: string;
@@ -97,14 +95,28 @@ interface CodegenClass {
97
95
  members: CodegenMember[];
98
96
  }
99
97
 
100
- export async function publishCodeGeneration(
101
- doc: Document,
102
- options: PublishCodeGenerationOptions,
103
- ): Promise<void> {
98
+ export async function publishCodeGeneration(doc: Document, options: PublishCodeGenerationOptions): Promise<void> {
104
99
  const logger = doc.getLogger();
105
100
  const settings = resolveCodeGenerationSettings(doc);
106
101
  if (!settings.allowGenCode) return;
107
102
 
103
+ const plugins = options.plugins?.filter((plugin) => typeof plugin.plugin.genCode === 'function') ?? [];
104
+ if (plugins.length > 0) {
105
+ let handled = false;
106
+ for (const plugin of plugins) {
107
+ try {
108
+ await plugin.plugin.genCode(doc, settings, options);
109
+ handled = true;
110
+ logger.info(`publish: Generated code using plugin "${plugin.name}"`);
111
+ } catch (error) {
112
+ logger.warn(`publish: Code generation plugin "${plugin.name}" failed: ${formatPluginError(error)}`);
113
+ }
114
+ }
115
+ if (handled) {
116
+ return;
117
+ }
118
+ }
119
+
108
120
  for (const pkg of options.packages) {
109
121
  if (!pkg.getGenCode()) continue;
110
122
 
@@ -129,7 +141,7 @@ export async function publishCodeGeneration(
129
141
  }
130
142
  }
131
143
 
132
- function resolveCodeGenerationSettings(doc: Document): ResolvedCodeGenerationSettings {
144
+ export function resolveCodeGenerationSettings(doc: Document): Required<CliCodeGenerationSettings> {
133
145
  const settings = (doc.getRoot().getSettings?.() ?? {}) as RootProjectSettings;
134
146
  const publish = settings.publish ?? {};
135
147
  const codeGeneration = publish.codeGeneration as CliCodeGenerationSettings | undefined;
@@ -159,11 +171,7 @@ function resolveCodeGenerationSettings(doc: Document): ResolvedCodeGenerationSet
159
171
  };
160
172
  }
161
173
 
162
- function resolvePackageCodegenPlan(
163
- pkg: Package,
164
- settings: ResolvedCodeGenerationSettings,
165
- options: PublishCodeGenerationOptions,
166
- ): ResolvedPackageCodegenPlan | null {
174
+ export function resolvePackageCodegenPlan(pkg: Package, settings: CliCodeGenerationSettings, options: PublishCodeGenerationOptions): ResolvedPackageCodegenPlan | null {
167
175
  const rawCodePath = (pkg.getCodePath() || settings.codePath || '').trim();
168
176
  if (!rawCodePath) return null;
169
177
 
@@ -276,17 +284,11 @@ async function cleanupGeneratedFiles(directory: string, fs: PublishFileSystem, e
276
284
  }
277
285
  }
278
286
 
279
- function buildCodegenClasses(
280
- doc: Document,
281
- pkg: Package,
282
- plan: ResolvedPackageCodegenPlan,
283
- ): CodegenClass[] {
284
- const exportedComponents = pkg.listComponents()
285
- .filter((component) => component.getExported())
286
- .sort((left, right) => left.getId().localeCompare(right.getId()));
287
+ export function buildCodegenClasses(doc: Document, pkg: Package, plan: ResolvedPackageCodegenPlan): CodegenClass[] {
288
+ const codegenComponents = pkg.listComponents().sort((left, right) => left.getId().localeCompare(right.getId()));
287
289
  const generatedById = new Map<string, CodegenClass>();
288
290
 
289
- for (const component of exportedComponents) {
291
+ for (const component of codegenComponents) {
290
292
  const encodedClassName = `${plan.settings.classNamePrefix}${normalizeTypeName(component.getName()) || 'Component'}`;
291
293
  generatedById.set(component.getId(), {
292
294
  classId: component.getId(),
@@ -300,7 +302,19 @@ function buildCodegenClasses(
300
302
  });
301
303
  }
302
304
 
303
- for (const component of exportedComponents) {
305
+ for (const component of codegenComponents) {
306
+ const classInfo = generatedById.get(component.getId());
307
+ if (!classInfo) continue;
308
+ classInfo.members = buildCodegenMembers(doc, pkg, component, plan, generatedById);
309
+ }
310
+
311
+ for (const [componentId, classInfo] of generatedById) {
312
+ if (classInfo.members.every((member) => member.ignored)) {
313
+ generatedById.delete(componentId);
314
+ }
315
+ }
316
+
317
+ for (const component of codegenComponents) {
304
318
  const classInfo = generatedById.get(component.getId());
305
319
  if (!classInfo) continue;
306
320
  classInfo.members = buildCodegenMembers(doc, pkg, component, plan, generatedById);
@@ -327,13 +341,17 @@ function buildCodegenMembers(
327
341
  }
328
342
 
329
343
  for (const child of component.listChildren()) {
344
+ if (!isRuntimeChild(child)) continue;
345
+ const index = childIndex++;
346
+ const resolvedChild = resolveChildType(doc, pkg, child, generatedById);
330
347
  members.push(createMember(
331
348
  ownerType,
332
349
  'child',
333
- resolveChildType(doc, pkg, child, generatedById),
350
+ resolvedChild.type,
334
351
  child.getName(),
335
- childIndex++,
352
+ index,
336
353
  plan,
354
+ resolvedChild.referencedComponent,
337
355
  ));
338
356
  }
339
357
 
@@ -355,6 +373,10 @@ function buildCodegenMembers(
355
373
  return members;
356
374
  }
357
375
 
376
+ function isRuntimeChild(child: GObject): boolean {
377
+ return child.propertyType !== 'GGroup' || (child as GObject & { getAdvanced?(): boolean }).getAdvanced?.() === true;
378
+ }
379
+
358
380
  function createMember(
359
381
  ownerType: string,
360
382
  kind: CodegenMember['kind'],
@@ -362,6 +384,7 @@ function createMember(
362
384
  originalName: string,
363
385
  index: number,
364
386
  plan: ResolvedPackageCodegenPlan,
387
+ referencedComponent?: CodegenReferencedComponent,
365
388
  ): CodegenMember {
366
389
  const ignored = plan.settings.ignoreNoname && isDefaultMemberName(ownerType, kind, originalName);
367
390
  return {
@@ -371,43 +394,59 @@ function createMember(
371
394
  originalName,
372
395
  type,
373
396
  ignored,
397
+ referencedComponent,
374
398
  };
375
399
  }
376
400
 
401
+ interface ResolvedChildCodegenType {
402
+ type: string;
403
+ referencedComponent?: CodegenReferencedComponent;
404
+ }
405
+
377
406
  function resolveChildType(
378
407
  doc: Document,
379
408
  pkg: Package,
380
409
  child: GObject,
381
410
  generatedById: Map<string, CodegenClass>,
382
- ): string {
411
+ ): ResolvedChildCodegenType {
383
412
  const src = (child as GObject & { getSrc?(): string }).getSrc?.();
384
413
  if (src) {
385
- const localResource = resolveChildSourceComponent(doc, pkg, src);
386
- if (localResource) {
387
- return generatedById.get(localResource.getId())?.encodedClassName
388
- ?? resolveComponentBaseType(localResource);
414
+ let referencedComponent: CodegenReferencedComponent | null = null;
415
+ if (src.startsWith('ui://')) {
416
+ const rest = src.slice(5);
417
+ const pkgId = rest.slice(0, 8);
418
+ const resourceId = rest.slice(8);
419
+ const targetPackage = doc.getRoot().listPackages().find((candidate) => candidate.getId() === pkgId);
420
+ const targetResource = targetPackage?.getResourceById(resourceId);
421
+ if (targetPackage && targetResource?.propertyType === 'Component') {
422
+ referencedComponent = { component: targetResource, package: targetPackage };
423
+ }
424
+ } else {
425
+ const packageId = (child as GComponent & { getPackageId?(): string }).getPackageId?.();
426
+ const targetPackage = packageId
427
+ ? doc.getRoot().listPackages().find((candidate) => candidate.getId() === packageId)
428
+ : pkg;
429
+ const targetResource = targetPackage?.getResourceById(src);
430
+ if (targetPackage && targetResource?.propertyType === 'Component') {
431
+ referencedComponent = { component: targetResource, package: targetPackage };
432
+ }
433
+ }
434
+
435
+ if (referencedComponent) {
436
+ const localGeneratedClass = referencedComponent.package === pkg
437
+ ? generatedById.get(referencedComponent.component.getId())
438
+ : undefined;
439
+ return {
440
+ type: localGeneratedClass?.encodedClassName ?? resolveComponentBaseType(referencedComponent.component),
441
+ referencedComponent,
442
+ };
389
443
  }
390
444
  }
391
445
 
392
446
  const instanceExtType = (child as GComponent & { getInstanceExtType?(): string }).getInstanceExtType?.();
393
- if (instanceExtType) return `G${instanceExtType}`;
394
-
395
- return child.propertyType;
396
- }
397
-
398
- function resolveChildSourceComponent(doc: Document, pkg: Package, src: string): Component | null {
399
- if (!src) return null;
400
- if (src.startsWith('ui://')) {
401
- const rest = src.slice(5);
402
- const pkgId = rest.slice(0, 8);
403
- const resourceId = rest.slice(8);
404
- const targetPackage = doc.getRoot().listPackages().find((candidate) => candidate.getId() === pkgId);
405
- const targetResource = targetPackage?.getResourceById(resourceId);
406
- return targetResource?.propertyType === 'Component' ? targetResource : null;
407
- }
447
+ if (instanceExtType) return { type: `G${instanceExtType}` };
408
448
 
409
- const localResource = pkg.getResourceById(src);
410
- return localResource?.propertyType === 'Component' ? localResource : null;
449
+ return { type: child.propertyType };
411
450
  }
412
451
 
413
452
  function resolveComponentBaseType(component: Component): string {
@@ -549,7 +588,7 @@ function resolveCodePath(
549
588
  return projectBasePath ? trimTrailingSlashes(fs.join(projectBasePath, codePath)) : trimTrailingSlashes(codePath);
550
589
  }
551
590
 
552
- function resolveProjectBasePath(basePath: string | undefined): string {
591
+ export function resolveProjectBasePath(basePath: string | undefined): string {
553
592
  if (!basePath) return '';
554
593
  const normalized = trimTrailingSlashes(basePath);
555
594
  const assetsMatch = normalized.match(/^(.*)[/\\]assets(?:_[^/\\]+)?$/i);
@@ -578,13 +617,13 @@ function isDefaultMemberName(ownerType: string, kind: CodegenMember['kind'], nam
578
617
  if (kind === 'transition') return false;
579
618
 
580
619
  if (ownerType === 'GButton' || ownerType === 'GLabel' || ownerType === 'GComboBox') {
581
- return name === 'title' || name === 'icon';
620
+ if (name === 'title' || name === 'icon') return true;
582
621
  }
583
622
  if (ownerType === 'GProgressBar') {
584
- return name === 'bar' || name === 'bar_v' || name === 'title' || name === 'ani';
623
+ if (name === 'bar' || name === 'bar_v' || name === 'title' || name === 'ani') return true;
585
624
  }
586
625
  if (ownerType === 'GSlider') {
587
- return name === 'bar' || name === 'bar_v' || name === 'grip' || name === 'title' || name === 'ani';
626
+ if (name === 'bar' || name === 'bar_v' || name === 'grip' || name === 'title' || name === 'ani') return true;
588
627
  }
589
628
  return /^n\d+(?:_.*)?$/i.test(name);
590
629
  }
@@ -647,10 +686,10 @@ async function writeTextFile(fs: PublishFileSystem, filePath: string, content: s
647
686
  await fs.writeFileRaw(filePath, encodeText(content));
648
687
  }
649
688
 
650
- function encodeText(value: string): Uint8Array {
689
+ export function encodeText(value: string): Uint8Array {
651
690
  return new TextEncoder().encode(value);
652
691
  }
653
692
 
654
- function decodeText(value: Uint8Array): string {
693
+ export function decodeText(value: Uint8Array): string {
655
694
  return new TextDecoder().decode(value);
656
695
  }
package/src/index.ts CHANGED
@@ -1,9 +1,48 @@
1
1
  export { inspect, type InspectReport, type InspectCategoryReport } from './inspect.js';
2
- export { validate, type ValidateOptions, type ValidationResult, type ValidationIssue, ValidationSeverity } from './validate.js';
2
+ export {
3
+ validate,
4
+ type ValidateOptions,
5
+ type ValidationResult,
6
+ type ValidationIssue,
7
+ ValidationSeverity,
8
+ } from './validate.js';
3
9
  export { prune, type PruneOptions } from './prune.js';
4
10
  export { rename, type RenameOptions } from './rename.js';
5
11
  export { atlas, type AtlasOptions } from './atlas.js';
6
- export { publishCodeGeneration, AUTO_GENERATED_CODE_MARK, type PublishCodeGenerationOptions } from './codegen.js';
12
+ export {
13
+ publishCodeGeneration,
14
+ AUTO_GENERATED_CODE_MARK,
15
+ type PublishCodeGenerationOptions,
16
+ type ResolvedPackageCodegenPlan,
17
+ resolvePackageCodegenPlan,
18
+ buildCodegenClasses,
19
+ type CodegenClass,
20
+ type CodegenMember,
21
+ type CodegenReferencedComponent,
22
+ encodeText,
23
+ decodeText,
24
+ } from './codegen.js';
25
+
26
+ export type {
27
+ CodeWriter,
28
+ ICodeWriterConfig,
29
+ MaybePromise,
30
+ LoadedPlugin,
31
+ Plugin,
32
+ PluginManifest,
33
+ PluginModule,
34
+ } from './plugins/types.js';
35
+ export type {
36
+ AtlasRasterBackend,
37
+ AtlasRasterCompositeInput,
38
+ AtlasRasterInput,
39
+ AtlasRasterMetadata,
40
+ AtlasRasterPipeline,
41
+ AtlasRasterResolvedBuffer,
42
+ PublishFileSystem,
43
+ PublishOutputFileSystem,
44
+ PublishSourceFileSystem,
45
+ } from './publish/contracts.js';
7
46
  export {
8
47
  restore,
9
48
  type RestoreFileSystem,
@@ -24,6 +63,7 @@ export {
24
63
  } from './publish.js';
25
64
  export {
26
65
  applyUamTransactionApp,
66
+ type ApplyUamTransactionAppDiagnostic,
27
67
  type ApplyUamTransactionAppError,
28
68
  type ApplyUamTransactionAppInput,
29
69
  type ApplyUamTransactionAppResult,
@@ -37,6 +77,6 @@ export type {
37
77
  HasOptionalSrc,
38
78
  HasOptionalUrl,
39
79
  PublishDependency,
40
- PublishFileSystem,
41
80
  RootProjectSettings,
81
+ CliCodeGenerationSettings,
42
82
  } from './shared-types.js';
package/src/node.ts ADDED
@@ -0,0 +1,8 @@
1
+ export {
2
+ publishNode,
3
+ type PublishNodeOptions,
4
+ } from './adapters/node/publish.js';
5
+ export {
6
+ restoreNode,
7
+ type RestoreNodeOptions,
8
+ } from './adapters/node/restore.js';
@@ -0,0 +1,56 @@
1
+ import type { Document } from '@openfairygui/core';
2
+ import type { PublishCodeGenerationOptions } from '../codegen.js';
3
+ import type { PublishOptions } from '../publish.js';
4
+ import type { CliCodeGenerationSettings } from '../shared-types.js';
5
+
6
+ export type MaybePromise<T> = T | Promise<T>;
7
+
8
+ export interface PluginManifest {
9
+ name: string;
10
+ displayName?: string;
11
+ description?: string;
12
+ version?: string;
13
+ author?: {
14
+ name?: string;
15
+ };
16
+ icon?: string;
17
+ main: string;
18
+ }
19
+
20
+ export interface ICodeWriterConfig {
21
+ blockStart?: string;
22
+ blockEnd?: string;
23
+ blockFromNewLine?: boolean;
24
+ usingTabs?: boolean;
25
+ endOfLine?: string;
26
+ fileMark?: string;
27
+ }
28
+
29
+ export interface CodeWriter {
30
+ writeMark(): void;
31
+ writeln(fmt?: string, ...args: any[]): CodeWriter;
32
+ startBlock(): CodeWriter;
33
+ endBlock(): CodeWriter;
34
+ incIndent(): CodeWriter;
35
+ decIndent(): CodeWriter;
36
+ reset(): void;
37
+ toString(): string;
38
+ save(filePath: string): void;
39
+ }
40
+
41
+ export interface Plugin {
42
+ genCode?: (doc: Document, settings: Required<CliCodeGenerationSettings>, options: PublishCodeGenerationOptions) => MaybePromise<void>;
43
+ onPublishStart?: (doc: Document, options: PublishOptions) => MaybePromise<void>;
44
+ onPublishEnd?: (doc: Document, options: PublishOptions) => MaybePromise<void>;
45
+ }
46
+
47
+ export interface LoadedPlugin {
48
+ name: string;
49
+ plugin: Plugin;
50
+ }
51
+
52
+ export type PluginModule = Plugin & { default?: Plugin };
53
+
54
+ export function formatPluginError(error: unknown): string {
55
+ return error instanceof Error ? error.message : String(error);
56
+ }
@@ -0,0 +1,80 @@
1
+ import type { FileSystem } from '@openfairygui/core';
2
+
3
+ /**
4
+ * Source files required by a publish adapter.
5
+ *
6
+ * The host owns this filesystem: Node adapters use native paths while web
7
+ * adapters can use File System Access, OPFS, IndexedDB, ZIP, or memory.
8
+ */
9
+ export type PublishSourceFileSystem = Pick<FileSystem, 'readFileRaw' | 'join'>;
10
+
11
+ /**
12
+ * Output files required by a publish adapter.
13
+ */
14
+ export type PublishOutputFileSystem = Pick<FileSystem, 'writeFileRaw' | 'mkdir' | 'join'>;
15
+
16
+ /**
17
+ * Full filesystem contract consumed by the capability-injected publish core.
18
+ *
19
+ * Read, enumeration, and delete operations are optional because individual
20
+ * publish lanes only request them when needed.
21
+ */
22
+ export type PublishFileSystem = PublishOutputFileSystem & {
23
+ deleteFile?: (path: string) => Promise<void>;
24
+ exists?: FileSystem['exists'];
25
+ readdir?: FileSystem['readdir'];
26
+ readFileRaw?: FileSystem['readFileRaw'];
27
+ };
28
+
29
+ export interface AtlasRasterMetadata {
30
+ width?: number;
31
+ height?: number;
32
+ channels?: number;
33
+ hasAlpha?: boolean;
34
+ trimOffsetLeft?: number;
35
+ trimOffsetTop?: number;
36
+ }
37
+
38
+ export interface AtlasRasterResolvedBuffer {
39
+ data: Uint8Array;
40
+ info: Required<Pick<AtlasRasterMetadata, 'width' | 'height' | 'channels'>> & AtlasRasterMetadata;
41
+ }
42
+
43
+ export interface AtlasRasterCompositeInput {
44
+ input: Uint8Array;
45
+ left: number;
46
+ top: number;
47
+ }
48
+
49
+ export type AtlasRasterInput =
50
+ | string
51
+ | Uint8Array
52
+ | {
53
+ create: {
54
+ width: number;
55
+ height: number;
56
+ channels: 4;
57
+ background: { r: number; g: number; b: number; alpha: number };
58
+ };
59
+ };
60
+
61
+ /**
62
+ * Host-provided raster pipeline used by atlas packing.
63
+ *
64
+ * Sharp and the browser Canvas adapter both satisfy this contract.
65
+ */
66
+ export interface AtlasRasterPipeline {
67
+ ensureAlpha(): AtlasRasterPipeline;
68
+ resize(options: { width: number; height: number; fit?: 'fill' }): AtlasRasterPipeline;
69
+ raw(): AtlasRasterPipeline;
70
+ extract(options: { left: number; top: number; width: number; height: number }): AtlasRasterPipeline;
71
+ png(): AtlasRasterPipeline;
72
+ rotate(angle: number): AtlasRasterPipeline;
73
+ composite(inputs: AtlasRasterCompositeInput[]): AtlasRasterPipeline;
74
+ metadata(): Promise<AtlasRasterMetadata>;
75
+ toBuffer(options: { resolveWithObject: true }): Promise<AtlasRasterResolvedBuffer>;
76
+ toBuffer(options?: { resolveWithObject?: false }): Promise<Uint8Array>;
77
+ toFile(path: string): Promise<unknown>;
78
+ }
79
+
80
+ export type AtlasRasterBackend = (input: AtlasRasterInput) => AtlasRasterPipeline;
@@ -0,0 +1,117 @@
1
+ import type { Package } from '@openfairygui/core';
2
+ import type { PublishFileSystem } from './contracts.js';
3
+ import {
4
+ extname,
5
+ getAnnotatedExportedResourceIds,
6
+ getAnnotatedPublishedResourceIds,
7
+ getPublishedId,
8
+ getPublishedSkeletonDependencyImageIds,
9
+ isImageResource,
10
+ isMiscResource,
11
+ isSkeletonResource,
12
+ isSoundResource,
13
+ resolveGenericResourcePath,
14
+ resolveImageFileName,
15
+ resolveImagePath,
16
+ resolveSoundPath,
17
+ } from './package-context.js';
18
+
19
+ interface PublishFileExtras extends Record<string, unknown> {
20
+ _publishedFile?: string;
21
+ }
22
+
23
+ export async function exportPackageSounds(
24
+ pkg: Package,
25
+ outputDir: string,
26
+ basePath: string | undefined,
27
+ fs: PublishFileSystem,
28
+ readFileRaw: PublishFileSystem['readFileRaw'] | undefined,
29
+ ): Promise<void> {
30
+ const publishedResourceIds = getAnnotatedPublishedResourceIds(pkg);
31
+ if (publishedResourceIds.size === 0) return;
32
+ if (!basePath || !readFileRaw) {
33
+ const hasPublishedSound = pkg.listResources().some((resource) => {
34
+ return isSoundResource(resource) && publishedResourceIds.has(resource.getId());
35
+ });
36
+ if (hasPublishedSound) {
37
+ throw new Error(
38
+ `publish: Sound resources in package "${pkg.getName()}" require basePath and readFileRaw for output.`,
39
+ );
40
+ }
41
+ return;
42
+ }
43
+
44
+ for (const resource of pkg.listResources()) {
45
+ if (!isSoundResource(resource)) continue;
46
+ if (!publishedResourceIds.has(resource.getId())) continue;
47
+
48
+ const sourcePath = resolveSoundPath(resource, pkg, basePath);
49
+ const targetName = `${pkg.getPublishName() || pkg.getName()}_${getPublishedId(resource)}${extname(resource.getFile() || '')}`;
50
+ const targetPath = fs.join(outputDir, targetName);
51
+
52
+ try {
53
+ const data = await readFileRaw(sourcePath);
54
+ await fs.writeFileRaw(targetPath, data);
55
+ } catch {
56
+ throw new Error(`publish: Could not export sound "${resource.getId()}" from package "${pkg.getName()}".`);
57
+ }
58
+ }
59
+ }
60
+
61
+ export async function exportPackageExternalResources(
62
+ pkg: Package,
63
+ outputDir: string,
64
+ basePath: string | undefined,
65
+ fs: PublishFileSystem,
66
+ readFileRaw: PublishFileSystem['readFileRaw'] | undefined,
67
+ ): Promise<void> {
68
+ const exportedResourceIds = getAnnotatedExportedResourceIds(pkg);
69
+ const skeletonDependencyImageIds = getPublishedSkeletonDependencyImageIds(pkg, exportedResourceIds);
70
+ if (exportedResourceIds.size === 0) return;
71
+ if (!basePath || !readFileRaw) {
72
+ const hasPublishedExternal = pkg.listResources().some((resource) => {
73
+ return (
74
+ ((isMiscResource(resource) || isSkeletonResource(resource)) &&
75
+ exportedResourceIds.has(resource.getId())) ||
76
+ skeletonDependencyImageIds.has(resource.getId())
77
+ );
78
+ });
79
+ if (hasPublishedExternal) {
80
+ throw new Error(
81
+ `publish: External resources in package "${pkg.getName()}" require basePath and readFileRaw for output.`,
82
+ );
83
+ }
84
+ return;
85
+ }
86
+
87
+ for (const resource of pkg.listResources()) {
88
+ const resourceId = resource.getId();
89
+ const isSkeletonExternal =
90
+ exportedResourceIds.has(resourceId) && (isMiscResource(resource) || isSkeletonResource(resource));
91
+ const isSkeletonImageDependency = skeletonDependencyImageIds.has(resourceId) && isImageResource(resource);
92
+ if (!isSkeletonExternal && !isSkeletonImageDependency) continue;
93
+
94
+ let sourcePath: string;
95
+ let targetName: string;
96
+ if (isSkeletonImageDependency) {
97
+ sourcePath = resolveImagePath(resource, pkg, basePath);
98
+ targetName = resolveImageFileName(resource);
99
+ } else if (isMiscResource(resource) || isSkeletonResource(resource)) {
100
+ sourcePath = resolveGenericResourcePath(resource, pkg, basePath);
101
+ targetName =
102
+ ((resource.getExtras() as PublishFileExtras | undefined) ?? {})._publishedFile ?? resource.getFile();
103
+ } else {
104
+ continue;
105
+ }
106
+ const targetPath = fs.join(outputDir, targetName);
107
+
108
+ try {
109
+ const data = await readFileRaw(sourcePath);
110
+ await fs.writeFileRaw(targetPath, data);
111
+ } catch {
112
+ throw new Error(
113
+ `publish: Could not export external resource "${resource.getId()}" from package "${pkg.getName()}".`,
114
+ );
115
+ }
116
+ }
117
+ }