@effindomv2/fui-as 0.1.0 → 0.1.2

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/COMMERCIAL.md ADDED
@@ -0,0 +1,7 @@
1
+ # FUI-AS Commercial License
2
+
3
+ `@effindomv2/fui-as` is available under a separate commercial license if you
4
+ want to use it without the obligations of AGPL-3.0-only.
5
+
6
+ To discuss commercial licensing, contact the repository maintainer through the
7
+ project's public contact channel.
package/LICENSE.md CHANGED
@@ -1,7 +1,8 @@
1
- `@effindomv2/fui-as` is licensed under Business Source License 1.1 (BSL-1.1),
2
- with a Change License to MIT after 5 years for each released version.
1
+ `@effindomv2/fui-as` is licensed under the GNU Affero General Public License
2
+ v3.0 (AGPL-3.0-only) or, at your option, the commercial license terms in
3
+ `COMMERCIAL.md`.
3
4
 
4
5
  See:
5
6
 
6
- - `../../LICENSE.md`
7
- - `../../LICENSES/BSL-1.1.md`
7
+ - `LICENSES/AGPL-3.0-only.md`
8
+ - `COMMERCIAL.md`
@@ -0,0 +1,8 @@
1
+ # GNU Affero General Public License v3.0
2
+
3
+ The full text of the GNU Affero General Public License, Version 3, is
4
+ available at:
5
+
6
+ - https://www.gnu.org/licenses/agpl-3.0.txt
7
+
8
+ This repository uses the AGPL-3.0-only license for `@effindomv2/fui-as`.
@@ -255,6 +255,19 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
255
255
  return textBridge.writeAppUtf8(ptr, capacity, text, context);
256
256
  }
257
257
 
258
+ function writeAppBytes(ptr: number, capacity: number, bytes: Uint8Array, context: string): number {
259
+ if (capacity < 0) {
260
+ throw new Error(`${context} has invalid buffer capacity ${String(capacity)}.`);
261
+ }
262
+ if (bytes.length > capacity) {
263
+ throw new Error(`${context} returned ${String(bytes.length)} bytes but the shared result buffer only holds ${String(capacity)}.`);
264
+ }
265
+ if (bytes.length > 0) {
266
+ new Uint8Array(getCurrentMemory().buffer, ptr, bytes.length).set(bytes);
267
+ }
268
+ return bytes.length;
269
+ }
270
+
258
271
  function withUiUtf8(
259
272
  text: string,
260
273
  callback: (ptr: WasmHandleLike | number, len: number) => void,
@@ -480,6 +493,41 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
480
493
  method: NormalizedHostEventMethod,
481
494
  args: readonly unknown[],
482
495
  ): Array<unknown> {
496
+ function alignOffset(value: number, alignment: number): number {
497
+ return alignment <= 1 ? value : (value + alignment - 1) & ~(alignment - 1);
498
+ }
499
+
500
+ function encodeTypedArrayArg(
501
+ type: "bytes" | "i32_array" | "f64_array",
502
+ arg: unknown,
503
+ context: string,
504
+ ): { bytes: Uint8Array; elementCount: number; alignment: number } {
505
+ if (type === "bytes") {
506
+ if (!(arg instanceof Uint8Array)) {
507
+ throw new Error(`${context} must be a Uint8Array.`);
508
+ }
509
+ return { bytes: arg, elementCount: arg.length, alignment: 1 };
510
+ }
511
+ if (type === "i32_array") {
512
+ if (!(arg instanceof Int32Array)) {
513
+ throw new Error(`${context} must be an Int32Array.`);
514
+ }
515
+ return {
516
+ bytes: new Uint8Array(arg.buffer, arg.byteOffset, arg.byteLength),
517
+ elementCount: arg.length,
518
+ alignment: 4,
519
+ };
520
+ }
521
+ if (!(arg instanceof Float64Array)) {
522
+ throw new Error(`${context} must be a Float64Array.`);
523
+ }
524
+ return {
525
+ bytes: new Uint8Array(arg.buffer, arg.byteOffset, arg.byteLength),
526
+ elementCount: arg.length,
527
+ alignment: 8,
528
+ };
529
+ }
530
+
483
531
  if (args.length != method.args.length) {
484
532
  throw new Error(`Host event ${method.serviceName}.${method.methodName} expected ${String(method.args.length)} args but received ${String(args.length)}.`);
485
533
  }
@@ -507,6 +555,22 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
507
555
  }
508
556
  continue;
509
557
  }
558
+ if (type === "bytes" || type === "i32_array" || type === "f64_array") {
559
+ const payload = encodeTypedArrayArg(type, arg, context);
560
+ if (payload.bytes.length > 0) {
561
+ const alignedOffset = alignOffset(byteOffset, payload.alignment);
562
+ if (session.textBufferPtr === 0 || alignedOffset + payload.bytes.length > session.textBufferSize) {
563
+ throw new Error(`${context} exceeds the shared AssemblyScript text buffer.`);
564
+ }
565
+ const memory = new Uint8Array(session.memory.buffer, session.textBufferPtr + alignedOffset, payload.bytes.length);
566
+ memory.set(payload.bytes);
567
+ callArgs.push(session.textBufferPtr + alignedOffset, payload.elementCount);
568
+ byteOffset = alignedOffset + payload.bytes.length;
569
+ } else {
570
+ callArgs.push(0, 0);
571
+ }
572
+ continue;
573
+ }
510
574
  if (type === 'bool') {
511
575
  if (typeof arg !== 'boolean') {
512
576
  throw new Error(`${context} must be a boolean.`);
@@ -703,6 +767,8 @@ export function startManagedHarness(options: ManagedHarnessOptions): void {
703
767
  const hostServiceImports = createHostServiceImportModule(hostServices, {
704
768
  readString: readAppUtf8,
705
769
  writeString: writeAppUtf8,
770
+ readBytes: readAppBytes,
771
+ writeBytes: writeAppBytes,
706
772
  });
707
773
  return {
708
774
  effindom_v2_ui: createUiImportModule({
@@ -3,6 +3,9 @@ import type { HostServiceTypeName } from "./host-services";
3
3
  type HostEventTypeValue<T extends HostServiceTypeName> =
4
4
  T extends "string" ? string :
5
5
  T extends "bool" ? boolean :
6
+ T extends "bytes" ? Uint8Array :
7
+ T extends "i32_array" ? Int32Array :
8
+ T extends "f64_array" ? Float64Array :
6
9
  T extends "void" ? void :
7
10
  number;
8
11
 
@@ -59,7 +62,15 @@ function buildExportName(eventName: string): string {
59
62
  }
60
63
 
61
64
  function validateEventType(type: string, context: string): asserts type is HostServiceTypeName {
62
- if (type === "string" || type === "bool" || type === "i32" || type === "f64") {
65
+ if (
66
+ type === "string" ||
67
+ type === "bool" ||
68
+ type === "i32" ||
69
+ type === "f64" ||
70
+ type === "bytes" ||
71
+ type === "i32_array" ||
72
+ type === "f64_array"
73
+ ) {
63
74
  return;
64
75
  }
65
76
  throw new Error(`${context} uses unsupported host-event type "${type}".`);
@@ -1,8 +1,19 @@
1
- export type HostServiceTypeName = "string" | "bool" | "i32" | "f64" | "void";
1
+ export type HostServiceTypeName =
2
+ | "string"
3
+ | "bool"
4
+ | "i32"
5
+ | "f64"
6
+ | "bytes"
7
+ | "i32_array"
8
+ | "f64_array"
9
+ | "void";
2
10
 
3
11
  type HostServiceTypeValue<T extends HostServiceTypeName> =
4
12
  T extends "string" ? string :
5
13
  T extends "bool" ? boolean :
14
+ T extends "bytes" ? Uint8Array :
15
+ T extends "i32_array" ? Int32Array :
16
+ T extends "f64_array" ? Float64Array :
6
17
  T extends "void" ? void :
7
18
  number;
8
19
 
@@ -33,6 +44,8 @@ export interface NormalizedHostServiceMethod {
33
44
  export interface HostServiceImportIo {
34
45
  readString(ptr: number, len: number): string;
35
46
  writeString(ptr: number, capacity: number, text: string, context: string): number;
47
+ readBytes(ptr: number, len: number): Uint8Array;
48
+ writeBytes(ptr: number, capacity: number, bytes: Uint8Array, context: string): number;
36
49
  }
37
50
 
38
51
  const IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -63,7 +76,16 @@ function buildImportName(serviceName: string, methodName: string): string {
63
76
  }
64
77
 
65
78
  function validateServiceType(type: string, context: string): asserts type is HostServiceTypeName {
66
- if (type === "string" || type === "bool" || type === "i32" || type === "f64" || type === "void") {
79
+ if (
80
+ type === "string" ||
81
+ type === "bool" ||
82
+ type === "i32" ||
83
+ type === "f64" ||
84
+ type === "bytes" ||
85
+ type === "i32_array" ||
86
+ type === "f64_array" ||
87
+ type === "void"
88
+ ) {
67
89
  return;
68
90
  }
69
91
  throw new Error(`${context} uses unsupported host-service type "${type}".`);
@@ -126,6 +148,27 @@ function expectString(value: unknown, context: string): string {
126
148
  return value;
127
149
  }
128
150
 
151
+ function expectBytes(value: unknown, context: string): Uint8Array {
152
+ if (!(value instanceof Uint8Array)) {
153
+ throw new Error(`${context} must be a Uint8Array.`);
154
+ }
155
+ return value;
156
+ }
157
+
158
+ function expectInt32Array(value: unknown, context: string): Int32Array {
159
+ if (!(value instanceof Int32Array)) {
160
+ throw new Error(`${context} must be an Int32Array.`);
161
+ }
162
+ return value;
163
+ }
164
+
165
+ function expectFloat64Array(value: unknown, context: string): Float64Array {
166
+ if (!(value instanceof Float64Array)) {
167
+ throw new Error(`${context} must be a Float64Array.`);
168
+ }
169
+ return value;
170
+ }
171
+
129
172
  function expectI32(value: unknown, context: string): number {
130
173
  const numberValue = expectNumber(value, context);
131
174
  if (!Number.isInteger(numberValue) || numberValue < -2147483648 || numberValue > 2147483647) {
@@ -134,6 +177,44 @@ function expectI32(value: unknown, context: string): number {
134
177
  return numberValue;
135
178
  }
136
179
 
180
+ function expectLength(value: unknown, context: string): number {
181
+ const length = expectNumber(value, context);
182
+ if (!Number.isInteger(length) || length < 0) {
183
+ throw new Error(`${context} must be a non-negative integer.`);
184
+ }
185
+ return length;
186
+ }
187
+
188
+ function bytesToI32Array(bytes: Uint8Array, context: string): Int32Array {
189
+ if ((bytes.byteLength & 3) !== 0) {
190
+ throw new Error(`${context} payload length must be divisible by 4.`);
191
+ }
192
+ const values = new Int32Array(bytes.byteLength >>> 2);
193
+ new Uint8Array(values.buffer).set(bytes);
194
+ return values;
195
+ }
196
+
197
+ function bytesToF64Array(bytes: Uint8Array, context: string): Float64Array {
198
+ if ((bytes.byteLength & 7) !== 0) {
199
+ throw new Error(`${context} payload length must be divisible by 8.`);
200
+ }
201
+ const values = new Float64Array(bytes.byteLength >>> 3);
202
+ new Uint8Array(values.buffer).set(bytes);
203
+ return values;
204
+ }
205
+
206
+ function typedArrayBytes(value: Uint8Array | Int32Array | Float64Array): Uint8Array {
207
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
208
+ }
209
+
210
+ function consumedRawArgCount(method: NormalizedHostServiceMethod): number {
211
+ let count = 0;
212
+ method.args.forEach((type) => {
213
+ count += type === "string" || type === "bytes" || type === "i32_array" || type === "f64_array" ? 2 : 1;
214
+ });
215
+ return count;
216
+ }
217
+
137
218
  function decodeHostServiceArgs(
138
219
  method: NormalizedHostServiceMethod,
139
220
  rawArgs: readonly unknown[],
@@ -150,6 +231,29 @@ function decodeHostServiceArgs(
150
231
  index += 2;
151
232
  return;
152
233
  }
234
+ if (type === "bytes") {
235
+ const ptr = expectNumber(rawArgs[index], `${context} ptr`);
236
+ const len = expectLength(rawArgs[index + 1], `${context} len`);
237
+ decodedArgs.push(len <= 0 ? new Uint8Array(0) : io.readBytes(ptr, len));
238
+ index += 2;
239
+ return;
240
+ }
241
+ if (type === "i32_array") {
242
+ const ptr = expectNumber(rawArgs[index], `${context} ptr`);
243
+ const len = expectLength(rawArgs[index + 1], `${context} len`);
244
+ const payload = len <= 0 ? new Uint8Array(0) : io.readBytes(ptr, len << 2);
245
+ decodedArgs.push(bytesToI32Array(payload, context));
246
+ index += 2;
247
+ return;
248
+ }
249
+ if (type === "f64_array") {
250
+ const ptr = expectNumber(rawArgs[index], `${context} ptr`);
251
+ const len = expectLength(rawArgs[index + 1], `${context} len`);
252
+ const payload = len <= 0 ? new Uint8Array(0) : io.readBytes(ptr, len << 3);
253
+ decodedArgs.push(bytesToF64Array(payload, context));
254
+ index += 2;
255
+ return;
256
+ }
153
257
  const rawValue = rawArgs[index];
154
258
  if (type === "bool") {
155
259
  decodedArgs.push(expectNumber(rawValue, context) !== 0);
@@ -179,14 +283,29 @@ export function createHostServiceImportModule(
179
283
  return;
180
284
  }
181
285
  if (method.returns === "string") {
182
- let outputIndex = 0;
183
- method.args.forEach((type) => {
184
- outputIndex += type === "string" ? 2 : 1;
185
- });
286
+ const outputIndex = consumedRawArgCount(method);
186
287
  const ptr = expectNumber(rawArgs[outputIndex], `${resultContext} ptr`);
187
288
  const capacity = expectNumber(rawArgs[outputIndex + 1], `${resultContext} capacity`);
188
289
  return io.writeString(ptr, capacity, expectString(result, resultContext), resultContext);
189
290
  }
291
+ if (method.returns === "bytes") {
292
+ const outputIndex = consumedRawArgCount(method);
293
+ const ptr = expectNumber(rawArgs[outputIndex], `${resultContext} ptr`);
294
+ const capacity = expectNumber(rawArgs[outputIndex + 1], `${resultContext} capacity`);
295
+ return io.writeBytes(ptr, capacity, expectBytes(result, resultContext), resultContext);
296
+ }
297
+ if (method.returns === "i32_array") {
298
+ const outputIndex = consumedRawArgCount(method);
299
+ const ptr = expectNumber(rawArgs[outputIndex], `${resultContext} ptr`);
300
+ const capacity = expectNumber(rawArgs[outputIndex + 1], `${resultContext} capacity`);
301
+ return io.writeBytes(ptr, capacity, typedArrayBytes(expectInt32Array(result, resultContext)), resultContext);
302
+ }
303
+ if (method.returns === "f64_array") {
304
+ const outputIndex = consumedRawArgCount(method);
305
+ const ptr = expectNumber(rawArgs[outputIndex], `${resultContext} ptr`);
306
+ const capacity = expectNumber(rawArgs[outputIndex + 1], `${resultContext} capacity`);
307
+ return io.writeBytes(ptr, capacity, typedArrayBytes(expectFloat64Array(result, resultContext)), resultContext);
308
+ }
190
309
  if (method.returns === "bool") {
191
310
  return expectBoolean(result, resultContext) ? 1 : 0;
192
311
  }
@@ -106,6 +106,31 @@ function writeUtf8(memory: WebAssembly.Memory | null, ptr: number, capacity: num
106
106
  return encoded.length;
107
107
  }
108
108
 
109
+ function readBytes(memory: WebAssembly.Memory | null, ptr: number, len: number): Uint8Array {
110
+ if (memory === null || len <= 0) {
111
+ return new Uint8Array(0);
112
+ }
113
+ const bytes = new Uint8Array(len);
114
+ bytes.set(new Uint8Array(memory.buffer, ptr, len));
115
+ return bytes;
116
+ }
117
+
118
+ function writeBytes(memory: WebAssembly.Memory | null, ptr: number, capacity: number, bytes: Uint8Array, context: string): number {
119
+ if (memory === null) {
120
+ throw new Error(`${context} requires worker memory.`);
121
+ }
122
+ if (capacity < 0) {
123
+ throw new Error(`${context} has invalid worker host-service buffer capacity.`);
124
+ }
125
+ if (bytes.length > capacity) {
126
+ throw new Error(`${context} exceeds the worker host-service result buffer.`);
127
+ }
128
+ if (bytes.length > 0) {
129
+ new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes);
130
+ }
131
+ return bytes.length;
132
+ }
133
+
109
134
  function encodeTextPartsPayload(values: readonly string[]): Uint8Array {
110
135
  const encodedValues = values.map((value) => encoder.encode(value));
111
136
  let totalBytes = 4;
@@ -277,6 +302,8 @@ async function startWorker(message: WorkerBootstrapStartMessage): Promise<void>
277
302
  fui_host_service: createHostServiceImportModule(hostServices, {
278
303
  readString: (ptr, len) => readUtf8(memory, ptr, len),
279
304
  writeString: (ptr, capacity, text, context) => writeUtf8(memory, ptr, capacity, text, context),
305
+ readBytes: (ptr, len) => readBytes(memory, ptr, len),
306
+ writeBytes: (ptr, capacity, bytes, context) => writeBytes(memory, ptr, capacity, bytes, context),
280
307
  }),
281
308
  fui_worker_host: {
282
309
  fui_worker_input_length(): number {
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@effindomv2/fui-as",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
- "license": "BSL-1.1",
6
- "description": "EffinDom v2 AssemblyScript SDK and browser harness",
5
+ "license": "AGPL-3.0-only OR LicenseRef-EffinDom-Commercial",
6
+ "description": "EffinDom v2 AssemblyScript frontend framework SDK and browser harness",
7
7
  "type": "module",
8
8
  "publishConfig": {
9
9
  "access": "public"
@@ -61,7 +61,10 @@
61
61
  "files": [
62
62
  "src",
63
63
  "browser/src",
64
- "scripts"
64
+ "scripts",
65
+ "LICENSE.md",
66
+ "COMMERCIAL.md",
67
+ "LICENSES"
65
68
  ],
66
69
  "scripts": {
67
70
  "build": "bash scripts/build.sh",
@@ -73,10 +76,10 @@
73
76
  "typecheck": "tsc -p tsconfig.json --noEmit"
74
77
  },
75
78
  "dependencies": {
79
+ "@assemblyscript/loader": "^0.28.17",
76
80
  "@effindomv2/runtime": "0.1.0"
77
81
  },
78
82
  "devDependencies": {
79
- "@assemblyscript/loader": "^0.28.17",
80
83
  "@as-pect/assembly": "8.1.0",
81
84
  "@as-pect/cli": "8.1.0",
82
85
  "@as-pect/core": "8.1.0",
@@ -5,12 +5,13 @@ set -euo pipefail
5
5
  PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
6
  REPO_ROOT="$(cd "${PACKAGE_DIR}/../.." && pwd)"
7
7
  DEMO_OUT_DIR="${REPO_ROOT}/public/v2/fui-as/demo"
8
+ MVC_OUT_DIR="${REPO_ROOT}/public/v2/fui-as/demo-mvc"
8
9
  HELLO_OUT_DIR="${REPO_ROOT}/public/v2/fui-as/demo-hello-world"
9
10
  HOST_SERVICE_GENERATOR_BUILD="${PACKAGE_DIR}/build/generate-host-services.mjs"
10
11
  HOST_EVENT_GENERATOR_BUILD="${PACKAGE_DIR}/build/generate-host-events.mjs"
11
12
  BUILD_TARGET="${1:-all}"
12
13
 
13
- mkdir -p "${PACKAGE_DIR}/build" "${DEMO_OUT_DIR}" "${HELLO_OUT_DIR}"
14
+ mkdir -p "${PACKAGE_DIR}/build" "${DEMO_OUT_DIR}" "${MVC_OUT_DIR}" "${HELLO_OUT_DIR}" "${PACKAGE_DIR}/templates/demo-mvc/src/generated" "${PACKAGE_DIR}/templates/demo-hello-world/src/generated"
14
15
  cd "${PACKAGE_DIR}"
15
16
 
16
17
  build_demo_app() {
@@ -28,6 +29,7 @@ generate_host_services() {
28
29
  local definition_file="$1"
29
30
  local export_name="$2"
30
31
  local output_file="$3"
32
+ local primitives_import="${4:-}"
31
33
 
32
34
  npx esbuild "${PACKAGE_DIR}/scripts/generate-host-services.ts" \
33
35
  --bundle \
@@ -37,13 +39,18 @@ generate_host_services() {
37
39
  --packages=external \
38
40
  --outfile="${HOST_SERVICE_GENERATOR_BUILD}"
39
41
 
40
- node "${HOST_SERVICE_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}"
42
+ if [ -n "${primitives_import}" ]; then
43
+ node "${HOST_SERVICE_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}" "${primitives_import}"
44
+ else
45
+ node "${HOST_SERVICE_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}"
46
+ fi
41
47
  }
42
48
 
43
49
  generate_host_events() {
44
50
  local definition_file="$1"
45
51
  local export_name="$2"
46
52
  local output_file="$3"
53
+ local primitives_import="${4:-}"
47
54
 
48
55
  npx esbuild "${PACKAGE_DIR}/scripts/generate-host-events.ts" \
49
56
  --bundle \
@@ -53,12 +60,20 @@ generate_host_events() {
53
60
  --packages=external \
54
61
  --outfile="${HOST_EVENT_GENERATOR_BUILD}"
55
62
 
56
- node "${HOST_EVENT_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}"
63
+ if [ -n "${primitives_import}" ]; then
64
+ node "${HOST_EVENT_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}" "${primitives_import}"
65
+ else
66
+ node "${HOST_EVENT_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}"
67
+ fi
57
68
  }
58
69
 
59
70
  generate_host_services "demo/src/host-services.ts" "demoHostServices" "demo/src/generated/HostServices.ts"
60
71
  generate_host_events "demo/src/host-events.ts" "demoHostEvents" "demo/src/generated/HostEvents.ts"
61
72
  generate_host_services "demo/src/worker-host-services.ts" "demoWorkerHostServices" "demo/src/generated/WorkerHostServices.ts"
73
+ generate_host_services "templates/demo-hello-world/src/host/host-services.ts" "appHostServices" "templates/demo-hello-world/src/generated/HostServices.ts" "../FuiPrimitives"
74
+ generate_host_events "templates/demo-hello-world/src/host/host-events.ts" "appHostEvents" "templates/demo-hello-world/src/generated/HostEvents.ts" "../FuiPrimitives"
75
+ generate_host_services "templates/demo-mvc/src/host/host-services.ts" "appHostServices" "templates/demo-mvc/src/generated/HostServices.ts" "../FuiPrimitives"
76
+ generate_host_events "templates/demo-mvc/src/host/host-events.ts" "appHostEvents" "templates/demo-mvc/src/generated/HostEvents.ts" "../FuiPrimitives"
62
77
 
63
78
  case "${BUILD_TARGET}" in
64
79
  all)
@@ -66,7 +81,9 @@ case "${BUILD_TARGET}" in
66
81
  build_demo_app "demo/src/routes/demo_home.ts" "${DEMO_OUT_DIR}/home.wasm"
67
82
  build_demo_app "demo/src/routes/demo_advanced_controls.ts" "${DEMO_OUT_DIR}/advanced-controls.wasm"
68
83
  build_demo_app "demo/src/routes/templated-controls.ts" "${DEMO_OUT_DIR}/templated-controls.wasm"
69
- build_demo_app "demo-hello-world/src/App.ts" "${HELLO_OUT_DIR}/app.wasm"
84
+ build_demo_app "templates/demo-mvc/src/routes/mvc_home.ts" "${MVC_OUT_DIR}/mvc-home.wasm"
85
+ build_demo_app "templates/demo-mvc/src/routes/mvc_settings.ts" "${MVC_OUT_DIR}/mvc-settings.wasm"
86
+ build_demo_app "templates/demo-hello-world/src/App.ts" "${HELLO_OUT_DIR}/app.wasm"
70
87
  ;;
71
88
  dashboard)
72
89
  build_demo_app "demo/src/dashboard.ts" "${DEMO_OUT_DIR}/demo.wasm"
@@ -80,12 +97,18 @@ case "${BUILD_TARGET}" in
80
97
  templated-controls|templated)
81
98
  build_demo_app "demo/src/routes/templated-controls.ts" "${DEMO_OUT_DIR}/templated-controls.wasm"
82
99
  ;;
100
+ mvc-home|mvc-home-page)
101
+ build_demo_app "templates/demo-mvc/src/routes/mvc_home.ts" "${MVC_OUT_DIR}/mvc-home.wasm"
102
+ ;;
103
+ mvc-settings|mvc-settings-page)
104
+ build_demo_app "templates/demo-mvc/src/routes/mvc_settings.ts" "${MVC_OUT_DIR}/mvc-settings.wasm"
105
+ ;;
83
106
  hello-world|hello)
84
- build_demo_app "demo-hello-world/src/App.ts" "${HELLO_OUT_DIR}/app.wasm"
107
+ build_demo_app "templates/demo-hello-world/src/App.ts" "${HELLO_OUT_DIR}/app.wasm"
85
108
  ;;
86
109
  *)
87
110
  echo "Unknown build target: ${BUILD_TARGET}" >&2
88
- echo "Usage: bash scripts/build-demo-as.sh [all|dashboard|home|advanced-controls|templated-controls|hello-world]" >&2
111
+ echo "Usage: bash scripts/build-demo-as.sh [all|dashboard|home|advanced-controls|templated-controls|mvc-home|mvc-settings|hello-world]" >&2
89
112
  exit 1
90
113
  ;;
91
114
  esac
package/scripts/build.sh CHANGED
@@ -8,6 +8,7 @@ BROWSER_SRC_DIR="${PACKAGE_DIR}/browser/src"
8
8
  SMOKE_FIXTURE_DIR="${PACKAGE_DIR}/tests/fixtures/smoke"
9
9
  OUT_DIR="${REPO_ROOT}/public/v2/fui-as"
10
10
  DEMO_OUT_DIR="${OUT_DIR}/demo"
11
+ MVC_OUT_DIR="${OUT_DIR}/demo-mvc"
11
12
  HELLO_OUT_DIR="${OUT_DIR}/demo-hello-world"
12
13
  WORKER_BUILD_DIR="${PACKAGE_DIR}/build/workers"
13
14
  WORKER_BOOTSTRAP_BUILD="${PACKAGE_DIR}/build/worker-bootstrap.js"
@@ -23,7 +24,7 @@ RUNTIME_CONFIG_FILE="effindom-runtime-config.js"
23
24
  DEFAULT_MANIFEST_PATH="./runtime/dist/effindom.v2.manifest.json"
24
25
 
25
26
  rm -rf "${OUT_DIR}"
26
- mkdir -p "${PACKAGE_DIR}/build" "${OUT_DIR}" "${DEMO_OUT_DIR}" "${HELLO_OUT_DIR}" "${WORKER_BUILD_DIR}"
27
+ mkdir -p "${PACKAGE_DIR}/build" "${OUT_DIR}" "${DEMO_OUT_DIR}" "${MVC_OUT_DIR}" "${HELLO_OUT_DIR}" "${WORKER_BUILD_DIR}" "${PACKAGE_DIR}/templates/demo-mvc/src/generated" "${PACKAGE_DIR}/templates/demo-hello-world/src/generated"
27
28
 
28
29
  cd "${PACKAGE_DIR}"
29
30
 
@@ -64,6 +65,7 @@ generate_host_services() {
64
65
  local definition_file="$1"
65
66
  local export_name="$2"
66
67
  local output_file="$3"
68
+ local primitives_import="${4:-}"
67
69
 
68
70
  npx esbuild "${PACKAGE_DIR}/scripts/generate-host-services.ts" \
69
71
  --bundle \
@@ -73,13 +75,18 @@ generate_host_services() {
73
75
  --packages=external \
74
76
  --outfile="${HOST_SERVICE_GENERATOR_BUILD}"
75
77
 
76
- node "${HOST_SERVICE_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}"
78
+ if [ -n "${primitives_import}" ]; then
79
+ node "${HOST_SERVICE_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}" "${primitives_import}"
80
+ else
81
+ node "${HOST_SERVICE_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}"
82
+ fi
77
83
  }
78
84
 
79
85
  generate_host_events() {
80
86
  local definition_file="$1"
81
87
  local export_name="$2"
82
88
  local output_file="$3"
89
+ local primitives_import="${4:-}"
83
90
 
84
91
  npx esbuild "${PACKAGE_DIR}/scripts/generate-host-events.ts" \
85
92
  --bundle \
@@ -89,7 +96,11 @@ generate_host_events() {
89
96
  --packages=external \
90
97
  --outfile="${HOST_EVENT_GENERATOR_BUILD}"
91
98
 
92
- node "${HOST_EVENT_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}"
99
+ if [ -n "${primitives_import}" ]; then
100
+ node "${HOST_EVENT_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}" "${primitives_import}"
101
+ else
102
+ node "${HOST_EVENT_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}"
103
+ fi
93
104
  }
94
105
 
95
106
  find_worker_entries() {
@@ -162,6 +173,10 @@ copy_runtime_assets() {
162
173
  cp "${RUNTIME_DIST_DIR}/icu-asset.json" "${destination}/runtime/dist/icu-asset.json"
163
174
  fi
164
175
  cp -R "${RUNTIME_DIST_DIR}/runtime" "${destination}/runtime/dist/runtime"
176
+ if [ -d "${RUNTIME_DIST_DIR}/fonts" ]; then
177
+ mkdir -p "${destination}/runtime"
178
+ cp -R "${RUNTIME_DIST_DIR}/fonts" "${destination}/runtime/fonts"
179
+ fi
165
180
  }
166
181
 
167
182
  build_workers() {
@@ -231,13 +246,19 @@ copy_worker_assets() {
231
246
  generate_host_services "demo/src/host-services.ts" "demoHostServices" "demo/src/generated/HostServices.ts"
232
247
  generate_host_events "demo/src/host-events.ts" "demoHostEvents" "demo/src/generated/HostEvents.ts"
233
248
  generate_host_services "demo/src/worker-host-services.ts" "demoWorkerHostServices" "demo/src/generated/WorkerHostServices.ts"
249
+ generate_host_services "templates/demo-hello-world/src/host/host-services.ts" "appHostServices" "templates/demo-hello-world/src/generated/HostServices.ts" "../FuiPrimitives"
250
+ generate_host_events "templates/demo-hello-world/src/host/host-events.ts" "appHostEvents" "templates/demo-hello-world/src/generated/HostEvents.ts" "../FuiPrimitives"
251
+ generate_host_services "templates/demo-mvc/src/host/host-services.ts" "appHostServices" "templates/demo-mvc/src/generated/HostServices.ts" "../FuiPrimitives"
252
+ generate_host_events "templates/demo-mvc/src/host/host-events.ts" "appHostEvents" "templates/demo-mvc/src/generated/HostEvents.ts" "../FuiPrimitives"
234
253
 
235
254
  build_app "tests/fixtures/smoke/app.ts" "${OUT_DIR}/app.wasm"
236
255
  build_app "demo/src/dashboard.ts" "${DEMO_OUT_DIR}/demo.wasm"
237
256
  build_app "demo/src/routes/demo_home.ts" "${DEMO_OUT_DIR}/home.wasm"
238
257
  build_app "demo/src/routes/demo_advanced_controls.ts" "${DEMO_OUT_DIR}/advanced-controls.wasm"
239
258
  build_app "demo/src/routes/templated-controls.ts" "${DEMO_OUT_DIR}/templated-controls.wasm"
240
- build_app "demo-hello-world/src/App.ts" "${HELLO_OUT_DIR}/app.wasm"
259
+ build_app "templates/demo-mvc/src/routes/mvc_home.ts" "${MVC_OUT_DIR}/mvc-home.wasm"
260
+ build_app "templates/demo-mvc/src/routes/mvc_settings.ts" "${MVC_OUT_DIR}/mvc-settings.wasm"
261
+ build_app "templates/demo-hello-world/src/App.ts" "${HELLO_OUT_DIR}/app.wasm"
241
262
  build_workers
242
263
  write_worker_manifest
243
264
 
@@ -259,7 +280,16 @@ npx esbuild "${PACKAGE_DIR}/demo/harness.ts" \
259
280
  --outfile="${DEMO_OUT_DIR}/harness.js" \
260
281
  --sourcemap
261
282
 
262
- npx esbuild "${PACKAGE_DIR}/demo-hello-world/harness.ts" \
283
+ npx esbuild "${PACKAGE_DIR}/templates/demo-mvc/harness.ts" \
284
+ --bundle \
285
+ --format=esm \
286
+ --platform=browser \
287
+ --target=es2020 \
288
+ --minify \
289
+ --outfile="${MVC_OUT_DIR}/harness.js" \
290
+ --sourcemap
291
+
292
+ npx esbuild "${PACKAGE_DIR}/templates/demo-hello-world/harness.ts" \
263
293
  --bundle \
264
294
  --format=esm \
265
295
  --platform=browser \
@@ -281,6 +311,8 @@ cp "${FILE_PROCESSING_WORKER_BUILD}" "${OUT_DIR}/file-processing-worker.js"
281
311
  cp "${FILE_PROCESSING_WORKER_MAP_BUILD}" "${OUT_DIR}/file-processing-worker.js.map"
282
312
  cp "${FILE_PROCESSING_WORKER_BUILD}" "${DEMO_OUT_DIR}/file-processing-worker.js"
283
313
  cp "${FILE_PROCESSING_WORKER_MAP_BUILD}" "${DEMO_OUT_DIR}/file-processing-worker.js.map"
314
+ cp "${FILE_PROCESSING_WORKER_BUILD}" "${MVC_OUT_DIR}/file-processing-worker.js"
315
+ cp "${FILE_PROCESSING_WORKER_MAP_BUILD}" "${MVC_OUT_DIR}/file-processing-worker.js.map"
284
316
  cp "${FILE_PROCESSING_WORKER_BUILD}" "${HELLO_OUT_DIR}/file-processing-worker.js"
285
317
  cp "${FILE_PROCESSING_WORKER_MAP_BUILD}" "${HELLO_OUT_DIR}/file-processing-worker.js.map"
286
318
 
@@ -304,22 +336,43 @@ npx esbuild "${PACKAGE_DIR}/demo/worker-host-services.ts" \
304
336
 
305
337
  cp "${SMOKE_FIXTURE_DIR}/index.html" "${OUT_DIR}/index.html"
306
338
  cp "${PACKAGE_DIR}/demo/index.html" "${DEMO_OUT_DIR}/index.html"
307
- cp "${PACKAGE_DIR}/demo-hello-world/index.html" "${HELLO_OUT_DIR}/index.html"
339
+ cp "${PACKAGE_DIR}/templates/demo-hello-world/index.html" "${HELLO_OUT_DIR}/index.html"
308
340
  cp "${PACKAGE_DIR}/demo/demo-texture.png" "${DEMO_OUT_DIR}/demo-texture.png"
309
341
  cp "${PACKAGE_DIR}/demo/demo-secondary-texture.png" "${DEMO_OUT_DIR}/demo-secondary-texture.png"
310
342
 
311
343
  mkdir -p "${DEMO_OUT_DIR}/advanced-controls" "${DEMO_OUT_DIR}/templated-controls"
312
344
  cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/advanced-controls/index.html"
313
345
  cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/templated-controls/index.html"
346
+ mkdir -p "${MVC_OUT_DIR}/mvc-home" "${MVC_OUT_DIR}/mvc-settings"
347
+ cp "${PACKAGE_DIR}/templates/demo-mvc/route-shell.html" "${MVC_OUT_DIR}/mvc-home/index.html"
348
+ cp "${PACKAGE_DIR}/templates/demo-mvc/route-shell.html" "${MVC_OUT_DIR}/mvc-settings/index.html"
349
+ cat > "${MVC_OUT_DIR}/index.html" <<'EOF'
350
+ <!doctype html>
351
+ <html lang="en">
352
+ <head>
353
+ <meta charset="utf-8" />
354
+ <meta http-equiv="refresh" content="0; url=./mvc-home/" />
355
+ <title>FUI-AS MVC Demo</title>
356
+ </head>
357
+ <body>
358
+ <p>Redirecting to <a href="./mvc-home/">MVC Home</a>…</p>
359
+ </body>
360
+ </html>
361
+ EOF
314
362
 
315
363
  copy_runtime_assets "${OUT_DIR}"
316
364
  copy_runtime_assets "${DEMO_OUT_DIR}"
365
+ copy_runtime_assets "${MVC_OUT_DIR}"
317
366
  copy_runtime_assets "${HELLO_OUT_DIR}"
318
367
  copy_worker_assets "${OUT_DIR}"
319
368
  copy_worker_assets "${DEMO_OUT_DIR}"
369
+ copy_worker_assets "${MVC_OUT_DIR}"
320
370
  copy_worker_assets "${HELLO_OUT_DIR}"
321
371
  write_runtime_config "${OUT_DIR}" "${DEFAULT_MANIFEST_PATH}"
322
372
  write_runtime_config "${DEMO_OUT_DIR}" "${DEFAULT_MANIFEST_PATH}"
373
+ write_runtime_config "${MVC_OUT_DIR}" "${DEFAULT_MANIFEST_PATH}"
323
374
  write_runtime_config "${HELLO_OUT_DIR}" "${DEFAULT_MANIFEST_PATH}"
324
375
  write_runtime_config "${DEMO_OUT_DIR}/advanced-controls" "../runtime/dist/effindom.v2.manifest.json"
325
376
  write_runtime_config "${DEMO_OUT_DIR}/templated-controls" "../runtime/dist/effindom.v2.manifest.json"
377
+ write_runtime_config "${MVC_OUT_DIR}/mvc-home" "../runtime/dist/effindom.v2.manifest.json"
378
+ write_runtime_config "${MVC_OUT_DIR}/mvc-settings" "../runtime/dist/effindom.v2.manifest.json"
@@ -32,6 +32,12 @@ function asTypeName(type: HostServiceTypeName): string {
32
32
  return "i32";
33
33
  case "f64":
34
34
  return "f64";
35
+ case "bytes":
36
+ return "Uint8Array";
37
+ case "i32_array":
38
+ return "Int32Array";
39
+ case "f64_array":
40
+ return "Float64Array";
35
41
  case "void":
36
42
  return "void";
37
43
  }
@@ -53,7 +59,7 @@ function callbackTypeFor(method: NormalizedHostEventMethod): string {
53
59
  function emitExportArgs(method: NormalizedHostEventMethod): string {
54
60
  const parts: Array<string> = [];
55
61
  method.args.forEach((type, index) => {
56
- if (type === "string") {
62
+ if (type === "string" || type === "bytes" || type === "i32_array" || type === "f64_array") {
57
63
  parts.push(`arg${String(index)}Ptr: usize`, `arg${String(index)}Len: u32`);
58
64
  return;
59
65
  }
@@ -66,6 +72,26 @@ function emitDecodedArgs(method: NormalizedHostEventMethod): Array<string> {
66
72
  const lines: Array<string> = [];
67
73
  method.args.forEach((type, index) => {
68
74
  if (type !== "string") {
75
+ if (type === "bytes") {
76
+ lines.push(` const arg${String(index)} = new Uint8Array(<i32>arg${String(index)}Len);`);
77
+ lines.push(` if (arg${String(index)}Len > 0) {`);
78
+ lines.push(` memory.copy(arg${String(index)}.dataStart, arg${String(index)}Ptr, <usize>arg${String(index)}Len);`);
79
+ lines.push(" }");
80
+ return;
81
+ }
82
+ if (type === "i32_array") {
83
+ lines.push(` const arg${String(index)} = new Int32Array(<i32>arg${String(index)}Len);`);
84
+ lines.push(` if (arg${String(index)}Len > 0) {`);
85
+ lines.push(` memory.copy(arg${String(index)}.dataStart, arg${String(index)}Ptr, <usize>arg${String(index)}Len << 2);`);
86
+ lines.push(" }");
87
+ return;
88
+ }
89
+ if (type === "f64_array") {
90
+ lines.push(` const arg${String(index)} = new Float64Array(<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 << 3);`);
93
+ lines.push(" }");
94
+ }
69
95
  return;
70
96
  }
71
97
  lines.push(
@@ -141,8 +167,9 @@ function emitBindingsFile(
141
167
  exportName: string,
142
168
  outputPath: string,
143
169
  methods: ReturnType<typeof listHostEventMethods>,
170
+ primitivesImportOverride: string | undefined,
144
171
  ): string {
145
- const callbackImport = relativeImport(outputPath, path.resolve(PACKAGE_DIR, "src/FuiPrimitives.ts"));
172
+ const callbackImport = primitivesImportOverride ?? relativeImport(outputPath, path.resolve(PACKAGE_DIR, "src/FuiPrimitives.ts"));
146
173
  const blocks: Array<string> = [
147
174
  `// Generated by scripts/generate-host-events.ts from ${toPosix(sourceModulePath)}#${exportName}.`,
148
175
  `import { Callback0, Callback1, Callback2 } from "${callbackImport}";`,
@@ -159,15 +186,15 @@ function emitBindingsFile(
159
186
  }
160
187
 
161
188
  async function main(): Promise<void> {
162
- const [moduleArg, exportName, outputArg] = process.argv.slice(2);
189
+ const [moduleArg, exportName, outputArg, primitivesImportArg] = process.argv.slice(2);
163
190
  if (moduleArg === undefined || exportName === undefined || outputArg === undefined) {
164
- throw new Error("Usage: generate-host-events <module-path> <export-name> <output-path>");
191
+ throw new Error("Usage: generate-host-events <module-path> <export-name> <output-path> [primitives-import]");
165
192
  }
166
193
  const modulePath = path.resolve(process.cwd(), moduleArg);
167
194
  const outputPath = path.resolve(process.cwd(), outputArg);
168
195
  const registry = await loadHostEvents(modulePath, exportName);
169
196
  const methods = listHostEventMethods(registry as never);
170
- const content = emitBindingsFile(modulePath, exportName, outputPath, methods);
197
+ const content = emitBindingsFile(modulePath, exportName, outputPath, methods, primitivesImportArg);
171
198
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
172
199
  await fs.writeFile(outputPath, content, "utf8");
173
200
  }
@@ -31,24 +31,38 @@ function asTypeName(type: HostServiceTypeName): string {
31
31
  return "i32";
32
32
  case "f64":
33
33
  return "f64";
34
+ case "bytes":
35
+ return "Uint8Array";
36
+ case "i32_array":
37
+ return "Int32Array";
38
+ case "f64_array":
39
+ return "Float64Array";
34
40
  case "void":
35
41
  return "void";
36
42
  }
37
43
  }
38
44
 
45
+ function isPointerLengthType(type: HostServiceTypeName): boolean {
46
+ return type === "string" || type === "bytes" || type === "i32_array" || type === "f64_array";
47
+ }
48
+
49
+ function returnsBufferType(type: HostServiceTypeName): boolean {
50
+ return type === "string" || type === "bytes" || type === "i32_array" || type === "f64_array";
51
+ }
52
+
39
53
  function emitExternalSignature(importName: string, args: readonly HostServiceTypeName[], returns: HostServiceTypeName): string {
40
54
  const signatureParts: Array<string> = [];
41
55
  args.forEach((type, index) => {
42
- if (type === "string") {
56
+ if (isPointerLengthType(type)) {
43
57
  signatureParts.push(`arg${String(index)}Ptr: usize`, `arg${String(index)}Len: u32`);
44
58
  return;
45
59
  }
46
60
  signatureParts.push(`arg${String(index)}: ${asTypeName(type)}`);
47
61
  });
48
- if (returns === "string") {
62
+ if (returnsBufferType(returns)) {
49
63
  signatureParts.push("resultPtr: usize", "resultCap: u32");
50
64
  }
51
- const returnType = returns === "string" ? "u32" : asTypeName(returns);
65
+ const returnType = returnsBufferType(returns) ? "u32" : asTypeName(returns);
52
66
  return [
53
67
  `@external("fui_host_service", "${importName}")`,
54
68
  `declare function __host_${importName}(${signatureParts.join(", ")}): ${returnType};`,
@@ -61,6 +75,10 @@ function emitWrapper(importName: string, args: readonly HostServiceTypeName[], r
61
75
  args.forEach((type, index) => {
62
76
  if (type === "string") {
63
77
  lines.push(` const arg${String(index)}Bytes = Uint8Array.wrap(String.UTF8.encode(arg${String(index)}, false));`);
78
+ return;
79
+ }
80
+ if (type === "bytes" || type === "i32_array" || type === "f64_array") {
81
+ lines.push(` const arg${String(index)}Bytes = arg${String(index)};`);
64
82
  }
65
83
  });
66
84
  const callArgs: Array<string> = [];
@@ -70,14 +88,32 @@ function emitWrapper(importName: string, args: readonly HostServiceTypeName[], r
70
88
  callArgs.push(`<u32>arg${String(index)}Bytes.length`);
71
89
  return;
72
90
  }
91
+ if (type === "bytes") {
92
+ callArgs.push(`arg${String(index)}Bytes.length > 0 ? arg${String(index)}Bytes.dataStart : 0`);
93
+ callArgs.push(`<u32>arg${String(index)}Bytes.length`);
94
+ return;
95
+ }
96
+ if (type === "i32_array" || type === "f64_array") {
97
+ callArgs.push(`arg${String(index)}Bytes.length > 0 ? arg${String(index)}Bytes.dataStart : 0`);
98
+ callArgs.push(`<u32>arg${String(index)}Bytes.length`);
99
+ return;
100
+ }
73
101
  callArgs.push(`arg${String(index)}`);
74
102
  });
75
- if (returns === "string") {
103
+ if (returnsBufferType(returns)) {
76
104
  lines.push(" const resultPtr = hostServiceResultBufferPtr();");
77
105
  lines.push(" const resultCap = hostServiceResultBufferSize();");
78
106
  callArgs.push("resultPtr", "resultCap");
79
107
  lines.push(` const resultLen = __host_${importName}(${callArgs.join(", ")});`);
80
- lines.push(` return decodeHostServiceStringResult(resultPtr, resultLen, "${importName}");`);
108
+ if (returns === "string") {
109
+ lines.push(` return decodeHostServiceStringResult(resultPtr, resultLen, "${importName}");`);
110
+ } else if (returns === "bytes") {
111
+ lines.push(` return decodeHostServiceBytesResult(resultPtr, resultLen, "${importName}");`);
112
+ } else if (returns === "i32_array") {
113
+ lines.push(` return decodeHostServiceI32ArrayResult(resultPtr, resultLen, "${importName}");`);
114
+ } else {
115
+ lines.push(` return decodeHostServiceF64ArrayResult(resultPtr, resultLen, "${importName}");`);
116
+ }
81
117
  } else if (returns === "void") {
82
118
  lines.push(` __host_${importName}(${callArgs.join(", ")});`);
83
119
  } else {
@@ -118,13 +154,35 @@ function emitBindingsFile(
118
154
  exportName: string,
119
155
  outputPath: string,
120
156
  methods: ReturnType<typeof listHostServiceMethods>,
157
+ primitivesImportOverride: string | undefined,
121
158
  ): string {
122
- const runtimeImport = relativeImport(outputPath, path.resolve(PACKAGE_DIR, "src/FuiPrimitives.ts"));
159
+ const runtimeImport = primitivesImportOverride ?? relativeImport(outputPath, path.resolve(PACKAGE_DIR, "src/FuiPrimitives.ts"));
123
160
  const blocks: Array<string> = [
124
161
  `// Generated by scripts/generate-host-services.ts from ${toPosix(sourceModulePath)}#${exportName}.`,
125
162
  ];
126
- if (methods.some((method) => method.returns === "string")) {
127
- blocks.push(`import { decodeHostServiceStringResult, hostServiceResultBufferPtr, hostServiceResultBufferSize } from "${runtimeImport}";`);
163
+ if (methods.some((method) => returnsBufferType(method.returns))) {
164
+ const helpers = [
165
+ "hostServiceResultBufferPtr",
166
+ "hostServiceResultBufferSize",
167
+ ...new Set(
168
+ methods
169
+ .map((method) => method.returns)
170
+ .filter((type) => returnsBufferType(type))
171
+ .map((type) => {
172
+ if (type === "string") {
173
+ return "decodeHostServiceStringResult";
174
+ }
175
+ if (type === "bytes") {
176
+ return "decodeHostServiceBytesResult";
177
+ }
178
+ if (type === "i32_array") {
179
+ return "decodeHostServiceI32ArrayResult";
180
+ }
181
+ return "decodeHostServiceF64ArrayResult";
182
+ }),
183
+ ),
184
+ ];
185
+ blocks.push(`import { ${helpers.join(", ")} } from "${runtimeImport}";`);
128
186
  blocks.push("");
129
187
  } else {
130
188
  blocks.push("");
@@ -141,15 +199,15 @@ function emitBindingsFile(
141
199
  }
142
200
 
143
201
  async function main(): Promise<void> {
144
- const [moduleArg, exportName, outputArg] = process.argv.slice(2);
202
+ const [moduleArg, exportName, outputArg, primitivesImportArg] = process.argv.slice(2);
145
203
  if (moduleArg === undefined || exportName === undefined || outputArg === undefined) {
146
- throw new Error("Usage: generate-host-services <module-path> <export-name> <output-path>");
204
+ throw new Error("Usage: generate-host-services <module-path> <export-name> <output-path> [primitives-import]");
147
205
  }
148
206
  const modulePath = path.resolve(process.cwd(), moduleArg);
149
207
  const outputPath = path.resolve(process.cwd(), outputArg);
150
208
  const registry = await loadHostServices(modulePath, exportName);
151
209
  const methods = listHostServiceMethods(registry as never);
152
- const content = emitBindingsFile(modulePath, exportName, outputPath, methods);
210
+ const content = emitBindingsFile(modulePath, exportName, outputPath, methods, primitivesImportArg);
153
211
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
154
212
  await fs.writeFile(outputPath, content, "utf8");
155
213
  }
@@ -12,4 +12,11 @@ export {
12
12
  } from "./core/BoundCallback";
13
13
  export { bind0, bind1, bind2, bindResult0, bindResult1 } from "./core/bind";
14
14
  export { clearCurrentSelection, tryGetBounds } from "./bindings/ui";
15
- export { decodeHostServiceStringResult, hostServiceResultBufferPtr, hostServiceResultBufferSize } from "./host-services/runtime";
15
+ export {
16
+ decodeHostServiceBytesResult,
17
+ decodeHostServiceF64ArrayResult,
18
+ decodeHostServiceI32ArrayResult,
19
+ decodeHostServiceStringResult,
20
+ hostServiceResultBufferPtr,
21
+ hostServiceResultBufferSize,
22
+ } from "./host-services/runtime";
@@ -8,7 +8,7 @@ export function hostServiceResultBufferSize(): u32 {
8
8
  return __fui_text_buffer_size();
9
9
  }
10
10
 
11
- export function decodeHostServiceStringResult(resultPtr: usize, resultLen: u32, importName: string): string {
11
+ function assertResultByteLength(resultLen: u32, importName: string): void {
12
12
  const capacity = __fui_text_buffer_size();
13
13
  if (resultLen > capacity) {
14
14
  throw new Error(
@@ -21,5 +21,42 @@ export function decodeHostServiceStringResult(resultPtr: usize, resultLen: u32,
21
21
  ".",
22
22
  );
23
23
  }
24
+ }
25
+
26
+ export function decodeHostServiceStringResult(resultPtr: usize, resultLen: u32, importName: string): string {
27
+ assertResultByteLength(resultLen, importName);
24
28
  return resultLen == 0 ? "" : String.UTF8.decodeUnsafe(resultPtr, <usize>resultLen, false);
25
29
  }
30
+
31
+ export function decodeHostServiceBytesResult(resultPtr: usize, resultLen: u32, importName: string): Uint8Array {
32
+ assertResultByteLength(resultLen, importName);
33
+ const bytes = new Uint8Array(<i32>resultLen);
34
+ if (resultLen > 0) {
35
+ memory.copy(bytes.dataStart, resultPtr, <usize>resultLen);
36
+ }
37
+ return bytes;
38
+ }
39
+
40
+ export function decodeHostServiceI32ArrayResult(resultPtr: usize, resultLen: u32, importName: string): Int32Array {
41
+ assertResultByteLength(resultLen, importName);
42
+ if ((resultLen & 3) != 0) {
43
+ throw new Error("Host service " + importName + " returned misaligned Int32Array byte length.");
44
+ }
45
+ const values = new Int32Array(<i32>(resultLen >> 2));
46
+ if (resultLen > 0) {
47
+ memory.copy(values.dataStart, resultPtr, <usize>resultLen);
48
+ }
49
+ return values;
50
+ }
51
+
52
+ export function decodeHostServiceF64ArrayResult(resultPtr: usize, resultLen: u32, importName: string): Float64Array {
53
+ assertResultByteLength(resultLen, importName);
54
+ if ((resultLen & 7) != 0) {
55
+ throw new Error("Host service " + importName + " returned misaligned Float64Array byte length.");
56
+ }
57
+ const values = new Float64Array(<i32>(resultLen >> 3));
58
+ if (resultLen > 0) {
59
+ memory.copy(values.dataStart, resultPtr, <usize>resultLen);
60
+ }
61
+ return values;
62
+ }