@volter/editor-game 0.5.65

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 (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,631 @@
1
+ /**
2
+ * IN-PAGE TRANSPILE + MODULE RESOLUTION, for the one module graph the session's
3
+ * Vite does not serve: a game vendored as a SERVED BUNDLE.
4
+ *
5
+ * A vendored game under `public/ingest/<id>/` ships as ordinary static files,
6
+ * outside the editor's own module graph, so `import('/ingest/…/game.js')`
7
+ * would ask the BROWSER to resolve its bare specifiers — and the browser would
8
+ * fetch a second copy of `three`. Instead its relative files are fetched and
9
+ * bundled here, its bare imports stay external, and each one is rewritten to a
10
+ * blob-URL shim re-exporting the editor's ALREADY-RUNNING namespace
11
+ * (`served-bundle-runtime-modules.ts` answers which). The flow:
12
+ *
13
+ * served source → esbuild-wasm bundle (in a Web Worker)
14
+ * → rewrite bare imports to shims over the live singletons
15
+ * → Blob → URL.createObjectURL → dynamic import()
16
+ *
17
+ * Sharing singletons is the load-bearing detail: two copies of `three` break
18
+ * `instanceof` and prototype chains, and two reconcilers cannot own one canvas.
19
+ *
20
+ * Idiomatic: esbuild-wasm is the same engine Vite uses, just run client-side;
21
+ * we call its documented API directly (no wrapper abstraction).
22
+ *
23
+ * A PROJECT's own modules never come through here: the session that opened it
24
+ * serves them.
25
+ */
26
+
27
+ import { commandLine } from '@volter/editor-core/product-command';
28
+ import type { ImportSpecifier as EsModuleImport } from 'es-module-lexer';
29
+ import { init as esModuleLexerInit, parse as parseEsModule } from 'es-module-lexer';
30
+ import * as esbuild from 'esbuild-wasm';
31
+ // Vite serves the wasm binary as a static asset URL — fully local, no CDN.
32
+ import esbuildWasmUrl from 'esbuild-wasm/esbuild.wasm?url';
33
+ import { GAME_GLOBALS_PRELUDE } from '@volter/editor-core/game-globals-prelude';
34
+
35
+ /** Live runtime modules, keyed by the bare specifier used to import them. */
36
+ type ModuleNamespace = Record<string, unknown>;
37
+ const runtimeModules = new Map<string, ModuleNamespace>();
38
+
39
+ /** Per-specifier shim blob URLs, memoized (the live module identity is stable). */
40
+ const shimUrlCache = new Map<string, string>();
41
+
42
+ let initPromise: Promise<void> | null = null;
43
+
44
+ /**
45
+ * Initialize esbuild-wasm exactly once. Safe to call repeatedly — concurrent
46
+ * callers share the same promise.
47
+ */
48
+ /**
49
+ * MEASURED EXPERIMENT, with a revert criterion — see the numbers below.
50
+ *
51
+ * esbuild-wasm defaults to running in a Web Worker in the browser. That keeps
52
+ * its parse/link off the main thread, but EVERY plugin callback then crosses a
53
+ * postMessage boundary, and this build's plugin is called twice per reached
54
+ * file (`onResolve` + `onLoad`) — about 164 round-trips per rebuild for
55
+ * `examples/third-person`, each structured-cloning source text.
56
+ *
57
+ * The same rebuild costs 182-364ms in Node, where the `worker` option is
58
+ * refused outright and callbacks are direct calls, and ~1150ms in the browser
59
+ * with a warm context (runhuman passes 102/104). Whether that gap is callback
60
+ * IPC or simply browser-vs-Node wasm speed cannot be settled in Node, because
61
+ * esbuild-wasm throws "The 'worker' option only works in the browser".
62
+ *
63
+ * So it is settled on the surface that has the question. The plugin work
64
+ * already runs on the main thread either way; this only moves esbuild's own
65
+ * parse/link there too, trading a thread for the round-trips.
66
+ *
67
+ * RESULT (runhuman pass 107, five rounds on the same entry pass 104 measured):
68
+ * esbuild fell from 1447-2358ms to 500-560ms, and nothing froze — the tester
69
+ * hovered controls and orbited the camera DURING an edit and reported the page
70
+ * responsive, calling the result "visually faster... like a second or two
71
+ * seconds faster". So the gap was substantially the callback round-trips, not
72
+ * browser-versus-Node wasm speed, and the trade is kept.
73
+ *
74
+ * INVERTED (measured 2026-09-03, six-drop runs per mode, medians over drops
75
+ * 2–6): once a content write stopped re-bundling every story module beside
76
+ * the world, there is ONE bundle per edit and the ~164 round-trips are cheap
77
+ * — worker esbuild 521 ms vs 951 on the main thread, remount 610 ms vs 1177 —
78
+ * while long-task sum, worst rAF gap and the FPS floor are indistinguishable
79
+ * between modes (the rest of the remount — R3F mount, Three scene, the reads
80
+ * themselves — is main-thread work either way). Main-thread esbuild also
81
+ * DEGRADES as
82
+ * the scene grows (649 → 1072 ms from the first drop to the fifth) because
83
+ * it competes with the editor's own React/Three work; the worker's stays
84
+ * flat at ~400–520 ms after its first, slower bundle. The pass-107 figure of
85
+ * 1447–2358 ms was measured under the ten-bundle storm and no longer
86
+ * describes the single-bundle path. So the parse/link is back off the main
87
+ * thread; the plugin callbacks still run here.
88
+ */
89
+ const ESBUILD_IN_WORKER = true;
90
+
91
+ /**
92
+ * `import.meta.env` for a PROJECT module bundled here. Vite defines it for the
93
+ * dev tier; esbuild defines nothing, so a project reading
94
+ * `import.meta.env['VITE_…']` — the cinematic example's video-plate switch —
95
+ * threw "Cannot read properties of undefined (reading 'VITE_VIDEO_PLATE')" on
96
+ * the first Play of every hosted session (runhuman pass 148). The hosted
97
+ * tier is a production build of the project with no `.env`: the standard
98
+ * Vite shape, every `VITE_*` absent.
99
+ */
100
+ const PROJECT_MODULE_DEFINES: Record<string, string> = {
101
+ 'import.meta.env': JSON.stringify({
102
+ MODE: 'production',
103
+ DEV: false,
104
+ PROD: true,
105
+ SSR: false,
106
+ BASE_URL: '/',
107
+ }),
108
+ 'import.meta.env.MODE': '"production"',
109
+ 'import.meta.env.DEV': 'false',
110
+ 'import.meta.env.PROD': 'true',
111
+ };
112
+
113
+ /**
114
+ * THE WASM BINARY'S OWN URL, ABSOLUTE, AGAINST THE ORIGIN THAT SERVES THIS
115
+ * MODULE — and it is the origin, never the PAGE's.
116
+ *
117
+ * Vite hands `esbuild.wasm?url` back ROOT-RELATIVE (`/@fs/…` in dev), and
118
+ * `esbuild-wasm`'s own loader then does `new URL(wasmURL, location.href)`
119
+ * before posting it into the blob worker it spawns
120
+ * (`esbuild-wasm/lib/browser.js`). Everywhere but one shape that is the same
121
+ * origin and nothing is wrong. Inside the Code-OSS DESKTOP frame the page is
122
+ * `vscode-file://vscode-app` while this module came from the session over
123
+ * loopback http, so the worker ended up fetching
124
+ * `vscode-file://vscode-app/@fs/…`: a scheme Electron's own
125
+ * `onBeforeRequest` cancels for a request with no frame, at a path that does
126
+ * not exist under the app root. MEASURED 2026-09-19 (the frame walk, beat
127
+ * 19): `TypeError: Failed to fetch`, surfacing as
128
+ * `Ingest failed: TypeError: Failed to fetch` — and ONLY for a
129
+ * `public/ingest/*` project, because the served-bundle lane is the only one
130
+ * that reaches esbuild at mount; the `/@fs/` lane uses a native `import()`.
131
+ *
132
+ * Neither half of the frame's origin shim could have caught it: the URL is
133
+ * absolutised before it reaches `fetch`, and the worker is a SAME-ORIGIN
134
+ * `blob:` the shim deliberately leaves unwrapped.
135
+ *
136
+ * NOTE THE SHAPE, and do not "simplify" it: `new URL(<expression>,
137
+ * import.meta.url)` is Vite's asset-URL pattern and is rewritten statically,
138
+ * so the origin is taken once with the ONE-argument form and used as a plain
139
+ * base string — the same rule `tool-loader.ts`'s `MODULE_SERVING_ORIGIN`
140
+ * states. An already-absolute URL (a production build with a CDN base) is
141
+ * left exactly as it is.
142
+ */
143
+ const ESBUILD_WASM_URL = esbuildWasmUrl.startsWith('/')
144
+ ? `${new URL(import.meta.url).origin}${esbuildWasmUrl}`
145
+ : esbuildWasmUrl;
146
+
147
+ export function initBrowserTranspile(): Promise<void> {
148
+ if (!initPromise) {
149
+ initPromise = esbuild
150
+ .initialize({ wasmURL: ESBUILD_WASM_URL, worker: ESBUILD_IN_WORKER })
151
+ .catch((err) => {
152
+ // Allow a retry on failure rather than caching a rejected promise.
153
+ initPromise = null;
154
+ throw err;
155
+ });
156
+ }
157
+ return initPromise;
158
+ }
159
+
160
+ /**
161
+ * Register the live runtime module namespaces that transpiled user code is
162
+ * allowed to import. Call once at editor/runtime init with the modules the
163
+ * host already imported statically (so identities are shared).
164
+ *
165
+ * registerRuntimeModules({
166
+ * three: THREE,
167
+ * '@volter/threejs-runtime/ecs/user-data': userDataModule,
168
+ * });
169
+ */
170
+ export function registerRuntimeModules(modules: Record<string, ModuleNamespace>): void {
171
+ for (const [spec, ns] of Object.entries(modules)) {
172
+ runtimeModules.set(spec, ns);
173
+ shimUrlCache.delete(spec); // invalidate any stale shim
174
+ }
175
+ // Exposed on globalThis so shim modules (which run in their own module scope)
176
+ // can reach the live namespaces by specifier.
177
+ (globalThis as Record<string, unknown>)['__vgaiModules'] = Object.fromEntries(runtimeModules);
178
+ }
179
+
180
+ /**
181
+ * A resolver from a bare specifier to a LAZY loader of the editor's live copy
182
+ * of that module, or `undefined` when the specifier is not one the browser
183
+ * editor ships. Installed once by `served-bundle-runtime-modules.ts`
184
+ * (`browserRuntimeModuleLoader`), which owns the dependency list; this module
185
+ * only asks it. Lazy on purpose: the list is the whole estate's dependency
186
+ * closure (engine subpaths, physics wasm, audio, …) and a project imports a
187
+ * handful of it, so a load happens per specifier a project ACTUALLY imports —
188
+ * never the whole table on first Play.
189
+ */
190
+ export type RuntimeModuleResolver = (specifier: string) => (() => Promise<unknown>) | undefined;
191
+
192
+ let runtimeModuleResolver: RuntimeModuleResolver | null = null;
193
+ const pendingRuntimeModuleLoads = new Map<string, Promise<void>>();
194
+
195
+ export function registerRuntimeModuleResolver(resolver: RuntimeModuleResolver): void {
196
+ runtimeModuleResolver = resolver;
197
+ }
198
+
199
+ /**
200
+ * THE WALL, in the product's own words. A bare import the browser editor does
201
+ * not ship is not a bug in the project and not a bug in the editor: it is the
202
+ * one dependency this tier cannot resolve, and the remedy is named — the dev
203
+ * server resolves the project's own `node_modules`, and the list grows by a
204
+ * capability declaring the package (which `validate-browser-runtime-modules`
205
+ * then holds it to). Never a silent degrade, never a partial mount.
206
+ */
207
+ function unregisteredRuntimeModules(specifiers: readonly string[]): Error {
208
+ const list = specifiers.map((s) => `'${s}'`).join(', ');
209
+ const verb = specifiers.length === 1 ? 'is' : 'are';
210
+ return new Error(
211
+ `${list} ${verb} not in the browser editor's dependency list, so this project cannot run ` +
212
+ 'here. The browser editor bundles project source itself and resolves every package ' +
213
+ 'import to a dependency it ships; a package outside that list needs a dev server. Open ' +
214
+ `the project locally with ${commandLine('edit')} (which resolves the project’s own node_modules), ` +
215
+ 'or add the package to a capability so it joins the list ' +
216
+ '(packages/editor/src/served-bundle-runtime-modules.ts, gated by ' +
217
+ '`npm run validate-browser-runtime-modules`).',
218
+ );
219
+ }
220
+
221
+ /**
222
+ * Every bare specifier `code` imports STATICALLY (re-exports included),
223
+ * PARSED — es-module-lexer, never a regex. The regex this replaced matched
224
+ * `from '...'`-shaped text inside STRING LITERALS, and the moment a game's
225
+ * bundle inlined a string-heavy library (Babylon's loader messages), Play
226
+ * refused it over junk "specifiers" (measured live, 2026-08-27). Literal dynamic imports are deliberately not in the eager set:
227
+ * `import('x')` is lazy by the language's own contract, and a project's
228
+ * Node-only fallback (`if (typeof indexedDB === 'undefined') await
229
+ * import('fake-indexeddb/auto')`) must stay unreached in a browser rather
230
+ * than be loaded — and walled — at bundle time. Those resolve at CALL time
231
+ * through {@link importRuntimeModule}, which the rewrite points them at.
232
+ */
233
+ async function parseModuleImports(code: string): Promise<readonly EsModuleImport[]> {
234
+ await esModuleLexerInit;
235
+ try {
236
+ return parseEsModule(code)[0];
237
+ } catch (error) {
238
+ throw new Error(
239
+ `browser transpile could not parse this module's imports: ${
240
+ error instanceof Error ? error.message : String(error)
241
+ }`,
242
+ );
243
+ }
244
+ }
245
+
246
+ /** Load ONE specifier through the resolver (shared in-flight promise), or throw the wall. */
247
+ function loadRuntimeModule(spec: string): Promise<void> {
248
+ let pending = pendingRuntimeModuleLoads.get(spec);
249
+ if (!pending) {
250
+ const load = runtimeModuleResolver?.(spec);
251
+ if (!load) return Promise.reject(unregisteredRuntimeModules([spec]));
252
+ pending = load()
253
+ .then((ns) => registerRuntimeModules({ [spec]: ns as ModuleNamespace }))
254
+ .finally(() => pendingRuntimeModuleLoads.delete(spec));
255
+ pendingRuntimeModuleLoads.set(spec, pending);
256
+ }
257
+ return pending;
258
+ }
259
+
260
+ /**
261
+ * The call-time half of a literal dynamic import in project code. The rewrite
262
+ * turns `import('tone')` into `globalThis.__vgaiImportRuntimeModule('tone')`,
263
+ * so the load — and the wall, if the browser editor does not ship it — happens
264
+ * exactly when the project's own code executes the import, never earlier.
265
+ */
266
+ export async function importRuntimeModule(spec: string): Promise<ModuleNamespace> {
267
+ if (!runtimeModules.has(spec)) await loadRuntimeModule(spec);
268
+ return runtimeModules.get(spec) as ModuleNamespace;
269
+ }
270
+ (globalThis as Record<string, unknown>)['__vgaiImportRuntimeModule'] = importRuntimeModule;
271
+
272
+ /**
273
+ * Bring every static bare import live, then rewrite by the lexer's own byte
274
+ * offsets — static specifiers to blob-URL shims over the live modules,
275
+ * literal dynamic imports to the call-time loader
276
+ * ({@link importRuntimeModule}). Offset-exact replacement (walked from the
277
+ * end so earlier offsets stay valid) is what makes this safe inside code
278
+ * that CONTAINS import-shaped strings. Specifiers already registered cost
279
+ * nothing, the rest load through the installed resolver (one in-flight load
280
+ * per specifier, shared by concurrent bundles), and the ones the resolver
281
+ * does not know are reported TOGETHER — a project with three unshipped
282
+ * packages learns all three at once.
283
+ */
284
+ export async function resolveAndRewriteBareImports(code: string): Promise<string> {
285
+ const imports = await parseModuleImports(code);
286
+ const staticBare = new Set<string>();
287
+ for (const record of imports) {
288
+ if (record.d === -1 && record.n !== undefined && !isResolvable(record.n)) {
289
+ staticBare.add(record.n);
290
+ }
291
+ }
292
+ const missing = [...staticBare].filter((spec) => !runtimeModules.has(spec));
293
+ if (missing.length > 0) {
294
+ const unknown = missing.filter((spec) => !runtimeModuleResolver?.(spec));
295
+ if (unknown.length > 0) throw unregisteredRuntimeModules(unknown);
296
+ await Promise.all(missing.map((spec) => loadRuntimeModule(spec)));
297
+ }
298
+ let out = code;
299
+ for (let i = imports.length - 1; i >= 0; i -= 1) {
300
+ const record = imports[i] as EsModuleImport;
301
+ if (record.n === undefined || isResolvable(record.n)) continue;
302
+ if (record.d === -1) {
303
+ // Static import / re-export: swap the specifier text (inside its quotes).
304
+ out = `${out.slice(0, record.s)}${shimUrlFor(record.n)}${out.slice(record.e)}`;
305
+ } else if (record.d >= 0) {
306
+ // Literal dynamic import: swap the whole `import(...)` expression.
307
+ out = `${out.slice(0, record.ss)}globalThis.__vgaiImportRuntimeModule(${JSON.stringify(
308
+ record.n,
309
+ )})${out.slice(record.se)}`;
310
+ }
311
+ }
312
+ return out;
313
+ }
314
+
315
+ const IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
316
+
317
+ // Reserved words can't be `export const <name>` binding identifiers. Some real
318
+ // modules expose them as convenience named exports (e.g. zod exports `catch`,
319
+ // `enum`, `function`, `void`) — re-exporting those verbatim would make the shim
320
+ // itself a SyntaxError ("Unexpected token 'catch'"). They're never imported by
321
+ // those names, so we skip them.
322
+ const RESERVED_WORDS = new Set([
323
+ 'break',
324
+ 'case',
325
+ 'catch',
326
+ 'class',
327
+ 'const',
328
+ 'continue',
329
+ 'debugger',
330
+ 'default',
331
+ 'delete',
332
+ 'do',
333
+ 'else',
334
+ 'enum',
335
+ 'export',
336
+ 'extends',
337
+ 'false',
338
+ 'finally',
339
+ 'for',
340
+ 'function',
341
+ 'if',
342
+ 'import',
343
+ 'in',
344
+ 'instanceof',
345
+ 'new',
346
+ 'null',
347
+ 'return',
348
+ 'super',
349
+ 'switch',
350
+ 'this',
351
+ 'throw',
352
+ 'true',
353
+ 'try',
354
+ 'typeof',
355
+ 'var',
356
+ 'void',
357
+ 'while',
358
+ 'with',
359
+ 'yield',
360
+ 'let',
361
+ 'static',
362
+ 'await',
363
+ 'implements',
364
+ 'interface',
365
+ 'package',
366
+ 'private',
367
+ 'protected',
368
+ 'public',
369
+ ]);
370
+
371
+ /** Build (or reuse) a blob-URL ESM module that re-exports a live namespace. */
372
+ function shimUrlFor(spec: string): string {
373
+ const cached = shimUrlCache.get(spec);
374
+ if (cached) return cached;
375
+
376
+ const ns = runtimeModules.get(spec);
377
+ // Every caller runs `ensureBareImportsLoaded` first, so this is reached only
378
+ // by a specifier the resolver was never asked about (a direct
379
+ // `registerRuntimeModules` user) — the same wall, said the same way.
380
+ if (!ns) throw unregisteredRuntimeModules([spec]);
381
+
382
+ const lines = [`const __m = globalThis.__vgaiModules[${JSON.stringify(spec)}];`];
383
+ for (const key of Object.keys(ns)) {
384
+ if (key === 'default' || !IDENT_RE.test(key) || RESERVED_WORDS.has(key)) continue;
385
+ lines.push(`export const ${key} = __m[${JSON.stringify(key)}];`);
386
+ }
387
+ if ('default' in ns) lines.push(`export default __m[${JSON.stringify('default')}];`);
388
+
389
+ const url = URL.createObjectURL(new Blob([lines.join('\n')], { type: 'text/javascript' }));
390
+ shimUrlCache.set(spec, url);
391
+ return url;
392
+ }
393
+
394
+ /** True for specifiers that are already directly importable in the browser. */
395
+ function isResolvable(spec: string): boolean {
396
+ return (
397
+ spec.startsWith('.') ||
398
+ spec.startsWith('/') ||
399
+ spec.startsWith('blob:') ||
400
+ spec.startsWith('http:') ||
401
+ spec.startsWith('https:')
402
+ );
403
+ }
404
+
405
+ /**
406
+ * Transpile TS/TSX source to an importable ESM module URL (blob:). Bare imports
407
+ * are rewritten to live-singleton shims.
408
+ *
409
+ * Low-level primitive: it does NOT apply the game-globals shadow. Project /
410
+ * game code must go through {@link importFromSource} (which wraps this) so its
411
+ * `window`/`document` input listeners stay focus-gated; only shadow-exempt
412
+ * editor-internal source should call this directly.
413
+ */
414
+ export async function transpileToModuleUrl(
415
+ source: string,
416
+ filename = 'in-browser-edit.tsx',
417
+ ): Promise<string> {
418
+ await initBrowserTranspile();
419
+ const { code } = await esbuild.transform(source, {
420
+ loader: filename.endsWith('.tsx') ? 'tsx' : 'ts',
421
+ format: 'esm',
422
+ target: 'es2022',
423
+ sourcefile: filename,
424
+ // Match the editor's tsconfig: decorators + classic field semantics.
425
+ tsconfigRaw: {
426
+ compilerOptions: { experimentalDecorators: true, useDefineForClassFields: false },
427
+ },
428
+ });
429
+ const rewritten = await resolveAndRewriteBareImports(code);
430
+ return URL.createObjectURL(new Blob([rewritten], { type: 'text/javascript' }));
431
+ }
432
+
433
+ /**
434
+ * Prepend the game-globals lexical shadow to project source — the browser-side
435
+ * analogue of the dev server's `vgai-game-globals` Vite plugin
436
+ * (`server/dev.ts` + `shouldShadowGameGlobals`). The server shadows per-module
437
+ * because Vite transforms EVERY module indiscriminately and must be told which
438
+ * ones are game code; the browser has no such firehose — {@link importFromSource}
439
+ * is the single client-transpile chokepoint and only ever receives project
440
+ * code, so "scoping" here is by construction (route project code through this
441
+ * seam; never through the raw {@link transpileToModuleUrl}). Applying the shadow
442
+ * to EVERY module that reaches this seam is what keeps game `window`/`document`
443
+ * input focus-gated on the whole browser path — the bundled project scripts
444
+ * (`served-bundle-runtime-modules.ts`) AND every single-file hot-swap alike, not just
445
+ * hot-swaps. Kept as its own named, exported function so that invariant is
446
+ * unit-testable and can't be silently dropped (`browser-globals-shadow.test.ts`).
447
+ */
448
+ export function applyGameGlobalsShadow(source: string): string {
449
+ return GAME_GLOBALS_PRELUDE + source;
450
+ }
451
+
452
+ /**
453
+ * Transpile TS/TSX source in-browser and dynamically import it, returning the
454
+ * module namespace. The blob URL is revoked once the import settles.
455
+ *
456
+ * This is the ONLY path project code should take: it applies the game-globals
457
+ * shadow ({@link applyGameGlobalsShadow}) so a component's `window`/`document`
458
+ * resolve to the editor's gated proxies (input stays focus-gated on the HMR
459
+ * path too). The raw {@link transpileToModuleUrl} deliberately does NOT shadow —
460
+ * it is the low-level primitive; feeding project code to it directly would leak
461
+ * ungated input listeners.
462
+ */
463
+ export async function importFromSource(
464
+ source: string,
465
+ filename = 'in-browser-edit.tsx',
466
+ ): Promise<Record<string, unknown>> {
467
+ const url = await transpileToModuleUrl(applyGameGlobalsShadow(source), filename);
468
+ try {
469
+ return (await import(/* @vite-ignore */ url)) as Record<string, unknown>;
470
+ } finally {
471
+ URL.revokeObjectURL(url);
472
+ }
473
+ }
474
+
475
+ export const SOURCE_EXTENSIONS = ['', '.ts', '.tsx', '.js', '.jsx', '.json'] as const;
476
+
477
+ export function resolveProjectPath(importer: string, specifier: string): string {
478
+ const base = specifier.startsWith('/') ? [] : importer.split('/').slice(0, -1);
479
+ for (const segment of specifier.split('/')) {
480
+ if (!segment || segment === '.') continue;
481
+ if (segment === '..') {
482
+ if (base.length === 0) {
483
+ throw new Error(`Project import '${specifier}' escapes the project root.`);
484
+ }
485
+ base.pop();
486
+ continue;
487
+ }
488
+ base.push(segment);
489
+ }
490
+ return base.join('/');
491
+ }
492
+
493
+ /**
494
+ * Extensions an `import url from './thing.png'` is allowed to name.
495
+ *
496
+ * Vite gives that import a URL string, and game source is ecosystem-native —
497
+ * `examples/top-down-strategy` imports its three terrain rasters exactly that
498
+ * way — so this bundle owes the same import SHAPE. It cannot owe the same
499
+ * URL: a served bundle's own files are not in the session's module graph, so
500
+ * the bytes travel inside the bundle as a data URL. `dataurl` reads the MIME from this extension, and
501
+ * it is the only loader whose default export is a string a `fetch`/`Image.src`
502
+ * consumer can use unchanged.
503
+ *
504
+ * The cost is base64's third: a large binary belongs in `public/` and is
505
+ * referenced by path, which is what every model in the estate already does.
506
+ * This list is for the small rasters and clips source genuinely imports.
507
+ */
508
+ const ASSET_EXTENSIONS = [
509
+ '.png',
510
+ '.jpg',
511
+ '.jpeg',
512
+ '.webp',
513
+ '.gif',
514
+ '.avif',
515
+ '.bmp',
516
+ '.ico',
517
+ '.svg',
518
+ '.mp3',
519
+ '.ogg',
520
+ '.wav',
521
+ '.m4a',
522
+ '.woff',
523
+ '.woff2',
524
+ '.ttf',
525
+ '.otf',
526
+ ];
527
+
528
+ export function loaderFor(path: string): esbuild.Loader {
529
+ const sourcePath = path.split(/[?#]/, 1)[0]!;
530
+ if (sourcePath.endsWith('.tsx')) return 'tsx';
531
+ if (sourcePath.endsWith('.jsx')) return 'jsx';
532
+ if (sourcePath.endsWith('.js') || sourcePath.endsWith('.mjs')) return 'js';
533
+ if (sourcePath.endsWith('.json')) return 'json';
534
+ if (sourcePath.endsWith('.glsl') || sourcePath.endsWith('.vert') || sourcePath.endsWith('.frag'))
535
+ return 'text';
536
+ const lower = sourcePath.toLowerCase();
537
+ if (ASSET_EXTENSIONS.some((extension) => lower.endsWith(extension))) return 'dataurl';
538
+ return 'ts';
539
+ }
540
+
541
+ function resolveServedModulePath(importer: string, specifier: string): string {
542
+ if (specifier.startsWith('/')) return specifier;
543
+ if (/^(?:https?:|blob:|data:)/.test(specifier)) return specifier;
544
+ const origin = globalThis.location?.origin ?? 'https://vgai.invalid';
545
+ const resolved = new URL(specifier, new URL(importer, origin));
546
+ return resolved.origin === origin
547
+ ? `${resolved.pathname}${resolved.search}${resolved.hash}`
548
+ : resolved.href;
549
+ }
550
+
551
+ /**
552
+ * Bundle one module graph served as ordinary static files. Unlike a direct
553
+ * `import('/ingest/.../game.js')`, this path does not ask the browser to
554
+ * resolve package specifiers: relative files are fetched and bundled here,
555
+ * while bare imports remain external and are rewritten to the host's live
556
+ * registered namespaces. That preserves the one-Three invariant in a static
557
+ * deployment as well as under Vite's development server.
558
+ */
559
+ export async function bundleServedModule(
560
+ entry: string,
561
+ fetchImpl: typeof fetch = globalThis.fetch,
562
+ ): Promise<string> {
563
+ await initBrowserTranspile();
564
+ const result = await esbuild.build({
565
+ entryPoints: [entry],
566
+ bundle: true,
567
+ write: false,
568
+ format: 'esm',
569
+ platform: 'browser',
570
+ target: 'es2022',
571
+ jsx: 'automatic',
572
+ define: PROJECT_MODULE_DEFINES,
573
+ plugins: [
574
+ {
575
+ name: 'vgai-served-module',
576
+ setup(build) {
577
+ build.onResolve({ filter: /.*/ }, (args) => {
578
+ if (
579
+ args.kind !== 'entry-point' &&
580
+ !args.path.startsWith('.') &&
581
+ !args.path.startsWith('/')
582
+ ) {
583
+ return { path: args.path, external: true };
584
+ }
585
+ const path =
586
+ args.kind === 'entry-point'
587
+ ? args.path
588
+ : resolveServedModulePath(args.importer, args.path);
589
+ return { path, namespace: 'vgai-served' };
590
+ });
591
+ build.onLoad({ filter: /.*/, namespace: 'vgai-served' }, async (args) => {
592
+ const response = await fetchImpl(args.path);
593
+ if (!response.ok) {
594
+ throw new Error(
595
+ `Cannot load static game module '${args.path}' (${response.status} ${response.statusText}).`,
596
+ );
597
+ }
598
+ const loader = loaderFor(args.path);
599
+ // An asset is BYTES; decoding it as text is what turns a PNG into
600
+ // a parse error against replacement characters.
601
+ if (loader === 'dataurl') {
602
+ return { contents: new Uint8Array(await response.arrayBuffer()), loader };
603
+ }
604
+ const source = await response.text();
605
+ return {
606
+ contents:
607
+ loader === 'json' || loader === 'text' ? source : applyGameGlobalsShadow(source),
608
+ loader,
609
+ };
610
+ });
611
+ },
612
+ },
613
+ ],
614
+ });
615
+ const output = result.outputFiles?.find((file) => !file.path.endsWith('.css'));
616
+ if (!output) throw new Error(`Static game module bundle produced no JavaScript for '${entry}'.`);
617
+ return await resolveAndRewriteBareImports(output.text);
618
+ }
619
+
620
+ export async function importServedModule(
621
+ entry: string,
622
+ fetchImpl: typeof fetch = globalThis.fetch,
623
+ ): Promise<Record<string, unknown>> {
624
+ const source = await bundleServedModule(entry, fetchImpl);
625
+ const url = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
626
+ try {
627
+ return (await import(/* @vite-ignore */ url)) as Record<string, unknown>;
628
+ } finally {
629
+ URL.revokeObjectURL(url);
630
+ }
631
+ }