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,333 @@
1
+ import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"
2
+ import { createReadStream, statSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import type { Manifest } from "../manifest"
5
+ import { resolveLocalPath } from "../localFiles"
6
+ import { collectLocalEntries } from "../compile/collectLocal"
7
+ import { applyLocalShaders, isShaderArtifactName, shadersCacheDir } from "../compile/shaders"
8
+ import { ambiguousEntryWarning, compileProject } from "../compile/projectCompile"
9
+ import { applyProjectFonts } from "../compile/fonts"
10
+ import { applyAppAssetIcons } from "../compile/assetIcons"
11
+ import { renderDevScreen, renderPrelude } from "./clientTemplates"
12
+ import { upgradeWebSocket, type WsConnection } from "./wsServer"
13
+ import { toKey } from "../util"
14
+ import { makeStackDemangler } from "../compile/nativeStack"
15
+
16
+ /*
17
+ * The `lecodes dev` LAN server: serves the project to devices and hot-reloads it on change.
18
+ *
19
+ * All routes live under a capability-token base path (/t/<token>):
20
+ * GET /t/<tok>/dev.js the bootstrap bundle the QR points at (placeholder screen + prelude)
21
+ * GET /t/<tok>/bundle.js?t=… the current project bundle + reload prelude (?t=poll for no-WS hosts)
22
+ * GET /t/<tok>/assets/<path> files streamed from the working tree (the bundle's asset URLs)
23
+ * GET /t/<tok>/shaders/<name> locally compiled shader artifacts (.lecodes/shaders — matc)
24
+ * GET /t/<tok>/version {"v":N} — polling fallback
25
+ * POST /t/<tok>/log log/error backchannel for polling clients
26
+ * WS /t/<tok>/ws reload push + log/error backchannel
27
+ * GET /t/<tok>/web/[file] the Web Lite browser runner (viewer-lite dist-embed, `dev --web`):
28
+ * it loads bundle.js?t=web — the bundle WITHOUT the native prelude;
29
+ * the page owns reload (ws / version polling) and the log backchannel
30
+ *
31
+ * Compile is eager: the command calls recompile() on every file change; a successful compile bumps
32
+ * `v` and broadcasts {type:"reload", v} to connected sockets (polling clients see /version move).
33
+ * A failed compile keeps the last good bundle — devices stay on it until the next good compile.
34
+ */
35
+
36
+ export type DevServerOptions = {
37
+ root: string
38
+ manifest: Manifest | null
39
+ name: string
40
+ /** The advertised origin baked into bundles + the prelude, e.g. "http://192.168.1.5:4499". */
41
+ origin: string
42
+ /** Bind address (default 0.0.0.0). */
43
+ bind?: string
44
+ port: number
45
+ token: string
46
+ entryOverride?: string
47
+ /** The Web Lite runner folder to serve under /web/ (src/dev/webRunner.ts); absent = no browser route. */
48
+ webDir?: string | null
49
+ /** Diagnostics (compile errors, device logs); defaults to stderr-style silence-safe noop. */
50
+ log?: (msg: string) => void
51
+ }
52
+
53
+ export type RecompileResult =
54
+ | { ok: true, changed: boolean, v: number, warnings: string[] }
55
+ | { ok: false, error: string }
56
+
57
+ export type DevServer = {
58
+ url: string
59
+ /** The browser runner page (`dev --web`), or null when no runner is vendored. */
60
+ webUrl: string | null
61
+ /** The token base path ("/t/<token>"). */
62
+ base: string
63
+ /** The actually-bound port (differs from options.port when 0 was passed, e.g. in tests). */
64
+ port: number
65
+ v: () => number
66
+ clientCount: () => number
67
+ recompile: () => Promise<RecompileResult>
68
+ close: () => void
69
+ }
70
+
71
+ const MIME: Record<string, string> = {
72
+ png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", gif: "image/gif",
73
+ svg: "image/svg+xml", ktx: "application/octet-stream", ktx2: "application/octet-stream",
74
+ glb: "model/gltf-binary", bin: "application/octet-stream",
75
+ mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", m4a: "audio/mp4",
76
+ mp4: "video/mp4", webm: "video/webm",
77
+ ttf: "font/ttf", otf: "font/otf", woff: "font/woff", woff2: "font/woff2",
78
+ json: "application/json", txt: "text/plain", js: "text/javascript", mjs: "text/javascript",
79
+ css: "text/css", html: "text/html", wasm: "application/wasm", map: "application/json",
80
+ }
81
+
82
+ const contentType = (path: string): string => {
83
+ const i = path.lastIndexOf(".")
84
+ return (i >= 0 && MIME[path.slice(i + 1).toLowerCase()]) || "application/octet-stream"
85
+ }
86
+
87
+ const sendText = (res: ServerResponse, status: number, type: string, body: string) => {
88
+ res.writeHead(status, { "content-type": type, "cache-control": "no-store" })
89
+ res.end(body)
90
+ }
91
+
92
+ const sendJson = (res: ServerResponse, status: number, body: unknown) =>
93
+ sendText(res, status, "application/json", JSON.stringify(body))
94
+
95
+ const readBody = (req: IncomingMessage): Promise<string> =>
96
+ new Promise((resolve) => {
97
+ let body = ""
98
+ req.on("data", (c: Buffer) => { if (body.length < 256 * 1024) body += c.toString("utf8") })
99
+ req.on("end", () => resolve(body))
100
+ req.on("error", () => resolve(body))
101
+ })
102
+
103
+ export const startDevServer = async (options: DevServerOptions): Promise<DevServer> => {
104
+ const { root, manifest, name, origin, token, entryOverride } = options
105
+ const log = options.log ?? (() => {})
106
+ const base = `/t/${token}`
107
+
108
+ // ---- compile state (the last good bundle; devices never see a broken one) ----------------------
109
+ let v = 0
110
+ let bundleJs: string | null = null
111
+ let compileChain: Promise<unknown> = Promise.resolve()
112
+
113
+ // The header's `// last-updated:` timestamp differs on every compile — ignore it when deciding
114
+ // whether anything really changed (a no-op save must not reload devices).
115
+ const stripVolatile = (js: string) => js.replace(/^\/\/ last-updated: .*$/m, "")
116
+
117
+ const compileOnce = async (): Promise<RecompileResult> => {
118
+ try {
119
+ const { entries, warnings } = collectLocalEntries(root, manifest, `${base}/assets`, { localShaders: true })
120
+ // Custom shaders compile LOCALLY via matc (downloaded on first use) into .lecodes/shaders,
121
+ // served under /shaders/ below — no push needed. Degrades to a warning when matc is
122
+ // unavailable; a broken .mat throws and fails this compile like any code error.
123
+ warnings.push(...(await applyLocalShaders(entries, { root, srcPrefix: `${base}/shaders` })).warnings)
124
+ // font() macro: vendor registry faces into assets/fonts/ (served from the working tree like
125
+ // any asset — LAN/offline friendly) and emit the `// fonts:` boot header.
126
+ const fontsHeader = await applyProjectFonts(entries, {
127
+ mode: "vendor",
128
+ root,
129
+ vendorDir: root,
130
+ vendorPrefix: "",
131
+ publicUrl: origin,
132
+ makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: `${base}/assets` + encodeURI(path), absPath }),
133
+ })
134
+ // assetIcon() macro: resolve from the user-level cache (~/.lecodes/cache/icons) and inline —
135
+ // one registry fetch per icon ever, so hot recompiles stay LAN/offline friendly.
136
+ const iconsHeader = await applyAppAssetIcons(entries)
137
+ if (!entryOverride) {
138
+ const ambiguous = ambiguousEntryWarning(entries)
139
+ if (ambiguous) warnings.push(ambiguous)
140
+ }
141
+ const js = await compileProject({ entries, name, publicUrl: origin, entryOverride, header: true, fontsHeader, iconsHeader })
142
+ const changed = bundleJs === null || stripVolatile(js) !== stripVolatile(bundleJs)
143
+ if (changed) {
144
+ bundleJs = js
145
+ v++
146
+ broadcast(JSON.stringify({ type: "reload", v }))
147
+ }
148
+ return { ok: true, changed, v, warnings }
149
+ } catch (e) {
150
+ return { ok: false, error: e instanceof Error ? e.message : String(e) }
151
+ }
152
+ }
153
+
154
+ // Serialized: a recompile requested mid-compile runs after it (compileOnce never rejects).
155
+ const recompile = (): Promise<RecompileResult> => {
156
+ const run = compileChain.then(compileOnce)
157
+ compileChain = run
158
+ return run
159
+ }
160
+
161
+ // dev.js: the placeholder project, compiled once per server start.
162
+ const devJs = await compileProject({
163
+ entries: [{ path: "/.dev-entry.ts", type: "text", text: renderDevScreen(name) }],
164
+ name: "LeCodes Dev",
165
+ publicUrl: origin,
166
+ entryOverride: "/.dev-entry.ts",
167
+ header: true,
168
+ })
169
+
170
+ // ---- websocket clients -------------------------------------------------------------------------
171
+ const clients = new Set<WsConnection>()
172
+ const broadcast = (text: string) => {
173
+ for (const client of clients) client.send(text)
174
+ }
175
+
176
+ // A device reports stacks against the bundle it evaluated ("module.js:5:51631" on the creator-pkg
177
+ // hosts) — rebuild the mapper whenever the bundle changes so those frames print as `main.ts:92:23`.
178
+ let demangleFor: { js: string, map: ((s: string) => string) | null } | null = null
179
+ const demangle = (text: string): string => {
180
+ if (bundleJs === null) return text
181
+ if (demangleFor?.js !== bundleJs) demangleFor = { js: bundleJs, map: makeStackDemangler(bundleJs) }
182
+ return demangleFor.map ? demangleFor.map(text) : text
183
+ }
184
+
185
+ const printDeviceMessage = (raw: string) => {
186
+ let m: { type?: string, level?: string, text?: string, v?: number } = {}
187
+ try { m = JSON.parse(raw) } catch { return }
188
+ if (m.type === "log") log(`[device:${m.level ?? "log"}] ${demangle(m.text ?? "")}`)
189
+ else if (m.type === "error") log(`[device:error] ${demangle(m.text ?? "")}`)
190
+ }
191
+
192
+ // ---- http --------------------------------------------------------------------------------------
193
+ // "web" = the browser runner: no prelude at all (the page brings its own reload + log channel).
194
+ const transportOf = (url: URL): "ws" | "poll" | "web" => {
195
+ const t = url.searchParams.get("t")
196
+ return t === "poll" ? "poll" : t === "web" ? "web" : "ws"
197
+ }
198
+ const withPrelude = (js: string, url: URL, v: number) => {
199
+ const transport = transportOf(url)
200
+ return transport === "web" ? js : js + "\n" + renderPrelude({ origin, base, transport, v })
201
+ }
202
+ const webDir = options.webDir ?? null
203
+
204
+ const handle = async (req: IncomingMessage, res: ServerResponse) => {
205
+ const url = new URL(req.url ?? "/", origin)
206
+ const path = url.pathname
207
+ // Browsers ask for it on every page load (the /web/ runner) — keep the console clean.
208
+ if (path === "/favicon.ico") { res.writeHead(204); res.end(); return }
209
+ if (!path.startsWith(base + "/")) return sendText(res, 404, "text/plain", "Not found")
210
+ const route = path.slice(base.length)
211
+
212
+ if (route === "/dev.js") {
213
+ return sendText(res, 200, "text/javascript", withPrelude(devJs, url, -1))
214
+ }
215
+ if (route === "/bundle.js") {
216
+ if (bundleJs === null) return sendText(res, 503, "text/plain", "No successful compile yet — check the terminal.")
217
+ res.setHeader("x-lecodes-v", String(v)) // the browser runner learns the version it runs
218
+ return sendText(res, 200, "text/javascript", withPrelude(bundleJs, url, v))
219
+ }
220
+ // The browser runner: a static folder with relative asset paths, so it needs the trailing slash.
221
+ if (route === "/web") {
222
+ res.writeHead(302, { location: `${base}/web/${url.search}` })
223
+ res.end()
224
+ return
225
+ }
226
+ if (route.startsWith("/web/")) {
227
+ if (!webDir) return sendText(res, 404, "text/plain", "No Web Lite runner vendored with this CLI (lecodes-viewer-lite dist-embed).")
228
+ const rel = route === "/web/" ? "embed.html" : route.slice("/web/".length)
229
+ if (rel.split("/").some((seg) => seg === ".." || seg === "" || seg === ".")) return sendText(res, 400, "text/plain", "Bad path")
230
+ const abs = join(webDir, ...rel.split("/"))
231
+ try {
232
+ if (!statSync(abs).isFile()) throw new Error("not a file")
233
+ } catch {
234
+ return sendText(res, 404, "text/plain", "Not found")
235
+ }
236
+ // The entry files (html, embed.js) must revalidate — a CLI update swaps them under the same URL.
237
+ const hashed = rel.startsWith("assets/")
238
+ res.writeHead(200, { "content-type": contentType(rel), "cache-control": hashed ? "max-age=31536000, immutable" : "no-store" })
239
+ createReadStream(abs).pipe(res)
240
+ return
241
+ }
242
+ if (route === "/version") {
243
+ return sendJson(res, 200, { v })
244
+ }
245
+ if (route === "/log" && req.method === "POST") {
246
+ printDeviceMessage(await readBody(req))
247
+ return sendJson(res, 200, { ok: true })
248
+ }
249
+ if (route.startsWith("/shaders/")) {
250
+ const name = route.slice("/shaders/".length)
251
+ if (!isShaderArtifactName(name)) return sendText(res, 400, "text/plain", "Bad path")
252
+ const abs = join(shadersCacheDir(root), name)
253
+ try {
254
+ if (!statSync(abs).isFile()) throw new Error("not a file")
255
+ } catch {
256
+ return sendText(res, 404, "text/plain", "Not found")
257
+ }
258
+ res.writeHead(200, { "content-type": "application/octet-stream", "cache-control": "no-store" })
259
+ createReadStream(abs).pipe(res)
260
+ return
261
+ }
262
+ if (route.startsWith("/assets/")) {
263
+ let key: string
264
+ try {
265
+ key = toKey(decodeURIComponent(route.slice("/assets".length)))
266
+ } catch {
267
+ return sendText(res, 400, "text/plain", "Bad path")
268
+ }
269
+ const abs = resolveLocalPath(root, key)
270
+ if (!abs || !statSync(abs).isFile()) return sendText(res, 404, "text/plain", "Not found")
271
+ res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" })
272
+ createReadStream(abs).pipe(res)
273
+ return
274
+ }
275
+ return sendText(res, 404, "text/plain", "Not found")
276
+ }
277
+
278
+ const server: Server = createServer((req, res) => {
279
+ handle(req, res).catch((e) => {
280
+ log(`request failed: ${e instanceof Error ? e.message : String(e)}`)
281
+ if (!res.headersSent) sendText(res, 500, "text/plain", "Internal error")
282
+ })
283
+ })
284
+
285
+ server.on("upgrade", (req, socket) => {
286
+ const url = new URL(req.url ?? "/", origin)
287
+ if (url.pathname !== base + "/ws") {
288
+ socket.destroy()
289
+ return
290
+ }
291
+ const conn = upgradeWebSocket(req, socket, {
292
+ onMessage: (text) => {
293
+ let m: { type?: string, v?: number } = {}
294
+ try { m = JSON.parse(text) } catch { /* not ours */ }
295
+ if (m.type === "hello") {
296
+ // A (re)connecting world announces what it runs; nudge it forward if it's stale.
297
+ if (typeof m.v === "number" && m.v !== v && bundleJs !== null) {
298
+ conn?.send(JSON.stringify({ type: "reload", v }))
299
+ }
300
+ } else {
301
+ printDeviceMessage(text)
302
+ }
303
+ },
304
+ onClose: () => { if (conn) clients.delete(conn) },
305
+ })
306
+ if (conn) clients.add(conn)
307
+ })
308
+
309
+ await new Promise<void>((resolve, reject) => {
310
+ server.once("error", reject)
311
+ server.listen(options.port, options.bind ?? "0.0.0.0", () => {
312
+ server.removeListener("error", reject)
313
+ resolve()
314
+ })
315
+ })
316
+
317
+ const address = server.address()
318
+ const boundPort = typeof address === "object" && address !== null ? address.port : options.port
319
+
320
+ return {
321
+ url: `${origin}${base}/dev.js`,
322
+ webUrl: webDir ? `${origin}${base}/web/` : null,
323
+ base,
324
+ port: boundPort,
325
+ v: () => v,
326
+ clientCount: () => clients.size,
327
+ recompile,
328
+ close: () => {
329
+ for (const client of clients) client.close()
330
+ server.close()
331
+ },
332
+ }
333
+ }
@@ -0,0 +1,22 @@
1
+ import { existsSync } from "node:fs"
2
+ import { createRequire } from "node:module"
3
+ import { dirname, join } from "node:path"
4
+ import { distRoot } from "../distRoot"
5
+
6
+ /**
7
+ * Where the browser runner for `lecodes dev --web` lives: viewer-lite's built `dist-embed`
8
+ * (embed.html + embed.js + assets/ — the Web Lite host as a static folder). Two shapes:
9
+ * - vendored next to the CLI as runtime/web (scripts/vendor-runtime.ts — the npm package and
10
+ * the standalone binary),
11
+ * - the lecodes-viewer-lite package's own dist-embed (monorepo dev after `bun run build:embed`).
12
+ * `null` = no runner available (the dev server then serves everything but /web/).
13
+ */
14
+ export const resolveWebRunnerDir = (): string | null => {
15
+ const candidates = [join(distRoot, "runtime", "web")]
16
+ try {
17
+ const pkg = createRequire(import.meta.url).resolve("lecodes-viewer-lite/package.json")
18
+ candidates.push(join(dirname(pkg), "dist-embed"))
19
+ } catch { /* no package: standalone */ }
20
+ for (const dir of candidates) if (existsSync(join(dir, "embed.html")) && existsSync(join(dir, "embed.js"))) return dir
21
+ return null
22
+ }
@@ -0,0 +1,144 @@
1
+ import { createHash } from "node:crypto"
2
+ import type { IncomingMessage } from "node:http"
3
+ import type { Duplex } from "node:stream"
4
+
5
+ /*
6
+ * A minimal RFC 6455 WebSocket server for the `lecodes dev` reload channel — dependency-free, like
7
+ * the rest of the CLI. Scope: server side only, text frames (the protocol is small JSON messages),
8
+ * ping→pong, close handshake. No fragmentation support (continuation frames are dropped) and no
9
+ * extensions — fine for OkHttp/NSURLSession/WinHTTP clients sending short messages.
10
+ */
11
+
12
+ const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
13
+
14
+ /** Sec-WebSocket-Accept for a client's Sec-WebSocket-Key (RFC 6455 §4.2.2). */
15
+ export const acceptKey = (key: string): string =>
16
+ createHash("sha1").update(key + WS_GUID).digest("base64")
17
+
18
+ /** A server→client text frame (FIN, unmasked — servers MUST NOT mask). */
19
+ export const encodeTextFrame = (text: string): Buffer => {
20
+ const payload = Buffer.from(text, "utf8")
21
+ const len = payload.length
22
+ let header: Buffer
23
+ if (len < 126) {
24
+ header = Buffer.from([ 0x81, len ])
25
+ } else if (len < 65536) {
26
+ header = Buffer.alloc(4)
27
+ header[0] = 0x81
28
+ header[1] = 126
29
+ header.writeUInt16BE(len, 2)
30
+ } else {
31
+ header = Buffer.alloc(10)
32
+ header[0] = 0x81
33
+ header[1] = 127
34
+ header.writeBigUInt64BE(BigInt(len), 2)
35
+ }
36
+ return Buffer.concat([ header, payload ])
37
+ }
38
+
39
+ const encodeControlFrame = (opcode: number, payload: Buffer = Buffer.alloc(0)): Buffer =>
40
+ Buffer.concat([ Buffer.from([ 0x80 | opcode, Math.min(payload.length, 125) ]), payload.subarray(0, 125) ])
41
+
42
+ export const OPCODE = { CONTINUATION: 0x0, TEXT: 0x1, BINARY: 0x2, CLOSE: 0x8, PING: 0x9, PONG: 0xa } as const
43
+
44
+ export type WsFrame = { opcode: number, payload: Buffer }
45
+
46
+ /** Stateful frame decoder: feed raw socket chunks, get complete (unmasked) frames back. */
47
+ export const createFrameDecoder = () => {
48
+ let buf: Buffer = Buffer.alloc(0)
49
+ return (chunk: Buffer): WsFrame[] => {
50
+ buf = buf.length === 0 ? chunk : Buffer.concat([ buf, chunk ])
51
+ const frames: WsFrame[] = []
52
+ while (buf.length >= 2) {
53
+ const opcode = buf[0] & 0x0f
54
+ const masked = (buf[1] & 0x80) !== 0
55
+ let len = buf[1] & 0x7f
56
+ let off = 2
57
+ if (len === 126) {
58
+ if (buf.length < 4) break
59
+ len = buf.readUInt16BE(2)
60
+ off = 4
61
+ } else if (len === 127) {
62
+ if (buf.length < 10) break
63
+ len = Number(buf.readBigUInt64BE(2))
64
+ off = 10
65
+ }
66
+ const maskLen = masked ? 4 : 0
67
+ if (buf.length < off + maskLen + len) break
68
+ let payload = buf.subarray(off + maskLen, off + maskLen + len)
69
+ if (masked) {
70
+ const mask = buf.subarray(off, off + 4)
71
+ const unmasked = Buffer.alloc(len)
72
+ for (let i = 0; i < len; i++) unmasked[i] = payload[i] ^ mask[i & 3]
73
+ payload = unmasked
74
+ }
75
+ buf = buf.subarray(off + maskLen + len)
76
+ frames.push({ opcode, payload })
77
+ }
78
+ return frames
79
+ }
80
+ }
81
+
82
+ export type WsConnection = {
83
+ send: (text: string) => void
84
+ close: () => void
85
+ }
86
+
87
+ export type WsHandlers = {
88
+ onMessage: (text: string) => void
89
+ /** Fired exactly once, however the connection ends. */
90
+ onClose: () => void
91
+ }
92
+
93
+ /**
94
+ * Complete the upgrade handshake on an `http.Server` `'upgrade'` event and speak WebSocket on the
95
+ * raw socket. Returns null (socket destroyed) if the request isn't a valid upgrade.
96
+ */
97
+ export const upgradeWebSocket = (req: IncomingMessage, socket: Duplex, handlers: WsHandlers): WsConnection | null => {
98
+ const key = req.headers["sec-websocket-key"]
99
+ if (typeof key !== "string" || (req.headers.upgrade ?? "").toLowerCase() !== "websocket") {
100
+ socket.destroy()
101
+ return null
102
+ }
103
+ socket.write(
104
+ "HTTP/1.1 101 Switching Protocols\r\n" +
105
+ "Upgrade: websocket\r\n" +
106
+ "Connection: Upgrade\r\n" +
107
+ `Sec-WebSocket-Accept: ${acceptKey(key)}\r\n\r\n`,
108
+ )
109
+
110
+ let open = true
111
+ const finish = () => {
112
+ if (!open) return
113
+ open = false
114
+ handlers.onClose()
115
+ }
116
+ const decode = createFrameDecoder()
117
+ socket.on("data", (chunk: Buffer) => {
118
+ for (const frame of decode(chunk)) {
119
+ if (frame.opcode === OPCODE.TEXT) {
120
+ handlers.onMessage(frame.payload.toString("utf8"))
121
+ } else if (frame.opcode === OPCODE.PING) {
122
+ if (open) socket.write(encodeControlFrame(OPCODE.PONG, frame.payload))
123
+ } else if (frame.opcode === OPCODE.CLOSE) {
124
+ if (open) socket.write(encodeControlFrame(OPCODE.CLOSE))
125
+ socket.end()
126
+ finish()
127
+ }
128
+ }
129
+ })
130
+ socket.on("close", finish)
131
+ socket.on("error", () => socket.destroy())
132
+
133
+ return {
134
+ send: (text: string) => {
135
+ if (open) socket.write(encodeTextFrame(text))
136
+ },
137
+ close: () => {
138
+ if (!open) return
139
+ socket.write(encodeControlFrame(OPCODE.CLOSE))
140
+ socket.end()
141
+ finish()
142
+ },
143
+ }
144
+ }
@@ -0,0 +1,20 @@
1
+ import { existsSync } from "node:fs"
2
+ import { dirname, join } from "node:path"
3
+ import { fileURLToPath } from "node:url"
4
+
5
+ /* Root of the CLI's on-disk companion files (runtime/ with the vendored SDK source + type bundle).
6
+ *
7
+ * Two distribution shapes:
8
+ * - npm package / monorepo dev: this module lives in dist/ (or src/), companions at ../runtime —
9
+ * resolve relative to import.meta.url as before.
10
+ * - standalone binary (`bun build --compile`, see scripts/build-bin.ts): import.meta.url points into
11
+ * the virtual bunfs archive which does NOT exist on disk, so the companions ship in the release
12
+ * archive NEXT TO the executable and are resolved via process.execPath. */
13
+ export const distRoot = (() => {
14
+ const candidates = [
15
+ join(dirname(fileURLToPath(import.meta.url)), ".."), // package root (dist/.. or src/..)
16
+ dirname(process.execPath), // standalone binary: next to the exe
17
+ ]
18
+ for (const c of candidates) if (existsSync(join(c, "runtime"))) return c
19
+ return candidates[0]
20
+ })()