lecodes-sdk 1.1.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 (257) 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 +11 -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/Particles.d.ts +15 -0
  25. package/dist/types/gl/Scene.d.ts +41 -13
  26. package/dist/types/gl/Texture.d.ts +1 -1
  27. package/dist/types/gl/animation/Locomotion.d.ts +59 -3
  28. package/dist/types/gl/terrain/Terrain.d.ts +8 -0
  29. package/dist/types/inject.d.ts +11 -11
  30. package/dist/types/inject.editor.d.ts +1 -0
  31. package/dist/types/net/core.d.ts +7 -0
  32. package/dist/types/plugin.d.ts +76 -0
  33. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  34. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  35. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  36. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  37. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  38. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  39. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  40. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  41. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  42. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  43. package/dist/types/runtime/app.d.ts +9 -2
  44. package/dist/types/runtime/fetch.d.ts +2 -0
  45. package/dist/types/runtime/input.d.ts +8 -1
  46. package/dist/types/runtime/media.d.ts +6 -10
  47. package/dist/types/runtime/misc.d.ts +4 -1
  48. package/dist/types/runtime/net.d.ts +3 -2
  49. package/dist/types/runtime/touch.d.ts +32 -0
  50. package/dist/types/scene/defineScene.d.ts +43 -2
  51. package/dist/types/scene/editor.d.ts +52 -0
  52. package/dist/types/scene/gizmos.d.ts +7 -4
  53. package/dist/types/ui/NativeView.d.ts +6 -4
  54. package/dist/types/ui/UI.d.ts +1 -1
  55. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  56. package/dist/types/ui/UIButton.d.ts +12 -14
  57. package/dist/types/ui/UIContainer.d.ts +0 -6
  58. package/dist/types/ui/UIImage.d.ts +1 -4
  59. package/dist/types/ui/UIInput.d.ts +8 -24
  60. package/dist/types/ui/UIModal.d.ts +0 -2
  61. package/dist/types/ui/UINode.d.ts +70 -56
  62. package/dist/types/ui/UIPager.d.ts +28 -27
  63. package/dist/types/ui/UIPopover.d.ts +0 -2
  64. package/dist/types/ui/UIScreen.d.ts +12 -17
  65. package/dist/types/ui/UIScrollable.d.ts +1 -4
  66. package/dist/types/ui/UIText.d.ts +0 -2
  67. package/dist/types/ui/UIVideo.d.ts +3 -5
  68. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  69. package/dist/types/ui/UIWidget.d.ts +10 -9
  70. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  71. package/dist/types/ui/presentable.d.ts +46 -32
  72. package/dist/types/ui/router.d.ts +18 -7
  73. package/dist/types/ui/styleColor.d.ts +1 -0
  74. package/dist/types/ui/transitions.d.ts +18 -0
  75. package/dist/types/ui/tree.d.ts +75 -0
  76. package/dist/types/version.d.ts +10 -0
  77. package/dist/types.json +1 -1
  78. package/package.json +12 -3
  79. package/prompts/2d.md +2 -6
  80. package/prompts/3d.md +1 -5
  81. package/prompts/README.md +142 -142
  82. package/prompts/canvas.md +9 -8
  83. package/prompts/compose.ts +1 -1
  84. package/prompts/core.md +3 -3
  85. package/prompts/design.md +1 -1
  86. package/prompts/dist/2d-game.md +77 -54
  87. package/prompts/dist/3d-app.md +75 -52
  88. package/prompts/dist/ar-app.md +74 -51
  89. package/prompts/dist/design.md +30 -8
  90. package/prompts/dist/ui-app.md +73 -46
  91. package/prompts/ui-design.md +2 -3
  92. package/prompts/ui.md +45 -37
  93. package/src/animate/tween/Animation.ts +262 -378
  94. package/src/animate/tween/animateValue.ts +103 -100
  95. package/src/animate/tween/easing.ts +10 -3
  96. package/src/animate/tween/spec.ts +505 -479
  97. package/src/audio/Sound.ts +3 -3
  98. package/src/audio/audio.ts +162 -161
  99. package/src/bridges/2d.d.ts +317 -0
  100. package/src/bridges/app.d.ts +91 -0
  101. package/src/bridges/audio.d.ts +97 -0
  102. package/src/bridges/canvas.d.ts +79 -0
  103. package/src/bridges/device.d.ts +72 -0
  104. package/src/bridges/fetch.d.ts +80 -0
  105. package/src/bridges/files.d.ts +70 -0
  106. package/src/bridges/gl.d.ts +1133 -0
  107. package/src/bridges/input.d.ts +72 -0
  108. package/src/bridges/media.d.ts +55 -0
  109. package/src/bridges/nav.d.ts +71 -0
  110. package/src/bridges/net.d.ts +52 -0
  111. package/src/bridges/service.d.ts +52 -0
  112. package/src/bridges/socket.d.ts +31 -0
  113. package/src/bridges/storage.d.ts +33 -0
  114. package/src/bridges/tree.d.ts +301 -0
  115. package/src/bridges/types.d.ts +49 -0
  116. package/src/canvas/Canvas.ts +114 -159
  117. package/src/canvas/gen/cssColor.ts +224 -0
  118. package/src/canvas/gen/recorder.ts +212 -0
  119. package/src/canvas/gen/spec.ts +201 -0
  120. package/src/chisel.ts +193 -0
  121. package/src/compile/assetMacro.ts +1 -1
  122. package/src/compile/bundler.ts +11 -2
  123. package/src/compile/compileProject.ts +43 -4
  124. package/src/compile/fontMacro.ts +3 -4
  125. package/src/compile/header.ts +26 -5
  126. package/src/compile/index.ts +3 -1
  127. package/src/compile/liteMaterial.ts +1 -1
  128. package/src/compile/sceneEditor.ts +11 -26
  129. package/src/core/color.ts +73 -30
  130. package/src/core/pins.ts +51 -0
  131. package/src/core/signals.ts +8 -1
  132. package/src/g2/CharacterController2D.ts +3 -3
  133. package/src/g2/Node2D.ts +57 -39
  134. package/src/g2/Physics2D.ts +2 -2
  135. package/src/g2/Scene2D.ts +35 -23
  136. package/src/g2/Texture2D.ts +1 -1
  137. package/src/g2/loop.ts +4 -4
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +13 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Model.ts +172 -167
  144. package/src/gl/Node.ts +91 -24
  145. package/src/gl/Particles.ts +20 -1
  146. package/src/gl/Scene.ts +100 -44
  147. package/src/gl/Texture.ts +8 -7
  148. package/src/gl/animation/AnimationClip.ts +1 -1
  149. package/src/gl/animation/Locomotion.ts +77 -8
  150. package/src/gl/nav/NavMesh.ts +3 -4
  151. package/src/gl/physics/Physics.ts +2 -2
  152. package/src/gl/physics/physicsEvents.ts +3 -3
  153. package/src/gl/terrain/Terrain.ts +33 -5
  154. package/src/gl/touch.ts +14 -15
  155. package/src/host.d.ts +2 -3
  156. package/src/inject.editor.ts +7 -0
  157. package/src/inject.ts +233 -236
  158. package/src/net/core.ts +6 -5
  159. package/src/net/index.ts +1 -1
  160. package/src/net/replication.ts +1 -1
  161. package/src/plugin.ts +191 -0
  162. package/src/plugins/gen/camera/contract.d.ts +27 -0
  163. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  164. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  165. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  166. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  167. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  168. package/src/plugins/gen/map/contract.d.ts +144 -0
  169. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  170. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  171. package/src/plugins/gen/push/contract.d.ts +61 -0
  172. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  173. package/src/plugins/gen/push/sdk/push.ts +105 -0
  174. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  175. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  176. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  177. package/src/plugins/permission.ts +5 -4
  178. package/src/runtime/app.ts +20 -9
  179. package/src/runtime/appEvents.ts +5 -4
  180. package/src/runtime/channel.ts +18 -15
  181. package/src/runtime/clipboard.ts +4 -3
  182. package/src/runtime/datetime.ts +2 -1
  183. package/src/runtime/device.ts +17 -15
  184. package/src/runtime/fetch.ts +30 -20
  185. package/src/runtime/files.ts +16 -15
  186. package/src/runtime/input.ts +21 -8
  187. package/src/runtime/media.ts +50 -46
  188. package/src/runtime/misc.ts +7 -3
  189. package/src/runtime/net.ts +8 -7
  190. package/src/runtime/rpc.ts +1 -3
  191. package/src/runtime/service.ts +19 -14
  192. package/src/runtime/share.ts +4 -3
  193. package/src/runtime/storage.ts +6 -4
  194. package/src/runtime/touch.ts +32 -0
  195. package/src/scene/defineScene.ts +61 -365
  196. package/src/scene/editor.ts +408 -0
  197. package/src/scene/editorPlugins.ts +3 -3
  198. package/src/scene/gizmos.ts +154 -148
  199. package/src/server/db/marci/query.ts +1 -1
  200. package/src/server/host.ts +1 -1
  201. package/src/server/runtime.ts +1 -1
  202. package/src/ui/NativeView.ts +72 -25
  203. package/src/ui/UI.ts +3 -3
  204. package/src/ui/UIBottomSheet.ts +16 -17
  205. package/src/ui/UIButton.ts +54 -16
  206. package/src/ui/UIContainer.ts +0 -6
  207. package/src/ui/UIImage.ts +34 -30
  208. package/src/ui/UIInput.ts +29 -37
  209. package/src/ui/UIModal.ts +1 -3
  210. package/src/ui/UINode.ts +347 -297
  211. package/src/ui/UIPager.ts +93 -78
  212. package/src/ui/UIPopover.ts +0 -2
  213. package/src/ui/UIScreen.ts +41 -36
  214. package/src/ui/UIScrollable.ts +19 -13
  215. package/src/ui/UISpacer.ts +1 -1
  216. package/src/ui/UITabs.ts +8 -6
  217. package/src/ui/UIText.ts +7 -17
  218. package/src/ui/UIVideo.ts +30 -27
  219. package/src/ui/UIVirtualizedList.ts +58 -59
  220. package/src/ui/UIWidget.ts +38 -18
  221. package/src/ui/colorKeys.gen.ts +37 -0
  222. package/src/ui/fonts.ts +2 -2
  223. package/src/ui/presentable.ts +59 -42
  224. package/src/ui/router.ts +52 -35
  225. package/src/ui/styleColor.ts +56 -0
  226. package/src/ui/theme.ts +7 -6
  227. package/src/ui/transitions.ts +249 -0
  228. package/src/ui/tree.ts +346 -0
  229. package/src/version.ts +24 -0
  230. package/tests/helpers/engineWorld.ts +11 -0
  231. package/tests/helpers/fakeTree.ts +353 -0
  232. package/tests/helpers/hostStubs.ts +31 -0
  233. package/tests/helpers/index.ts +14 -0
  234. package/tests/helpers/memoryMarci.ts +124 -0
  235. package/tests/helpers/phases.ts +23 -0
  236. package/tests/helpers/preload.ts +18 -0
  237. package/tests/helpers/stubApp.ts +2 -0
  238. package/tests/helpers/stubDevice.ts +2 -0
  239. package/tests/helpers/stubFetch.ts +2 -0
  240. package/tests/helpers/stubInput.ts +2 -0
  241. package/dist/inject.js +0 -4730
  242. package/dist/types/core/registry.d.ts +0 -7
  243. package/dist/types/plugins/camera.d.ts +0 -25
  244. package/dist/types/plugins/push.d.ts +0 -46
  245. package/src/bridges.d.ts +0 -1760
  246. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  247. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  248. package/src/compile/__tests__/assetName.test.ts +0 -55
  249. package/src/compile/__tests__/compile.test.ts +0 -310
  250. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  251. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  252. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  253. package/src/core/__tests__/stateMachine.test.ts +0 -132
  254. package/src/core/registry.ts +0 -23
  255. package/src/plugins/camera.ts +0 -81
  256. package/src/plugins/push.ts +0 -132
  257. package/src/plugins/qr.ts +0 -73
@@ -1,310 +0,0 @@
1
- import { test, expect } from "bun:test"
2
- import { compileProject } from "../compileProject"
3
- import { buildHeader, detectProjectKind } from "../header"
4
- import { desugarAssetMacros } from "../assetMacro"
5
-
6
- // The parity suite. This is the guard that lets the backend / CLI / renderer share ONE compiler:
7
- // it pins the header output, the engine-detection signals, the resource/shader/svg file kinds, and
8
- // the 2D↔3D tree-shake disjointness. No host is needed — we assert on the bundled text.
9
-
10
- test("a 3D project (new Scene) gets a name header and keeps GL enabled", async () => {
11
- const js = await compileProject({
12
- name: "Demo Project",
13
- publicUrl: "https://le.codes",
14
- entries: [ { path: "/main.ts", type: "text", text: "const s = new Scene()\nconsole.log(s)\n" } ],
15
- })
16
- expect(js).toContain("// name: Demo Project")
17
- expect(js).not.toContain("// gl: disable") // createScene present → GL enabled
18
- expect(js).toContain("_creator.createScene") // the SDK runtime got inlined
19
- })
20
-
21
- test("a UI-only project (no _creator) is marked gl: disable", async () => {
22
- const js = await compileProject({
23
- name: "UI",
24
- publicUrl: "https://le.codes",
25
- entries: [ { path: "/main.ts", type: "text", text: "toast('hello')\nconsole.log(1)\n" } ],
26
- })
27
- expect(js).toContain("// gl: disable")
28
- expect(js).not.toContain("// 2d: enable")
29
- })
30
-
31
- test("a 2D project (new Scene2D) is marked 2d: enable and gl: disable", async () => {
32
- const js = await compileProject({
33
- name: "RPG",
34
- publicUrl: "https://le.codes",
35
- entries: [ { path: "/main.ts", type: "text", text: "const s = new Scene2D()\ns.open()\n" } ],
36
- })
37
- expect(js).toContain("// 2d: enable") // 2D-aware hosts bring up creator-2d
38
- expect(js).toContain("// gl: disable") // every host skips Filament
39
- expect(js).toContain("_creator2d.createScene")
40
- })
41
-
42
- test("a 3D project with a Canvas is NOT marked 2d: enable (Canvas keeps a `_creator2d.` texture path)", async () => {
43
- const js = await compileProject({
44
- name: "Car",
45
- publicUrl: "https://le.codes",
46
- entries: [ { path: "/main.ts", type: "text", text: "const c = new Canvas(64, 64)\nc.update()\nnew Scene().open()\n" } ],
47
- })
48
- expect(js).toContain("_creator2d.") // the canvas 2D-texture path survives tree-shaking
49
- expect(js).not.toContain("// 2d: enable") // …but no Scene2D → the 2D engine is not in use
50
- expect(js).not.toContain("// gl: disable")
51
- })
52
-
53
- test("2D and 3D bundles are DISJOINT (tree-shake drops the other engine)", async () => {
54
- const twoD = await compileProject({
55
- name: "2D", publicUrl: "https://le.codes", header: false,
56
- entries: [ { path: "/main.ts", type: "text", text: "new Scene2D().open()\n" } ],
57
- })
58
- const threeD = await compileProject({
59
- name: "3D", publicUrl: "https://le.codes", header: false,
60
- entries: [ { path: "/main.ts", type: "text", text: "new Scene().open()\n" } ],
61
- })
62
- expect(twoD).toContain("_creator2d.")
63
- expect(twoD).not.toContain("_creator.createScene")
64
- expect(threeD).toContain("_creator.createScene")
65
- expect(threeD).not.toContain("_creator2d.")
66
- })
67
-
68
- test("preload header extracts local-asset signals from the bundle", () => {
69
- // The contract is keyed on the bundled `_creatorUtils.fetchLocal(<literal>)` call and the implicit
70
- // engine archives. (Matches the worker bundler byte-for-byte: a literal arg is captured; a call
71
- // through the fetchLocal wrapper captures its param name `path`, same as today.)
72
- expect(buildHeader("n", '_creatorUtils.fetchLocal("hero.glb")')).toContain("// preload: hero.glb")
73
- expect(buildHeader("n", "_creator.createGlb(x)")).toContain("// preload: uberarchive")
74
- expect(buildHeader("n", "_creator.captureImage(s)")).toContain("// preload: capture")
75
- })
76
-
77
- test("resource and shader files become URL modules (asset URLs from server metadata)", async () => {
78
- const js = await compileProject({
79
- name: "Assets",
80
- publicUrl: "https://le.codes",
81
- entries: [
82
- {
83
- path: "/main.ts",
84
- type: "text",
85
- text: "import logo from \"./logo.png\"\nimport mat from \"./m.mat\"\nconst s = new Scene()\nconsole.log(logo, mat, s)\n",
86
- },
87
- { path: "/logo.png", type: "resource", fileSrc: "/uploads/logo123.png" },
88
- {
89
- path: "/m.mat",
90
- type: "shader",
91
- shaders: [
92
- { platform: "opengl", src: "/uploads/sh_opengl.filamat" },
93
- { platform: "metal", src: "/uploads/sh_metal.filamat" },
94
- { platform: "desktop", src: "/uploads/sh_desktop.filamat" },
95
- ],
96
- },
97
- ],
98
- })
99
- expect(js).toContain("https://le.codes/uploads/logo123.png") // resource URL inlined
100
- expect(js).toContain("_creator.backend") // shader URL uses the runtime backend
101
- })
102
-
103
- test("locally compiled shaders load through fetchLocal (host with no asset server)", async () => {
104
- // `shadersLocal` = the CLI compiled the .mat itself and stages the artifacts next to the
105
- // bundle, so the module resolves a LOCAL file instead of a URL. The name is assembled at
106
- // RUNTIME: one bundle serves every native backend (desktop host = "desktop" on Windows,
107
- // "opengl" on Linux, Apple = "metal").
108
- const js = await compileProject({
109
- name: "Local shader",
110
- publicUrl: "https://le.codes",
111
- localAssets: true,
112
- minify: false,
113
- entries: [
114
- { path: "/main.ts", type: "text", text: `import mat from "./m.mat"
115
- console.log(mat)
116
- ` },
117
- {
118
- path: "/m.mat",
119
- type: "shader",
120
- shadersLocal: true,
121
- shaders: [
122
- { platform: "opengl", src: "abc123_opengl.filamat" },
123
- { platform: "metal", src: "abc123_metal.filamat" },
124
- { platform: "desktop", src: "abc123_desktop.filamat" },
125
- ],
126
- },
127
- ],
128
- })
129
- expect(js).toContain('_creatorUtils.fetchLocal("abc123_" + _creator.backend + ".filamat")')
130
- expect(js).toContain('"id:"') // the host's local-resource scheme, like asset()
131
- expect(js).not.toContain("https://le.codes/abc123") // never a URL in this mode
132
- })
133
-
134
- test("a shader with server artifacts keeps its URL even under localAssets", async () => {
135
- // Only entries the CLI actually compiled+staged carry `shadersLocal`. A server-backed project
136
- // whose shaders came from the backend must NOT be rewritten to fetchLocal — nothing staged
137
- // those files next to the bundle.
138
- const js = await compileProject({
139
- name: "Server shader",
140
- publicUrl: "https://le.codes",
141
- localAssets: true,
142
- minify: false,
143
- entries: [
144
- { path: "/main.ts", type: "text", text: `import mat from "./m.mat"
145
- console.log(mat)
146
- ` },
147
- {
148
- path: "/m.mat",
149
- type: "shader",
150
- shaders: [
151
- { platform: "opengl", src: "/uploads/sh_opengl.filamat" },
152
- { platform: "metal", src: "/uploads/sh_metal.filamat" },
153
- { platform: "desktop", src: "/uploads/sh_desktop.filamat" },
154
- ],
155
- },
156
- ],
157
- })
158
- expect(js).toContain("https://le.codes/uploads/sh_")
159
- expect(js).not.toContain("fetchLocal")
160
- })
161
-
162
- test("a computed fetchLocal argument never lands in the preload header", () => {
163
- // The local-shader lookup builds its name at runtime; only literal names are files to preload.
164
- const header = buildHeader("n", '_creatorUtils.fetchLocal("abc_" + _creator.backend + ".filamat")')
165
- expect(header).not.toContain("// preload:")
166
- expect(buildHeader("n", '_creatorUtils.fetchLocal("hero.glb")')).toContain("// preload: hero.glb")
167
- })
168
-
169
- test("asset() desugaring never shifts original line numbers (source-map integrity)", () => {
170
- // The bundle's source map is used against the ORIGINAL file the editor shows (error overlay,
171
- // element inspector). Injected import lines at the top would offset every following line.
172
- const src = "const a = asset('./x.png')\nconst b = asset('./y.png')\nconst c = asset('./x.png')\nconsole.log(a, b, c)"
173
- const out = desugarAssetMacros(src, "/main.ts")
174
- const lines = out.split("\n")
175
- expect(lines[0]).toBe("const a = __asset_0")
176
- expect(lines[1]).toBe("const b = __asset_1")
177
- expect(lines[2]).toBe("const c = __asset_0") // identical path → one identifier
178
- expect(lines[3]).toBe("console.log(a, b, c)")
179
- // The imports live AFTER the original content (ESM hoists them, so position is irrelevant).
180
- expect(out.indexOf("import __asset_0")).toBeGreaterThan(out.indexOf("console.log"))
181
- expect(out).toContain('import __asset_1 from "./y.png";')
182
- })
183
-
184
- test("a project using asset() bundles with the appended (hoisted) imports", async () => {
185
- const js = await compileProject({
186
- name: "A", publicUrl: "https://le.codes", header: false,
187
- entries: [
188
- { path: "/main.ts", type: "text", text: "const logo = asset('./logo.png')\ntoast(logo)\n" },
189
- { path: "/logo.png", type: "resource", fileSrc: "/uploads/logo1.png" },
190
- ],
191
- })
192
- expect(js).toContain("https://le.codes/uploads/logo1.png")
193
- })
194
-
195
- test("respects an explicit entryOverride and header:false", async () => {
196
- const js = await compileProject({
197
- name: "X",
198
- publicUrl: "https://le.codes",
199
- entryOverride: "/start.ts",
200
- header: false,
201
- entries: [
202
- { path: "/start.ts", type: "text", text: "import { go } from \"./lib\"\ngo()\n" },
203
- { path: "/lib.ts", type: "text", text: "export const go = () => new Scene()\n" },
204
- ],
205
- })
206
- expect(js).not.toContain("// name:") // header omitted
207
- expect(js).toContain("_creator.createScene") // the imported lib was bundled & called
208
- })
209
-
210
- test("iife format produces a self-running bundle", async () => {
211
- const js = await compileProject({
212
- name: "IIFE", publicUrl: "https://le.codes", header: false, format: "iife",
213
- entries: [ { path: "/main.ts", type: "text", text: "new Scene2D().open()\n" } ],
214
- })
215
- // iife wraps in a function expression and still references host globals freely
216
- expect(js).toMatch(/\(\s*\(\s*\)\s*=>\s*\{|\(function/)
217
- expect(js).toContain("_creator2d.")
218
- })
219
-
220
- // ── User component libraries (bare-specifier imports → /__lib) ──────────────────────────────
221
-
222
- test("a project imports a component from a user library (bare specifier → /__lib)", async () => {
223
- const js = await compileProject({
224
- name: "L", publicUrl: "https://le.codes", header: false,
225
- entries: [ { path: "/main.ts", type: "text", text: `import { Card } from "acme-ui"\nCard().open()\n` } ],
226
- libraries: { "acme-ui": { "/index.ts": `export const Card = () => UIColumn()` } },
227
- })
228
- // The library's UIColumn call was resolved & inlined — its element-type literal survives minify.
229
- expect(js).toContain(`"column"`)
230
- })
231
-
232
- test("a library sub-path import resolves", async () => {
233
- const js = await compileProject({
234
- name: "L", publicUrl: "https://le.codes", header: false,
235
- entries: [ { path: "/main.ts", type: "text", text: `import { accent } from "acme-ui/theme"\ntoast(accent)\n` } ],
236
- libraries: { "acme-ui": { "/theme.ts": `export const accent = "#4f46e5"` } },
237
- })
238
- expect(js).toContain("#4f46e5")
239
- })
240
-
241
- test("a library-internal relative import resolves within the /__lib prefix", async () => {
242
- const js = await compileProject({
243
- name: "L", publicUrl: "https://le.codes", header: false,
244
- entries: [ { path: "/main.ts", type: "text", text: `import { Card } from "acme-ui"\nCard().open()\n` } ],
245
- libraries: { "acme-ui": {
246
- "/index.ts": `export { Card } from "./components/card"`,
247
- "/components/card.ts": `export const Card = () => UIRow()`,
248
- } },
249
- })
250
- expect(js).toContain(`"row"`) // the re-exported, library-internal component resolved & bundled
251
- })
252
-
253
- test("asset() inside a library resolves library-relative", async () => {
254
- const js = await compileProject({
255
- name: "L", publicUrl: "https://le.codes", header: false,
256
- entries: [ { path: "/main.ts", type: "text", text: `import { Logo } from "acme-ui"\ntoast(Logo())\n` } ],
257
- libraries: { "acme-ui": {
258
- "/index.ts": `export const Logo = () => asset("./logo.png")`,
259
- "/logo.png": `export default "https://le.codes/uploads/liblogo.png"`,
260
- } },
261
- })
262
- expect(js).toContain("https://le.codes/uploads/liblogo.png")
263
- })
264
-
265
- test("an unknown bare import fails with the friendly library message", async () => {
266
- await expect(compileProject({
267
- name: "L", publicUrl: "https://le.codes", header: false,
268
- entries: [ { path: "/main.ts", type: "text", text: `import { X } from "nope-lib"\nX()\n` } ],
269
- })).rejects.toThrow(/Unknown library "nope-lib"/)
270
- })
271
-
272
- test("a bare-specifier-shaped string literal is NOT rewritten (no false positive)", async () => {
273
- const js = await compileProject({
274
- name: "L", publicUrl: "https://le.codes", header: false,
275
- entries: [ { path: "/main.ts", type: "text", text: `const msg = "import X from 'acme-ui'"\ntoast(msg)\n` } ],
276
- libraries: { "acme-ui": { "/index.ts": `export const X = 1` } },
277
- })
278
- expect(js).toContain("import X from 'acme-ui'") // the string literal survives verbatim
279
- })
280
-
281
- test("reactive_ui desugaring applies to library files (UI written like app code)", async () => {
282
- const js = await compileProject({
283
- name: "L", publicUrl: "https://le.codes", header: false, minify: false,
284
- entries: [ { path: "/main.ts", type: "text", text: `import { List } from "acme-ui"\nList(["a","b"]).open()\n` } ],
285
- libraries: { "acme-ui": { "/index.ts": `export const List = (items) => UIColumn(() => items.map(i => UIText(i)))` } },
286
- })
287
- expect(js).toContain("__uiMap") // the children .map inside the library got memoized like user code
288
- })
289
-
290
- test("buildHeader / detectProjectKind signal the engine from bundled code", () => {
291
- expect(buildHeader("n", "_creator2d.createScene()")).toContain("// 2d: enable")
292
- expect(buildHeader("n", "const x = 1")).toContain("// gl: disable")
293
- expect(buildHeader("n", "_creator.createARController()")).toContain("// ar: enable")
294
- expect(detectProjectKind("_creator2d.createScene()")).toBe("2d")
295
- expect(detectProjectKind("_creator.createScene()")).toBe("3d")
296
- expect(detectProjectKind("const x = 1")).toBe("ui")
297
- expect(detectProjectKind("_creator.createARController()")).toBe("ar")
298
- })
299
-
300
- test("a .json project file is data — asset()/import gives the parsed value", async () => {
301
- const js = await compileProject({
302
- name: "Data", publicUrl: "https://le.codes", header: false,
303
- entries: [
304
- { path: "/main.ts", type: "text", text: 'toast(asset("./style.json").name)\n' },
305
- { path: "/style.json", type: "text", text: '{ "name": "OSM Bright", "version": 8 }\n' },
306
- ],
307
- })
308
- expect(js).toContain("OSM Bright")
309
- expect(js).not.toContain('"./style.json"') // desugared to the import, not left as a string
310
- })
@@ -1,151 +0,0 @@
1
- import { test, expect } from "bun:test"
2
- import { detectEntry, scanModulesFallback } from "../detectEntry"
3
- import { compileProject } from "../compileProject"
4
-
5
- // Entry detection contract: the entry is an import-graph ROOT (no other file imports it), with
6
- // /main.ts as the always-wins convention. These scenarios must resolve identically through the
7
- // chisel scan and the regex fallback — the suite passes with either binary generation.
8
-
9
- const text = (path: string, t: string) => ({ path, type: "text" as const, text: t })
10
-
11
- test("a side-effect registration module imported by the entry is never the entry (even when bigger)", () => {
12
- const { entry, roots } = detectEntry([
13
- text("/app.ts", "import './register'\ntoast('hi')\n"),
14
- text("/register.ts", "// registration code, no exports, larger than the entry\n" + "registerThing()\n".repeat(20)),
15
- ])
16
- expect(entry).toBe("/app.ts")
17
- expect(roots).toEqual(["/app.ts"])
18
- })
19
-
20
- test(".d.ts files are never the entry", () => {
21
- const { entry } = detectEntry([
22
- text("/app.ts", "toast('hi')\n"),
23
- text("/globals.d.ts", "declare const BIG: number\n".repeat(50)),
24
- ])
25
- expect(entry).toBe("/app.ts")
26
- })
27
-
28
- test("the word 'export' inside a string does not disqualify the entry", () => {
29
- const { entry } = detectEntry([
30
- text("/app.ts", "toast(\"export done\")\ntoast('more')\n"),
31
- text("/tiny.ts", "console.log(1)\n"),
32
- ])
33
- expect(entry).toBe("/app.ts") // both are roots; app.ts is the larger script
34
- })
35
-
36
- test("a single file WITH exports is still a valid entry (old heuristic errored)", () => {
37
- const { entry } = detectEntry([
38
- text("/app.ts", "export const state = signal(0)\nUIScreen([]).open()\n"),
39
- ])
40
- expect(entry).toBe("/app.ts")
41
- })
42
-
43
- test("/main.ts wins over a bigger unimported script", () => {
44
- const { entry } = detectEntry([
45
- text("/main.ts", "import { s } from './scratch'\ntoast(s)\n"),
46
- text("/old-proto.ts", "console.log('leftover prototype')\n".repeat(30)),
47
- text("/scratch.ts", "export const s = 'x'\n"),
48
- ])
49
- expect(entry).toBe("/main.ts")
50
- })
51
-
52
- test("an imported /main.ts is not a root — the importer wins (paranoid guard)", () => {
53
- const { entry } = detectEntry([
54
- text("/run.ts", "import './main'\nconsole.log('wrapper')\n"),
55
- text("/main.ts", "toast('app')\n"),
56
- ])
57
- expect(entry).toBe("/run.ts")
58
- })
59
-
60
- test(".editor.ts files are never the entry and their imports don't consume candidacy", () => {
61
- const { entry } = detectEntry([
62
- text("/app.ts", "toast('hi')\n"),
63
- text("/gizmo.editor.ts", "import './app'\nconsole.log('editor plugin, no exports, big')\n".repeat(5)),
64
- ])
65
- expect(entry).toBe("/app.ts")
66
- })
67
-
68
- test("two unrelated scripts: ambiguity is reported via roots, larger wins", () => {
69
- const { entry, roots } = detectEntry([
70
- text("/a.ts", "toast('a')\n".repeat(5)),
71
- text("/b.ts", "toast('b')\n"),
72
- ])
73
- expect(entry).toBe("/a.ts")
74
- expect(roots.sort()).toEqual(["/a.ts", "/b.ts"])
75
- })
76
-
77
- test("export-free roots beat exporting roots regardless of size", () => {
78
- const { entry } = detectEntry([
79
- text("/app.ts", "toast('hi')\n"),
80
- text("/helpers.ts", "export const unusedHelpers = 1\n".repeat(10)),
81
- ])
82
- expect(entry).toBe("/app.ts")
83
- })
84
-
85
- test("resources and svg files never participate", () => {
86
- const { entry } = detectEntry([
87
- text("/app.ts", "toast('hi')\n"),
88
- { path: "/logo.png", type: "resource" as const, fileSrc: "/uploads/x.png" },
89
- text("/icon.svg", "<svg></svg>"),
90
- ])
91
- expect(entry).toBe("/app.ts")
92
- })
93
-
94
- // ── the regex fallback scanner (exercised directly — chisel may cover detectEntry above) ──────
95
-
96
- test("fallback scanner: resolves relative/absolute specifiers with ts/js/index fallbacks", () => {
97
- const scan = scanModulesFallback({
98
- "/main.ts": "import './reg'\nimport { a } from \"./sub/a\"\nimport b from '/b.js'\nimport { c } from './pkg'\nimport 'acme-lib'\n",
99
- "/reg.ts": "console.log(1)",
100
- "/sub/a.ts": "export const a = 1",
101
- "/b.js": "export default 2",
102
- "/pkg/index.ts": "export const c = 3",
103
- })
104
- expect(scan["/main.ts"].imports.sort()).toEqual(["/b.js", "/pkg/index.ts", "/reg.ts", "/sub/a.ts"])
105
- expect(scan["/main.ts"].hasExports).toBe(false)
106
- expect(scan["/sub/a.ts"].hasExports).toBe(true)
107
- })
108
-
109
- test("fallback scanner: multi-line imports and re-exports count as edges", () => {
110
- const scan = scanModulesFallback({
111
- "/main.ts": "import {\n one,\n two,\n} from './util'\nexport * from './hub'\n",
112
- "/util.ts": "export const one = 1, two = 2",
113
- "/hub.ts": "export const h = 1",
114
- })
115
- expect(scan["/main.ts"].imports.sort()).toEqual(["/hub.ts", "/util.ts"])
116
- expect(scan["/main.ts"].hasExports).toBe(true)
117
- })
118
-
119
- // ── end to end through compileProject ─────────────────────────────────────────────────────────
120
-
121
- test("compileProject auto-detects the importer as entry and runs the registration module", async () => {
122
- const js = await compileProject({
123
- name: "Reg", publicUrl: "https://le.codes", header: false,
124
- entries: [
125
- text("/app.ts", "import './register'\ntoast('app-side-effect')\n"),
126
- // Bigger than the entry — the old size heuristic picked this and dropped app.ts entirely.
127
- text("/register.ts", "console.log('register-side-effect')\n// padding padding padding padding padding\n"),
128
- ],
129
- })
130
- expect(js).toContain("app-side-effect")
131
- expect(js).toContain("register-side-effect")
132
- })
133
-
134
- test("design/ screens are never the entry — a design-only project has none, app code next to a design wins", () => {
135
- // A Figma import: screens on the board, shared modules, no app yet. The largest root used to win.
136
- const designOnly = detectEntry([
137
- text("/design/screens/a4-40.ts", "import { at } from '../shared/figma'\nexport default UIScreen([])\n" + "// big\n".repeat(50)),
138
- text("/design/shared/figma.ts", "export const at = () => ({})\n"),
139
- text("/app.server.ts", "export function ping() { return 1 }\n"),
140
- ])
141
- expect(designOnly.entry).toBe("")
142
- expect(designOnly.roots).toEqual([])
143
-
144
- // The same project once the app exists: main.ts is the entry however small it is.
145
- const withApp = detectEntry([
146
- text("/design/screens/a4-40.ts", "export default UIScreen([])\n" + "// big\n".repeat(50)),
147
- text("/main.ts", "Router.init(home)\n"),
148
- ])
149
- expect(withApp.entry).toBe("/main.ts")
150
- expect(withApp.roots).toEqual(["/main.ts"])
151
- })
@@ -1,199 +0,0 @@
1
- import { describe, expect, test } from "bun:test"
2
- import { applyFontMacro, buildFontsHeaderValue, scanFontCalls, type FontMacroEntry } from "../fontMacro"
3
- import { parseSfnt } from "../sfnt"
4
-
5
- // ---- a minimal sfnt fixture (name + OS/2 tables — exactly what parseSfnt reads) -----------------
6
-
7
- const buildFont = (opts: { family: string, weight?: number, italic?: boolean, tag?: number }): Uint8Array => {
8
- const nameStr: number[] = []
9
- for (const ch of opts.family) { const c = ch.charCodeAt(0); nameStr.push(c >> 8, c & 0xff) }
10
- const name = new Uint8Array(18 + nameStr.length)
11
- const ndv = new DataView(name.buffer)
12
- ndv.setUint16(2, 1) // count
13
- ndv.setUint16(4, 18) // stringOffset
14
- ndv.setUint16(6, 3) // platformID: windows
15
- ndv.setUint16(8, 1) // encodingID: unicode BMP
16
- ndv.setUint16(10, 0x409) // languageID: en-US
17
- ndv.setUint16(12, 1) // nameID: family
18
- ndv.setUint16(14, nameStr.length)
19
- name.set(nameStr, 18)
20
-
21
- const os2 = new Uint8Array(96)
22
- const odv = new DataView(os2.buffer)
23
- odv.setUint16(4, opts.weight ?? 400) // usWeightClass
24
- odv.setUint16(62, opts.italic ? 1 : 0) // fsSelection ITALIC
25
-
26
- const tables: [string, Uint8Array][] = [["name", name], ["OS/2", os2]]
27
- const out = new Uint8Array(12 + tables.length * 16 + tables.reduce((s, [, t]) => s + t.length, 0))
28
- const dv = new DataView(out.buffer)
29
- dv.setUint32(0, opts.tag ?? 0x00010000)
30
- dv.setUint16(4, tables.length)
31
- let off = 12 + tables.length * 16
32
- tables.forEach(([tag, data], i) => {
33
- const rec = 12 + i * 16
34
- for (let k = 0; k < 4; k++) out[rec + k] = tag.charCodeAt(k)
35
- dv.setUint32(rec + 8, off)
36
- dv.setUint32(rec + 12, data.length)
37
- out.set(data, off)
38
- off += data.length
39
- })
40
- return out
41
- }
42
-
43
- describe("parseSfnt", () => {
44
- test("family from the name table, weight/italic from OS/2", () => {
45
- const info = parseSfnt(buildFont({ family: "Brand Sans", weight: 600, italic: true }))
46
- expect(info).toEqual({ family: "Brand Sans", weight: 600, italic: true })
47
- })
48
- test("OTTO (CFF) fonts parse too", () => {
49
- expect(parseSfnt(buildFont({ family: "Serifed", tag: 0x4f54544f })).family).toBe("Serifed")
50
- })
51
- test("collections and WOFF are clear errors", () => {
52
- expect(() => parseSfnt(buildFont({ family: "X", tag: 0x74746366 }))).toThrow(/collections/)
53
- expect(() => parseSfnt(buildFont({ family: "X", tag: 0x774f4646 }))).toThrow(/WOFF/)
54
- expect(() => parseSfnt(new Uint8Array([1, 2, 3]))).toThrow(/truncated/)
55
- })
56
- })
57
-
58
- describe("scanFontCalls", () => {
59
- test("bare registry call: id, span, no error", () => {
60
- const src = `theme({ fontFamily: font("manrope") })`
61
- const [c] = scanFontCalls(src)
62
- expect(c.kind).toBe("registry")
63
- expect(c.id).toBe("manrope")
64
- expect(c.error).toBeUndefined()
65
- expect(src.slice(c.start, c.end)).toBe(`font("manrope")`)
66
- })
67
- test("member calls, strings and comments are not matched", () => {
68
- expect(scanFontCalls(`ctx.font("x"); "font(\\"y\\")"; // font("z")\n/* font("w") */`)).toHaveLength(0)
69
- })
70
- test("dynamic argument is a hard error", () => {
71
- expect(scanFontCalls(`font(name)`)[0].error).toMatch(/string-literal/)
72
- })
73
- test("bad id shape is a hard error", () => {
74
- expect(scanFontCalls(`font("Manrope")`)[0].error).toMatch(/registry id/)
75
- })
76
- test("system generic families point at plain fontFamily, not font()", () => {
77
- expect(scanFontCalls(`font("serif")`)[0].error).toMatch(/system font.*fontFamily/)
78
- expect(scanFontCalls(`font("monospaced")`)[0].error).toMatch(/system font/)
79
- expect(scanFontCalls(`font("sans-serif")`)[0].error).toMatch(/system font/)
80
- })
81
- test("options: weights + italic literals", () => {
82
- const [c] = scanFontCalls(`font("rubik", { weights: [400, 700], italic: true })`)
83
- expect(c.weights).toEqual([400, 700])
84
- expect(c.italic).toBe(true)
85
- })
86
- test("unknown / non-literal options are hard errors", () => {
87
- expect(scanFontCalls(`font("rubik", { weight: 400 })`)[0].error).toMatch(/unknown option/)
88
- expect(scanFontCalls(`font("rubik", { weights: w })`)[0].error).toMatch(/literal number array/)
89
- })
90
- test("path-shaped argument: file kind; options rejected; non-ttf rejected", () => {
91
- expect(scanFontCalls(`font("./fonts/Brand.ttf")`)[0].kind).toBe("file")
92
- expect(scanFontCalls(`font("./fonts/Brand.ttf", { weights: [400] })`)[0].error).toMatch(/no options/)
93
- expect(scanFontCalls(`font("./fonts/Brand.woff2")`)[0].error).toMatch(/\.ttf\/\.otf/)
94
- })
95
- })
96
-
97
- const entry = (path: string, text: string): FontMacroEntry => ({ path, type: "text", text })
98
-
99
- describe("applyFontMacro", () => {
100
- test("cdn mode: call → imported family const; sibling module registers defaultWeights faces", async () => {
101
- const entries = [entry("/index.ts", `theme({ fontFamily: font("manrope") })`)]
102
- const faces = await applyFontMacro(entries, { mode: "cdn", registryBase: "https://cdn.test/v1" })
103
-
104
- expect(faces.map((f) => f.weight).sort()).toEqual([400, 500, 700])
105
- expect(entries[0].text).toContain(`theme({ fontFamily: __font_0 })`)
106
- expect(entries[0].text).toContain(`import { __font_0 } from "./.index.ts.__fonts.ts"`)
107
-
108
- const mod = entries.find((e) => e.path.endsWith(".__fonts.ts"))!
109
- expect(mod.text).toContain(`export const __font_0 = "Manrope"`)
110
- expect(mod.text).toContain(
111
- `_creatorUI.registerFont("Manrope", "https://cdn.test/v1/manrope/manrope-400." + (_creatorUI.fontFormat ?? "ttf"), { weight: 400 }, () => {}, () => {})`,
112
- )
113
- })
114
-
115
- test("weights narrow the face set; an unavailable weight is a clear error", async () => {
116
- const one = [entry("/a.ts", `font("manrope", { weights: [200] })`)]
117
- expect((await applyFontMacro(one, { mode: "cdn" })).map((f) => f.weight)).toEqual([200])
118
- await expect(applyFontMacro([entry("/a.ts", `font("manrope", { weights: [150] })`)], { mode: "cdn" }))
119
- .rejects.toThrow(/no 150 face/)
120
- })
121
-
122
- test("italic adds italic faces; a family without italics errors", async () => {
123
- const entries = [entry("/a.ts", `font("rubik", { weights: [400], italic: true })`)]
124
- const faces = await applyFontMacro(entries, { mode: "cdn" })
125
- expect(faces.map((f) => f.italic).sort()).toEqual([false, true])
126
- expect(entries.find((e) => e.path.endsWith(".__fonts.ts"))!.text).toContain(`{ weight: 400, style: "italic" }`)
127
- await expect(applyFontMacro([entry("/a.ts", `font("manrope", { italic: true })`)], { mode: "cdn" }))
128
- .rejects.toThrow(/no italic/)
129
- })
130
-
131
- test("unknown registry id lists available families", async () => {
132
- await expect(applyFontMacro([entry("/a.ts", `font("comic-sans")`)], { mode: "cdn" }))
133
- .rejects.toThrow(/unknown registry font\. Available: /)
134
- })
135
-
136
- test("vendor mode: hook called once per face; module imports the vendored entries", async () => {
137
- const vendored: string[] = []
138
- const entries = [entry("/shared/tokens.ts", `export const brand = font("manrope", { weights: [400, 700] })`)]
139
- await applyFontMacro(entries, {
140
- mode: "vendor",
141
- vendorFace: (f) => { vendored.push(f.fileName); return `/assets/fonts/${f.fileName}` },
142
- })
143
- expect(vendored.sort()).toEqual(["manrope-400.ttf", "manrope-700.ttf"])
144
- const mod = entries.find((e) => e.path.endsWith(".__fonts.ts"))!
145
- expect(mod.path).toBe("shared/.tokens.ts.__fonts.ts")
146
- expect(mod.text).toContain(`import __face_0 from "../assets/fonts/manrope-400.ttf"`)
147
- expect(mod.text).toContain(`_creatorUI.registerFont("Manrope", __face_0, { weight: 400 }, () => {}, () => {})`)
148
- })
149
-
150
- test("project file: family/weight/italic come from the font itself; same family across files dedupes", async () => {
151
- const bytes = buildFont({ family: "Brand Sans", weight: 600 })
152
- const entries = [
153
- entry("/index.ts", `Text("x").style({ fontFamily: font("./fonts/Brand.ttf") })`),
154
- entry("/screens/home.ts", `font("../fonts/Brand.ttf")`),
155
- { path: "/fonts/Brand.ttf", type: "resource" },
156
- ]
157
- const faces = await applyFontMacro(entries, { mode: "cdn", readBytes: () => bytes })
158
- expect(faces).toHaveLength(1)
159
- expect(faces[0]).toMatchObject({ family: "Brand Sans", weight: 600, italic: false, source: { kind: "entry", path: "fonts/Brand.ttf" } })
160
- expect(entries[0].text).toContain(`fontFamily: __font_0`)
161
- const homeMod = entries.find((e) => e.path === "screens/.home.ts.__fonts.ts")!
162
- expect(homeMod.text).toContain(`import __face_0 from "../fonts/Brand.ttf"`)
163
- })
164
-
165
- test("a missing project font file is a clear error", async () => {
166
- await expect(applyFontMacro([entry("/a.ts", `font("./nope.ttf")`)], { mode: "cdn", readBytes: () => null }))
167
- .rejects.toThrow(/no such file/)
168
- })
169
-
170
- test("a file that declares its own `font` is left alone", async () => {
171
- const src = `const font = (s: string) => s\nfont("manrope")`
172
- const entries = [entry("/a.ts", src)]
173
- expect(await applyFontMacro(entries, { mode: "cdn" })).toEqual([])
174
- expect(entries[0].text).toBe(src)
175
- })
176
-
177
- test("two files using the same family share the face set (one registration source each)", async () => {
178
- const entries = [entry("/a.ts", `font("manrope")`), entry("/b.ts", `font("manrope")`)]
179
- const faces = await applyFontMacro(entries, { mode: "cdn" })
180
- expect(faces).toHaveLength(3)
181
- expect(entries.filter((e) => e.path.endsWith(".__fonts.ts"))).toHaveLength(2)
182
- })
183
- })
184
-
185
- describe("buildFontsHeaderValue", () => {
186
- test("cdn faces carry {fmt}; entry faces need an absolute URL or are omitted", () => {
187
- const value = buildFontsHeaderValue([
188
- { family: "Manrope", weight: 400, italic: false, source: { kind: "cdn", urlBase: "https://cdn.test/v1/manrope/manrope-400." } },
189
- { family: "Rubik", weight: 700, italic: true, source: { kind: "cdn", urlBase: "https://cdn.test/v1/rubik/rubik-700-italic." } },
190
- { family: "Brand", weight: 400, italic: false, source: { kind: "entry", path: "fonts/Brand.ttf" } },
191
- ], (path) => (path === "fonts/Brand.ttf" ? "https://api.test/uploads/b.ttf" : null))
192
- expect(value).toBe(
193
- "Manrope@400=https://cdn.test/v1/manrope/manrope-400.{fmt};"
194
- + "Rubik@700i=https://cdn.test/v1/rubik/rubik-700-italic.{fmt};"
195
- + "Brand@400=https://api.test/uploads/b.ttf",
196
- )
197
- expect(buildFontsHeaderValue([{ family: "B", weight: 400, italic: false, source: { kind: "entry", path: "x.ttf" } }])).toBe("")
198
- })
199
- })