lecodes-cli 2.4.1 → 2.4.3
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 +36134 -11203
- package/package.json +10 -9
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/assets/ar-D6oDL4pw.js +1 -0
- package/runtime/web/assets/ar-mVXX3kRQ.js +2 -0
- package/runtime/web/assets/qrAnchorWorker-Cb5E8cF-.js +1 -0
- package/runtime/web/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
- package/runtime/web/index.html +1 -0
- package/runtime/web/player.js +12 -12
- package/runtime/web-lite/assets/{index-B2zEJlWS.js → index-oPsDD47E.js} +1 -1
- package/runtime/web-lite/assets/scene3d-BAl_QQyi.js +1105 -0
- package/runtime/web-lite/features.json +2 -2
- package/runtime/web-lite/index.html +2 -1
- package/runtime/web-lite/lite.js +9 -9
- package/runtime/web-lite/plugins/ar/_virtual_lecodes-half-resources-CNYmVR88.js +1 -0
- package/runtime/web-lite/plugins/ar/assets/qrAnchorWorker-DcxVKAqo.js +1 -0
- package/runtime/web-lite/plugins/ar/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
- package/runtime/web-lite/plugins/ar/index-C2uuuLhz.js +2 -0
- package/runtime/web-lite/plugins.json +1 -1
- package/src/commands/app/android.ts +68 -19
- package/src/commands/app/index.ts +84 -17
- package/src/commands/app/shared.ts +157 -24
- package/src/commands/app/splash.ts +251 -0
- package/src/commands/app/templates/android.ts +148 -25
- package/src/commands/app/templates/ios.ts +123 -12
- package/src/commands/init/index.ts +195 -195
- package/src/commands/plugin.ts +3 -3
- package/src/commands/push.ts +2 -2
- package/src/commands/shared.ts +79 -79
- package/src/commands/thumbs.ts +185 -185
- package/src/commands/web.ts +12 -5
- package/src/compile/collect.ts +2 -3
- package/src/compile/collectLocal.ts +13 -2
- package/src/compile/designCompile.ts +2 -3
- package/src/compile/projectCompile.ts +3 -0
- package/src/compile/screenEntry.ts +123 -123
- package/src/dev/devServer.ts +63 -7
- package/src/dev/https.ts +50 -0
- package/src/dev/serve.ts +15 -9
- package/src/dev/webRunner.ts +11 -3
- package/src/platform/serverDiff.ts +2 -3
- package/src/plugin/emitPackage.ts +18 -5
- package/src/plugin/emitWeb.ts +17 -8
- package/src/plugin/firstParty.ts +30 -0
- package/src/plugin/generate.ts +52 -24
- package/src/plugin/manifest.ts +66 -7
- package/src/project/ignore.ts +164 -164
- package/src/project/localFiles.ts +44 -7
- package/src/web/build.ts +33 -9
- package/src/web/halves.ts +76 -0
- package/runtime/web/assets/ar-CHvaraG4.js +0 -1
- package/runtime/web-lite/assets/scene3d-Bew8g7Ws.js +0 -1081
- package/runtime/web-lite/plugins/ar/index-CHvaraG4.js +0 -1
package/src/commands/thumbs.ts
CHANGED
|
@@ -1,185 +1,185 @@
|
|
|
1
|
-
import { createRequire } from "node:module"
|
|
2
|
-
import { execFileSync } from "node:child_process"
|
|
3
|
-
import { existsSync, mkdtempSync, readdirSync, rmSync, statSync } from "node:fs"
|
|
4
|
-
import { tmpdir } from "node:os"
|
|
5
|
-
import { extname, join, relative, resolve } from "node:path"
|
|
6
|
-
import { loadPeer } from "../hosts/peerInstall"
|
|
7
|
-
import { projectRootOf } from "./scene"
|
|
8
|
-
import { CliError, bool, c, defineCommand, endProgress, log, note, num, progress, warnErr } from "../cli"
|
|
9
|
-
import { toKey } from "../project/paths"
|
|
10
|
-
|
|
11
|
-
/*
|
|
12
|
-
* `lecodes thumbs [path…]` — the scene editor's asset thumbnails, generated from the COMMAND LINE.
|
|
13
|
-
*
|
|
14
|
-
* The editor renders previews in a hidden browser host (3D assets need an engine), but image
|
|
15
|
-
* thumbnails need no engine at all — and a .ktx2 has no picture until something decodes it. This
|
|
16
|
-
* command fills the same cache the editor reads (`.lecodes/preview/<asset>.<hash>.png`, owned by
|
|
17
|
-
* lecodes-3d-editor's server/previewCache) with no browser in the loop, so opening the editor on a
|
|
18
|
-
* texture-heavy project shows a full grid immediately instead of rendering 40 thumbnails on the
|
|
19
|
-
* spot.
|
|
20
|
-
*
|
|
21
|
-
* Incremental by construction: the cache file name carries the first 10 hex of the SOURCE file's
|
|
22
|
-
* sha256, so an asset whose preview is already there is skipped after one hash — re-running over a
|
|
23
|
-
* big project is nearly free. `--force` ignores that.
|
|
24
|
-
*
|
|
25
|
-
* Decoders, in the order tried: sharp (through the optional `lecodes-assets` peer — handles webp),
|
|
26
|
-
* else jimp (bundled with this CLI: png / jpeg / bmp / gif / tiff). KTX2 goes through
|
|
27
|
-
* KTX-Software's `ktx extract` first (it is a GPU-compressed format; nothing in JS reads it), and
|
|
28
|
-
* the extracted PNG then takes the same resize path.
|
|
29
|
-
*
|
|
30
|
-
* NOT covered: models, materials and prefabs — those are RENDERED, which needs an engine. The
|
|
31
|
-
* editor still does them on demand (and `lecodes render --desktop` is the browserless renderer if
|
|
32
|
-
* that ever becomes a batch job).
|
|
33
|
-
*/
|
|
34
|
-
|
|
35
|
-
const DEFAULT_SIZE = 256
|
|
36
|
-
const IMAGE_EXT = /\.(png|jpe?g|webp|bmp|gif|tiff?|ktx2)$/i
|
|
37
|
-
const RENDERED_EXT = /\.(glb|gltf|material\.ts|scene\.ts)$/i
|
|
38
|
-
|
|
39
|
-
/** KTX-Software's `ktx` CLI (v4.3+ has `extract`), resolved like lecodes-assets resolves toktx. */
|
|
40
|
-
const findKtx = (): string | null => {
|
|
41
|
-
const exe = process.platform === "win32" ? "ktx.exe" : "ktx"
|
|
42
|
-
const candidates = [
|
|
43
|
-
process.env.LECODES_KTX,
|
|
44
|
-
process.env.KTX_CLI,
|
|
45
|
-
process.env.KTX_TOKTX?.replace(/toktx(\.exe)?$/i, exe),
|
|
46
|
-
process.platform === "win32" ? join(process.env.LOCALAPPDATA ?? "", "Programs", "KTX-Software", "bin", exe) : null,
|
|
47
|
-
process.platform === "win32" ? join(process.env.ProgramFiles ?? "", "KTX-Software", "bin", exe) : null,
|
|
48
|
-
"/usr/local/bin/ktx",
|
|
49
|
-
"/opt/homebrew/bin/ktx",
|
|
50
|
-
].filter((p): p is string => !!p)
|
|
51
|
-
for (const path of candidates) { if (existsSync(path)) return path }
|
|
52
|
-
try {
|
|
53
|
-
execFileSync(exe, [ "--version" ], { stdio: "ignore" })
|
|
54
|
-
return exe
|
|
55
|
-
} catch { return null }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
type Resizer = (file: string, size: number) => Promise<Uint8Array>
|
|
59
|
-
|
|
60
|
-
/** sharp through the `lecodes-assets` peer (it declares it optional), or null when neither is
|
|
61
|
-
* installed — jimp then covers everything but webp. */
|
|
62
|
-
const sharpResizer = (): Resizer | null => {
|
|
63
|
-
try {
|
|
64
|
-
const req = createRequire(import.meta.url)
|
|
65
|
-
const sharp = createRequire(req.resolve("lecodes-assets/cli"))("sharp")
|
|
66
|
-
return async (file, size) =>
|
|
67
|
-
sharp(file).resize({ width: size, height: size, fit: "inside", withoutEnlargement: true }).png().toBuffer()
|
|
68
|
-
} catch { return null }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const jimpResizer = (): Resizer => async (file, size) => {
|
|
72
|
-
const { Jimp } = await import("jimp")
|
|
73
|
-
const img = await Jimp.read(file)
|
|
74
|
-
if (Math.max(img.width, img.height) > size) img.scaleToFit({ w: size, h: size })
|
|
75
|
-
return await img.getBuffer("image/png")
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** KTX2 → a temporary PNG (level 0, transcoded to RGBA8 when the payload is Basis-compressed). */
|
|
79
|
-
const ktxToPng = (ktx: string, file: string, out: string): void => {
|
|
80
|
-
const run = (args: string[]) => execFileSync(ktx, args, { stdio: [ "ignore", "ignore", "pipe" ], encoding: "utf8" })
|
|
81
|
-
try {
|
|
82
|
-
run([ "extract", "--transcode", "rgba8", "--level", "0", file, out ])
|
|
83
|
-
} catch (e: any) {
|
|
84
|
-
// Only Basis (ETC1S/UASTC) payloads are transcodable; a plain uncompressed KTX2 extracts as-is
|
|
85
|
-
run([ "extract", "--level", "0", file, out ])
|
|
86
|
-
if (!existsSync(out)) throw e
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** Every previewable file under a folder — the SAME sweep the editor's asset panel lists with
|
|
91
|
-
* (node_modules + dotfiles out, nothing else): what this command doesn't cover, the browser would
|
|
92
|
-
* end up rendering itself. */
|
|
93
|
-
const collect = (dir: string): string[] => {
|
|
94
|
-
const out: string[] = []
|
|
95
|
-
for (const name of readdirSync(dir)) {
|
|
96
|
-
if (name === "node_modules" || name.startsWith(".")) continue
|
|
97
|
-
const full = join(dir, name)
|
|
98
|
-
if (statSync(full).isDirectory()) out.push(...collect(full))
|
|
99
|
-
else if (IMAGE_EXT.test(name)) out.push(full)
|
|
100
|
-
}
|
|
101
|
-
return out
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export default defineCommand({
|
|
105
|
-
name: "thumbs",
|
|
106
|
-
summary: "Asset thumbnails for the scene editor, without a browser",
|
|
107
|
-
usage: "[path…]",
|
|
108
|
-
description: "Decodes the project's images (KTX2 too, via KTX-Software's ktx tool) into the editor's preview cache. Incremental: the cached name carries the asset's content hash, so unchanged files are skipped. Models, materials and prefabs are rendered by the editor, not here.",
|
|
109
|
-
flags: {
|
|
110
|
-
force: bool("redo even the up-to-date ones"),
|
|
111
|
-
size: num("longest edge, px", { default: DEFAULT_SIZE }),
|
|
112
|
-
},
|
|
113
|
-
run: ({ args, flags }) => thumbs(args, flags.size, flags.force),
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
const thumbs = async (targets: string[], size: number, force: boolean): Promise<void> => {
|
|
117
|
-
const root = projectRootOf(process.cwd())
|
|
118
|
-
|
|
119
|
-
// The scene editor owns the preview cache — installed into ~/.lecodes/peers on first use.
|
|
120
|
-
const sceneServer = await loadPeer<typeof import("lecodes-3d-editor/server")>("lecodes-3d-editor", "server", { for: "lecodes thumbs" })
|
|
121
|
-
|
|
122
|
-
// Targets: the paths given (a file or a folder each), else the whole project.
|
|
123
|
-
let files: string[] = []
|
|
124
|
-
const rendered: string[] = []
|
|
125
|
-
if (targets.length > 0) {
|
|
126
|
-
for (const arg of targets) {
|
|
127
|
-
const abs = resolve(process.cwd(), arg)
|
|
128
|
-
if (!existsSync(abs)) throw new CliError(`No such file or folder: ${arg}`)
|
|
129
|
-
if (statSync(abs).isDirectory()) files.push(...collect(abs))
|
|
130
|
-
else if (IMAGE_EXT.test(abs)) files.push(abs)
|
|
131
|
-
else if (RENDERED_EXT.test(abs)) rendered.push(arg)
|
|
132
|
-
else throw new CliError(`Not a previewable asset: ${arg}`)
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
files = collect(root)
|
|
136
|
-
}
|
|
137
|
-
files = [ ...new Set(files) ]
|
|
138
|
-
|
|
139
|
-
if (files.length === 0 && rendered.length === 0) {
|
|
140
|
-
log("No images to thumbnail.")
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const ktx = files.some((f) => f.toLowerCase().endsWith(".ktx2")) ? findKtx() : null
|
|
145
|
-
const sharp = sharpResizer()
|
|
146
|
-
const resize = sharp ?? jimpResizer()
|
|
147
|
-
const tmp = mkdtempSync(join(tmpdir(), "lecodes-thumbs-"))
|
|
148
|
-
|
|
149
|
-
let made = 0, fresh = 0
|
|
150
|
-
const skipped: string[] = []
|
|
151
|
-
try {
|
|
152
|
-
for (const [ i, file ] of files.entries()) {
|
|
153
|
-
const asset = "./" + toKey(relative(root, file))
|
|
154
|
-
progress(i, files.length, asset)
|
|
155
|
-
const hash = sceneServer.hashAsset(root, asset)
|
|
156
|
-
if (!hash) continue
|
|
157
|
-
if (!force && sceneServer.previewIsCurrent(root, asset, hash)) { fresh++; continue }
|
|
158
|
-
|
|
159
|
-
const ext = extname(file).toLowerCase()
|
|
160
|
-
if (ext === ".webp" && !sharp) { skipped.push(`${asset} (webp needs sharp — npm install -g lecodes-assets)`); continue }
|
|
161
|
-
try {
|
|
162
|
-
let source = file
|
|
163
|
-
if (ext === ".ktx2") {
|
|
164
|
-
if (!ktx) { skipped.push(`${asset} (KTX2 needs KTX-Software's \`ktx\` — set LECODES_KTX or put it on PATH)`); continue }
|
|
165
|
-
source = join(tmp, `${i}.png`)
|
|
166
|
-
ktxToPng(ktx, file, source)
|
|
167
|
-
}
|
|
168
|
-
sceneServer.writePreviewFile(root, asset, await resize(source, size))
|
|
169
|
-
made++
|
|
170
|
-
} catch (e: any) {
|
|
171
|
-
skipped.push(`${asset} (${(e?.message ?? String(e)).split("\n")[0]})`)
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
} finally {
|
|
175
|
-
endProgress()
|
|
176
|
-
rmSync(tmp, { recursive: true, force: true })
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
log(`${c.bold(String(made))} generated · ${fresh} already current${skipped.length > 0 ? ` · ${skipped.length} skipped` : ""}`)
|
|
180
|
-
for (const s of skipped) warnErr(s)
|
|
181
|
-
if (rendered.length > 0) {
|
|
182
|
-
warnErr(`Models, materials and prefabs are RENDERED, not decoded — the scene editor does those: ${rendered.join(", ")}`)
|
|
183
|
-
}
|
|
184
|
-
note(`Cache: ${join(root, ".lecodes", "preview")} (re-runs skip everything unchanged)`)
|
|
185
|
-
}
|
|
1
|
+
import { createRequire } from "node:module"
|
|
2
|
+
import { execFileSync } from "node:child_process"
|
|
3
|
+
import { existsSync, mkdtempSync, readdirSync, rmSync, statSync } from "node:fs"
|
|
4
|
+
import { tmpdir } from "node:os"
|
|
5
|
+
import { extname, join, relative, resolve } from "node:path"
|
|
6
|
+
import { loadPeer } from "../hosts/peerInstall"
|
|
7
|
+
import { projectRootOf } from "./scene"
|
|
8
|
+
import { CliError, bool, c, defineCommand, endProgress, log, note, num, progress, warnErr } from "../cli"
|
|
9
|
+
import { toKey } from "../project/paths"
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* `lecodes thumbs [path…]` — the scene editor's asset thumbnails, generated from the COMMAND LINE.
|
|
13
|
+
*
|
|
14
|
+
* The editor renders previews in a hidden browser host (3D assets need an engine), but image
|
|
15
|
+
* thumbnails need no engine at all — and a .ktx2 has no picture until something decodes it. This
|
|
16
|
+
* command fills the same cache the editor reads (`.lecodes/preview/<asset>.<hash>.png`, owned by
|
|
17
|
+
* lecodes-3d-editor's server/previewCache) with no browser in the loop, so opening the editor on a
|
|
18
|
+
* texture-heavy project shows a full grid immediately instead of rendering 40 thumbnails on the
|
|
19
|
+
* spot.
|
|
20
|
+
*
|
|
21
|
+
* Incremental by construction: the cache file name carries the first 10 hex of the SOURCE file's
|
|
22
|
+
* sha256, so an asset whose preview is already there is skipped after one hash — re-running over a
|
|
23
|
+
* big project is nearly free. `--force` ignores that.
|
|
24
|
+
*
|
|
25
|
+
* Decoders, in the order tried: sharp (through the optional `lecodes-assets` peer — handles webp),
|
|
26
|
+
* else jimp (bundled with this CLI: png / jpeg / bmp / gif / tiff). KTX2 goes through
|
|
27
|
+
* KTX-Software's `ktx extract` first (it is a GPU-compressed format; nothing in JS reads it), and
|
|
28
|
+
* the extracted PNG then takes the same resize path.
|
|
29
|
+
*
|
|
30
|
+
* NOT covered: models, materials and prefabs — those are RENDERED, which needs an engine. The
|
|
31
|
+
* editor still does them on demand (and `lecodes render --desktop` is the browserless renderer if
|
|
32
|
+
* that ever becomes a batch job).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const DEFAULT_SIZE = 256
|
|
36
|
+
const IMAGE_EXT = /\.(png|jpe?g|webp|bmp|gif|tiff?|ktx2)$/i
|
|
37
|
+
const RENDERED_EXT = /\.(glb|gltf|material\.ts|scene\.ts)$/i
|
|
38
|
+
|
|
39
|
+
/** KTX-Software's `ktx` CLI (v4.3+ has `extract`), resolved like lecodes-assets resolves toktx. */
|
|
40
|
+
const findKtx = (): string | null => {
|
|
41
|
+
const exe = process.platform === "win32" ? "ktx.exe" : "ktx"
|
|
42
|
+
const candidates = [
|
|
43
|
+
process.env.LECODES_KTX,
|
|
44
|
+
process.env.KTX_CLI,
|
|
45
|
+
process.env.KTX_TOKTX?.replace(/toktx(\.exe)?$/i, exe),
|
|
46
|
+
process.platform === "win32" ? join(process.env.LOCALAPPDATA ?? "", "Programs", "KTX-Software", "bin", exe) : null,
|
|
47
|
+
process.platform === "win32" ? join(process.env.ProgramFiles ?? "", "KTX-Software", "bin", exe) : null,
|
|
48
|
+
"/usr/local/bin/ktx",
|
|
49
|
+
"/opt/homebrew/bin/ktx",
|
|
50
|
+
].filter((p): p is string => !!p)
|
|
51
|
+
for (const path of candidates) { if (existsSync(path)) return path }
|
|
52
|
+
try {
|
|
53
|
+
execFileSync(exe, [ "--version" ], { stdio: "ignore" })
|
|
54
|
+
return exe
|
|
55
|
+
} catch { return null }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type Resizer = (file: string, size: number) => Promise<Uint8Array>
|
|
59
|
+
|
|
60
|
+
/** sharp through the `lecodes-assets` peer (it declares it optional), or null when neither is
|
|
61
|
+
* installed — jimp then covers everything but webp. */
|
|
62
|
+
const sharpResizer = (): Resizer | null => {
|
|
63
|
+
try {
|
|
64
|
+
const req = createRequire(import.meta.url)
|
|
65
|
+
const sharp = createRequire(req.resolve("lecodes-assets/cli"))("sharp")
|
|
66
|
+
return async (file, size) =>
|
|
67
|
+
sharp(file).resize({ width: size, height: size, fit: "inside", withoutEnlargement: true }).png().toBuffer()
|
|
68
|
+
} catch { return null }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const jimpResizer = (): Resizer => async (file, size) => {
|
|
72
|
+
const { Jimp } = await import("jimp")
|
|
73
|
+
const img = await Jimp.read(file)
|
|
74
|
+
if (Math.max(img.width, img.height) > size) img.scaleToFit({ w: size, h: size })
|
|
75
|
+
return await img.getBuffer("image/png")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** KTX2 → a temporary PNG (level 0, transcoded to RGBA8 when the payload is Basis-compressed). */
|
|
79
|
+
const ktxToPng = (ktx: string, file: string, out: string): void => {
|
|
80
|
+
const run = (args: string[]) => execFileSync(ktx, args, { stdio: [ "ignore", "ignore", "pipe" ], encoding: "utf8" })
|
|
81
|
+
try {
|
|
82
|
+
run([ "extract", "--transcode", "rgba8", "--level", "0", file, out ])
|
|
83
|
+
} catch (e: any) {
|
|
84
|
+
// Only Basis (ETC1S/UASTC) payloads are transcodable; a plain uncompressed KTX2 extracts as-is
|
|
85
|
+
run([ "extract", "--level", "0", file, out ])
|
|
86
|
+
if (!existsSync(out)) throw e
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Every previewable file under a folder — the SAME sweep the editor's asset panel lists with
|
|
91
|
+
* (node_modules + dotfiles out, nothing else): what this command doesn't cover, the browser would
|
|
92
|
+
* end up rendering itself. */
|
|
93
|
+
const collect = (dir: string): string[] => {
|
|
94
|
+
const out: string[] = []
|
|
95
|
+
for (const name of readdirSync(dir)) {
|
|
96
|
+
if (name === "node_modules" || name.startsWith(".")) continue
|
|
97
|
+
const full = join(dir, name)
|
|
98
|
+
if (statSync(full).isDirectory()) out.push(...collect(full))
|
|
99
|
+
else if (IMAGE_EXT.test(name)) out.push(full)
|
|
100
|
+
}
|
|
101
|
+
return out
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default defineCommand({
|
|
105
|
+
name: "thumbs",
|
|
106
|
+
summary: "Asset thumbnails for the scene editor, without a browser",
|
|
107
|
+
usage: "[path…]",
|
|
108
|
+
description: "Decodes the project's images (KTX2 too, via KTX-Software's ktx tool) into the editor's preview cache. Incremental: the cached name carries the asset's content hash, so unchanged files are skipped. Models, materials and prefabs are rendered by the editor, not here.",
|
|
109
|
+
flags: {
|
|
110
|
+
force: bool("redo even the up-to-date ones"),
|
|
111
|
+
size: num("longest edge, px", { default: DEFAULT_SIZE }),
|
|
112
|
+
},
|
|
113
|
+
run: ({ args, flags }) => thumbs(args, flags.size, flags.force),
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const thumbs = async (targets: string[], size: number, force: boolean): Promise<void> => {
|
|
117
|
+
const root = projectRootOf(process.cwd())
|
|
118
|
+
|
|
119
|
+
// The scene editor owns the preview cache — installed into ~/.lecodes/peers on first use.
|
|
120
|
+
const sceneServer = await loadPeer<typeof import("lecodes-3d-editor/server")>("lecodes-3d-editor", "server", { for: "lecodes thumbs" })
|
|
121
|
+
|
|
122
|
+
// Targets: the paths given (a file or a folder each), else the whole project.
|
|
123
|
+
let files: string[] = []
|
|
124
|
+
const rendered: string[] = []
|
|
125
|
+
if (targets.length > 0) {
|
|
126
|
+
for (const arg of targets) {
|
|
127
|
+
const abs = resolve(process.cwd(), arg)
|
|
128
|
+
if (!existsSync(abs)) throw new CliError(`No such file or folder: ${arg}`)
|
|
129
|
+
if (statSync(abs).isDirectory()) files.push(...collect(abs))
|
|
130
|
+
else if (IMAGE_EXT.test(abs)) files.push(abs)
|
|
131
|
+
else if (RENDERED_EXT.test(abs)) rendered.push(arg)
|
|
132
|
+
else throw new CliError(`Not a previewable asset: ${arg}`)
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
files = collect(root)
|
|
136
|
+
}
|
|
137
|
+
files = [ ...new Set(files) ]
|
|
138
|
+
|
|
139
|
+
if (files.length === 0 && rendered.length === 0) {
|
|
140
|
+
log("No images to thumbnail.")
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const ktx = files.some((f) => f.toLowerCase().endsWith(".ktx2")) ? findKtx() : null
|
|
145
|
+
const sharp = sharpResizer()
|
|
146
|
+
const resize = sharp ?? jimpResizer()
|
|
147
|
+
const tmp = mkdtempSync(join(tmpdir(), "lecodes-thumbs-"))
|
|
148
|
+
|
|
149
|
+
let made = 0, fresh = 0
|
|
150
|
+
const skipped: string[] = []
|
|
151
|
+
try {
|
|
152
|
+
for (const [ i, file ] of files.entries()) {
|
|
153
|
+
const asset = "./" + toKey(relative(root, file))
|
|
154
|
+
progress(i, files.length, asset)
|
|
155
|
+
const hash = sceneServer.hashAsset(root, asset)
|
|
156
|
+
if (!hash) continue
|
|
157
|
+
if (!force && sceneServer.previewIsCurrent(root, asset, hash)) { fresh++; continue }
|
|
158
|
+
|
|
159
|
+
const ext = extname(file).toLowerCase()
|
|
160
|
+
if (ext === ".webp" && !sharp) { skipped.push(`${asset} (webp needs sharp — npm install -g lecodes-assets)`); continue }
|
|
161
|
+
try {
|
|
162
|
+
let source = file
|
|
163
|
+
if (ext === ".ktx2") {
|
|
164
|
+
if (!ktx) { skipped.push(`${asset} (KTX2 needs KTX-Software's \`ktx\` — set LECODES_KTX or put it on PATH)`); continue }
|
|
165
|
+
source = join(tmp, `${i}.png`)
|
|
166
|
+
ktxToPng(ktx, file, source)
|
|
167
|
+
}
|
|
168
|
+
sceneServer.writePreviewFile(root, asset, await resize(source, size))
|
|
169
|
+
made++
|
|
170
|
+
} catch (e: any) {
|
|
171
|
+
skipped.push(`${asset} (${(e?.message ?? String(e)).split("\n")[0]})`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} finally {
|
|
175
|
+
endProgress()
|
|
176
|
+
rmSync(tmp, { recursive: true, force: true })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
log(`${c.bold(String(made))} generated · ${fresh} already current${skipped.length > 0 ? ` · ${skipped.length} skipped` : ""}`)
|
|
180
|
+
for (const s of skipped) warnErr(s)
|
|
181
|
+
if (rendered.length > 0) {
|
|
182
|
+
warnErr(`Models, materials and prefabs are RENDERED, not decoded — the scene editor does those: ${rendered.join(", ")}`)
|
|
183
|
+
}
|
|
184
|
+
note(`Cache: ${join(root, ".lecodes", "preview")} (re-runs skip everything unchanged)`)
|
|
185
|
+
}
|
package/src/commands/web.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { buildWeb } from "../web/build"
|
|
|
9
9
|
import { startPreview } from "../web/preview"
|
|
10
10
|
import { bundleFlags } from "./shared"
|
|
11
11
|
import { lanCandidates, loadDevToken } from "./dev"
|
|
12
|
+
import { devCertificate } from "../dev/https"
|
|
12
13
|
|
|
13
14
|
/*
|
|
14
15
|
* `lecodes web` — the project as a WEB PAGE (Web Lite: DOM and CSS, the browser's layout, text and
|
|
@@ -50,26 +51,31 @@ const kb = (bytes: number): string => bytes < 1024 ? `${bytes} B` : bytes < 1024
|
|
|
50
51
|
const dev = defineCommand({
|
|
51
52
|
name: "dev",
|
|
52
53
|
summary: "Open the project as a web page, reloaded on every save",
|
|
53
|
-
description: "Serves the project's bundle and assets with the page of Web Lite around them; a save recompiles and runs the app again, the page's console output and errors stream back here. The address's hash is the route the app is opened with (#/product/42 → app.launchUrl).",
|
|
54
|
+
description: "Serves the project's bundle and assets with the page of Web Lite around them; a save recompiles and runs the app again, the page's console output and errors stream back here. The address's hash is the route the app is opened with (#/product/42 → app.launchUrl). A web half of the project's own (app.json `\"ar\": { \"web\": \"./plugins/ar-8thwall\" }`) is built with every compile and served beside the page, its `web.static` folder from the project.\n\n`--https`: the page over https, for a phone's camera and motion sensors (a browser gives them to a secure page only) — a self-signed certificate for localhost and this machine's LAN addresses, made once and kept in ~/.lecodes/dev-https/. The phone's browser warns about it: accept it once.",
|
|
54
55
|
flags: {
|
|
55
56
|
port: num("server port", { default: DEFAULT_PORT }),
|
|
56
57
|
entry: bundleFlags.entry,
|
|
58
|
+
https: bool("serve over https with a self-signed certificate (a phone's camera and sensors need it)"),
|
|
57
59
|
"no-open": bool("don't open the browser"),
|
|
58
60
|
},
|
|
59
|
-
examples: ["lecodes web dev", "lecodes web dev --port 8080 --no-open"],
|
|
61
|
+
examples: ["lecodes web dev", "lecodes web dev --port 8080 --no-open", "lecodes web dev --https"],
|
|
60
62
|
run: async ({ flags }) => {
|
|
61
63
|
const { root, manifest, name } = project()
|
|
62
64
|
const lite = litePage()
|
|
63
65
|
|
|
64
66
|
const port = flags.port || DEFAULT_PORT
|
|
65
67
|
// The page is opened on this machine; a phone on the same network reaches it by the LAN address.
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
+
const lans = lanCandidates().map((candidate) => candidate.address)
|
|
69
|
+
const lan = lans[0]
|
|
70
|
+
const scheme = flags.https ? "https" : "http"
|
|
71
|
+
const origin = `${scheme}://localhost:${port}`
|
|
72
|
+
const tls = flags.https ? await devCertificate(lans) : undefined
|
|
68
73
|
const server = await startDevServer({
|
|
69
74
|
root, manifest, name, origin, port,
|
|
70
75
|
token: loadDevToken(root),
|
|
71
76
|
entryOverride: flags.entry,
|
|
72
77
|
lite,
|
|
78
|
+
tls,
|
|
73
79
|
log: note,
|
|
74
80
|
}).catch((e: unknown) => {
|
|
75
81
|
const msg = e instanceof Error ? e.message : String(e)
|
|
@@ -95,7 +101,8 @@ const dev = defineCommand({
|
|
|
95
101
|
log(`${c.bold("LeCodes Web")} — ${name}`)
|
|
96
102
|
log("")
|
|
97
103
|
log(`In the browser: ${c.bold(url)}`)
|
|
98
|
-
if (lan) note(`On this network: ${url.replace(
|
|
104
|
+
if (lan) note(`On this network: ${url.replace(`${scheme}://localhost:`, `${scheme}://${lan}:`)}`)
|
|
105
|
+
if (tls) note("The certificate is this machine's own (~/.lecodes/dev-https): a browser warns about it — accept it once per device.")
|
|
99
106
|
log("")
|
|
100
107
|
log(`Watching ${root} — save a file to reload. Ctrl+C to stop.`)
|
|
101
108
|
|
package/src/compile/collect.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs"
|
|
2
|
-
import { basename } from "node:path"
|
|
3
2
|
import type { Project } from "../platform/api"
|
|
4
3
|
import type { Manifest } from "../project/manifest"
|
|
5
|
-
import {
|
|
4
|
+
import { scanLocal } from "../project/localFiles"
|
|
6
5
|
import { loadIgnore } from "../project/ignore"
|
|
7
6
|
import { buildPaths } from "../project/materialize"
|
|
8
7
|
import type { CompileEntry } from "./projectCompile"
|
|
@@ -35,7 +34,7 @@ export const collectEntries = (
|
|
|
35
34
|
const warnings: string[] = []
|
|
36
35
|
const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) })
|
|
37
36
|
for (const file of scan.files) {
|
|
38
|
-
const type = manifest.files[file.path]?.type ??
|
|
37
|
+
const type = manifest.files[file.path]?.type ?? file.kind
|
|
39
38
|
if (type === "resource") {
|
|
40
39
|
const src = serverByPath.get(file.path)?.fileSrc
|
|
41
40
|
if (!src && (opts.warnMissingUrl ?? true)) warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs"
|
|
2
2
|
import { basename } from "node:path"
|
|
3
3
|
import type { Manifest } from "../project/manifest"
|
|
4
|
-
import {
|
|
4
|
+
import { scanLocal } from "../project/localFiles"
|
|
5
5
|
import { loadIgnore } from "../project/ignore"
|
|
6
6
|
import type { CompileEntry } from "./projectCompile"
|
|
7
7
|
|
|
@@ -13,6 +13,10 @@ import type { CompileEntry } from "./projectCompile"
|
|
|
13
13
|
* in the artifacts; callers that don't pass `localShaders` keep the old degradation — an empty
|
|
14
14
|
* artifact list + a push-once warning (same as `collectEntries` reports for unpushed shaders).
|
|
15
15
|
*/
|
|
16
|
+
const isHalfManifest = (abs: string): boolean => {
|
|
17
|
+
try { return typeof (JSON.parse(readFileSync(abs, "utf8").replace(/^/, "")) as { implements?: unknown }).implements === "string" } catch { return false }
|
|
18
|
+
}
|
|
19
|
+
|
|
16
20
|
export const collectLocalEntries = (
|
|
17
21
|
root: string, manifest: Manifest | null, assetBase: string,
|
|
18
22
|
opts: { localShaders?: boolean } = {},
|
|
@@ -20,8 +24,15 @@ export const collectLocalEntries = (
|
|
|
20
24
|
const entries: CompileEntry[] = []
|
|
21
25
|
const warnings: string[] = []
|
|
22
26
|
const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) })
|
|
27
|
+
// A HALF FOLDER (a manifest that `implements` a first-party plugin) is no part of the app: its
|
|
28
|
+
// app side is the SDK's wrapper, its files are a platform's half and what that half fetches (a
|
|
29
|
+
// runtime of megabytes) — never read into a compile, never a candidate entry.
|
|
30
|
+
const halfFolders = scan.files
|
|
31
|
+
.filter((f) => basename(f.path) === "lecodes-plugin.json" && isHalfManifest(f.absPath))
|
|
32
|
+
.map((f) => f.path.slice(0, -"lecodes-plugin.json".length))
|
|
23
33
|
for (const file of scan.files) {
|
|
24
|
-
|
|
34
|
+
if (halfFolders.some((dir) => file.path.startsWith(dir))) continue
|
|
35
|
+
const type = manifest?.files[file.path]?.type ?? file.kind
|
|
25
36
|
if (type === "resource") {
|
|
26
37
|
entries.push({ path: file.path, type, fileSrc: assetBase + encodeURI(file.path), absPath: file.absPath })
|
|
27
38
|
} else if (type === "shader") {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs"
|
|
2
|
-
import { basename } from "node:path"
|
|
3
2
|
import type { Manifest } from "../project/manifest"
|
|
4
|
-
import {
|
|
3
|
+
import { scanLocal } from "../project/localFiles"
|
|
5
4
|
import { loadIgnore } from "../project/ignore"
|
|
6
5
|
import { compileProject, type CompileEntry } from "./projectCompile"
|
|
7
6
|
import { applyAssetIcons } from "./assetIcons"
|
|
@@ -44,7 +43,7 @@ export const compileDesignScreen = async (opts: {
|
|
|
44
43
|
const entries: CompileEntry[] = []
|
|
45
44
|
const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) })
|
|
46
45
|
for (const file of scan.files) {
|
|
47
|
-
const type = manifest?.files[file.path]?.type ??
|
|
46
|
+
const type = manifest?.files[file.path]?.type ?? file.kind
|
|
48
47
|
if (type === "resource") {
|
|
49
48
|
entries.push({
|
|
50
49
|
path: file.path,
|
|
@@ -25,6 +25,9 @@ const resolveSdkInject = (): string | null => {
|
|
|
25
25
|
}
|
|
26
26
|
const sdkInject = resolveSdkInject()
|
|
27
27
|
if (sdkInject) process.env.LECODES_SDK_INJECT = sdkInject
|
|
28
|
+
/** The SDK's source tree projects compile against (null: none found) — where the first-party
|
|
29
|
+
* plugins' contracts are read from too (plugin/firstParty.ts). */
|
|
30
|
+
export const sdkSourceDir: string | null = sdkInject ? dirname(sdkInject) : null
|
|
28
31
|
|
|
29
32
|
/** Absolute path to the chisel binary: env override → the @letary/chisel npm package → a chisel
|
|
30
33
|
* binary shipped next to the executable (standalone binary distribution). Null when none found. */
|