lecodes-cli 2.4.10 → 2.5.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 (57) hide show
  1. package/dist/index.js +2510 -1035
  2. package/package.json +10 -10
  3. package/runtime/materials/decal-relief.mat +21 -46
  4. package/runtime/materials/decal.mat +41 -61
  5. package/runtime/materials/foliage-cards.mat +28 -140
  6. package/runtime/materials/foliage-impostor.mat +485 -0
  7. package/runtime/materials/foliage.mat +232 -0
  8. package/runtime/materials/grass.mat +244 -0
  9. package/runtime/materials/particles-quad.mat +5 -3
  10. package/runtime/materials/particles.mat +5 -3
  11. package/runtime/sdk-types.json +1 -1
  12. package/runtime/web/VERSION.json +3 -3
  13. package/runtime/web/assets/materials-Cs_z1zLi.bin +0 -0
  14. package/runtime/web/assets/materials-world-BtoAfznA.bin +0 -0
  15. package/runtime/web/player.js +3 -3
  16. package/runtime/web-lite/assets/{canvas-WxiusXCB.js → canvas-wFjS40IX.js} +1 -1
  17. package/runtime/web-lite/assets/creator2d-1pSkYRCn.js +1 -0
  18. package/runtime/web-lite/assets/creator2d-Ci5-pZGr.wasm +0 -0
  19. package/runtime/web-lite/assets/{pager-DpGa2uaE.js → pager-BZAO9gMC.js} +1 -1
  20. package/runtime/web-lite/assets/{plugins-CuEPHbHL.js → plugins-B7TuO_p4.js} +1 -1
  21. package/runtime/web-lite/assets/scene2d-F6T1Io7I.js +1 -0
  22. package/runtime/web-lite/assets/{tween-BnWYhJ7P.js → tween-DbrQTld4.js} +1 -1
  23. package/runtime/web-lite/assets/{video-BLj61nDx.js → video-bzR_t2i6.js} +1 -1
  24. package/runtime/web-lite/assets/{vlist-CwSYpLz-.js → vlist-Ap-F4H5J.js} +1 -1
  25. package/runtime/web-lite/features.json +18 -6
  26. package/runtime/web-lite/lite.js +6 -6
  27. package/src/commands/compile.ts +2 -1
  28. package/src/commands/hub/create.ts +100 -0
  29. package/src/commands/hub/credential.ts +32 -0
  30. package/src/commands/hub/index.ts +21 -0
  31. package/src/commands/hub/login.ts +89 -0
  32. package/src/commands/hub/status.ts +109 -0
  33. package/src/commands/index.ts +5 -1
  34. package/src/commands/level.ts +123 -0
  35. package/src/commands/lightmap.ts +41 -79
  36. package/src/commands/navmesh.ts +84 -67
  37. package/src/commands/secrets.ts +147 -0
  38. package/src/commands/update/index.ts +154 -64
  39. package/src/compile/designCompile.ts +2 -0
  40. package/src/compile/headlessBundle.ts +2 -0
  41. package/src/compile/projectCompile.ts +12 -1
  42. package/src/compile/sceneCompile.ts +2 -1
  43. package/src/dev/backendWorker.ts +6 -1
  44. package/src/dev/devServer.ts +2 -1
  45. package/src/dev/localBackend.ts +21 -14
  46. package/src/dev/projectBackend.ts +7 -3
  47. package/src/dev/watchProject.ts +2 -0
  48. package/src/hosts/desktopRenderer.ts +10 -2
  49. package/src/hosts/peerInstall.ts +7 -3
  50. package/src/hub/api.ts +220 -0
  51. package/src/hub/config.ts +53 -0
  52. package/src/hub/git.ts +148 -0
  53. package/src/platform/api.ts +21 -0
  54. package/src/project/env.ts +29 -0
  55. package/src/web/build.ts +149 -88
  56. package/runtime/web/assets/materials-CJzxzdUK.bin +0 -0
  57. package/runtime/web/assets/materials-world-BNQb0Z1l.bin +0 -0
package/src/web/build.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { readAppEnv } from "../project/env"
1
2
  import { createHash } from "node:crypto"
2
3
  import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"
3
4
  import { dirname, extname, join, resolve } from "node:path"
@@ -5,7 +6,7 @@ import { buildLiteMaterialArtifact } from "lecodes-sdk/compile"
5
6
  import { collectLocalEntries } from "../compile/collectLocal"
6
7
  import { applyProjectFonts } from "../compile/fonts"
7
8
  import { applyAppAssetIcons } from "../compile/assetIcons"
8
- import { ambiguousEntryWarning, compileProject } from "../compile/projectCompile"
9
+ import { ambiguousEntryWarning, compileProject, type CompileEntry } from "../compile/projectCompile"
9
10
  import { readAppConfigAt } from "../commands/app/shared"
10
11
  import type { Manifest } from "../project/manifest"
11
12
  import { CliError } from "../cli"
@@ -98,70 +99,62 @@ const walk = (dir: string, prefix = ""): { path: string, bytes: number }[] => {
98
99
  return out
99
100
  }
100
101
 
101
- export const buildWeb = async (options: WebBuildOptions): Promise<WebBuildResult> => {
102
- const { root, manifest, name, lite } = options
103
- const out = resolve(root, options.out)
104
- if (out === resolve(root) || resolve(root).startsWith(out + "\\") || resolve(root).startsWith(out + "/")) {
105
- throw new CliError(`The output folder is cleaned before a build: "${options.out}" holds the project itself.`)
106
- }
107
- const config: WebConfig = (readAppConfigAt(root) as { web?: WebConfig } | null)?.web ?? {}
102
+ /** The bundle with its header's time stamp as `-`: the same project builds into the same bytes
103
+ * (the line stays, for the source map's count of lines). */
104
+ export const stableBundle = (js: string): string => js.replace(/^\/\/ last-updated: .*$/m, "// last-updated: -")
105
+
106
+ /** The plugins a bundle's header lists (`// plugins: ar;camera`): the ones its page carries. */
107
+ export const listedPlugins = (js: string): Set<string> =>
108
+ new Set((/^\/\/ plugins: (.+)$/m.exec(js.slice(0, 2000))?.[1] ?? "").split(";").map((id) => id.trim()).filter(Boolean))
109
+
110
+ /** A project's web half, built (web/halves.ts): what the page carries for a plugin it lists. */
111
+ export type BuiltHalf = { half: ProjectHalf, js: string }
112
+
113
+ /** The project's own web halves of the plugins the bundle lists, built — before anything is
114
+ * written: one that does not build throws, and the folder of the last build stays as it was. */
115
+ export const buildListedHalves = async (root: string, js: string, opts: { minify?: boolean } = {}): Promise<BuiltHalf[]> => {
116
+ const listed = listedPlugins(js)
117
+ const halves: BuiltHalf[] = []
118
+ for (const half of projectHalves(root)) if (listed.has(half.id)) halves.push({ half, js: await buildHalf(half, { minify: opts.minify }) })
119
+ return halves
120
+ }
121
+
122
+ export type LitePageOptions = {
123
+ /** The bundle's text: what decides the features and the plugins the page keeps. */
124
+ js: string
125
+ /** The bundle as the page names it: `app.<hash>.js` (relative — `web build`), `/app.js` (a host
126
+ * of its own — the deploy worker). */
127
+ bundleSrc: string
128
+ /** app.json's `web`. */
129
+ web: WebConfig
130
+ /** The page's title when `web.title` names none. */
131
+ name: string
132
+ /** app.json's `icon`: the file (absolute) and what app.json says (for the warning); null = none. */
133
+ icon: { file: string, declared: string } | null
134
+ /** The project's halves of the plugins the bundle lists (buildListedHalves). */
135
+ halves: BuiltHalf[]
136
+ }
137
+
138
+ /**
139
+ * The page of Web Lite for one bundle, into `out` (an existing, empty folder): Web Lite's dist
140
+ * (`dist`) copied, the features and fonts the bundle never names pruned, the plugins it does not
141
+ * list dropped and the project's halves put beside the first-party ones (plugins.json), and
142
+ * `index.html` from the dist's (title, theme color, icon, the bundle's src; `<base>` and `404.html`
143
+ * in path mode). The bundle and the project's files are the caller's (copyNamedAssets after this).
144
+ * `lecodes web build` and the deploy worker (tools/worker) both build their folder through this: a
145
+ * project the platform deploys gets the page `web build` would have written. Returns its warnings.
146
+ */
147
+ export const assembleLitePage = (dist: string, out: string, opts: LitePageOptions): string[] => {
148
+ const { js, web: config, halves } = opts
108
149
  const mode = config.urls === "path" ? "path" : "hash"
109
150
  const base = normalBase(config.base)
110
151
  const warnings: string[] = []
111
-
112
- // ---- the bundle: every asset a url relative to the page ------------------------------------------------
113
- const assetUrl = (path: string) => "assets" + encodeURI(path)
114
- const collected = collectLocalEntries(root, manifest, "assets", { localShaders: true })
115
- const entries = collected.entries.filter((e) => {
116
- // what a build made is no part of the project
117
- const abs = e.absPath ? resolve(e.absPath) : null
118
- return !(abs && (abs === out || abs.startsWith(out + "\\") || abs.startsWith(out + "/")))
119
- })
120
- warnings.push(...collected.warnings)
121
- // A shader's artifact is named by a hash of its text (a hosting keeps a file of one name for
122
- // good). The bundle builds the name at run time from the `_opengl` template (compileProject):
123
- // on the page that is `<hash>_webgl-lite.filamat`, the only artifact written.
124
- const shaders = new Map<string, string>() // hash → the lite artifact's JSON
125
- for (const entry of entries) {
126
- if (entry.type !== "shader" || !entry.absPath) continue
127
- const text = readFileSync(entry.absPath, "utf8")
128
- const hash = createHash("sha256").update(text).digest("hex").slice(0, 16)
129
- shaders.set(hash, JSON.stringify(buildLiteMaterialArtifact(text)))
130
- entry.shaders = [{ platform: "opengl", src: `shaders/${hash}_opengl.filamat` }]
131
- }
132
- const fontsHeader = await applyProjectFonts(entries, {
133
- mode: "vendor", root, vendorDir: root, vendorPrefix: "", publicUrl: "", relative: true,
134
- makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: assetUrl(path), absPath }),
135
- })
136
- const iconsHeader = await applyAppAssetIcons(entries)
137
- if (!options.entry) {
138
- const ambiguous = ambiguousEntryWarning(entries)
139
- if (ambiguous) warnings.push(ambiguous)
140
- }
141
- const compiled = await compileProject({
142
- entries, name, publicUrl: "", entryOverride: options.entry, header: true, fontsHeader, iconsHeader,
143
- sourcemap: options.sourcemap === true, minify: options.minify,
144
- onWarning: (w) => warnings.push(w),
145
- })
146
- // The same project builds into the same bytes: the bundle's name is its hash, and a hosting
147
- // keeps a file of that name for good. The header's time stamp is the one line that would
148
- // differ (the line stays, for the source map's count of lines).
149
- const js = compiled.replace(/^\/\/ last-updated: .*$/m, "// last-updated: -")
150
- const header = js.slice(0, 2000)
151
- if (/^\/\/ 2d: enable/m.test(header)) {
152
+ if (/^\/\/ 2d: enable/m.test(js.slice(0, 2000))) {
152
153
  warnings.push("This project uses 2D scenes, which Web Lite does not show yet: the page runs its UI and its 3D, a 2D scene's calls fail. (`lecodes dev --web` runs the whole app on the web player.)")
153
154
  }
155
+ const listed = listedPlugins(js)
154
156
 
155
- // the project's own web halves of the plugins it lists, built before the folder is cleaned: one
156
- // that does not build leaves the last build as it was
157
- const listed = new Set((/^\/\/ plugins: (.+)$/m.exec(header)?.[1] ?? "").split(";").map((id) => id.trim()).filter(Boolean))
158
- const halves: { half: ProjectHalf, js: string }[] = []
159
- for (const half of projectHalves(root)) if (listed.has(half.id)) halves.push({ half, js: await buildHalf(half, { minify: options.minify }) })
160
-
161
- // ---- the folder ----------------------------------------------------------------------------------------------
162
- rmSync(out, { recursive: true, force: true })
163
- mkdirSync(out, { recursive: true })
164
- cpSync(lite, out, { recursive: true })
157
+ cpSync(dist, out, { recursive: true })
165
158
  // the features this bundle never names are not part of its page
166
159
  const table = join(out, "features.json")
167
160
  if (existsSync(table)) {
@@ -200,44 +193,23 @@ export const buildWeb = async (options: WebBuildOptions): Promise<WebBuildResult
200
193
  else { rmSync(pluginsTable, { force: true }); rmSync(join(out, "plugins"), { recursive: true, force: true }) }
201
194
  }
202
195
 
203
- const bundle = `app.${createHash("sha256").update(js).digest("hex").slice(0, 10)}.js`
204
- writeFileSync(join(out, bundle), js)
205
-
206
- // the project's assets: the ones the bundle names
207
- for (const entry of entries) {
208
- if (entry.type !== "resource" || !entry.absPath || !entry.fileSrc) continue
209
- if (!js.includes(entry.fileSrc)) continue
210
- const to = join(out, "assets", ...entry.path.split("/").filter(Boolean))
211
- if (existsSync(to)) { warnings.push(`${entry.path}: a file of the page has this name in the built folder — the project's file was not copied`); continue }
212
- mkdirSync(dirname(to), { recursive: true })
213
- copyFileSync(entry.absPath, to)
214
- }
215
- // …and its shaders: the ones the bundle names
216
- for (const [hash, artifact] of shaders) {
217
- if (!js.includes(`shaders/${hash}_`)) continue
218
- mkdirSync(join(out, "shaders"), { recursive: true })
219
- writeFileSync(join(out, "shaders", `${hash}_webgl-lite.filamat`), artifact)
220
- }
221
-
222
196
  // ---- the page ----------------------------------------------------------------------------------------------
223
- const app = readAppConfigAt(root)
224
- let html = readFileSync(join(lite, "index.html"), "utf8")
197
+ let html = readFileSync(join(dist, "index.html"), "utf8")
225
198
  const head: string[] = []
226
199
  head.push(`<meta name="theme-color" content="${escapeHtml(config.themeColor ?? "#000000")}" />`)
227
- if (app?.icon) {
228
- const icon = resolve(root, app.icon)
229
- if (existsSync(icon)) {
230
- const file = `icon${extname(icon).toLowerCase() || ".png"}`
231
- copyFileSync(icon, join(out, file))
200
+ if (opts.icon) {
201
+ if (existsSync(opts.icon.file)) {
202
+ const file = `icon${extname(opts.icon.file).toLowerCase() || ".png"}`
203
+ copyFileSync(opts.icon.file, join(out, file))
232
204
  head.push(`<link rel="icon" href="${file}" />`, `<link rel="apple-touch-icon" href="${file}" />`)
233
205
  } else {
234
- warnings.push(`app.json icon "${app.icon}" is not there — the page has no icon`)
206
+ warnings.push(`app.json icon "${opts.icon.declared}" is not there — the page has no icon`)
235
207
  }
236
208
  }
237
209
  // in path mode a route is a path: what the page names relatively is under the base, whatever the route
238
210
  if (mode === "path") head.unshift(`<base href="${escapeHtml(base)}" />`)
239
211
  const attrs = [
240
- `data-src="${bundle}"`,
212
+ `data-src="${escapeHtml(opts.bundleSrc)}"`,
241
213
  ...(mode === "path" ? [`data-urls="path"`, `data-base="${escapeHtml(base)}"`] : []),
242
214
  ...(config.font === "system" ? [`data-font="system"`] : []),
243
215
  ].join(" ")
@@ -246,12 +218,101 @@ export const buildWeb = async (options: WebBuildOptions): Promise<WebBuildResult
246
218
  if (next === html) throw new CliError(`The page of Web Lite has no ${String(from)} to fill — this CLI and its page are of different versions.`)
247
219
  html = next
248
220
  }
249
- replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(config.title ?? app?.name ?? name)}</title>`)
221
+ replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(config.title ?? opts.name)}</title>`)
250
222
  replace(`<div id="app"></div>`, `<div id="app" ${attrs}></div>`)
251
223
  replace("</head>", ` ${head.join("\n ")}\n </head>`)
252
224
  if (config.lang) replace(/<html lang="[^"]*">/, `<html lang="${escapeHtml(config.lang)}">`)
253
225
  writeFileSync(join(out, "index.html"), html)
254
226
  if (mode === "path") writeFileSync(join(out, "404.html"), html)
227
+ return warnings
228
+ }
229
+
230
+ /** The project's resources the bundle names (`js` holds their `fileSrc`), copied to
231
+ * `out/assets/<path>` — after the page (assembleLitePage): a file of the page keeps a name both
232
+ * have. Returns its warnings. */
233
+ export const copyNamedAssets = (entries: CompileEntry[], js: string, out: string): string[] => {
234
+ const warnings: string[] = []
235
+ for (const entry of entries) {
236
+ if (entry.type !== "resource" || !entry.absPath || !entry.fileSrc) continue
237
+ if (!js.includes(entry.fileSrc)) continue
238
+ const to = join(out, "assets", ...entry.path.split("/").filter(Boolean))
239
+ if (existsSync(to)) { warnings.push(`${entry.path}: a file of the page has this name in the built folder — the project's file was not copied`); continue }
240
+ mkdirSync(dirname(to), { recursive: true })
241
+ copyFileSync(entry.absPath, to)
242
+ }
243
+ return warnings
244
+ }
245
+
246
+ export const buildWeb = async (options: WebBuildOptions): Promise<WebBuildResult> => {
247
+ const { root, manifest, name, lite } = options
248
+ const out = resolve(root, options.out)
249
+ if (out === resolve(root) || resolve(root).startsWith(out + "\\") || resolve(root).startsWith(out + "/")) {
250
+ throw new CliError(`The output folder is cleaned before a build: "${options.out}" holds the project itself.`)
251
+ }
252
+ const config: WebConfig = (readAppConfigAt(root) as { web?: WebConfig } | null)?.web ?? {}
253
+ const warnings: string[] = []
254
+
255
+ // ---- the bundle: every asset a url relative to the page ------------------------------------------------
256
+ const assetUrl = (path: string) => "assets" + encodeURI(path)
257
+ const collected = collectLocalEntries(root, manifest, "assets", { localShaders: true })
258
+ const entries = collected.entries.filter((e) => {
259
+ // what a build made is no part of the project
260
+ const abs = e.absPath ? resolve(e.absPath) : null
261
+ return !(abs && (abs === out || abs.startsWith(out + "\\") || abs.startsWith(out + "/")))
262
+ })
263
+ warnings.push(...collected.warnings)
264
+ // A shader's artifact is named by a hash of its text (a hosting keeps a file of one name for
265
+ // good). The bundle builds the name at run time from the `_opengl` template (compileProject):
266
+ // on the page that is `<hash>_webgl-lite.filamat`, the only artifact written.
267
+ const shaders = new Map<string, string>() // hash → the lite artifact's JSON
268
+ for (const entry of entries) {
269
+ if (entry.type !== "shader" || !entry.absPath) continue
270
+ const text = readFileSync(entry.absPath, "utf8")
271
+ const hash = createHash("sha256").update(text).digest("hex").slice(0, 16)
272
+ shaders.set(hash, JSON.stringify(buildLiteMaterialArtifact(text)))
273
+ entry.shaders = [{ platform: "opengl", src: `shaders/${hash}_opengl.filamat` }]
274
+ }
275
+ const fontsHeader = await applyProjectFonts(entries, {
276
+ mode: "vendor", root, vendorDir: root, vendorPrefix: "", publicUrl: "", relative: true,
277
+ makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: assetUrl(path), absPath }),
278
+ })
279
+ const iconsHeader = await applyAppAssetIcons(entries)
280
+ if (!options.entry) {
281
+ const ambiguous = ambiguousEntryWarning(entries)
282
+ if (ambiguous) warnings.push(ambiguous)
283
+ }
284
+ const compiled = await compileProject({
285
+ entries, name, publicUrl: "", entryOverride: options.entry, header: true, fontsHeader, iconsHeader,
286
+ sourcemap: options.sourcemap === true, minify: options.minify, env: readAppEnv(root),
287
+ onWarning: (w) => warnings.push(w),
288
+ })
289
+ // The same project builds into the same bytes: the bundle's name is its hash, and a hosting
290
+ // keeps a file of that name for good.
291
+ const js = stableBundle(compiled)
292
+
293
+ // the project's own web halves of the plugins it lists, built before the folder is cleaned: one
294
+ // that does not build leaves the last build as it was
295
+ const halves = await buildListedHalves(root, js, { minify: options.minify })
296
+
297
+ // ---- the folder ----------------------------------------------------------------------------------------------
298
+ rmSync(out, { recursive: true, force: true })
299
+ mkdirSync(out, { recursive: true })
300
+ const bundle = `app.${createHash("sha256").update(js).digest("hex").slice(0, 10)}.js`
301
+ const app = readAppConfigAt(root)
302
+ warnings.push(...assembleLitePage(lite, out, {
303
+ js, bundleSrc: bundle, web: config, name: app?.name ?? name, halves,
304
+ icon: app?.icon ? { file: resolve(root, app.icon), declared: app.icon } : null,
305
+ }))
306
+ writeFileSync(join(out, bundle), js)
307
+
308
+ // the project's assets: the ones the bundle names
309
+ warnings.push(...copyNamedAssets(entries, js, out))
310
+ // …and its shaders: the ones the bundle names
311
+ for (const [hash, artifact] of shaders) {
312
+ if (!js.includes(`shaders/${hash}_`)) continue
313
+ mkdirSync(join(out, "shaders"), { recursive: true })
314
+ writeFileSync(join(out, "shaders", `${hash}_webgl-lite.filamat`), artifact)
315
+ }
255
316
 
256
317
  return { out, bundle, files: walk(out), warnings }
257
318
  }