lecodes-cli 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.js +424 -19
- package/package.json +18 -2
- package/runtime/gl-matrix/common.js +54 -0
- package/runtime/gl-matrix/header.js +12 -0
- package/runtime/gl-matrix/index.js +11 -0
- package/runtime/gl-matrix/mat2.js +425 -0
- package/runtime/gl-matrix/mat2d.js +476 -0
- package/runtime/gl-matrix/mat3.js +765 -0
- package/runtime/gl-matrix/mat4.js +1947 -0
- package/runtime/gl-matrix/quat.js +737 -0
- package/runtime/gl-matrix/quat2.js +822 -0
- package/runtime/gl-matrix/vec2.js +620 -0
- package/runtime/gl-matrix/vec3.js +791 -0
- package/runtime/gl-matrix/vec4.js +652 -0
- package/runtime/worker/animate.js +304 -0
- package/runtime/worker/chunk-4ZN32GYS.js +34 -0
- package/runtime/worker/chunk-G5ZA3LKB.js +52 -0
- package/runtime/worker/chunk-MI3UXE5H.js +1395 -0
- package/runtime/worker/creator.js +930 -0
- package/runtime/worker/globals.js +208 -0
- package/runtime/worker/ui.js +763 -0
- package/runtime/worker/utils.js +16 -0
package/README.md
CHANGED
|
@@ -22,6 +22,7 @@ cd myapp
|
|
|
22
22
|
# …edit files in VS Code (IntelliSense works out of the box)…
|
|
23
23
|
lecodes status # see what changed
|
|
24
24
|
lecodes push -m "Tweak the menu" # push as a new checkpoint
|
|
25
|
+
lecodes preview > app.js # compile the local project to JS
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
## Commands
|
|
@@ -33,6 +34,8 @@ lecodes push -m "Tweak the menu" # push as a new checkpoint
|
|
|
33
34
|
| `status` | Show added / modified / moved / deleted files vs the last sync. |
|
|
34
35
|
| `pull` | Overwrite local files with the project's current remote state. `--force` to discard local edits. |
|
|
35
36
|
| `push` | Push all local changes as **one** new checkpoint. `-m`/`--message` for the message, `--force` to override the "server moved on" guard. |
|
|
37
|
+
| `preview` | Compile the local project to a single JS bundle, printed to **stdout** (pipeable). `-o`/`--out <file>` to write a file, `--entry <path>` to override the entrypoint, `--no-header` to omit the metadata header, `--public-url <url>` for the asset URL prefix. Diagnostics go to stderr. |
|
|
38
|
+
| `render` | Render the local **UI** project. By default emits a semantic JSON description (great for AI/automated checks) to stdout or `-o`/`--out <file>`; `--png [file]` rasterizes a PNG (default `screenshot.png`, `--scale <n>` for DPR). `--width`/`--height` set the viewport (default 390×844). Requires the optional `lecodes-render` package (`npm i -D lecodes-render`); PNG also needs `@napi-rs/canvas`. 3D scenes aren't rendered. |
|
|
36
39
|
|
|
37
40
|
## How it works
|
|
38
41
|
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
2
4
|
|
|
3
5
|
// src/commands/clone.ts
|
|
4
6
|
import { existsSync as existsSync2, mkdirSync as mkdirSync5, readdirSync } from "node:fs";
|
|
@@ -21,6 +23,25 @@ var log = (msg = "") => process.stdout.write(msg + `
|
|
|
21
23
|
`);
|
|
22
24
|
var info = (msg) => log(msg);
|
|
23
25
|
var success = (msg) => log(c.green("✓ ") + msg);
|
|
26
|
+
var logErr = (msg = "") => process.stderr.write(msg + `
|
|
27
|
+
`);
|
|
28
|
+
var note = (msg) => logErr(c.dim(msg));
|
|
29
|
+
var warnErr = (msg) => logErr(c.yellow("! ") + msg);
|
|
30
|
+
var ellipsize = (s, max) => s.length <= max ? s : "…" + s.slice(s.length - max + 1);
|
|
31
|
+
var progress = (done, total, label = "") => {
|
|
32
|
+
if (!process.stdout.isTTY)
|
|
33
|
+
return;
|
|
34
|
+
const pct = total > 0 ? Math.round(done / total * 100) : 100;
|
|
35
|
+
process.stdout.clearLine(0);
|
|
36
|
+
process.stdout.cursorTo(0);
|
|
37
|
+
process.stdout.write(` ${c.cyan(`${pct}`.padStart(3) + "%")} ${c.dim(`(${done}/${total})`)} ${c.dim(ellipsize(label, 48))}`);
|
|
38
|
+
};
|
|
39
|
+
var endProgress = () => {
|
|
40
|
+
if (process.stdout.isTTY)
|
|
41
|
+
process.stdout.write(`
|
|
42
|
+
`);
|
|
43
|
+
};
|
|
44
|
+
|
|
24
45
|
class CliError extends Error {
|
|
25
46
|
}
|
|
26
47
|
var sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
|
@@ -214,9 +235,11 @@ var buildPaths = (assets) => {
|
|
|
214
235
|
}
|
|
215
236
|
return pathById;
|
|
216
237
|
};
|
|
217
|
-
var materializeAssets = async (root, project, apiUrl, token) => {
|
|
238
|
+
var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
|
|
218
239
|
const pathById = buildPaths(project.assets);
|
|
219
240
|
const files = {};
|
|
241
|
+
const total = project.assets.filter((a) => a.type !== "folder").length;
|
|
242
|
+
let done = 0;
|
|
220
243
|
for (const asset of project.assets) {
|
|
221
244
|
const key = pathById.get(asset.id);
|
|
222
245
|
const abs = absOf(root, key);
|
|
@@ -238,6 +261,7 @@ var materializeAssets = async (root, project, apiUrl, token) => {
|
|
|
238
261
|
sha = sha256(text);
|
|
239
262
|
}
|
|
240
263
|
files[key] = { id: asset.id, type: kindOf(asset.type), sha };
|
|
264
|
+
onProgress?.(++done, total, key);
|
|
241
265
|
}
|
|
242
266
|
return files;
|
|
243
267
|
};
|
|
@@ -310,8 +334,9 @@ var parseUuid = (input) => {
|
|
|
310
334
|
throw new CliError("Usage: lecodes clone <project-uuid|url> [directory]");
|
|
311
335
|
if (!input.includes("/"))
|
|
312
336
|
return input;
|
|
313
|
-
const
|
|
314
|
-
|
|
337
|
+
const clean = input.split(/[?#]/)[0].replace(/\/+$/, "");
|
|
338
|
+
const match = clean.match(/\/(?:projects|view)\/([^/]+)/);
|
|
339
|
+
return match ? match[1] : clean.slice(clean.lastIndexOf("/") + 1);
|
|
315
340
|
};
|
|
316
341
|
var slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
317
342
|
var clone = async (args) => {
|
|
@@ -319,7 +344,6 @@ var clone = async (args) => {
|
|
|
319
344
|
const apiUrl = requireApiUrl(config);
|
|
320
345
|
const token = requireToken(config);
|
|
321
346
|
const uuid = parseUuid(args._[0]);
|
|
322
|
-
info(c.dim(`Fetching project ${uuid}…`));
|
|
323
347
|
const project = await getProject(apiUrl, token, uuid);
|
|
324
348
|
const dir = args._[1] ?? slug(project.name);
|
|
325
349
|
const root = resolve(dir);
|
|
@@ -327,8 +351,9 @@ var clone = async (args) => {
|
|
|
327
351
|
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
328
352
|
}
|
|
329
353
|
mkdirSync5(root, { recursive: true });
|
|
330
|
-
info(c.dim(`
|
|
331
|
-
const files = await materializeAssets(root, project, apiUrl, token);
|
|
354
|
+
info(c.dim(`Fetching project ${c.bold(project.name)}…`));
|
|
355
|
+
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
356
|
+
endProgress();
|
|
332
357
|
if (!flagBool(args, "no-types")) {
|
|
333
358
|
info(c.dim("Fetching type definitions…"));
|
|
334
359
|
await materializeTypes(root, apiUrl);
|
|
@@ -336,7 +361,7 @@ var clone = async (args) => {
|
|
|
336
361
|
}
|
|
337
362
|
const commits = await getCommits(apiUrl, token, uuid);
|
|
338
363
|
writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
|
|
339
|
-
success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}
|
|
364
|
+
success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`);
|
|
340
365
|
info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``));
|
|
341
366
|
};
|
|
342
367
|
|
|
@@ -429,9 +454,13 @@ var login = async (args) => {
|
|
|
429
454
|
success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`);
|
|
430
455
|
};
|
|
431
456
|
|
|
432
|
-
// src/commands/
|
|
433
|
-
import {
|
|
434
|
-
import {
|
|
457
|
+
// src/commands/preview.ts
|
|
458
|
+
import { writeFileSync as writeFileSync5 } from "node:fs";
|
|
459
|
+
import { resolve as resolve2 } from "node:path";
|
|
460
|
+
|
|
461
|
+
// src/compile/collect.ts
|
|
462
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
463
|
+
import { basename } from "node:path";
|
|
435
464
|
|
|
436
465
|
// src/localFiles.ts
|
|
437
466
|
import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -520,7 +549,369 @@ var diffLocal = (manifest, scan) => {
|
|
|
520
549
|
};
|
|
521
550
|
var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
|
|
522
551
|
|
|
552
|
+
// src/compile/collect.ts
|
|
553
|
+
var collectEntries = (root, manifest, project) => {
|
|
554
|
+
const pathById = buildPaths(project.assets);
|
|
555
|
+
const serverByPath = new Map;
|
|
556
|
+
for (const asset of project.assets) {
|
|
557
|
+
if (asset.type === "folder")
|
|
558
|
+
continue;
|
|
559
|
+
serverByPath.set(pathById.get(asset.id), { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] });
|
|
560
|
+
}
|
|
561
|
+
const entries = [];
|
|
562
|
+
const warnings = [];
|
|
563
|
+
for (const file of scanLocal(root).files) {
|
|
564
|
+
const type = manifest.files[file.path]?.type ?? classifyKind(basename(file.path));
|
|
565
|
+
if (type === "resource") {
|
|
566
|
+
const src = serverByPath.get(file.path)?.fileSrc;
|
|
567
|
+
if (!src)
|
|
568
|
+
warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`);
|
|
569
|
+
entries.push({ path: file.path, type, fileSrc: src });
|
|
570
|
+
} else if (type === "shader") {
|
|
571
|
+
const shaders = serverByPath.get(file.path)?.shaders;
|
|
572
|
+
if (!shaders?.length)
|
|
573
|
+
warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
|
|
574
|
+
entries.push({ path: file.path, type, shaders });
|
|
575
|
+
} else {
|
|
576
|
+
entries.push({ path: file.path, type, text: readFileSync4(file.absPath, "utf8") });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return { entries, warnings };
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// src/compile/bundler.ts
|
|
583
|
+
import * as esbuild from "esbuild";
|
|
584
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
585
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
586
|
+
import { fileURLToPath } from "node:url";
|
|
587
|
+
|
|
588
|
+
// src/compile/assetMacro.ts
|
|
589
|
+
var ASSET_RE = /\basset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
|
|
590
|
+
var hasAssetMacro = (src) => src.includes("asset(");
|
|
591
|
+
var desugarAssetMacros = (src, filePath) => {
|
|
592
|
+
const byPath = new Map;
|
|
593
|
+
let i = 0;
|
|
594
|
+
const out = src.replace(ASSET_RE, (_match, _quote, path) => {
|
|
595
|
+
let name = byPath.get(path);
|
|
596
|
+
if (!name) {
|
|
597
|
+
name = `__asset_${i++}`;
|
|
598
|
+
byPath.set(path, name);
|
|
599
|
+
}
|
|
600
|
+
return name;
|
|
601
|
+
});
|
|
602
|
+
if (/\basset\(/.test(out)) {
|
|
603
|
+
throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`);
|
|
604
|
+
}
|
|
605
|
+
if (byPath.size === 0)
|
|
606
|
+
return src;
|
|
607
|
+
const header = [...byPath].map(([path, name]) => `import ${name} from ${JSON.stringify(path)};`).join(`
|
|
608
|
+
`);
|
|
609
|
+
return header + `
|
|
610
|
+
` + out;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// src/compile/bundler.ts
|
|
614
|
+
var GL_MATRIX_HEADER = `
|
|
615
|
+
import * as glMatrix from "gl-matrix/common";
|
|
616
|
+
import * as mat2 from "gl-matrix/mat2";
|
|
617
|
+
import * as mat2d from "gl-matrix/mat2d";
|
|
618
|
+
import * as mat3 from "gl-matrix/mat3";
|
|
619
|
+
import * as mat4 from "gl-matrix/mat4";
|
|
620
|
+
import * as quat from "gl-matrix/quat";
|
|
621
|
+
import * as quat2 from "gl-matrix/quat2";
|
|
622
|
+
import * as vec2 from "gl-matrix/vec2";
|
|
623
|
+
import * as vec3 from "gl-matrix/vec3";
|
|
624
|
+
import * as vec4 from "gl-matrix/vec4";
|
|
625
|
+
`;
|
|
626
|
+
var resolveRuntime = () => {
|
|
627
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
628
|
+
const vendored = join6(here, "..", "runtime");
|
|
629
|
+
if (existsSync3(join6(vendored, "worker", "creator.js"))) {
|
|
630
|
+
const w = join6(vendored, "worker");
|
|
631
|
+
return {
|
|
632
|
+
alias: {
|
|
633
|
+
creator: join6(w, "creator.js"),
|
|
634
|
+
ui: join6(w, "ui.js"),
|
|
635
|
+
animate: join6(w, "animate.js"),
|
|
636
|
+
utils: join6(w, "utils.js"),
|
|
637
|
+
"gl-matrix": join6(vendored, "gl-matrix")
|
|
638
|
+
},
|
|
639
|
+
inject: [join6(w, "globals.js")]
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
const repoRoot = join6(here, "..", "..", "..", "..");
|
|
643
|
+
const ws = join6(repoRoot, "packages", "worker", "src");
|
|
644
|
+
if (existsSync3(join6(ws, "index.ts"))) {
|
|
645
|
+
return {
|
|
646
|
+
alias: {
|
|
647
|
+
creator: join6(ws, "index.ts"),
|
|
648
|
+
ui: join6(ws, "ui.ts"),
|
|
649
|
+
animate: join6(ws, "animate.ts"),
|
|
650
|
+
utils: join6(ws, "utils.ts"),
|
|
651
|
+
"gl-matrix": join6(repoRoot, "packages", "backend", "src", "assets", "dependencies", "gl-matrix")
|
|
652
|
+
},
|
|
653
|
+
inject: [join6(ws, "globals.ts")]
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
throw new CliError("Could not locate the compile runtime — try reinstalling lecodes-cli.");
|
|
657
|
+
};
|
|
658
|
+
function normalize(path) {
|
|
659
|
+
const parts = [];
|
|
660
|
+
for (const part of path.split("/")) {
|
|
661
|
+
if (!part || part === ".")
|
|
662
|
+
continue;
|
|
663
|
+
if (part === "..")
|
|
664
|
+
parts.pop();
|
|
665
|
+
else
|
|
666
|
+
parts.push(part);
|
|
667
|
+
}
|
|
668
|
+
return "/" + parts.join("/");
|
|
669
|
+
}
|
|
670
|
+
function dirOf(path) {
|
|
671
|
+
const i = path.lastIndexOf("/");
|
|
672
|
+
return i <= 0 ? "/" : path.slice(0, i);
|
|
673
|
+
}
|
|
674
|
+
var hasExtension = (path) => /\.[a-zA-Z0-9]+$/.test(path);
|
|
675
|
+
function resolveBase(importer, specifier) {
|
|
676
|
+
if (specifier.startsWith("/"))
|
|
677
|
+
return normalize(specifier);
|
|
678
|
+
if (specifier.startsWith("./") || specifier.startsWith("../"))
|
|
679
|
+
return normalize(dirOf(importer) + "/" + specifier);
|
|
680
|
+
throw new Error(`Bare import is not allowed here: ${specifier}`);
|
|
681
|
+
}
|
|
682
|
+
function resolveWithExtensions(files, importer, specifier) {
|
|
683
|
+
const base = resolveBase(importer, specifier);
|
|
684
|
+
if (hasExtension(base)) {
|
|
685
|
+
if (files.has(base))
|
|
686
|
+
return base;
|
|
687
|
+
throw new Error(`Module not found: ${base}`);
|
|
688
|
+
}
|
|
689
|
+
for (const candidate of [base, `${base}.ts`, `${base}.js`]) {
|
|
690
|
+
if (files.has(candidate))
|
|
691
|
+
return candidate;
|
|
692
|
+
}
|
|
693
|
+
throw new Error(`Module not found: ${specifier} from ${importer}`);
|
|
694
|
+
}
|
|
695
|
+
var getLoader = (path) => path.endsWith(".ts") ? "ts" : "js";
|
|
696
|
+
async function buildInMemory(entrypoint, inputFiles, format = "esm") {
|
|
697
|
+
const runtime = resolveRuntime();
|
|
698
|
+
const files = new Map(Object.entries(inputFiles).map(([k, v]) => [normalize(k), v]));
|
|
699
|
+
const entry = normalize(entrypoint);
|
|
700
|
+
if (!files.has(entry))
|
|
701
|
+
throw new CliError(`Entrypoint not found: ${entry}`);
|
|
702
|
+
const plugin = {
|
|
703
|
+
name: "memfs",
|
|
704
|
+
setup(build2) {
|
|
705
|
+
build2.onResolve({ filter: /^entry$/ }, () => ({ path: entry, namespace: "mem" }));
|
|
706
|
+
build2.onResolve({ filter: /.*/, namespace: "mem" }, (args) => {
|
|
707
|
+
const isRelative = args.path.startsWith("./") || args.path.startsWith("../");
|
|
708
|
+
const isAbsolute = args.path.startsWith("/");
|
|
709
|
+
if (!isRelative && !isAbsolute)
|
|
710
|
+
return null;
|
|
711
|
+
try {
|
|
712
|
+
return { path: resolveWithExtensions(files, args.importer || entry, args.path), namespace: "mem" };
|
|
713
|
+
} catch (error) {
|
|
714
|
+
return { errors: [{ text: error instanceof Error ? error.message : String(error) }] };
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
build2.onLoad({ filter: /.*/, namespace: "mem" }, (args) => {
|
|
718
|
+
const contents = files.get(args.path);
|
|
719
|
+
if (contents == null)
|
|
720
|
+
return { errors: [{ text: `Module not found: ${args.path}` }] };
|
|
721
|
+
const finalContents = /\.[tj]sx?$/.test(args.path) && hasAssetMacro(contents) ? desugarAssetMacros(contents, args.path) : contents;
|
|
722
|
+
return { contents: finalContents, loader: getLoader(args.path), resolveDir: dirOf(args.path) };
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
const result = await esbuild.build({
|
|
727
|
+
stdin: { contents: `import "entry";`, loader: "js", sourcefile: "bootstrap.js" },
|
|
728
|
+
alias: runtime.alias,
|
|
729
|
+
define: { RAD2DEG: "57.29577951308232", DEG2RAD: "0.017453292519943295" },
|
|
730
|
+
inject: runtime.inject,
|
|
731
|
+
bundle: true,
|
|
732
|
+
write: false,
|
|
733
|
+
format,
|
|
734
|
+
platform: "browser",
|
|
735
|
+
plugins: [plugin],
|
|
736
|
+
charset: "utf8"
|
|
737
|
+
});
|
|
738
|
+
const output = result.outputFiles?.[0];
|
|
739
|
+
if (!output)
|
|
740
|
+
throw new CliError("Compilation produced no output");
|
|
741
|
+
return output.text;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/compile/projectCompile.ts
|
|
745
|
+
var buildHeader = (name, code) => {
|
|
746
|
+
let header = `// name: ${name.slice(0, 20)}
|
|
747
|
+
`;
|
|
748
|
+
header += `// last-updated: ${new Date().toISOString()}
|
|
749
|
+
`;
|
|
750
|
+
const localAssets = new Set;
|
|
751
|
+
for (const m of code.matchAll(/_creatorUtils\.fetchLocal\((.+?)\)/g)) {
|
|
752
|
+
localAssets.add(m[1].replaceAll(/['"]/g, "").trim());
|
|
753
|
+
}
|
|
754
|
+
if (/_creator\.createGlb/.test(code))
|
|
755
|
+
localAssets.add("uberarchive");
|
|
756
|
+
if (/_creator\.captureImage/.test(code))
|
|
757
|
+
localAssets.add("capture");
|
|
758
|
+
if (localAssets.size > 0)
|
|
759
|
+
header += `// preload: ${[...localAssets].join(";")}
|
|
760
|
+
`;
|
|
761
|
+
if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
|
|
762
|
+
header += `// gl: disable
|
|
763
|
+
`;
|
|
764
|
+
else if (code.includes("_creator.createEngine"))
|
|
765
|
+
header += `// gl: defer
|
|
766
|
+
`;
|
|
767
|
+
if (code.includes("_creator.createARController"))
|
|
768
|
+
header += "// ar: enable";
|
|
769
|
+
header += `
|
|
770
|
+
`;
|
|
771
|
+
return header;
|
|
772
|
+
};
|
|
773
|
+
var detectEntry = (entries) => {
|
|
774
|
+
let entry = "";
|
|
775
|
+
let max = -1;
|
|
776
|
+
for (const e of entries) {
|
|
777
|
+
const ext2 = e.path.split(".").at(-1)?.toLowerCase();
|
|
778
|
+
if (e.type === "text" && e.text !== undefined && !e.text.includes("export") && (ext2 === "ts" || ext2 === "js")) {
|
|
779
|
+
if (e.text.length > max) {
|
|
780
|
+
entry = e.path;
|
|
781
|
+
max = e.text.length;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return entry;
|
|
786
|
+
};
|
|
787
|
+
var compileProject = async (opts) => {
|
|
788
|
+
const entry = opts.entryOverride ?? detectEntry(opts.entries);
|
|
789
|
+
if (!entry) {
|
|
790
|
+
throw new CliError("Couldn't find an entrypoint (a top-level .ts/.js file with no `export`). Pass --entry <path>.");
|
|
791
|
+
}
|
|
792
|
+
const files = {};
|
|
793
|
+
for (const e of opts.entries) {
|
|
794
|
+
if (e.type === "resource") {
|
|
795
|
+
files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
|
|
796
|
+
} else if (e.type === "shader") {
|
|
797
|
+
const shaders = e.shaders ?? [];
|
|
798
|
+
if (shaders.length > 2) {
|
|
799
|
+
files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``;
|
|
800
|
+
} else if (shaders.length > 0) {
|
|
801
|
+
files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
|
|
802
|
+
} else {
|
|
803
|
+
files[e.path] = `export default ""`;
|
|
804
|
+
}
|
|
805
|
+
} else if (e.path.toLowerCase().endsWith(".svg")) {
|
|
806
|
+
files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
|
|
807
|
+
} else {
|
|
808
|
+
files[e.path] = GL_MATRIX_HEADER + (e.text ?? "");
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const code = await buildInMemory(entry, files, opts.format ?? "esm");
|
|
812
|
+
return opts.header === false ? code : buildHeader(opts.name, code) + code;
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// src/commands/preview.ts
|
|
816
|
+
var preview = async (args) => {
|
|
817
|
+
const root = findProjectRoot(process.cwd());
|
|
818
|
+
const manifest = readManifest(root);
|
|
819
|
+
const config = loadConfig();
|
|
820
|
+
const apiUrl = requireApiUrl(config);
|
|
821
|
+
const token = requireToken(config);
|
|
822
|
+
note("Resolving project assets…");
|
|
823
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
824
|
+
const { entries, warnings } = collectEntries(root, manifest, project);
|
|
825
|
+
note("Compiling…");
|
|
826
|
+
const js = await compileProject({
|
|
827
|
+
entries,
|
|
828
|
+
name: manifest.name,
|
|
829
|
+
publicUrl: flagStr(args, "public-url") ?? apiUrl,
|
|
830
|
+
entryOverride: flagStr(args, "entry"),
|
|
831
|
+
header: !flagBool(args, "no-header")
|
|
832
|
+
});
|
|
833
|
+
for (const w of warnings)
|
|
834
|
+
warnErr(w);
|
|
835
|
+
const out = flagStr(args, "o", "out");
|
|
836
|
+
if (out) {
|
|
837
|
+
writeFileSync5(resolve2(out), js);
|
|
838
|
+
note(`Wrote ${out} (${js.length} bytes)`);
|
|
839
|
+
} else {
|
|
840
|
+
process.stdout.write(js);
|
|
841
|
+
if (!js.endsWith(`
|
|
842
|
+
`))
|
|
843
|
+
process.stdout.write(`
|
|
844
|
+
`);
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// src/commands/render.ts
|
|
849
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
850
|
+
import { resolve as resolve3 } from "node:path";
|
|
851
|
+
var render = async (args) => {
|
|
852
|
+
const root = findProjectRoot(process.cwd());
|
|
853
|
+
const manifest = readManifest(root);
|
|
854
|
+
const config = loadConfig();
|
|
855
|
+
const apiUrl = requireApiUrl(config);
|
|
856
|
+
const token = requireToken(config);
|
|
857
|
+
note("Resolving project assets…");
|
|
858
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
859
|
+
const { entries, warnings } = collectEntries(root, manifest, project);
|
|
860
|
+
note("Compiling…");
|
|
861
|
+
const js = await compileProject({
|
|
862
|
+
entries,
|
|
863
|
+
name: manifest.name,
|
|
864
|
+
publicUrl: flagStr(args, "public-url") ?? apiUrl,
|
|
865
|
+
entryOverride: flagStr(args, "entry"),
|
|
866
|
+
header: false,
|
|
867
|
+
format: "iife"
|
|
868
|
+
});
|
|
869
|
+
if (js.includes("_creator.createScene") || js.includes("_creator.createEntity")) {
|
|
870
|
+
warnings.push("This looks like a 3D/GL project — only the UI layer is rendered.");
|
|
871
|
+
}
|
|
872
|
+
let renderer;
|
|
873
|
+
try {
|
|
874
|
+
renderer = await import("lecodes-render");
|
|
875
|
+
} catch {
|
|
876
|
+
throw new CliError("The renderer isn't installed. Run: npm i -D lecodes-render");
|
|
877
|
+
}
|
|
878
|
+
const width = Number(flagStr(args, "width")) || 390;
|
|
879
|
+
const height = Number(flagStr(args, "height")) || 844;
|
|
880
|
+
const pngFlag = flagStr(args, "png") ?? (flagBool(args, "png") ? "screenshot.png" : undefined);
|
|
881
|
+
if (pngFlag !== undefined) {
|
|
882
|
+
note("Rendering PNG…");
|
|
883
|
+
const { png, warnings: pngWarnings } = await renderer.renderToPng(js, {
|
|
884
|
+
width,
|
|
885
|
+
height,
|
|
886
|
+
scale: Number(flagStr(args, "scale")) || 2
|
|
887
|
+
});
|
|
888
|
+
const pngPath = resolve3(pngFlag);
|
|
889
|
+
writeFileSync6(pngPath, png);
|
|
890
|
+
for (const w of [...warnings, ...pngWarnings])
|
|
891
|
+
warnErr(w);
|
|
892
|
+
note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
note("Rendering…");
|
|
896
|
+
const result = await renderer.renderToJson(js, { width, height });
|
|
897
|
+
for (const w of [...warnings, ...result.warnings])
|
|
898
|
+
warnErr(w);
|
|
899
|
+
const out = flagStr(args, "o", "out");
|
|
900
|
+
if (out) {
|
|
901
|
+
writeFileSync6(resolve3(out), result.text);
|
|
902
|
+
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
903
|
+
} else {
|
|
904
|
+
process.stdout.write(result.text);
|
|
905
|
+
if (!result.text.endsWith(`
|
|
906
|
+
`))
|
|
907
|
+
process.stdout.write(`
|
|
908
|
+
`);
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
|
|
523
912
|
// src/commands/pull.ts
|
|
913
|
+
import { existsSync as existsSync4, rmSync } from "node:fs";
|
|
914
|
+
import { join as join7 } from "node:path";
|
|
524
915
|
var pull = async (args) => {
|
|
525
916
|
const root = findProjectRoot(process.cwd());
|
|
526
917
|
const manifest = readManifest(root);
|
|
@@ -535,12 +926,13 @@ var pull = async (args) => {
|
|
|
535
926
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
|
|
536
927
|
for (const path of Object.keys(manifest.files)) {
|
|
537
928
|
if (!remotePaths.has(path)) {
|
|
538
|
-
const abs =
|
|
539
|
-
if (
|
|
929
|
+
const abs = join7(root, ...path.replace(/^\//, "").split("/"));
|
|
930
|
+
if (existsSync4(abs))
|
|
540
931
|
rmSync(abs);
|
|
541
932
|
}
|
|
542
933
|
}
|
|
543
|
-
const files = await materializeAssets(root, project, apiUrl, token);
|
|
934
|
+
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
935
|
+
endProgress();
|
|
544
936
|
if (!flagBool(args, "no-types")) {
|
|
545
937
|
await materializeTypes(root, apiUrl);
|
|
546
938
|
writeTsconfig(root);
|
|
@@ -551,8 +943,8 @@ var pull = async (args) => {
|
|
|
551
943
|
};
|
|
552
944
|
|
|
553
945
|
// src/commands/push.ts
|
|
554
|
-
import { basename } from "node:path";
|
|
555
|
-
import { readFileSync as
|
|
946
|
+
import { basename as basename2 } from "node:path";
|
|
947
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
556
948
|
var push = async (args) => {
|
|
557
949
|
const root = findProjectRoot(process.cwd());
|
|
558
950
|
const manifest = readManifest(root);
|
|
@@ -577,14 +969,14 @@ var push = async (args) => {
|
|
|
577
969
|
let uploaded = 0;
|
|
578
970
|
for (const file of scan.files) {
|
|
579
971
|
const source = sourceFor(file.path);
|
|
580
|
-
const kind = source?.type ?? classifyKind(
|
|
972
|
+
const kind = source?.type ?? classifyKind(basename2(file.path));
|
|
581
973
|
const entry = { path: file.path, type: kind };
|
|
582
974
|
if (source)
|
|
583
975
|
entry.assetId = source.id;
|
|
584
976
|
if (isTextKind(kind)) {
|
|
585
|
-
entry.text =
|
|
977
|
+
entry.text = readFileSync5(file.absPath, "utf8");
|
|
586
978
|
} else if (!source || source.sha !== file.sha) {
|
|
587
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid,
|
|
979
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename2(file.path), readFileSync5(file.absPath));
|
|
588
980
|
entry.blobFileId = blob.id;
|
|
589
981
|
uploaded++;
|
|
590
982
|
}
|
|
@@ -656,9 +1048,22 @@ ${c.bold("Commands:")}
|
|
|
656
1048
|
push Push local changes as one new checkpoint
|
|
657
1049
|
-m, --message checkpoint message
|
|
658
1050
|
--force push even if the server moved on
|
|
1051
|
+
preview Compile the local project to JS (prints to stdout)
|
|
1052
|
+
-o, --out <file> write to a file instead of stdout
|
|
1053
|
+
--entry <path> override the entrypoint
|
|
1054
|
+
--no-header omit the metadata header
|
|
1055
|
+
--public-url <url> asset URL prefix (default: the API origin)
|
|
1056
|
+
render Render the local UI project to a semantic JSON description
|
|
1057
|
+
(needs the optional 'lecodes-render' package; UI projects only)
|
|
1058
|
+
-o, --out <file> write JSON to a file instead of stdout
|
|
1059
|
+
--png [file] rasterize a PNG (default screenshot.png;
|
|
1060
|
+
needs @napi-rs/canvas)
|
|
1061
|
+
--scale <n> PNG device-pixel scale (default 2)
|
|
1062
|
+
--width/--height viewport size (default 390x844)
|
|
1063
|
+
--entry <path> override the entrypoint
|
|
659
1064
|
|
|
660
1065
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
661
|
-
var commands = { login, clone, status, pull, push };
|
|
1066
|
+
var commands = { login, clone, status, pull, push, preview, render };
|
|
662
1067
|
var main = async () => {
|
|
663
1068
|
const argv = process.argv.slice(2);
|
|
664
1069
|
const command = argv[0];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lecodes-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Clone, edit and push LeCodes projects from your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,15 +13,31 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
+
"runtime",
|
|
16
17
|
"README.md"
|
|
17
18
|
],
|
|
18
19
|
"engines": {
|
|
19
20
|
"node": ">=18"
|
|
20
21
|
},
|
|
21
22
|
"scripts": {
|
|
22
|
-
"
|
|
23
|
+
"vendor-runtime": "bun scripts/vendor-runtime.ts",
|
|
24
|
+
"build": "bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external esbuild --external lecodes-render --banner=\"#!/usr/bin/env node\"",
|
|
23
25
|
"typecheck": "tsc --noEmit",
|
|
24
26
|
"dev": "bun run src/index.ts",
|
|
25
27
|
"prepublishOnly": "bun run build"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"esbuild": "^0.27.3"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"lecodes-render": "*"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"lecodes-render": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"lecodes-render": "workspace:*"
|
|
26
42
|
}
|
|
27
43
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common utilities
|
|
3
|
+
* @module glMatrix
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Configuration Constants
|
|
7
|
+
export var EPSILON = 0.000001;
|
|
8
|
+
export var RANDOM = Math.random;
|
|
9
|
+
export var ANGLE_ORDER = "zyx";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Symmetric round
|
|
13
|
+
* see https://www.npmjs.com/package/round-half-up-symmetric#user-content-detailed-background
|
|
14
|
+
*
|
|
15
|
+
* @param {Number} a value to round
|
|
16
|
+
*/
|
|
17
|
+
export function round(a) {
|
|
18
|
+
if (a >= 0) return Math.round(a);
|
|
19
|
+
return a % 0.5 === 0 ? Math.floor(a) : Math.round(a);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert Degree To Radian
|
|
25
|
+
*
|
|
26
|
+
* @param {Number} a Angle in Degrees
|
|
27
|
+
*/
|
|
28
|
+
export function toRadian(a) {
|
|
29
|
+
return a * 0.017453292519943295;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert Radian To Degree
|
|
34
|
+
*
|
|
35
|
+
* @param {Number} a Angle in Radians
|
|
36
|
+
*/
|
|
37
|
+
export function toDegree(a) {
|
|
38
|
+
return a * 57.29577951308232;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Tests whether or not the arguments have approximately the same value, within an absolute
|
|
43
|
+
* or relative tolerance of glMatrix.EPSILON (an absolute tolerance is used for values less
|
|
44
|
+
* than or equal to 1.0, and a relative tolerance is used for larger values)
|
|
45
|
+
*
|
|
46
|
+
* @param {Number} a The first number to test.
|
|
47
|
+
* @param {Number} b The second number to test.
|
|
48
|
+
* @param {Number} tolerance Absolute or relative tolerance (default glMatrix.EPSILON)
|
|
49
|
+
* @returns {Boolean} True if the numbers are approximately equal, false otherwise.
|
|
50
|
+
*/
|
|
51
|
+
export function equals(a, b) {
|
|
52
|
+
var tolerance = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : EPSILON;
|
|
53
|
+
return Math.abs(a - b) <= tolerance * Math.max(1, Math.abs(a), Math.abs(b));
|
|
54
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as glMatrix from "gl-matrix/common";
|
|
2
|
+
import * as mat2 from "gl-matrix/mat2";
|
|
3
|
+
import * as mat2d from "gl-matrix/mat2d";
|
|
4
|
+
import * as mat3 from "gl-matrix/mat3";
|
|
5
|
+
import * as mat4 from "gl-matrix/mat4";
|
|
6
|
+
import * as quat from "gl-matrix/quat";
|
|
7
|
+
import * as quat2 from "gl-matrix/quat2";
|
|
8
|
+
import * as vec2 from "gl-matrix/vec2";
|
|
9
|
+
import * as vec3 from "gl-matrix/vec3";
|
|
10
|
+
import * as vec4 from "gl-matrix/vec4";
|
|
11
|
+
|
|
12
|
+
export { glMatrix, mat2, mat2d, mat3, mat4, quat, quat2, vec2, vec3, vec4 };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as glMatrix from "./common.js";
|
|
2
|
+
import * as mat2 from "./mat2.js";
|
|
3
|
+
import * as mat2d from "./mat2d.js";
|
|
4
|
+
import * as mat3 from "./mat3.js";
|
|
5
|
+
import * as mat4 from "./mat4.js";
|
|
6
|
+
import * as quat from "./quat.js";
|
|
7
|
+
import * as quat2 from "./quat2.js";
|
|
8
|
+
import * as vec2 from "./vec2.js";
|
|
9
|
+
import * as vec3 from "./vec3.js";
|
|
10
|
+
import * as vec4 from "./vec4.js";
|
|
11
|
+
export { glMatrix, mat2, mat2d, mat3, mat4, quat, quat2, vec2, vec3, vec4 };
|