lecodes-cli 0.20.2 → 2.0.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 (154) hide show
  1. package/README.md +56 -57
  2. package/dist/index.js +11052 -7592
  3. package/package.json +14 -8
  4. package/runtime/materials/decal-relief.mat +12 -1
  5. package/runtime/materials/decal.mat +17 -3
  6. package/runtime/materials/lightmap.mat +117 -138
  7. package/runtime/materials/particles-quad.mat +6 -2
  8. package/runtime/materials/terrain-lightmap.mat +123 -0
  9. package/runtime/materials/terrain.mat +36 -55
  10. package/runtime/sdk-types.json +1 -1
  11. package/runtime/web/VERSION.json +5 -0
  12. package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
  13. package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
  14. package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
  15. package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
  16. package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
  17. package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
  18. package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
  19. package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
  20. package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
  21. package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
  22. package/runtime/web/assets/map-CyBxOSV0.js +1 -0
  23. package/runtime/web/assets/map-XzoOFt0E.js +2 -0
  24. package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
  25. package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
  26. package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
  27. package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
  28. package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
  29. package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
  30. package/runtime/web/index.html +36 -0
  31. package/runtime/web/player.js +21 -0
  32. package/runtime/web-page/index.html +41 -0
  33. package/runtime/web-page/page.css +1 -0
  34. package/runtime/web-page/page.js +31 -0
  35. package/src/api.ts +1 -302
  36. package/src/cli/args.ts +37 -0
  37. package/src/cli/command.ts +113 -0
  38. package/src/cli/errors.ts +13 -0
  39. package/src/cli/help.ts +158 -0
  40. package/src/cli/index.ts +7 -0
  41. package/src/cli/output.ts +102 -0
  42. package/src/cli/run.ts +106 -0
  43. package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
  44. package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
  45. package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
  46. package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
  47. package/src/commands/{app.ts → app/index.ts} +400 -290
  48. package/src/commands/{appShared.ts → app/shared.ts} +43 -44
  49. package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
  50. package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
  51. package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
  52. package/src/commands/assets.ts +23 -21
  53. package/src/commands/clone.ts +48 -51
  54. package/src/commands/compile.ts +76 -89
  55. package/src/commands/create.ts +46 -47
  56. package/src/commands/design/comments.ts +361 -0
  57. package/src/commands/design/context.ts +101 -0
  58. package/src/commands/design/index.ts +391 -0
  59. package/src/commands/design/snapshot.ts +134 -0
  60. package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
  61. package/src/commands/desktop.ts +94 -82
  62. package/src/commands/dev.ts +260 -182
  63. package/src/commands/diff.ts +39 -51
  64. package/src/commands/index.ts +64 -0
  65. package/src/commands/{init.ts → init/index.ts} +195 -191
  66. package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
  67. package/src/commands/install.ts +80 -67
  68. package/src/commands/lightmap.ts +543 -252
  69. package/src/commands/link.ts +87 -94
  70. package/src/commands/login.ts +25 -23
  71. package/src/commands/navmesh.ts +169 -139
  72. package/src/commands/plugin.ts +46 -0
  73. package/src/commands/pn.ts +201 -244
  74. package/src/commands/pull.ts +49 -56
  75. package/src/commands/push.ts +48 -54
  76. package/src/commands/render.ts +176 -111
  77. package/src/commands/scene.ts +63 -55
  78. package/src/commands/serve.ts +58 -0
  79. package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
  80. package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
  81. package/src/commands/shared.ts +79 -0
  82. package/src/commands/status.ts +22 -26
  83. package/src/commands/test.ts +94 -63
  84. package/src/commands/thumbs.ts +185 -175
  85. package/src/commands/update/index.ts +226 -0
  86. package/src/commands/{types.ts → update/types.ts} +31 -22
  87. package/src/compile/collect.ts +5 -5
  88. package/src/compile/collectLocal.ts +3 -3
  89. package/src/compile/designCompile.ts +4 -4
  90. package/src/compile/headlessBundle.ts +16 -12
  91. package/src/compile/nativeStack.ts +1 -1
  92. package/src/compile/projectCompile.ts +12 -4
  93. package/src/compile/sceneCompile.ts +14 -26
  94. package/src/compile/screenEntry.ts +123 -127
  95. package/src/compile/shaders.ts +3 -3
  96. package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
  97. package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
  98. package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
  99. package/src/dev/androidDev.ts +1 -1
  100. package/src/dev/clientTemplates.ts +18 -14
  101. package/src/dev/devServer.ts +78 -41
  102. package/src/dev/iosDev.ts +180 -0
  103. package/src/dev/webRunner.ts +83 -15
  104. package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
  105. package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
  106. package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
  107. package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
  108. package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
  109. package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
  110. package/src/{peers.ts → hosts/peers.ts} +2 -2
  111. package/src/{releases.ts → hosts/releases.ts} +1 -1
  112. package/src/index.ts +35 -480
  113. package/src/platform/api.ts +302 -0
  114. package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
  115. package/src/{config.ts → platform/config.ts} +1 -1
  116. package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
  117. package/src/plugin/contract.ts +438 -0
  118. package/src/plugin/emitKotlin.ts +357 -0
  119. package/src/plugin/emitSdk.ts +259 -0
  120. package/src/plugin/emitSwift.ts +336 -0
  121. package/src/plugin/emitWeb.ts +383 -0
  122. package/src/plugin/generate.ts +157 -0
  123. package/src/plugin/lines.ts +28 -0
  124. package/src/plugin/lower.ts +231 -0
  125. package/src/plugin/manifest.ts +192 -0
  126. package/src/{projectEnv.ts → project/env.ts} +33 -2
  127. package/src/{ignore.ts → project/ignore.ts} +164 -163
  128. package/src/{localFiles.ts → project/localFiles.ts} +1 -1
  129. package/src/{manifest.ts → project/manifest.ts} +1 -1
  130. package/src/{project.ts → project/materialize.ts} +2 -2
  131. package/src/project/paths.ts +20 -0
  132. package/src/{textDiff.ts → project/textDiff.ts} +1 -1
  133. package/src/{types.ts → project/types.ts} +0 -0
  134. package/runtime/materials/lightmap-baked-lite.mat +0 -175
  135. package/runtime/materials/lightmap-baked.mat +0 -176
  136. package/runtime/scene-harness.json +0 -1
  137. package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
  138. package/runtime/web/assets/basis-C64VHDVD.js +0 -1
  139. package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
  140. package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
  141. package/runtime/web/assets/draco-BiISTFcR.js +0 -118
  142. package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
  143. package/runtime/web/assets/index-BMt7AnC5.js +0 -2
  144. package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
  145. package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
  146. package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
  147. package/runtime/web/embed.html +0 -32
  148. package/runtime/web/embed.js +0 -171
  149. package/src/commands/design.ts +0 -846
  150. package/src/commands/update.ts +0 -211
  151. package/src/util.ts +0 -146
  152. /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
  153. /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
  154. /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
@@ -1,54 +1,63 @@
1
1
  import { resolve } from "node:path"
2
- import { getTypes } from "../api"
3
- import { DEFAULT_API, loadConfig, normalizeApiUrl } from "../config"
4
- import { findProjectRoot, LECODES_DIR } from "../manifest"
2
+ import { getTypes } from "../../platform/api"
3
+ import { DEFAULT_API, loadConfig, normalizeApiUrl } from "../../platform/config"
4
+ import { findProjectRoot, LECODES_DIR } from "../../project/manifest"
5
5
  import {
6
6
  bundleHash, readLocalTypeBundle, readProjectTypeBundle, writeTsconfig, writeTypeBundle,
7
7
  type TypeBundle,
8
- } from "../types"
9
- import { CliError, c, flagBool, flagStr, log, note, success, warn, type Args } from "../util"
8
+ } from "../../project/types"
9
+ import { CliError, c, log, note, success, warn } from "../../cli"
10
10
 
11
11
  /*
12
- * The types step of `lecodes update` (formerly `lecodes types [dir]`) — re-materialize the IDE type surface (<root>/.lecodes/types/ + tsconfig).
12
+ * The types step of `lecodes update` (formerly `lecodes types [dir]`) — re-materialize the IDE type
13
+ * surface (<root>/.lecodes/types/ + tsconfig).
13
14
  *
14
15
  * Every project gets these types once, at scaffold time: `clone`/`create`/`pull` fetch them from
15
16
  * GET /api/types (the deployed runtime's surface), while `init` and `design` — which run with no
16
17
  * server and no login — write the bundle vendored into the CLI at publish time. Neither source
17
- * refreshes on its own, so a local project's types age with the CLI binary that made it. This
18
- * command is the refresh: server by default (newest SDK the platform actually runs), `--local` for
19
- * the CLI's own copy, `--check` to only report drift (non-zero exit when stale).
18
+ * refreshes on its own, so a local project's types age with the CLI binary that made it. This step
19
+ * is the refresh: server by default (newest SDK the platform actually runs), `local` for the CLI's
20
+ * own copy, `check` to only report drift (a CliError when stale).
20
21
  *
21
22
  * IDE-only: the compiler reads the SDK sources vendored in runtime/sdk, never .lecodes/types. When
22
- * the two sources disagree the command says so — types promising APIs this CLI can't compile is
23
+ * the two sources disagree the step says so — types promising APIs this CLI can't compile is
23
24
  * exactly the confusing case worth naming out loud.
24
25
  */
25
26
 
27
+ export type RefreshTypesOptions = {
28
+ /** The project folder (default: the project root above the cwd, else the cwd). */
29
+ dir?: string
30
+ /** The bundle vendored in this CLI instead of the server's. */
31
+ local: boolean
32
+ /** Only report drift — write nothing, throw when stale. */
33
+ check: boolean
34
+ /** API origin for the server bundle (default: the configured one). */
35
+ api?: string
36
+ }
37
+
26
38
  const label = (bundle: TypeBundle) => `${Object.keys(bundle).length} file(s)`
27
39
 
28
- export const types = async (args: Args) => {
29
- const dirArg = args._[0]
30
- const root = dirArg ? resolve(dirArg) : (() => {
40
+ export const refreshTypes = async (opts: RefreshTypesOptions): Promise<void> => {
41
+ const root = opts.dir ? resolve(opts.dir) : (() => {
31
42
  try { return findProjectRoot(process.cwd()) } catch { return process.cwd() }
32
43
  })()
33
44
 
34
- const useLocal = flagBool(args, "local")
35
- const checkOnly = flagBool(args, "check")
36
45
  const local = readLocalTypeBundle()
37
46
 
38
47
  // ---- pick the source ----
39
48
  let bundle: TypeBundle | null = null
40
49
  let source = ""
41
- if (useLocal) {
50
+ if (opts.local) {
42
51
  if (!local) throw new CliError("This CLI ships no vendored type bundle — drop --local to fetch them from the server.")
43
52
  bundle = local
44
53
  source = "the CLI's bundled SDK"
45
54
  } else {
46
- const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? loadConfig().apiUrl ?? DEFAULT_API)
55
+ const apiUrl = normalizeApiUrl(opts.api ?? loadConfig().apiUrl ?? DEFAULT_API)
47
56
  try {
48
57
  bundle = await getTypes(apiUrl)
49
58
  source = apiUrl
50
59
  } catch (e) {
51
- if (!local || checkOnly) throw e
60
+ if (!local || opts.check) throw e
52
61
  warn(`${(e as Error).message}`)
53
62
  bundle = local
54
63
  source = "the CLI's bundled SDK (server unreachable)"
@@ -56,10 +65,10 @@ export const types = async (args: Args) => {
56
65
  }
57
66
  log(`${c.green("source")} ${source} — ${label(bundle)}`)
58
67
 
59
- // ---- report drift, write unless --check ----
68
+ // ---- report drift, write unless check ----
60
69
  const onDisk = readProjectTypeBundle(root)
61
70
  const same = bundleHash(onDisk) === bundleHash(bundle)
62
- if (checkOnly) {
71
+ if (opts.check) {
63
72
  if (Object.keys(onDisk).length === 0) throw new CliError(`No types in ${LECODES_DIR}/types/ — run \`lecodes update\`.`)
64
73
  if (!same) throw new CliError(`Types are out of date (${label(onDisk)} on disk) — run \`lecodes update\`.`)
65
74
  success(`Types are up to date (${label(onDisk)}).`)
@@ -77,11 +86,11 @@ export const types = async (args: Args) => {
77
86
 
78
87
  // The compiler uses the SDK vendored in this CLI, not these declarations — worth saying when the
79
88
  // server's surface is a different one, since that gap shows up as "autocompletes but won't build".
80
- if (!useLocal && local && bundleHash(local) !== bundleHash(bundle)) {
89
+ if (!opts.local && local && bundleHash(local) !== bundleHash(bundle)) {
81
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 (`npm install -g lecodes-cli@latest`) or re-run with --local.")
82
91
  }
83
92
 
84
93
  log("")
85
94
  success(stats.added + stats.updated + stats.removed > 0 ? "Types synced." : "Types already current.")
86
- if (!dirArg) note("Reload the TypeScript server in your editor to pick them up.")
95
+ if (!opts.dir) note("Reload the TypeScript server in your editor to pick them up.")
87
96
  }
@@ -1,10 +1,10 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { basename } from "node:path"
3
- import type { Project } from "../api"
4
- import type { Manifest } from "../manifest"
5
- import { classifyKind, scanLocal } from "../localFiles"
6
- import { loadIgnore } from "../ignore"
7
- import { buildPaths } from "../project"
3
+ import type { Project } from "../platform/api"
4
+ import type { Manifest } from "../project/manifest"
5
+ import { classifyKind, scanLocal } from "../project/localFiles"
6
+ import { loadIgnore } from "../project/ignore"
7
+ import { buildPaths } from "../project/materialize"
8
8
  import type { CompileEntry } from "./projectCompile"
9
9
 
10
10
  /*
@@ -1,8 +1,8 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { basename } from "node:path"
3
- import type { Manifest } from "../manifest"
4
- import { classifyKind, scanLocal } from "../localFiles"
5
- import { loadIgnore } from "../ignore"
3
+ import type { Manifest } from "../project/manifest"
4
+ import { classifyKind, scanLocal } from "../project/localFiles"
5
+ import { loadIgnore } from "../project/ignore"
6
6
  import type { CompileEntry } from "./projectCompile"
7
7
 
8
8
  /*
@@ -1,8 +1,8 @@
1
1
  import { readFileSync } from "node:fs"
2
2
  import { basename } from "node:path"
3
- import type { Manifest } from "../manifest"
4
- import { classifyKind, scanLocal } from "../localFiles"
5
- import { loadIgnore } from "../ignore"
3
+ import type { Manifest } from "../project/manifest"
4
+ import { classifyKind, scanLocal } from "../project/localFiles"
5
+ import { loadIgnore } from "../project/ignore"
6
6
  import { compileProject, type CompileEntry } from "./projectCompile"
7
7
  import { applyAssetIcons } from "./assetIcons"
8
8
  import { applyProjectFonts } from "./fonts"
@@ -18,7 +18,7 @@ import { applyProjectFonts } from "./fonts"
18
18
 
19
19
  export type DesignAssetMode = "server" | "disk"
20
20
 
21
- // Must match lecodes-renderer's LOCAL_ASSET_SCHEME (headless/index.ts) — same as `lecodes render`.
21
+ // Must match lecodes-headless's LOCAL_ASSET_SCHEME (headless/index.ts) — same as `lecodes render`.
22
22
  const LOCAL_ASSET_SCHEME = "lecodesfile://"
23
23
 
24
24
  export const compileDesignScreen = async (opts: {
@@ -1,13 +1,13 @@
1
- import { getProject } from "../api"
2
- import { loadConfig, requireApiUrl, requireToken } from "../config"
3
- import { findProjectRoot, readManifest, type Manifest } from "../manifest"
1
+ import { getProject } from "../platform/api"
2
+ import { loadConfig, requireApiUrl, requireToken } from "../platform/config"
3
+ import { findProjectRoot, readManifest, type Manifest } from "../project/manifest"
4
4
  import { collectEntries } from "./collect"
5
5
  import { collectLocalEntries } from "./collectLocal"
6
6
  import { ambiguousEntryWarning, compileProject } from "./projectCompile"
7
7
  import { applyProjectFonts } from "./fonts"
8
8
  import { applyAppAssetIcons } from "./assetIcons"
9
9
  import type { ScreenTarget } from "./screenEntry"
10
- import { flagBool, flagStr, note, type Args } from "../util"
10
+ import { note } from "../cli"
11
11
 
12
12
  /*
13
13
  * Compile the project into the iife bundle the headless renderer runs — shared by `lecodes render`
@@ -23,7 +23,7 @@ import { flagBool, flagStr, note, type Args } from "../util"
23
23
  */
24
24
 
25
25
  // URL scheme emitted for a local project's resources so the renderer reads them off disk. Must match
26
- // lecodes-renderer's LOCAL_ASSET_SCHEME (headless/runProject.ts).
26
+ // lecodes-headless's LOCAL_ASSET_SCHEME (headless/runProject.ts).
27
27
  export const LOCAL_ASSET_SCHEME = "lecodesfile://"
28
28
 
29
29
  export type HeadlessBundle = {
@@ -36,8 +36,12 @@ export type HeadlessBundle = {
36
36
  useLocalAssets: boolean
37
37
  }
38
38
 
39
- /** Compile the local project for a headless run (honors --entry / --public-url / --remote-assets). */
40
- export const compileHeadlessBundle = async (args: Args, opts: { screen?: ScreenTarget } = {}): Promise<HeadlessBundle> => {
39
+ /** What a headless compile honours: `--entry` / `--public-url` / `--remote-assets` / `--wait-network`,
40
+ * and the screen module a `render <file>` named. */
41
+ export type HeadlessOptions = { entry?: string, publicUrl?: string, remoteAssets?: boolean, waitNetwork?: boolean, screen?: ScreenTarget }
42
+
43
+ /** Compile the local project for a headless run. */
44
+ export const compileHeadlessBundle = async (opts: HeadlessOptions = {}): Promise<HeadlessBundle> => {
41
45
  let root: string
42
46
  let manifest: Manifest | null
43
47
  try {
@@ -48,9 +52,9 @@ export const compileHeadlessBundle = async (args: Args, opts: { screen?: ScreenT
48
52
  manifest = null
49
53
  }
50
54
 
51
- const publicUrlFlag = flagStr(args, "public-url")
55
+ const publicUrlFlag = opts.publicUrl
52
56
  // A local (server-less) project has no remote asset URLs — it always reads from disk.
53
- const useLocalAssets = manifest ? (!publicUrlFlag && !flagBool(args, "remote-assets")) : true
57
+ const useLocalAssets = manifest ? (!publicUrlFlag && !opts.remoteAssets) : true
54
58
 
55
59
  let entries
56
60
  let warnings: string[]
@@ -73,7 +77,7 @@ export const compileHeadlessBundle = async (args: Args, opts: { screen?: ScreenT
73
77
  ;({ entries, warnings } = collectLocalEntries(root, null, ""))
74
78
  name = root.split(/[\\/]/).pop() ?? "project"
75
79
  publicUrl = LOCAL_ASSET_SCHEME
76
- if (flagBool(args, "remote-assets") || publicUrlFlag) {
80
+ if (opts.remoteAssets || publicUrlFlag) {
77
81
  warnings.push("This is a local project (no server) — assets always load from the working tree.")
78
82
  }
79
83
  }
@@ -103,7 +107,7 @@ export const compileHeadlessBundle = async (args: Args, opts: { screen?: ScreenT
103
107
  await applyAppAssetIcons(compileEntries)
104
108
 
105
109
  note("Compiling…")
106
- const entryFlag = opts.screen?.entryPath ?? flagStr(args, "entry")
110
+ const entryFlag = opts.screen?.entryPath ?? opts.entry
107
111
  if (!entryFlag) {
108
112
  const ambiguous = ambiguousEntryWarning(compileEntries)
109
113
  if (ambiguous) warnings.push(ambiguous)
@@ -121,7 +125,7 @@ export const compileHeadlessBundle = async (args: Args, opts: { screen?: ScreenT
121
125
  }
122
126
  // With --remote-assets, 2D textures come from the network; without --wait-network they resolve empty
123
127
  // and sprites render as flat placeholders. Local assets (default) load from disk, so no hint needed.
124
- if (js.includes("_creator2d") && !useLocalAssets && !flagBool(args, "wait-network")) {
128
+ if (js.includes("_creator2d") && !useLocalAssets && !opts.waitNetwork) {
125
129
  warnings.push("2D scene detected: sprite textures load over the network — add --wait-network to include them (else sprites render as flat placeholders).")
126
130
  }
127
131
 
@@ -1,4 +1,4 @@
1
- import { extractInlineSourceMap, mapNamedFileStack } from "lecodes-viewer-lite/src/sourcemapError"
1
+ import { extractInlineSourceMap, mapNamedFileStack } from "lecodes-web-shared/src/sourcemapError"
2
2
 
3
3
  /*
4
4
  * De-minify native (QuickJS) stack traces in a desktop host's output.
@@ -1,16 +1,16 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import { createRequire } from "node:module"
3
3
  import { dirname, join } from "node:path"
4
- import type { FileKind } from "../manifest"
5
- import { distRoot } from "../distRoot"
6
- import { compileProject as sdkCompileProject, detectEntry, scanModulesChisel, ENTRY_CONVENTION } from "lecodes-sdk/compile"
4
+ import type { FileKind } from "../project/manifest"
5
+ import { distRoot } from "../hosts/distRoot"
6
+ import { compileProject as sdkCompileProject, detectEntry, scanModulesChisel, localAssetNames, ENTRY_CONVENTION } from "lecodes-sdk/compile"
7
7
 
8
8
  // Every project compiles through the SDK's one import-free compiler, which bundles with chisel (the
9
9
  // Rust method-granular bundler, @letary/chisel) — the only bundler. The SDK resolves the chisel
10
10
  // binary itself; here we just hand it an explicit path when the @letary/chisel package is present,
11
11
  // and point it at the SDK *source* tree chisel compiles from:
12
12
  // 1. the `lecodes-sdk` package when it resolves — the npm install's dependency, or in the core
13
- // workspace the live packages/sdk (so an SDK edit is picked up with no re-vendoring);
13
+ // workspace the live sdk (so an SDK edit is picked up with no re-vendoring);
14
14
  // 2. else the copy vendored beside the standalone binary (runtime/sdk/, scripts/vendor-runtime.ts
15
15
  // --sdk) — the binary has no node_modules.
16
16
  const resolveSdkInject = (): string | null => {
@@ -81,6 +81,9 @@ export type CompileOptions = {
81
81
  /** Editor compile: the `EDITOR` global compiles to `true` and `*.editor.ts` plugin files are
82
82
  * bundled in (scene-editor bundles). Default false — production compiles skip them. */
83
83
  editor?: boolean
84
+ /** What the compile has to say that is not an error (a plugin the app uses and app.json does not
85
+ * list). */
86
+ onWarning?: (message: string) => void
84
87
  }
85
88
 
86
89
  /** A warning when auto-detection has several import-graph roots and no main.ts to settle it —
@@ -106,4 +109,9 @@ export const compileProject = (opts: CompileOptions): Promise<string> =>
106
109
  minify: opts.minify,
107
110
  sourcemap: opts.sourcemap,
108
111
  editor: opts.editor,
112
+ onWarning: opts.onWarning,
109
113
  })
114
+
115
+ /** Re-exported so the CALLERS that stage files next to the bundle name them the way the bundle asks
116
+ * for them (see localAssetNames in the SDK compile — two packs shipping SM_Sign_01.glb). */
117
+ export { localAssetNames }
@@ -1,11 +1,10 @@
1
1
  import { createHash } from "node:crypto"
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
3
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"
4
+ import type { Manifest } from "../project/manifest"
5
+ import { scanLocal } from "../project/localFiles"
6
+ import { loadIgnore } from "../project/ignore"
7
+ import { warnErr } from "../cli"
9
8
  import { compileProject, type CompileEntry } from "./projectCompile"
10
9
  import { applyLocalShaders, shadersCacheDir } from "./shaders"
11
10
 
@@ -14,8 +13,9 @@ import { applyLocalShaders, shadersCacheDir } from "./shaders"
14
13
  * point (the platform backend and the standalone playground are the other two). Reads the project
15
14
  * off disk, composes the edit-mode bundle with the SDK's shared `sceneEditorEntries`, and drives it
16
15
  * 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.
16
+ * closure (lecodes-3d-editor's src/server/inProcess.ts) field-for-field so every host emits the same
17
+ * bundle — only the file source differs. The harness is the editor's: the caller hands in
18
+ * `sceneHarness()` of the editor package it serves the UI from, so the two are one version.
19
19
  *
20
20
  * publicUrl "" + resource fileSrc "/project/<rel>" → the scene server's /project/* mount.
21
21
  * minify:false — mangling erases class names the inspector matches aspect schemas by.
@@ -29,22 +29,6 @@ const TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i
29
29
  /** Strip scanLocal's leading-slash key convention ("/city.scene.ts" → "city.scene.ts"). */
30
30
  const rel = (key: string) => key.replace(/^\//, "")
31
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
32
  export const compileSceneEditor = async (opts: {
49
33
  root: string
50
34
  /** Null in standalone mode (a bare folder, no cloned project). */
@@ -53,9 +37,11 @@ export const compileSceneEditor = async (opts: {
53
37
  name: string
54
38
  /** Project path of the scene file as the editor sends it ("./city.scene.ts"). */
55
39
  scenePath: string
40
+ /** The editor's harness, module name → source (`sceneHarness()` of lecodes-3d-editor/server). */
41
+ harness: Record<string, string>
56
42
  }): Promise<{ js: string | null, error?: string }> => {
57
43
  const { root, scenePath } = opts
58
- const { loadSceneHarness, sceneEditorEntries, SCENE_ENTRY_PATH } = await import("lecodes-sdk/compile")
44
+ const { sceneEditorEntries, sceneImportsOf, SCENE_ENTRY_PATH } = await import("lecodes-sdk/compile")
59
45
 
60
46
  const entries: CompileEntry[] = []
61
47
  const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(opts.manifest?.files ?? {})) })
@@ -96,10 +82,12 @@ export const compileSceneEditor = async (opts: {
96
82
  }
97
83
 
98
84
  const scene = scenePath.startsWith("./") ? scenePath : "./" + rel(scenePath)
99
- if (!entries.some((e) => e.path === scene && e.type === "text")) {
85
+ const sceneEntry = entries.find((e) => e.path === scene && e.type === "text")
86
+ if (!sceneEntry) {
100
87
  return { js: null, error: `Scene file not found: ${scene}` }
101
88
  }
102
- entries.push(...sceneEditorEntries(scene, resolveHarness(loadSceneHarness), entries.map((e) => e.path)))
89
+ const paths = entries.map((e) => e.path)
90
+ entries.push(...sceneEditorEntries(scene, opts.harness, paths, sceneImportsOf(sceneEntry.text ?? "", scene, paths)))
103
91
 
104
92
  try {
105
93
  const js = await compileProject({
@@ -1,127 +1,123 @@
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
- }
1
+ import { existsSync, readFileSync, statSync } from "node:fs"
2
+ import { isAbsolute, join, relative, resolve } from "node:path"
3
+ import { screenFrame } from "../project/designMeta"
4
+ import { CliError } from "../cli"
5
+ import { toKey } from "../project/paths"
6
+ import type { CompileEntry } from "./projectCompile"
7
+
8
+ /*
9
+ * Rendering ONE screen module instead of the whole app — `lecodes render <file>` (and
10
+ * `--desktop`). A design screen (`design/screens/login.ts`) never boots itself: it default-exports
11
+ * a `UIScreen`, or a `(state) => UIScreen` function for its states, and the design board / snapshot
12
+ * supply the `.open()`. This builds the same virtual entry for the normal project pipeline, so any
13
+ * file written that way — inside design/ or not — renders with the full `lecodes render` flag set
14
+ * (--png/--clip/--script/--device/…) against the project's real imports and assets.
15
+ *
16
+ * Kept in step with compile/designCompile.ts, which does the same wrapping per screen for the board.
17
+ */
18
+
19
+ /** Extensions a screen module may carry (so `render design/screens/login` works too). */
20
+ const SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"]
21
+
22
+ /** Where the generated entry lives — the project root, so its import specifier is the screen's
23
+ * own project key ("./design/screens/login"). Leading dot: never a real project file. */
24
+ const SCREEN_ENTRY_PATH = "/.screen-entry.ts"
25
+
26
+ /** The design folder a bare screen id (`lecodes render login`) is looked up in. */
27
+ const DEFAULT_DESIGN_DIR = "design"
28
+
29
+ export type ScreenTarget = {
30
+ /** The virtual entry module — hand it to the compiler alongside the project's own entries. */
31
+ entry: CompileEntry
32
+ /** Path of that entry, for `entryOverride`. */
33
+ entryPath: string
34
+ /** The board frame from the design folder's meta.json, when this IS a design screen. */
35
+ frame?: [number, number]
36
+ /** How the screen is named in progress notes ("design/screens/login.ts", "login@empty"). */
37
+ label: string
38
+ }
39
+
40
+ /** `lecodes render <file|screen-id>` — the positional a screen render is asked for by. */
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
+ /** The screen target named on the command line (`render <file>`), or undefined when no screen was
114
+ * named. Rejects the flag combinations that would fight over the entrypoint. */
115
+ export type ScreenArgs = { file?: string, state?: string, entry?: string, dir?: string }
116
+ export const screenTargetFrom = (root: string, a: ScreenArgs): ScreenTarget | undefined => {
117
+ if (a.file === undefined) {
118
+ if (a.state !== undefined) throw new CliError("--state picks a screen's state — name the screen file too: lecodes render <file> --state <name>")
119
+ return undefined
120
+ }
121
+ if (a.entry !== undefined) throw new CliError("Use either a screen file or --entry, not both — each names the module to compile from.")
122
+ return resolveScreenTarget(root, a.file, { state: a.state, dir: a.dir })
123
+ }