lecodes-cli 0.19.0 → 0.19.2
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 +730 -235
- package/package.json +14 -13
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk-types.json +1 -1
- package/src/api.ts +302 -0
- package/src/browserAuth.ts +87 -0
- package/src/cmgenTool.ts +104 -0
- package/src/commands/app.ts +891 -0
- package/src/commands/appAndroid.ts +603 -0
- package/src/commands/appDesktop.ts +326 -0
- package/src/commands/appDesktopMac.ts +470 -0
- package/src/commands/appIcon.ts +187 -0
- package/src/commands/appShared.ts +448 -0
- package/src/commands/appTemplates.ts +883 -0
- package/src/commands/appTemplatesAndroid.ts +599 -0
- package/src/commands/appTemplatesGradlew.ts +9 -0
- package/src/commands/assets.ts +28 -0
- package/src/commands/clone.ts +60 -0
- package/src/commands/compile.ts +253 -0
- package/src/commands/create.ts +59 -0
- package/src/commands/design.ts +863 -0
- package/src/commands/designTemplates.ts +11 -0
- package/src/commands/desktop.ts +122 -0
- package/src/commands/dev.ts +214 -0
- package/src/commands/diff.ts +59 -0
- package/src/commands/init.ts +191 -0
- package/src/commands/install.ts +198 -0
- package/src/commands/lightmap.ts +290 -0
- package/src/commands/link.ts +147 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/navmesh.ts +225 -0
- package/src/commands/pn.ts +305 -0
- package/src/commands/projectTemplates.ts +129 -0
- package/src/commands/pull.ts +109 -0
- package/src/commands/push.ts +173 -0
- package/src/commands/render.ts +414 -0
- package/src/commands/scene.ts +148 -0
- package/src/commands/shaders.ts +157 -0
- package/src/commands/status.ts +29 -0
- package/src/commands/test.ts +370 -0
- package/src/commands/thumbs.ts +178 -0
- package/src/commands/types.ts +87 -0
- package/src/commands/update.ts +190 -0
- package/src/compile/assetIcons.ts +214 -0
- package/src/compile/collect.ts +46 -0
- package/src/compile/collectLocal.ts +37 -0
- package/src/compile/designCompile.ts +104 -0
- package/src/compile/fonts.ts +156 -0
- package/src/compile/headlessBundle.ts +129 -0
- package/src/compile/nativeStack.ts +21 -0
- package/src/compile/projectCompile.ts +109 -0
- package/src/compile/sceneCompile.ts +113 -0
- package/src/compile/screenEntry.ts +127 -0
- package/src/compile/shaders.ts +245 -0
- package/src/config.ts +42 -0
- package/src/designMeta.ts +35 -0
- package/src/desktopRenderer.ts +532 -0
- package/src/desktopScript.ts +276 -0
- package/src/dev/clientTemplates.ts +160 -0
- package/src/dev/devServer.ts +292 -0
- package/src/dev/wsServer.ts +144 -0
- package/src/distRoot.ts +20 -0
- package/src/ignore.ts +163 -0
- package/src/index.ts +491 -0
- package/src/lecodes-3d-editor.d.ts +41 -0
- package/src/lecodes-assets.d.ts +7 -0
- package/src/lecodes-design.d.ts +191 -0
- package/src/lecodes-renderer.d.ts +131 -0
- package/src/localFiles.ts +144 -0
- package/src/manifest.ts +46 -0
- package/src/matcTool.ts +137 -0
- package/src/peers.ts +40 -0
- package/src/project.ts +61 -0
- package/src/projectEnv.ts +94 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/releases.ts +125 -0
- package/src/serverDiff.ts +78 -0
- package/src/textDiff.ts +103 -0
- package/src/types.ts +0 -0
- package/src/util.ts +146 -0
- package/runtime/sdk/animate/animate.ts +0 -238
- package/runtime/sdk/animate/bezier.ts +0 -138
- package/runtime/sdk/animate/easings.ts +0 -126
- package/runtime/sdk/canvas/Canvas.ts +0 -305
- package/runtime/sdk/compile/aspectMacro.ts +0 -86
- package/runtime/sdk/compile/assetIconMacro.ts +0 -384
- package/runtime/sdk/compile/assetMacro.ts +0 -146
- package/runtime/sdk/compile/assetName.ts +0 -50
- package/runtime/sdk/compile/bundler.ts +0 -287
- package/runtime/sdk/compile/compileProject.ts +0 -129
- package/runtime/sdk/compile/detectEntry.ts +0 -128
- package/runtime/sdk/compile/fontMacro.ts +0 -459
- package/runtime/sdk/compile/fontRegistry.ts +0 -78
- package/runtime/sdk/compile/header.ts +0 -67
- package/runtime/sdk/compile/index.ts +0 -100
- package/runtime/sdk/compile/libraryImports.ts +0 -52
- package/runtime/sdk/compile/liteMaterial.ts +0 -247
- package/runtime/sdk/compile/sceneEditor.ts +0 -88
- package/runtime/sdk/compile/serverSplit.ts +0 -233
- package/runtime/sdk/compile/serverTypes.ts +0 -227
- package/runtime/sdk/compile/sfnt.ts +0 -98
- package/runtime/sdk/compile/shaderSchema.ts +0 -202
- package/runtime/sdk/compile/shaderTargets.ts +0 -81
- package/runtime/sdk/compile/sourcemap.ts +0 -25
- package/runtime/sdk/core/Aspect.ts +0 -568
- package/runtime/sdk/core/InspectorUI.ts +0 -212
- package/runtime/sdk/core/StateMachine.ts +0 -308
- package/runtime/sdk/core/color.ts +0 -66
- package/runtime/sdk/core/compWrite.ts +0 -42
- package/runtime/sdk/core/events.ts +0 -38
- package/runtime/sdk/core/fields.ts +0 -120
- package/runtime/sdk/core/registry.ts +0 -23
- package/runtime/sdk/core/signals.ts +0 -277
- package/runtime/sdk/core/time.ts +0 -84
- package/runtime/sdk/g2/Camera2D.ts +0 -40
- package/runtime/sdk/g2/CharacterController2D.ts +0 -276
- package/runtime/sdk/g2/Node2D.ts +0 -267
- package/runtime/sdk/g2/OneWay2D.ts +0 -66
- package/runtime/sdk/g2/Physics2D.ts +0 -346
- package/runtime/sdk/g2/Scene2D.ts +0 -209
- package/runtime/sdk/g2/Shape2D.ts +0 -259
- package/runtime/sdk/g2/Sprite.ts +0 -89
- package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
- package/runtime/sdk/g2/SpriteSheet.ts +0 -166
- package/runtime/sdk/g2/Texture2D.ts +0 -47
- package/runtime/sdk/g2/Tilemap.ts +0 -41
- package/runtime/sdk/g2/Tileset.ts +0 -71
- package/runtime/sdk/g2/Trigger2D.ts +0 -77
- package/runtime/sdk/g2/autotile.ts +0 -433
- package/runtime/sdk/g2/cells.ts +0 -91
- package/runtime/sdk/g2/defineScene2d.ts +0 -381
- package/runtime/sdk/g2/groups2d.ts +0 -106
- package/runtime/sdk/g2/loop.ts +0 -50
- package/runtime/sdk/g2/scenarios2d.ts +0 -69
- package/runtime/sdk/g2/touch.ts +0 -83
- package/runtime/sdk/gl/Camera.ts +0 -160
- package/runtime/sdk/gl/CameraPlace.ts +0 -52
- package/runtime/sdk/gl/CharacterController.ts +0 -238
- package/runtime/sdk/gl/Gearbox.ts +0 -212
- package/runtime/sdk/gl/Geometry.ts +0 -279
- package/runtime/sdk/gl/IK.ts +0 -193
- package/runtime/sdk/gl/InstancedMesh.ts +0 -132
- package/runtime/sdk/gl/Light.ts +0 -107
- package/runtime/sdk/gl/Lightmap.ts +0 -256
- package/runtime/sdk/gl/Material.ts +0 -260
- package/runtime/sdk/gl/Mesh.ts +0 -83
- package/runtime/sdk/gl/Model.ts +0 -124
- package/runtime/sdk/gl/NavAgent.ts +0 -337
- package/runtime/sdk/gl/NavMesh.ts +0 -397
- package/runtime/sdk/gl/Node.ts +0 -350
- package/runtime/sdk/gl/Noise.ts +0 -30
- package/runtime/sdk/gl/Particles.ts +0 -676
- package/runtime/sdk/gl/Physics.ts +0 -222
- package/runtime/sdk/gl/Plane.ts +0 -53
- package/runtime/sdk/gl/Ragdoll.ts +0 -270
- package/runtime/sdk/gl/Ray.ts +0 -16
- package/runtime/sdk/gl/Scene.ts +0 -486
- package/runtime/sdk/gl/Shape.ts +0 -409
- package/runtime/sdk/gl/Terrain.ts +0 -1092
- package/runtime/sdk/gl/Texture.ts +0 -63
- package/runtime/sdk/gl/Trigger.ts +0 -45
- package/runtime/sdk/gl/Vehicle.ts +0 -473
- package/runtime/sdk/gl/Wheel.ts +0 -240
- package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
- package/runtime/sdk/gl/animation/Animator.ts +0 -120
- package/runtime/sdk/gl/animation/Layer.ts +0 -31
- package/runtime/sdk/gl/animation/Loop.ts +0 -30
- package/runtime/sdk/gl/animation/Playback.ts +0 -44
- package/runtime/sdk/gl/animation/core.ts +0 -336
- package/runtime/sdk/gl/controls.ts +0 -95
- package/runtime/sdk/gl/physicsEvents.ts +0 -20
- package/runtime/sdk/gl/scenarios.ts +0 -291
- package/runtime/sdk/gl/state.ts +0 -6
- package/runtime/sdk/gl/terrainMesh.ts +0 -219
- package/runtime/sdk/gl/touch.ts +0 -68
- package/runtime/sdk/inject.ts +0 -197
- package/runtime/sdk/math/Mathf.ts +0 -118
- package/runtime/sdk/math/mat4.ts +0 -278
- package/runtime/sdk/math/quat.ts +0 -232
- package/runtime/sdk/math/vec.ts +0 -255
- package/runtime/sdk/net/codec.ts +0 -119
- package/runtime/sdk/net/core.ts +0 -384
- package/runtime/sdk/net/index.ts +0 -181
- package/runtime/sdk/net/replication.ts +0 -622
- package/runtime/sdk/plugins/camera.ts +0 -81
- package/runtime/sdk/plugins/geolocation.ts +0 -123
- package/runtime/sdk/plugins/oauth.ts +0 -61
- package/runtime/sdk/plugins/permission.ts +0 -7
- package/runtime/sdk/plugins/push.ts +0 -132
- package/runtime/sdk/plugins/qr.ts +0 -73
- package/runtime/sdk/plugins/service.ts +0 -47
- package/runtime/sdk/runtime/app.ts +0 -101
- package/runtime/sdk/runtime/appEvents.ts +0 -54
- package/runtime/sdk/runtime/channel.ts +0 -61
- package/runtime/sdk/runtime/clipboard.ts +0 -20
- package/runtime/sdk/runtime/datetime.ts +0 -329
- package/runtime/sdk/runtime/device.ts +0 -293
- package/runtime/sdk/runtime/fetch.ts +0 -77
- package/runtime/sdk/runtime/files.ts +0 -23
- package/runtime/sdk/runtime/input.ts +0 -175
- package/runtime/sdk/runtime/media.ts +0 -111
- package/runtime/sdk/runtime/misc.ts +0 -16
- package/runtime/sdk/runtime/net.ts +0 -36
- package/runtime/sdk/runtime/rpc.ts +0 -218
- package/runtime/sdk/runtime/service.ts +0 -83
- package/runtime/sdk/runtime/share.ts +0 -9
- package/runtime/sdk/runtime/storage.ts +0 -13
- package/runtime/sdk/runtime/touch.ts +0 -76
- package/runtime/sdk/scene/defineScene.ts +0 -1353
- package/runtime/sdk/scene/editorPlugins.ts +0 -110
- package/runtime/sdk/scene/gizmos.ts +0 -148
- package/runtime/sdk/scene/grammar.ts +0 -120
- package/runtime/sdk/scene/material.ts +0 -188
- package/runtime/sdk/server/auth/appConfig.ts +0 -12
- package/runtime/sdk/server/auth/global.ts +0 -80
- package/runtime/sdk/server/auth/host.ts +0 -318
- package/runtime/sdk/server/auth/models.ts +0 -83
- package/runtime/sdk/server/auth/types.ts +0 -50
- package/runtime/sdk/server/channel.ts +0 -56
- package/runtime/sdk/server/context.ts +0 -36
- package/runtime/sdk/server/db/defineDb.ts +0 -237
- package/runtime/sdk/server/db/fields.ts +0 -132
- package/runtime/sdk/server/db/httpTransport.ts +0 -93
- package/runtime/sdk/server/db/index.ts +0 -7
- package/runtime/sdk/server/db/marci/query.ts +0 -412
- package/runtime/sdk/server/db/types.ts +0 -202
- package/runtime/sdk/server/errors.ts +0 -12
- package/runtime/sdk/server/host.ts +0 -74
- package/runtime/sdk/server/inject.ts +0 -13
- package/runtime/sdk/server/runtime.ts +0 -133
- package/runtime/sdk/server/validate.ts +0 -87
- package/runtime/sdk/ui/NativeView.ts +0 -142
- package/runtime/sdk/ui/UI.ts +0 -39
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
- package/runtime/sdk/ui/UIButton.ts +0 -101
- package/runtime/sdk/ui/UIContainer.ts +0 -60
- package/runtime/sdk/ui/UIImage.ts +0 -83
- package/runtime/sdk/ui/UIInput.ts +0 -185
- package/runtime/sdk/ui/UIModal.ts +0 -139
- package/runtime/sdk/ui/UINode.ts +0 -830
- package/runtime/sdk/ui/UIPager.ts +0 -362
- package/runtime/sdk/ui/UIPopover.ts +0 -100
- package/runtime/sdk/ui/UIScreen.ts +0 -123
- package/runtime/sdk/ui/UIScrollable.ts +0 -87
- package/runtime/sdk/ui/UISpacer.ts +0 -14
- package/runtime/sdk/ui/UITabs.ts +0 -236
- package/runtime/sdk/ui/UIText.ts +0 -51
- package/runtime/sdk/ui/UIVideo.ts +0 -88
- package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
- package/runtime/sdk/ui/UIWidget.ts +0 -127
- package/runtime/sdk/ui/fonts.ts +0 -13
- package/runtime/sdk/ui/presentable.ts +0 -117
- package/runtime/sdk/ui/router.ts +0 -132
- package/runtime/sdk/ui/theme.ts +0 -84
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import type { Manifest } from "../manifest"
|
|
5
|
+
import { scanLocal } from "../localFiles"
|
|
6
|
+
import { loadIgnore } from "../ignore"
|
|
7
|
+
import { distRoot } from "../distRoot"
|
|
8
|
+
import { CliError, warnErr } from "../util"
|
|
9
|
+
import { compileProject, type CompileEntry } from "./projectCompile"
|
|
10
|
+
import { applyLocalShaders, shadersCacheDir } from "./shaders"
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* The `lecodes scene` compile closure — the CLI's implementation of the scene-editor host's inject
|
|
14
|
+
* point (the platform backend and the standalone playground are the other two). Reads the project
|
|
15
|
+
* off disk, composes the edit-mode bundle with the SDK's shared `sceneEditorEntries`, and drives it
|
|
16
|
+
* through the CLI's chisel + vendored-SDK pipeline. Deliberately mirrors the playground's in-process
|
|
17
|
+
* closure (scene-editor/src/server/inProcess.ts) field-for-field so every host emits the same
|
|
18
|
+
* bundle — only the file source (disk vs. the folder) and the harness source differ.
|
|
19
|
+
*
|
|
20
|
+
* publicUrl "" + resource fileSrc "/project/<rel>" → the scene server's /project/* mount.
|
|
21
|
+
* minify:false — mangling erases class names the inspector matches aspect schemas by.
|
|
22
|
+
* editor:true — the EDITOR global is `true`; `*.editor.ts` plugin files are bundled in.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Text files handed to the compiler as source; everything else is a binary resource served from
|
|
26
|
+
* the project folder (matches the playground's TEXT_EXT split, not the manifest's kind map). */
|
|
27
|
+
const TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i
|
|
28
|
+
|
|
29
|
+
/** Strip scanLocal's leading-slash key convention ("/city.scene.ts" → "city.scene.ts"). */
|
|
30
|
+
const rel = (key: string) => key.replace(/^\//, "")
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the scene-editor harness sources (packages/projects/src/scene-editor):
|
|
34
|
+
* LECODES_SCENE_HARNESS_DIR env → the in-repo dir (monorepo dev), both via the SDK helper, else
|
|
35
|
+
* the JSON snapshot vendored into the CLI at publish time (runtime/scene-harness.json).
|
|
36
|
+
*/
|
|
37
|
+
const resolveHarness = (loadSceneHarness: (dir?: string) => Record<string, string>): Record<string, string> => {
|
|
38
|
+
const inRepo = join(distRoot, "..", "projects", "src", "scene-editor")
|
|
39
|
+
try {
|
|
40
|
+
return loadSceneHarness(existsSync(inRepo) ? inRepo : undefined)
|
|
41
|
+
} catch {
|
|
42
|
+
const vendored = join(distRoot, "runtime", "scene-harness.json")
|
|
43
|
+
if (existsSync(vendored)) return JSON.parse(readFileSync(vendored, "utf8")) as Record<string, string>
|
|
44
|
+
throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.")
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const compileSceneEditor = async (opts: {
|
|
49
|
+
root: string
|
|
50
|
+
/** Null in standalone mode (a bare folder, no cloned project). */
|
|
51
|
+
manifest: Manifest | null
|
|
52
|
+
/** Project display name (compile metadata only, header is off). */
|
|
53
|
+
name: string
|
|
54
|
+
/** Project path of the scene file as the editor sends it ("./city.scene.ts"). */
|
|
55
|
+
scenePath: string
|
|
56
|
+
}): Promise<{ js: string | null, error?: string }> => {
|
|
57
|
+
const { root, scenePath } = opts
|
|
58
|
+
const { loadSceneHarness, sceneEditorEntries, SCENE_ENTRY_PATH } = await import("lecodes-sdk/compile")
|
|
59
|
+
|
|
60
|
+
const entries: CompileEntry[] = []
|
|
61
|
+
const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(opts.manifest?.files ?? {})) })
|
|
62
|
+
for (const file of scan.files) {
|
|
63
|
+
const path = "./" + rel(file.path)
|
|
64
|
+
if (TEXT_EXT.test(path)) {
|
|
65
|
+
entries.push({ path, type: "text", text: readFileSync(file.absPath, "utf8") })
|
|
66
|
+
} else if (/\.mat$/i.test(path)) {
|
|
67
|
+
// custom shaders compile locally (matc + the lite JSON) into .lecodes/shaders, served through
|
|
68
|
+
// the same /project/* mount — see the fallback below when matc isn't available
|
|
69
|
+
entries.push({ path, type: "shader", text: readFileSync(file.absPath, "utf8"), absPath: file.absPath })
|
|
70
|
+
} else {
|
|
71
|
+
// Binary assets resolve to URLs the scene server serves from the project folder (/project/*).
|
|
72
|
+
entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path) })
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Shader artifacts: the web targets only (the editor runs in a browser — lite viewport +
|
|
77
|
+
// filament preview, which loads the mobile `opengl` artifact — there is no web-specific matc
|
|
78
|
+
// build). Without matc (offline first run) the lite artifact is still built from the
|
|
79
|
+
// source alone, hashed on the text, the way the playground does — the lite viewport keeps
|
|
80
|
+
// working and only the Rendered mode lacks the shader.
|
|
81
|
+
const shaderEntries = entries.filter((e) => e.type === "shader")
|
|
82
|
+
if (shaderEntries.length > 0) {
|
|
83
|
+
const { warnings } = await applyLocalShaders(shaderEntries, { root, srcPrefix: "/project/.lecodes/shaders", platforms: [ "opengl", "webgl-lite" ] })
|
|
84
|
+
for (const w of warnings) warnErr(w)
|
|
85
|
+
const { buildLiteMaterialArtifact } = await import("lecodes-sdk/compile")
|
|
86
|
+
for (const e of shaderEntries) {
|
|
87
|
+
if (e.shaders?.length) continue
|
|
88
|
+
const hash = createHash("sha256").update(e.text ?? "").digest("hex").slice(0, 16)
|
|
89
|
+
const dir = shadersCacheDir(root)
|
|
90
|
+
mkdirSync(dir, { recursive: true })
|
|
91
|
+
const liteName = `${hash}_webgl-lite.filamat`
|
|
92
|
+
if (!existsSync(join(dir, liteName))) writeFileSync(join(dir, liteName), JSON.stringify(buildLiteMaterialArtifact(e.text ?? "")))
|
|
93
|
+
const src = `/project/.lecodes/shaders/${hash}_opengl.filamat`
|
|
94
|
+
e.shaders = [ "opengl", "webgl-lite" ].map((platform) => ({ platform, src }))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const scene = scenePath.startsWith("./") ? scenePath : "./" + rel(scenePath)
|
|
99
|
+
if (!entries.some((e) => e.path === scene && e.type === "text")) {
|
|
100
|
+
return { js: null, error: `Scene file not found: ${scene}` }
|
|
101
|
+
}
|
|
102
|
+
entries.push(...sceneEditorEntries(scene, resolveHarness(loadSceneHarness), entries.map((e) => e.path)))
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const js = await compileProject({
|
|
106
|
+
entries, name: opts.name, publicUrl: "", entryOverride: SCENE_ENTRY_PATH,
|
|
107
|
+
header: false, format: "iife", minify: false, editor: true,
|
|
108
|
+
})
|
|
109
|
+
return { js }
|
|
110
|
+
} catch (e: any) {
|
|
111
|
+
return { js: null, error: e?.message?.replace(/mem:/g, "") ?? String(e) }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs"
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path"
|
|
3
|
+
import { screenFrame } from "../designMeta"
|
|
4
|
+
import { CliError, flagStr, toKey, type Args } from "../util"
|
|
5
|
+
import type { CompileEntry } from "./projectCompile"
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Rendering ONE screen module instead of the whole app — `lecodes render <file>` (and
|
|
9
|
+
* `--desktop`). A design screen (`design/screens/login.ts`) never boots itself: it default-exports
|
|
10
|
+
* a `UIScreen`, or a `(state) => UIScreen` function for its states, and the design board / snapshot
|
|
11
|
+
* supply the `.open()`. This builds the same virtual entry for the normal project pipeline, so any
|
|
12
|
+
* file written that way — inside design/ or not — renders with the full `lecodes render` flag set
|
|
13
|
+
* (--png/--clip/--script/--device/…) against the project's real imports and assets.
|
|
14
|
+
*
|
|
15
|
+
* Kept in step with compile/designCompile.ts, which does the same wrapping per screen for the board.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Extensions a screen module may carry (so `render design/screens/login` works too). */
|
|
19
|
+
const SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"]
|
|
20
|
+
|
|
21
|
+
/** Where the generated entry lives — the project root, so its import specifier is the screen's
|
|
22
|
+
* own project key ("./design/screens/login"). Leading dot: never a real project file. */
|
|
23
|
+
const SCREEN_ENTRY_PATH = "/.screen-entry.ts"
|
|
24
|
+
|
|
25
|
+
/** The design folder a bare screen id (`lecodes render login`) is looked up in. */
|
|
26
|
+
const DEFAULT_DESIGN_DIR = "design"
|
|
27
|
+
|
|
28
|
+
export type ScreenTarget = {
|
|
29
|
+
/** The virtual entry module — hand it to the compiler alongside the project's own entries. */
|
|
30
|
+
entry: CompileEntry
|
|
31
|
+
/** Path of that entry, for `entryOverride`. */
|
|
32
|
+
entryPath: string
|
|
33
|
+
/** The board frame from the design folder's meta.json, when this IS a design screen. */
|
|
34
|
+
frame?: [number, number]
|
|
35
|
+
/** How the screen is named in progress notes ("design/screens/login.ts", "login@empty"). */
|
|
36
|
+
label: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** `lecodes render <file|screen-id>` — the positional a screen render is asked for by. */
|
|
40
|
+
const screenArg = (args: Args): string | undefined => args._[0]
|
|
41
|
+
|
|
42
|
+
/** Whether the argument names a path at all (vs a bare screen id). */
|
|
43
|
+
const hasSeparator = (file: string) => file.includes("/") || file.includes("\\")
|
|
44
|
+
|
|
45
|
+
/** Find the screen file: a path (with or without extension), or a bare id resolved against the
|
|
46
|
+
* design folder's screens/ (`lecodes render login`, honoring --dir). */
|
|
47
|
+
const findScreenFile = (root: string, file: string, dirFlag: string | undefined): string => {
|
|
48
|
+
const candidates = [resolve(process.cwd(), file)]
|
|
49
|
+
if (!hasSeparator(file)) {
|
|
50
|
+
candidates.push(join(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file))
|
|
51
|
+
}
|
|
52
|
+
for (const base of candidates) {
|
|
53
|
+
if (existsSync(base) && statSync(base).isFile()) return base
|
|
54
|
+
for (const ext of SCREEN_EXTS) if (existsSync(base + ext)) return base + ext
|
|
55
|
+
}
|
|
56
|
+
throw new CliError(
|
|
57
|
+
`No screen file at "${file}".` +
|
|
58
|
+
(hasSeparator(file) ? "" : ` Pass a path, or a screen id that exists in ${dirFlag ?? DEFAULT_DESIGN_DIR}/screens/.`))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The design folder a screen file belongs to ("<root>/design" for "/design/screens/login.ts"),
|
|
62
|
+
* or null when it isn't laid out as a design screen. */
|
|
63
|
+
const designDirOf = (root: string, key: string): { dir: string, id: string } | null => {
|
|
64
|
+
const m = /^\/(.*)screens\/([^/]+)\.[^./]+$/.exec(key)
|
|
65
|
+
if (!m) return null
|
|
66
|
+
const dir = join(root, m[1])
|
|
67
|
+
return existsSync(join(dir, "meta.json")) ? { dir, id: m[2] } : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve `<file>` into the virtual entry that opens its default export.
|
|
72
|
+
* `state` picks the branch of a `(state) => UIScreen` screen (ignored by a value export).
|
|
73
|
+
*/
|
|
74
|
+
const resolveScreenTarget = (
|
|
75
|
+
root: string, file: string, opts: { state?: string, dir?: string } = {},
|
|
76
|
+
): ScreenTarget => {
|
|
77
|
+
const abs = findScreenFile(root, file, opts.dir)
|
|
78
|
+
const rel = relative(root, abs)
|
|
79
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
80
|
+
throw new CliError(
|
|
81
|
+
`"${file}" is outside the project (${root}) — a screen renders as part of its project, so its ` +
|
|
82
|
+
`imports and asset() files resolve from the same tree.`)
|
|
83
|
+
}
|
|
84
|
+
const key = toKey(rel)
|
|
85
|
+
|
|
86
|
+
// A file with no default export compiles to an empty render; say so here instead.
|
|
87
|
+
if (!/^\s*export\s+default\b/m.test(readFileSync(abs, "utf8"))) {
|
|
88
|
+
throw new CliError(
|
|
89
|
+
`${key.slice(1)} has no default export. A screen file ends with ` +
|
|
90
|
+
`\`export default UIScreen([...])\` — or \`export default (state) => UIScreen([...])\` for screen states.`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Same wrapper the design board compiles (designCompile.ts): a function export is invoked with the
|
|
94
|
+
// chosen state (a bare call uses the parameter's default = the canonical state), a value is used
|
|
95
|
+
// as-is, and either way the screen is opened so the runtime has something to render.
|
|
96
|
+
const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()"
|
|
97
|
+
const spec = "." + key.replace(/\.[^./]+$/, "")
|
|
98
|
+
const design = designDirOf(root, key)
|
|
99
|
+
return {
|
|
100
|
+
entryPath: SCREEN_ENTRY_PATH,
|
|
101
|
+
entry: {
|
|
102
|
+
path: SCREEN_ENTRY_PATH,
|
|
103
|
+
type: "text",
|
|
104
|
+
text: `import screen from ${JSON.stringify(spec)}\n`
|
|
105
|
+
+ `const __screen = typeof screen === "function" ? ${invoke} : screen\n`
|
|
106
|
+
+ `__screen.open()\n`,
|
|
107
|
+
},
|
|
108
|
+
frame: design ? screenFrame(design.dir, design.id) : undefined,
|
|
109
|
+
label: (design ? design.id : key.slice(1)) + (opts.state !== undefined ? `@${opts.state}` : ""),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Read the screen target off the CLI args, or undefined when no screen was named. Rejects the
|
|
114
|
+
* flag combinations that would fight over the entrypoint. */
|
|
115
|
+
export const screenTargetFromArgs = (root: string, args: Args): ScreenTarget | undefined => {
|
|
116
|
+
const file = screenArg(args)
|
|
117
|
+
if (file === undefined) {
|
|
118
|
+
if (flagStr(args, "state") !== undefined) {
|
|
119
|
+
throw new CliError("--state picks a screen's state — name the screen file too: lecodes render <file> --state <name>")
|
|
120
|
+
}
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
if (flagStr(args, "entry") !== undefined) {
|
|
124
|
+
throw new CliError("Use either a screen file or --entry, not both — each names the module to compile from.")
|
|
125
|
+
}
|
|
126
|
+
return resolveScreenTarget(root, file, { state: flagStr(args, "state"), dir: flagStr(args, "dir") })
|
|
127
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { execFile } from "node:child_process"
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto"
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"
|
|
4
|
+
import { tmpdir } from "node:os"
|
|
5
|
+
import { basename, join } from "node:path"
|
|
6
|
+
import {
|
|
7
|
+
ALL_PLATFORM_BUILD, buildLiteMaterialArtifact, externalSamplerReg, LOCAL_ONLY_PLATFORMS,
|
|
8
|
+
matcArgs, MATC_PLATFORMS, TARGET_MATRIX_GENERATION,
|
|
9
|
+
type AnyMatcPlatform, type AnyShaderPlatform,
|
|
10
|
+
} from "lecodes-sdk/compile"
|
|
11
|
+
import { resolveMatcExe, resolveMatcExeCached } from "../matcTool"
|
|
12
|
+
import { LECODES_DIR } from "../manifest"
|
|
13
|
+
import { CliError } from "../util"
|
|
14
|
+
import type { CompileEntry } from "./projectCompile"
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* LOCAL shader compile — the working-tree twin of the backend's compileShaderAsset
|
|
18
|
+
* (backend/src/utils/compileShader.ts): each .mat source compiles via matc (matcTool.ts —
|
|
19
|
+
* downloaded on first use) into the same artifact family the server produces, so `lecodes dev`
|
|
20
|
+
* doesn't need a push for shaders to work. The target matrix + the webgl-lite JSON artifact
|
|
21
|
+
* builder are shared through sdk/compile (shaderTargets.ts / liteMaterial.ts) — keep the two
|
|
22
|
+
* compile paths producing the SAME set of names: the bundle derives per-backend URLs by
|
|
23
|
+
* substituting `_creator.backend` into the opengl src.
|
|
24
|
+
*
|
|
25
|
+
* Artifacts cache in <root>/.lecodes/shaders/<hash>_<platform>.filamat, keyed by a hash of the
|
|
26
|
+
* source text + the matc binary's identity (size/mtime) — a changed shader or an updated matc
|
|
27
|
+
* recompiles, an unchanged one costs a hash. The dev server serves the cache under /shaders/;
|
|
28
|
+
* `lecodes shaders` (commands/shaders.ts) lists/compiles the same cache explicitly.
|
|
29
|
+
*
|
|
30
|
+
* When matc can't be resolved (offline first run, unpublished platform), entries degrade to the
|
|
31
|
+
* old behavior — empty artifact list + the push-once warning — instead of failing the compile.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const SHADERS_DIR = "shaders"
|
|
35
|
+
|
|
36
|
+
export const shadersCacheDir = (root: string): string => join(root, LECODES_DIR, SHADERS_DIR)
|
|
37
|
+
|
|
38
|
+
/** Served artifact names are exactly what applyLocalShaders emits — hash + a known platform. */
|
|
39
|
+
export const isShaderArtifactName = (name: string): boolean =>
|
|
40
|
+
/^[0-9a-f]{16}_[a-z-]+\.filamat$/.test(name)
|
|
41
|
+
|
|
42
|
+
/** The matc binary's cache-key identity — an updated (or locally rebuilt) matc recompiles. */
|
|
43
|
+
export const matcIdentity = (exe: string): string => {
|
|
44
|
+
const stat = statSync(exe)
|
|
45
|
+
return `${stat.size}:${Math.trunc(stat.mtimeMs)}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// TARGET_MATRIX_GENERATION is in the key so that changing the matc argv behind an EXISTING target
|
|
49
|
+
// name invalidates caches that would otherwise look complete (adding/removing a name is already
|
|
50
|
+
// caught by artifactFamily). Same class as matcId: a toolchain or matrix change means recompile.
|
|
51
|
+
const shaderHash = (text: string, matcId: string): string =>
|
|
52
|
+
createHash("sha256").update(`${TARGET_MATRIX_GENERATION}\0${matcId}\0${text}`).digest("hex").slice(0, 16)
|
|
53
|
+
|
|
54
|
+
/** The full artifact family: the server's matc targets + the CLI's local-only ones + the lite JSON.
|
|
55
|
+
* This is what "compiled" means for `lecodes shaders` status and what a caller gets when it
|
|
56
|
+
* doesn't ask for a narrower set. The web runtime loads the mobile `opengl` artifact (the engine
|
|
57
|
+
* rewrites external samplers itself — sdk/compile shaderTargets.ts), so there is no web target. */
|
|
58
|
+
const artifactFamily = (): AnyShaderPlatform[] => [...MATC_PLATFORMS, ...LOCAL_ONLY_PLATFORMS, "webgl-lite"]
|
|
59
|
+
|
|
60
|
+
/** True when every artifact for this source is already in the cache (`lecodes shaders` status). */
|
|
61
|
+
export const shaderIsCompiled = (root: string, text: string, matcId: string): boolean => {
|
|
62
|
+
const hash = shaderHash(text, matcId)
|
|
63
|
+
return artifactFamily().every((p) => existsSync(join(shadersCacheDir(root), `${hash}_${p}.filamat`)))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Which artifacts of the family are present — `lecodes shaders` status distinguishes a deliberate
|
|
67
|
+
* subset (`lecodes shaders compile --platform …`, or a desktop build's single target) from a cache
|
|
68
|
+
* that is simply stale. Without this, anyone using a narrower set reads a permanent "not compiled"
|
|
69
|
+
* and re-runs a full compile to clear it, undoing the saving. */
|
|
70
|
+
export const shaderCompiledPlatforms = (root: string, text: string, matcId: string): AnyShaderPlatform[] => {
|
|
71
|
+
const hash = shaderHash(text, matcId)
|
|
72
|
+
return artifactFamily().filter((p) => existsSync(join(shadersCacheDir(root), `${hash}_${p}.filamat`)))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The cached artifact FILE for a shader source + platform, when matc is installed and the
|
|
76
|
+
* artifact was compiled (the scene editor host's `/api/shader` reads it for live material
|
|
77
|
+
* patches). Null otherwise — the caller falls back to building the lite JSON on the fly. */
|
|
78
|
+
export const localShaderArtifact = (root: string, text: string, platform: string): string | null => {
|
|
79
|
+
const matcExe = resolveMatcExeCached()
|
|
80
|
+
if (!matcExe) return null
|
|
81
|
+
const file = join(shadersCacheDir(root), `${shaderHash(text, matcIdentity(matcExe))}_${platform}.filamat`)
|
|
82
|
+
return existsSync(file) ? file : null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every platform name the local compile knows, for validating a user-supplied `--platform` list. */
|
|
86
|
+
export const allShaderPlatforms = (): AnyShaderPlatform[] =>
|
|
87
|
+
[...(Object.keys(ALL_PLATFORM_BUILD) as AnyMatcPlatform[]), "webgl-lite"]
|
|
88
|
+
|
|
89
|
+
const runMatc = (exe: string, args: string[]): Promise<{ ok: boolean, stderr: string }> =>
|
|
90
|
+
new Promise((resolve) => {
|
|
91
|
+
execFile(exe, args, { windowsHide: true }, (error, _stdout, stderr) => {
|
|
92
|
+
// error covers both a nonzero exit (compile error) and a failed spawn — the message joins
|
|
93
|
+
// stderr so a missing/undownloadable binary still explains itself.
|
|
94
|
+
const text = String(stderr ?? "") || (error ? error.message : "")
|
|
95
|
+
resolve({ ok: !error, stderr: text })
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
/** matc's stderr names the temp file — rewrite to the project path and cut the trailing noise. */
|
|
100
|
+
const cleanMatcOutput = (err: string, tempName: string, realName: string): string => {
|
|
101
|
+
const output = err.replaceAll("\r", "").replaceAll(tempName, realName)
|
|
102
|
+
const cut = output.indexOf("\n\n")
|
|
103
|
+
return (cut >= 0 ? output.slice(0, cut) : output).trim()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Artifacts the local compile produced, for a caller that must SHIP them (the desktop host reads
|
|
107
|
+
* them by name from next to the bundle — see `local` mode below). */
|
|
108
|
+
export type StagedShader = { name: string, absPath: string }
|
|
109
|
+
|
|
110
|
+
export type LocalShaderResult = { warnings: string[], artifacts: StagedShader[] }
|
|
111
|
+
|
|
112
|
+
/** The artifacts a native host can actually load. webgl-lite is web-only, so it stays in
|
|
113
|
+
* the cache instead of being staged into a desktop build. `_creator.backend` returns exactly one
|
|
114
|
+
* of these: "desktop" (Windows desktop GL), "desktop-vulkan" (Windows desktop Vulkan),
|
|
115
|
+
* "opengl" (Linux desktop, Android), "metal" (Apple).
|
|
116
|
+
*
|
|
117
|
+
* NATIVE_PLATFORMS is the default when the caller does NOT know which host will run the bundle (a
|
|
118
|
+
* generic `preview --local`): every native artifact ships and the runtime picks. It deliberately
|
|
119
|
+
* omits "desktop-vulkan" — that host is opt-in, and shipping ~293 KB per shader to every unknown
|
|
120
|
+
* target for it would be a poor trade. A caller that DOES know — every desktop path, which stages
|
|
121
|
+
* the very exe that will run — passes `platforms` and ships exactly one, which is where
|
|
122
|
+
* desktop-vulkan gets staged. ALL_NATIVE_PLATFORMS is the set such a request is intersected with,
|
|
123
|
+
* i.e. "which of the requested platforms is a native host able to load at all". */
|
|
124
|
+
const NATIVE_PLATFORMS: AnyMatcPlatform[] = ["opengl", "metal", "desktop"]
|
|
125
|
+
const ALL_NATIVE_PLATFORMS: AnyMatcPlatform[] = ["opengl", "metal", "desktop", "desktop-vulkan"]
|
|
126
|
+
|
|
127
|
+
/** The single artifact a desktop host asks for, by (target OS, renderer) — the same mapping
|
|
128
|
+
* `js_getBackend` implements in creator-pkg. Linux desktop runs GLES and reports "opengl"; the
|
|
129
|
+
* macOS host (the Apple repo's LeCodes Desktop.app) is Filament-on-Metal and reports "metal" —
|
|
130
|
+
* staging "opengl" there leaves every custom material failing to load at the first scene. */
|
|
131
|
+
export const desktopShaderPlatform = (
|
|
132
|
+
platform: NodeJS.Platform, renderer: "gl" | "vulkan",
|
|
133
|
+
): AnyMatcPlatform =>
|
|
134
|
+
platform === "win32" ? (renderer === "vulkan" ? "desktop-vulkan" : "desktop")
|
|
135
|
+
: platform === "darwin" ? "metal"
|
|
136
|
+
: "opengl"
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Compile the project's shaders into the local cache and point each entry's `shaders` list at the
|
|
140
|
+
* results. Two modes:
|
|
141
|
+
* - URL mode (`srcPrefix`, the dev server): srcs become `<srcPrefix>/<name>`, served over HTTP.
|
|
142
|
+
* - `local` mode (hosts with no asset server — the desktop host): srcs become bare artifact
|
|
143
|
+
* names, `shadersLocal` marks the entry, and the returned artifacts must be staged next to
|
|
144
|
+
* the bundle so the host's fetchLocal finds them.
|
|
145
|
+
* Entries that already carry artifacts are skipped in URL mode (the server compiled them); in
|
|
146
|
+
* local mode they are recompiled, since a local-assets run reads the working tree by definition.
|
|
147
|
+
* Returns warnings (matc unavailable → the entry keeps whatever it had); throws CliError on an
|
|
148
|
+
* actual matc compile error (a broken .mat fails the compile like a TS error).
|
|
149
|
+
*
|
|
150
|
+
* `platforms` narrows the set compiled AND staged — pass it only when the target host is known with
|
|
151
|
+
* certainty (every desktop path: the exe is bundled or spawned by the same command). Leave it off
|
|
152
|
+
* for `lecodes dev`, which feeds a phone, a browser and the desktop host from one bundle and cannot
|
|
153
|
+
* know which will connect. Compiling is incremental per artifact, so a narrow run and a later full
|
|
154
|
+
* one add up instead of redoing each other's work.
|
|
155
|
+
*/
|
|
156
|
+
export const applyLocalShaders = async (
|
|
157
|
+
entries: CompileEntry[],
|
|
158
|
+
opts: { root: string, srcPrefix?: string, local?: boolean, platforms?: AnyShaderPlatform[] },
|
|
159
|
+
): Promise<LocalShaderResult> => {
|
|
160
|
+
const shaderEntries = entries.filter((e) =>
|
|
161
|
+
e.type === "shader" && e.absPath && (opts.local || !e.shaders?.length))
|
|
162
|
+
if (shaderEntries.length === 0) return { warnings: [], artifacts: [] }
|
|
163
|
+
|
|
164
|
+
let matcExe: string
|
|
165
|
+
try {
|
|
166
|
+
matcExe = await resolveMatcExe()
|
|
167
|
+
} catch (e) {
|
|
168
|
+
const reason = e instanceof CliError ? e.message : String(e)
|
|
169
|
+
return { warnings: [`Local shader compile unavailable (${reason})\n Shaders fall back to the server — push the project once for them to work.`], artifacts: [] }
|
|
170
|
+
}
|
|
171
|
+
const matcId = matcIdentity(matcExe)
|
|
172
|
+
|
|
173
|
+
const cacheDir = shadersCacheDir(opts.root)
|
|
174
|
+
mkdirSync(cacheDir, { recursive: true })
|
|
175
|
+
const artifacts: StagedShader[] = []
|
|
176
|
+
|
|
177
|
+
for (const entry of shaderEntries) {
|
|
178
|
+
const text = readFileSync(entry.absPath!, "utf8")
|
|
179
|
+
const hash = shaderHash(text, matcId)
|
|
180
|
+
const family = artifactFamily()
|
|
181
|
+
const artifact = (platform: string) => `${hash}_${platform}.filamat`
|
|
182
|
+
const has = (platform: string) => existsSync(join(cacheDir, artifact(platform)))
|
|
183
|
+
|
|
184
|
+
// What this run is responsible for: the whole family, or the caller's subset intersected with it.
|
|
185
|
+
const wanted = opts.platforms ? family.filter((p) => opts.platforms!.includes(p)) : family
|
|
186
|
+
|
|
187
|
+
// Compile only what is MISSING. Per-artifact rather than all-or-nothing so a narrow run
|
|
188
|
+
// (`--platform`, a desktop build) and a later full one add up instead of redoing each other's
|
|
189
|
+
// work. The old whole-family gate existed so a run that died midway could not serve a partial
|
|
190
|
+
// set — that property is preserved by each artifact being written-or-removed individually
|
|
191
|
+
// (the rmSync on failure below), which is what actually matters.
|
|
192
|
+
const missing = wanted.filter((p) => !has(p))
|
|
193
|
+
const matcMissing = missing.filter((p): p is AnyMatcPlatform => p !== "webgl-lite")
|
|
194
|
+
if (matcMissing.length > 0) {
|
|
195
|
+
// Temp source in tmpdir, rewritten per platform for the samplerExternal → sampler2d targets.
|
|
196
|
+
const tempName = `lecodes-matc-${randomBytes(6).toString("hex")}.mat`
|
|
197
|
+
const tempFile = join(tmpdir(), tempName)
|
|
198
|
+
try {
|
|
199
|
+
for (const platform of matcMissing) {
|
|
200
|
+
const build = ALL_PLATFORM_BUILD[platform]
|
|
201
|
+
writeFileSync(tempFile, build.replaceExternal ? text.replace(externalSamplerReg, "type:sampler2d") : text)
|
|
202
|
+
const out = join(cacheDir, artifact(platform))
|
|
203
|
+
const { ok, stderr } = await runMatc(matcExe, matcArgs(platform, out, tempFile))
|
|
204
|
+
if (!ok) {
|
|
205
|
+
rmSync(out, { force: true })
|
|
206
|
+
throw new CliError(`${entry.path}: shader compile failed [${platform}]\n${cleanMatcOutput(stderr, tempName, basename(entry.path))}`)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
rmSync(tempFile, { force: true })
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Web Lite artifact — JSON, not a filamat package (viewer-lite wraps the parsed fragment in
|
|
214
|
+
// its own GLSL template). buildLiteMaterialArtifact never throws — an exotic material becomes
|
|
215
|
+
// an `unsupported` artifact and the lite runtime falls back to unlit.
|
|
216
|
+
if (missing.includes("webgl-lite")) {
|
|
217
|
+
writeFileSync(join(cacheDir, artifact("webgl-lite")), JSON.stringify(buildLiteMaterialArtifact(text)))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// opengl first — its src is the template the bundle substitutes the runtime backend into.
|
|
221
|
+
if (opts.local) {
|
|
222
|
+
// Stage the native artifacts the target can ask for: the caller's set when it knows which host
|
|
223
|
+
// will run (one, for a desktop build), else every native one. `_creator.backend` picks at
|
|
224
|
+
// runtime, so shipping more than one is only ever wasted bytes — never a correctness need.
|
|
225
|
+
const staged = opts.platforms
|
|
226
|
+
? ALL_NATIVE_PLATFORMS.filter((p) => opts.platforms!.includes(p))
|
|
227
|
+
: NATIVE_PLATFORMS
|
|
228
|
+
// opengl must lead: compileProject derives the runtime name by substituting the backend into
|
|
229
|
+
// shaders[0]'s "_opengl". A staged set without it (a Windows desktop build) still needs the
|
|
230
|
+
// template, so synthesise the entry from the opengl NAME even when that file isn't shipped.
|
|
231
|
+
entry.shaders = [
|
|
232
|
+
{ platform: "opengl", src: artifact("opengl") },
|
|
233
|
+
...staged.filter((p) => p !== "opengl").map((platform) => ({ platform, src: artifact(platform) })),
|
|
234
|
+
]
|
|
235
|
+
entry.shadersLocal = true
|
|
236
|
+
for (const platform of staged) {
|
|
237
|
+
artifacts.push({ name: artifact(platform), absPath: join(cacheDir, artifact(platform)) })
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
const order = ["opengl", "metal", "desktop", "webgl-lite"]
|
|
241
|
+
entry.shaders = order.map((platform) => ({ platform, src: `${opts.srcPrefix}/${artifact(platform)}` }))
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { warnings: [], artifacts }
|
|
245
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { homedir } from "node:os"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
4
|
+
import { CliError } from "./util"
|
|
5
|
+
|
|
6
|
+
/* Global CLI config in ~/.lecodes/config.json: the server URL + the personal access token.
|
|
7
|
+
* Env vars LECODES_API / LECODES_TOKEN override the stored values (handy for CI). */
|
|
8
|
+
|
|
9
|
+
export type Config = { apiUrl?: string, token?: string }
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_API = "https://le.codes"
|
|
12
|
+
|
|
13
|
+
const CONFIG_DIR = join(homedir(), ".lecodes")
|
|
14
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json")
|
|
15
|
+
|
|
16
|
+
export const loadConfig = (): Config => {
|
|
17
|
+
let stored: Config = {}
|
|
18
|
+
try { stored = JSON.parse(readFileSync(CONFIG_FILE, "utf8")) } catch { /* none yet */ }
|
|
19
|
+
return {
|
|
20
|
+
apiUrl: process.env.LECODES_API ?? stored.apiUrl,
|
|
21
|
+
token: process.env.LECODES_TOKEN ?? stored.token,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const saveConfig = (config: Config) => {
|
|
26
|
+
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
27
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Strip a trailing slash so apiUrl + "/api" / apiUrl + src compose cleanly. */
|
|
31
|
+
export const normalizeApiUrl = (url: string) => url.trim().replace(/\/+$/, "")
|
|
32
|
+
|
|
33
|
+
/** The token, or a friendly error telling the user to log in. */
|
|
34
|
+
export const requireToken = (config: Config): string => {
|
|
35
|
+
if (!config.token) throw new CliError("Not logged in. Run `lecodes login` first.")
|
|
36
|
+
return config.token
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const requireApiUrl = (config: Config): string => {
|
|
40
|
+
if (!config.apiUrl) throw new CliError("No server URL configured. Run `lecodes login` first.")
|
|
41
|
+
return normalizeApiUrl(config.apiUrl)
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
|
|
4
|
+
/* The design folder's meta.json, read WITHOUT the optional `lecodes-design` package: the CLI needs
|
|
5
|
+
* a screen's board frame in paths that must work on a bare install (`lecodes design snapshot`,
|
|
6
|
+
* `lecodes render <screen>`). Mirrors lecodes-design's `metaDefaultSize` validation — duplicated
|
|
7
|
+
* on purpose, since the design package is an optional peer. */
|
|
8
|
+
|
|
9
|
+
export type ScreenMeta = { size?: [number, number] }
|
|
10
|
+
export type MetaFrames = { screens: Record<string, ScreenMeta>, defaultSize: [number, number] }
|
|
11
|
+
|
|
12
|
+
export const PHONE_SIZE: [number, number] = [390, 844]
|
|
13
|
+
|
|
14
|
+
/** Per-screen sizes + the board default frame (meta.defaultSize — a desktop design sets
|
|
15
|
+
* [1280, 800]). A missing/broken meta.json degrades to the phone frame. */
|
|
16
|
+
export const readMetaFrames = (designDir: string): MetaFrames => {
|
|
17
|
+
try {
|
|
18
|
+
const meta = JSON.parse(readFileSync(join(designDir, "meta.json"), "utf8"))
|
|
19
|
+
const d = meta?.defaultSize
|
|
20
|
+
const valid = Array.isArray(d) && d.length === 2 &&
|
|
21
|
+
d.every((n: unknown) => typeof n === "number" && Number.isFinite(n) && n >= 64 && n <= 8192)
|
|
22
|
+
return {
|
|
23
|
+
screens: meta?.screens ?? {},
|
|
24
|
+
defaultSize: valid ? [Math.round(d[0]), Math.round(d[1])] : PHONE_SIZE,
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
return { screens: {}, defaultSize: PHONE_SIZE }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The frame a screen renders at: its own `size` in meta.json, else the board default. */
|
|
32
|
+
export const screenFrame = (designDir: string, screenId: string): [number, number] => {
|
|
33
|
+
const frames = readMetaFrames(designDir)
|
|
34
|
+
return frames.screens[screenId]?.size ?? frames.defaultSize
|
|
35
|
+
}
|