@effindomv2/fui-as 0.1.0

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 (137) hide show
  1. package/LICENSE.md +7 -0
  2. package/browser/src/common-harness/host-imports.ts +430 -0
  3. package/browser/src/common-harness/interop.ts +39 -0
  4. package/browser/src/common-harness/managed-harness-bitmap-host.ts +92 -0
  5. package/browser/src/common-harness/managed-harness-fetch-host.ts +201 -0
  6. package/browser/src/common-harness/managed-harness-file-host.ts +1101 -0
  7. package/browser/src/common-harness/managed-harness-file-payloads.ts +143 -0
  8. package/browser/src/common-harness/managed-harness-file-types.ts +106 -0
  9. package/browser/src/common-harness/managed-harness-session.ts +15 -0
  10. package/browser/src/common-harness/managed-harness.ts +1323 -0
  11. package/browser/src/common-harness/managed-history.ts +168 -0
  12. package/browser/src/common-harness/persisted-restore-policy.ts +50 -0
  13. package/browser/src/common-harness/persisted-ui-state-controller.ts +309 -0
  14. package/browser/src/common-harness/text-session-bridge.ts +452 -0
  15. package/browser/src/common-harness/types.ts +205 -0
  16. package/browser/src/common-harness/ui-chrome.ts +191 -0
  17. package/browser/src/common-harness/ui-imports.ts +529 -0
  18. package/browser/src/common-harness/wasm-module-cache.ts +47 -0
  19. package/browser/src/common-harness.ts +27 -0
  20. package/browser/src/file-processing-worker.ts +89 -0
  21. package/browser/src/host-events.ts +97 -0
  22. package/browser/src/host-services.ts +203 -0
  23. package/browser/src/index.ts +62 -0
  24. package/browser/src/persisted-ui-state.ts +206 -0
  25. package/browser/src/routed-harness.ts +198 -0
  26. package/browser/src/worker-bootstrap.ts +483 -0
  27. package/browser/src/worker-manager.ts +230 -0
  28. package/browser/src/worker-types.ts +50 -0
  29. package/package.json +89 -0
  30. package/scripts/build-demo-as.sh +91 -0
  31. package/scripts/build.sh +325 -0
  32. package/scripts/generate-host-events.ts +175 -0
  33. package/scripts/generate-host-services.ts +157 -0
  34. package/src/Fui.ts +205 -0
  35. package/src/FuiExports.ts +55 -0
  36. package/src/FuiPrimitives.ts +15 -0
  37. package/src/FuiWorker.ts +3 -0
  38. package/src/FuiWorkerExports.ts +6 -0
  39. package/src/bindings/ui.ts +531 -0
  40. package/src/color.ts +86 -0
  41. package/src/controls/AntiSelectionArea.ts +23 -0
  42. package/src/controls/Button.ts +750 -0
  43. package/src/controls/Checkbox.ts +181 -0
  44. package/src/controls/ContextMenu.ts +885 -0
  45. package/src/controls/ControlTemplateSet.ts +37 -0
  46. package/src/controls/Dialog.ts +355 -0
  47. package/src/controls/Dropdown.ts +856 -0
  48. package/src/controls/Form.ts +110 -0
  49. package/src/controls/NavLink.ts +211 -0
  50. package/src/controls/Popup.ts +129 -0
  51. package/src/controls/ProgressBar.ts +180 -0
  52. package/src/controls/RadioButton.ts +135 -0
  53. package/src/controls/RadioGroup.ts +244 -0
  54. package/src/controls/SelectionArea.ts +75 -0
  55. package/src/controls/Slider.ts +471 -0
  56. package/src/controls/Switch.ts +132 -0
  57. package/src/controls/TextArea.ts +20 -0
  58. package/src/controls/TextInput.ts +7 -0
  59. package/src/controls/index.ts +18 -0
  60. package/src/controls/internal/ButtonPresenter.ts +95 -0
  61. package/src/controls/internal/CheckboxIndicatorPresenter.ts +93 -0
  62. package/src/controls/internal/DropdownChevronPresenter.ts +67 -0
  63. package/src/controls/internal/DropdownFieldPresenter.ts +110 -0
  64. package/src/controls/internal/DropdownOptionRowPresenter.ts +82 -0
  65. package/src/controls/internal/PopupPresenter.ts +198 -0
  66. package/src/controls/internal/PressableIndicatorPresenter.ts +32 -0
  67. package/src/controls/internal/PressableLabeledControl.ts +221 -0
  68. package/src/controls/internal/RadioIndicatorPresenter.ts +73 -0
  69. package/src/controls/internal/SliderPresenter.ts +157 -0
  70. package/src/controls/internal/SwitchIndicatorPresenter.ts +72 -0
  71. package/src/controls/internal/TextInputCore.ts +695 -0
  72. package/src/controls/internal/TextInputPresenter.ts +72 -0
  73. package/src/controls/templating.ts +54 -0
  74. package/src/core/Action.ts +94 -0
  75. package/src/core/Actions.ts +37 -0
  76. package/src/core/Animation.ts +412 -0
  77. package/src/core/Application.ts +328 -0
  78. package/src/core/Assets.ts +264 -0
  79. package/src/core/AttachedProperties.ts +32 -0
  80. package/src/core/Bitmap.ts +70 -0
  81. package/src/core/BoundCallback.ts +104 -0
  82. package/src/core/Callbacks.ts +17 -0
  83. package/src/core/ContextMenuManager.ts +466 -0
  84. package/src/core/DebugApi.ts +30 -0
  85. package/src/core/Disposable.ts +10 -0
  86. package/src/core/DragDropManager.ts +179 -0
  87. package/src/core/DragGesture.ts +184 -0
  88. package/src/core/DynamicAssetIds.ts +24 -0
  89. package/src/core/Errors.ts +48 -0
  90. package/src/core/EventRouter.ts +408 -0
  91. package/src/core/ExternalDropManager.ts +122 -0
  92. package/src/core/Fetch.ts +264 -0
  93. package/src/core/FetchFfi.ts +15 -0
  94. package/src/core/File.ts +1002 -0
  95. package/src/core/FocusAdornerManager.ts +263 -0
  96. package/src/core/FocusVisibility.ts +36 -0
  97. package/src/core/FrameScheduler.ts +28 -0
  98. package/src/core/KeyboardScroll.ts +161 -0
  99. package/src/core/KeyboardScrollTracker.ts +386 -0
  100. package/src/core/Logger.ts +80 -0
  101. package/src/core/Navigation.ts +13 -0
  102. package/src/core/Node.ts +1708 -0
  103. package/src/core/PersistedState.ts +102 -0
  104. package/src/core/PersistedUiState.ts +142 -0
  105. package/src/core/Platform.ts +219 -0
  106. package/src/core/Signal.ts +89 -0
  107. package/src/core/Theme.ts +365 -0
  108. package/src/core/Timers.ts +129 -0
  109. package/src/core/ToolTip.ts +122 -0
  110. package/src/core/ToolTipManager.ts +459 -0
  111. package/src/core/Transitions.ts +34 -0
  112. package/src/core/Typography.ts +204 -0
  113. package/src/core/Worker.ts +196 -0
  114. package/src/core/bind.ts +37 -0
  115. package/src/core/event_exports.ts +596 -0
  116. package/src/core/ffi.ts +728 -0
  117. package/src/host-services/runtime.ts +25 -0
  118. package/src/nodes/FlexBox.ts +789 -0
  119. package/src/nodes/GradientStop.ts +9 -0
  120. package/src/nodes/Grid.ts +183 -0
  121. package/src/nodes/Image.ts +189 -0
  122. package/src/nodes/Portal.ts +14 -0
  123. package/src/nodes/RichText.ts +312 -0
  124. package/src/nodes/ScrollBar.ts +570 -0
  125. package/src/nodes/ScrollBox.ts +415 -0
  126. package/src/nodes/ScrollState.ts +10 -0
  127. package/src/nodes/ScrollView.ts +511 -0
  128. package/src/nodes/Svg.ts +142 -0
  129. package/src/nodes/Text.ts +145 -0
  130. package/src/nodes/TextCore.ts +558 -0
  131. package/src/nodes/VirtualList.ts +431 -0
  132. package/src/nodes/helpers.ts +25 -0
  133. package/src/nodes/index.ts +14 -0
  134. package/src/tsconfig.json +7 -0
  135. package/src/worker/Worker.ts +169 -0
  136. package/src/worker/WorkerJob.ts +65 -0
  137. package/src/worker/ffi.ts +23 -0
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ PACKAGE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ REPO_ROOT="$(cd "${PACKAGE_DIR}/../.." && pwd)"
7
+ BROWSER_SRC_DIR="${PACKAGE_DIR}/browser/src"
8
+ SMOKE_FIXTURE_DIR="${PACKAGE_DIR}/tests/fixtures/smoke"
9
+ OUT_DIR="${REPO_ROOT}/public/v2/fui-as"
10
+ DEMO_OUT_DIR="${OUT_DIR}/demo"
11
+ HELLO_OUT_DIR="${OUT_DIR}/demo-hello-world"
12
+ WORKER_BUILD_DIR="${PACKAGE_DIR}/build/workers"
13
+ WORKER_BOOTSTRAP_BUILD="${PACKAGE_DIR}/build/worker-bootstrap.js"
14
+ WORKER_BOOTSTRAP_MAP_BUILD="${PACKAGE_DIR}/build/worker-bootstrap.js.map"
15
+ WORKER_HOST_SERVICES_BUILD="${PACKAGE_DIR}/build/worker-host-services.js"
16
+ WORKER_HOST_SERVICES_MAP_BUILD="${PACKAGE_DIR}/build/worker-host-services.js.map"
17
+ WORKER_MANIFEST_BUILD="${PACKAGE_DIR}/build/worker-manifest.json"
18
+ FILE_PROCESSING_WORKER_BUILD="${PACKAGE_DIR}/build/file-processing-worker.js"
19
+ FILE_PROCESSING_WORKER_MAP_BUILD="${PACKAGE_DIR}/build/file-processing-worker.js.map"
20
+ HOST_SERVICE_GENERATOR_BUILD="${PACKAGE_DIR}/build/generate-host-services.mjs"
21
+ HOST_EVENT_GENERATOR_BUILD="${PACKAGE_DIR}/build/generate-host-events.mjs"
22
+ RUNTIME_CONFIG_FILE="effindom-runtime-config.js"
23
+ DEFAULT_MANIFEST_PATH="./runtime/dist/effindom.v2.manifest.json"
24
+
25
+ rm -rf "${OUT_DIR}"
26
+ mkdir -p "${PACKAGE_DIR}/build" "${OUT_DIR}" "${DEMO_OUT_DIR}" "${HELLO_OUT_DIR}" "${WORKER_BUILD_DIR}"
27
+
28
+ cd "${PACKAGE_DIR}"
29
+
30
+ if [ -x "${PACKAGE_DIR}/node_modules/.bin/tsc" ]; then
31
+ TSC_BIN="${PACKAGE_DIR}/node_modules/.bin/tsc"
32
+ elif [ -x "${REPO_ROOT}/node_modules/.bin/tsc" ]; then
33
+ TSC_BIN="${REPO_ROOT}/node_modules/.bin/tsc"
34
+ else
35
+ echo "Could not locate tsc in node_modules/.bin." >&2
36
+ exit 1
37
+ fi
38
+
39
+ "${TSC_BIN}" -p tsconfig.json --noEmit
40
+
41
+ build_app() {
42
+ local entry_file="$1"
43
+ local wasm_out="$2"
44
+
45
+ npx asc "${entry_file}" --config asconfig.json --target release
46
+ cp "${PACKAGE_DIR}/build/app.wasm" "${wasm_out}"
47
+ if [ -f "${PACKAGE_DIR}/build/app.wasm.map" ]; then
48
+ cp "${PACKAGE_DIR}/build/app.wasm.map" "${wasm_out}.map"
49
+ fi
50
+ }
51
+
52
+ build_worker() {
53
+ local entry_file="$1"
54
+ local wasm_out="$2"
55
+
56
+ npx asc "${entry_file}" --config asconfig.json --target release
57
+ cp "${PACKAGE_DIR}/build/app.wasm" "${wasm_out}"
58
+ if [ -f "${PACKAGE_DIR}/build/app.wasm.map" ]; then
59
+ cp "${PACKAGE_DIR}/build/app.wasm.map" "${wasm_out}.map"
60
+ fi
61
+ }
62
+
63
+ generate_host_services() {
64
+ local definition_file="$1"
65
+ local export_name="$2"
66
+ local output_file="$3"
67
+
68
+ npx esbuild "${PACKAGE_DIR}/scripts/generate-host-services.ts" \
69
+ --bundle \
70
+ --format=esm \
71
+ --platform=node \
72
+ --target=node20 \
73
+ --packages=external \
74
+ --outfile="${HOST_SERVICE_GENERATOR_BUILD}"
75
+
76
+ node "${HOST_SERVICE_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}"
77
+ }
78
+
79
+ generate_host_events() {
80
+ local definition_file="$1"
81
+ local export_name="$2"
82
+ local output_file="$3"
83
+
84
+ npx esbuild "${PACKAGE_DIR}/scripts/generate-host-events.ts" \
85
+ --bundle \
86
+ --format=esm \
87
+ --platform=node \
88
+ --target=node20 \
89
+ --packages=external \
90
+ --outfile="${HOST_EVENT_GENERATOR_BUILD}"
91
+
92
+ node "${HOST_EVENT_GENERATOR_BUILD}" "${definition_file}" "${export_name}" "${output_file}"
93
+ }
94
+
95
+ find_worker_entries() {
96
+ find \
97
+ "${PACKAGE_DIR}/src/workers" \
98
+ "${PACKAGE_DIR}/demo/src/workers" \
99
+ "${SMOKE_FIXTURE_DIR}/workers" \
100
+ -maxdepth 1 -type f -name '*.ts' 2>/dev/null | sort
101
+ }
102
+
103
+ resolve_runtime_dist_dir() {
104
+ local candidate=""
105
+ local candidates=()
106
+
107
+ if [ -n "${EFFINDOM_RUNTIME_DIST_DIR:-}" ]; then
108
+ candidates+=("${EFFINDOM_RUNTIME_DIST_DIR}")
109
+ fi
110
+
111
+ candidates+=(
112
+ "${PACKAGE_DIR}/node_modules/@effindomv2/runtime/dist"
113
+ "${REPO_ROOT}/node_modules/@effindomv2/runtime/dist"
114
+ "${REPO_ROOT}/v2/browser-bridge/dist"
115
+ "${REPO_ROOT}/public/v2/browser-bridge"
116
+ )
117
+
118
+ for candidate in "${candidates[@]}"; do
119
+ if [ -f "${candidate}/bridge.js" ] && [ -f "${candidate}/effindom.v2.manifest.json" ] && [ -d "${candidate}/runtime" ]; then
120
+ printf '%s\n' "${candidate}"
121
+ return 0
122
+ fi
123
+ done
124
+
125
+ echo "Could not locate runtime dist assets." >&2
126
+ echo "Expected one of:" >&2
127
+ echo " - \$EFFINDOM_RUNTIME_DIST_DIR" >&2
128
+ echo " - ${PACKAGE_DIR}/node_modules/@effindomv2/runtime/dist" >&2
129
+ echo " - ${REPO_ROOT}/node_modules/@effindomv2/runtime/dist" >&2
130
+ echo " - ${REPO_ROOT}/v2/browser-bridge/dist" >&2
131
+ echo " - ${REPO_ROOT}/public/v2/browser-bridge" >&2
132
+ echo "Install @effindomv2/runtime or build runtime assets first." >&2
133
+ exit 1
134
+ }
135
+
136
+ RUNTIME_DIST_DIR="$(resolve_runtime_dist_dir)"
137
+
138
+ write_runtime_config() {
139
+ local destination="$1"
140
+ local manifest_url="$2"
141
+
142
+ cat > "${destination}/${RUNTIME_CONFIG_FILE}" <<EOF
143
+ window.__effindomRuntime = Object.assign({}, window.__effindomRuntime, {
144
+ manifestUrl: '${manifest_url}',
145
+ });
146
+ EOF
147
+ }
148
+
149
+ copy_runtime_assets() {
150
+ local destination="$1"
151
+ cp "${RUNTIME_DIST_DIR}/bridge.js" "${destination}/bridge.js"
152
+ if [ -f "${RUNTIME_DIST_DIR}/bridge.js.map" ]; then
153
+ cp "${RUNTIME_DIST_DIR}/bridge.js.map" "${destination}/bridge.js.map"
154
+ else
155
+ rm -f "${destination}/bridge.js.map"
156
+ fi
157
+ rm -f "${destination}/effindom.v2.manifest.json" "${destination}/icu-asset.json"
158
+ rm -rf "${destination}/runtime"
159
+ mkdir -p "${destination}/runtime/dist"
160
+ cp "${RUNTIME_DIST_DIR}/effindom.v2.manifest.json" "${destination}/runtime/dist/effindom.v2.manifest.json"
161
+ if [ -f "${RUNTIME_DIST_DIR}/icu-asset.json" ]; then
162
+ cp "${RUNTIME_DIST_DIR}/icu-asset.json" "${destination}/runtime/dist/icu-asset.json"
163
+ fi
164
+ cp -R "${RUNTIME_DIST_DIR}/runtime" "${destination}/runtime/dist/runtime"
165
+ }
166
+
167
+ build_workers() {
168
+ rm -rf "${WORKER_BUILD_DIR}"
169
+ mkdir -p "${WORKER_BUILD_DIR}"
170
+
171
+ local worker_entry=""
172
+ while IFS= read -r worker_entry; do
173
+ [ -n "${worker_entry}" ] || continue
174
+ local worker_name
175
+ worker_name="$(basename "${worker_entry}" .ts)"
176
+ build_worker "${worker_entry}" "${WORKER_BUILD_DIR}/${worker_name}.wasm"
177
+ done < <(find_worker_entries)
178
+ }
179
+
180
+ write_worker_manifest() {
181
+ declare -A worker_entries=()
182
+ local worker_entry=""
183
+ while IFS= read -r worker_entry; do
184
+ [ -n "${worker_entry}" ] || continue
185
+ local worker_name
186
+ worker_name="$(basename "${worker_entry}" .ts)"
187
+ local export_name=""
188
+ while IFS= read -r export_name; do
189
+ [ -n "${export_name}" ] || continue
190
+ if [ -n "${worker_entries[$export_name]:-}" ]; then
191
+ echo "Duplicate worker export name: ${export_name}" >&2
192
+ exit 1
193
+ fi
194
+ worker_entries["${export_name}"]="./workers/${worker_name}.wasm"
195
+ done < <(rg 'export function ([A-Za-z_][A-Za-z0-9_]*)\s*\(' "${worker_entry}" -or '$1')
196
+ done < <(find_worker_entries)
197
+
198
+ {
199
+ printf '{\n "version": 1,\n "entries": {\n'
200
+ local first=1
201
+ local entry_name=""
202
+ while IFS= read -r entry_name; do
203
+ [ -n "${entry_name}" ] || continue
204
+ if [ "${first}" -eq 0 ]; then
205
+ printf ',\n'
206
+ fi
207
+ first=0
208
+ printf ' "%s": "%s"' "${entry_name}" "${worker_entries[$entry_name]}"
209
+ done < <(printf '%s\n' "${!worker_entries[@]}" | sort)
210
+ printf '\n }\n}\n'
211
+ } > "${WORKER_MANIFEST_BUILD}"
212
+ }
213
+
214
+ copy_worker_assets() {
215
+ local destination="$1"
216
+ rm -rf "${destination}/workers"
217
+ mkdir -p "${destination}/workers"
218
+ shopt -s nullglob
219
+ local worker_asset=""
220
+ for worker_asset in "${WORKER_BUILD_DIR}"/*; do
221
+ cp "${worker_asset}" "${destination}/workers/$(basename "${worker_asset}")"
222
+ done
223
+ shopt -u nullglob
224
+ cp "${WORKER_BOOTSTRAP_BUILD}" "${destination}/worker-bootstrap.js"
225
+ cp "${WORKER_BOOTSTRAP_MAP_BUILD}" "${destination}/worker-bootstrap.js.map"
226
+ cp "${WORKER_HOST_SERVICES_BUILD}" "${destination}/worker-host-services.js"
227
+ cp "${WORKER_HOST_SERVICES_MAP_BUILD}" "${destination}/worker-host-services.js.map"
228
+ cp "${WORKER_MANIFEST_BUILD}" "${destination}/worker-manifest.json"
229
+ }
230
+
231
+ generate_host_services "demo/src/host-services.ts" "demoHostServices" "demo/src/generated/HostServices.ts"
232
+ generate_host_events "demo/src/host-events.ts" "demoHostEvents" "demo/src/generated/HostEvents.ts"
233
+ generate_host_services "demo/src/worker-host-services.ts" "demoWorkerHostServices" "demo/src/generated/WorkerHostServices.ts"
234
+
235
+ build_app "tests/fixtures/smoke/app.ts" "${OUT_DIR}/app.wasm"
236
+ build_app "demo/src/dashboard.ts" "${DEMO_OUT_DIR}/demo.wasm"
237
+ build_app "demo/src/routes/demo_home.ts" "${DEMO_OUT_DIR}/home.wasm"
238
+ build_app "demo/src/routes/demo_advanced_controls.ts" "${DEMO_OUT_DIR}/advanced-controls.wasm"
239
+ 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"
241
+ build_workers
242
+ write_worker_manifest
243
+
244
+ npx esbuild "${SMOKE_FIXTURE_DIR}/harness.ts" \
245
+ --bundle \
246
+ --format=esm \
247
+ --platform=browser \
248
+ --target=es2020 \
249
+ --minify \
250
+ --outfile="${OUT_DIR}/harness.js" \
251
+ --sourcemap
252
+
253
+ npx esbuild "${PACKAGE_DIR}/demo/harness.ts" \
254
+ --bundle \
255
+ --format=esm \
256
+ --platform=browser \
257
+ --target=es2020 \
258
+ --minify \
259
+ --outfile="${DEMO_OUT_DIR}/harness.js" \
260
+ --sourcemap
261
+
262
+ npx esbuild "${PACKAGE_DIR}/demo-hello-world/harness.ts" \
263
+ --bundle \
264
+ --format=esm \
265
+ --platform=browser \
266
+ --target=es2020 \
267
+ --minify \
268
+ --outfile="${HELLO_OUT_DIR}/harness.js" \
269
+ --sourcemap
270
+
271
+ npx esbuild "${BROWSER_SRC_DIR}/file-processing-worker.ts" \
272
+ --bundle \
273
+ --format=iife \
274
+ --platform=browser \
275
+ --target=es2020 \
276
+ --minify \
277
+ --outfile="${FILE_PROCESSING_WORKER_BUILD}" \
278
+ --sourcemap
279
+
280
+ cp "${FILE_PROCESSING_WORKER_BUILD}" "${OUT_DIR}/file-processing-worker.js"
281
+ cp "${FILE_PROCESSING_WORKER_MAP_BUILD}" "${OUT_DIR}/file-processing-worker.js.map"
282
+ cp "${FILE_PROCESSING_WORKER_BUILD}" "${DEMO_OUT_DIR}/file-processing-worker.js"
283
+ cp "${FILE_PROCESSING_WORKER_MAP_BUILD}" "${DEMO_OUT_DIR}/file-processing-worker.js.map"
284
+ cp "${FILE_PROCESSING_WORKER_BUILD}" "${HELLO_OUT_DIR}/file-processing-worker.js"
285
+ cp "${FILE_PROCESSING_WORKER_MAP_BUILD}" "${HELLO_OUT_DIR}/file-processing-worker.js.map"
286
+
287
+ npx esbuild "${BROWSER_SRC_DIR}/worker-bootstrap.ts" \
288
+ --bundle \
289
+ --format=iife \
290
+ --platform=browser \
291
+ --target=es2020 \
292
+ --minify \
293
+ --outfile="${WORKER_BOOTSTRAP_BUILD}" \
294
+ --sourcemap
295
+
296
+ npx esbuild "${PACKAGE_DIR}/demo/worker-host-services.ts" \
297
+ --bundle \
298
+ --format=iife \
299
+ --platform=browser \
300
+ --target=es2020 \
301
+ --minify \
302
+ --outfile="${WORKER_HOST_SERVICES_BUILD}" \
303
+ --sourcemap
304
+
305
+ cp "${SMOKE_FIXTURE_DIR}/index.html" "${OUT_DIR}/index.html"
306
+ 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"
308
+ cp "${PACKAGE_DIR}/demo/demo-texture.png" "${DEMO_OUT_DIR}/demo-texture.png"
309
+ cp "${PACKAGE_DIR}/demo/demo-secondary-texture.png" "${DEMO_OUT_DIR}/demo-secondary-texture.png"
310
+
311
+ mkdir -p "${DEMO_OUT_DIR}/advanced-controls" "${DEMO_OUT_DIR}/templated-controls"
312
+ cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/advanced-controls/index.html"
313
+ cp "${PACKAGE_DIR}/demo/route-shell.html" "${DEMO_OUT_DIR}/templated-controls/index.html"
314
+
315
+ copy_runtime_assets "${OUT_DIR}"
316
+ copy_runtime_assets "${DEMO_OUT_DIR}"
317
+ copy_runtime_assets "${HELLO_OUT_DIR}"
318
+ copy_worker_assets "${OUT_DIR}"
319
+ copy_worker_assets "${DEMO_OUT_DIR}"
320
+ copy_worker_assets "${HELLO_OUT_DIR}"
321
+ write_runtime_config "${OUT_DIR}" "${DEFAULT_MANIFEST_PATH}"
322
+ write_runtime_config "${DEMO_OUT_DIR}" "${DEFAULT_MANIFEST_PATH}"
323
+ write_runtime_config "${HELLO_OUT_DIR}" "${DEFAULT_MANIFEST_PATH}"
324
+ write_runtime_config "${DEMO_OUT_DIR}/advanced-controls" "../runtime/dist/effindom.v2.manifest.json"
325
+ write_runtime_config "${DEMO_OUT_DIR}/templated-controls" "../runtime/dist/effindom.v2.manifest.json"
@@ -0,0 +1,175 @@
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 { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { listHostEventMethods, type NormalizedHostEventMethod } from "../browser/src/host-events";
7
+ import type { HostServiceTypeName } from "../browser/src/host-services";
8
+
9
+ const SCRIPT_PATH = fileURLToPath(import.meta.url);
10
+ const PACKAGE_DIR = path.resolve(path.dirname(SCRIPT_PATH), "..");
11
+
12
+ function toPosix(value: string): string {
13
+ return value.split(path.sep).join("/");
14
+ }
15
+
16
+ function relativeImport(fromFile: string, targetFile: string): string {
17
+ let relative = path.relative(path.dirname(fromFile), targetFile);
18
+ relative = relative.replace(/\.[^.]+$/, "");
19
+ if (!relative.startsWith(".")) {
20
+ relative = `./${relative}`;
21
+ }
22
+ return toPosix(relative);
23
+ }
24
+
25
+ function asTypeName(type: HostServiceTypeName): string {
26
+ switch (type) {
27
+ case "string":
28
+ return "string";
29
+ case "bool":
30
+ return "bool";
31
+ case "i32":
32
+ return "i32";
33
+ case "f64":
34
+ return "f64";
35
+ case "void":
36
+ return "void";
37
+ }
38
+ }
39
+
40
+ function callbackTypeFor(method: NormalizedHostEventMethod): string {
41
+ if (method.args.length == 0) {
42
+ return "Callback0";
43
+ }
44
+ if (method.args.length == 1) {
45
+ return `Callback1<${asTypeName(method.args[0])}>`;
46
+ }
47
+ if (method.args.length == 2) {
48
+ return `Callback2<${asTypeName(method.args[0])}, ${asTypeName(method.args[1])}>`;
49
+ }
50
+ throw new Error(`Host event ${method.serviceName}.${method.methodName} uses ${String(method.args.length)} args; only 0-2 are supported right now.`);
51
+ }
52
+
53
+ function emitExportArgs(method: NormalizedHostEventMethod): string {
54
+ const parts: Array<string> = [];
55
+ method.args.forEach((type, index) => {
56
+ if (type === "string") {
57
+ parts.push(`arg${String(index)}Ptr: usize`, `arg${String(index)}Len: u32`);
58
+ return;
59
+ }
60
+ parts.push(`arg${String(index)}: ${asTypeName(type)}`);
61
+ });
62
+ return parts.join(", ");
63
+ }
64
+
65
+ function emitDecodedArgs(method: NormalizedHostEventMethod): Array<string> {
66
+ const lines: Array<string> = [];
67
+ method.args.forEach((type, index) => {
68
+ if (type !== "string") {
69
+ return;
70
+ }
71
+ lines.push(
72
+ ` const arg${String(index)} = arg${String(index)}Len == 0 ? "" : String.UTF8.decodeUnsafe(arg${String(index)}Ptr, <usize>arg${String(index)}Len, false);`,
73
+ );
74
+ });
75
+ return lines;
76
+ }
77
+
78
+ function emitCallbackInvokeArgs(method: NormalizedHostEventMethod): string {
79
+ return method.args
80
+ .map((_type, index) => method.args[index] === "string" ? `arg${String(index)}` : `arg${String(index)}`)
81
+ .join(", ");
82
+ }
83
+
84
+ function emitHandlerBlock(method: NormalizedHostEventMethod): string {
85
+ const eventName = method.eventName;
86
+ const publicName = eventName.length == 0 ? eventName : `${eventName[0].toUpperCase()}${eventName.slice(1)}`;
87
+ const callbackType = callbackTypeFor(method);
88
+ const exportArgs = emitExportArgs(method);
89
+ const decodedArgs = emitDecodedArgs(method);
90
+ const invokeArgs = emitCallbackInvokeArgs(method);
91
+ const directSignature = `${callbackType} | null`;
92
+ return [
93
+ `let __${eventName}Handler: ${directSignature} = null;`,
94
+ "",
95
+ `export function on${publicName}(callback: ${directSignature}): void {`,
96
+ ` __${eventName}Handler = callback;`,
97
+ `}`,
98
+ "",
99
+ `export function clear${publicName}(): void {`,
100
+ ` __${eventName}Handler = null;`,
101
+ `}`,
102
+ "",
103
+ `export function ${method.exportName}(${exportArgs}): void {`,
104
+ ` const callback = __${eventName}Handler;`,
105
+ ` if (callback === null) {`,
106
+ ` return;`,
107
+ ` }`,
108
+ ...decodedArgs,
109
+ method.args.length == 0
110
+ ? ` callback.invoke();`
111
+ : ` callback.invoke(${invokeArgs});`,
112
+ `}`,
113
+ ].join("\n");
114
+ }
115
+
116
+ async function loadHostEvents(modulePath: string, exportName: string): Promise<unknown> {
117
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "fui-host-events-"));
118
+ const bundledFile = path.join(tempDir, "host-events.mjs");
119
+ try {
120
+ await build({
121
+ entryPoints: [modulePath],
122
+ outfile: bundledFile,
123
+ bundle: true,
124
+ format: "esm",
125
+ platform: "node",
126
+ target: "node20",
127
+ logLevel: "silent",
128
+ });
129
+ const loaded = await import(pathToFileURL(bundledFile).href);
130
+ if (!(exportName in loaded)) {
131
+ throw new Error(`Host-events module does not export "${exportName}".`);
132
+ }
133
+ return loaded[exportName as keyof typeof loaded];
134
+ } finally {
135
+ await fs.rm(tempDir, { recursive: true, force: true });
136
+ }
137
+ }
138
+
139
+ function emitBindingsFile(
140
+ sourceModulePath: string,
141
+ exportName: string,
142
+ outputPath: string,
143
+ methods: ReturnType<typeof listHostEventMethods>,
144
+ ): string {
145
+ const callbackImport = relativeImport(outputPath, path.resolve(PACKAGE_DIR, "src/FuiPrimitives.ts"));
146
+ const blocks: Array<string> = [
147
+ `// Generated by scripts/generate-host-events.ts from ${toPosix(sourceModulePath)}#${exportName}.`,
148
+ `import { Callback0, Callback1, Callback2 } from "${callbackImport}";`,
149
+ "",
150
+ ];
151
+ methods.forEach((method, index) => {
152
+ blocks.push(emitHandlerBlock(method));
153
+ if (index + 1 < methods.length) {
154
+ blocks.push("");
155
+ blocks.push("");
156
+ }
157
+ });
158
+ return `${blocks.join("\n")}\n`;
159
+ }
160
+
161
+ async function main(): Promise<void> {
162
+ const [moduleArg, exportName, outputArg] = process.argv.slice(2);
163
+ if (moduleArg === undefined || exportName === undefined || outputArg === undefined) {
164
+ throw new Error("Usage: generate-host-events <module-path> <export-name> <output-path>");
165
+ }
166
+ const modulePath = path.resolve(process.cwd(), moduleArg);
167
+ const outputPath = path.resolve(process.cwd(), outputArg);
168
+ const registry = await loadHostEvents(modulePath, exportName);
169
+ const methods = listHostEventMethods(registry as never);
170
+ const content = emitBindingsFile(modulePath, exportName, outputPath, methods);
171
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
172
+ await fs.writeFile(outputPath, content, "utf8");
173
+ }
174
+
175
+ await main();
@@ -0,0 +1,157 @@
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 { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { listHostServiceMethods, type HostServiceTypeName } from "../browser/src/host-services";
7
+
8
+ const SCRIPT_PATH = fileURLToPath(import.meta.url);
9
+ const PACKAGE_DIR = path.resolve(path.dirname(SCRIPT_PATH), "..");
10
+
11
+ function toPosix(value: string): string {
12
+ return value.split(path.sep).join("/");
13
+ }
14
+
15
+ function relativeImport(fromFile: string, targetFile: string): string {
16
+ let relative = path.relative(path.dirname(fromFile), targetFile);
17
+ relative = relative.replace(/\.[^.]+$/, "");
18
+ if (!relative.startsWith(".")) {
19
+ relative = `./${relative}`;
20
+ }
21
+ return toPosix(relative);
22
+ }
23
+
24
+ function asTypeName(type: HostServiceTypeName): string {
25
+ switch (type) {
26
+ case "string":
27
+ return "string";
28
+ case "bool":
29
+ return "bool";
30
+ case "i32":
31
+ return "i32";
32
+ case "f64":
33
+ return "f64";
34
+ case "void":
35
+ return "void";
36
+ }
37
+ }
38
+
39
+ function emitExternalSignature(importName: string, args: readonly HostServiceTypeName[], returns: HostServiceTypeName): string {
40
+ const signatureParts: Array<string> = [];
41
+ args.forEach((type, index) => {
42
+ if (type === "string") {
43
+ signatureParts.push(`arg${String(index)}Ptr: usize`, `arg${String(index)}Len: u32`);
44
+ return;
45
+ }
46
+ signatureParts.push(`arg${String(index)}: ${asTypeName(type)}`);
47
+ });
48
+ if (returns === "string") {
49
+ signatureParts.push("resultPtr: usize", "resultCap: u32");
50
+ }
51
+ const returnType = returns === "string" ? "u32" : asTypeName(returns);
52
+ return [
53
+ `@external("fui_host_service", "${importName}")`,
54
+ `declare function __host_${importName}(${signatureParts.join(", ")}): ${returnType};`,
55
+ ].join("\n");
56
+ }
57
+
58
+ function emitWrapper(importName: string, args: readonly HostServiceTypeName[], returns: HostServiceTypeName): string {
59
+ const wrapperArgs = args.map((type, index) => `arg${String(index)}: ${asTypeName(type)}`);
60
+ const lines: Array<string> = [];
61
+ args.forEach((type, index) => {
62
+ if (type === "string") {
63
+ lines.push(` const arg${String(index)}Bytes = Uint8Array.wrap(String.UTF8.encode(arg${String(index)}, false));`);
64
+ }
65
+ });
66
+ const callArgs: Array<string> = [];
67
+ args.forEach((type, index) => {
68
+ if (type === "string") {
69
+ callArgs.push(`arg${String(index)}Bytes.length > 0 ? arg${String(index)}Bytes.dataStart : 0`);
70
+ callArgs.push(`<u32>arg${String(index)}Bytes.length`);
71
+ return;
72
+ }
73
+ callArgs.push(`arg${String(index)}`);
74
+ });
75
+ if (returns === "string") {
76
+ lines.push(" const resultPtr = hostServiceResultBufferPtr();");
77
+ lines.push(" const resultCap = hostServiceResultBufferSize();");
78
+ callArgs.push("resultPtr", "resultCap");
79
+ lines.push(` const resultLen = __host_${importName}(${callArgs.join(", ")});`);
80
+ lines.push(` return decodeHostServiceStringResult(resultPtr, resultLen, "${importName}");`);
81
+ } else if (returns === "void") {
82
+ lines.push(` __host_${importName}(${callArgs.join(", ")});`);
83
+ } else {
84
+ lines.push(` return __host_${importName}(${callArgs.join(", ")});`);
85
+ }
86
+ return [
87
+ `export function ${importName}(${wrapperArgs.join(", ")}): ${returns === "string" ? "string" : asTypeName(returns)} {`,
88
+ ...lines,
89
+ "}",
90
+ ].join("\n");
91
+ }
92
+
93
+ async function loadHostServices(modulePath: string, exportName: string): Promise<unknown> {
94
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "fui-host-services-"));
95
+ const bundledFile = path.join(tempDir, "host-services.mjs");
96
+ try {
97
+ await build({
98
+ entryPoints: [modulePath],
99
+ outfile: bundledFile,
100
+ bundle: true,
101
+ format: "esm",
102
+ platform: "node",
103
+ target: "node20",
104
+ logLevel: "silent",
105
+ });
106
+ const loaded = await import(pathToFileURL(bundledFile).href);
107
+ if (!(exportName in loaded)) {
108
+ throw new Error(`Host-services module does not export "${exportName}".`);
109
+ }
110
+ return loaded[exportName as keyof typeof loaded];
111
+ } finally {
112
+ await fs.rm(tempDir, { recursive: true, force: true });
113
+ }
114
+ }
115
+
116
+ function emitBindingsFile(
117
+ sourceModulePath: string,
118
+ exportName: string,
119
+ outputPath: string,
120
+ methods: ReturnType<typeof listHostServiceMethods>,
121
+ ): string {
122
+ const runtimeImport = relativeImport(outputPath, path.resolve(PACKAGE_DIR, "src/FuiPrimitives.ts"));
123
+ const blocks: Array<string> = [
124
+ `// Generated by scripts/generate-host-services.ts from ${toPosix(sourceModulePath)}#${exportName}.`,
125
+ ];
126
+ if (methods.some((method) => method.returns === "string")) {
127
+ blocks.push(`import { decodeHostServiceStringResult, hostServiceResultBufferPtr, hostServiceResultBufferSize } from "${runtimeImport}";`);
128
+ blocks.push("");
129
+ } else {
130
+ blocks.push("");
131
+ }
132
+ methods.forEach((method, index) => {
133
+ blocks.push(emitExternalSignature(method.importName, method.args, method.returns));
134
+ blocks.push("");
135
+ blocks.push(emitWrapper(method.importName, method.args, method.returns));
136
+ if (index + 1 < methods.length) {
137
+ blocks.push("");
138
+ }
139
+ });
140
+ return `${blocks.join("\n")}\n`;
141
+ }
142
+
143
+ async function main(): Promise<void> {
144
+ const [moduleArg, exportName, outputArg] = process.argv.slice(2);
145
+ if (moduleArg === undefined || exportName === undefined || outputArg === undefined) {
146
+ throw new Error("Usage: generate-host-services <module-path> <export-name> <output-path>");
147
+ }
148
+ const modulePath = path.resolve(process.cwd(), moduleArg);
149
+ const outputPath = path.resolve(process.cwd(), outputArg);
150
+ const registry = await loadHostServices(modulePath, exportName);
151
+ const methods = listHostServiceMethods(registry as never);
152
+ const content = emitBindingsFile(modulePath, exportName, outputPath, methods);
153
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
154
+ await fs.writeFile(outputPath, content, "utf8");
155
+ }
156
+
157
+ await main();