lecodes-sdk 0.19.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 (367) hide show
  1. package/README.md +114 -0
  2. package/dist/global.d.ts +503 -0
  3. package/dist/host.d.ts +61 -0
  4. package/dist/inject.js +4730 -0
  5. package/dist/types/animate/animate.d.ts +20 -0
  6. package/dist/types/animate/bezier.d.ts +2 -0
  7. package/dist/types/animate/easings.d.ts +21 -0
  8. package/dist/types/canvas/Canvas.d.ts +168 -0
  9. package/dist/types/core/Aspect.d.ts +171 -0
  10. package/dist/types/core/InspectorUI.d.ts +88 -0
  11. package/dist/types/core/StateMachine.d.ts +81 -0
  12. package/dist/types/core/color.d.ts +13 -0
  13. package/dist/types/core/compWrite.d.ts +11 -0
  14. package/dist/types/core/events.d.ts +11 -0
  15. package/dist/types/core/fields.d.ts +58 -0
  16. package/dist/types/core/registry.d.ts +7 -0
  17. package/dist/types/core/signals.d.ts +29 -0
  18. package/dist/types/core/time.d.ts +31 -0
  19. package/dist/types/g2/Camera2D.d.ts +19 -0
  20. package/dist/types/g2/CharacterController2D.d.ts +134 -0
  21. package/dist/types/g2/Node2D.d.ts +91 -0
  22. package/dist/types/g2/OneWay2D.d.ts +24 -0
  23. package/dist/types/g2/Physics2D.d.ts +147 -0
  24. package/dist/types/g2/Scene2D.d.ts +72 -0
  25. package/dist/types/g2/Shape2D.d.ts +58 -0
  26. package/dist/types/g2/Sprite.d.ts +51 -0
  27. package/dist/types/g2/SpriteAnimation.d.ts +50 -0
  28. package/dist/types/g2/SpriteSheet.d.ts +69 -0
  29. package/dist/types/g2/Texture2D.d.ts +16 -0
  30. package/dist/types/g2/Tilemap.d.ts +20 -0
  31. package/dist/types/g2/Tileset.d.ts +32 -0
  32. package/dist/types/g2/Trigger2D.d.ts +20 -0
  33. package/dist/types/g2/autotile.d.ts +133 -0
  34. package/dist/types/g2/cells.d.ts +14 -0
  35. package/dist/types/g2/defineScene2d.d.ts +141 -0
  36. package/dist/types/g2/groups2d.d.ts +16 -0
  37. package/dist/types/g2/loop.d.ts +4 -0
  38. package/dist/types/g2/scenarios2d.d.ts +30 -0
  39. package/dist/types/g2/touch.d.ts +2 -0
  40. package/dist/types/gl/Camera.d.ts +79 -0
  41. package/dist/types/gl/CameraPlace.d.ts +26 -0
  42. package/dist/types/gl/CharacterController.d.ts +98 -0
  43. package/dist/types/gl/Gearbox.d.ts +86 -0
  44. package/dist/types/gl/Geometry.d.ts +39 -0
  45. package/dist/types/gl/IK.d.ts +53 -0
  46. package/dist/types/gl/InstancedMesh.d.ts +46 -0
  47. package/dist/types/gl/Light.d.ts +73 -0
  48. package/dist/types/gl/Lightmap.d.ts +85 -0
  49. package/dist/types/gl/Locomotion.d.ts +279 -0
  50. package/dist/types/gl/Material.d.ts +100 -0
  51. package/dist/types/gl/Mesh.d.ts +33 -0
  52. package/dist/types/gl/Model.d.ts +53 -0
  53. package/dist/types/gl/NavAgent.d.ts +99 -0
  54. package/dist/types/gl/NavMesh.d.ts +169 -0
  55. package/dist/types/gl/Node.d.ts +99 -0
  56. package/dist/types/gl/Noise.d.ts +20 -0
  57. package/dist/types/gl/Particles.d.ts +277 -0
  58. package/dist/types/gl/Physics.d.ts +91 -0
  59. package/dist/types/gl/Plane.d.ts +19 -0
  60. package/dist/types/gl/Ragdoll.d.ts +86 -0
  61. package/dist/types/gl/Ray.d.ts +8 -0
  62. package/dist/types/gl/Scene.d.ts +221 -0
  63. package/dist/types/gl/Shape.d.ts +122 -0
  64. package/dist/types/gl/Terrain.d.ts +280 -0
  65. package/dist/types/gl/Texture.d.ts +27 -0
  66. package/dist/types/gl/Trigger.d.ts +10 -0
  67. package/dist/types/gl/Vehicle.d.ts +191 -0
  68. package/dist/types/gl/Wheel.d.ts +95 -0
  69. package/dist/types/gl/animation/AnimationClip.d.ts +60 -0
  70. package/dist/types/gl/animation/Animator.d.ts +219 -0
  71. package/dist/types/gl/animation/Layer.d.ts +31 -0
  72. package/dist/types/gl/animation/Loop.d.ts +17 -0
  73. package/dist/types/gl/animation/Playback.d.ts +36 -0
  74. package/dist/types/gl/animation/core.d.ts +367 -0
  75. package/dist/types/gl/controls.d.ts +19 -0
  76. package/dist/types/gl/physicsEvents.d.ts +1 -0
  77. package/dist/types/gl/scenarios.d.ts +98 -0
  78. package/dist/types/gl/state.d.ts +5 -0
  79. package/dist/types/gl/terrainMesh.d.ts +46 -0
  80. package/dist/types/gl/touch.d.ts +2 -0
  81. package/dist/types/inject.d.ts +123 -0
  82. package/dist/types/math/Mathf.d.ts +39 -0
  83. package/dist/types/math/mat4.d.ts +66 -0
  84. package/dist/types/math/quat.d.ts +53 -0
  85. package/dist/types/math/vec.d.ts +129 -0
  86. package/dist/types/net/codec.d.ts +25 -0
  87. package/dist/types/net/core.d.ts +106 -0
  88. package/dist/types/net/index.d.ts +80 -0
  89. package/dist/types/net/replication.d.ts +101 -0
  90. package/dist/types/plugins/camera.d.ts +25 -0
  91. package/dist/types/plugins/geolocation.d.ts +39 -0
  92. package/dist/types/plugins/oauth.d.ts +25 -0
  93. package/dist/types/plugins/permission.d.ts +1 -0
  94. package/dist/types/plugins/push.d.ts +46 -0
  95. package/dist/types/plugins/qr.d.ts +21 -0
  96. package/dist/types/plugins/service.d.ts +33 -0
  97. package/dist/types/runtime/app.d.ts +67 -0
  98. package/dist/types/runtime/appEvents.d.ts +7 -0
  99. package/dist/types/runtime/channel.d.ts +1 -0
  100. package/dist/types/runtime/clipboard.d.ts +7 -0
  101. package/dist/types/runtime/datetime.d.ts +80 -0
  102. package/dist/types/runtime/device.d.ts +126 -0
  103. package/dist/types/runtime/fetch.d.ts +31 -0
  104. package/dist/types/runtime/files.d.ts +33 -0
  105. package/dist/types/runtime/input.d.ts +121 -0
  106. package/dist/types/runtime/media.d.ts +46 -0
  107. package/dist/types/runtime/misc.d.ts +11 -0
  108. package/dist/types/runtime/net.d.ts +15 -0
  109. package/dist/types/runtime/rpc.d.ts +44 -0
  110. package/dist/types/runtime/service.d.ts +1 -0
  111. package/dist/types/runtime/share.d.ts +2 -0
  112. package/dist/types/runtime/storage.d.ts +5 -0
  113. package/dist/types/runtime/touch.d.ts +56 -0
  114. package/dist/types/scene/defineScene.d.ts +296 -0
  115. package/dist/types/scene/editorPlugins.d.ts +82 -0
  116. package/dist/types/scene/gizmos.d.ts +56 -0
  117. package/dist/types/scene/grammar.d.ts +56 -0
  118. package/dist/types/scene/level.d.ts +98 -0
  119. package/dist/types/scene/material.d.ts +76 -0
  120. package/dist/types/server/auth/appConfig.d.ts +16 -0
  121. package/dist/types/server/auth/global.d.ts +56 -0
  122. package/dist/types/server/auth/models.d.ts +164 -0
  123. package/dist/types/server/auth/types.d.ts +56 -0
  124. package/dist/types/server/channel.d.ts +28 -0
  125. package/dist/types/server/context.d.ts +27 -0
  126. package/dist/types/server/db/defineDb.d.ts +36 -0
  127. package/dist/types/server/db/fields.d.ts +146 -0
  128. package/dist/types/server/db/index.d.ts +6 -0
  129. package/dist/types/server/db/marci/query.d.ts +326 -0
  130. package/dist/types/server/db/types.d.ts +163 -0
  131. package/dist/types/server/errors.d.ts +8 -0
  132. package/dist/types/server/inject.d.ts +5 -0
  133. package/dist/types/ui/NativeView.d.ts +38 -0
  134. package/dist/types/ui/UI.d.ts +27 -0
  135. package/dist/types/ui/UIBottomSheet.d.ts +71 -0
  136. package/dist/types/ui/UIButton.d.ts +30 -0
  137. package/dist/types/ui/UIContainer.d.ts +33 -0
  138. package/dist/types/ui/UIImage.d.ts +35 -0
  139. package/dist/types/ui/UIInput.d.ts +110 -0
  140. package/dist/types/ui/UIModal.d.ts +70 -0
  141. package/dist/types/ui/UINode.d.ts +363 -0
  142. package/dist/types/ui/UIPager.d.ts +123 -0
  143. package/dist/types/ui/UIPopover.d.ts +39 -0
  144. package/dist/types/ui/UIScreen.d.ts +57 -0
  145. package/dist/types/ui/UIScrollable.d.ts +40 -0
  146. package/dist/types/ui/UISpacer.d.ts +9 -0
  147. package/dist/types/ui/UITabs.d.ts +96 -0
  148. package/dist/types/ui/UIText.d.ts +19 -0
  149. package/dist/types/ui/UIVideo.d.ts +28 -0
  150. package/dist/types/ui/UIVirtualizedList.d.ts +97 -0
  151. package/dist/types/ui/UIWidget.d.ts +65 -0
  152. package/dist/types/ui/fonts.d.ts +9 -0
  153. package/dist/types/ui/presentable.d.ts +68 -0
  154. package/dist/types/ui/router.d.ts +40 -0
  155. package/dist/types/ui/theme.d.ts +26 -0
  156. package/dist/types.json +1 -0
  157. package/package.json +46 -0
  158. package/prompts/2d.md +395 -0
  159. package/prompts/3d-scene-files.md +113 -0
  160. package/prompts/3d-scene.md +119 -0
  161. package/prompts/3d.md +303 -0
  162. package/prompts/README.md +142 -0
  163. package/prompts/ar.md +109 -0
  164. package/prompts/canvas.md +27 -0
  165. package/prompts/compose.ts +46 -0
  166. package/prompts/concept.md +49 -0
  167. package/prompts/core-design.md +75 -0
  168. package/prompts/core.md +264 -0
  169. package/prompts/design.md +218 -0
  170. package/prompts/dist/2d-game.md +1530 -0
  171. package/prompts/dist/3d-app.md +1672 -0
  172. package/prompts/dist/ar-app.md +1548 -0
  173. package/prompts/dist/concept.md +49 -0
  174. package/prompts/dist/design.md +498 -0
  175. package/prompts/dist/ui-app.md +1134 -0
  176. package/prompts/index.md +37 -0
  177. package/prompts/intake-prompt.md +33 -0
  178. package/prompts/namer-prompt.md +12 -0
  179. package/prompts/router-prompt.md +50 -0
  180. package/prompts/select.ts +111 -0
  181. package/prompts/ui-design.md +203 -0
  182. package/prompts/ui.md +803 -0
  183. package/src/animate/animate.ts +238 -0
  184. package/src/animate/bezier.ts +138 -0
  185. package/src/animate/easings.ts +126 -0
  186. package/src/bridges.d.ts +1345 -0
  187. package/src/canvas/Canvas.ts +305 -0
  188. package/src/compile/__tests__/assetIconMacro.test.ts +219 -0
  189. package/src/compile/__tests__/assetMacro.test.ts +74 -0
  190. package/src/compile/__tests__/assetName.test.ts +55 -0
  191. package/src/compile/__tests__/compile.test.ts +287 -0
  192. package/src/compile/__tests__/detectEntry.test.ts +132 -0
  193. package/src/compile/__tests__/fontMacro.test.ts +199 -0
  194. package/src/compile/aspectMacro.ts +86 -0
  195. package/src/compile/assetIconMacro.ts +384 -0
  196. package/src/compile/assetMacro.ts +146 -0
  197. package/src/compile/assetName.ts +50 -0
  198. package/src/compile/bundler.ts +287 -0
  199. package/src/compile/compileProject.ts +127 -0
  200. package/src/compile/detectEntry.ts +128 -0
  201. package/src/compile/fontMacro.ts +459 -0
  202. package/src/compile/fontRegistry.ts +78 -0
  203. package/src/compile/header.ts +67 -0
  204. package/src/compile/index.ts +100 -0
  205. package/src/compile/libraryImports.ts +52 -0
  206. package/src/compile/liteMaterial.ts +247 -0
  207. package/src/compile/sceneEditor.ts +88 -0
  208. package/src/compile/serverSplit.ts +233 -0
  209. package/src/compile/serverTypes.ts +227 -0
  210. package/src/compile/sfnt.ts +98 -0
  211. package/src/compile/shaderSchema.ts +202 -0
  212. package/src/compile/shaderTargets.ts +77 -0
  213. package/src/compile/sourcemap.ts +25 -0
  214. package/src/core/Aspect.ts +568 -0
  215. package/src/core/InspectorUI.ts +212 -0
  216. package/src/core/StateMachine.ts +308 -0
  217. package/src/core/__tests__/stateMachine.test.ts +132 -0
  218. package/src/core/color.ts +66 -0
  219. package/src/core/compWrite.ts +42 -0
  220. package/src/core/events.ts +38 -0
  221. package/src/core/fields.ts +120 -0
  222. package/src/core/registry.ts +23 -0
  223. package/src/core/signals.ts +277 -0
  224. package/src/core/time.ts +84 -0
  225. package/src/g2/Camera2D.ts +40 -0
  226. package/src/g2/CharacterController2D.ts +276 -0
  227. package/src/g2/Node2D.ts +267 -0
  228. package/src/g2/OneWay2D.ts +66 -0
  229. package/src/g2/Physics2D.ts +346 -0
  230. package/src/g2/Scene2D.ts +209 -0
  231. package/src/g2/Shape2D.ts +259 -0
  232. package/src/g2/Sprite.ts +89 -0
  233. package/src/g2/SpriteAnimation.ts +171 -0
  234. package/src/g2/SpriteSheet.ts +166 -0
  235. package/src/g2/Texture2D.ts +47 -0
  236. package/src/g2/Tilemap.ts +41 -0
  237. package/src/g2/Tileset.ts +71 -0
  238. package/src/g2/Trigger2D.ts +77 -0
  239. package/src/g2/autotile.ts +433 -0
  240. package/src/g2/cells.ts +91 -0
  241. package/src/g2/defineScene2d.ts +381 -0
  242. package/src/g2/groups2d.ts +106 -0
  243. package/src/g2/loop.ts +50 -0
  244. package/src/g2/scenarios2d.ts +69 -0
  245. package/src/g2/touch.ts +83 -0
  246. package/src/gl/Camera.ts +160 -0
  247. package/src/gl/CameraPlace.ts +52 -0
  248. package/src/gl/CharacterController.ts +238 -0
  249. package/src/gl/Gearbox.ts +212 -0
  250. package/src/gl/Geometry.ts +279 -0
  251. package/src/gl/IK.ts +193 -0
  252. package/src/gl/InstancedMesh.ts +132 -0
  253. package/src/gl/Light.ts +135 -0
  254. package/src/gl/Lightmap.ts +402 -0
  255. package/src/gl/Locomotion.ts +498 -0
  256. package/src/gl/Material.ts +279 -0
  257. package/src/gl/Mesh.ts +83 -0
  258. package/src/gl/Model.ts +124 -0
  259. package/src/gl/NavAgent.ts +337 -0
  260. package/src/gl/NavMesh.ts +397 -0
  261. package/src/gl/Node.ts +350 -0
  262. package/src/gl/Noise.ts +30 -0
  263. package/src/gl/Particles.ts +676 -0
  264. package/src/gl/Physics.ts +222 -0
  265. package/src/gl/Plane.ts +53 -0
  266. package/src/gl/Ragdoll.ts +270 -0
  267. package/src/gl/Ray.ts +16 -0
  268. package/src/gl/Scene.ts +505 -0
  269. package/src/gl/Shape.ts +409 -0
  270. package/src/gl/Terrain.ts +1092 -0
  271. package/src/gl/Texture.ts +63 -0
  272. package/src/gl/Trigger.ts +45 -0
  273. package/src/gl/Vehicle.ts +473 -0
  274. package/src/gl/Wheel.ts +240 -0
  275. package/src/gl/animation/AnimationClip.ts +204 -0
  276. package/src/gl/animation/Animator.ts +329 -0
  277. package/src/gl/animation/Layer.ts +39 -0
  278. package/src/gl/animation/Loop.ts +31 -0
  279. package/src/gl/animation/Playback.ts +52 -0
  280. package/src/gl/animation/core.ts +670 -0
  281. package/src/gl/controls.ts +95 -0
  282. package/src/gl/physicsEvents.ts +20 -0
  283. package/src/gl/scenarios.ts +291 -0
  284. package/src/gl/state.ts +6 -0
  285. package/src/gl/terrainMesh.ts +219 -0
  286. package/src/gl/touch.ts +68 -0
  287. package/src/host.d.ts +61 -0
  288. package/src/inject.ts +203 -0
  289. package/src/math/Mathf.ts +118 -0
  290. package/src/math/mat4.ts +278 -0
  291. package/src/math/quat.ts +232 -0
  292. package/src/math/vec.ts +255 -0
  293. package/src/net/codec.ts +119 -0
  294. package/src/net/core.ts +384 -0
  295. package/src/net/index.ts +181 -0
  296. package/src/net/replication.ts +622 -0
  297. package/src/plugins/camera.ts +81 -0
  298. package/src/plugins/geolocation.ts +123 -0
  299. package/src/plugins/oauth.ts +61 -0
  300. package/src/plugins/permission.ts +7 -0
  301. package/src/plugins/push.ts +132 -0
  302. package/src/plugins/qr.ts +73 -0
  303. package/src/plugins/service.ts +47 -0
  304. package/src/runtime/app.ts +101 -0
  305. package/src/runtime/appEvents.ts +54 -0
  306. package/src/runtime/channel.ts +61 -0
  307. package/src/runtime/clipboard.ts +20 -0
  308. package/src/runtime/datetime.ts +329 -0
  309. package/src/runtime/device.ts +293 -0
  310. package/src/runtime/fetch.ts +77 -0
  311. package/src/runtime/files.ts +108 -0
  312. package/src/runtime/input.ts +175 -0
  313. package/src/runtime/media.ts +111 -0
  314. package/src/runtime/misc.ts +16 -0
  315. package/src/runtime/net.ts +36 -0
  316. package/src/runtime/rpc.ts +218 -0
  317. package/src/runtime/service.ts +83 -0
  318. package/src/runtime/share.ts +9 -0
  319. package/src/runtime/storage.ts +13 -0
  320. package/src/runtime/touch.ts +76 -0
  321. package/src/scene/defineScene.ts +1396 -0
  322. package/src/scene/editorPlugins.ts +110 -0
  323. package/src/scene/gizmos.ts +148 -0
  324. package/src/scene/grammar.ts +120 -0
  325. package/src/scene/level.ts +296 -0
  326. package/src/scene/material.ts +188 -0
  327. package/src/server/auth/appConfig.ts +12 -0
  328. package/src/server/auth/global.ts +80 -0
  329. package/src/server/auth/host.ts +318 -0
  330. package/src/server/auth/models.ts +83 -0
  331. package/src/server/auth/types.ts +50 -0
  332. package/src/server/channel.ts +56 -0
  333. package/src/server/context.ts +36 -0
  334. package/src/server/db/defineDb.ts +237 -0
  335. package/src/server/db/fields.ts +132 -0
  336. package/src/server/db/httpTransport.ts +93 -0
  337. package/src/server/db/index.ts +7 -0
  338. package/src/server/db/marci/query.ts +412 -0
  339. package/src/server/db/types.ts +202 -0
  340. package/src/server/errors.ts +12 -0
  341. package/src/server/host.ts +74 -0
  342. package/src/server/inject.ts +13 -0
  343. package/src/server/runtime.ts +133 -0
  344. package/src/server/validate.ts +87 -0
  345. package/src/ui/NativeView.ts +142 -0
  346. package/src/ui/UI.ts +39 -0
  347. package/src/ui/UIBottomSheet.ts +139 -0
  348. package/src/ui/UIButton.ts +101 -0
  349. package/src/ui/UIContainer.ts +60 -0
  350. package/src/ui/UIImage.ts +83 -0
  351. package/src/ui/UIInput.ts +185 -0
  352. package/src/ui/UIModal.ts +139 -0
  353. package/src/ui/UINode.ts +830 -0
  354. package/src/ui/UIPager.ts +362 -0
  355. package/src/ui/UIPopover.ts +100 -0
  356. package/src/ui/UIScreen.ts +123 -0
  357. package/src/ui/UIScrollable.ts +87 -0
  358. package/src/ui/UISpacer.ts +14 -0
  359. package/src/ui/UITabs.ts +236 -0
  360. package/src/ui/UIText.ts +51 -0
  361. package/src/ui/UIVideo.ts +88 -0
  362. package/src/ui/UIVirtualizedList.ts +241 -0
  363. package/src/ui/UIWidget.ts +127 -0
  364. package/src/ui/fonts.ts +13 -0
  365. package/src/ui/presentable.ts +117 -0
  366. package/src/ui/router.ts +132 -0
  367. package/src/ui/theme.ts +84 -0
@@ -0,0 +1,287 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
2
+ import { spawnSync } from "node:child_process"
3
+ import { createRequire } from "node:module"
4
+ import { dirname, join, relative } from "node:path"
5
+ import { fileURLToPath } from "node:url"
6
+ import { collectAssetMacros, desugarAssetMacros, hasAssetMacro, type AssetMacroRef } from "./assetMacro"
7
+ import type { ScanModule } from "./detectEntry"
8
+ import { hasAspectExtends, injectAspectNames } from "./aspectMacro"
9
+ import { hasImportStatement, rewriteLibraryImports } from "./libraryImports"
10
+
11
+ /*
12
+ * The user-project bundler. Project files live in an in-memory map; the whole SDK is delivered via
13
+ * chisel's `inject` so user code uses globals (Scene, Sprite, toast, …) with NO imports. chisel — the
14
+ * Rust method-granular bundler (@letary/chisel) — tree-shakes the injected SDK down to what the
15
+ * project references, including unused class *methods*. It is the only bundler (the esbuild fallback
16
+ * is gone). Bare imports are a hard error (chisel's resolver enforces it).
17
+ */
18
+
19
+ export type MemoryFiles = Record<string, string>
20
+
21
+ /** Locate the SDK inject entry: an explicit override (LECODES_SDK_INJECT — used by the published CLI,
22
+ * which vendors the SDK source), else the source in-repo. chisel needs the SOURCE tree (not a bundle). */
23
+ const resolveInject = (): string => {
24
+ const override = process.env.LECODES_SDK_INJECT
25
+ if (override && existsSync(override)) return override
26
+ const here = dirname(fileURLToPath(import.meta.url)) // …/sdk/src/compile — portable (Node + Bun)
27
+ const src = join(here, "..", "inject.ts")
28
+ if (existsSync(src)) return src
29
+ throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)")
30
+ }
31
+
32
+ const normalize = (path: string): string => {
33
+ const parts: string[] = []
34
+ for (const part of path.split("/")) {
35
+ if (!part || part === ".") continue
36
+ if (part === "..") parts.pop()
37
+ else parts.push(part)
38
+ }
39
+ return "/" + parts.join("/")
40
+ }
41
+
42
+ // ---- chisel (Rust) bundler -------------------------------------------------------------------
43
+ //
44
+ // chisel is fed an in-memory file map over JSON-stdio: the SDK source under `/__sdk/…` (injected via
45
+ // `/__sdk/inject.ts`), the project files at their own paths, and a `define` map. It does
46
+ // method-granular tree-shaking + (when minify is on) compress + mangle. Assets/shaders/svg reach it
47
+ // as `asset()` macros desugared to default imports; aspect names are injected as `static aspect`.
48
+
49
+ // `EDITOR` is a boolean define: `false` (const-folded, `if (EDITOR)` branches DCE'd away) in
50
+ // every shipped bundle, `true` only in scene-editor compiles (CompileOptions.editor). Requires a
51
+ // chisel with boolean-define support — an older binary ignores the entry and leaves `EDITOR` as a
52
+ // free identifier (a runtime ReferenceError only in code that actually uses it).
53
+ const CHISEL_DEFINE: Record<string, string> = { RAD2DEG: "57.29577951308232", DEG2RAD: "0.017453292519943295" }
54
+
55
+ // Methods the native host invokes by name have no in-bundle caller, so method-DCE can't see them.
56
+ // The SDK prefixes its framework / host-facing methods with `_` (`_emitTouchStart`, `_emitClick`,
57
+ // `_onSlotEvent`, `_emitCollision`, …), so keep every underscore-prefixed method on classes that survive.
58
+ const CHISEL_KEEP = [ "_*" ]
59
+
60
+ /** The two inject modules (paths inside chisel's file map, see collectSdkFiles): the client SDK
61
+ * (every UI/2D/3D global) and the server SDK (`t`, `model`, `defineDb`, `channel`, … — see
62
+ * src/server/inject.ts). A bundle gets exactly one of them. */
63
+ export const CLIENT_INJECT = "/__sdk/inject.ts"
64
+ export const SERVER_INJECT = "/__sdk/server/inject.ts"
65
+
66
+ /** The SDK source root (parent of `inject.ts`). chisel needs the source tree, not a prebuilt bundle. */
67
+ const resolveSdkSrc = (): string => {
68
+ const inject = resolveInject()
69
+ if (!inject.endsWith("inject.ts")) {
70
+ throw new Error(`chisel needs the SDK source tree (src/inject.ts); got ${inject}. Set LECODES_SDK_INJECT to the source.`)
71
+ }
72
+ return dirname(inject)
73
+ }
74
+
75
+ const isSdkSource = (path: string) =>
76
+ /\.[tj]s$/.test(path) && !path.endsWith(".d.ts") &&
77
+ !path.includes("/compile/") && !path.includes("/tests/") && !path.includes("/__tests__/")
78
+
79
+ // The SDK tree doesn't change within a process — read it once.
80
+ const sdkFilesCache = new Map<string, MemoryFiles>()
81
+ const collectSdkFiles = (sdkSrc: string): MemoryFiles => {
82
+ const cached = sdkFilesCache.get(sdkSrc)
83
+ if (cached) return cached
84
+ const out: MemoryFiles = {}
85
+ const walk = (dir: string) => {
86
+ for (const name of readdirSync(dir)) {
87
+ const p = join(dir, name)
88
+ if (statSync(p).isDirectory()) walk(p)
89
+ else if (isSdkSource(p)) out["/__sdk/" + relative(sdkSrc, p).split("\\").join("/")] = readFileSync(p, "utf8")
90
+ }
91
+ }
92
+ walk(sdkSrc)
93
+ sdkFilesCache.set(sdkSrc, out)
94
+ return out
95
+ }
96
+
97
+ /** Locate the chisel binary: an explicit override (LECODES_CHISEL_BIN), else the prebuilt binary from
98
+ * the @letary/chisel npm package (which resolves the platform via optional @letary/chisel-<platform>). */
99
+ const resolveChiselBin = (): string => {
100
+ const override = process.env.LECODES_CHISEL_BIN
101
+ if (override) {
102
+ if (existsSync(override)) return override
103
+ throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`)
104
+ }
105
+ try {
106
+ const bin = createRequire(import.meta.url)("@letary/chisel").binaryPath() as string | null
107
+ if (bin && existsSync(bin)) return bin
108
+ } catch { /* @letary/chisel (or its platform binary) not installed */ }
109
+ throw new Error("chisel binary not found. Install @letary/chisel, or set LECODES_CHISEL_BIN.")
110
+ }
111
+
112
+ /** Parse every module in `files` via chisel scan (no bundling): resolved local imports +
113
+ * hasExports per file. Null when the binary is missing or predates scan mode (it then rejects
114
+ * the entry-less input) — detectEntry then falls back to its regex scanner. Passed into
115
+ * `detectEntry` as its `scanner` by every node caller (detectEntry itself stays browser-safe). */
116
+ export const scanModulesChisel = (files: MemoryFiles): Record<string, ScanModule> | null => {
117
+ let bin: string
118
+ try {
119
+ bin = resolveChiselBin()
120
+ } catch {
121
+ return null
122
+ }
123
+ const input = JSON.stringify({ scan: true, files })
124
+ const r = spawnSync(bin, [], { input, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 })
125
+ if (r.status !== 0 || r.error) return null
126
+ try {
127
+ const parsed = JSON.parse(r.stdout || "{}") as { scan?: Record<string, ScanModule> }
128
+ return parsed.scan ?? null
129
+ } catch {
130
+ return null
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Every `asset('…')` must name a file that's actually in the compile input — a missing one is a
136
+ * COMPILE ERROR, never a fallback.
137
+ *
138
+ * chisel used to turn an asset import with no backing module into `const x = "<the specifier>"`:
139
+ * a relative source path nothing resolves at runtime, so the app got a silently broken reference
140
+ * (and, since the desugared imports are appended, a const declared after its own use — a TDZ
141
+ * ReferenceError at boot that read like a bundler crash). Reporting the call site here instead
142
+ * names the real problem: the file isn't there.
143
+ */
144
+ const assertAssetsExist = (refs: { file: string, ref: AssetMacroRef }[], files: MemoryFiles) => {
145
+ const missing: string[] = []
146
+ for (const { file, ref } of refs) {
147
+ // Bare specifiers aren't paths at all — chisel reports those as bare imports.
148
+ if (!ref.path.startsWith("./") && !ref.path.startsWith("../") && !ref.path.startsWith("/")) continue
149
+ const dir = file.slice(0, file.lastIndexOf("/"))
150
+ const base = ref.path.startsWith("/") ? normalize(ref.path) : normalize(`${dir}/${ref.path}`)
151
+ // Same candidate list chisel's resolver uses, so "found here" means "found there".
152
+ const found = [ base, `${base}.ts`, `${base}.js`, `${base}/index.ts` ].some(c => files[c] !== undefined)
153
+ if (!found) missing.push(`asset not found: ${ref.path} (${file.replace(/^\//, "")}:${ref.line})`)
154
+ }
155
+ if (missing.length > 0) throw new Error(missing.join("\n"))
156
+ }
157
+
158
+ const bundleProjectChisel = async (
159
+ entrypoint: string,
160
+ files: MemoryFiles,
161
+ format: "esm" | "iife",
162
+ sourcemap: boolean,
163
+ minifyOverride?: boolean,
164
+ editor?: boolean,
165
+ libraries?: Record<string, MemoryFiles>,
166
+ inject: string = CLIENT_INJECT,
167
+ ): Promise<{ code: string, map: string | null, diagnostics: string[] }> => {
168
+ const entry = normalize(entrypoint)
169
+ const slugs = new Set(Object.keys(libraries ?? {}))
170
+ // Per-file source transforms (project files only — SDK aspects declare their own `static aspect`):
171
+ // - rewrite bare library imports (`from "acme-ui"`) → the injected `/__lib/<slug>/…` tree
172
+ // - desugar `asset('./x')` → a default import that resolves to the synthetic asset module
173
+ // - inject `static aspect = '<name>'` from `class … extends Aspect<'<name>', …>`
174
+ const projFiles: MemoryFiles = {}
175
+ const assetRefs: { file: string, ref: AssetMacroRef }[] = []
176
+ for (const [ k, v ] of Object.entries(files)) {
177
+ const path = normalize(k)
178
+ let src = v
179
+ if (/\.[tj]sx?$/.test(path)) {
180
+ if (hasImportStatement(src)) src = rewriteLibraryImports(src, slugs)
181
+ if (hasAssetMacro(src)) {
182
+ const found = collectAssetMacros(src, path)
183
+ for (const ref of found) assetRefs.push({ file: path, ref })
184
+ src = desugarAssetMacros(src, path, found)
185
+ }
186
+ if (hasAspectExtends(src)) src = injectAspectNames(src)
187
+ }
188
+ projFiles[path] = src
189
+ }
190
+ if (!projFiles[entry]) throw new Error(`Entrypoint not found: ${entry}`)
191
+
192
+ // Library dependencies: merge each library's files under /__lib/<slug>/… (like /__sdk/…). They
193
+ // get the same asset/aspect desugaring as project code, but NOT the bare-import rewrite — a
194
+ // library can't declare dependencies (flat graph v1), so any bare import in a library is an
195
+ // error chisel surfaces. Library-internal relative imports resolve within the prefix untouched.
196
+ for (const [ slug, libFiles ] of Object.entries(libraries ?? {})) {
197
+ for (const [ k, v ] of Object.entries(libFiles)) {
198
+ const path = normalize(`/__lib/${slug}/${k}`)
199
+ let src = v
200
+ if (/\.[tj]sx?$/.test(path)) {
201
+ if (hasAssetMacro(src)) {
202
+ const found = collectAssetMacros(src, path)
203
+ for (const ref of found) assetRefs.push({ file: path, ref })
204
+ src = desugarAssetMacros(src, path, found)
205
+ }
206
+ if (hasAspectExtends(src)) src = injectAspectNames(src)
207
+ }
208
+ projFiles[path] = src
209
+ }
210
+ }
211
+
212
+ assertAssetsExist(assetRefs, projFiles)
213
+
214
+ const sdkFiles = collectSdkFiles(resolveSdkSrc())
215
+ // Shipped bundles minify by default; edit-mode bundles pass `false` — mangling erases class
216
+ // names, which the scene editor matches aspect schemas by (`describeAspect().className`).
217
+ const minify = minifyOverride ?? (process.env.LECODES_CHISEL_MINIFY !== "0")
218
+ const input = JSON.stringify({
219
+ files: { ...sdkFiles, ...projFiles },
220
+ entry,
221
+ inject: [ inject ],
222
+ format,
223
+ minify,
224
+ // Chain fusion, ON by default (`LECODES_CHISEL_FUSE=0` opts out): Vec3/date math chains lower to
225
+ // scalar arithmetic (bit-exact — JS numbers are f64 and the lowering never reassociates) and
226
+ // particle `curve()`/`colorCurve()` chains constant-fold into the exact native Float32Array.
227
+ fuse: process.env.LECODES_CHISEL_FUSE !== "0",
228
+ define: { ...CHISEL_DEFINE, EDITOR: editor ? "true" : "false" },
229
+ keep: CHISEL_KEEP,
230
+ sourcemap,
231
+ // Reactive-UI desugaring (memoized children .map → __uiMap + auto-wrapped signal reads in
232
+ // UIText/style args). Only fires on free references to the injected globals, i.e. user code.
233
+ // An older chisel binary ignores the unknown field — bundles still work, children just
234
+ // rebuild per change instead of reusing nodes.
235
+ reactive_ui: process.env.LECODES_CHISEL_REACTIVE_UI !== "0",
236
+ // Flatten literal array args of UI factory calls into variadic args (`UIColumn([a, b])` →
237
+ // `UIColumn(a, b)`) — the SDK's buildUI dispatch flattens array args one level anyway, so
238
+ // this only drops an array allocation per call site. Safe to always request: the sibling SDK
239
+ // source above has the variadic dispatch, and an older binary ignores the unknown field.
240
+ flatten_ui: process.env.LECODES_CHISEL_FLATTEN_UI !== "0",
241
+ })
242
+
243
+ const bin = resolveChiselBin()
244
+ const r = spawnSync(bin, [], { input, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 })
245
+ if (r.status === 2 || r.error) {
246
+ throw new Error(`chisel failed: ${r.error?.message ?? r.stderr ?? "unknown error"}`)
247
+ }
248
+ let parsed: { code?: string, map?: string, error?: string, diagnostics?: string[] }
249
+ try {
250
+ parsed = JSON.parse(r.stdout || "{}")
251
+ } catch {
252
+ throw new Error(`chisel produced invalid output: ${(r.stdout || r.stderr || "").slice(0, 200)}`)
253
+ }
254
+ if (parsed.error) throw new Error(`chisel: ${parsed.error}`)
255
+ if (parsed.code == null) throw new Error("chisel produced no code")
256
+ // Non-fatal build-time findings (`path:line:col: message`) — today: particle curve chains the
257
+ // engine would reject at set time. Warn so they're visible in the CLI / server log; the bundle
258
+ // itself is unaffected (chisel leaves an invalid chain as real calls).
259
+ const diagnostics = parsed.diagnostics ?? []
260
+ for (const d of diagnostics) console.warn(`warning: ${d}`)
261
+ return { code: parsed.code, map: parsed.map ?? null, diagnostics }
262
+ }
263
+
264
+ /**
265
+ * Bundle the project `files` (path → source) starting at `entrypoint`, with the SDK injected.
266
+ * `format` is "esm" by default (what the platform ships); "iife" produces a self-running script.
267
+ */
268
+ export const bundleProject = async (
269
+ entrypoint: string,
270
+ files: MemoryFiles,
271
+ format: "esm" | "iife" = "esm",
272
+ ): Promise<string> => (await bundleProjectChisel(entrypoint, files, format, false)).code
273
+
274
+ /** Like `bundleProject`, but also returns chisel's source map (and any build-time `diagnostics`).
275
+ * The map's `sources` are the original module paths and its generated positions are relative to
276
+ * `code` (line 1 = first line of `code`). Pass `sourcemap: false` to skip map generation entirely —
277
+ * `map` then comes back null. */
278
+ export const bundleProjectWithMap = async (
279
+ entrypoint: string,
280
+ files: MemoryFiles,
281
+ format: "esm" | "iife" = "esm",
282
+ minify?: boolean,
283
+ editor?: boolean,
284
+ libraries?: Record<string, MemoryFiles>,
285
+ sourcemap = true,
286
+ inject?: string,
287
+ ): Promise<{ code: string, map: string | null, diagnostics: string[] }> => bundleProjectChisel(entrypoint, files, format, sourcemap, minify, editor, libraries, inject)
@@ -0,0 +1,127 @@
1
+ // Turns a project's files into the esbuild input map + entrypoint + metadata header, then bundles.
2
+ // The one canonical "user project → deployable bundle" path: the backend and CLI both call this
3
+ // (the backend resolves asset URLs from the DB, the CLI from local metadata — same function, the
4
+ // URLs are passed in as `publicUrl` + per-entry `fileSrc`/`shaders`).
5
+
6
+ import { bundleProjectWithMap, scanModulesChisel, type MemoryFiles } from "./bundler"
7
+ import { detectEntry } from "./detectEntry"
8
+ import { buildHeader } from "./header"
9
+ import { inlineSourceMapComment, offsetSourceMap } from "./sourcemap"
10
+ import { stubServerEntries } from "./serverSplit"
11
+
12
+ export type CompileFileType = "text" | "resource" | "shader"
13
+
14
+ export type CompileEntry = {
15
+ path: string
16
+ type: CompileFileType
17
+ /** Source text for code / .svg files. */
18
+ text?: string
19
+ /** Server src for a binary resource (e.g. "/uploads/x.png"); prefixed with publicUrl. */
20
+ fileSrc?: string
21
+ /** Under `localAssets`: keep THIS resource in the URL form (publicUrl + fileSrc) anyway — for
22
+ * hosts that bundle most assets locally but stream heavy ones (e.g. videos) from the server. */
23
+ remote?: boolean
24
+ /** Compiled-shader artifacts (platform + src); the first drives the per-backend URL. */
25
+ shaders?: { platform: string, src: string }[]
26
+ /** `shaders` are LOCAL files staged next to the bundle (bare artifact names, not server srcs):
27
+ * the module compiles to a fetchLocal lookup instead of a URL. Set by the CLI's local shader
28
+ * compile for hosts with no asset server (the desktop host). */
29
+ shadersLocal?: boolean
30
+ }
31
+
32
+ export type CompileOptions = {
33
+ entries: CompileEntry[]
34
+ name: string
35
+ publicUrl: string
36
+ entryOverride?: string
37
+ /** Prepend the metadata header (default true). */
38
+ header?: boolean
39
+ /** Generated-icon URL for the header (backend only). */
40
+ icon?: string
41
+ /** `// fonts:` header payload (from the font() macro pass the caller ran over the entries). */
42
+ fontsHeader?: string
43
+ /** `// icons:` header payload (from the assetIcon() macro pass the caller ran over the entries). */
44
+ iconsHeader?: string
45
+ /** "esm" (default, shipped) or "iife" (self-running, for the local renderer). */
46
+ format?: "esm" | "iife"
47
+ /** Resolve `asset()` resources to a LOCAL host asset at runtime instead of a `publicUrl` URL: each
48
+ * resource compiles to `"id:" + _creatorUtils.fetchLocal(<name>)` (the host reads the file by name
49
+ * and returns its systemId). For hosts with no asset server (the desktop native host), where a URL
50
+ * can't be fetched. Off by default — the shipped/web build keeps the URL form. */
51
+ localAssets?: boolean
52
+ /** Override minification (default: on, `LECODES_CHISEL_MINIFY` env aside). Scene-editor bundles
53
+ * compile with `false`: mangling erases class names, which the editor matches aspect schemas by. */
54
+ minify?: boolean
55
+ /** Generate a source map (default true) and inline it into the bundle. `false` skips the map
56
+ * entirely — a smaller bundle, but runtime errors report generated positions only. */
57
+ sourcemap?: boolean
58
+ /** Editor compile: the `EDITOR` global compiles to `true` (scene-editor bundles). Default false —
59
+ * shipped bundles get `EDITOR` = `false`, const-folded so `if (EDITOR)` branches are DCE'd. */
60
+ editor?: boolean
61
+ /** Component-library dependencies: slug → the library's files (root-relative paths). Merged under
62
+ * `/__lib/<slug>/…` and reachable from project code via `import … from "<slug>"`. See
63
+ * docs/library-system-plan.md §4. */
64
+ libraries?: Record<string, MemoryFiles>
65
+ /** Base URL of the project's deployed server (draft or prod), baked into the RPC/channel stubs
66
+ * that replace every `*.server.ts` module in the client bundle (docs/backend-plan.md §4). Server
67
+ * files never reach the client bundle either way; with no URL, calling an endpoint fails with
68
+ * "server not deployed". */
69
+ serverUrl?: string
70
+ }
71
+
72
+ export const compileProject = async (opts: CompileOptions): Promise<string> => {
73
+ // Client bundle: every *.server.ts becomes its RPC/channel stub (server code stays server-side).
74
+ const entries = stubServerEntries(opts.entries, opts.serverUrl ?? "")
75
+ const entry = opts.entryOverride ?? detectEntry(entries, scanModulesChisel).entry
76
+ if (!entry) {
77
+ throw new Error("Couldn't find an entrypoint (no .ts/.js project file — name yours main.ts). Pass entryOverride.")
78
+ }
79
+
80
+ const files: Record<string, string> = {}
81
+ for (const e of entries) {
82
+ if (e.type === "resource") {
83
+ if (opts.localAssets && !e.remote) {
84
+ // The host resolves the asset by name at runtime (reads the local file → systemId); the
85
+ // "id:" prefix is the host's local-resource URL scheme (parsed by the UI + 2D texture paths).
86
+ const name = e.path.split(/[\\/]/).pop() ?? e.path
87
+ files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(name)}))`
88
+ } else {
89
+ files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`
90
+ }
91
+ } else if (e.type === "shader") {
92
+ const shaders = e.shaders ?? []
93
+ if (e.shadersLocal && shaders.length > 0) {
94
+ // Artifacts compiled locally and staged NEXT TO the bundle (no asset server): the host
95
+ // reads them by name through the same fetchLocal door the built-in materials use
96
+ // (`Material.lit` → fetchLocal("lit.filamat")), and Material.load parses the "id:" scheme.
97
+ // The name is built at RUNTIME because one bundle serves every native backend — the
98
+ // desktop host reports "desktop" on Windows and "opengl" on Linux, iOS "metal".
99
+ // Only entries the CLI actually compiled+staged carry `shadersLocal`; server artifacts
100
+ // (URLs) and un-staged ones keep the URL forms below.
101
+ const name = shaders[0].src.split(/[\\/]/).pop() ?? ""
102
+ const [prefix, suffix = ".filamat"] = name.split("_opengl")
103
+ files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(prefix + "_")} + _creator.backend + ${JSON.stringify(suffix)}))`
104
+ } else if (shaders.length > 0) {
105
+ files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``
106
+ } else {
107
+ files[e.path] = `export default ""` // local-only shader with no compiled artifact yet
108
+ }
109
+ } else if (e.path.toLowerCase().endsWith(".svg")) {
110
+ files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`
111
+ } else {
112
+ files[e.path] = e.text ?? ""
113
+ }
114
+ }
115
+
116
+ const { code, map } = await bundleProjectWithMap(
117
+ entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries, opts.sourcemap ?? true,
118
+ )
119
+ if (opts.header === false) {
120
+ return map ? `${code}\n${inlineSourceMapComment(map)}` : code
121
+ }
122
+ const header = buildHeader(opts.name, code, { icon: opts.icon, fonts: opts.fontsHeader, icons: opts.iconsHeader })
123
+ if (!map) return header + code
124
+ // Shift the map down past the header (prepended before the code); the inline comment lands last.
125
+ const headerLines = (header.match(/\n/g) ?? []).length
126
+ return `${header}${code}\n${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`
127
+ }
@@ -0,0 +1,128 @@
1
+ // Entrypoint detection from the project's import graph. The entry is the file no other project
2
+ // file imports (an import-graph root): a side-effect registration module is imported by the entry,
3
+ // so it stops being a candidate, and the entry itself may freely use `export`.
4
+ //
5
+ // Pick order among roots: `/main.ts` (the documented convention) → export-free roots by size →
6
+ // any root by size. `roots.length > 1` without a `/main.ts` means the choice was ambiguous —
7
+ // CLI callers surface that as a warning suggesting `main.ts` or `--entry`.
8
+ //
9
+ // This module is intentionally dependency-free (no node imports) so the FRONTEND can run the same
10
+ // detection live in the editor. Node callers inject chisel's `scan` mode (real parser — comments,
11
+ // strings, `import type` handled correctly) via the `scanner` parameter (`scanModulesChisel` from
12
+ // bundler.ts); without one — in the browser, or with a pre-scan chisel binary — the regex fallback
13
+ // below runs over the same resolution rules.
14
+
15
+ type Files = Record<string, string>
16
+
17
+ /** Per-module facts entry detection consumes (chisel `scan` output shape). */
18
+ export type ScanModule = { imports: string[], hasExports: boolean, error?: string }
19
+
20
+ /** A pluggable module scanner: path→source in, per-module facts out (null = unavailable). */
21
+ export type ModuleScanner = (files: Files) => Record<string, ScanModule> | null
22
+
23
+ export type DetectEntryInput = {
24
+ path: string
25
+ type?: string
26
+ text?: string | null
27
+ }
28
+
29
+ export type DetectedEntry = {
30
+ /** The chosen entry path ("" when the project has no candidate module at all). */
31
+ entry: string
32
+ /** Import-graph roots. More than one (without a `/main.ts` among them) = ambiguous choice. */
33
+ roots: string[]
34
+ }
35
+
36
+ /** The conventional entry filename — always wins when it is an import-graph root. */
37
+ export const ENTRY_CONVENTION = "/main.ts"
38
+
39
+ const isCode = (path: string) => /\.[tj]s$/.test(path) && !path.endsWith(".d.ts")
40
+ /** Never entries: editor plugins and server modules (docs/backend-plan.md — a `*.server.ts` nobody
41
+ * imports is still not the app; server files compile into their own bundle). */
42
+ const isNonEntry = (path: string) => path.endsWith(".editor.ts") || /\.server\.[tj]s$/.test(path)
43
+
44
+ const normalize = (path: string): string => {
45
+ const parts: string[] = []
46
+ for (const part of path.split("/")) {
47
+ if (!part || part === ".") continue
48
+ if (part === "..") parts.pop()
49
+ else parts.push(part)
50
+ }
51
+ return "/" + parts.join("/")
52
+ }
53
+
54
+ const dirOf = (path: string): string => {
55
+ const i = path.lastIndexOf("/")
56
+ return i <= 0 ? "/" : path.slice(0, i)
57
+ }
58
+
59
+ /** Mirror of chisel's resolver: relative/absolute specifiers only, `.ts`/`.js`/`/index.ts`
60
+ * fallback, exact match first. Null for bare imports (libraries) and unresolved paths. */
61
+ const resolveSpec = (files: Files, importer: string, spec: string): string | null => {
62
+ if (!spec.startsWith("./") && !spec.startsWith("../") && !spec.startsWith("/")) return null
63
+ const base = normalize(spec.startsWith("/") ? spec : dirOf(importer) + "/" + spec)
64
+ for (const cand of [base, base + ".ts", base + ".js", base + "/index.ts"]) {
65
+ if (cand in files) return cand
66
+ }
67
+ return null
68
+ }
69
+
70
+ // One pattern, two alternatives: `import|export … from "<spec>"` (multi-line safe — the specifier
71
+ // clause may sit on a later line) and a side-effect `import "<spec>"`.
72
+ const IMPORT_RE = /(?:^|[\n;])\s*(?:import|export)\b[^'"`]*?\bfrom\s*['"]([^'"\n]+)['"]|(?:^|[\n;])\s*import\s*['"]([^'"\n]+)['"]/g
73
+ const EXPORT_RE = /(?:^|[\n;])\s*export\b/
74
+
75
+ /** Regex fallback for a chisel binary without scan support. Same output shape; less precise
76
+ * (a specifier inside a comment counts, `export type` counts as an export) — both only affect
77
+ * which root wins a tie, never whether the true entry is a root. */
78
+ export const scanModulesFallback = (files: Files): Record<string, ScanModule> => {
79
+ const out: Record<string, ScanModule> = {}
80
+ for (const [path, text] of Object.entries(files)) {
81
+ const imports: string[] = []
82
+ for (const m of text.matchAll(IMPORT_RE)) {
83
+ const target = resolveSpec(files, path, m[1] ?? m[2] ?? "")
84
+ if (target && isCode(target) && !imports.includes(target)) imports.push(target)
85
+ }
86
+ out[path] = { imports, hasExports: EXPORT_RE.test(text) }
87
+ }
88
+ return out
89
+ }
90
+
91
+ /**
92
+ * Detect the project's entrypoint. `entries` is the full project file list; only `.ts`/`.js` text
93
+ * files participate (`.d.ts`, `.editor.ts` and `*.server.ts` are never entries, and editor-only imports don't
94
+ * remove a file from candidacy — production compiles don't bundle editor plugins).
95
+ */
96
+ export const detectEntry = (entries: DetectEntryInput[], scanner?: ModuleScanner): DetectedEntry => {
97
+ const files: Files = {}
98
+ for (const e of entries) {
99
+ if (e.type !== undefined && e.type !== "text") continue
100
+ if (typeof e.text !== "string") continue
101
+ const path = normalize(e.path.split("\\").join("/"))
102
+ if (isCode(path)) files[path] = e.text
103
+ }
104
+
105
+ const candidates = Object.keys(files).filter((p) => !isNonEntry(p))
106
+ if (candidates.length === 0) return { entry: "", roots: [] }
107
+
108
+ const scan = scanner?.(files) ?? scanModulesFallback(files)
109
+
110
+ const imported = new Set<string>()
111
+ for (const [path, mod] of Object.entries(scan)) {
112
+ if (path.endsWith(".editor.ts")) continue
113
+ for (const target of mod.imports) imported.add(target)
114
+ }
115
+
116
+ let roots = candidates.filter((p) => !imported.has(p))
117
+ // Everything imported (a cycle through every file) — degrade to all candidates.
118
+ if (roots.length === 0) roots = candidates
119
+
120
+ if (roots.includes(ENTRY_CONVENTION)) return { entry: ENTRY_CONVENTION, roots }
121
+
122
+ // Export-free roots are scripts — the old heuristic's signal, kept as the tie-breaker so
123
+ // existing projects resolve the same way; a lone exporting root is still a valid entry.
124
+ const scripts = roots.filter((p) => !scan[p]?.hasExports)
125
+ const pool = scripts.length > 0 ? scripts : roots
126
+ const bySize = [...pool].sort((a, b) => files[b].length - files[a].length)
127
+ return { entry: bySize[0], roots }
128
+ }