lecodes-cli 0.19.0 → 0.20.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 (270) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +1069 -396
  3. package/package.json +14 -13
  4. package/runtime/scene-harness.json +1 -1
  5. package/runtime/sdk-types.json +1 -1
  6. package/runtime/web/assets/Roboto-Variable-DHm9jpd-.woff2 +0 -0
  7. package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +1 -0
  8. package/runtime/web/assets/basis-C64VHDVD.js +1 -0
  9. package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  10. package/runtime/web/assets/createViewerLite-BvHobVhp.js +852 -0
  11. package/runtime/web/assets/draco-BiISTFcR.js +118 -0
  12. package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  13. package/runtime/web/assets/index-CfNCCBjN.js +2 -0
  14. package/runtime/web/assets/mapViewImpl-B2JcES8l.js +810 -0
  15. package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +8 -0
  16. package/runtime/web/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
  17. package/runtime/web/assets/worker-Caf-yYEI.js +2 -0
  18. package/runtime/web/embed.html +32 -0
  19. package/runtime/web/embed.js +171 -0
  20. package/src/api.ts +302 -0
  21. package/src/browserAuth.ts +87 -0
  22. package/src/cmgenTool.ts +104 -0
  23. package/src/commands/app.ts +896 -0
  24. package/src/commands/appAndroid.ts +603 -0
  25. package/src/commands/appDesktop.ts +339 -0
  26. package/src/commands/appDesktopMac.ts +470 -0
  27. package/src/commands/appIcon.ts +187 -0
  28. package/src/commands/appShared.ts +542 -0
  29. package/src/commands/appTemplates.ts +907 -0
  30. package/src/commands/appTemplatesAndroid.ts +599 -0
  31. package/src/commands/appTemplatesGradlew.ts +9 -0
  32. package/src/commands/assets.ts +28 -0
  33. package/src/commands/clone.ts +60 -0
  34. package/src/commands/compile.ts +253 -0
  35. package/src/commands/create.ts +59 -0
  36. package/src/commands/design.ts +863 -0
  37. package/src/commands/designTemplates.ts +11 -0
  38. package/src/commands/desktop.ts +127 -0
  39. package/src/commands/dev.ts +236 -0
  40. package/src/commands/diff.ts +59 -0
  41. package/src/commands/init.ts +191 -0
  42. package/src/commands/install.ts +198 -0
  43. package/src/commands/lightmap.ts +290 -0
  44. package/src/commands/link.ts +147 -0
  45. package/src/commands/login.ts +24 -0
  46. package/src/commands/navmesh.ts +225 -0
  47. package/src/commands/pn.ts +305 -0
  48. package/src/commands/projectTemplates.ts +130 -0
  49. package/src/commands/pull.ts +109 -0
  50. package/src/commands/push.ts +173 -0
  51. package/src/commands/render.ts +416 -0
  52. package/src/commands/scene.ts +148 -0
  53. package/src/commands/shaders.ts +157 -0
  54. package/src/commands/status.ts +29 -0
  55. package/src/commands/test.ts +371 -0
  56. package/src/commands/thumbs.ts +178 -0
  57. package/src/commands/types.ts +87 -0
  58. package/src/commands/update.ts +190 -0
  59. package/src/compile/assetIcons.ts +214 -0
  60. package/src/compile/collect.ts +46 -0
  61. package/src/compile/collectLocal.ts +37 -0
  62. package/src/compile/designCompile.ts +104 -0
  63. package/src/compile/fonts.ts +156 -0
  64. package/src/compile/headlessBundle.ts +129 -0
  65. package/src/compile/nativeStack.ts +21 -0
  66. package/src/compile/projectCompile.ts +109 -0
  67. package/src/compile/sceneCompile.ts +113 -0
  68. package/src/compile/screenEntry.ts +127 -0
  69. package/src/compile/shaders.ts +245 -0
  70. package/src/config.ts +42 -0
  71. package/src/designMeta.ts +35 -0
  72. package/src/desktopRenderer.ts +545 -0
  73. package/src/desktopScript.ts +276 -0
  74. package/src/dev/clientTemplates.ts +160 -0
  75. package/src/dev/devServer.ts +333 -0
  76. package/src/dev/webRunner.ts +22 -0
  77. package/src/dev/wsServer.ts +144 -0
  78. package/src/distRoot.ts +20 -0
  79. package/src/ignore.ts +163 -0
  80. package/src/index.ts +494 -0
  81. package/src/lecodes-3d-editor.d.ts +41 -0
  82. package/src/lecodes-assets.d.ts +7 -0
  83. package/src/lecodes-design.d.ts +191 -0
  84. package/src/lecodes-renderer.d.ts +131 -0
  85. package/src/localFiles.ts +144 -0
  86. package/src/manifest.ts +46 -0
  87. package/src/matcTool.ts +137 -0
  88. package/src/peers.ts +40 -0
  89. package/src/project.ts +61 -0
  90. package/src/projectEnv.ts +94 -0
  91. package/src/qrcode-terminal.d.ts +9 -0
  92. package/src/releases.ts +125 -0
  93. package/src/serverDiff.ts +78 -0
  94. package/src/textDiff.ts +103 -0
  95. package/src/types.ts +0 -0
  96. package/src/util.ts +146 -0
  97. package/runtime/sdk/animate/animate.ts +0 -238
  98. package/runtime/sdk/animate/bezier.ts +0 -138
  99. package/runtime/sdk/animate/easings.ts +0 -126
  100. package/runtime/sdk/canvas/Canvas.ts +0 -305
  101. package/runtime/sdk/compile/aspectMacro.ts +0 -86
  102. package/runtime/sdk/compile/assetIconMacro.ts +0 -384
  103. package/runtime/sdk/compile/assetMacro.ts +0 -146
  104. package/runtime/sdk/compile/assetName.ts +0 -50
  105. package/runtime/sdk/compile/bundler.ts +0 -287
  106. package/runtime/sdk/compile/compileProject.ts +0 -129
  107. package/runtime/sdk/compile/detectEntry.ts +0 -128
  108. package/runtime/sdk/compile/fontMacro.ts +0 -459
  109. package/runtime/sdk/compile/fontRegistry.ts +0 -78
  110. package/runtime/sdk/compile/header.ts +0 -67
  111. package/runtime/sdk/compile/index.ts +0 -100
  112. package/runtime/sdk/compile/libraryImports.ts +0 -52
  113. package/runtime/sdk/compile/liteMaterial.ts +0 -247
  114. package/runtime/sdk/compile/sceneEditor.ts +0 -88
  115. package/runtime/sdk/compile/serverSplit.ts +0 -233
  116. package/runtime/sdk/compile/serverTypes.ts +0 -227
  117. package/runtime/sdk/compile/sfnt.ts +0 -98
  118. package/runtime/sdk/compile/shaderSchema.ts +0 -202
  119. package/runtime/sdk/compile/shaderTargets.ts +0 -81
  120. package/runtime/sdk/compile/sourcemap.ts +0 -25
  121. package/runtime/sdk/core/Aspect.ts +0 -568
  122. package/runtime/sdk/core/InspectorUI.ts +0 -212
  123. package/runtime/sdk/core/StateMachine.ts +0 -308
  124. package/runtime/sdk/core/color.ts +0 -66
  125. package/runtime/sdk/core/compWrite.ts +0 -42
  126. package/runtime/sdk/core/events.ts +0 -38
  127. package/runtime/sdk/core/fields.ts +0 -120
  128. package/runtime/sdk/core/registry.ts +0 -23
  129. package/runtime/sdk/core/signals.ts +0 -277
  130. package/runtime/sdk/core/time.ts +0 -84
  131. package/runtime/sdk/g2/Camera2D.ts +0 -40
  132. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  133. package/runtime/sdk/g2/Node2D.ts +0 -267
  134. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  135. package/runtime/sdk/g2/Physics2D.ts +0 -346
  136. package/runtime/sdk/g2/Scene2D.ts +0 -209
  137. package/runtime/sdk/g2/Shape2D.ts +0 -259
  138. package/runtime/sdk/g2/Sprite.ts +0 -89
  139. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  140. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  141. package/runtime/sdk/g2/Texture2D.ts +0 -47
  142. package/runtime/sdk/g2/Tilemap.ts +0 -41
  143. package/runtime/sdk/g2/Tileset.ts +0 -71
  144. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  145. package/runtime/sdk/g2/autotile.ts +0 -433
  146. package/runtime/sdk/g2/cells.ts +0 -91
  147. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  148. package/runtime/sdk/g2/groups2d.ts +0 -106
  149. package/runtime/sdk/g2/loop.ts +0 -50
  150. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  151. package/runtime/sdk/g2/touch.ts +0 -83
  152. package/runtime/sdk/gl/Camera.ts +0 -160
  153. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  154. package/runtime/sdk/gl/CharacterController.ts +0 -238
  155. package/runtime/sdk/gl/Gearbox.ts +0 -212
  156. package/runtime/sdk/gl/Geometry.ts +0 -279
  157. package/runtime/sdk/gl/IK.ts +0 -193
  158. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  159. package/runtime/sdk/gl/Light.ts +0 -107
  160. package/runtime/sdk/gl/Lightmap.ts +0 -256
  161. package/runtime/sdk/gl/Material.ts +0 -260
  162. package/runtime/sdk/gl/Mesh.ts +0 -83
  163. package/runtime/sdk/gl/Model.ts +0 -124
  164. package/runtime/sdk/gl/NavAgent.ts +0 -337
  165. package/runtime/sdk/gl/NavMesh.ts +0 -397
  166. package/runtime/sdk/gl/Node.ts +0 -350
  167. package/runtime/sdk/gl/Noise.ts +0 -30
  168. package/runtime/sdk/gl/Particles.ts +0 -676
  169. package/runtime/sdk/gl/Physics.ts +0 -222
  170. package/runtime/sdk/gl/Plane.ts +0 -53
  171. package/runtime/sdk/gl/Ragdoll.ts +0 -270
  172. package/runtime/sdk/gl/Ray.ts +0 -16
  173. package/runtime/sdk/gl/Scene.ts +0 -486
  174. package/runtime/sdk/gl/Shape.ts +0 -409
  175. package/runtime/sdk/gl/Terrain.ts +0 -1092
  176. package/runtime/sdk/gl/Texture.ts +0 -63
  177. package/runtime/sdk/gl/Trigger.ts +0 -45
  178. package/runtime/sdk/gl/Vehicle.ts +0 -473
  179. package/runtime/sdk/gl/Wheel.ts +0 -240
  180. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  181. package/runtime/sdk/gl/animation/Animator.ts +0 -120
  182. package/runtime/sdk/gl/animation/Layer.ts +0 -31
  183. package/runtime/sdk/gl/animation/Loop.ts +0 -30
  184. package/runtime/sdk/gl/animation/Playback.ts +0 -44
  185. package/runtime/sdk/gl/animation/core.ts +0 -336
  186. package/runtime/sdk/gl/controls.ts +0 -95
  187. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  188. package/runtime/sdk/gl/scenarios.ts +0 -291
  189. package/runtime/sdk/gl/state.ts +0 -6
  190. package/runtime/sdk/gl/terrainMesh.ts +0 -219
  191. package/runtime/sdk/gl/touch.ts +0 -68
  192. package/runtime/sdk/inject.ts +0 -197
  193. package/runtime/sdk/math/Mathf.ts +0 -118
  194. package/runtime/sdk/math/mat4.ts +0 -278
  195. package/runtime/sdk/math/quat.ts +0 -232
  196. package/runtime/sdk/math/vec.ts +0 -255
  197. package/runtime/sdk/net/codec.ts +0 -119
  198. package/runtime/sdk/net/core.ts +0 -384
  199. package/runtime/sdk/net/index.ts +0 -181
  200. package/runtime/sdk/net/replication.ts +0 -622
  201. package/runtime/sdk/plugins/camera.ts +0 -81
  202. package/runtime/sdk/plugins/geolocation.ts +0 -123
  203. package/runtime/sdk/plugins/oauth.ts +0 -61
  204. package/runtime/sdk/plugins/permission.ts +0 -7
  205. package/runtime/sdk/plugins/push.ts +0 -132
  206. package/runtime/sdk/plugins/qr.ts +0 -73
  207. package/runtime/sdk/plugins/service.ts +0 -47
  208. package/runtime/sdk/runtime/app.ts +0 -101
  209. package/runtime/sdk/runtime/appEvents.ts +0 -54
  210. package/runtime/sdk/runtime/channel.ts +0 -61
  211. package/runtime/sdk/runtime/clipboard.ts +0 -20
  212. package/runtime/sdk/runtime/datetime.ts +0 -329
  213. package/runtime/sdk/runtime/device.ts +0 -293
  214. package/runtime/sdk/runtime/fetch.ts +0 -77
  215. package/runtime/sdk/runtime/files.ts +0 -23
  216. package/runtime/sdk/runtime/input.ts +0 -175
  217. package/runtime/sdk/runtime/media.ts +0 -111
  218. package/runtime/sdk/runtime/misc.ts +0 -16
  219. package/runtime/sdk/runtime/net.ts +0 -36
  220. package/runtime/sdk/runtime/rpc.ts +0 -218
  221. package/runtime/sdk/runtime/service.ts +0 -83
  222. package/runtime/sdk/runtime/share.ts +0 -9
  223. package/runtime/sdk/runtime/storage.ts +0 -13
  224. package/runtime/sdk/runtime/touch.ts +0 -76
  225. package/runtime/sdk/scene/defineScene.ts +0 -1353
  226. package/runtime/sdk/scene/editorPlugins.ts +0 -110
  227. package/runtime/sdk/scene/gizmos.ts +0 -148
  228. package/runtime/sdk/scene/grammar.ts +0 -120
  229. package/runtime/sdk/scene/material.ts +0 -188
  230. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  231. package/runtime/sdk/server/auth/global.ts +0 -80
  232. package/runtime/sdk/server/auth/host.ts +0 -318
  233. package/runtime/sdk/server/auth/models.ts +0 -83
  234. package/runtime/sdk/server/auth/types.ts +0 -50
  235. package/runtime/sdk/server/channel.ts +0 -56
  236. package/runtime/sdk/server/context.ts +0 -36
  237. package/runtime/sdk/server/db/defineDb.ts +0 -237
  238. package/runtime/sdk/server/db/fields.ts +0 -132
  239. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  240. package/runtime/sdk/server/db/index.ts +0 -7
  241. package/runtime/sdk/server/db/marci/query.ts +0 -412
  242. package/runtime/sdk/server/db/types.ts +0 -202
  243. package/runtime/sdk/server/errors.ts +0 -12
  244. package/runtime/sdk/server/host.ts +0 -74
  245. package/runtime/sdk/server/inject.ts +0 -13
  246. package/runtime/sdk/server/runtime.ts +0 -133
  247. package/runtime/sdk/server/validate.ts +0 -87
  248. package/runtime/sdk/ui/NativeView.ts +0 -142
  249. package/runtime/sdk/ui/UI.ts +0 -39
  250. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  251. package/runtime/sdk/ui/UIButton.ts +0 -101
  252. package/runtime/sdk/ui/UIContainer.ts +0 -60
  253. package/runtime/sdk/ui/UIImage.ts +0 -83
  254. package/runtime/sdk/ui/UIInput.ts +0 -185
  255. package/runtime/sdk/ui/UIModal.ts +0 -139
  256. package/runtime/sdk/ui/UINode.ts +0 -830
  257. package/runtime/sdk/ui/UIPager.ts +0 -362
  258. package/runtime/sdk/ui/UIPopover.ts +0 -100
  259. package/runtime/sdk/ui/UIScreen.ts +0 -123
  260. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  261. package/runtime/sdk/ui/UISpacer.ts +0 -14
  262. package/runtime/sdk/ui/UITabs.ts +0 -236
  263. package/runtime/sdk/ui/UIText.ts +0 -51
  264. package/runtime/sdk/ui/UIVideo.ts +0 -88
  265. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  266. package/runtime/sdk/ui/UIWidget.ts +0 -127
  267. package/runtime/sdk/ui/fonts.ts +0 -13
  268. package/runtime/sdk/ui/presentable.ts +0 -117
  269. package/runtime/sdk/ui/router.ts +0 -132
  270. package/runtime/sdk/ui/theme.ts +0 -84
@@ -0,0 +1,46 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { basename } from "node:path"
3
+ import type { Project } from "../api"
4
+ import type { Manifest } from "../manifest"
5
+ import { classifyKind, scanLocal } from "../localFiles"
6
+ import { loadIgnore } from "../ignore"
7
+ import { buildPaths } from "../project"
8
+ import type { CompileEntry } from "./projectCompile"
9
+
10
+ /*
11
+ * Builds the compile input from the working tree + the project's server metadata: code/.svg/scene
12
+ * content comes from local files (fresh); binary & shader URLs come from the server (matched by
13
+ * path). Local-only / locally-changed binaries and shaders are reported as warnings. Shared by
14
+ * `preview` and `render`. Untracked files matching .lecodesignore are skipped — same contract as
15
+ * `status`/`push` (tracked files are always in), so a generated `ios/` shell never leaks in here.
16
+ */
17
+ export const collectEntries = (
18
+ root: string, manifest: Manifest, project: Project,
19
+ opts: { /** Warn about resources with no server URL yet (default true; shell compiles bundle them instead). */ warnMissingUrl?: boolean } = {},
20
+ ): { entries: CompileEntry[], warnings: string[] } => {
21
+ const pathById = buildPaths(project.assets)
22
+ const serverByPath = new Map<string, { fileSrc?: string, shaders?: { platform: string, src: string }[] }>()
23
+ for (const asset of project.assets) {
24
+ if (asset.type === "folder") continue
25
+ serverByPath.set(pathById.get(asset.id)!, { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] })
26
+ }
27
+
28
+ const entries: CompileEntry[] = []
29
+ const warnings: string[] = []
30
+ const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) })
31
+ for (const file of scan.files) {
32
+ const type = manifest.files[file.path]?.type ?? classifyKind(basename(file.path))
33
+ if (type === "resource") {
34
+ const src = serverByPath.get(file.path)?.fileSrc
35
+ if (!src && (opts.warnMissingUrl ?? true)) warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`)
36
+ entries.push({ path: file.path, type, fileSrc: src, absPath: file.absPath })
37
+ } else if (type === "shader") {
38
+ const shaders = serverByPath.get(file.path)?.shaders
39
+ if (!shaders?.length) warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`)
40
+ entries.push({ path: file.path, type, shaders })
41
+ } else {
42
+ entries.push({ path: file.path, type, text: readFileSync(file.absPath, "utf8") })
43
+ }
44
+ }
45
+ return { entries, warnings }
46
+ }
@@ -0,0 +1,37 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { basename } from "node:path"
3
+ import type { Manifest } from "../manifest"
4
+ import { classifyKind, scanLocal } from "../localFiles"
5
+ import { loadIgnore } from "../ignore"
6
+ import type { CompileEntry } from "./projectCompile"
7
+
8
+ /*
9
+ * Builds the compile input for `lecodes dev` from the working tree ALONE — no server round-trip,
10
+ * no login. Binary resources compile to dev-server URLs (`<assetBase>/<path>`, served straight
11
+ * from the working tree), so the bundle works fully offline. Shaders carry their absPath so a
12
+ * caller that compiles them locally (the dev server's applyLocalShaders pass, via matc) can fill
13
+ * in the artifacts; callers that don't pass `localShaders` keep the old degradation — an empty
14
+ * artifact list + a push-once warning (same as `collectEntries` reports for unpushed shaders).
15
+ */
16
+ export const collectLocalEntries = (
17
+ root: string, manifest: Manifest | null, assetBase: string,
18
+ opts: { localShaders?: boolean } = {},
19
+ ): { entries: CompileEntry[], warnings: string[] } => {
20
+ const entries: CompileEntry[] = []
21
+ const warnings: string[] = []
22
+ const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) })
23
+ for (const file of scan.files) {
24
+ const type = manifest?.files[file.path]?.type ?? classifyKind(basename(file.path))
25
+ if (type === "resource") {
26
+ entries.push({ path: file.path, type, fileSrc: assetBase + encodeURI(file.path), absPath: file.absPath })
27
+ } else if (type === "shader") {
28
+ if (!opts.localShaders) {
29
+ warnings.push(`${file.path}: shaders compile on the server — push the project once for shaders to work in dev mode.`)
30
+ }
31
+ entries.push({ path: file.path, type, shaders: [], absPath: file.absPath })
32
+ } else {
33
+ entries.push({ path: file.path, type, text: readFileSync(file.absPath, "utf8") })
34
+ }
35
+ }
36
+ return { entries, warnings }
37
+ }
@@ -0,0 +1,104 @@
1
+ import { readFileSync } from "node:fs"
2
+ import { basename } from "node:path"
3
+ import type { Manifest } from "../manifest"
4
+ import { classifyKind, scanLocal } from "../localFiles"
5
+ import { loadIgnore } from "../ignore"
6
+ import { compileProject, type CompileEntry } from "./projectCompile"
7
+ import { applyAssetIcons } from "./assetIcons"
8
+ import { applyProjectFonts } from "./fonts"
9
+
10
+ /*
11
+ * Per-screen compile for `lecodes design`. A design screen file default-exports either an
12
+ * already-computed UIScreen (a value) or a `(state) => UIScreen` function (screen states); the
13
+ * virtual entry imports it, invokes the function form for the requested state, and opens the result.
14
+ * Bundled through the normal project pipeline. Fully local — no server round-trip:
15
+ * assets "server" → resource URLs are `/project/<path>` (the design dev server serves them)
16
+ * assets "disk" → `lecodesfile://<abs-path>` (the headless renderer reads them off disk)
17
+ */
18
+
19
+ export type DesignAssetMode = "server" | "disk"
20
+
21
+ // Must match lecodes-renderer's LOCAL_ASSET_SCHEME (headless/index.ts) — same as `lecodes render`.
22
+ const LOCAL_ASSET_SCHEME = "lecodesfile://"
23
+
24
+ export const compileDesignScreen = async (opts: {
25
+ root: string
26
+ /** Null in standalone mode (no cloned project): kinds come from filenames alone. */
27
+ manifest: Manifest | null
28
+ /** Project display name (compile metadata). */
29
+ name: string
30
+ /** Absolute path of the design folder — where `assetIcon()` icons are vendored (assets/icons/,
31
+ * icons.lock.json, ICON-NOTICES.md). */
32
+ designDir: string
33
+ /** The design folder as a project key ("/design"). */
34
+ designKey: string
35
+ /** Screen id = filename without .ts ("login"). */
36
+ screenId: string
37
+ /** State to render for a function-of-state screen. Omit for the canonical state (the export's
38
+ * param default). Ignored by a value-export screen. */
39
+ state?: string
40
+ assets: DesignAssetMode
41
+ }): Promise<string> => {
42
+ const { root, manifest, designDir, designKey, screenId, state } = opts
43
+
44
+ const entries: CompileEntry[] = []
45
+ const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) })
46
+ for (const file of scan.files) {
47
+ const type = manifest?.files[file.path]?.type ?? classifyKind(basename(file.path))
48
+ if (type === "resource") {
49
+ entries.push({
50
+ path: file.path,
51
+ type,
52
+ fileSrc: opts.assets === "disk" ? encodeURIComponent(file.absPath) : encodeURI(file.path),
53
+ absPath: file.absPath,
54
+ })
55
+ } else if (type === "shader") {
56
+ entries.push({ path: file.path, type, shaders: [] }) // UI-only render — no shader artifacts needed
57
+ } else {
58
+ entries.push({ path: file.path, type, text: readFileSync(file.absPath, "utf8") })
59
+ }
60
+ }
61
+
62
+ // The virtual entry lives in the design folder so `./screens/…` and the screen's own `../shared/…`
63
+ // imports resolve naturally. It never touches disk.
64
+ // A function-of-state export is invoked with the chosen state (canonical = a bare call, using the
65
+ // param's default); a value export is used as-is. Either way we .open() the resulting screen.
66
+ const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()"
67
+ const entryPath = `${designKey}/.screen-entry.ts`
68
+ entries.push({
69
+ path: entryPath,
70
+ type: "text",
71
+ text: `import screen from "./screens/${screenId}"\n`
72
+ + `const __screen = typeof screen === "function" ? ${invoke} : screen\n`
73
+ + `__screen.open()\n`,
74
+ })
75
+
76
+ // Resolve any `assetIcon('pack:name')` calls: vendor the SVGs into the design folder and rewrite
77
+ // each call to an inline SvgSource(...) so the bundle renders offline (no network at render time).
78
+ await applyAssetIcons(entries, designDir)
79
+
80
+ // Resolve `font(...)` calls the same way: registry faces vendor into the design folder
81
+ // (assets/fonts/ + fonts.lock.json) and each call becomes its family name plus a generated
82
+ // registration module — offline after the first compile, board and headless render alike.
83
+ await applyProjectFonts(entries, {
84
+ mode: "vendor",
85
+ root,
86
+ vendorDir: designDir,
87
+ vendorPrefix: designKey,
88
+ makeResourceEntry: (path, absPath) => ({
89
+ path,
90
+ type: "resource",
91
+ fileSrc: opts.assets === "disk" ? encodeURIComponent(absPath) : encodeURI(path),
92
+ absPath,
93
+ }),
94
+ })
95
+
96
+ return compileProject({
97
+ entries,
98
+ name: opts.name,
99
+ publicUrl: opts.assets === "disk" ? LOCAL_ASSET_SCHEME : "/project",
100
+ entryOverride: entryPath,
101
+ header: false,
102
+ format: "iife",
103
+ })
104
+ }
@@ -0,0 +1,156 @@
1
+ import { createHash } from "node:crypto"
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3
+ import { dirname, join } from "node:path"
4
+ import { applyFontMacro, buildFontsHeaderValue, fontRegistryBase, type FontFaceRef } from "lecodes-sdk/compile"
5
+ import type { CompileEntry } from "./projectCompile"
6
+
7
+ /*
8
+ * The CLI side of the `font()` compile macro (docs/font-system-plan.md): the sdk core scans and
9
+ * rewrites; this wrapper decides where face FILES come from per compile mode and keeps the vendor
10
+ * bookkeeping (fonts.lock.json + FONT-NOTICES.md, mirroring assetIcons.ts):
11
+ *
12
+ * cdn — server-backed compiles: registry faces stay remote (baked CDN URLs), only
13
+ * project-file fonts touch entries. No disk writes.
14
+ * vendor — local/dev/render/design compiles: registry faces are downloaded once into
15
+ * <vendorDir>/assets/fonts/ (offline after that) and pushed as resource entries, so
16
+ * they ride the normal asset pipeline (dev URLs / `id:`+fetchLocal / lecodesfile://).
17
+ *
18
+ * Returns the `// fonts:` header value (boot faces a host prefetches + holds first paint for),
19
+ * or undefined when the project uses no fonts.
20
+ */
21
+
22
+ type LicenseEntry = { spdx: string, title: string, url: string, attribution: string | null, modifiedNotice: boolean }
23
+ type LockFace = { id: string, family: string, weight: number, italic: boolean, hash: string, spdx: string, path: string }
24
+ type Lock = { registry: string, faces: Record<string, LockFace>, families: Record<string, LicenseEntry> }
25
+
26
+ export type ProjectFontsOptions = {
27
+ mode: "cdn" | "vendor"
28
+ /** Project root (abs) — fallback for reading project-font files without an absPath. */
29
+ root: string
30
+ /** vendor: where assets/fonts/, fonts.lock.json and FONT-NOTICES.md live (abs). */
31
+ vendorDir?: string
32
+ /** vendor: leading-slash entries prefix for vendorDir ("" = project root, "/design" = a design folder). */
33
+ vendorPrefix?: string
34
+ /** vendor: shape a resource entry for a vendored face the caller's way (fileSrc convention). */
35
+ makeResourceEntry?: (path: string, absPath: string) => CompileEntry
36
+ /** Registry override (URL or a local directory with the registry layout). */
37
+ registry?: string
38
+ /** Absolute-URL prefix of the compile (URL modes) — used for the header's entry-face URLs. */
39
+ publicUrl?: string
40
+ }
41
+
42
+ export const applyProjectFonts = async (entries: CompileEntry[], opts: ProjectFontsOptions): Promise<string | undefined> => {
43
+ const registry = (opts.registry ?? fontRegistryBase()).replace(/\/+$/, "")
44
+ const byPath = () => new Map(entries.map((e) => [e.path.replace(/^\//, ""), e]))
45
+
46
+ const faces = await applyFontMacro(entries, {
47
+ mode: opts.mode,
48
+ registryBase: registry,
49
+ readBytes: (entryPath) => {
50
+ const entry = byPath().get(entryPath)
51
+ const abs = entry?.absPath ?? join(opts.root, entryPath)
52
+ try { return new Uint8Array(readFileSync(abs)) } catch { return null }
53
+ },
54
+ vendorFace: opts.mode === "vendor"
55
+ ? async (face) => {
56
+ const vendorDir = opts.vendorDir ?? opts.root
57
+ const rel = `assets/fonts/${face.fileName}`
58
+ const abs = join(vendorDir, rel)
59
+ const entryPath = `${opts.vendorPrefix ?? ""}/${rel}`
60
+ if (!existsSync(abs)) await downloadFace(registry, vendorDir, face, rel, abs)
61
+ if (!byPath().has(entryPath.replace(/^\//, ""))) {
62
+ entries.push(opts.makeResourceEntry?.(entryPath, abs) ?? { path: entryPath, type: "resource", absPath: abs })
63
+ }
64
+ return entryPath
65
+ }
66
+ : undefined,
67
+ })
68
+ if (faces.length === 0) return undefined
69
+
70
+ return buildFontsHeaderValue(faces, (path) => {
71
+ if (!opts.publicUrl || !/^https?:/i.test(opts.publicUrl)) return null
72
+ const fileSrc = byPath().get(path.replace(/^\//, ""))?.fileSrc
73
+ return fileSrc ? opts.publicUrl + fileSrc : null
74
+ }) || undefined
75
+ }
76
+
77
+ // ---- vendoring (registry → assets/fonts + lock + notices; mirrors assetIcons.ts) ----------------
78
+
79
+ const isHttp = (s: string) => /^https?:/i.test(s)
80
+ const sha12 = (b: Uint8Array) => createHash("sha256").update(b).digest("hex").slice(0, 12)
81
+ const registryHome = (registry: string) => registry.replace(/\/v\d+$/, "")
82
+
83
+ const readRegistry = async (registry: string, rel: string): Promise<Uint8Array | null> => {
84
+ if (isHttp(registry)) {
85
+ const res = await fetch(`${registry}/${rel}`)
86
+ return res.ok ? new Uint8Array(await res.arrayBuffer()) : null
87
+ }
88
+ try { return new Uint8Array(readFileSync(join(registry, rel))) } catch { return null }
89
+ }
90
+
91
+ const readLock = (path: string): Lock | null => {
92
+ try {
93
+ const lock = JSON.parse(readFileSync(path, "utf8")) as Lock
94
+ if (lock && typeof lock === "object" && lock.faces && lock.families) return lock
95
+ } catch { /* absent or corrupt → start fresh */ }
96
+ return null
97
+ }
98
+
99
+ /** Stable serialization: keys sorted, no timestamp — same inputs, byte-identical output. */
100
+ const serializeLock = (lock: Lock): string => {
101
+ const sortKeys = <T>(rec: Record<string, T>): Record<string, T> =>
102
+ Object.fromEntries(Object.keys(rec).sort().map((k) => [k, rec[k]]))
103
+ return JSON.stringify({ registry: lock.registry, faces: sortKeys(lock.faces), families: sortKeys(lock.families) }, null, 2) + "\n"
104
+ }
105
+
106
+ const renderNotices = (lock: Lock): string => {
107
+ const ids = [...new Set(Object.values(lock.faces).map((f) => f.id))].sort()
108
+ const lines = [
109
+ "# Font attribution & notices",
110
+ "",
111
+ "Fonts vendored from the font registry via `font(...)`. Keep this file in your repo / product.",
112
+ "",
113
+ ]
114
+ for (const id of ids) {
115
+ const l = lock.families[id]
116
+ if (!l) continue
117
+ let note = `- **${l.title || id}** — ${l.spdx}${l.url ? ` — ${l.url}` : ""}`
118
+ if (l.attribution) note += `\n - **Attribution required:** ${l.attribution}`
119
+ lines.push(note)
120
+ }
121
+ lines.push("")
122
+ return lines.join("\n")
123
+ }
124
+
125
+ const writeIfChanged = (path: string, text: string) => {
126
+ try { if (readFileSync(path, "utf8") === text) return } catch { /* absent → write */ }
127
+ writeFileSync(path, text)
128
+ }
129
+
130
+ const downloadFace = async (
131
+ registry: string, vendorDir: string,
132
+ face: { id: string, family: string, weight: number, italic: boolean, fileName: string },
133
+ rel: string, abs: string,
134
+ ): Promise<void> => {
135
+ const licRaw = await readRegistry(registry, "licenses.json")
136
+ if (!licRaw) throw new Error(`font: can't reach the font registry (${registry}). Check your connection, or set LECODES_FONT_REGISTRY.`)
137
+ const licenses = JSON.parse(new TextDecoder().decode(licRaw)) as Record<string, LicenseEntry>
138
+ const lic = licenses[face.id]
139
+ if (!lic) throw new Error(`font("${face.id}"): the registry has no license entry for this family — it may not be published yet. Browse ${registryHome(registry)}.`)
140
+
141
+ const bytes = await readRegistry(registry, `${face.id}/${face.fileName}`)
142
+ if (!bytes) throw new Error(`font("${face.id}"): face "${face.fileName}" not found in the registry (${registryHome(registry)}).`)
143
+
144
+ mkdirSync(dirname(abs), { recursive: true })
145
+ writeFileSync(abs, bytes)
146
+
147
+ const lockPath = join(vendorDir, "fonts.lock.json")
148
+ const lock: Lock = readLock(lockPath) ?? { registry, faces: {}, families: {} }
149
+ lock.registry = registry
150
+ lock.faces[`${face.id}@${face.weight}${face.italic ? "i" : ""}`] =
151
+ { id: face.id, family: face.family, weight: face.weight, italic: face.italic, hash: sha12(bytes), spdx: lic.spdx, path: rel }
152
+ lock.families[face.id] = lic
153
+ writeIfChanged(lockPath, serializeLock(lock))
154
+ // Attribution is fail-closed: notices always reflect the vendored families (write throws → build fails).
155
+ writeIfChanged(join(vendorDir, "FONT-NOTICES.md"), renderNotices(lock))
156
+ }
@@ -0,0 +1,129 @@
1
+ import { getProject } from "../api"
2
+ import { loadConfig, requireApiUrl, requireToken } from "../config"
3
+ import { findProjectRoot, readManifest, type Manifest } from "../manifest"
4
+ import { collectEntries } from "./collect"
5
+ import { collectLocalEntries } from "./collectLocal"
6
+ import { ambiguousEntryWarning, compileProject } from "./projectCompile"
7
+ import { applyProjectFonts } from "./fonts"
8
+ import { applyAppAssetIcons } from "./assetIcons"
9
+ import type { ScreenTarget } from "./screenEntry"
10
+ import { flagBool, flagStr, note, type Args } from "../util"
11
+
12
+ /*
13
+ * Compile the project into the iife bundle the headless renderer runs — shared by `lecodes render`
14
+ * and `lecodes test`. Two modes, picked automatically (same split as `lecodes compile`):
15
+ * - Server-backed project (.lecodes/manifest.json): entries resolve from local files + the
16
+ * project's server metadata (needs login).
17
+ * - Local project (from `lecodes init` — no manifest): compiles straight from the working tree,
18
+ * no login.
19
+ * In both, resource assets default to LOCAL disk reads: each asset('./x.png') URL becomes
20
+ * `lecodesfile://<abs-path>` which the renderer reads off disk — unpushed/edited files render with
21
+ * no network. `--remote-assets` / an explicit `--public-url` switch to server URLs instead
22
+ * (server-backed projects only; then data needs --wait-network).
23
+ */
24
+
25
+ // URL scheme emitted for a local project's resources so the renderer reads them off disk. Must match
26
+ // lecodes-renderer's LOCAL_ASSET_SCHEME (headless/runProject.ts).
27
+ export const LOCAL_ASSET_SCHEME = "lecodesfile://"
28
+
29
+ export type HeadlessBundle = {
30
+ js: string
31
+ /** The project root the bundle was compiled from. */
32
+ root: string
33
+ /** Non-fatal compile-time notices (missing URLs, engine hints) — print via warnErr. */
34
+ warnings: string[]
35
+ /** Whether asset URLs resolve from the local disk (`lecodesfile://`). */
36
+ useLocalAssets: boolean
37
+ }
38
+
39
+ /** Compile the local project for a headless run (honors --entry / --public-url / --remote-assets). */
40
+ export const compileHeadlessBundle = async (args: Args, opts: { screen?: ScreenTarget } = {}): Promise<HeadlessBundle> => {
41
+ let root: string
42
+ let manifest: Manifest | null
43
+ try {
44
+ root = findProjectRoot(process.cwd())
45
+ manifest = readManifest(root)
46
+ } catch {
47
+ root = process.cwd()
48
+ manifest = null
49
+ }
50
+
51
+ const publicUrlFlag = flagStr(args, "public-url")
52
+ // A local (server-less) project has no remote asset URLs — it always reads from disk.
53
+ const useLocalAssets = manifest ? (!publicUrlFlag && !flagBool(args, "remote-assets")) : true
54
+
55
+ let entries
56
+ let warnings: string[]
57
+ let name: string
58
+ let publicUrl: string
59
+
60
+ if (manifest) {
61
+ const config = loadConfig()
62
+ const apiUrl = requireApiUrl(config)
63
+ const token = requireToken(config)
64
+
65
+ note("Resolving project assets…")
66
+ const project = await getProject(apiUrl, token, manifest.uuid)
67
+ ;({ entries, warnings } = collectEntries(root, manifest, project))
68
+ name = manifest.name
69
+ publicUrl = useLocalAssets ? LOCAL_ASSET_SCHEME : (publicUrlFlag ?? apiUrl)
70
+ // In local mode the "push it so its URL resolves" notices are moot (we read from disk).
71
+ if (useLocalAssets) warnings = warnings.filter((w) => !w.includes("No server URL"))
72
+ } else {
73
+ ;({ entries, warnings } = collectLocalEntries(root, null, ""))
74
+ name = root.split(/[\\/]/).pop() ?? "project"
75
+ publicUrl = LOCAL_ASSET_SCHEME
76
+ if (flagBool(args, "remote-assets") || publicUrlFlag) {
77
+ warnings.push("This is a local project (no server) — assets always load from the working tree.")
78
+ }
79
+ }
80
+
81
+ // Local assets (default): resolve each resource (asset('./x.png')) from the working tree instead of
82
+ // the server, so unpushed / edited images show up with no --wait-network. Each resource's URL becomes
83
+ // `lecodesfile://<abs-path>`, which the renderer reads off disk.
84
+ const compileEntries = useLocalAssets
85
+ ? entries.map((e) => (e.type === "resource" && e.absPath)
86
+ ? { ...e, fileSrc: encodeURIComponent(e.absPath) }
87
+ : e)
88
+ : [...entries]
89
+ // Rendering one screen: the generated entry that imports it and calls .open() (see screenEntry.ts).
90
+ if (opts.screen) compileEntries.push(opts.screen.entry)
91
+
92
+ // font() macro: vendor registry faces into assets/fonts/ so headless renders read them off disk
93
+ // (lecodesfile://); with --remote-assets they stay CDN URLs like every other remote resource.
94
+ await applyProjectFonts(compileEntries, useLocalAssets
95
+ ? {
96
+ mode: "vendor", root, vendorDir: root, vendorPrefix: "",
97
+ makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURIComponent(absPath), absPath }),
98
+ }
99
+ : { mode: "cdn", root })
100
+
101
+ // assetIcon() macro: resolve from the user-level cache (~/.lecodes/cache/icons) and inline, so
102
+ // headless renders stay deterministic and offline after the first fetch (like local assets).
103
+ await applyAppAssetIcons(compileEntries)
104
+
105
+ note("Compiling…")
106
+ const entryFlag = opts.screen?.entryPath ?? flagStr(args, "entry")
107
+ if (!entryFlag) {
108
+ const ambiguous = ambiguousEntryWarning(compileEntries)
109
+ if (ambiguous) warnings.push(ambiguous)
110
+ }
111
+ const js = await compileProject({
112
+ entries: compileEntries,
113
+ name,
114
+ publicUrl,
115
+ entryOverride: entryFlag,
116
+ header: false,
117
+ format: "iife",
118
+ })
119
+ if (js.includes("_creator.createScene") || js.includes("_creator.createEntity")) {
120
+ warnings.push("3D project: the scene graph is captured as `scene3d` in the JSON output; 3D isn't rasterized into PNGs.")
121
+ }
122
+ // With --remote-assets, 2D textures come from the network; without --wait-network they resolve empty
123
+ // and sprites render as flat placeholders. Local assets (default) load from disk, so no hint needed.
124
+ if (js.includes("_creator2d") && !useLocalAssets && !flagBool(args, "wait-network")) {
125
+ warnings.push("2D scene detected: sprite textures load over the network — add --wait-network to include them (else sprites render as flat placeholders).")
126
+ }
127
+
128
+ return { js, root, warnings, useLocalAssets }
129
+ }
@@ -0,0 +1,21 @@
1
+ import { extractInlineSourceMap, mapNamedFileStack } from "lecodes-viewer-lite/src/sourcemapError"
2
+
3
+ /*
4
+ * De-minify native (QuickJS) stack traces in a desktop host's output.
5
+ *
6
+ * The host evaluates the compiled bundle as one file, so every frame it prints reads
7
+ * `at e$ (module.js:5:51631)` — a minified column offset nobody can act on. The bundle carries its
8
+ * map inline (`//# lecodesMap=`, see sdk/compile/sourcemap.ts) and the CLI holds the very string it
9
+ * staged, so it can rewrite those frames into `main.ts:92:23` on the way through. The map consumer
10
+ * lives in viewer-lite (the preview needs the same decoding for its `new Function` frames).
11
+ */
12
+
13
+ /** quickjs names the bundle "module.js" when a project world is `run()` into a fresh context
14
+ * (creatorUtilsHandler) — "main.js" is the host's own boot eval, whose frames aren't ours. */
15
+ const BUNDLE_FILENAME = "module.js"
16
+
17
+ /** A rewriter for one bundle's output, or `null` when it carries no map (nothing to do). */
18
+ export const makeStackDemangler = (bundleJs: string): ((chunk: string) => string) | null => {
19
+ const map = extractInlineSourceMap(bundleJs)
20
+ return map ? (chunk: string) => mapNamedFileStack(chunk, map, BUNDLE_FILENAME) : null
21
+ }
@@ -0,0 +1,109 @@
1
+ import { existsSync } from "node:fs"
2
+ import { createRequire } from "node:module"
3
+ import { dirname, join } from "node:path"
4
+ import type { FileKind } from "../manifest"
5
+ import { distRoot } from "../distRoot"
6
+ import { compileProject as sdkCompileProject, detectEntry, scanModulesChisel, ENTRY_CONVENTION } from "lecodes-sdk/compile"
7
+
8
+ // Every project compiles through the SDK's one import-free compiler, which bundles with chisel (the
9
+ // Rust method-granular bundler, @letary/chisel) — the only bundler. The SDK resolves the chisel
10
+ // binary itself; here we just hand it an explicit path when the @letary/chisel package is present,
11
+ // and point it at the SDK *source* tree chisel compiles from:
12
+ // 1. the `lecodes-sdk` package when it resolves — the npm install's dependency, or in the core
13
+ // workspace the live packages/sdk (so an SDK edit is picked up with no re-vendoring);
14
+ // 2. else the copy vendored beside the standalone binary (runtime/sdk/, scripts/vendor-runtime.ts
15
+ // --sdk) — the binary has no node_modules.
16
+ const resolveSdkInject = (): string | null => {
17
+ try { return createRequire(import.meta.url).resolve("lecodes-sdk/inject") } catch { /* no package: standalone */ }
18
+ const vendored = join(distRoot, "runtime", "sdk", "inject.ts")
19
+ return existsSync(vendored) ? vendored : null
20
+ }
21
+ const sdkInject = resolveSdkInject()
22
+ if (sdkInject) process.env.LECODES_SDK_INJECT = sdkInject
23
+
24
+ /** Absolute path to the chisel binary: env override → the @letary/chisel npm package → a chisel
25
+ * binary shipped next to the executable (standalone binary distribution). Null when none found. */
26
+ const resolveChiselBin = (): string | null => {
27
+ if (process.env.LECODES_CHISEL_BIN) return process.env.LECODES_CHISEL_BIN
28
+ try {
29
+ const fromPkg = createRequire(import.meta.url)("@letary/chisel").binaryPath()
30
+ if (fromPkg) return fromPkg
31
+ } catch {
32
+ // @letary/chisel (or its platform binary) not installed — try the exe-adjacent copy
33
+ }
34
+ const local = join(dirname(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel")
35
+ return existsSync(local) ? local : null
36
+ }
37
+
38
+ const chiselBin = resolveChiselBin()
39
+ if (chiselBin) process.env.LECODES_CHISEL_BIN = chiselBin
40
+
41
+ export type CompileEntry = {
42
+ path: string
43
+ type: FileKind
44
+ /** Source text for code / .svg / scene files. */
45
+ text?: string
46
+ /** Server src for a binary resource (e.g. "/uploads/x.png"); prefixed with publicUrl. */
47
+ fileSrc?: string
48
+ /** Under `localAssets`: keep THIS resource in the URL form anyway (app.json resources.remote). */
49
+ remote?: boolean
50
+ /** Absolute path of the local file (resources + shaders) — `render` resolves assets from disk
51
+ * with it; the dev server's local shader compile (compile/shaders.ts) reads .mat sources. */
52
+ absPath?: string
53
+ /** Compiled-shader artifacts (platform + src); the first drives the per-backend URL. */
54
+ shaders?: { platform: string, src: string }[]
55
+ /** `shaders` are LOCAL files staged next to the bundle — the module compiles to a fetchLocal
56
+ * lookup instead of a URL (set by compile/shaders.ts in `local` mode). */
57
+ shadersLocal?: boolean
58
+ }
59
+
60
+ export type CompileOptions = {
61
+ entries: CompileEntry[]
62
+ name: string
63
+ publicUrl: string
64
+ entryOverride?: string
65
+ header?: boolean
66
+ /** `// fonts:` header payload (from applyProjectFonts — the font() macro pass). */
67
+ fontsHeader?: string
68
+ /** `// icons:` header payload (from applyAppAssetIcons — the assetIcon() macro pass). */
69
+ iconsHeader?: string
70
+ /** Output format: "esm" (default, shipped) or "iife" (self-running, for the local renderer). */
71
+ format?: "esm" | "iife"
72
+ /** Resolve `asset()` resources via the host's `fetchLocal` (local file → systemId) instead of a
73
+ * `publicUrl` URL — for a host with no asset server (e.g. the desktop native runtime). */
74
+ localAssets?: boolean
75
+ /** Override minification (default on). Scene-editor bundles pass `false`: mangling erases class
76
+ * names, which the inspector matches aspect schemas by (describeAspect().className). */
77
+ minify?: boolean
78
+ /** Generate + inline a source map (default true). `false` gives a smaller bundle whose runtime
79
+ * errors report generated positions only. */
80
+ sourcemap?: boolean
81
+ /** Editor compile: the `EDITOR` global compiles to `true` and `*.editor.ts` plugin files are
82
+ * bundled in (scene-editor bundles). Default false — production compiles skip them. */
83
+ editor?: boolean
84
+ }
85
+
86
+ /** A warning when auto-detection has several import-graph roots and no main.ts to settle it —
87
+ * the pick still works, but the developer should make the entry explicit. Null when unambiguous.
88
+ * Callers skip this when an explicit --entry / entryOverride is in play. */
89
+ export const ambiguousEntryWarning = (entries: { path: string, type?: string, text?: string }[]): string | null => {
90
+ const { entry, roots } = detectEntry(entries, scanModulesChisel)
91
+ if (!entry || roots.length < 2 || roots.includes(ENTRY_CONVENTION)) return null
92
+ return `Ambiguous entrypoint: nothing imports ${roots.join(", ")} — picked ${entry}. Rename your entry to main.ts (or pass --entry).`
93
+ }
94
+
95
+ export const compileProject = (opts: CompileOptions): Promise<string> =>
96
+ sdkCompileProject({
97
+ entries: opts.entries.map((e) => ({ path: e.path, type: e.type as any, text: e.text, fileSrc: e.fileSrc, remote: e.remote, shaders: e.shaders, shadersLocal: e.shadersLocal })),
98
+ name: opts.name,
99
+ publicUrl: opts.publicUrl,
100
+ entryOverride: opts.entryOverride,
101
+ header: opts.header,
102
+ fontsHeader: opts.fontsHeader,
103
+ iconsHeader: opts.iconsHeader,
104
+ format: opts.format,
105
+ localAssets: opts.localAssets,
106
+ minify: opts.minify,
107
+ sourcemap: opts.sourcemap,
108
+ editor: opts.editor,
109
+ })