@effindomv2/fui-as 0.1.37 → 0.1.41

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 (119) hide show
  1. package/browser/routed-app-conventions.ts +1 -1
  2. package/browser/src/common-harness.ts +1 -27
  3. package/browser/src/index.ts +19 -26
  4. package/browser/src/routed-harness.ts +1 -194
  5. package/browser/src/shared-browser.ts +3 -0
  6. package/package.json +23 -13
  7. package/scripts/build.sh +70 -46
  8. package/scripts/check-runtime-dependency.sh +0 -19
  9. package/scripts/framework-host-services.ts +1 -1
  10. package/scripts/generate-host-events.ts +15 -241
  11. package/scripts/generate-host-services.ts +13 -261
  12. package/scripts/hostgen/assemblyscript-host-events.ts +165 -0
  13. package/scripts/hostgen/assemblyscript-host-services.ts +207 -0
  14. package/scripts/hostgen/common.ts +73 -0
  15. package/scripts/hostgen/registry.ts +159 -0
  16. package/src/Fui.ts +65 -5
  17. package/src/FuiExports.ts +12 -1
  18. package/src/bindings/ui.ts +157 -8
  19. package/src/controls/Button.ts +98 -71
  20. package/src/controls/Checkbox.ts +23 -21
  21. package/src/controls/ComboBox.ts +1201 -0
  22. package/src/controls/ContextMenu.ts +279 -165
  23. package/src/controls/Dialog.ts +91 -17
  24. package/src/controls/Dropdown.ts +101 -333
  25. package/src/controls/NavLink.ts +29 -6
  26. package/src/controls/Popup.ts +10 -5
  27. package/src/controls/ProgressBar.ts +1 -2
  28. package/src/controls/RadioButton.ts +34 -13
  29. package/src/controls/RadioGroup.ts +32 -19
  30. package/src/controls/Slider.ts +21 -23
  31. package/src/controls/Switch.ts +34 -17
  32. package/src/controls/index.ts +2 -1
  33. package/src/controls/internal/ButtonPresenter.ts +5 -2
  34. package/src/controls/internal/CheckboxIndicatorPresenter.ts +10 -10
  35. package/src/controls/internal/DropdownChevronPresenter.ts +3 -3
  36. package/src/controls/internal/DropdownFieldPresenter.ts +6 -6
  37. package/src/controls/internal/DropdownOptionRowPresenter.ts +5 -4
  38. package/src/controls/internal/PressableLabeledControl.ts +4 -7
  39. package/src/controls/internal/RadioIndicatorPresenter.ts +7 -7
  40. package/src/controls/internal/SelectablePopupList.ts +463 -0
  41. package/src/controls/internal/SliderPresenter.ts +5 -5
  42. package/src/controls/internal/SwitchIndicatorPresenter.ts +63 -17
  43. package/src/controls/internal/TextInputCore.ts +307 -41
  44. package/src/controls/internal/TextInputPresenter.ts +2 -2
  45. package/src/controls/templating.ts +10 -0
  46. package/src/core/Application.ts +17 -8
  47. package/src/core/Bitmap.ts +192 -3
  48. package/src/core/ContextMenuManager.ts +128 -44
  49. package/src/core/DebugApi.ts +17 -2
  50. package/src/core/DragGesture.ts +8 -7
  51. package/src/core/EventRouter.ts +332 -12
  52. package/src/core/Fetch.ts +15 -7
  53. package/src/core/FetchFfi.ts +1 -15
  54. package/src/core/File.ts +75 -34
  55. package/src/core/FocusAdornerManager.ts +3 -3
  56. package/src/core/FocusVisibility.ts +7 -0
  57. package/src/core/FrameScheduler.ts +104 -5
  58. package/src/core/ImageSampling.ts +43 -0
  59. package/src/core/Logger.ts +2 -0
  60. package/src/core/MobileTextSelectionToolbarManager.ts +901 -0
  61. package/src/core/Node.ts +1039 -97
  62. package/src/core/Platform.ts +16 -16
  63. package/src/core/SelectionHandleAdornerManager.ts +697 -0
  64. package/src/core/Theme.ts +34 -20
  65. package/src/core/ToolTipManager.ts +3 -3
  66. package/src/core/Typography.ts +411 -19
  67. package/src/core/Worker.ts +69 -33
  68. package/src/core/event_exports.ts +123 -5
  69. package/src/core/ffi.ts +5 -865
  70. package/src/core/generated/FetchHostAbi.ts +10 -0
  71. package/src/core/generated/FrameworkHostServices.ts +1 -1
  72. package/src/core/generated/HostAbi.ts +220 -0
  73. package/src/core/generated/UiAbi.ts +356 -0
  74. package/src/core/generated/UiEnums.ts +186 -0
  75. package/src/drawing/DrawCallback.ts +17 -1
  76. package/src/drawing/DrawContext.ts +140 -32
  77. package/src/nodes/CustomDrawable.ts +8 -1
  78. package/src/nodes/FlexBox.ts +40 -11
  79. package/src/nodes/Grid.ts +5 -5
  80. package/src/nodes/Image.ts +20 -1
  81. package/src/nodes/RichText.ts +36 -25
  82. package/src/nodes/ScrollBar.ts +2 -1
  83. package/src/nodes/ScrollBox.ts +7 -2
  84. package/src/nodes/ScrollView.ts +17 -6
  85. package/src/nodes/Svg.ts +18 -1
  86. package/src/nodes/Text.ts +1 -1
  87. package/src/nodes/TextCore.ts +115 -20
  88. package/src/nodes/index.ts +1 -1
  89. package/src/text/DynamicTextLayout.ts +275 -0
  90. package/src/text/TextLayout.ts +308 -0
  91. package/src/worker/Worker.ts +3 -36
  92. package/src/worker/WorkerJob.ts +5 -9
  93. package/src/worker/ffi.ts +0 -6
  94. package/browser/src/common-harness/host-imports.ts +0 -447
  95. package/browser/src/common-harness/interop.ts +0 -39
  96. package/browser/src/common-harness/managed-harness-bitmap-host.ts +0 -92
  97. package/browser/src/common-harness/managed-harness-canvas-host.ts +0 -72
  98. package/browser/src/common-harness/managed-harness-fetch-host.ts +0 -201
  99. package/browser/src/common-harness/managed-harness-file-host.ts +0 -1066
  100. package/browser/src/common-harness/managed-harness-file-payloads.ts +0 -143
  101. package/browser/src/common-harness/managed-harness-file-types.ts +0 -71
  102. package/browser/src/common-harness/managed-harness-session.ts +0 -15
  103. package/browser/src/common-harness/managed-harness.ts +0 -1472
  104. package/browser/src/common-harness/managed-history.ts +0 -168
  105. package/browser/src/common-harness/persisted-restore-policy.ts +0 -50
  106. package/browser/src/common-harness/persisted-ui-state-controller.ts +0 -309
  107. package/browser/src/common-harness/text-session-bridge.ts +0 -452
  108. package/browser/src/common-harness/types.ts +0 -206
  109. package/browser/src/common-harness/ui-chrome.ts +0 -206
  110. package/browser/src/common-harness/ui-imports.ts +0 -559
  111. package/browser/src/common-harness/wasm-module-cache.ts +0 -47
  112. package/browser/src/file-processing-worker.ts +0 -89
  113. package/browser/src/host-events.ts +0 -118
  114. package/browser/src/host-services.ts +0 -483
  115. package/browser/src/persisted-ui-state.ts +0 -206
  116. package/browser/src/routed-app-conventions.ts +0 -292
  117. package/browser/src/worker-bootstrap.ts +0 -916
  118. package/browser/src/worker-manager.ts +0 -234
  119. package/browser/src/worker-types.ts +0 -68
@@ -0,0 +1,165 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { listHostEventMethods, type HostServiceTypeName } from "./registry";
4
+ import { loadModuleExport, relativeImport, sourcePathForHeader } from "./common";
5
+
6
+ function asTypeName(type: HostServiceTypeName): string {
7
+ switch (type) {
8
+ case "string":
9
+ return "string";
10
+ case "bool":
11
+ return "bool";
12
+ case "i32":
13
+ return "i32";
14
+ case "u32":
15
+ return "u32";
16
+ case "i64":
17
+ return "i64";
18
+ case "u64":
19
+ return "u64";
20
+ case "f64":
21
+ return "f64";
22
+ case "bytes":
23
+ return "Uint8Array";
24
+ case "i32_array":
25
+ return "Int32Array";
26
+ case "u32_array":
27
+ return "Uint32Array";
28
+ case "i64_array":
29
+ return "Int64Array";
30
+ case "u64_array":
31
+ return "Uint64Array";
32
+ case "f64_array":
33
+ return "Float64Array";
34
+ case "void":
35
+ return "void";
36
+ }
37
+ }
38
+
39
+ function callbackTypeFor(argTypes: readonly HostServiceTypeName[]): string {
40
+ if (argTypes.length === 0) {
41
+ return "Callback0";
42
+ }
43
+ if (argTypes.length === 1) {
44
+ return `Callback1<${asTypeName(argTypes[0])}>`;
45
+ }
46
+ if (argTypes.length === 2) {
47
+ return `Callback2<${asTypeName(argTypes[0])}, ${asTypeName(argTypes[1])}>`;
48
+ }
49
+ throw new Error(`Host event uses ${String(argTypes.length)} args; only 0-2 are supported right now.`);
50
+ }
51
+
52
+ function emitExportArgs(argTypes: readonly HostServiceTypeName[]): string {
53
+ const parts: string[] = [];
54
+ argTypes.forEach((type, index) => {
55
+ if (
56
+ type === "string" ||
57
+ type === "bytes" ||
58
+ type === "i32_array" ||
59
+ type === "u32_array" ||
60
+ type === "i64_array" ||
61
+ type === "u64_array" ||
62
+ type === "f64_array"
63
+ ) {
64
+ parts.push(`arg${String(index)}Ptr: usize`, `arg${String(index)}Len: u32`);
65
+ return;
66
+ }
67
+ parts.push(`arg${String(index)}: ${asTypeName(type)}`);
68
+ });
69
+ return parts.join(", ");
70
+ }
71
+
72
+ function emitDecodedArgs(argTypes: readonly HostServiceTypeName[]): string[] {
73
+ const lines: string[] = [];
74
+ argTypes.forEach((type, index) => {
75
+ if (type === "string") {
76
+ lines.push(
77
+ ` const arg${String(index)} = arg${String(index)}Len == 0 ? "" : String.UTF8.decodeUnsafe(arg${String(index)}Ptr, <usize>arg${String(index)}Len, false);`,
78
+ );
79
+ return;
80
+ }
81
+ if (type === "bytes") {
82
+ lines.push(` const arg${String(index)} = new Uint8Array(<i32>arg${String(index)}Len);`);
83
+ lines.push(` if (arg${String(index)}Len > 0) {`);
84
+ lines.push(` memory.copy(arg${String(index)}.dataStart, arg${String(index)}Ptr, <usize>arg${String(index)}Len);`);
85
+ lines.push(" }");
86
+ return;
87
+ }
88
+ if (type === "i32_array" || type === "u32_array") {
89
+ const arrayType = type === "i32_array" ? "Int32Array" : "Uint32Array";
90
+ lines.push(` const arg${String(index)} = new ${arrayType}(<i32>arg${String(index)}Len);`);
91
+ lines.push(` if (arg${String(index)}Len > 0) {`);
92
+ lines.push(` memory.copy(arg${String(index)}.dataStart, arg${String(index)}Ptr, <usize>arg${String(index)}Len << 2);`);
93
+ lines.push(" }");
94
+ return;
95
+ }
96
+ if (type === "i64_array" || type === "u64_array" || type === "f64_array") {
97
+ const arrayType =
98
+ type === "i64_array" ? "Int64Array" : type === "u64_array" ? "Uint64Array" : "Float64Array";
99
+ lines.push(` const arg${String(index)} = new ${arrayType}(<i32>arg${String(index)}Len);`);
100
+ lines.push(` if (arg${String(index)}Len > 0) {`);
101
+ lines.push(` memory.copy(arg${String(index)}.dataStart, arg${String(index)}Ptr, <usize>arg${String(index)}Len << 3);`);
102
+ lines.push(" }");
103
+ return;
104
+ }
105
+ });
106
+ return lines;
107
+ }
108
+
109
+ export async function generateAssemblyScriptHostEventsFile(
110
+ modulePath: string,
111
+ exportName: string,
112
+ outputPath: string,
113
+ primitivesImportOverride: string | undefined,
114
+ ): Promise<void> {
115
+ const registry = await loadModuleExport(modulePath, exportName, "fui-host-events-");
116
+ const methods = listHostEventMethods(registry as never);
117
+ const callbackImport =
118
+ primitivesImportOverride ??
119
+ relativeImport(outputPath, path.resolve(path.dirname(modulePath), "..", "..", "src/FuiPrimitives.ts"));
120
+ const blocks: string[] = [
121
+ "// @ts-nocheck",
122
+ `// Generated by hostgen from ${sourcePathForHeader(modulePath)}#${exportName}.`,
123
+ `import { Callback0, Callback1, Callback2 } from "${callbackImport}";`,
124
+ "",
125
+ ];
126
+ methods.forEach((method, index) => {
127
+ const callbackType = callbackTypeFor(method.args);
128
+ const exportArgs = emitExportArgs(method.args);
129
+ const decodedArgs = emitDecodedArgs(method.args);
130
+ const publicName =
131
+ method.eventName.length === 0
132
+ ? method.eventName
133
+ : `${method.eventName.charAt(0).toUpperCase()}${method.eventName.slice(1)}`;
134
+ blocks.push(`let __${method.eventName}Handler: ${callbackType} | null = null;`);
135
+ blocks.push("");
136
+ blocks.push(`export function on${publicName}(callback: ${callbackType} | null): void {`);
137
+ blocks.push(` __${method.eventName}Handler = callback;`);
138
+ blocks.push("}");
139
+ blocks.push("");
140
+ blocks.push(`export function clear${publicName}(): void {`);
141
+ blocks.push(` __${method.eventName}Handler = null;`);
142
+ blocks.push("}");
143
+ blocks.push("");
144
+ blocks.push(`export function ${method.exportName}(${exportArgs}): void {`);
145
+ blocks.push(` const callback = __${method.eventName}Handler;`);
146
+ blocks.push(" if (callback === null) {");
147
+ blocks.push(" return;");
148
+ blocks.push(" }");
149
+ blocks.push(...decodedArgs);
150
+ if (method.args.length === 0) {
151
+ blocks.push(" callback.invoke();");
152
+ } else {
153
+ blocks.push(
154
+ ` callback.invoke(${method.args.map((_type, index) => `arg${String(index)}`).join(", ")});`,
155
+ );
156
+ }
157
+ blocks.push("}");
158
+ if (index + 1 < methods.length) {
159
+ blocks.push("");
160
+ blocks.push("");
161
+ }
162
+ });
163
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
164
+ await fs.writeFile(outputPath, `${blocks.join("\n")}\n`, "utf8");
165
+ }
@@ -0,0 +1,207 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { listHostServiceMethods, type HostServiceTypeName } from "./registry";
4
+ import { loadModuleExport, relativeImport, sourcePathForHeader } from "./common";
5
+
6
+ function asTypeName(type: HostServiceTypeName): string {
7
+ switch (type) {
8
+ case "string":
9
+ return "string";
10
+ case "bool":
11
+ return "bool";
12
+ case "i32":
13
+ return "i32";
14
+ case "u32":
15
+ return "u32";
16
+ case "i64":
17
+ return "i64";
18
+ case "u64":
19
+ return "u64";
20
+ case "f64":
21
+ return "f64";
22
+ case "bytes":
23
+ return "Uint8Array";
24
+ case "i32_array":
25
+ return "Int32Array";
26
+ case "u32_array":
27
+ return "Uint32Array";
28
+ case "i64_array":
29
+ return "Int64Array";
30
+ case "u64_array":
31
+ return "Uint64Array";
32
+ case "f64_array":
33
+ return "Float64Array";
34
+ case "void":
35
+ return "void";
36
+ }
37
+ }
38
+
39
+ function isPointerLengthType(type: HostServiceTypeName): boolean {
40
+ return (
41
+ type === "string" ||
42
+ type === "bytes" ||
43
+ type === "i32_array" ||
44
+ type === "u32_array" ||
45
+ type === "i64_array" ||
46
+ type === "u64_array" ||
47
+ type === "f64_array"
48
+ );
49
+ }
50
+
51
+ function returnsBufferType(type: HostServiceTypeName): boolean {
52
+ return isPointerLengthType(type);
53
+ }
54
+
55
+ function emitExternalSignature(
56
+ importName: string,
57
+ args: readonly HostServiceTypeName[],
58
+ returns: HostServiceTypeName,
59
+ moduleName: string,
60
+ ): string {
61
+ const signatureParts: string[] = [];
62
+ args.forEach((type, index) => {
63
+ if (isPointerLengthType(type)) {
64
+ signatureParts.push(`arg${String(index)}Ptr: usize`, `arg${String(index)}Len: u32`);
65
+ return;
66
+ }
67
+ signatureParts.push(`arg${String(index)}: ${asTypeName(type)}`);
68
+ });
69
+ if (returnsBufferType(returns)) {
70
+ signatureParts.push("resultPtr: usize", "resultCap: u32");
71
+ }
72
+ const returnType = returnsBufferType(returns) ? "u32" : asTypeName(returns);
73
+ return [
74
+ `@external("${moduleName}", "${importName}")`,
75
+ `declare function __host_${importName}(${signatureParts.join(", ")}): ${returnType};`,
76
+ ].join("\n");
77
+ }
78
+
79
+ function emitWrapper(
80
+ importName: string,
81
+ args: readonly HostServiceTypeName[],
82
+ returns: HostServiceTypeName,
83
+ ): string {
84
+ const wrapperArgs = args.map((type, index) => `arg${String(index)}: ${asTypeName(type)}`);
85
+ const lines: string[] = [];
86
+ args.forEach((type, index) => {
87
+ if (type === "string") {
88
+ lines.push(
89
+ ` const arg${String(index)}Bytes = Uint8Array.wrap(String.UTF8.encode(arg${String(index)}, false));`,
90
+ );
91
+ return;
92
+ }
93
+ if (
94
+ type === "bytes" ||
95
+ type === "i32_array" ||
96
+ type === "u32_array" ||
97
+ type === "i64_array" ||
98
+ type === "u64_array" ||
99
+ type === "f64_array"
100
+ ) {
101
+ lines.push(` const arg${String(index)}Bytes = arg${String(index)};`);
102
+ }
103
+ });
104
+ const callArgs: string[] = [];
105
+ args.forEach((type, index) => {
106
+ if (type === "string" || type === "bytes") {
107
+ callArgs.push(`arg${String(index)}Bytes.length > 0 ? arg${String(index)}Bytes.dataStart : 0`);
108
+ callArgs.push(`<u32>arg${String(index)}Bytes.length`);
109
+ return;
110
+ }
111
+ if (
112
+ type === "i32_array" ||
113
+ type === "u32_array" ||
114
+ type === "i64_array" ||
115
+ type === "u64_array" ||
116
+ type === "f64_array"
117
+ ) {
118
+ callArgs.push(`arg${String(index)}Bytes.length > 0 ? arg${String(index)}Bytes.dataStart : 0`);
119
+ callArgs.push(`<u32>arg${String(index)}Bytes.length`);
120
+ return;
121
+ }
122
+ callArgs.push(`arg${String(index)}`);
123
+ });
124
+ if (returnsBufferType(returns)) {
125
+ lines.push(" const resultPtr = hostServiceResultBufferPtr();");
126
+ lines.push(" const resultCap = hostServiceResultBufferSize();");
127
+ callArgs.push("resultPtr", "resultCap");
128
+ lines.push(` const resultLen = __host_${importName}(${callArgs.join(", ")});`);
129
+ if (returns === "string") {
130
+ lines.push(` return decodeHostServiceStringResult(resultPtr, resultLen, "${importName}");`);
131
+ } else if (returns === "bytes") {
132
+ lines.push(` return decodeHostServiceBytesResult(resultPtr, resultLen, "${importName}");`);
133
+ } else if (returns === "i32_array") {
134
+ lines.push(` return decodeHostServiceI32ArrayResult(resultPtr, resultLen, "${importName}");`);
135
+ } else if (returns === "u32_array") {
136
+ lines.push(` return decodeHostServiceU32ArrayResult(resultPtr, resultLen, "${importName}");`);
137
+ } else if (returns === "i64_array") {
138
+ lines.push(` return decodeHostServiceI64ArrayResult(resultPtr, resultLen, "${importName}");`);
139
+ } else if (returns === "u64_array") {
140
+ lines.push(` return decodeHostServiceU64ArrayResult(resultPtr, resultLen, "${importName}");`);
141
+ } else {
142
+ lines.push(` return decodeHostServiceF64ArrayResult(resultPtr, resultLen, "${importName}");`);
143
+ }
144
+ } else if (returns === "void") {
145
+ lines.push(` __host_${importName}(${callArgs.join(", ")});`);
146
+ } else {
147
+ lines.push(` return __host_${importName}(${callArgs.join(", ")});`);
148
+ }
149
+ return [
150
+ `export function ${importName}(${wrapperArgs.join(", ")}): ${returns === "string" ? "string" : asTypeName(returns)} {`,
151
+ ...lines,
152
+ "}",
153
+ ].join("\n");
154
+ }
155
+
156
+ export async function generateAssemblyScriptHostServicesFile(
157
+ modulePath: string,
158
+ exportName: string,
159
+ outputPath: string,
160
+ primitivesImportOverride: string | undefined,
161
+ moduleName: string,
162
+ ): Promise<void> {
163
+ const registry = await loadModuleExport(modulePath, exportName, "fui-host-services-");
164
+ const methods = listHostServiceMethods(registry as never);
165
+ const runtimeImport =
166
+ primitivesImportOverride ??
167
+ relativeImport(outputPath, path.resolve(path.dirname(modulePath), "..", "..", "src/FuiPrimitives.ts"));
168
+ const blocks: string[] = [
169
+ "// @ts-nocheck",
170
+ `// Generated by hostgen from ${sourcePathForHeader(modulePath)}#${exportName}.`,
171
+ ];
172
+ if (methods.some((method) => returnsBufferType(method.returns))) {
173
+ const helpers = [
174
+ "hostServiceResultBufferPtr",
175
+ "hostServiceResultBufferSize",
176
+ ...new Set(
177
+ methods
178
+ .map((method) => method.returns)
179
+ .filter((type) => returnsBufferType(type))
180
+ .map((type) => {
181
+ if (type === "string") return "decodeHostServiceStringResult";
182
+ if (type === "bytes") return "decodeHostServiceBytesResult";
183
+ if (type === "i32_array") return "decodeHostServiceI32ArrayResult";
184
+ if (type === "u32_array") return "decodeHostServiceU32ArrayResult";
185
+ if (type === "i64_array") return "decodeHostServiceI64ArrayResult";
186
+ if (type === "u64_array") return "decodeHostServiceU64ArrayResult";
187
+ return "decodeHostServiceF64ArrayResult";
188
+ }),
189
+ ),
190
+ ];
191
+ blocks.push(`import { ${helpers.join(", ")} } from "${runtimeImport}";`);
192
+ blocks.push("");
193
+ } else {
194
+ blocks.push("");
195
+ }
196
+ methods.forEach((method, index) => {
197
+ blocks.push(emitExternalSignature(method.importName, method.args, method.returns, moduleName));
198
+ blocks.push("");
199
+ blocks.push(emitWrapper(method.importName, method.args, method.returns));
200
+ if (index + 1 < methods.length) {
201
+ blocks.push("");
202
+ }
203
+ });
204
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
205
+ await fs.writeFile(outputPath, `${blocks.join("\n")}\n`, "utf8");
206
+ }
207
+
@@ -0,0 +1,73 @@
1
+ import { build } from "esbuild";
2
+ import { promises as fs } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ export function toPosix(value: string): string {
8
+ return value.split(path.sep).join("/");
9
+ }
10
+
11
+ export function relativeImport(fromFile: string, targetFile: string): string {
12
+ let relative = path.relative(path.dirname(fromFile), targetFile);
13
+ relative = relative.replace(/\.[^.]+$/, "");
14
+ if (!relative.startsWith(".")) {
15
+ relative = `./${relative}`;
16
+ }
17
+ return toPosix(relative);
18
+ }
19
+
20
+ export function sourcePathForHeader(sourceModulePath: string): string {
21
+ const relative = path.relative(process.cwd(), sourceModulePath);
22
+ return toPosix(relative.startsWith(".") ? relative : `./${relative}`);
23
+ }
24
+
25
+ export async function loadModuleExport(
26
+ modulePath: string,
27
+ exportName: string,
28
+ tempPrefix: string,
29
+ ): Promise<unknown> {
30
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), tempPrefix));
31
+ const bundledFile = path.join(tempDir, "module.mjs");
32
+ try {
33
+ await build({
34
+ entryPoints: [modulePath],
35
+ outfile: bundledFile,
36
+ bundle: true,
37
+ format: "esm",
38
+ platform: "node",
39
+ target: "node20",
40
+ logLevel: "silent",
41
+ });
42
+ const loaded = await import(pathToFileURL(bundledFile).href) as Record<string, unknown>;
43
+ if (!(exportName in loaded)) {
44
+ throw new Error(`Module does not export "${exportName}".`);
45
+ }
46
+ return loaded[exportName];
47
+ } finally {
48
+ await fs.rm(tempDir, { recursive: true, force: true });
49
+ }
50
+ }
51
+
52
+ export function snakeCaseIdentifier(value: string): string {
53
+ let result = "";
54
+ for (let index = 0; index < value.length; index += 1) {
55
+ const char = value[index] ?? "";
56
+ const previous = index > 0 ? value[index - 1] ?? "" : "";
57
+ const next = index + 1 < value.length ? value[index + 1] ?? "" : "";
58
+ const isUpper = char >= "A" && char <= "Z";
59
+ const prevIsLowerOrDigit =
60
+ (previous >= "a" && previous <= "z") || (previous >= "0" && previous <= "9");
61
+ const nextIsLower = next >= "a" && next <= "z";
62
+ if (
63
+ index > 0 &&
64
+ isUpper &&
65
+ (prevIsLowerOrDigit || ((previous >= "A" && previous <= "Z") && nextIsLower))
66
+ ) {
67
+ result += "_";
68
+ }
69
+ result += char.toLowerCase();
70
+ }
71
+ return result;
72
+ }
73
+
@@ -0,0 +1,159 @@
1
+ export type HostServiceTypeName =
2
+ | "string"
3
+ | "bool"
4
+ | "i32"
5
+ | "u32"
6
+ | "i64"
7
+ | "u64"
8
+ | "f64"
9
+ | "bytes"
10
+ | "i32_array"
11
+ | "u32_array"
12
+ | "i64_array"
13
+ | "u64_array"
14
+ | "f64_array"
15
+ | "void";
16
+
17
+ interface HostServiceMethodDefinition {
18
+ readonly importName?: string;
19
+ readonly args: readonly HostServiceTypeName[];
20
+ readonly returns: HostServiceTypeName;
21
+ readonly implementation: (...args: readonly unknown[]) => unknown;
22
+ }
23
+
24
+ type HostServicesDefinition = Record<string, Record<string, HostServiceMethodDefinition>>;
25
+
26
+ interface HostEventMethodDefinition {
27
+ readonly args: readonly HostServiceTypeName[];
28
+ readonly subscribe: (emit: (...args: readonly unknown[]) => void) => (() => void) | undefined;
29
+ }
30
+
31
+ type HostEventsDefinition = Record<string, Record<string, HostEventMethodDefinition>>;
32
+
33
+ export interface NormalizedHostServiceMethod {
34
+ readonly serviceName: string;
35
+ readonly methodName: string;
36
+ readonly importName: string;
37
+ readonly args: readonly HostServiceTypeName[];
38
+ readonly returns: HostServiceTypeName;
39
+ readonly implementation: (...args: readonly unknown[]) => unknown;
40
+ }
41
+
42
+ export interface NormalizedHostEventMethod {
43
+ readonly serviceName: string;
44
+ readonly methodName: string;
45
+ readonly eventName: string;
46
+ readonly exportName: string;
47
+ readonly args: readonly HostServiceTypeName[];
48
+ readonly subscribe: (emit: (...args: readonly unknown[]) => void) => (() => void) | undefined;
49
+ }
50
+
51
+ const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
52
+
53
+ function assertIdentifier(value: string, context: string): void {
54
+ if (!IDENTIFIER_RE.test(value)) {
55
+ throw new Error(`${context} "${value}" must be a valid identifier.`);
56
+ }
57
+ }
58
+
59
+ function capitalize(value: string): string {
60
+ return value.length === 0 ? value : `${value.slice(0, 1).toUpperCase()}${value.slice(1)}`;
61
+ }
62
+
63
+ function validateServiceType(type: string, context: string): asserts type is HostServiceTypeName {
64
+ if (
65
+ type === "string" ||
66
+ type === "bool" ||
67
+ type === "i32" ||
68
+ type === "u32" ||
69
+ type === "i64" ||
70
+ type === "u64" ||
71
+ type === "f64" ||
72
+ type === "bytes" ||
73
+ type === "i32_array" ||
74
+ type === "u32_array" ||
75
+ type === "i64_array" ||
76
+ type === "u64_array" ||
77
+ type === "f64_array" ||
78
+ type === "void"
79
+ ) {
80
+ return;
81
+ }
82
+ throw new Error(`${context} uses unsupported host-service type "${type}".`);
83
+ }
84
+
85
+ function validateEventType(type: string, context: string): asserts type is HostServiceTypeName {
86
+ if (type !== "void") {
87
+ validateServiceType(type, context);
88
+ return;
89
+ }
90
+ throw new Error(`${context} uses unsupported host-event type "${type}".`);
91
+ }
92
+
93
+ export function listHostServiceMethods(services: HostServicesDefinition | undefined): readonly NormalizedHostServiceMethod[] {
94
+ if (services === undefined) {
95
+ return [];
96
+ }
97
+ const methods: NormalizedHostServiceMethod[] = [];
98
+ const seenImports = new Set<string>();
99
+ for (const [serviceName, serviceMethods] of Object.entries(services)) {
100
+ assertIdentifier(serviceName, "Host service");
101
+ for (const [methodName, definition] of Object.entries(serviceMethods)) {
102
+ assertIdentifier(methodName, `Host service ${serviceName} method`);
103
+ const importName = definition.importName ?? `${serviceName}${capitalize(methodName)}`;
104
+ assertIdentifier(importName, `Host service ${serviceName}.${methodName} import`);
105
+ if (seenImports.has(importName)) {
106
+ throw new Error(`Duplicate host-service import name "${importName}".`);
107
+ }
108
+ seenImports.add(importName);
109
+ const args = [...definition.args];
110
+ args.forEach((type, index) => {
111
+ validateServiceType(type, `Host service ${serviceName}.${methodName} arg ${String(index)}`);
112
+ });
113
+ validateServiceType(definition.returns, `Host service ${serviceName}.${methodName} return`);
114
+ methods.push({
115
+ serviceName,
116
+ methodName,
117
+ importName,
118
+ args,
119
+ returns: definition.returns,
120
+ implementation: definition.implementation,
121
+ });
122
+ }
123
+ }
124
+ methods.sort((left, right) => left.importName.localeCompare(right.importName));
125
+ return methods;
126
+ }
127
+
128
+ export function listHostEventMethods(events: HostEventsDefinition | undefined): readonly NormalizedHostEventMethod[] {
129
+ if (events === undefined) {
130
+ return [];
131
+ }
132
+ const methods: NormalizedHostEventMethod[] = [];
133
+ const seenEvents = new Set<string>();
134
+ for (const [serviceName, serviceMethods] of Object.entries(events)) {
135
+ assertIdentifier(serviceName, "Host event service");
136
+ for (const [methodName, definition] of Object.entries(serviceMethods)) {
137
+ assertIdentifier(methodName, `Host event ${serviceName} method`);
138
+ const eventName = `${serviceName}${capitalize(methodName)}`;
139
+ if (seenEvents.has(eventName)) {
140
+ throw new Error(`Duplicate host-event name "${eventName}".`);
141
+ }
142
+ seenEvents.add(eventName);
143
+ const args = [...definition.args];
144
+ args.forEach((type, index) => {
145
+ validateEventType(type, `Host event ${serviceName}.${methodName} arg ${String(index)}`);
146
+ });
147
+ methods.push({
148
+ serviceName,
149
+ methodName,
150
+ eventName,
151
+ exportName: `__fui_host_event_${eventName}`,
152
+ args,
153
+ subscribe: definition.subscribe,
154
+ });
155
+ }
156
+ }
157
+ methods.sort((left, right) => left.eventName.localeCompare(right.eventName));
158
+ return methods;
159
+ }