lecodes-sdk 1.2.0 → 2.0.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 (260) hide show
  1. package/README.md +5 -2
  2. package/dist/editor.d.ts +12 -0
  3. package/dist/global.d.ts +4 -8
  4. package/dist/host.d.ts +2 -3
  5. package/dist/types/animate/tween/Animation.d.ts +0 -3
  6. package/dist/types/animate/tween/animateValue.d.ts +4 -2
  7. package/dist/types/animate/tween/easing.d.ts +8 -0
  8. package/dist/types/animate/tween/spec.d.ts +15 -7
  9. package/dist/types/audio/audio.d.ts +2 -1
  10. package/dist/types/canvas/Canvas.d.ts +40 -108
  11. package/dist/types/canvas/gen/cssColor.d.ts +17 -0
  12. package/dist/types/canvas/gen/recorder.d.ts +118 -0
  13. package/dist/types/canvas/gen/spec.d.ts +144 -0
  14. package/dist/types/core/color.d.ts +3 -1
  15. package/dist/types/core/pins.d.ts +18 -0
  16. package/dist/types/g2/Node2D.d.ts +5 -8
  17. package/dist/types/g2/Scene2D.d.ts +6 -2
  18. package/dist/types/gl/Foliage.d.ts +30 -7
  19. package/dist/types/gl/Light.d.ts +8 -0
  20. package/dist/types/gl/Lightmap.d.ts +13 -2
  21. package/dist/types/gl/Material.d.ts +17 -3
  22. package/dist/types/gl/Model.d.ts +6 -2
  23. package/dist/types/gl/Node.d.ts +3 -6
  24. package/dist/types/gl/Scene.d.ts +41 -13
  25. package/dist/types/gl/Texture.d.ts +1 -1
  26. package/dist/types/gl/animation/Locomotion.d.ts +8 -1
  27. package/dist/types/inject.d.ts +11 -11
  28. package/dist/types/inject.editor.d.ts +1 -0
  29. package/dist/types/net/core.d.ts +7 -0
  30. package/dist/types/plugin.d.ts +76 -0
  31. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  32. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  33. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  34. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  35. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  36. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  37. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  38. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  39. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  40. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  41. package/dist/types/runtime/app.d.ts +9 -2
  42. package/dist/types/runtime/fetch.d.ts +2 -0
  43. package/dist/types/runtime/input.d.ts +1 -1
  44. package/dist/types/runtime/media.d.ts +6 -10
  45. package/dist/types/runtime/misc.d.ts +4 -1
  46. package/dist/types/runtime/net.d.ts +3 -2
  47. package/dist/types/runtime/touch.d.ts +32 -0
  48. package/dist/types/scene/defineScene.d.ts +43 -2
  49. package/dist/types/scene/editor.d.ts +52 -0
  50. package/dist/types/scene/gizmos.d.ts +7 -4
  51. package/dist/types/ui/NativeView.d.ts +6 -4
  52. package/dist/types/ui/UI.d.ts +1 -1
  53. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  54. package/dist/types/ui/UIButton.d.ts +12 -14
  55. package/dist/types/ui/UIContainer.d.ts +0 -6
  56. package/dist/types/ui/UIImage.d.ts +1 -4
  57. package/dist/types/ui/UIInput.d.ts +8 -24
  58. package/dist/types/ui/UIModal.d.ts +0 -2
  59. package/dist/types/ui/UINode.d.ts +70 -56
  60. package/dist/types/ui/UIPager.d.ts +28 -27
  61. package/dist/types/ui/UIPopover.d.ts +0 -2
  62. package/dist/types/ui/UIScreen.d.ts +12 -17
  63. package/dist/types/ui/UIScrollable.d.ts +1 -4
  64. package/dist/types/ui/UIText.d.ts +0 -2
  65. package/dist/types/ui/UIVideo.d.ts +3 -5
  66. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  67. package/dist/types/ui/UIWidget.d.ts +10 -9
  68. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  69. package/dist/types/ui/presentable.d.ts +46 -32
  70. package/dist/types/ui/router.d.ts +18 -7
  71. package/dist/types/ui/styleColor.d.ts +1 -0
  72. package/dist/types/ui/transitions.d.ts +18 -0
  73. package/dist/types/ui/tree.d.ts +75 -0
  74. package/dist/types/version.d.ts +10 -0
  75. package/dist/types.json +1 -1
  76. package/package.json +12 -3
  77. package/prompts/2d.md +2 -6
  78. package/prompts/3d.md +1 -5
  79. package/prompts/README.md +1 -1
  80. package/prompts/canvas.md +9 -8
  81. package/prompts/compose.ts +1 -1
  82. package/prompts/core.md +3 -3
  83. package/prompts/design.md +1 -1
  84. package/prompts/dist/2d-game.md +473 -239
  85. package/prompts/dist/3d-app.md +553 -205
  86. package/prompts/dist/ar-app.md +435 -202
  87. package/prompts/dist/design.md +113 -95
  88. package/prompts/dist/ui-app.md +386 -170
  89. package/prompts/ui-design.md +2 -3
  90. package/prompts/ui.md +45 -37
  91. package/src/animate/tween/Animation.ts +34 -150
  92. package/src/animate/tween/Timeline.ts +175 -175
  93. package/src/animate/tween/animateValue.ts +6 -3
  94. package/src/animate/tween/easing.ts +10 -3
  95. package/src/animate/tween/spec.ts +41 -15
  96. package/src/audio/Sound.ts +3 -3
  97. package/src/audio/audio.ts +2 -1
  98. package/src/bridges/2d.d.ts +317 -0
  99. package/src/bridges/app.d.ts +91 -0
  100. package/src/bridges/audio.d.ts +97 -0
  101. package/src/bridges/canvas.d.ts +79 -0
  102. package/src/bridges/device.d.ts +72 -0
  103. package/src/bridges/fetch.d.ts +80 -0
  104. package/src/bridges/files.d.ts +70 -0
  105. package/src/bridges/gl.d.ts +1133 -0
  106. package/src/bridges/input.d.ts +72 -0
  107. package/src/bridges/media.d.ts +55 -0
  108. package/src/bridges/nav.d.ts +71 -0
  109. package/src/bridges/net.d.ts +52 -0
  110. package/src/bridges/service.d.ts +52 -0
  111. package/src/bridges/socket.d.ts +31 -0
  112. package/src/bridges/storage.d.ts +33 -0
  113. package/src/bridges/tree.d.ts +301 -0
  114. package/src/bridges/types.d.ts +49 -0
  115. package/src/canvas/Canvas.ts +114 -159
  116. package/src/canvas/gen/cssColor.ts +224 -0
  117. package/src/canvas/gen/recorder.ts +212 -0
  118. package/src/canvas/gen/spec.ts +201 -0
  119. package/src/chisel.ts +193 -0
  120. package/src/compile/assetMacro.ts +1 -1
  121. package/src/compile/bundler.ts +11 -2
  122. package/src/compile/compileProject.ts +43 -4
  123. package/src/compile/fontMacro.ts +3 -4
  124. package/src/compile/header.ts +26 -5
  125. package/src/compile/index.ts +3 -1
  126. package/src/compile/liteMaterial.ts +1 -1
  127. package/src/compile/sceneEditor.ts +11 -26
  128. package/src/core/color.ts +73 -30
  129. package/src/core/pins.ts +51 -0
  130. package/src/core/signals.ts +8 -1
  131. package/src/g2/CharacterController2D.ts +3 -3
  132. package/src/g2/Node2D.ts +57 -39
  133. package/src/g2/Physics2D.ts +2 -2
  134. package/src/g2/Scene2D.ts +35 -23
  135. package/src/g2/Texture2D.ts +1 -1
  136. package/src/g2/loop.ts +4 -4
  137. package/src/gl/CameraPlace.ts +52 -52
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +10 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Mesh.ts +120 -120
  144. package/src/gl/Model.ts +21 -16
  145. package/src/gl/Node.ts +91 -24
  146. package/src/gl/Particles.ts +1 -1
  147. package/src/gl/Scene.ts +100 -44
  148. package/src/gl/Texture.ts +8 -7
  149. package/src/gl/animation/AnimationClip.ts +1 -1
  150. package/src/gl/animation/DynamicBone.ts +482 -482
  151. package/src/gl/animation/Locomotion.ts +8 -3
  152. package/src/gl/nav/NavMesh.ts +3 -4
  153. package/src/gl/physics/Physics.ts +2 -2
  154. package/src/gl/physics/physicsEvents.ts +3 -3
  155. package/src/gl/scenarios.ts +291 -291
  156. package/src/gl/terrain/Terrain.ts +4 -5
  157. package/src/gl/touch.ts +14 -15
  158. package/src/host.d.ts +2 -3
  159. package/src/inject.editor.ts +7 -0
  160. package/src/inject.ts +13 -16
  161. package/src/net/core.ts +6 -5
  162. package/src/net/index.ts +1 -1
  163. package/src/net/replication.ts +1 -1
  164. package/src/plugin.ts +191 -0
  165. package/src/plugins/gen/camera/contract.d.ts +27 -0
  166. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  167. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  168. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  169. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  170. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  171. package/src/plugins/gen/map/contract.d.ts +144 -0
  172. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  173. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  174. package/src/plugins/gen/push/contract.d.ts +61 -0
  175. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  176. package/src/plugins/gen/push/sdk/push.ts +105 -0
  177. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  178. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  179. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  180. package/src/plugins/permission.ts +5 -4
  181. package/src/runtime/app.ts +20 -9
  182. package/src/runtime/appEvents.ts +5 -4
  183. package/src/runtime/channel.ts +18 -15
  184. package/src/runtime/clipboard.ts +4 -3
  185. package/src/runtime/datetime.ts +2 -1
  186. package/src/runtime/device.ts +17 -15
  187. package/src/runtime/fetch.ts +30 -20
  188. package/src/runtime/files.ts +16 -15
  189. package/src/runtime/input.ts +12 -10
  190. package/src/runtime/media.ts +50 -46
  191. package/src/runtime/misc.ts +7 -3
  192. package/src/runtime/net.ts +8 -7
  193. package/src/runtime/rpc.ts +1 -3
  194. package/src/runtime/service.ts +19 -14
  195. package/src/runtime/share.ts +4 -3
  196. package/src/runtime/storage.ts +6 -4
  197. package/src/runtime/touch.ts +32 -0
  198. package/src/scene/defineScene.ts +61 -365
  199. package/src/scene/editor.ts +408 -0
  200. package/src/scene/editorPlugins.ts +3 -3
  201. package/src/scene/gizmos.ts +15 -9
  202. package/src/server/db/marci/query.ts +1 -1
  203. package/src/server/host.ts +1 -1
  204. package/src/server/runtime.ts +1 -1
  205. package/src/ui/NativeView.ts +72 -25
  206. package/src/ui/UI.ts +3 -3
  207. package/src/ui/UIBottomSheet.ts +16 -17
  208. package/src/ui/UIButton.ts +54 -16
  209. package/src/ui/UIContainer.ts +0 -6
  210. package/src/ui/UIImage.ts +34 -30
  211. package/src/ui/UIInput.ts +29 -37
  212. package/src/ui/UIModal.ts +1 -3
  213. package/src/ui/UINode.ts +347 -297
  214. package/src/ui/UIPager.ts +93 -78
  215. package/src/ui/UIPopover.ts +0 -2
  216. package/src/ui/UIScreen.ts +41 -36
  217. package/src/ui/UIScrollable.ts +19 -13
  218. package/src/ui/UISpacer.ts +1 -1
  219. package/src/ui/UITabs.ts +8 -6
  220. package/src/ui/UIText.ts +7 -17
  221. package/src/ui/UIVideo.ts +30 -27
  222. package/src/ui/UIVirtualizedList.ts +58 -59
  223. package/src/ui/UIWidget.ts +38 -18
  224. package/src/ui/colorKeys.gen.ts +37 -0
  225. package/src/ui/fonts.ts +2 -2
  226. package/src/ui/presentable.ts +59 -42
  227. package/src/ui/router.ts +52 -35
  228. package/src/ui/styleColor.ts +56 -0
  229. package/src/ui/theme.ts +7 -6
  230. package/src/ui/transitions.ts +249 -0
  231. package/src/ui/tree.ts +346 -0
  232. package/src/version.ts +24 -0
  233. package/tests/helpers/engineWorld.ts +11 -0
  234. package/tests/helpers/fakeTree.ts +353 -0
  235. package/tests/helpers/hostStubs.ts +31 -0
  236. package/tests/helpers/index.ts +14 -0
  237. package/tests/helpers/memoryMarci.ts +124 -0
  238. package/tests/helpers/phases.ts +23 -0
  239. package/tests/helpers/preload.ts +18 -0
  240. package/tests/helpers/stubApp.ts +2 -0
  241. package/tests/helpers/stubDevice.ts +2 -0
  242. package/tests/helpers/stubFetch.ts +2 -0
  243. package/tests/helpers/stubInput.ts +2 -0
  244. package/dist/inject.js +0 -4629
  245. package/dist/types/core/registry.d.ts +0 -7
  246. package/dist/types/plugins/camera.d.ts +0 -25
  247. package/dist/types/plugins/push.d.ts +0 -46
  248. package/src/bridges.d.ts +0 -1769
  249. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  250. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  251. package/src/compile/__tests__/assetName.test.ts +0 -55
  252. package/src/compile/__tests__/compile.test.ts +0 -310
  253. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  254. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  255. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  256. package/src/core/__tests__/stateMachine.test.ts +0 -132
  257. package/src/core/registry.ts +0 -23
  258. package/src/plugins/camera.ts +0 -81
  259. package/src/plugins/push.ts +0 -132
  260. package/src/plugins/qr.ts +0 -73
@@ -0,0 +1,353 @@
1
+ // A TypeScript stand-in for `_creatorTree` (sdk/src/bridges/tree.d.ts) for the SDK's unit tests:
2
+ // an in-memory tree with the runtime's semantics the SDK relies on (ids, insert / remove, parent /
3
+ // children, the pager's pages, the vlist window, destinations + the router stack, widgets) and a
4
+ // call recorder. Events reach the SDK through the emitter it installs (`fake.emit(...)` fires one
5
+ // by hand; the fake itself fires open / close / routerChange / detached / vlistSync the way the
6
+ // runtime does, synchronously). Installed for every test file by tests/helpers/preload.ts.
7
+ import {
8
+ TREE_DEST_NONE, TREE_DEST_SCREEN, TREE_DEST_VIDEO,
9
+ TREE_EVENT_CLOSE, TREE_EVENT_DETACHED, TREE_EVENT_FREED, TREE_EVENT_OPEN, TREE_EVENT_ROUTER_CHANGE, TREE_EVENT_VLIST_SYNC,
10
+ } from "../../src/ui/tree"
11
+ import { _resetTransitions } from "../../src/ui/transitions"
12
+
13
+ export type FakeNode = {
14
+ id: number, type: string, parent: number, children: number[],
15
+ strings: Record<string, string | null>, ints: Record<string, number>, flags: number,
16
+ style: Record<string, unknown>, classes: Set<string>,
17
+ pages?: number[], lens?: number[], index?: number, pagerOwner?: number,
18
+ vlist?: { keys: string[], mounted: Map<string, number> },
19
+ }
20
+ export type Call = { method: string, args: any[] }
21
+ type Dest = { kind: number, id: number, viewName: string, paramsJson: string }
22
+
23
+ export class FakeTree {
24
+ nodes = new Map<number, FakeNode>()
25
+ calls: Call[] = []
26
+ emitter: ((id: number, kind: number, ...args: any[]) => any) | null = null
27
+ rects = new Map<number, [number, number, number, number]>()
28
+ displaySize: [number, number] = [0, 0]
29
+ current: Dest | null = null
30
+ stack: Dest[] = []
31
+ widgets: number[] = []
32
+ freed: number[] = []
33
+ tweens: { id: number, data: Float32Array, strings: string[], targets: number[] }[] = []
34
+ tweenEvent: ((id: number, kind: number, index: number, ...lanes: number[]) => void) | null = null
35
+ /** What `defineTransition` registered; `transitions[id - 1]` is the transition of a wire id. */
36
+ transitions: { id: number, data: Float32Array, strings: string[], roles: number[], flags: number }[] = []
37
+ fontFormat?: "woff2" | "ttf"
38
+ private nextId = 1
39
+ private nextTween = 1
40
+
41
+ // ---- helpers for tests ---------------------------------------------------------------------------
42
+ node(id: number): FakeNode { const n = this.nodes.get(id); if (!n) throw new Error(`fakeTree: no node ${id}`); return n }
43
+ names(): string[] { return this.calls.map((c) => c.method) }
44
+ find(method: string): Call | undefined { return this.calls.find((c) => c.method === method) }
45
+ filter(method: string): Call[] { return this.calls.filter((c) => c.method === method) }
46
+ reset(): void { this.calls = [] }
47
+ /** Every node id this fake ever handed out (alive or not). */
48
+ allIds(): number[] { return Array.from({ length: this.nextId - 1 }, (_, i) => i + 1) }
49
+ /** Fire an event at the SDK the way the runtime would. */
50
+ emit(id: number, kind: number, ...args: any[]): any { return this.emitter?.(id, kind, ...args) }
51
+ private rec(method: string, ...args: any[]): void { this.calls.push({ method, args }) }
52
+ private destId(d: Dest): number { return d.id }
53
+ private fireDest(d: Dest | null, kind: number): void {
54
+ if (!d) return
55
+ const node = d.kind === TREE_DEST_SCREEN || d.kind === TREE_DEST_VIDEO ? d.id : 0
56
+ this.emitter?.(node, kind, d.kind, this.destId(d))
57
+ }
58
+ private sameDest(a: Dest | null, b: Dest | null): boolean {
59
+ return !!a && !!b && a.kind === b.kind && a.id === b.id
60
+ }
61
+ private inStack(d: Dest): boolean { return this.stack.some((s) => this.sameDest(s, d)) }
62
+
63
+ // ---- nodes -----------------------------------------------------------------------------------------
64
+ create = (type: string): Handle => {
65
+ const id = this.nextId++
66
+ this.nodes.set(id, { id, type, parent: 0, children: [], strings: {}, ints: {}, flags: 0, style: {}, classes: new Set() })
67
+ this.rec("create", type, id)
68
+ return { id }
69
+ }
70
+ destroy = (id: number): void => {
71
+ this.rec("destroy", id)
72
+ const n = this.nodes.get(id)
73
+ if (!n) return
74
+ const freeRec = (m: FakeNode) => {
75
+ for (const c of m.children) { const cn = this.nodes.get(c); if (cn) freeRec(cn) }
76
+ this.nodes.delete(m.id)
77
+ if (m.id !== id) this.freed.push(m.id)
78
+ }
79
+ freeRec(n)
80
+ }
81
+ insert = (parent: number, child: number, index: number): void => {
82
+ this.rec("insert", parent, child, index)
83
+ const p = this.nodes.get(parent), c = this.nodes.get(child)
84
+ if (!p || !c) return
85
+ if (c.parent !== 0) return
86
+ c.parent = parent
87
+ const i = Math.max(0, Math.min(index, p.children.length))
88
+ p.children.splice(i, 0, child)
89
+ }
90
+ remove = (parent: number, child: number): void => {
91
+ this.rec("remove", parent, child)
92
+ const p = this.nodes.get(parent), c = this.nodes.get(child)
93
+ if (!p || !c || c.parent !== parent) return
94
+ c.parent = 0
95
+ p.children = p.children.filter((x) => x !== child)
96
+ }
97
+ parent = (id: number): number => {
98
+ const n = this.nodes.get(id)
99
+ return n ? (n.pagerOwner ?? n.parent) : 0
100
+ }
101
+ children = (id: number): number[] => {
102
+ const n = this.nodes.get(id)
103
+ if (!n) return []
104
+ return n.pages ? n.pages.slice() : n.children.slice()
105
+ }
106
+
107
+ // ---- content / style -------------------------------------------------------------------------------
108
+ setString = (id: number, key: string, value: string | null): void => { this.rec("setString", id, key, value); const n = this.nodes.get(id); if (n) n.strings[key] = value }
109
+ setInt = (id: number, key: string, value: number): void => { this.rec("setInt", id, key, value); const n = this.nodes.get(id); if (n) n.ints[key] = value }
110
+ setFlags = (id: number, mask: number): void => { this.rec("setFlags", id, mask); const n = this.nodes.get(id); if (n) n.flags = mask }
111
+ getTextValue = (id: number): string => this.nodes.get(id)?.strings.value ?? ""
112
+ setStyle = (id: number, key: string, value: any): void => { this.rec("setStyle", id, key, value); const n = this.nodes.get(id); if (n) n.style[key] = value }
113
+ mergeStyle = (id: number, style: object): void => { this.rec("mergeStyle", id, style); const n = this.nodes.get(id); if (n) Object.assign(n.style, style) }
114
+ setClass = (id: number, name: string, enabled: boolean): void => {
115
+ this.rec("setClass", id, name, enabled)
116
+ const n = this.nodes.get(id); if (!n) return
117
+ if (enabled) n.classes.add(name); else n.classes.delete(name)
118
+ }
119
+ setTheme = (values: Record<string, any>): void => { this.rec("setTheme", values) }
120
+
121
+ // ---- presentation ----------------------------------------------------------------------------------
122
+ private present(d: Dest, transition: number): void {
123
+ if (this.sameDest(this.current, d)) return
124
+ const prev = this.current
125
+ this.fireDest(prev, TREE_EVENT_CLOSE)
126
+ this.current = d
127
+ this.fireDest(d, TREE_EVENT_OPEN)
128
+ if (prev && !this.inStack(prev)) this.retire(prev)
129
+ }
130
+ private retire(d: Dest): void {
131
+ if (d.kind === TREE_DEST_SCREEN && !this.inStack(d) && !this.sameDest(this.current, d)) this.emitter?.(d.id, TREE_EVENT_DETACHED)
132
+ }
133
+ defineTransition = (data: Float32Array, strings: string[], roles: number[], flags: number): number => {
134
+ const id = this.transitions.length + 1
135
+ this.transitions.push({ id, data, strings: strings.slice(), roles: roles.slice(), flags })
136
+ this.rec("defineTransition", id, roles.slice(), flags)
137
+ return id
138
+ }
139
+ open = (kind: number, id: number, viewName: string, paramsJson: string, transition: number): void => {
140
+ this.rec("open", kind, id, viewName, paramsJson, transition)
141
+ this.present({ kind, id, viewName, paramsJson }, transition)
142
+ }
143
+ // Like the runtime's closeViewOf: only the destination on screen closes.
144
+ close = (kind: number, id: number, transition: number): void => {
145
+ this.rec("close", kind, id, transition)
146
+ const prev = this.current
147
+ if (!prev || prev.kind !== kind || prev.id !== id) return
148
+ this.closeCurrent()
149
+ }
150
+ private closeCurrent(): void {
151
+ const prev = this.current
152
+ if (!prev) return
153
+ this.fireDest(prev, TREE_EVENT_CLOSE)
154
+ this.current = null
155
+ if (!this.inStack(prev)) this.retire(prev)
156
+ }
157
+ private change(d: Dest, transition: number): void {
158
+ this.present(d, transition)
159
+ this.emitter?.(0, TREE_EVENT_ROUTER_CHANGE, d.kind, this.destId(d))
160
+ }
161
+ routerOpen = (kind: number, id: number, viewName: string, paramsJson: string, showBack: boolean): void => {
162
+ this.rec("routerOpen", kind, id, viewName, paramsJson, showBack)
163
+ const old = this.stack
164
+ this.stack = []
165
+ for (const d of old) if (!this.sameDest(d, this.current)) this.retire(d)
166
+ const d = { kind, id, viewName, paramsJson }
167
+ this.stack.push(d)
168
+ this.present(d, 0)
169
+ }
170
+ routerPush = (kind: number, id: number, viewName: string, paramsJson: string, transition: number, back: number): void => {
171
+ this.rec("routerPush", kind, id, viewName, paramsJson, transition, back)
172
+ const d = { kind, id, viewName, paramsJson }
173
+ this.stack.push(d)
174
+ this.change(d, transition)
175
+ }
176
+ routerReplace = (kind: number, id: number, viewName: string, paramsJson: string, transition: number, back: number): void => {
177
+ this.rec("routerReplace", kind, id, viewName, paramsJson, transition, back)
178
+ const d = { kind, id, viewName, paramsJson }
179
+ const evicted = this.stack.pop()
180
+ this.stack.push(d)
181
+ this.change(d, transition)
182
+ if (evicted) this.retire(evicted)
183
+ }
184
+ routerPop = (index: number, transition: number): void => {
185
+ this.rec("routerPop", index, transition)
186
+ if (this.stack.length <= 1) return
187
+ const count = this.stack.length
188
+ const target = index === -1 ? count - 2 : index < -1 ? Math.max(count - 1 + index, 0) : index === 0 ? 0 : Math.min(index, count - 1)
189
+ const evicted = this.stack.splice(target + 1)
190
+ this.change(this.stack[this.stack.length - 1]!, transition)
191
+ for (const d of evicted) this.retire(d)
192
+ }
193
+ routerHide = (transition: number): void => { this.rec("routerHide", transition); this.closeCurrent() }
194
+ routerRestore = (transition: number): void => { this.rec("routerRestore", transition); const top = this.stack[this.stack.length - 1]; if (top) this.present(top, transition) }
195
+ isViewSupported = (name: string): boolean => { this.rec("isViewSupported", name); return this.supportedViews.has(name) }
196
+ supportedViews = new Set<string>()
197
+ /** The contract version of a supported view (1 unless a test says otherwise). */
198
+ viewVersions = new Map<string, number>()
199
+ viewVersion = (name: string): number => this.supportedViews.has(name) ? this.viewVersions.get(name) ?? 1 : 0
200
+ viewCall = (viewId: number, method: string, args: unknown[], onComplete: (r?: unknown) => void, onReject: (m: string) => void): void => {
201
+ this.rec("viewCall", viewId, method, args)
202
+ const h = this.viewCallHandler
203
+ if (h) h(viewId, method, args, onComplete, onReject)
204
+ else onReject(`no view ${viewId}`)
205
+ }
206
+ viewCallHandler: ((viewId: number, method: string, args: any[], res: (r?: unknown) => void, rej: (m: string) => void) => void) | null = null
207
+ showWidget = (id: number, ownerKind: number, ownerId: number): void => { this.rec("showWidget", id, ownerKind, ownerId); if (!this.widgets.includes(id)) this.widgets.push(id) }
208
+ hideWidget = (id: number): void => { this.rec("hideWidget", id); this.widgets = this.widgets.filter((w) => w !== id) }
209
+
210
+ // ---- pager -----------------------------------------------------------------------------------------
211
+ private pagerTop(p: FakeNode): number | null {
212
+ if (!p.pages || !p.lens) return null
213
+ let off = 0
214
+ for (let t = 0; t < (p.index ?? 0); t++) off += p.lens[t] ?? 0
215
+ const len = p.lens[p.index ?? 0] ?? 0
216
+ return len > 0 ? p.pages[off + len - 1] ?? null : null
217
+ }
218
+ private pagerOpen = new Map<number, number>() // pager → the open page
219
+ private pagerRefresh(p: FakeNode): void {
220
+ const top = this.pagerTop(p)
221
+ const open = this.pagerOpen.get(p.id) ?? null
222
+ if (top === open) return
223
+ if (open !== null) { this.pagerOpen.delete(p.id); this.emitter?.(open, TREE_EVENT_CLOSE, TREE_DEST_SCREEN, open) }
224
+ if (top !== null) { this.pagerOpen.set(p.id, top); this.emitter?.(top, TREE_EVENT_OPEN, TREE_DEST_SCREEN, top) }
225
+ }
226
+ pagerSetContent = (id: number, pages: number[], lens: number[], index: number): void => {
227
+ this.rec("pagerSetContent", id, pages.slice(), lens.slice(), index)
228
+ const p = this.nodes.get(id); if (!p) return
229
+ const gone = (p.pages ?? []).filter((old) => !pages.includes(old))
230
+ p.pages = pages.slice(); p.lens = lens.slice(); p.index = Math.max(0, Math.min(index, lens.length - 1))
231
+ for (const pg of pages) { const n = this.nodes.get(pg); if (n) n.pagerOwner = id }
232
+ this.pagerRefresh(p) // the old top's CLOSE fires BEFORE its DETACHED, like the runtime
233
+ for (const old of gone) { const n = this.nodes.get(old); if (n) n.pagerOwner = undefined; this.emitter?.(old, TREE_EVENT_DETACHED) }
234
+ }
235
+ pagerSelect = (id: number, index: number, animated: boolean): void => {
236
+ this.rec("pagerSelect", id, index, animated)
237
+ const p = this.nodes.get(id); if (!p || !p.lens) return
238
+ p.index = Math.max(0, Math.min(index, p.lens.length - 1))
239
+ this.pagerRefresh(p)
240
+ }
241
+ private pagerOffset(p: FakeNode): number { let off = 0; for (let t = 0; t < (p.index ?? 0); t++) off += p.lens![t] ?? 0; return off }
242
+ pagerPush = (id: number, page: number, transition: number, back: number): void => {
243
+ this.rec("pagerPush", id, page, transition, back)
244
+ const p = this.nodes.get(id); if (!p || !p.pages || !p.lens) return
245
+ p.pages.splice(this.pagerOffset(p) + p.lens[p.index!]!, 0, page); p.lens[p.index!]!++
246
+ const n = this.nodes.get(page); if (n) n.pagerOwner = id
247
+ this.pagerRefresh(p)
248
+ }
249
+ pagerPop = (id: number, transition: number): void => {
250
+ this.rec("pagerPop", id, transition)
251
+ const p = this.nodes.get(id); if (!p || !p.pages || !p.lens || p.lens[p.index!]! <= 1) return
252
+ const [gone] = p.pages.splice(this.pagerOffset(p) + p.lens[p.index!]! - 1, 1); p.lens[p.index!]!--
253
+ this.pagerRefresh(p)
254
+ if (gone !== undefined) { const n = this.nodes.get(gone); if (n) n.pagerOwner = undefined; this.emitter?.(gone, TREE_EVENT_DETACHED) }
255
+ }
256
+ pagerPopToRoot = (id: number, transition: number): void => {
257
+ this.rec("pagerPopToRoot", id, transition)
258
+ const p = this.nodes.get(id); if (!p || !p.pages || !p.lens) return
259
+ const len = p.lens[p.index!]!; if (len <= 1) return
260
+ const gone = p.pages.splice(this.pagerOffset(p) + 1, len - 1); p.lens[p.index!] = 1
261
+ this.pagerRefresh(p)
262
+ for (const g of gone) { const n = this.nodes.get(g); if (n) n.pagerOwner = undefined; this.emitter?.(g, TREE_EVENT_DETACHED) }
263
+ }
264
+ pagerReplace = (id: number, page: number, transition: number, back: number): void => {
265
+ this.rec("pagerReplace", id, page, transition, back)
266
+ const p = this.nodes.get(id); if (!p || !p.pages || !p.lens) return
267
+ const at = this.pagerOffset(p) + p.lens[p.index!]! - 1
268
+ const old = p.pages[at]
269
+ p.pages[at] = page
270
+ const n = this.nodes.get(page); if (n) n.pagerOwner = id
271
+ this.pagerRefresh(p)
272
+ if (old !== undefined) { const on = this.nodes.get(old); if (on) on.pagerOwner = undefined; this.emitter?.(old, TREE_EVENT_DETACHED) }
273
+ }
274
+
275
+ // ---- vlist (the window = every key: tests don't scroll) --------------------------------------------
276
+ private vl(id: number): FakeNode["vlist"] | undefined { const n = this.nodes.get(id); if (!n) return; return n.vlist ??= { keys: [], mounted: new Map() } }
277
+ private vlistSync(id: number): void {
278
+ const v = this.vl(id); if (!v) return
279
+ const unmount = [...v.mounted.keys()].filter((k) => !v.keys.includes(k))
280
+ const mount = v.keys.filter((k) => !v.mounted.has(k))
281
+ for (const k of unmount) v.mounted.delete(k)
282
+ if (unmount.length || mount.length) this.emitter?.(id, TREE_EVENT_VLIST_SYNC, unmount, mount)
283
+ }
284
+ vlistInit = (id: number, overscan: number, inverted: boolean, start: number, end: number): void => { this.rec("vlistInit", id, overscan, inverted, start, end); this.vl(id) }
285
+ vlistSetKeys = (id: number, keys: string[], estimates: number[]): void => { this.rec("vlistSetKeys", id, keys.slice(), estimates.slice()); const v = this.vl(id); if (!v) return; v.keys = keys.slice(); this.vlistSync(id) }
286
+ vlistInsertKeys = (id: number, index: number, keys: string[], estimates: number[]): void => {
287
+ this.rec("vlistInsertKeys", id, index, keys.slice(), estimates.slice())
288
+ const v = this.vl(id); if (!v) return
289
+ if (index < 0) v.keys.push(...keys); else v.keys.splice(index, 0, ...keys)
290
+ this.vlistSync(id)
291
+ }
292
+ vlistRemoveKeys = (id: number, keys: string[]): void => { this.rec("vlistRemoveKeys", id, keys.slice()); const v = this.vl(id); if (!v) return; v.keys = v.keys.filter((k) => !keys.includes(k)); this.vlistSync(id) }
293
+ vlistInvalidate = (id: number, key: string): void => { this.rec("vlistInvalidate", id, key) }
294
+ vlistMount = (id: number, key: string, item: number): void => { this.rec("vlistMount", id, key, item); const v = this.vl(id); if (v) v.mounted.set(key, item) }
295
+ vlistScrollTo = (id: number, offset: number, animated: boolean): void => { this.rec("vlistScrollTo", id, offset, animated) }
296
+ vlistScrollToKey = (id: number, key: string, animated: boolean): void => { this.rec("vlistScrollToKey", id, key, animated) }
297
+ vlistScrollToEnd = (id: number, animated: boolean): void => { this.rec("vlistScrollToEnd", id, animated) }
298
+
299
+ // ---- misc ------------------------------------------------------------------------------------------
300
+ registerFont = (family: string, url: string, weight: number, style: number, onComplete: () => void, onReject: () => void): void => { this.rec("registerFont", family, url, weight, style); void onReject; onComplete() }
301
+ toast = (message: string, durationMs: number): void => { this.rec("toast", message, durationMs) }
302
+ pressed = 0 // the node a test declares pressed (isButtonPressed)
303
+ isButtonPressed = (id: number): boolean => { this.rec("isButtonPressed", id); return id === this.pressed }
304
+ hovered = 0 // the node a test declares under the cursor (isHovered)
305
+ isHovered = (id: number): boolean => { this.rec("isHovered", id); return id === this.hovered }
306
+ getBoundingClientRect = (id: number): [number, number, number, number] | null => this.rects.get(id) ?? null
307
+ getDisplaySize = (): [number, number] => this.displaySize
308
+ tweenCreate = (data: Float32Array, strings: string[], targets: number[]): Handle => { const id = this.nextTween++; this.tweens.push({ id, data, strings, targets }); this.rec("tweenCreate", id, targets.slice()); return { id } }
309
+ tweenControl = (id: number, op: number): void => { this.rec("tweenControl", id, op) }
310
+ tweenSeek = (id: number, ms: number): void => { this.rec("tweenSeek", id, ms) }
311
+ tweenSetRate = (id: number, rate: number): void => { this.rec("tweenSetRate", id, rate) }
312
+ tweenGetTime = (id: number): number => 0
313
+ tweenGetElapsed = (id: number): number => 0
314
+ registerTweenEvent = (cb: (id: number, kind: number, index: number, ...lanes: number[]) => void): void => { this.tweenEvent = cb }
315
+ setEmitter = (fn: (id: number, kind: number, ...args: any[]) => any): void => {
316
+ this.emitter = fn
317
+ if (!FakeTree.primary) FakeTree.primary = fn
318
+ }
319
+ /** The FIRST emitter ever installed in this process: the test SDK's (the preload calls
320
+ * `ensureEmitter()` right after installing the first fake). A test that compiles an app bundle
321
+ * and imports it runs a SECOND SDK copy whose first element calls `setEmitter` too, hijacking
322
+ * the shared fake for the rest of the process — every later file's tree events would land in
323
+ * that dead copy's pins. `installFakeTree()` therefore restores the primary, not whatever the
324
+ * previous fake last had; within the bundle test itself the bundle's emitter stays active. */
325
+ static primary: ((id: number, kind: number, ...args: any[]) => any) | null = null
326
+ }
327
+
328
+ /** Install a fresh fake as `_creatorTree`. The SDK installs its emitter / tween callback once per
329
+ * world, so the fake carries them forward: the PRIMARY emitter (the test SDK's — see
330
+ * `FakeTree.primary`), and the tween callback the previous fake had. The previous fake's nodes are
331
+ * all reported freed first (TREE_EVENT_FREED, as the runtime reports a torn-down subtree): the new
332
+ * fake restarts the ids at 1, and an element the SDK still pinned from an earlier test or file
333
+ * would otherwise answer for the new node with the same id (`parent`, `children`, events). */
334
+ export const installFakeTree = (): FakeTree => {
335
+ const prev = (globalThis as any)._creatorTree as FakeTree | undefined
336
+ if (prev) FakeTree.primary?.(0, TREE_EVENT_FREED, prev.allIds())
337
+ const fake = new FakeTree()
338
+ fake.emitter = FakeTree.primary ?? prev?.emitter ?? null
339
+ if (prev?.tweenEvent) fake.tweenEvent = prev.tweenEvent // registered once per world too
340
+ _resetTransitions() // …while a transition is registered per RUNTIME: the new fake has none
341
+ ;(globalThis as any)._creatorTree = fake
342
+ return fake
343
+ }
344
+
345
+ /** The fake currently installed. */
346
+ export const fakeTree = (): FakeTree => (globalThis as any)._creatorTree as FakeTree
347
+
348
+ /** Hand the installed fake back to the test SDK's emitter (`FakeTree.primary`). A file that evaluates
349
+ * an app bundle runs a second SDK copy whose first element calls `setEmitter` on the SHARED fake:
350
+ * it calls this in its afterAll, or every later file's tree events land in that dead copy. */
351
+ export const restorePrimaryEmitter = (): void => {
352
+ if (FakeTree.primary) fakeTree().emitter = FakeTree.primary
353
+ }
@@ -0,0 +1,31 @@
1
+ // The split host tables (sdk/src/bridges: `_creatorApp _creatorDevice _creatorFetch _creatorStorage
2
+ // _creatorMedia _creatorInput`) as MUTABLE stub objects, installed by the preload (bunfig.toml) BEFORE
3
+ // any SDK module loads. The SDK reads the globals at call time, but bun test shares one module
4
+ // registry across every test file, and the SDK's once-per-process registrations (the app-event
5
+ // dispatcher, the media emitter) hand their callback to whichever table was current at that moment.
6
+ // A test therefore MUTATES a stub (add / replace / delete members, or `Object.assign`), never
7
+ // replaces the global; `resetHostStubs()` empties them all (call it from a beforeEach that installs
8
+ // its own members, or `beforeAll` in a file that sets them once). `_creatorService`, `_creatorSocket`
9
+ // and `_creatorFiles` are feature-detected at CALL time (typeof), so a test sets / deletes those
10
+ // globals itself.
11
+ export const appStub: any = {}
12
+ export const deviceStub: any = {}
13
+ export const fetchStub: any = {}
14
+ export const storageStub: any = {}
15
+ export const mediaStub: any = {}
16
+ export const inputStub: any = {}
17
+
18
+ const g = globalThis as any
19
+ g._creatorApp = appStub
20
+ g._creatorDevice = deviceStub
21
+ g._creatorFetch = fetchStub
22
+ g._creatorStorage = storageStub
23
+ g._creatorMedia = mediaStub
24
+ g._creatorInput = inputStub
25
+
26
+ /** Empty every stub table (the members a previous test / file installed). */
27
+ export const resetHostStubs = () => {
28
+ for (const s of [appStub, deviceStub, fetchStub, storageStub, mediaStub, inputStub]) {
29
+ for (const k of Object.keys(s)) delete s[k]
30
+ }
31
+ }
@@ -0,0 +1,14 @@
1
+ // `lecodes-sdk/testing` — the fake host of the SDK's own unit tests, for a test OUTSIDE this repo
2
+ // that evaluates a compiled bundle with no runtime around (the scene editor's harness test in
3
+ // lecodes-tools): the in-memory tree bridge, the host tables as mutable stubs, the entity world's
4
+ // teardown. Such a suite preloads `lecodes-sdk/testing/preload` (bunfig.toml), exactly as this
5
+ // package's does (tests/README.md has the rules: one process, leave nothing behind).
6
+ //
7
+ // Also what a `_creator` stub needs of the SDK to answer the way an engine does: the tree's event
8
+ // constants and the quaternion (the euler order is the SDK's).
9
+
10
+ export { fakeTree, installFakeTree, restorePrimaryEmitter, FakeTree, type FakeNode, type Call } from "./fakeTree"
11
+ export { appStub, deviceStub, fetchStub, inputStub, mediaStub, storageStub, resetHostStubs } from "./hostStubs"
12
+ export { freeEntities } from "./engineWorld"
13
+ export * from "../../src/ui/tree"
14
+ export { Quat } from "../../src/math/quat"
@@ -0,0 +1,124 @@
1
+ /**
2
+ * An in-memory `MarciTransport` for tests: enough of marcidb's query language for the auth runtime and
3
+ * simple endpoint tests (equality `$where` on scalars and `one` relations, `$and`, `$order`/`$limit`/
4
+ * `$skip`, select shapes with nested `one` selects, `$increment`, updateMany/deleteMany, count, batch).
5
+ * Defaults come from the model definitions. Not a marcidb — the e2e test against the real engine is
6
+ * the source of truth; this makes the flows runnable everywhere.
7
+ */
8
+ import type { MarciOp, MarciTransport } from "../../src/server/db/marci/query"
9
+ import type { Field, FieldDef } from "../../src/server/db/fields"
10
+ import type { Model } from "../../src/server/db/types"
11
+
12
+ type Row = Record<string, any>
13
+
14
+ export const memoryMarci = (models: Record<string, Model> | (() => Record<string, Model>)) => {
15
+ const modelsOf = typeof models === "function" ? models : () => models
16
+ const tables = new Map<string, Row[]>()
17
+ const nextId = new Map<string, number>()
18
+ const defs = (model: string): Record<string, FieldDef> =>
19
+ Object.fromEntries(Object.entries(modelsOf()[model]?.fields ?? {}).map(([k, f]) => [k, (f as Field<any, any>).def]))
20
+ const rows = (model: string) => { let t = tables.get(model); if (!t) { t = []; tables.set(model, t) } return t }
21
+ const ops: MarciOp[] = []
22
+
23
+ const matchesField = (row: Row, key: string, cond: any, d: FieldDef | undefined): boolean => {
24
+ const v = row[key]
25
+ if (d && (d.kind === "one")) {
26
+ if (cond === null) return v === null || v === undefined
27
+ if (typeof cond === "object" && "id" in cond) return v?.id === cond.id
28
+ if (typeof cond === "object" && "$not" in cond) return !matchesField(row, key, cond.$not, d)
29
+ throw new Error(`memoryMarci: unsupported relation condition ${JSON.stringify(cond)}`)
30
+ }
31
+ if (cond !== null && typeof cond === "object" && !Array.isArray(cond)) {
32
+ for (const [op, x] of Object.entries(cond)) {
33
+ switch (op) {
34
+ case "$eq": if (v !== x) return false; break
35
+ case "$ne": case "$not": if (v === x) return false; break
36
+ case "$in": if (!(x as any[]).includes(v)) return false; break
37
+ case "$gt": if (!(v > (x as any))) return false; break
38
+ case "$gte": if (!(v >= (x as any))) return false; break
39
+ case "$lt": if (!(v < (x as any))) return false; break
40
+ case "$lte": if (!(v <= (x as any))) return false; break
41
+ default: throw new Error(`memoryMarci: unsupported operator ${op}`)
42
+ }
43
+ }
44
+ return true
45
+ }
46
+ return v === cond
47
+ }
48
+ const matches = (model: string, row: Row, where: any): boolean => {
49
+ if (!where) return true
50
+ if (where.$and) return where.$and.every((w: any) => matches(model, row, w))
51
+ if (where.$or) return where.$or.some((w: any) => matches(model, row, w))
52
+ if (where.$not) return !matches(model, row, where.$not)
53
+ const d = defs(model)
54
+ return Object.entries(where).every(([k, c]) => matchesField(row, k, c, d[k]))
55
+ }
56
+ const project = (model: string, row: Row, shape: Record<string, any>): Row => {
57
+ const out: Row = {}
58
+ const d = defs(model)
59
+ for (const [k, v] of Object.entries(shape)) {
60
+ if (k.startsWith("$") || v === false || v === undefined) continue
61
+ const def = d[k]
62
+ if (def?.kind === "one") {
63
+ const ref = row[k]
64
+ if (!ref) { out[k] = null; continue }
65
+ const target = rows(def.model!).find(r => r.id === ref.id)
66
+ out[k] = target ? (v === true ? { id: target.id } : project(def.model!, target, v)) : null
67
+ continue
68
+ }
69
+ out[k] = row[k] === undefined ? null : row[k]
70
+ }
71
+ return out
72
+ }
73
+ const select = (model: string, q: Record<string, any>): Row[] => {
74
+ let list = rows(model).filter(r => matches(model, r, q.$where))
75
+ if (q.$order) {
76
+ const [[f, dir]] = Object.entries(q.$order) as [string, string][]
77
+ list = [...list].sort((a, b) => (a[f] < b[f] ? -1 : a[f] > b[f] ? 1 : 0) * (dir === "desc" ? -1 : 1))
78
+ }
79
+ if (q.$skip) list = list.slice(q.$skip)
80
+ if (q.$limit !== undefined) list = list.slice(0, q.$limit)
81
+ return list.map(r => project(model, r, q))
82
+ }
83
+ const applyUpdate = (model: string, row: Row, data: Row) => {
84
+ const d = defs(model)
85
+ for (const [k, v] of Object.entries(data)) {
86
+ if (v !== null && typeof v === "object" && "$increment" in v) { row[k] = (row[k] ?? 0) + v.$increment; continue }
87
+ if (d[k]?.kind === "one") { row[k] = v === null ? null : { id: (v as any).id ?? (v as any).$connect?.id }; continue }
88
+ row[k] = v
89
+ }
90
+ }
91
+ const insert = (model: string, data: Row): Row => {
92
+ const d = defs(model)
93
+ const id = (nextId.get(model) ?? 0) + 1
94
+ nextId.set(model, id)
95
+ const row: Row = { id }
96
+ for (const [k, def] of Object.entries(d)) {
97
+ if (def.kind === "many" || def.kind === "list") continue
98
+ if (k in data && data[k] !== undefined) { row[k] = def.kind === "one" ? { id: data[k].id } : data[k]; continue }
99
+ if (def.hasDefault) { row[k] = def.default === "now" ? Date.now() : def.default; continue }
100
+ row[k] = null
101
+ }
102
+ rows(model).push(row)
103
+ return row
104
+ }
105
+ const idOf = (id: any) => (typeof id === "object" ? id.id : id)
106
+
107
+ const exec = async (op: MarciOp): Promise<any> => {
108
+ ops.push(op)
109
+ const { model, action } = op
110
+ switch (action) {
111
+ case "insert": return { id: insert(model, op.data).id }
112
+ case "findMany": return select(model, op.query ?? {})
113
+ case "findFirst": return select(model, { ...(op.query ?? {}), $limit: 1 })[0] ?? null
114
+ case "update": { const r = rows(model).find(r => r.id === idOf(op.id)); if (!r) throw new Error("not found"); applyUpdate(model, r, op.data); return null }
115
+ case "updateMany": { let n = 0; for (const r of rows(model)) if (matches(model, r, op.query?.$where)) { applyUpdate(model, r, op.data); n++ } return n }
116
+ case "delete": { const t = rows(model); const i = t.findIndex(r => r.id === idOf(op.id)); if (i >= 0) t.splice(i, 1); return i >= 0 }
117
+ case "deleteMany": { const t = rows(model); const keep = t.filter(r => !matches(model, r, op.query?.$where)); const n = t.length - keep.length; tables.set(model, keep); return n }
118
+ case "count": return rows(model).filter(r => matches(model, r, op.query?.$where)).length
119
+ default: throw new Error(`memoryMarci: unsupported action ${action}`)
120
+ }
121
+ }
122
+ const transport: MarciTransport = { exec, batch: async (batch) => { const out = []; for (const op of batch) out.push(await exec(op)); return out } }
123
+ return { transport, ops, rows, tables }
124
+ }
@@ -0,0 +1,23 @@
1
+ // The aspect frame source for every test file, installed by the preload (bunfig.toml) BEFORE any
2
+ // SDK module runs. core/Aspect.ts wires its phase runners ONCE per process: the first engine layer's
3
+ // installer wins (`_installAspectFrames`, called by the first Scene / Scene2D), and the first
4
+ // attached updater hands the runners to it — or, with no installer yet, to the `setLoop` of that
5
+ // moment. bun test shares one module registry across files, so a file's own
6
+ // `_creator.setEarlyUpdate` stub only ever saw them if that file happened to attach first (a
7
+ // DynamicBone attached with no Scene, in an earlier file, sent them into its `setLoop = () => 1`).
8
+ // A file that drives the phases by hand ticks through `tick` here instead.
9
+ //
10
+ // No fixed hook is installed: the SDK's own 1/60 accumulator steps `updateFixed` inside the early
11
+ // phase, as on a host without setFixedUpdate. Files that compile a bundle (aspect-update,
12
+ // aspect-lifecycle) run their own copy of the SDK and still capture the bridges' hooks themselves.
13
+ import { _installAspectFrames } from "../../src/core/Aspect"
14
+
15
+ type PhaseFn = (dt: number) => void
16
+
17
+ /** The two render-synced phase runners, set on the first updater's attach (undefined until then). */
18
+ export const phases: { early?: PhaseFn; late?: PhaseFn } = {}
19
+
20
+ /** One frame: the early phase (updateBefore + the fixed fallback), then the late one (update). */
21
+ export const tick = (dt: number): void => { phases.early?.(dt); phases.late?.(dt) }
22
+
23
+ _installAspectFrames((early, late) => { phases.early = early; phases.late = late; return true })
@@ -0,0 +1,18 @@
1
+ // bun test preload (sdk/bunfig.toml): every test file starts with the split host tables installed as
2
+ // mutable stubs (./hostStubs.ts — they MUST exist before the first SDK import, see there) and a fake
3
+ // tree bridge, so UI elements can be constructed anywhere (the tree of truth: an element IS a runtime
4
+ // node). Tests that inspect bridge traffic call `installFakeTree()` in their beforeEach for a fresh
5
+ // recorder, and mutate / `resetHostStubs()` the host stubs for their own members. The aspect phase
6
+ // runners go to ./phases.ts (once per process — see there); a file drives frames with its `tick`.
7
+ //
8
+ // `EDITOR` is a compile-time define (bundler.ts), so SDK source run as it is — here — has to be
9
+ // given one: `true`, the editor bundle's value, which leaves edit mode to the run-time flag a test
10
+ // sets (`__lecodesSceneEdit`) exactly as before.
11
+ ;(globalThis as { EDITOR?: boolean }).EDITOR = true
12
+ import "./hostStubs"
13
+ import "./phases"
14
+ import { installFakeTree } from "./fakeTree"
15
+ import { ensureEmitter } from "../../src/ui/tree"
16
+
17
+ installFakeTree()
18
+ ensureEmitter() // the test SDK's emitter becomes the fake's PRIMARY (see FakeTree.primary)
@@ -0,0 +1,2 @@
1
+ // The `_creatorApp` stub — installed by the preload (see ./hostStubs.ts); mutate it, never replace the global.
2
+ export { appStub } from "./hostStubs"
@@ -0,0 +1,2 @@
1
+ // The `_creatorDevice` stub — installed by the preload (see ./hostStubs.ts); mutate it, never replace the global.
2
+ export { deviceStub } from "./hostStubs"
@@ -0,0 +1,2 @@
1
+ // The `_creatorFetch` stub — installed by the preload (see ./hostStubs.ts); mutate it, never replace the global.
2
+ export { fetchStub } from "./hostStubs"
@@ -0,0 +1,2 @@
1
+ // The `_creatorInput` stub — installed by the preload (see ./hostStubs.ts); mutate it, never replace the global.
2
+ export { inputStub } from "./hostStubs"