lecodes-cli 2.0.4 → 2.0.6
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.
- package/dist/index.js +3506 -1819
- package/package.json +20 -11
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/assets/creator-2d-CYlV77UR.js +1 -0
- package/runtime/web/assets/creator-2d-nG7rGEmq.wasm +0 -0
- package/runtime/web/assets/{creator-full-D2dSDdDj.wasm → creator-full-Cla1R8N6.wasm} +0 -0
- package/runtime/web/assets/creator-full-DmfoULcB.js +1 -0
- package/runtime/web/assets/creator-full.js-D8SMHuCt.symbols +8419 -0
- package/runtime/web/assets/creator-ui-CbtoaRlz.js +1 -0
- package/runtime/web/assets/creator-ui-DZaoxIpX.wasm +0 -0
- package/runtime/web/player.js +18 -18
- package/runtime/web-lite/assets/__vite-browser-external-BIHI7g3E.js +1 -0
- package/runtime/web-lite/assets/ar-BOdQ-wV4.js +1 -0
- package/runtime/web-lite/assets/basis-C64VHDVD.js +1 -0
- package/runtime/web-lite/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web-lite/assets/{canvas-C6G_Pb2Q.js → canvas-WxiusXCB.js} +1 -1
- package/runtime/web-lite/assets/creatorAnim-BOwKr-C1.js +1 -0
- package/runtime/web-lite/assets/creatorParticles-DYlv4g1J.js +1 -0
- package/runtime/web-lite/assets/creatorPhysics-k7wL0kV1.js +1 -0
- package/runtime/web-lite/assets/draco-BiISTFcR.js +118 -0
- package/runtime/web-lite/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web-lite/assets/index-3CbsYWmo.js +2 -0
- package/runtime/web-lite/assets/neutral_ibl128-D-lzdkLN.ktx +0 -0
- package/runtime/web-lite/assets/{pager-O712dZv9.js → pager-Db5BY7FB.js} +1 -1
- package/runtime/web-lite/assets/plugins-CuEPHbHL.js +1 -0
- package/runtime/web-lite/assets/scene3d-CX7MAqmN.js +866 -0
- package/runtime/web-lite/assets/tween-BnWYhJ7P.js +1 -0
- package/runtime/web-lite/assets/video-BLj61nDx.js +1 -0
- package/runtime/web-lite/assets/{vlist-gEB8Sn0S.js → vlist-DrV2Q7P5.js} +1 -1
- package/runtime/web-lite/assets/worker-Caf-yYEI.js +2 -0
- package/runtime/web-lite/features.json +87 -4
- package/runtime/web-lite/lite.js +39 -10
- package/runtime/web-lite/plugins/camera/index-5XOM8eHl.js +1 -0
- package/runtime/web-lite/plugins/geolocation/index-BjTKN9_P.js +1 -0
- package/runtime/web-lite/plugins/map/_virtual_lecodes-half-CQBO044A.js +1 -0
- package/runtime/web-lite/plugins/map/_virtual_lecodes-half-resources-BYwqQkWQ.js +1 -0
- package/runtime/web-lite/plugins/map/assets/maplibre-gl-worker-DeQFNGa4.js +8 -0
- package/runtime/web-lite/plugins/map/index-BTBHQ264.js +1 -0
- package/runtime/web-lite/plugins/map/mapImpl-C6E1SwCS.js +823 -0
- package/runtime/web-lite/plugins/qr-scanner/index-CK_cYoJR.js +1 -0
- package/runtime/web-lite/plugins.json +8 -0
- package/src/commands/compile.ts +7 -1
- package/src/commands/desktop.ts +7 -4
- package/src/commands/dev.ts +4 -14
- package/src/commands/render.ts +6 -2
- package/src/commands/test.ts +25 -9
- package/src/commands/update/index.ts +12 -7
- package/src/commands/update/types.ts +1 -1
- package/src/compile/projectCompile.ts +48 -1
- package/src/dev/androidDev.ts +1 -1
- package/src/dev/backendWorker.ts +138 -0
- package/src/dev/devServer.ts +119 -115
- package/src/dev/localBackend.ts +214 -0
- package/src/dev/projectBackend.ts +199 -0
- package/src/dev/serve.ts +164 -0
- package/src/hosts/cmgenTool.ts +6 -9
- package/src/hosts/desktopRenderer.ts +6 -5
- package/src/hosts/liteRunner.ts +52 -9
- package/src/hosts/matcTool.ts +5 -22
- package/src/hosts/packageManager.ts +45 -0
- package/src/hosts/peerInstall.ts +8 -13
- package/src/hosts/peers.ts +2 -0
- package/src/hosts/releases.ts +6 -0
- package/src/index.ts +5 -1
- package/src/plugin/generate.ts +10 -6
- package/src/web/build.ts +20 -3
- package/runtime/web/assets/creator-2d-D9WKYM6h.js +0 -1
- package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
- package/runtime/web/assets/creator-full-BBXzQlhq.js +0 -1
- package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +0 -8124
- package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
- package/runtime/web/assets/creator-ui-_1Z_AX2M.js +0 -1
- package/runtime/web-lite/assets/tween-t_XDy3Nd.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const h={present:e=>e!=null,f64:e=>typeof e=="number"&&Number.isFinite(e)?e:void 0,i32:e=>typeof e=="number"&&Number.isInteger(e)&&e>=-2147483648&&e<=2147483647?e:void 0,bool:e=>typeof e=="boolean"?e:void 0,string:e=>typeof e=="string"?e:void 0,json:e=>e,bytes:e=>Object.prototype.toString.call(e)==="[object Uint8Array]"?e.slice():void 0,object:e=>typeof e=="object"&&e!==null&&!Array.isArray(e)?e:void 0,array:(e,r)=>Array.isArray(e)&&(r===void 0||e.length===r)?e:void 0,list:(e,r)=>{if(!Array.isArray(e))return;const n=[];for(const c of e){const o=r(c);if(o===void 0)return;n.push(o)}return n},map:(e,r)=>{if(typeof e!="object"||e===null||Array.isArray(e))return;const n={};for(const[c,o]of Object.entries(e)){const s=r(o);if(s===void 0)return;n[c]=s}return n}},j=e=>!h.present(e)||h.object(e)?{}:void 0,v={name:"qrScanner",version:1},A=(e,r)=>{e.registerView(v.name,(n,c,o)=>{const s={scan:i=>c.emit("scan",i)},a=r(j(n)??{},s,o),t=async(i,p)=>{throw new Error(`Unknown method: ${i}`)};return{el:a.el,call:t,destroy:()=>{var i;return(i=a.destroy)==null?void 0:i.call(a)}}},v.version)},S=150,k=10,T=e=>{var n;const r=globalThis.BarcodeDetector;r&&(typeof navigator>"u"||typeof((n=navigator.mediaDevices)==null?void 0:n.getUserMedia)!="function"||A(e,(c,o,s)=>{const a=s.createElement("div");a.style.cssText="background:#000;overflow:hidden";const t=s.createElement("video");t.autoplay=!0,t.muted=!0,t.playsInline=!0,t.setAttribute("playsinline",""),t.style.cssText="position:absolute;inset:0;width:100%;height:100%;object-fit:cover",a.appendChild(t);const i=new r({formats:["qr_code"]}),p=s.defaultView??globalThis;let u=null,b=null,f=!1,m=!1,y=null,g=0;const w=async()=>{var l;if(!(m||f||t.videoWidth===0)){m=!0;try{const d=((l=(await i.detect(t))[0])==null?void 0:l.rawValue)??null;if(f)return;d!==null?(g=0,d!==y&&(y=d,o.scan({data:d}))):y!==null&&++g>=k&&(y=null,o.scan({data:null}))}catch{}finally{m=!1}}};return navigator.mediaDevices.getUserMedia({video:{facingMode:"environment"},audio:!1}).then(l=>{if(f){for(const d of l.getTracks())d.stop();return}u=l,t.srcObject=l,t.play().catch(()=>{}),b=p.setInterval(()=>{w()},S)},()=>{}),{el:a,destroy(){if(f=!0,b!==null&&p.clearInterval(b),u)for(const l of u.getTracks())l.stop();u=null,t.srcObject=null}}}))};export{T as register};
|
package/src/commands/compile.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { buildGlobMatcher } from "../project/ignore"
|
|
|
13
13
|
import { readAppConfigAt } from "./app/shared"
|
|
14
14
|
import { defineCommand, isNullSink, note, warnErr } from "../cli"
|
|
15
15
|
import { bundleFlags, bundleOptions, outFlag, session, type BundleOptions } from "./shared"
|
|
16
|
+
import type { RunBackend } from "../dev/projectBackend"
|
|
16
17
|
|
|
17
18
|
/*
|
|
18
19
|
* `lecodes compile` — the local project as one JS bundle (the server's pipeline), printed to stdout
|
|
@@ -81,6 +82,10 @@ export type BuildBundleOptions = {
|
|
|
81
82
|
entryOverride?: string
|
|
82
83
|
/** Shell mode (`lecodes app sync`) — see above. */
|
|
83
84
|
shell?: ShellCompile
|
|
85
|
+
/** The bundle is about to RUN on this machine: a project with server files gets its backend
|
|
86
|
+
* started here (dev/projectBackend.ts) and that backend's url in its stubs. Without it the stubs
|
|
87
|
+
* of a `*.server.ts` have no server to call. */
|
|
88
|
+
backend?: RunBackend
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
export default defineCommand({
|
|
@@ -213,8 +218,9 @@ export const buildBundle = async (bundle: BundleOptions, opts: BuildBundleOption
|
|
|
213
218
|
const ambiguous = ambiguousEntryWarning(entries)
|
|
214
219
|
if (ambiguous) warnErr(ambiguous)
|
|
215
220
|
}
|
|
221
|
+
const serverUrl = await opts.backend?.start(entries, (w) => warnings.push(w))
|
|
216
222
|
const js = await compileProject({
|
|
217
|
-
entries, name, publicUrl, entryOverride,
|
|
223
|
+
entries, name, publicUrl, entryOverride, serverUrl,
|
|
218
224
|
header: bundle.header ?? true,
|
|
219
225
|
localAssets, fontsHeader, iconsHeader,
|
|
220
226
|
minify: bundle.minify,
|
package/src/commands/desktop.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { basename } from "node:path"
|
|
2
|
-
import { CliError, bool, c, defineCommand, log, note, num, str } from "../cli"
|
|
2
|
+
import { CliError, bool, c, defineCommand, log, logErr, note, num, str } from "../cli"
|
|
3
3
|
import {
|
|
4
4
|
DESKTOP_RENDERER_VERSION, fetchDesktopManifest, findMacHostExe, installedDesktopVersions, runDesktopApp,
|
|
5
5
|
selectedDesktopVersion, semverNewer, updateDesktop,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { desktopBuild } from "./app/desktop"
|
|
8
8
|
import { desktopPluginLibraries, readAppConfigAt, resolveDesktopRenderer } from "./app/shared"
|
|
9
9
|
import { buildBundle } from "./compile"
|
|
10
|
+
import { runBackend } from "../dev/projectBackend"
|
|
10
11
|
import { bundleFlags, bundleOptions, localProject } from "./shared"
|
|
11
12
|
|
|
12
13
|
/*
|
|
@@ -40,7 +41,7 @@ export const resolveRenderScale = (flag: string | undefined, cfg: number | undef
|
|
|
40
41
|
const run = defineCommand({
|
|
41
42
|
name: "run",
|
|
42
43
|
summary: "Compile the current project and open it in a desktop window",
|
|
43
|
-
description: "Window size / min size / title default from app.json's desktop block; asset() resources load from the working tree. Multiplayer: --server starts the host as a dedicated server, --connect joins one — both only set Net.launch (the app decides).",
|
|
44
|
+
description: "Window size / min size / title default from app.json's desktop block; asset() resources load from the working tree. A project with *.server.ts files gets its backend run on this machine for as long as the window is open (the database of `lecodes dev`: .lecodes/db). Multiplayer: --server starts the host as a dedicated server, --connect joins one — both only set Net.launch (the app decides).",
|
|
44
45
|
flags: {
|
|
45
46
|
width: num("window width (logical px)"),
|
|
46
47
|
height: num("window height (logical px)"),
|
|
@@ -68,7 +69,9 @@ const run = defineCommand({
|
|
|
68
69
|
// ONE renderer decision per run: it picks the host binary AND which .filamat gets staged beside
|
|
69
70
|
// the bundle. Reading the config twice is how a Vulkan exe ends up next to an OpenGL shader.
|
|
70
71
|
const renderer = resolveDesktopRenderer(root, flags.renderer)
|
|
71
|
-
|
|
72
|
+
// The project's backend (its *.server.ts files), when it has one: run here while the window is open.
|
|
73
|
+
const backend = runBackend({ root, command: "lecodes desktop run", log: logErr })
|
|
74
|
+
const { js, resources } = await buildBundle(bundleOptions(flags), { localAssets: true, desktopRenderer: renderer, backend })
|
|
72
75
|
// Native plugins: the project's plugins/ dir + declared plugin libraries, scanned by the host.
|
|
73
76
|
const { dirs: pluginDirs, files: pluginFiles } = desktopPluginLibraries(root)
|
|
74
77
|
if (pluginFiles.length > 0) note(`Native plugins: ${pluginFiles.map((f) => basename(f)).join(", ")}`)
|
|
@@ -101,7 +104,7 @@ const run = defineCommand({
|
|
|
101
104
|
net: flags.server
|
|
102
105
|
? { role: "server", port: flags.port, maxClients: flags["max-clients"] }
|
|
103
106
|
: flags.connect ? { role: "client", address: flags.connect } : undefined,
|
|
104
|
-
})
|
|
107
|
+
}).finally(() => backend.close())
|
|
105
108
|
},
|
|
106
109
|
})
|
|
107
110
|
|
package/src/commands/dev.ts
CHANGED
|
@@ -22,7 +22,8 @@ import { bundleFlags } from "./shared"
|
|
|
22
22
|
* working tree. The device opens the QR URL like any project URL; every save recompiles and
|
|
23
23
|
* hot-swaps the running app (`_creatorApp.run` — fresh context, rollback on error). Device
|
|
24
24
|
* console output and errors stream back to this terminal. Fully local: no login, no le.codes
|
|
25
|
-
* backend
|
|
25
|
+
* backend — a project's own backend (its `*.server.ts` files) runs here too, its database in
|
|
26
|
+
* .lecodes/db (dev/localBackend.ts). Custom shaders compile locally too (matc, downloaded on first use — matcTool.ts);
|
|
26
27
|
* only when matc can't be fetched do they fall back to server-compiled artifacts (push once).
|
|
27
28
|
*
|
|
28
29
|
* `--web` opens the same project in the browser: the dev server also serves, under /web/, the
|
|
@@ -145,16 +146,6 @@ export default defineCommand({
|
|
|
145
146
|
}
|
|
146
147
|
const name = manifest?.name ?? basename(root)
|
|
147
148
|
|
|
148
|
-
// Bun's node:http can't complete a websocket upgrade (writes to the upgraded socket are dropped),
|
|
149
|
-
// so the reload push would silently never reach devices: a phone that scanned a websocket url
|
|
150
|
-
// sat on "Connected — loading the project…" forever. Under Bun every device url — the QR, the
|
|
151
|
-
// printed one, what --ios / --android open — is the POLL form (/dev-poll.js: a path, since the
|
|
152
|
-
// launcher only fetches urls that end in .js). Node is the shipped runtime.
|
|
153
|
-
const underBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined"
|
|
154
|
-
if (underBun) {
|
|
155
|
-
warnErr("Running under Bun: no websocket reload channel here — devices poll instead (the QR points at /dev-poll.js). Node is the shipped runtime.")
|
|
156
|
-
}
|
|
157
|
-
|
|
158
149
|
const port = flags.port || DEFAULT_PORT
|
|
159
150
|
const candidates = lanCandidates()
|
|
160
151
|
const host = flags.host ?? candidates[0]?.address
|
|
@@ -203,15 +194,14 @@ export default defineCommand({
|
|
|
203
194
|
log("")
|
|
204
195
|
log(`${c.bold("LeCodes Dev")} — ${name}`)
|
|
205
196
|
log("")
|
|
206
|
-
|
|
207
|
-
const deviceUrl = underBun ? server.url.replace(/\/dev\.js$/, "/dev-poll.js") : server.url
|
|
197
|
+
const deviceUrl = server.url
|
|
208
198
|
if (!flags["no-qr"]) await printQr(deviceUrl)
|
|
209
199
|
log(`Scan the QR with the LeCodes app, or open: ${c.bold(deviceUrl)}`)
|
|
210
200
|
// The browser runner: localhost is a secure context (camera works there), the LAN address is
|
|
211
201
|
// what a phone's browser can reach.
|
|
212
202
|
const webLocal = server.webUrl ? server.webUrl.replace(`http://${host}:`, "http://localhost:") : null
|
|
213
203
|
if (server.webUrl && webLocal) log(`In the browser: ${c.bold(webLocal)} (LAN: ${server.webUrl})`)
|
|
214
|
-
|
|
204
|
+
note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`)
|
|
215
205
|
if (alternatives.length > 0) {
|
|
216
206
|
note(`Device can't reach it? This machine also has ${alternatives.map((a) => `${a.address} (${a.iface})`).join(", ")} — retry with --host <ip>.`)
|
|
217
207
|
}
|
package/src/commands/render.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { compileHeadlessBundle } from "../compile/headlessBundle"
|
|
|
4
4
|
import { screenTargetFrom } from "../compile/screenEntry"
|
|
5
5
|
import { findProjectRoot } from "../project/manifest"
|
|
6
6
|
import { buildBundle } from "./compile"
|
|
7
|
+
import { runBackend } from "../dev/projectBackend"
|
|
7
8
|
import { desktopDefaultViewport, desktopPluginLibraries, readAppConfigAt, resolveDesktopRenderer } from "./app/shared"
|
|
8
9
|
import { runDesktopRender } from "../hosts/desktopRenderer"
|
|
9
10
|
import { evaluateDesktopRun, translateScenario } from "../hosts/desktopScript"
|
|
@@ -285,8 +286,10 @@ const renderDesktop = async (flags: RenderFlags, file: string | undefined): Prom
|
|
|
285
286
|
const desktopRenderer = resolveDesktopRenderer(root, flags.renderer)
|
|
286
287
|
// Native plugins (project plugins/ dir + declared libraries) — the host scans CREATOR_PLUGIN_DIRS.
|
|
287
288
|
const { dirs: pluginDirs } = desktopPluginLibraries(root)
|
|
289
|
+
// The project's backend, when it has one (the database of `lecodes dev`).
|
|
290
|
+
const backend = runBackend({ root, command: "lecodes render", log: (msg) => { if (logs || msg.includes("[server:error]")) logErr(msg) } })
|
|
288
291
|
const { js, resources } = await buildBundle(bundleOptions(flags), {
|
|
289
|
-
localAssets: true, desktopRenderer,
|
|
292
|
+
localAssets: true, desktopRenderer, backend,
|
|
290
293
|
extraEntries: screen ? [screen.entry] : undefined, entryOverride: screen?.entryPath,
|
|
291
294
|
})
|
|
292
295
|
// The host writes PNGs but never creates folders — make every output dir up front.
|
|
@@ -305,6 +308,7 @@ const renderDesktop = async (flags: RenderFlags, file: string | undefined): Prom
|
|
|
305
308
|
renderer: desktopRenderer, host: sim ? "server" : "desktop", pluginDirs,
|
|
306
309
|
width, height, frames: plan.frameCap, outPng: pngFlag, logs, timeoutMs, script: plan.text, fixedDtMs,
|
|
307
310
|
})
|
|
311
|
+
await backend.close()
|
|
308
312
|
const result = evaluateDesktopRun(plan, output)
|
|
309
313
|
if (!result.loaded) {
|
|
310
314
|
throw new CliError("This desktop host doesn't support scripted input (CREATOR_SCRIPT) — it ran the frames and ignored the steps. Update it: lecodes desktop update (or point LECODES_DESKTOP_EXE at a newer build).")
|
|
@@ -326,7 +330,7 @@ const renderDesktop = async (flags: RenderFlags, file: string | undefined): Prom
|
|
|
326
330
|
await runDesktopRender(js, resources, {
|
|
327
331
|
renderer: desktopRenderer, pluginDirs,
|
|
328
332
|
width, height, frames, outPng: pngFlag, logs, timeoutMs, fixedDtMs, camera, view,
|
|
329
|
-
})
|
|
333
|
+
}).finally(() => backend.close())
|
|
330
334
|
note(`Wrote ${pngFlag} (${width}x${height}, native render${view ? `, ${view.split(";")[0]} view` : camera ? ", custom camera" : ""})`)
|
|
331
335
|
}
|
|
332
336
|
|
package/src/commands/test.ts
CHANGED
|
@@ -4,11 +4,12 @@ import { compileHeadlessBundle } from "../compile/headlessBundle"
|
|
|
4
4
|
import { desktopDefaultViewport, desktopPluginLibraries, resolveDesktopRenderer } from "./app/shared"
|
|
5
5
|
import { desktopRenderFlags, fileScenarioIO, requireRenderer, resolveFixtures, resolveSafeAreaChoice, viewportFlags } from "./render"
|
|
6
6
|
import { buildBundle } from "./compile"
|
|
7
|
+
import { runBackend } from "../dev/projectBackend"
|
|
7
8
|
import { findProjectRoot } from "../project/manifest"
|
|
8
9
|
import { runDesktopRender } from "../hosts/desktopRenderer"
|
|
9
10
|
import { evaluateDesktopRun, translateScenario } from "../hosts/desktopScript"
|
|
10
11
|
import { startLiteSession, type LiteSession } from "../hosts/liteRunner"
|
|
11
|
-
import { resolveLiteRunner } from "../dev/webRunner"
|
|
12
|
+
import { resolveLiteRunner, resolveWebRunner } from "../dev/webRunner"
|
|
12
13
|
import { readManifest, type Manifest } from "../project/manifest"
|
|
13
14
|
import { CliError, bool, c, defineCommand, log, logErr, note, num, str, warnErr, type FlagValues } from "../cli"
|
|
14
15
|
import { bundleFlags, bundleOptions } from "./shared"
|
|
@@ -157,6 +158,7 @@ const testFlags = {
|
|
|
157
158
|
...bundleFlags,
|
|
158
159
|
...desktopRenderFlags,
|
|
159
160
|
lite: bool("run the file scenarios on Web Lite, in a browser (needs Playwright): the same steps, targets and expects; the clock is the browser's, so a step that expects a log line waits for it; design flows stay headless"),
|
|
161
|
+
web: bool("run the file scenarios on the web player, in a browser (needs Playwright): as --lite, on the runtime as wasm; no `fill` step (tap a field and `type`)"),
|
|
160
162
|
desktop: bool("run the file scenarios through the NATIVE host: the same steps, targets and expects as headless (expect.audio and the json output excepted); design flows stay headless. --frames / --fixed-dt as in render --desktop"),
|
|
161
163
|
}
|
|
162
164
|
// (`sim` comes with desktopRenderFlags: the file scenarios through the WINDOWLESS host — no GPU, no
|
|
@@ -168,7 +170,7 @@ export default defineCommand({
|
|
|
168
170
|
name: "test",
|
|
169
171
|
summary: "Run the flow tests (tests/*.flow.json + design-derived flows)",
|
|
170
172
|
usage: "[name…]",
|
|
171
|
-
description: "Headless flow tests: tests/*.flow.json scenarios, plus flows derived from the design graph (design/meta.json activator edges — checks the built app against the designed navigation; needs named elements + screen roots). Failing steps dump the frame (JSON + PNG) under tests/.artifacts/<flow
|
|
173
|
+
description: "Headless flow tests: tests/*.flow.json scenarios, plus flows derived from the design graph (design/meta.json activator edges — checks the built app against the designed navigation; needs named elements + screen roots). Failing steps dump the frame (JSON + PNG) under tests/.artifacts/<flow>/. On --desktop / --lite / --web a project with *.server.ts files gets its backend run for the tests, on a database of its own (.lecodes/test-db) that is emptied before every case. The headless renderer has no backend, and --sim does not wait for one: its frames are not tied to real time.\n\nDesign flows read tests/fixtures.json automatically (canned fetch responses) so backend-gated transitions run headless; file scenarios declare their own via the \"fixtures\" envelope. [name…] runs only those files (a path, or a name under the tests folder).",
|
|
172
174
|
flags: testFlags,
|
|
173
175
|
examples: ["lecodes test", "lecodes test hero --logs", "lecodes test --flows --json", "lecodes test --desktop"],
|
|
174
176
|
run: ({ args, flags }) => runTests(args, flags),
|
|
@@ -177,9 +179,12 @@ export default defineCommand({
|
|
|
177
179
|
const runTests = async (args: string[], flags: TestFlags): Promise<void> => {
|
|
178
180
|
// --sim is the native path on the windowless host: everything --desktop does, minus pixels.
|
|
179
181
|
const sim = flags.sim
|
|
180
|
-
|
|
181
|
-
if (lite &&
|
|
182
|
-
|
|
182
|
+
// --lite / --web: a browser run, on Web Lite or on the web player — one runner, two pages.
|
|
183
|
+
if (flags.lite && flags.web) throw new CliError("--lite and --web are two hosts — pick one.")
|
|
184
|
+
const lite = flags.lite || flags.web
|
|
185
|
+
const browserFlag = flags.web ? "--web" : "--lite"
|
|
186
|
+
if (lite && (flags.desktop || sim)) throw new CliError(`${browserFlag} is a host of its own — drop --desktop / --sim.`)
|
|
187
|
+
if (lite && flags.flows) throw new CliError(`--flows needs the headless renderer (design flows read expect.screen and fill inputs) — drop ${browserFlag}.`)
|
|
183
188
|
const desktop = flags.desktop || sim
|
|
184
189
|
if (desktop && flags.flows) throw new CliError(`--flows needs the headless renderer (design flows read expect.screen and fill inputs) — drop --${sim ? "sim" : "desktop"}.`)
|
|
185
190
|
// --desktop: ONE renderer decision (host binary + the staged .filamat variant). The root is
|
|
@@ -187,7 +192,9 @@ const runTests = async (args: string[], flags: TestFlags): Promise<void> => {
|
|
|
187
192
|
let desktopRoot: string
|
|
188
193
|
try { desktopRoot = findProjectRoot(process.cwd()) } catch { desktopRoot = process.cwd() }
|
|
189
194
|
const desktopRenderer = desktop ? resolveDesktopRenderer(desktopRoot, flags.renderer) : undefined
|
|
190
|
-
|
|
195
|
+
// The project's backend, when it has one: on a database of its own, emptied before every case.
|
|
196
|
+
const backend = desktop ? runBackend({ root: desktopRoot, command: "lecodes test", test: true, log: (msg) => { if (flags.logs || msg.includes("[server:error]")) logErr(msg) } }) : null
|
|
197
|
+
const native = desktop ? await buildBundle(bundleOptions(flags), { localAssets: true, desktopRenderer, backend: backend! }) : null
|
|
191
198
|
const headless = desktop || lite ? null : await compileHeadlessBundle({ entry: flags.entry, publicUrl: flags["public-url"], remoteAssets: flags["remote-assets"], waitNetwork: flags["wait-network"] })
|
|
192
199
|
const warnings = headless?.warnings ?? []
|
|
193
200
|
const useLocalAssets = headless?.useLocalAssets ?? false
|
|
@@ -338,11 +345,17 @@ const runTests = async (args: string[], flags: TestFlags): Promise<void> => {
|
|
|
338
345
|
// Web Lite: one compile, one browser; a page of its own per case.
|
|
339
346
|
let liteSession: LiteSession | null = null
|
|
340
347
|
if (lite) {
|
|
341
|
-
const page = resolveLiteRunner(note)
|
|
342
|
-
if (!page) throw new CliError("No Web Lite page found: this CLI has no runtime/web-lite (a monorepo checkout needs `bun run build` in hosts/web-lite).")
|
|
343
348
|
let manifest: Manifest | null = null
|
|
344
349
|
try { manifest = readManifest(root) } catch { /* a folder standing alone */ }
|
|
345
|
-
|
|
350
|
+
if (flags.web) {
|
|
351
|
+
const web = resolveWebRunner(note)
|
|
352
|
+
if (!web) throw new CliError("No web player found: this CLI has no runtime/web (a monorepo checkout needs `bun run build` in hosts/web-player).")
|
|
353
|
+
liteSession = await startLiteSession({ root, manifest, name: manifest?.name ?? "app", web, entry: flags.entry })
|
|
354
|
+
} else {
|
|
355
|
+
const page = resolveLiteRunner(note)
|
|
356
|
+
if (!page) throw new CliError("No Web Lite page found: this CLI has no runtime/web-lite (a monorepo checkout needs `bun run build` in hosts/web-lite).")
|
|
357
|
+
liteSession = await startLiteSession({ root, manifest, name: manifest?.name ?? "app", lite: page, entry: flags.entry })
|
|
358
|
+
}
|
|
346
359
|
}
|
|
347
360
|
const runLiteCase = async (kase: FlowCase): Promise<CaseResult> => {
|
|
348
361
|
const caseDir = resolve(artifactsRoot, slug(kase.name))
|
|
@@ -364,6 +377,8 @@ const runTests = async (args: string[], flags: TestFlags): Promise<void> => {
|
|
|
364
377
|
|
|
365
378
|
for (const kase of cases) {
|
|
366
379
|
let result: CaseResult
|
|
380
|
+
// every case starts on an empty database, like it starts in a fresh host
|
|
381
|
+
if (backend && results.length > 0) await backend.reset()
|
|
367
382
|
if (lite) {
|
|
368
383
|
try {
|
|
369
384
|
result = await runLiteCase(kase)
|
|
@@ -446,6 +461,7 @@ const runTests = async (args: string[], flags: TestFlags): Promise<void> => {
|
|
|
446
461
|
}
|
|
447
462
|
|
|
448
463
|
await liteSession?.close()
|
|
464
|
+
await backend?.close()
|
|
449
465
|
clearTimeout(killer)
|
|
450
466
|
process.exit(failed > 0 ? 1 : 0)
|
|
451
467
|
}
|
|
@@ -7,6 +7,7 @@ import { CliError, bool, c, defineCommand, log, note, readFlags, type Command }
|
|
|
7
7
|
import { OPTIONAL_PEERS, peerSource, peerVersion } from "../../hosts/peers"
|
|
8
8
|
import { installPeers } from "../../hosts/peerInstall"
|
|
9
9
|
import { archiveSpec, archivedVersion } from "../../hosts/archives"
|
|
10
|
+
import { commandLine, installGlobalArgv, ownManager } from "../../hosts/packageManager"
|
|
10
11
|
import { updateDesktop } from "../../hosts/desktopRenderer"
|
|
11
12
|
import { updateMatc } from "../../hosts/matcTool"
|
|
12
13
|
import { refreshTypes } from "./types"
|
|
@@ -94,7 +95,7 @@ const STANDALONE = existsSync(join(dirname(process.execPath), "runtime")) &&
|
|
|
94
95
|
export default defineCommand({
|
|
95
96
|
name: "update",
|
|
96
97
|
summary: "Move everything the project depends on to its newest published state",
|
|
97
|
-
description: "In dependency order: the desktop host (`desktop update`), matc (`shaders update`), the project's shader compiles, the IDE types (.lecodes/types/ + tsconfig.json, from the server — the former `lecodes types`), the ios/android shell pins (`app update`), then the GLOBAL
|
|
98
|
+
description: "In dependency order: the desktop host (`desktop update`), matc (`shaders update`), the project's shader compiles, the IDE types (.lecodes/types/ + tsconfig.json, from the server — the former `lecodes types`), the ios/android shell pins (`app update`), then the GLOBAL installs: every installed optional peer (lecodes-headless, lecodes-design, lecodes-3d-editor, lecodes-assets) and this CLI that is behind npm's latest is installed again, with the package manager the CLI came by (`npm install -g`, or `bun add -g` for a CLI installed with bun).\n\nPinned tools are skipped, not overridden: LECODES_DESKTOP_EXE / LECODES_MATC mean a local build. Keeps going when a step fails; the summary says which.",
|
|
98
99
|
flags: {
|
|
99
100
|
local: bool("types from the SDK bundled in this CLI"),
|
|
100
101
|
"no-shells": bool("leave ios/android pins alone (no re-sync)"),
|
|
@@ -159,8 +160,8 @@ export default defineCommand({
|
|
|
159
160
|
return "ok"
|
|
160
161
|
})
|
|
161
162
|
|
|
162
|
-
// 6. this CLI + its optional peers — the GLOBAL
|
|
163
|
-
//
|
|
163
|
+
// 6. this CLI + its optional peers — the GLOBAL installs (`npm install -g` or `bun install -g`,
|
|
164
|
+
// whichever this CLI came by: hosts/packageManager.ts): every installed one that is behind npm's latest moves forward. The peers go
|
|
164
165
|
// first, the CLI itself last — replacing the running CLI's files is fine (this process already
|
|
165
166
|
// holds its code), but its new version is what the next invocation runs, so nothing after it here.
|
|
166
167
|
// Peers that are NOT installed stay that way: they're optional, installing one is a choice.
|
|
@@ -195,7 +196,7 @@ export default defineCommand({
|
|
|
195
196
|
const prefixBehind = behind.filter((r) => r.where === "prefix")
|
|
196
197
|
const globalSpecs = await Promise.all(behind.filter((r) => r.where !== "prefix").map(async (r) => await archiveSpec(r.name) ?? r.name + "@latest"))
|
|
197
198
|
if (flags.check) {
|
|
198
|
-
return ["updated", behind.length + " behind — run: lecodes update" + (globalSpecs.length ? " (global:
|
|
199
|
+
return ["updated", behind.length + " behind — run: lecodes update" + (globalSpecs.length ? " (global: " + commandLine(ownManager(), installGlobalArgv(ownManager(), globalSpecs)) + ")" : "")]
|
|
199
200
|
}
|
|
200
201
|
const done: string[] = []
|
|
201
202
|
if (prefixBehind.length) {
|
|
@@ -207,9 +208,13 @@ export default defineCommand({
|
|
|
207
208
|
}
|
|
208
209
|
const toInstall = STANDALONE ? globalSpecs.filter((s) => !s.startsWith("lecodes-cli@")) : globalSpecs
|
|
209
210
|
if (toInstall.length) {
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
211
|
+
// with the manager this CLI was installed by: another one's global dir is another copy
|
|
212
|
+
const tool = ownManager()
|
|
213
|
+
const argv = installGlobalArgv(tool, toInstall)
|
|
214
|
+
log(" " + c.dim("$") + " " + commandLine(tool, argv))
|
|
215
|
+
const r = spawnSync(tool, argv, { stdio: "inherit", shell: true })
|
|
216
|
+
if (r.error) throw new CliError(tool + " could not run: " + r.error.message)
|
|
217
|
+
if (r.status !== 0) throw new CliError(commandLine(tool, argv.slice(0, 2)) + " exited with " + (r.status ?? "a signal"))
|
|
213
218
|
done.push(...behind.filter((r) => r.where !== "prefix" && (!STANDALONE || r.name !== "lecodes-cli")).map((r) => r.name))
|
|
214
219
|
}
|
|
215
220
|
if (done.length === 0) return ["skipped", "only the standalone CLI itself is behind — download the release"]
|
|
@@ -87,7 +87,7 @@ export const refreshTypes = async (opts: RefreshTypesOptions): Promise<void> =>
|
|
|
87
87
|
// The compiler uses the SDK vendored in this CLI, not these declarations — worth saying when the
|
|
88
88
|
// server's surface is a different one, since that gap shows up as "autocompletes but won't build".
|
|
89
89
|
if (!opts.local && local && bundleHash(local) !== bundleHash(bundle)) {
|
|
90
|
-
warn("The server's SDK surface differs from the one this CLI compiles with — if something autocompletes but fails to compile, update the CLI (`
|
|
90
|
+
warn("The server's SDK surface differs from the one this CLI compiles with — if something autocompletes but fails to compile, update the CLI (`lecodes update`) or re-run with --local.")
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
log("")
|
|
@@ -3,7 +3,12 @@ import { createRequire } from "node:module"
|
|
|
3
3
|
import { dirname, join } from "node:path"
|
|
4
4
|
import type { FileKind } from "../project/manifest"
|
|
5
5
|
import { distRoot } from "../hosts/distRoot"
|
|
6
|
-
import {
|
|
6
|
+
import { installPeers } from "../hosts/peerInstall"
|
|
7
|
+
import { peerDirInPrefix } from "../hosts/peers"
|
|
8
|
+
import {
|
|
9
|
+
compileProject as sdkCompileProject, compileServerBundle, detectEntry, isServerPath, scanModulesChisel, localAssetNames, ENTRY_CONVENTION,
|
|
10
|
+
type CompileServerOptions,
|
|
11
|
+
} from "lecodes-sdk/compile"
|
|
7
12
|
|
|
8
13
|
// Every project compiles through the SDK's one import-free compiler, which bundles with chisel (the
|
|
9
14
|
// Rust method-granular bundler, @letary/chisel) — the only bundler. The SDK resolves the chisel
|
|
@@ -84,6 +89,9 @@ export type CompileOptions = {
|
|
|
84
89
|
/** What the compile has to say that is not an error (a plugin the app uses and app.json does not
|
|
85
90
|
* list). */
|
|
86
91
|
onWarning?: (message: string) => void
|
|
92
|
+
/** Base URL of the project's backend, baked into the stubs of its `*.server.ts` modules; absent =
|
|
93
|
+
* every call to the server rejects (status 0). */
|
|
94
|
+
serverUrl?: string
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
/** A warning when auto-detection has several import-graph roots and no main.ts to settle it —
|
|
@@ -110,6 +118,45 @@ export const compileProject = (opts: CompileOptions): Promise<string> =>
|
|
|
110
118
|
sourcemap: opts.sourcemap,
|
|
111
119
|
editor: opts.editor,
|
|
112
120
|
onWarning: opts.onWarning,
|
|
121
|
+
serverUrl: opts.serverUrl,
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
/** Does the project have a server side (`*.server.ts` files)? */
|
|
125
|
+
export const hasServerFiles = (entries: CompileEntry[]): boolean =>
|
|
126
|
+
entries.some((e) => e.type === "text" && isServerPath(e.path))
|
|
127
|
+
|
|
128
|
+
type Typescript = NonNullable<CompileServerOptions["typescript"]>
|
|
129
|
+
|
|
130
|
+
/** TypeScript with its lib files on disk — the server code is checked, and the endpoint validators
|
|
131
|
+
* derived, by its checker. The project's own, else one this CLI's install can see, else the CLI's
|
|
132
|
+
* peer (installed on first use, like the database engine). One WITHOUT the compiler API is passed
|
|
133
|
+
* over: TypeScript 7 — what `install typescript` brings today — is the native compiler, with no
|
|
134
|
+
* `createProgram` to call. */
|
|
135
|
+
const findTypescript = async (root: string): Promise<Typescript> => {
|
|
136
|
+
const from = (at: string): Typescript | null => {
|
|
137
|
+
try {
|
|
138
|
+
const mod = createRequire(at)("typescript")
|
|
139
|
+
return mod?.createProgram ? mod : mod?.default?.createProgram ? mod.default : null
|
|
140
|
+
} catch { return null }
|
|
141
|
+
}
|
|
142
|
+
const prefix = join(peerDirInPrefix("typescript"), "package.json")
|
|
143
|
+
const found = from(join(root, "package.json")) ?? from(import.meta.url) ?? from(prefix)
|
|
144
|
+
if (found) return found
|
|
145
|
+
await installPeers(["typescript"], "The project's server code (*.server.ts)")
|
|
146
|
+
return from(prefix) ?? (() => { throw new Error(`The typescript installed in ${peerDirInPrefix("typescript")} has no compiler API`) })()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The project's server bundle (its `*.server.ts` files and what they import), or null when it has
|
|
151
|
+
* none. `app` = app.json's name (the sign-in mail). Always checked with TypeScript, the way the
|
|
152
|
+
* le.codes build checks it: an endpoint that is not async, an unawaited `requireUser()` are errors
|
|
153
|
+
* only the checker finds.
|
|
154
|
+
*/
|
|
155
|
+
export const compileServer = async (opts: { entries: CompileEntry[], root: string, app?: CompileServerOptions["app"] }) =>
|
|
156
|
+
compileServerBundle({
|
|
157
|
+
entries: opts.entries.map((e) => ({ path: e.path, type: e.type as any, text: e.text })),
|
|
158
|
+
app: opts.app,
|
|
159
|
+
typescript: await findTypescript(opts.root),
|
|
113
160
|
})
|
|
114
161
|
|
|
115
162
|
/** Re-exported so the CALLERS that stage files next to the bundle name them the way the bundle asks
|
package/src/dev/androidDev.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { CliError, note, warnErr } from "../cli"
|
|
|
12
12
|
* app's VIEW filter is pinned to le.codes/qr, and an explicit start bypasses filter matching — with
|
|
13
13
|
* the plain dev.js url: the app's initial.js only fetches a url that ends in ".js", anything else is
|
|
14
14
|
* sent to le.codes/api/code, which answers 400 for a dev bundle (the "Произошла ошибка при открытии
|
|
15
|
-
* проекта" screen).
|
|
15
|
+
* проекта" screen).
|
|
16
16
|
*
|
|
17
17
|
* A device run has no frame cap (unlike the desktop host), so every exit path force-stops the app:
|
|
18
18
|
* a 3D scene left rendering is a sustained GPU load on a charging phone.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, renameSync, rmSync } from "node:fs"
|
|
2
|
+
import { tmpdir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { format } from "node:util"
|
|
5
|
+
import {
|
|
6
|
+
createAuthHost, createInvoker, createSubscribeGuard, describeServer, loadServerModules,
|
|
7
|
+
setChannelPublisher, setDbTransport, type AuthHost, type LoadedServer,
|
|
8
|
+
} from "lecodes-sdk/server"
|
|
9
|
+
import { loadPeer } from "../hosts/peerInstall"
|
|
10
|
+
import type { FromWorker, LogLevel, ToWorker } from "./localBackend"
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* The process a project's server bundle runs in under `lecodes dev` — the local twin of the runner's
|
|
14
|
+
* worker (tools/runner/src/worker.ts), started by localBackend.ts as this same CLI with
|
|
15
|
+
* BACKEND_WORKER_ARG and spoken to over the IPC channel. One process per loaded bundle: a reload is
|
|
16
|
+
* a new process, so nothing of the old code (its timers, its module state) outlives it.
|
|
17
|
+
*
|
|
18
|
+
* The database is marcidb's embedded engine (the peer marcidb-embedded), opened here, in the
|
|
19
|
+
* directory the parent names. The schema is synced on every load, the way the runner syncs a draft
|
|
20
|
+
* slot: what the models no longer have is dropped; a change the engine can't migrate in place moves
|
|
21
|
+
* the old database aside and starts an empty one.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
type Embedded = {
|
|
25
|
+
exec(op: unknown): Promise<any>
|
|
26
|
+
batch(ops: unknown[]): Promise<any[]>
|
|
27
|
+
$sync(schema: string): Promise<null>
|
|
28
|
+
close(): void
|
|
29
|
+
}
|
|
30
|
+
type EmbeddedModule = { openDatabase(dir: string, options?: { disableFsync?: boolean }): Embedded }
|
|
31
|
+
|
|
32
|
+
const post = (m: FromWorker) => { process.send?.(m) }
|
|
33
|
+
|
|
34
|
+
// ── console → the parent (the dev terminal) ──
|
|
35
|
+
const hook = (level: LogLevel) => (...args: unknown[]) => post({ t: "log", level, text: format(...args) })
|
|
36
|
+
console.log = hook("log"); console.info = hook("info"); console.warn = hook("warn"); console.error = hook("error"); console.debug = hook("debug")
|
|
37
|
+
process.on("unhandledRejection", (e) => post({ t: "log", level: "error", text: `Unhandled rejection: ${(e as Error)?.stack ?? e}` }))
|
|
38
|
+
process.on("uncaughtException", (e) => post({ t: "log", level: "error", text: `Uncaught exception: ${(e as Error)?.stack ?? e}` }))
|
|
39
|
+
|
|
40
|
+
/** The code `db.auth.signInWithCode` takes for any email under `lecodes dev` (no `sendCode` needed). */
|
|
41
|
+
const DEV_CODE = "000000"
|
|
42
|
+
|
|
43
|
+
let db: Embedded | null = null
|
|
44
|
+
let invoke: ReturnType<typeof createInvoker> | null = null
|
|
45
|
+
let subscribe: ReturnType<typeof createSubscribeGuard> | null = null
|
|
46
|
+
let authHost: AuthHost | null = null
|
|
47
|
+
|
|
48
|
+
const quit = () => {
|
|
49
|
+
try { db?.close() } catch { /* already closed */ }
|
|
50
|
+
process.exit(0)
|
|
51
|
+
}
|
|
52
|
+
// the dev server went away (Ctrl+C, a crash): nothing is left to serve
|
|
53
|
+
process.on("disconnect", quit)
|
|
54
|
+
// …and when it was killed outright the channel may never say so: a process left behind keeps the
|
|
55
|
+
// database locked, and the next `lecodes dev` of the project could not open it.
|
|
56
|
+
const parent = process.ppid
|
|
57
|
+
setInterval(() => { try { process.kill(parent, 0) } catch { quit() } }, 1000).unref?.()
|
|
58
|
+
process.on("SIGINT", quit)
|
|
59
|
+
process.on("SIGTERM", quit)
|
|
60
|
+
|
|
61
|
+
/** Open the project's database and bring it to `schema`. Returns what the developer should be told. */
|
|
62
|
+
const openDb = async (dir: string, schema: string): Promise<string[]> => {
|
|
63
|
+
const { openDatabase } = await loadPeer<EmbeddedModule>("marcidb-embedded", undefined, { for: "lecodes dev", install: false })
|
|
64
|
+
?? (() => { throw new Error("marcidb-embedded is not installed") })()
|
|
65
|
+
mkdirSync(dir, { recursive: true })
|
|
66
|
+
try {
|
|
67
|
+
db = openDatabase(dir)
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// one process holds a database: the engine's lock is the sign of a second server on this project
|
|
70
|
+
if (/Locked/i.test((e as Error).message)) throw new Error(`The project's database (${dir}) is in use — this project's backend is already running (another \`lecodes dev\` or \`lecodes desktop run\`). Stop it first.`)
|
|
71
|
+
throw e
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await db.$sync(schema)
|
|
75
|
+
return []
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if ((e as { kind?: string }).kind !== "bad_request") throw e
|
|
78
|
+
const reason = (e as Error).message
|
|
79
|
+
// Not migratable in place — or not a schema at all. An empty database tells the two apart, so a
|
|
80
|
+
// broken schema never costs the data.
|
|
81
|
+
const probeDir = mkdtempSync(join(tmpdir(), "lecodes-db-"))
|
|
82
|
+
const probe = openDatabase(probeDir, { disableFsync: true })
|
|
83
|
+
try { await probe.$sync(schema) } finally { probe.close(); rmSync(probeDir, { recursive: true, force: true }) }
|
|
84
|
+
db.close()
|
|
85
|
+
const aside = `${dir}-old`
|
|
86
|
+
rmSync(aside, { recursive: true, force: true })
|
|
87
|
+
renameSync(dir, aside)
|
|
88
|
+
mkdirSync(dir, { recursive: true })
|
|
89
|
+
db = openDatabase(dir)
|
|
90
|
+
await db.$sync(schema)
|
|
91
|
+
return [`database started over — the schema change couldn't be migrated (${reason}); the previous data is in ${aside}`]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const load = async (m: Extract<ToWorker, { t: "load" }>) => {
|
|
96
|
+
const noDb = () => { throw new Error("db: the database is not open yet — query it inside an endpoint, not while the module loads") }
|
|
97
|
+
setDbTransport({ exec: (op) => (db ?? noDb()).exec(op), batch: (ops) => (db ?? noDb()).batch(ops) })
|
|
98
|
+
setChannelPublisher((ch, group, data) => post({ t: "publish", ch, group, data }))
|
|
99
|
+
// The bundle is a plain script (chisel esm output with only side effects): registers globalThis.__lecodesServer
|
|
100
|
+
;(0, eval)(m.code)
|
|
101
|
+
const server: LoadedServer = loadServerModules()
|
|
102
|
+
const describe = describeServer(server)
|
|
103
|
+
const notes = describe.marci ? await openDb(m.dbDir, describe.marci) : []
|
|
104
|
+
// Sessions, passwords and email codes over the project's own db, as on the runner. A code that was
|
|
105
|
+
// asked for is printed here, and DEV_CODE signs any email in without one — the developer is the
|
|
106
|
+
// only user.
|
|
107
|
+
authHost = createAuthHost({
|
|
108
|
+
db: server.dbs.find(d => d.$auth) ?? null,
|
|
109
|
+
appName: server.manifest.app?.name,
|
|
110
|
+
echoCodes: true,
|
|
111
|
+
devCode: DEV_CODE,
|
|
112
|
+
log: (level, text) => post({ t: "log", level, text }),
|
|
113
|
+
})
|
|
114
|
+
authHost.install()
|
|
115
|
+
const auth = authHost.enabled ? authHost : null
|
|
116
|
+
const onError = (id: string, e: unknown) => post({ t: "log", level: "error", text: `${id}: ${(e as Error)?.stack ?? e}` })
|
|
117
|
+
invoke = createInvoker(server, { auth, onError })
|
|
118
|
+
subscribe = createSubscribeGuard(server, { auth, onError })
|
|
119
|
+
post({ t: "loaded", describe, notes })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
process.on("message", async (m: ToWorker) => {
|
|
123
|
+
switch (m.t) {
|
|
124
|
+
case "load":
|
|
125
|
+
try { await load(m) } catch (e) { post({ t: "loadError", message: (e as Error)?.stack ?? String(e) }) }
|
|
126
|
+
return
|
|
127
|
+
case "invoke": {
|
|
128
|
+
const result = invoke ? await invoke(m.id, m.args, m.ctx) : { ok: false as const, status: 503, message: "server not loaded" }
|
|
129
|
+
return post({ t: "result", req: m.req, result })
|
|
130
|
+
}
|
|
131
|
+
case "subscribe": {
|
|
132
|
+
const result = subscribe ? await subscribe(m.ch, m.args, m.ctx) : { ok: false as const, status: 503, message: "server not loaded" }
|
|
133
|
+
return post({ t: "result", req: m.req, result })
|
|
134
|
+
}
|
|
135
|
+
case "stop":
|
|
136
|
+
return quit()
|
|
137
|
+
}
|
|
138
|
+
})
|