@effindomv2/fui-as 0.1.37 → 0.1.39

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 (118) 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 +6 -0
  6. package/package.json +23 -13
  7. package/scripts/build.sh +47 -46
  8. package/scripts/framework-host-services.ts +1 -1
  9. package/scripts/generate-host-events.ts +15 -241
  10. package/scripts/generate-host-services.ts +13 -261
  11. package/scripts/hostgen/assemblyscript-host-events.ts +165 -0
  12. package/scripts/hostgen/assemblyscript-host-services.ts +207 -0
  13. package/scripts/hostgen/common.ts +73 -0
  14. package/scripts/hostgen/registry.ts +159 -0
  15. package/src/Fui.ts +65 -5
  16. package/src/FuiExports.ts +12 -1
  17. package/src/bindings/ui.ts +157 -8
  18. package/src/controls/Button.ts +98 -71
  19. package/src/controls/Checkbox.ts +23 -21
  20. package/src/controls/ComboBox.ts +1201 -0
  21. package/src/controls/ContextMenu.ts +279 -165
  22. package/src/controls/Dialog.ts +91 -17
  23. package/src/controls/Dropdown.ts +101 -333
  24. package/src/controls/NavLink.ts +29 -6
  25. package/src/controls/Popup.ts +10 -5
  26. package/src/controls/ProgressBar.ts +1 -2
  27. package/src/controls/RadioButton.ts +34 -13
  28. package/src/controls/RadioGroup.ts +32 -19
  29. package/src/controls/Slider.ts +21 -23
  30. package/src/controls/Switch.ts +34 -17
  31. package/src/controls/index.ts +2 -1
  32. package/src/controls/internal/ButtonPresenter.ts +5 -2
  33. package/src/controls/internal/CheckboxIndicatorPresenter.ts +10 -10
  34. package/src/controls/internal/DropdownChevronPresenter.ts +3 -3
  35. package/src/controls/internal/DropdownFieldPresenter.ts +6 -6
  36. package/src/controls/internal/DropdownOptionRowPresenter.ts +5 -4
  37. package/src/controls/internal/PressableLabeledControl.ts +4 -7
  38. package/src/controls/internal/RadioIndicatorPresenter.ts +7 -7
  39. package/src/controls/internal/SelectablePopupList.ts +463 -0
  40. package/src/controls/internal/SliderPresenter.ts +5 -5
  41. package/src/controls/internal/SwitchIndicatorPresenter.ts +63 -17
  42. package/src/controls/internal/TextInputCore.ts +307 -41
  43. package/src/controls/internal/TextInputPresenter.ts +2 -2
  44. package/src/controls/templating.ts +10 -0
  45. package/src/core/Application.ts +17 -8
  46. package/src/core/Bitmap.ts +192 -3
  47. package/src/core/ContextMenuManager.ts +128 -44
  48. package/src/core/DebugApi.ts +17 -2
  49. package/src/core/DragGesture.ts +8 -7
  50. package/src/core/EventRouter.ts +332 -12
  51. package/src/core/Fetch.ts +15 -7
  52. package/src/core/FetchFfi.ts +1 -15
  53. package/src/core/File.ts +75 -34
  54. package/src/core/FocusAdornerManager.ts +3 -3
  55. package/src/core/FocusVisibility.ts +7 -0
  56. package/src/core/FrameScheduler.ts +104 -5
  57. package/src/core/ImageSampling.ts +43 -0
  58. package/src/core/Logger.ts +2 -0
  59. package/src/core/MobileTextSelectionToolbarManager.ts +901 -0
  60. package/src/core/Node.ts +1039 -97
  61. package/src/core/Platform.ts +16 -16
  62. package/src/core/SelectionHandleAdornerManager.ts +697 -0
  63. package/src/core/Theme.ts +34 -20
  64. package/src/core/ToolTipManager.ts +3 -3
  65. package/src/core/Typography.ts +411 -19
  66. package/src/core/Worker.ts +69 -33
  67. package/src/core/event_exports.ts +123 -5
  68. package/src/core/ffi.ts +5 -865
  69. package/src/core/generated/FetchHostAbi.ts +10 -0
  70. package/src/core/generated/FrameworkHostServices.ts +1 -1
  71. package/src/core/generated/HostAbi.ts +220 -0
  72. package/src/core/generated/UiAbi.ts +356 -0
  73. package/src/core/generated/UiEnums.ts +186 -0
  74. package/src/drawing/DrawCallback.ts +17 -1
  75. package/src/drawing/DrawContext.ts +140 -32
  76. package/src/nodes/CustomDrawable.ts +8 -1
  77. package/src/nodes/FlexBox.ts +40 -11
  78. package/src/nodes/Grid.ts +5 -5
  79. package/src/nodes/Image.ts +20 -1
  80. package/src/nodes/RichText.ts +36 -25
  81. package/src/nodes/ScrollBar.ts +2 -1
  82. package/src/nodes/ScrollBox.ts +7 -2
  83. package/src/nodes/ScrollView.ts +17 -6
  84. package/src/nodes/Svg.ts +18 -1
  85. package/src/nodes/Text.ts +1 -1
  86. package/src/nodes/TextCore.ts +115 -20
  87. package/src/nodes/index.ts +1 -1
  88. package/src/text/DynamicTextLayout.ts +275 -0
  89. package/src/text/TextLayout.ts +308 -0
  90. package/src/worker/Worker.ts +3 -36
  91. package/src/worker/WorkerJob.ts +5 -9
  92. package/src/worker/ffi.ts +0 -6
  93. package/browser/src/common-harness/host-imports.ts +0 -447
  94. package/browser/src/common-harness/interop.ts +0 -39
  95. package/browser/src/common-harness/managed-harness-bitmap-host.ts +0 -92
  96. package/browser/src/common-harness/managed-harness-canvas-host.ts +0 -72
  97. package/browser/src/common-harness/managed-harness-fetch-host.ts +0 -201
  98. package/browser/src/common-harness/managed-harness-file-host.ts +0 -1066
  99. package/browser/src/common-harness/managed-harness-file-payloads.ts +0 -143
  100. package/browser/src/common-harness/managed-harness-file-types.ts +0 -71
  101. package/browser/src/common-harness/managed-harness-session.ts +0 -15
  102. package/browser/src/common-harness/managed-harness.ts +0 -1472
  103. package/browser/src/common-harness/managed-history.ts +0 -168
  104. package/browser/src/common-harness/persisted-restore-policy.ts +0 -50
  105. package/browser/src/common-harness/persisted-ui-state-controller.ts +0 -309
  106. package/browser/src/common-harness/text-session-bridge.ts +0 -452
  107. package/browser/src/common-harness/types.ts +0 -206
  108. package/browser/src/common-harness/ui-chrome.ts +0 -206
  109. package/browser/src/common-harness/ui-imports.ts +0 -559
  110. package/browser/src/common-harness/wasm-module-cache.ts +0 -47
  111. package/browser/src/file-processing-worker.ts +0 -89
  112. package/browser/src/host-events.ts +0 -118
  113. package/browser/src/host-services.ts +0 -483
  114. package/browser/src/persisted-ui-state.ts +0 -206
  115. package/browser/src/routed-app-conventions.ts +0 -292
  116. package/browser/src/worker-bootstrap.ts +0 -916
  117. package/browser/src/worker-manager.ts +0 -234
  118. 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
+ }