@openfairygui/functions 0.2.0-alpha.0 → 0.2.0-alpha.11

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/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,6 +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/loader.js';
15
16
  import type { CliCodeGenerationSettings, PublishFileSystem, RootProjectSettings } from './shared-types.js';
16
17
 
17
18
  export const AUTO_GENERATED_CODE_MARK = '/** This is an automatically generated class by FairyGUI. Please do not modify it. **/';
@@ -22,25 +23,15 @@ export interface PublishCodeGenerationOptions {
22
23
  basePath?: string;
23
24
  fs: PublishFileSystem;
24
25
  packages: Package[];
26
+ plugins?: LoadedPlugin[];
25
27
  }
26
28
 
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 {
29
+ export interface ResolvedPackageCodegenPlan {
39
30
  outputDir: string;
40
31
  packageFolderName: string;
41
32
  packageNamespace: string;
42
33
  binderClassName: string;
43
- settings: ResolvedCodeGenerationSettings;
34
+ settings: CliCodeGenerationSettings;
44
35
  }
45
36
 
46
37
  interface FguiTypescriptVariant {
@@ -77,16 +68,22 @@ const SHARED_FGUI_TYPESCRIPT_VARIANT: FguiTypescriptVariant = {
77
68
  runtimeNamespace: 'fgui',
78
69
  };
79
70
 
80
- interface CodegenMember {
71
+ export interface CodegenMember {
81
72
  index: number;
82
73
  kind: 'child' | 'controller' | 'transition';
83
74
  name: string;
84
75
  originalName: string;
85
76
  type: string;
86
77
  ignored: boolean;
78
+ referencedComponent?: CodegenReferencedComponent;
87
79
  }
88
80
 
89
- interface CodegenClass {
81
+ export interface CodegenReferencedComponent {
82
+ component: Component;
83
+ package: Package;
84
+ }
85
+
86
+ export interface CodegenClass {
90
87
  classId: string;
91
88
  className: string;
92
89
  encodedClassName: string;
@@ -97,14 +94,28 @@ interface CodegenClass {
97
94
  members: CodegenMember[];
98
95
  }
99
96
 
100
- export async function publishCodeGeneration(
101
- doc: Document,
102
- options: PublishCodeGenerationOptions,
103
- ): Promise<void> {
97
+ export async function publishCodeGeneration(doc: Document, options: PublishCodeGenerationOptions): Promise<void> {
104
98
  const logger = doc.getLogger();
105
99
  const settings = resolveCodeGenerationSettings(doc);
106
100
  if (!settings.allowGenCode) return;
107
101
 
102
+ const plugins = options.plugins?.filter((plugin) => typeof plugin.plugin.genCode === 'function') ?? [];
103
+ if (plugins.length > 0) {
104
+ let handled = false;
105
+ for (const plugin of plugins) {
106
+ try {
107
+ await plugin.plugin.genCode(doc, settings, options);
108
+ handled = true;
109
+ logger.info(`publish: Generated code using plugin "${plugin.name}"`);
110
+ } catch (error) {
111
+ logger.warn(`publish: Code generation plugin "${plugin.name}" failed: ${formatPluginError(error)}`);
112
+ }
113
+ }
114
+ if (handled) {
115
+ return;
116
+ }
117
+ }
118
+
108
119
  for (const pkg of options.packages) {
109
120
  if (!pkg.getGenCode()) continue;
110
121
 
@@ -129,7 +140,7 @@ export async function publishCodeGeneration(
129
140
  }
130
141
  }
131
142
 
132
- function resolveCodeGenerationSettings(doc: Document): ResolvedCodeGenerationSettings {
143
+ export function resolveCodeGenerationSettings(doc: Document): Required<CliCodeGenerationSettings> {
133
144
  const settings = (doc.getRoot().getSettings?.() ?? {}) as RootProjectSettings;
134
145
  const publish = settings.publish ?? {};
135
146
  const codeGeneration = publish.codeGeneration as CliCodeGenerationSettings | undefined;
@@ -159,11 +170,7 @@ function resolveCodeGenerationSettings(doc: Document): ResolvedCodeGenerationSet
159
170
  };
160
171
  }
161
172
 
162
- function resolvePackageCodegenPlan(
163
- pkg: Package,
164
- settings: ResolvedCodeGenerationSettings,
165
- options: PublishCodeGenerationOptions,
166
- ): ResolvedPackageCodegenPlan | null {
173
+ export function resolvePackageCodegenPlan(pkg: Package, settings: CliCodeGenerationSettings, options: PublishCodeGenerationOptions): ResolvedPackageCodegenPlan | null {
167
174
  const rawCodePath = (pkg.getCodePath() || settings.codePath || '').trim();
168
175
  if (!rawCodePath) return null;
169
176
 
@@ -276,17 +283,11 @@ async function cleanupGeneratedFiles(directory: string, fs: PublishFileSystem, e
276
283
  }
277
284
  }
278
285
 
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()));
286
+ export function buildCodegenClasses(doc: Document, pkg: Package, plan: ResolvedPackageCodegenPlan): CodegenClass[] {
287
+ const codegenComponents = pkg.listComponents().sort((left, right) => left.getId().localeCompare(right.getId()));
287
288
  const generatedById = new Map<string, CodegenClass>();
288
289
 
289
- for (const component of exportedComponents) {
290
+ for (const component of codegenComponents) {
290
291
  const encodedClassName = `${plan.settings.classNamePrefix}${normalizeTypeName(component.getName()) || 'Component'}`;
291
292
  generatedById.set(component.getId(), {
292
293
  classId: component.getId(),
@@ -300,7 +301,19 @@ function buildCodegenClasses(
300
301
  });
301
302
  }
302
303
 
303
- for (const component of exportedComponents) {
304
+ for (const component of codegenComponents) {
305
+ const classInfo = generatedById.get(component.getId());
306
+ if (!classInfo) continue;
307
+ classInfo.members = buildCodegenMembers(doc, pkg, component, plan, generatedById);
308
+ }
309
+
310
+ for (const [componentId, classInfo] of generatedById) {
311
+ if (classInfo.members.every((member) => member.ignored)) {
312
+ generatedById.delete(componentId);
313
+ }
314
+ }
315
+
316
+ for (const component of codegenComponents) {
304
317
  const classInfo = generatedById.get(component.getId());
305
318
  if (!classInfo) continue;
306
319
  classInfo.members = buildCodegenMembers(doc, pkg, component, plan, generatedById);
@@ -327,13 +340,17 @@ function buildCodegenMembers(
327
340
  }
328
341
 
329
342
  for (const child of component.listChildren()) {
343
+ if (!isRuntimeChild(child)) continue;
344
+ const index = childIndex++;
345
+ const resolvedChild = resolveChildType(doc, pkg, child, generatedById);
330
346
  members.push(createMember(
331
347
  ownerType,
332
348
  'child',
333
- resolveChildType(doc, pkg, child, generatedById),
349
+ resolvedChild.type,
334
350
  child.getName(),
335
- childIndex++,
351
+ index,
336
352
  plan,
353
+ resolvedChild.referencedComponent,
337
354
  ));
338
355
  }
339
356
 
@@ -355,6 +372,10 @@ function buildCodegenMembers(
355
372
  return members;
356
373
  }
357
374
 
375
+ function isRuntimeChild(child: GObject): boolean {
376
+ return child.propertyType !== 'GGroup' || (child as GObject & { getAdvanced?(): boolean }).getAdvanced?.() === true;
377
+ }
378
+
358
379
  function createMember(
359
380
  ownerType: string,
360
381
  kind: CodegenMember['kind'],
@@ -362,6 +383,7 @@ function createMember(
362
383
  originalName: string,
363
384
  index: number,
364
385
  plan: ResolvedPackageCodegenPlan,
386
+ referencedComponent?: CodegenReferencedComponent,
365
387
  ): CodegenMember {
366
388
  const ignored = plan.settings.ignoreNoname && isDefaultMemberName(ownerType, kind, originalName);
367
389
  return {
@@ -371,43 +393,59 @@ function createMember(
371
393
  originalName,
372
394
  type,
373
395
  ignored,
396
+ referencedComponent,
374
397
  };
375
398
  }
376
399
 
400
+ interface ResolvedChildCodegenType {
401
+ type: string;
402
+ referencedComponent?: CodegenReferencedComponent;
403
+ }
404
+
377
405
  function resolveChildType(
378
406
  doc: Document,
379
407
  pkg: Package,
380
408
  child: GObject,
381
409
  generatedById: Map<string, CodegenClass>,
382
- ): string {
410
+ ): ResolvedChildCodegenType {
383
411
  const src = (child as GObject & { getSrc?(): string }).getSrc?.();
384
412
  if (src) {
385
- const localResource = resolveChildSourceComponent(doc, pkg, src);
386
- if (localResource) {
387
- return generatedById.get(localResource.getId())?.encodedClassName
388
- ?? resolveComponentBaseType(localResource);
413
+ let referencedComponent: CodegenReferencedComponent | null = null;
414
+ if (src.startsWith('ui://')) {
415
+ const rest = src.slice(5);
416
+ const pkgId = rest.slice(0, 8);
417
+ const resourceId = rest.slice(8);
418
+ const targetPackage = doc.getRoot().listPackages().find((candidate) => candidate.getId() === pkgId);
419
+ const targetResource = targetPackage?.getResourceById(resourceId);
420
+ if (targetPackage && targetResource?.propertyType === 'Component') {
421
+ referencedComponent = { component: targetResource, package: targetPackage };
422
+ }
423
+ } else {
424
+ const packageId = (child as GComponent & { getPackageId?(): string }).getPackageId?.();
425
+ const targetPackage = packageId
426
+ ? doc.getRoot().listPackages().find((candidate) => candidate.getId() === packageId)
427
+ : pkg;
428
+ const targetResource = targetPackage?.getResourceById(src);
429
+ if (targetPackage && targetResource?.propertyType === 'Component') {
430
+ referencedComponent = { component: targetResource, package: targetPackage };
431
+ }
432
+ }
433
+
434
+ if (referencedComponent) {
435
+ const localGeneratedClass = referencedComponent.package === pkg
436
+ ? generatedById.get(referencedComponent.component.getId())
437
+ : undefined;
438
+ return {
439
+ type: localGeneratedClass?.encodedClassName ?? resolveComponentBaseType(referencedComponent.component),
440
+ referencedComponent,
441
+ };
389
442
  }
390
443
  }
391
444
 
392
445
  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
- }
446
+ if (instanceExtType) return { type: `G${instanceExtType}` };
408
447
 
409
- const localResource = pkg.getResourceById(src);
410
- return localResource?.propertyType === 'Component' ? localResource : null;
448
+ return { type: child.propertyType };
411
449
  }
412
450
 
413
451
  function resolveComponentBaseType(component: Component): string {
@@ -549,7 +587,7 @@ function resolveCodePath(
549
587
  return projectBasePath ? trimTrailingSlashes(fs.join(projectBasePath, codePath)) : trimTrailingSlashes(codePath);
550
588
  }
551
589
 
552
- function resolveProjectBasePath(basePath: string | undefined): string {
590
+ export function resolveProjectBasePath(basePath: string | undefined): string {
553
591
  if (!basePath) return '';
554
592
  const normalized = trimTrailingSlashes(basePath);
555
593
  const assetsMatch = normalized.match(/^(.*)[/\\]assets(?:_[^/\\]+)?$/i);
@@ -578,13 +616,13 @@ function isDefaultMemberName(ownerType: string, kind: CodegenMember['kind'], nam
578
616
  if (kind === 'transition') return false;
579
617
 
580
618
  if (ownerType === 'GButton' || ownerType === 'GLabel' || ownerType === 'GComboBox') {
581
- return name === 'title' || name === 'icon';
619
+ if (name === 'title' || name === 'icon') return true;
582
620
  }
583
621
  if (ownerType === 'GProgressBar') {
584
- return name === 'bar' || name === 'bar_v' || name === 'title' || name === 'ani';
622
+ if (name === 'bar' || name === 'bar_v' || name === 'title' || name === 'ani') return true;
585
623
  }
586
624
  if (ownerType === 'GSlider') {
587
- return name === 'bar' || name === 'bar_v' || name === 'grip' || name === 'title' || name === 'ani';
625
+ if (name === 'bar' || name === 'bar_v' || name === 'grip' || name === 'title' || name === 'ani') return true;
588
626
  }
589
627
  return /^n\d+(?:_.*)?$/i.test(name);
590
628
  }
@@ -647,10 +685,10 @@ async function writeTextFile(fs: PublishFileSystem, filePath: string, content: s
647
685
  await fs.writeFileRaw(filePath, encodeText(content));
648
686
  }
649
687
 
650
- function encodeText(value: string): Uint8Array {
688
+ export function encodeText(value: string): Uint8Array {
651
689
  return new TextEncoder().encode(value);
652
690
  }
653
691
 
654
- function decodeText(value: Uint8Array): string {
692
+ export function decodeText(value: Uint8Array): string {
655
693
  return new TextDecoder().decode(value);
656
694
  }
package/src/index.ts CHANGED
@@ -3,7 +3,29 @@ export { validate, type ValidateOptions, type ValidationResult, type ValidationI
3
3
  export { prune, type PruneOptions } from './prune.js';
4
4
  export { rename, type RenameOptions } from './rename.js';
5
5
  export { atlas, type AtlasOptions } from './atlas.js';
6
- export { publishCodeGeneration, AUTO_GENERATED_CODE_MARK, type PublishCodeGenerationOptions } from './codegen.js';
6
+ export {
7
+ publishCodeGeneration,
8
+ AUTO_GENERATED_CODE_MARK,
9
+ type PublishCodeGenerationOptions,
10
+ type ResolvedPackageCodegenPlan,
11
+ resolvePackageCodegenPlan,
12
+ buildCodegenClasses,
13
+ type CodegenClass,
14
+ type CodegenMember,
15
+ type CodegenReferencedComponent,
16
+ encodeText,
17
+ decodeText,
18
+ } from './codegen.js';
19
+
20
+
21
+ export type {
22
+ CodeWriter,
23
+ ICodeWriterConfig,
24
+ MaybePromise,
25
+ Plugin,
26
+ PluginManifest,
27
+ PluginModule,
28
+ } from './plugins/types.js';
7
29
  export {
8
30
  restore,
9
31
  type RestoreFileSystem,
@@ -24,6 +46,7 @@ export {
24
46
  } from './publish.js';
25
47
  export {
26
48
  applyUamTransactionApp,
49
+ type ApplyUamTransactionAppDiagnostic,
27
50
  type ApplyUamTransactionAppError,
28
51
  type ApplyUamTransactionAppInput,
29
52
  type ApplyUamTransactionAppResult,
@@ -39,4 +62,5 @@ export type {
39
62
  PublishDependency,
40
63
  PublishFileSystem,
41
64
  RootProjectSettings,
65
+ CliCodeGenerationSettings,
42
66
  } from './shared-types.js';
@@ -0,0 +1,85 @@
1
+ import type { Document } from '@openfairygui/core';
2
+ import { createJiti } from 'jiti';
3
+ import type { Plugin, PluginManifest, PluginModule } from './types.js';
4
+
5
+ export interface LoadedPlugin {
6
+ name: string;
7
+ plugin: Plugin;
8
+ }
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
+ export function formatPluginError(error: unknown): string {
50
+ return error instanceof Error ? error.message : String(error);
51
+ }
52
+
53
+ async function readPluginManifest(
54
+ fs: typeof import('node:fs/promises'),
55
+ path: typeof import('node:path'),
56
+ pluginDir: string,
57
+ ): Promise<PluginPackageJson | null> {
58
+ const manifestPath = path.join(pluginDir, 'package.json');
59
+ const content = await fs.readFile(manifestPath, 'utf-8');
60
+ const manifest = JSON.parse(content) as PluginPackageJson;
61
+ if (!manifest.name) throw new Error(`Codegen plugin at ${pluginDir} is missing package.json name.`);
62
+ if (!manifest.main) throw new Error(`Codegen plugin "${manifest.name}" is missing package.json main.`);
63
+ return manifest;
64
+ }
65
+
66
+ function resolvePluginMain(path: typeof import('node:path'), pluginDir: string, manifest: PluginPackageJson): string {
67
+ const mainPath = path.resolve(pluginDir, manifest.main!);
68
+ const relative = path.relative(pluginDir, mainPath);
69
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
70
+ throw new Error(`Codegen plugin "${manifest.name}" main must resolve inside its plugin directory.`);
71
+ }
72
+ return mainPath;
73
+ }
74
+
75
+ async function loadPlugin(mainPath: string): Promise<Plugin> {
76
+ const jiti = createJiti(import.meta.url);
77
+ const mod = await jiti.import<PluginModule>(mainPath);
78
+ const defaultExport = mod.default;
79
+ const plugin = isObject(defaultExport) ? defaultExport : mod;
80
+ return plugin as Plugin;
81
+ }
82
+
83
+ function isObject(value: unknown): value is Record<string, unknown> {
84
+ return value !== null && typeof value === 'object';
85
+ }
@@ -0,0 +1,47 @@
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 type PluginModule = Plugin & { default?: Plugin };