lecodes-cli 0.2.0 → 0.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.
- package/README.md +3 -0
- package/dist/index.js +883 -507
- package/package.json +9 -4
- package/runtime/sdk/canvas/Canvas.ts +270 -0
- package/runtime/sdk/compile/aspectMacro.ts +42 -0
- package/runtime/sdk/compile/assetMacro.ts +44 -0
- package/runtime/sdk/compile/bundler.ts +173 -0
- package/runtime/sdk/compile/compileProject.ts +84 -0
- package/runtime/sdk/compile/header.ts +46 -0
- package/runtime/sdk/compile/index.ts +12 -0
- package/runtime/sdk/compile/sourcemap.ts +25 -0
- package/runtime/sdk/core/Aspect.ts +82 -26
- package/runtime/sdk/core/signals.ts +277 -0
- package/runtime/sdk/g2/CharacterController2D.ts +5 -4
- package/runtime/sdk/g2/Node2D.ts +1 -1
- package/runtime/sdk/g2/Physics2D.ts +22 -12
- package/runtime/sdk/g2/Scene2D.ts +30 -0
- package/runtime/sdk/g2/{Shape.ts → Shape2D.ts} +10 -10
- package/runtime/sdk/g2/Sprite.ts +16 -6
- package/runtime/sdk/g2/Texture2D.ts +12 -2
- package/runtime/sdk/g2/{Trigger.ts → Trigger2D.ts} +9 -9
- package/runtime/sdk/gl/CharacterController.ts +101 -0
- package/runtime/sdk/gl/Material.ts +10 -3
- package/runtime/sdk/gl/Node.ts +11 -2
- package/runtime/sdk/gl/Physics.ts +65 -41
- package/runtime/sdk/gl/Scene.ts +18 -0
- package/runtime/sdk/gl/Shape.ts +98 -0
- package/runtime/sdk/gl/Texture.ts +13 -2
- package/runtime/sdk/gl/Trigger.ts +46 -0
- package/runtime/sdk/gl/physicsEvents.ts +20 -0
- package/runtime/sdk/inject.ts +14 -3
- package/runtime/sdk/runtime/camera.ts +31 -0
- package/runtime/sdk/runtime/device.ts +26 -0
- package/runtime/sdk/runtime/share.ts +9 -0
- package/runtime/sdk/ui/UI.ts +5 -1
- package/runtime/sdk/ui/UIButton.ts +11 -5
- package/runtime/sdk/ui/UIContainer.ts +28 -13
- package/runtime/sdk/ui/UIImage.ts +31 -11
- package/runtime/sdk/ui/UIInput.ts +10 -0
- package/runtime/sdk/ui/UINode.ts +206 -8
- package/runtime/sdk/ui/UIScreen.ts +17 -7
- package/runtime/sdk/ui/UIScrollable.ts +10 -5
- package/runtime/sdk/ui/UISpacer.ts +4 -0
- package/runtime/sdk/ui/UIText.ts +21 -5
- package/runtime/sdk/ui/UIVideo.ts +5 -0
- package/runtime/sdk/ui/UIVirtualizedList.ts +5 -0
- package/runtime/sdk/ui/UIWidget.ts +10 -5
- package/runtime/sdk/gl/Collider.ts +0 -29
package/dist/index.js
CHANGED
|
@@ -3,8 +3,8 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
|
-
import { readFileSync as
|
|
7
|
-
import { dirname as dirname7, join as
|
|
6
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
7
|
+
import { dirname as dirname7, join as join12 } from "node:path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
9
9
|
|
|
10
10
|
// src/commands/clone.ts
|
|
@@ -483,6 +483,14 @@ var create = async (args) => {
|
|
|
483
483
|
info(c.dim(` cd ${dir} — edit main.ts, then \`lecodes push -m "first commit"\` to save it`));
|
|
484
484
|
};
|
|
485
485
|
|
|
486
|
+
// src/commands/design.ts
|
|
487
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync7, appendFileSync } from "node:fs";
|
|
488
|
+
import { basename as basename2, join as join10 } from "node:path";
|
|
489
|
+
|
|
490
|
+
// src/compile/designCompile.ts
|
|
491
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
492
|
+
import { basename } from "node:path";
|
|
493
|
+
|
|
486
494
|
// src/localFiles.ts
|
|
487
495
|
import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
488
496
|
import { join as join7, relative } from "node:path";
|
|
@@ -592,193 +600,352 @@ var diffLocal = (manifest, scan) => {
|
|
|
592
600
|
};
|
|
593
601
|
var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
|
|
594
602
|
|
|
595
|
-
// src/
|
|
596
|
-
import {
|
|
597
|
-
import {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
if (server && !local) {
|
|
619
|
-
const text = isTextAsset(server.type);
|
|
620
|
-
entries.push({ path, status: "removed", binary: !text, serverText: text ? server.text ?? "" : undefined, localText: "" });
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
if (server && local) {
|
|
624
|
-
if (isTextAsset(server.type)) {
|
|
625
|
-
const serverText = server.text ?? "";
|
|
626
|
-
const localText = readFileSync5(local.absPath, "utf8");
|
|
627
|
-
if (serverText !== localText)
|
|
628
|
-
entries.push({ path, status: "modified", binary: false, serverText, localText });
|
|
629
|
-
} else {
|
|
630
|
-
const serverSize = server.file?.size;
|
|
631
|
-
const localSize = Buffer.byteLength(readFileSync5(local.absPath));
|
|
632
|
-
if (serverSize !== undefined && serverSize !== localSize) {
|
|
633
|
-
entries.push({ path, status: "modified", binary: true });
|
|
634
|
-
}
|
|
635
|
-
}
|
|
603
|
+
// src/compile/projectCompile.ts
|
|
604
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
605
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
606
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
607
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
608
|
+
// ../sdk/src/compile/bundler.ts
|
|
609
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync } from "node:fs";
|
|
610
|
+
import { spawnSync } from "node:child_process";
|
|
611
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
612
|
+
import { dirname as dirname4, join as join8, relative as relative2 } from "node:path";
|
|
613
|
+
import { fileURLToPath } from "node:url";
|
|
614
|
+
|
|
615
|
+
// ../sdk/src/compile/assetMacro.ts
|
|
616
|
+
var ASSET_RE = /\basset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
|
|
617
|
+
var hasAssetMacro = (src) => src.includes("asset(");
|
|
618
|
+
var desugarAssetMacros = (src, filePath) => {
|
|
619
|
+
const byPath = new Map;
|
|
620
|
+
let i = 0;
|
|
621
|
+
const out = src.replace(ASSET_RE, (_match, _quote, path) => {
|
|
622
|
+
let name = byPath.get(path);
|
|
623
|
+
if (!name) {
|
|
624
|
+
name = `__asset_${i++}`;
|
|
625
|
+
byPath.set(path, name);
|
|
636
626
|
}
|
|
627
|
+
return name;
|
|
628
|
+
});
|
|
629
|
+
if (/\basset\(/.test(out)) {
|
|
630
|
+
throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`);
|
|
637
631
|
}
|
|
638
|
-
|
|
632
|
+
if (byPath.size === 0)
|
|
633
|
+
return src;
|
|
634
|
+
const imports = [...byPath].map(([path, name]) => `import ${name} from ${JSON.stringify(path)};`).join(`
|
|
635
|
+
`);
|
|
636
|
+
return out + `
|
|
637
|
+
` + imports;
|
|
639
638
|
};
|
|
640
639
|
|
|
641
|
-
// src/
|
|
642
|
-
var
|
|
643
|
-
var
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
640
|
+
// ../sdk/src/compile/aspectMacro.ts
|
|
641
|
+
var ASPECT_RE = /\bclass\s+\w+\s+extends\s+Aspect\s*<\s*(["'])((?:[^"'\\]|\\.)*)\1[^{};]*>\s*\{/g;
|
|
642
|
+
var hasAspectExtends = (src) => /\bextends\s+Aspect\s*</.test(src);
|
|
643
|
+
var injectAspectNames = (src) => {
|
|
644
|
+
let out = "";
|
|
645
|
+
let last = 0;
|
|
646
|
+
ASPECT_RE.lastIndex = 0;
|
|
647
|
+
let m;
|
|
648
|
+
while ((m = ASPECT_RE.exec(src)) !== null) {
|
|
649
|
+
const braceEnd = m.index + m[0].length;
|
|
650
|
+
if (!classBodyDeclaresAspect(src, braceEnd)) {
|
|
651
|
+
out += src.slice(last, braceEnd) + ` static aspect = ${JSON.stringify(m[2])};`;
|
|
652
|
+
last = braceEnd;
|
|
649
653
|
}
|
|
650
654
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
ops.push({ tag: "+", line: b[j] });
|
|
663
|
-
j++;
|
|
664
|
-
}
|
|
655
|
+
return last === 0 ? src : out + src.slice(last);
|
|
656
|
+
};
|
|
657
|
+
var classBodyDeclaresAspect = (src, from) => {
|
|
658
|
+
let depth = 1;
|
|
659
|
+
let i = from;
|
|
660
|
+
for (;i < src.length && depth > 0; i++) {
|
|
661
|
+
const c2 = src[i];
|
|
662
|
+
if (c2 === "{")
|
|
663
|
+
depth++;
|
|
664
|
+
else if (c2 === "}")
|
|
665
|
+
depth--;
|
|
665
666
|
}
|
|
666
|
-
|
|
667
|
-
ops.push({ tag: "-", line: a[i++] });
|
|
668
|
-
while (j < m)
|
|
669
|
-
ops.push({ tag: "+", line: b[j++] });
|
|
670
|
-
return ops;
|
|
667
|
+
return /\bstatic\b[^;{}()=]*\baspect\b/.test(src.slice(from, i));
|
|
671
668
|
};
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
669
|
+
|
|
670
|
+
// ../sdk/src/compile/bundler.ts
|
|
671
|
+
var resolveInject = () => {
|
|
672
|
+
const override = process.env.LECODES_SDK_INJECT;
|
|
673
|
+
if (override && existsSync5(override))
|
|
674
|
+
return override;
|
|
675
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
676
|
+
const src = join8(here, "..", "inject.ts");
|
|
677
|
+
if (existsSync5(src))
|
|
678
|
+
return src;
|
|
679
|
+
throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)");
|
|
680
680
|
};
|
|
681
|
-
var
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const hunks = [];
|
|
691
|
-
let aLine = 0, bLine = 0;
|
|
692
|
-
let cur = null;
|
|
693
|
-
let trailingContext = 0;
|
|
694
|
-
for (let k = 0;k < ops.length; k++) {
|
|
695
|
-
const op = ops[k];
|
|
696
|
-
const isChange = op.tag !== " ";
|
|
697
|
-
if (isChange) {
|
|
698
|
-
if (!cur) {
|
|
699
|
-
const back = [];
|
|
700
|
-
let bi = k - 1;
|
|
701
|
-
while (bi >= 0 && ops[bi].tag === " " && back.length < context) {
|
|
702
|
-
back.unshift(ops[bi]);
|
|
703
|
-
bi--;
|
|
704
|
-
}
|
|
705
|
-
cur = { aStart: aLine - back.length, bStart: bLine - back.length, lines: [...back] };
|
|
706
|
-
}
|
|
707
|
-
cur.lines.push(op);
|
|
708
|
-
trailingContext = 0;
|
|
709
|
-
} else if (cur) {
|
|
710
|
-
cur.lines.push(op);
|
|
711
|
-
if (++trailingContext >= context * 2) {
|
|
712
|
-
cur.lines.length -= trailingContext - context;
|
|
713
|
-
hunks.push(cur);
|
|
714
|
-
cur = null;
|
|
715
|
-
trailingContext = 0;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
if (op.tag !== "+")
|
|
719
|
-
aLine++;
|
|
720
|
-
if (op.tag !== "-")
|
|
721
|
-
bLine++;
|
|
681
|
+
var normalize = (path) => {
|
|
682
|
+
const parts = [];
|
|
683
|
+
for (const part of path.split("/")) {
|
|
684
|
+
if (!part || part === ".")
|
|
685
|
+
continue;
|
|
686
|
+
if (part === "..")
|
|
687
|
+
parts.pop();
|
|
688
|
+
else
|
|
689
|
+
parts.push(part);
|
|
722
690
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
691
|
+
return "/" + parts.join("/");
|
|
692
|
+
};
|
|
693
|
+
var CHISEL_DEFINE = { RAD2DEG: "57.29577951308232", DEG2RAD: "0.017453292519943295" };
|
|
694
|
+
var CHISEL_KEEP = ["_*"];
|
|
695
|
+
var resolveSdkSrc = () => {
|
|
696
|
+
const inject = resolveInject();
|
|
697
|
+
if (!inject.endsWith("inject.ts")) {
|
|
698
|
+
throw new Error(`chisel needs the SDK source tree (src/inject.ts); got ${inject}. Set LECODES_SDK_INJECT to the source.`);
|
|
727
699
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
700
|
+
return dirname4(inject);
|
|
701
|
+
};
|
|
702
|
+
var isSdkSource = (path) => /\.[tj]s$/.test(path) && !path.endsWith(".d.ts") && !path.includes("/compile/") && !path.includes("/tests/") && !path.includes("/__tests__/");
|
|
703
|
+
var sdkFilesCache = new Map;
|
|
704
|
+
var collectSdkFiles = (sdkSrc) => {
|
|
705
|
+
const cached = sdkFilesCache.get(sdkSrc);
|
|
706
|
+
if (cached)
|
|
707
|
+
return cached;
|
|
708
|
+
const out = {};
|
|
709
|
+
const walk = (dir) => {
|
|
710
|
+
for (const name of readdirSync4(dir)) {
|
|
711
|
+
const p = join8(dir, name);
|
|
712
|
+
if (statSync(p).isDirectory())
|
|
713
|
+
walk(p);
|
|
714
|
+
else if (isSdkSource(p))
|
|
715
|
+
out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync5(p, "utf8");
|
|
740
716
|
}
|
|
717
|
+
};
|
|
718
|
+
walk(sdkSrc);
|
|
719
|
+
sdkFilesCache.set(sdkSrc, out);
|
|
720
|
+
return out;
|
|
721
|
+
};
|
|
722
|
+
var resolveChiselBin = () => {
|
|
723
|
+
const override = process.env.LECODES_CHISEL_BIN;
|
|
724
|
+
if (override) {
|
|
725
|
+
if (existsSync5(override))
|
|
726
|
+
return override;
|
|
727
|
+
throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`);
|
|
741
728
|
}
|
|
742
|
-
|
|
743
|
-
|
|
729
|
+
try {
|
|
730
|
+
const bin = createRequire2(import.meta.url)("@letary/chisel").binaryPath();
|
|
731
|
+
if (bin && existsSync5(bin))
|
|
732
|
+
return bin;
|
|
733
|
+
} catch {}
|
|
734
|
+
throw new Error("chisel binary not found. Install @letary/chisel, or set LECODES_CHISEL_BIN.");
|
|
744
735
|
};
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const keys = args._.map(toKey);
|
|
759
|
-
entries = entries.filter((e) => keys.some((k) => e.path === k || e.path.startsWith(k + "/")));
|
|
736
|
+
var bundleProjectChisel = async (entrypoint, files, format, sourcemap) => {
|
|
737
|
+
const entry = normalize(entrypoint);
|
|
738
|
+
const projFiles = {};
|
|
739
|
+
for (const [k, v] of Object.entries(files)) {
|
|
740
|
+
const path = normalize(k);
|
|
741
|
+
let src = v;
|
|
742
|
+
if (/\.[tj]sx?$/.test(path)) {
|
|
743
|
+
if (hasAssetMacro(src))
|
|
744
|
+
src = desugarAssetMacros(src, path);
|
|
745
|
+
if (hasAspectExtends(src))
|
|
746
|
+
src = injectAspectNames(src);
|
|
747
|
+
}
|
|
748
|
+
projFiles[path] = src;
|
|
760
749
|
}
|
|
761
|
-
if (
|
|
762
|
-
|
|
763
|
-
|
|
750
|
+
if (!projFiles[entry])
|
|
751
|
+
throw new Error(`Entrypoint not found: ${entry}`);
|
|
752
|
+
const sdkFiles = collectSdkFiles(resolveSdkSrc());
|
|
753
|
+
const minify = process.env.LECODES_CHISEL_MINIFY !== "0";
|
|
754
|
+
const input = JSON.stringify({
|
|
755
|
+
files: { ...sdkFiles, ...projFiles },
|
|
756
|
+
entry,
|
|
757
|
+
inject: ["/__sdk/inject.ts"],
|
|
758
|
+
format,
|
|
759
|
+
minify,
|
|
760
|
+
fuse: process.env.LECODES_CHISEL_FUSE === "1",
|
|
761
|
+
define: CHISEL_DEFINE,
|
|
762
|
+
keep: CHISEL_KEEP,
|
|
763
|
+
sourcemap,
|
|
764
|
+
reactive_ui: process.env.LECODES_CHISEL_REACTIVE_UI !== "0"
|
|
765
|
+
});
|
|
766
|
+
const bin = resolveChiselBin();
|
|
767
|
+
const r = spawnSync(bin, [], { input, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
768
|
+
if (r.status === 2 || r.error) {
|
|
769
|
+
throw new Error(`chisel failed: ${r.error?.message ?? r.stderr ?? "unknown error"}`);
|
|
764
770
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
771
|
+
let parsed;
|
|
772
|
+
try {
|
|
773
|
+
parsed = JSON.parse(r.stdout || "{}");
|
|
774
|
+
} catch {
|
|
775
|
+
throw new Error(`chisel produced invalid output: ${(r.stdout || r.stderr || "").slice(0, 200)}`);
|
|
776
|
+
}
|
|
777
|
+
if (parsed.error)
|
|
778
|
+
throw new Error(`chisel: ${parsed.error}`);
|
|
779
|
+
if (parsed.code == null)
|
|
780
|
+
throw new Error("chisel produced no code");
|
|
781
|
+
return { code: parsed.code, map: parsed.map ?? null };
|
|
782
|
+
};
|
|
783
|
+
var bundleProjectWithMap = async (entrypoint, files, format = "esm") => bundleProjectChisel(entrypoint, files, format, true);
|
|
784
|
+
|
|
785
|
+
// ../sdk/src/compile/header.ts
|
|
786
|
+
var buildHeader = (name, code, opts = {}) => {
|
|
787
|
+
let header = `// name: ${name.slice(0, 20)}
|
|
788
|
+
`;
|
|
789
|
+
if (opts.icon)
|
|
790
|
+
header += `// icon: ${opts.icon}
|
|
791
|
+
`;
|
|
792
|
+
header += `// last-updated: ${new Date().toISOString()}
|
|
793
|
+
`;
|
|
794
|
+
const localAssets = new Set;
|
|
795
|
+
for (const m of code.matchAll(/_creatorUtils\.fetchLocal\((.+?)\)/g)) {
|
|
796
|
+
localAssets.add(m[1].replaceAll(/['"]/g, "").trim());
|
|
769
797
|
}
|
|
798
|
+
if (/_creator\.createGlb/.test(code))
|
|
799
|
+
localAssets.add("uberarchive");
|
|
800
|
+
if (/_creator\.captureImage/.test(code))
|
|
801
|
+
localAssets.add("capture");
|
|
802
|
+
if (localAssets.size > 0)
|
|
803
|
+
header += `// preload: ${[...localAssets].join(";")}
|
|
804
|
+
`;
|
|
805
|
+
if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
|
|
806
|
+
header += `// gl: disable
|
|
807
|
+
`;
|
|
808
|
+
else if (code.includes("_creator.createEngine"))
|
|
809
|
+
header += `// gl: defer
|
|
810
|
+
`;
|
|
811
|
+
if (code.includes("_creator2d."))
|
|
812
|
+
header += `// 2d: enable
|
|
813
|
+
`;
|
|
814
|
+
if (code.includes("_creator.createARController"))
|
|
815
|
+
header += "// ar: enable";
|
|
816
|
+
header += `
|
|
817
|
+
`;
|
|
818
|
+
return header;
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
// ../sdk/src/compile/sourcemap.ts
|
|
822
|
+
var offsetSourceMap = (mapJson, lines) => {
|
|
823
|
+
if (lines <= 0)
|
|
824
|
+
return mapJson;
|
|
825
|
+
const map = JSON.parse(mapJson);
|
|
826
|
+
map.mappings = ";".repeat(lines) + map.mappings;
|
|
827
|
+
return JSON.stringify(map);
|
|
828
|
+
};
|
|
829
|
+
var inlineSourceMapComment = (mapJson) => {
|
|
830
|
+
const b64 = Buffer.from(mapJson, "utf8").toString("base64");
|
|
831
|
+
return `//# lecodesMap=data:application/json;charset=utf-8;base64,${b64}`;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
// ../sdk/src/compile/compileProject.ts
|
|
835
|
+
var detectEntry = (entries) => {
|
|
836
|
+
let entry = "";
|
|
837
|
+
let max = -1;
|
|
770
838
|
for (const e of entries) {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
839
|
+
const ext2 = e.path.split(".").at(-1)?.toLowerCase();
|
|
840
|
+
if (e.type === "text" && e.text !== undefined && !e.text.includes("export") && (ext2 === "ts" || ext2 === "js")) {
|
|
841
|
+
if (e.text.length > max) {
|
|
842
|
+
entry = e.path;
|
|
843
|
+
max = e.text.length;
|
|
844
|
+
}
|
|
774
845
|
}
|
|
775
|
-
const d = unifiedDiff(e.serverText ?? "", e.localText ?? "", `server${e.path}`, `local${e.path}`);
|
|
776
|
-
if (d)
|
|
777
|
-
log(`
|
|
778
|
-
` + d);
|
|
779
846
|
}
|
|
847
|
+
return entry;
|
|
848
|
+
};
|
|
849
|
+
var compileProject = async (opts) => {
|
|
850
|
+
const entry = opts.entryOverride ?? detectEntry(opts.entries);
|
|
851
|
+
if (!entry) {
|
|
852
|
+
throw new Error("Couldn't find an entrypoint (a top-level .ts/.js file with no `export`). Pass entryOverride.");
|
|
853
|
+
}
|
|
854
|
+
const files = {};
|
|
855
|
+
for (const e of opts.entries) {
|
|
856
|
+
if (e.type === "resource") {
|
|
857
|
+
files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
|
|
858
|
+
} else if (e.type === "shader") {
|
|
859
|
+
const shaders = e.shaders ?? [];
|
|
860
|
+
if (shaders.length > 2) {
|
|
861
|
+
files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``;
|
|
862
|
+
} else if (shaders.length > 0) {
|
|
863
|
+
files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
|
|
864
|
+
} else {
|
|
865
|
+
files[e.path] = `export default ""`;
|
|
866
|
+
}
|
|
867
|
+
} else if (e.path.toLowerCase().endsWith(".svg")) {
|
|
868
|
+
files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
|
|
869
|
+
} else {
|
|
870
|
+
files[e.path] = e.text ?? "";
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm");
|
|
874
|
+
if (opts.header === false) {
|
|
875
|
+
return map ? `${code}
|
|
876
|
+
${inlineSourceMapComment(map)}` : code;
|
|
877
|
+
}
|
|
878
|
+
const header = buildHeader(opts.name, code, { icon: opts.icon });
|
|
879
|
+
if (!map)
|
|
880
|
+
return header + code;
|
|
881
|
+
const headerLines = (header.match(/\n/g) ?? []).length;
|
|
882
|
+
return `${header}${code}
|
|
883
|
+
${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`;
|
|
884
|
+
};
|
|
885
|
+
// src/compile/projectCompile.ts
|
|
886
|
+
var here = dirname5(fileURLToPath2(import.meta.url));
|
|
887
|
+
var vendoredInject = join9(here, "..", "runtime", "sdk", "inject.ts");
|
|
888
|
+
if (existsSync6(vendoredInject))
|
|
889
|
+
process.env.LECODES_SDK_INJECT = vendoredInject;
|
|
890
|
+
var resolveChiselBin2 = () => {
|
|
891
|
+
if (process.env.LECODES_CHISEL_BIN)
|
|
892
|
+
return process.env.LECODES_CHISEL_BIN;
|
|
893
|
+
try {
|
|
894
|
+
return createRequire3(import.meta.url)("@letary/chisel").binaryPath() ?? null;
|
|
895
|
+
} catch {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
var chiselBin = resolveChiselBin2();
|
|
900
|
+
if (chiselBin)
|
|
901
|
+
process.env.LECODES_CHISEL_BIN = chiselBin;
|
|
902
|
+
var compileProject2 = (opts) => compileProject({
|
|
903
|
+
entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, shaders: e.shaders })),
|
|
904
|
+
name: opts.name,
|
|
905
|
+
publicUrl: opts.publicUrl,
|
|
906
|
+
entryOverride: opts.entryOverride,
|
|
907
|
+
header: opts.header,
|
|
908
|
+
format: opts.format
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// src/compile/designCompile.ts
|
|
912
|
+
var LOCAL_ASSET_SCHEME = "lecodesfile://";
|
|
913
|
+
var compileDesignScreen = async (opts) => {
|
|
914
|
+
const { root, manifest, designKey, screenId } = opts;
|
|
915
|
+
const entries = [];
|
|
916
|
+
const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
|
|
917
|
+
for (const file of scan.files) {
|
|
918
|
+
const type = manifest?.files[file.path]?.type ?? classifyKind(basename(file.path));
|
|
919
|
+
if (type === "resource") {
|
|
920
|
+
entries.push({
|
|
921
|
+
path: file.path,
|
|
922
|
+
type,
|
|
923
|
+
fileSrc: opts.assets === "disk" ? encodeURIComponent(file.absPath) : encodeURI(file.path),
|
|
924
|
+
absPath: file.absPath
|
|
925
|
+
});
|
|
926
|
+
} else if (type === "shader") {
|
|
927
|
+
entries.push({ path: file.path, type, shaders: [] });
|
|
928
|
+
} else {
|
|
929
|
+
entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const entryPath = `${designKey}/.screen-entry.ts`;
|
|
933
|
+
entries.push({
|
|
934
|
+
path: entryPath,
|
|
935
|
+
type: "text",
|
|
936
|
+
text: `import screen from "./screens/${screenId}"
|
|
937
|
+
screen.open()
|
|
938
|
+
`
|
|
939
|
+
});
|
|
940
|
+
return compileProject2({
|
|
941
|
+
entries,
|
|
942
|
+
name: opts.name,
|
|
943
|
+
publicUrl: opts.assets === "disk" ? LOCAL_ASSET_SCHEME : "/project",
|
|
944
|
+
entryOverride: entryPath,
|
|
945
|
+
header: false,
|
|
946
|
+
format: "iife"
|
|
947
|
+
});
|
|
780
948
|
};
|
|
781
|
-
var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
|
|
782
949
|
|
|
783
950
|
// src/browserAuth.ts
|
|
784
951
|
import { createServer } from "node:http";
|
|
@@ -858,359 +1025,556 @@ var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser
|
|
|
858
1025
|
}
|
|
859
1026
|
};
|
|
860
1027
|
|
|
861
|
-
// src/commands/
|
|
862
|
-
var
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1028
|
+
// src/commands/designTemplates.ts
|
|
1029
|
+
var META_JSON = `{
|
|
1030
|
+
"screens": {
|
|
1031
|
+
"home": { "pos": [0, 0], "desc": "Example screen — replace me" }
|
|
1032
|
+
},
|
|
1033
|
+
"edges": []
|
|
1034
|
+
}
|
|
1035
|
+
`;
|
|
1036
|
+
var TOKENS_TS = `// Design tokens shared by every screen. Colors are hex strings (the engine parses hex only).
|
|
1037
|
+
export const colors = {
|
|
1038
|
+
bg: "#101114",
|
|
1039
|
+
card: "#1c1e24",
|
|
1040
|
+
text: "#f2f3f5",
|
|
1041
|
+
muted: "#8a919e",
|
|
1042
|
+
accent: "#5b8cff",
|
|
1043
|
+
danger: "#e5484d",
|
|
1044
|
+
}
|
|
871
1045
|
|
|
872
|
-
|
|
873
|
-
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
874
|
-
import { resolve as resolve3 } from "node:path";
|
|
1046
|
+
export const spacing = { xs: 4, sm: 8, md: 16, lg: 24 }
|
|
875
1047
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
import {
|
|
879
|
-
var collectEntries = (root, manifest, project) => {
|
|
880
|
-
const pathById = buildPaths(project.assets);
|
|
881
|
-
const serverByPath = new Map;
|
|
882
|
-
for (const asset of project.assets) {
|
|
883
|
-
if (asset.type === "folder")
|
|
884
|
-
continue;
|
|
885
|
-
serverByPath.set(pathById.get(asset.id), { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] });
|
|
886
|
-
}
|
|
887
|
-
const entries = [];
|
|
888
|
-
const warnings = [];
|
|
889
|
-
for (const file of scanLocal(root).files) {
|
|
890
|
-
const type = manifest.files[file.path]?.type ?? classifyKind(basename2(file.path));
|
|
891
|
-
if (type === "resource") {
|
|
892
|
-
const src = serverByPath.get(file.path)?.fileSrc;
|
|
893
|
-
if (!src)
|
|
894
|
-
warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`);
|
|
895
|
-
entries.push({ path: file.path, type, fileSrc: src, absPath: file.absPath });
|
|
896
|
-
} else if (type === "shader") {
|
|
897
|
-
const shaders = serverByPath.get(file.path)?.shaders;
|
|
898
|
-
if (!shaders?.length)
|
|
899
|
-
warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
|
|
900
|
-
entries.push({ path: file.path, type, shaders });
|
|
901
|
-
} else {
|
|
902
|
-
entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
return { entries, warnings };
|
|
906
|
-
};
|
|
1048
|
+
export const radius = { sm: 8, md: 12, lg: 20 }
|
|
1049
|
+
`;
|
|
1050
|
+
var HOME_SCREEN_TS = `import { colors, spacing, radius } from "../shared/tokens"
|
|
907
1051
|
|
|
908
|
-
//
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync } from "node:fs";
|
|
915
|
-
import { spawnSync } from "node:child_process";
|
|
916
|
-
import { createRequire as createRequire2 } from "node:module";
|
|
917
|
-
import { dirname as dirname4, join as join8, relative as relative2 } from "node:path";
|
|
918
|
-
import { fileURLToPath } from "node:url";
|
|
1052
|
+
// Mock data: the domain shown on realistic values. Inline, per screen.
|
|
1053
|
+
const items = [
|
|
1054
|
+
{ title: "Design the home screen", done: true },
|
|
1055
|
+
{ title: "Sketch the flow on the canvas", done: false },
|
|
1056
|
+
{ title: "Ask Claude to add a details screen", done: false },
|
|
1057
|
+
]
|
|
919
1058
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1059
|
+
export default UIScreen([
|
|
1060
|
+
UIText("Today").style({ fontSize: 28, fontWeight: 700, color: colors.text, mt: spacing.lg }),
|
|
1061
|
+
UIText("3 tasks").style({ fontSize: 14, color: colors.muted, mt: spacing.xs }),
|
|
1062
|
+
UIColumn(items.map((item) =>
|
|
1063
|
+
UIRow([
|
|
1064
|
+
UIColumn(item.done ? [ UIText("✓").style({ fontSize: 12, color: colors.text }) ] : []).style({
|
|
1065
|
+
width: 22,
|
|
1066
|
+
height: 22,
|
|
1067
|
+
borderRadius: 11,
|
|
1068
|
+
justifyContent: "center",
|
|
1069
|
+
alignItems: "center",
|
|
1070
|
+
border: item.done ? undefined : \`2px solid \${colors.muted}\`,
|
|
1071
|
+
bgColor: item.done ? colors.accent : undefined,
|
|
1072
|
+
}),
|
|
1073
|
+
UIText(item.title).style({ fontSize: 16, color: item.done ? colors.muted : colors.text, flexGrow: 1 }),
|
|
1074
|
+
]).style({
|
|
1075
|
+
bgColor: colors.card,
|
|
1076
|
+
borderRadius: radius.md,
|
|
1077
|
+
p: spacing.md,
|
|
1078
|
+
gap: spacing.md,
|
|
1079
|
+
alignItems: "center",
|
|
1080
|
+
}),
|
|
1081
|
+
)).style({ gap: spacing.sm, mt: spacing.lg }),
|
|
1082
|
+
UISpacer(),
|
|
1083
|
+
UIButton([
|
|
1084
|
+
UIText("Add task").style({ fontSize: 16, fontWeight: 600, color: colors.text }),
|
|
1085
|
+
]).style({
|
|
1086
|
+
name: "add-task",
|
|
1087
|
+
bgColor: colors.accent,
|
|
1088
|
+
borderRadius: radius.lg,
|
|
1089
|
+
p: spacing.md,
|
|
1090
|
+
mb: spacing.lg,
|
|
1091
|
+
}),
|
|
1092
|
+
]).style({ bgColor: colors.bg, p: spacing.md, pt: "safe-top" })
|
|
1093
|
+
`;
|
|
1094
|
+
var CLAUDE_MD = `# LeCodes Design — screen prototyping canvas
|
|
1095
|
+
|
|
1096
|
+
This folder is a **design canvas** for a LeCodes app: each file in \`screens/\` is one live,
|
|
1097
|
+
executable screen shown on a spatial canvas in the browser (\`lecodes design\` runs the dev server).
|
|
1098
|
+
You (Claude) write the screens; the developer views them on the canvas and gives feedback.
|
|
1099
|
+
This is an intent-capture stage — screens run on inline mock data, before any backend exists.
|
|
1100
|
+
|
|
1101
|
+
## Read this first, then meta.json
|
|
1102
|
+
|
|
1103
|
+
\`meta.json\` is the app map: every screen (position, description) and the edges between them
|
|
1104
|
+
(navigation intent). **Read it before opening any screen file.** It is the human's file — the
|
|
1105
|
+
canvas and the developer own positions and descriptions. You MAY add entries for new screens you
|
|
1106
|
+
create (pick a free position: screens are 390×844, leave ~160px gaps) and add edges when the flow
|
|
1107
|
+
is clear, but never reposition or rewrite existing human-authored descriptions.
|
|
1108
|
+
|
|
1109
|
+
\`\`\`json
|
|
1110
|
+
{
|
|
1111
|
+
"screens": { "login": { "pos": [0, 0], "desc": "Phone login" } },
|
|
1112
|
+
"edges": [ { "from": "login", "to": "code", "label": "after phone entry", "activator": "submit" } ]
|
|
1113
|
+
}
|
|
1114
|
+
\`\`\`
|
|
1115
|
+
|
|
1116
|
+
- An edge's optional \`activator\` names the element (its \`name\` style option) that triggers the
|
|
1117
|
+
transition. Give a \`name\` to interactive elements an edge refers to — nothing else needs names.
|
|
1118
|
+
|
|
1119
|
+
## Screen rules
|
|
1120
|
+
|
|
1121
|
+
- **One file = one screen.** \`screens/<id>.ts\` default-exports an already-computed screen:
|
|
1122
|
+
\`export default UIScreen([ … ]).style({ … })\`. The filename is the screen's identity.
|
|
1123
|
+
- **Styles go through the chained \`.style({ … })\` method** — e.g.
|
|
1124
|
+
\`UIText("Today").style({ fontSize: 28, fontWeight: 700 })\`. Passing a style object as the
|
|
1125
|
+
first argument is a legacy form.
|
|
1126
|
+
- **Screens are islands.** A screen file imports ONLY from \`../shared/\` — never from another
|
|
1127
|
+
screen, and screens take no props. All relational info lives in meta.json.
|
|
1128
|
+
- **Mock data is the domain.** Put realistic inline mock data at the top of the screen file
|
|
1129
|
+
(entities + fields ARE the future schema — make them meaningful, not "Lorem ipsum").
|
|
1130
|
+
- **Reuse via shared/.** Design tokens and reusable components live in \`shared/\`. Use them in
|
|
1131
|
+
every screen. Promoting a repeated pattern into \`shared/\` is an explicit, deliberate step.
|
|
1132
|
+
- The LeCodes UI API (\`UIScreen\`, \`UIColumn\`, \`UIRow\`, \`UIBox\`, \`UIText\`, \`UIImage\`,
|
|
1133
|
+
\`UIButton\`, \`UIInput\`, \`UISpacer\`, \`UIScrollable\`, …) is **global** — no imports needed.
|
|
1134
|
+
Colors must be hex strings.
|
|
1135
|
+
|
|
1136
|
+
## See what you built
|
|
1137
|
+
|
|
1138
|
+
The developer watches the canvas live (screens hot-reload on save). To see a screen yourself:
|
|
1139
|
+
|
|
1140
|
+
\`\`\`sh
|
|
1141
|
+
lecodes design snapshot <id> # semantic JSON of the laid-out screen → stdout
|
|
1142
|
+
lecodes design snapshot <id> --png # + PNG screenshot → design/.snapshots/<id>.png
|
|
1143
|
+
lecodes design snapshot # all screens → design/.snapshots/
|
|
1144
|
+
\`\`\`
|
|
1145
|
+
|
|
1146
|
+
Snapshot after every screen you create or change, and fix what looks wrong (overflow, overlap,
|
|
1147
|
+
unreadable contrast, off-screen content) before reporting back.
|
|
1148
|
+
|
|
1149
|
+
## Workflow
|
|
1150
|
+
|
|
1151
|
+
1. Read \`meta.json\`, then the \`shared/\` signatures, then only the screens you need.
|
|
1152
|
+
2. Create/edit screen files; register new screens (and clear edges) in \`meta.json\`.
|
|
1153
|
+
3. Snapshot → verify → fix.
|
|
1154
|
+
4. When the developer edits a description or the flow on the canvas, treat meta.json as the
|
|
1155
|
+
updated spec.
|
|
1156
|
+
`;
|
|
1157
|
+
var README_HINT = `# design/
|
|
1158
|
+
|
|
1159
|
+
LeCodes Design canvas — run \`lecodes design\` here and open the printed URL.
|
|
1160
|
+
See CLAUDE.md for the conventions (it's written for AI assistants, but it's the spec).
|
|
1161
|
+
`;
|
|
1162
|
+
|
|
1163
|
+
// src/commands/design.ts
|
|
1164
|
+
var DEFAULT_DIR = "design";
|
|
1165
|
+
var DEFAULT_PORT = 4477;
|
|
1166
|
+
var getContext = (args) => {
|
|
1167
|
+
let root;
|
|
1168
|
+
let manifest;
|
|
1169
|
+
try {
|
|
1170
|
+
root = findProjectRoot(process.cwd());
|
|
1171
|
+
manifest = readManifest(root);
|
|
1172
|
+
} catch {
|
|
1173
|
+
root = process.cwd();
|
|
1174
|
+
manifest = null;
|
|
936
1175
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1176
|
+
const dirName = (flagStr(args, "dir") ?? DEFAULT_DIR).replace(/[/\\]+$/, "");
|
|
1177
|
+
return {
|
|
1178
|
+
root,
|
|
1179
|
+
manifest,
|
|
1180
|
+
name: manifest?.name ?? basename2(root),
|
|
1181
|
+
dirName,
|
|
1182
|
+
designDir: join10(root, dirName),
|
|
1183
|
+
designKey: "/" + dirName.replace(/\\/g, "/")
|
|
1184
|
+
};
|
|
943
1185
|
};
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1186
|
+
var listScreens = (designDir) => {
|
|
1187
|
+
const dir = join10(designDir, "screens");
|
|
1188
|
+
if (!existsSync7(dir))
|
|
1189
|
+
return [];
|
|
1190
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".ts") && !f.startsWith(".")).map((f) => f.slice(0, -3)).sort();
|
|
1191
|
+
};
|
|
1192
|
+
var readMetaSizes = (designDir) => {
|
|
1193
|
+
try {
|
|
1194
|
+
const meta = JSON.parse(readFileSync7(join10(designDir, "meta.json"), "utf8"));
|
|
1195
|
+
return meta?.screens ?? {};
|
|
1196
|
+
} catch {
|
|
1197
|
+
return {};
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
var init = async (ctx) => {
|
|
1201
|
+
const files = [
|
|
1202
|
+
["meta.json", META_JSON],
|
|
1203
|
+
["README.md", README_HINT],
|
|
1204
|
+
["CLAUDE.md", CLAUDE_MD],
|
|
1205
|
+
[join10("shared", "tokens.ts"), TOKENS_TS],
|
|
1206
|
+
[join10("screens", "home.ts"), HOME_SCREEN_TS]
|
|
1207
|
+
];
|
|
1208
|
+
mkdirSync7(join10(ctx.designDir, "screens"), { recursive: true });
|
|
1209
|
+
mkdirSync7(join10(ctx.designDir, "shared"), { recursive: true });
|
|
1210
|
+
let created = 0;
|
|
1211
|
+
for (const [rel, content] of files) {
|
|
1212
|
+
const abs = join10(ctx.designDir, rel);
|
|
1213
|
+
if (existsSync7(abs)) {
|
|
1214
|
+
note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
writeFileSync7(abs, content);
|
|
1218
|
+
log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
|
|
1219
|
+
created++;
|
|
1220
|
+
}
|
|
1221
|
+
const ignorePath = join10(ctx.root, ".lecodesignore");
|
|
1222
|
+
const ignoreLine = `${ctx.dirName}/.snapshots/`;
|
|
1223
|
+
const current = existsSync7(ignorePath) ? readFileSync7(ignorePath, "utf8") : "";
|
|
1224
|
+
if (!current.split(/\r?\n/).includes(ignoreLine)) {
|
|
1225
|
+
appendFileSync(ignorePath, (current && !current.endsWith(`
|
|
1226
|
+
`) ? `
|
|
1227
|
+
` : "") + ignoreLine + `
|
|
1228
|
+
`);
|
|
1229
|
+
log(`${c.green("ignored")} ${ignoreLine} (in .lecodesignore)`);
|
|
1230
|
+
}
|
|
1231
|
+
log("");
|
|
1232
|
+
log(created ? "Design canvas ready." : "Design canvas already initialized.");
|
|
1233
|
+
log(` ${c.bold("lecodes design")} start the canvas (browser)`);
|
|
1234
|
+
log(` ${c.bold("lecodes design snapshot")} render screens headless (for AI feedback)`);
|
|
1235
|
+
log(` Point Claude Code at ${c.bold(ctx.dirName + "/CLAUDE.md")} — it documents the conventions.`);
|
|
1236
|
+
};
|
|
1237
|
+
var snapshot = async (ctx, args) => {
|
|
1238
|
+
const all = listScreens(ctx.designDir);
|
|
1239
|
+
if (all.length === 0)
|
|
1240
|
+
throw new CliError(`No screens in ${ctx.dirName}/screens/ — run "lecodes design init" first.`);
|
|
1241
|
+
const requested = args._.slice(1);
|
|
1242
|
+
for (const id of requested) {
|
|
1243
|
+
if (!all.includes(id))
|
|
1244
|
+
throw new CliError(`No screen "${id}" (have: ${all.join(", ")}).`);
|
|
1245
|
+
}
|
|
1246
|
+
const targets = requested.length ? requested : all;
|
|
1247
|
+
let renderer;
|
|
1248
|
+
try {
|
|
1249
|
+
renderer = await import("lecodes-renderer/headless");
|
|
1250
|
+
} catch {
|
|
1251
|
+
throw new CliError("The renderer isn't installed. Run: npm i -D lecodes-renderer");
|
|
1252
|
+
}
|
|
1253
|
+
const killer = setTimeout(() => {
|
|
1254
|
+
warnErr("Snapshot timed out after 120s.");
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
}, 120000);
|
|
1257
|
+
killer.unref?.();
|
|
1258
|
+
const png = flagBool(args, "png") || flagStr(args, "png") !== undefined;
|
|
1259
|
+
const sizes = readMetaSizes(ctx.designDir);
|
|
1260
|
+
const outDir = flagStr(args, "out-dir") ?? join10(ctx.designDir, ".snapshots");
|
|
1261
|
+
const single = targets.length === 1 && requested.length === 1 && !png && !flagStr(args, "out-dir");
|
|
1262
|
+
const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
|
|
1263
|
+
if (!single)
|
|
1264
|
+
mkdirSync7(outDir, { recursive: true });
|
|
1265
|
+
for (const id of targets) {
|
|
1266
|
+
note(`Rendering ${id}…`);
|
|
1267
|
+
const js = await compileDesignScreen({
|
|
1268
|
+
root: ctx.root,
|
|
1269
|
+
manifest: ctx.manifest,
|
|
1270
|
+
name: ctx.name,
|
|
1271
|
+
designKey: ctx.designKey,
|
|
1272
|
+
screenId: id,
|
|
1273
|
+
assets: "disk"
|
|
1274
|
+
});
|
|
1275
|
+
const [width, height] = sizes[id]?.size ?? [390, 844];
|
|
1276
|
+
const result = await renderer.renderToJson(js, { width, height, localAssets: true, onConsole });
|
|
1277
|
+
for (const w of result.warnings)
|
|
1278
|
+
warnErr(`${id}: ${w}`);
|
|
1279
|
+
if (single) {
|
|
1280
|
+
const text = result.text.endsWith(`
|
|
1281
|
+
`) ? result.text : result.text + `
|
|
1282
|
+
`;
|
|
1283
|
+
process.stdout.write(text);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
writeFileSync7(join10(outDir, `${id}.json`), result.text);
|
|
1287
|
+
log(`${c.green("✓")} ${id} → ${join10(outDir, `${id}.json`)}`);
|
|
1288
|
+
if (png) {
|
|
1289
|
+
const pngResult = await renderer.renderToPng(js, { width, height, localAssets: true, scale: 2, onConsole });
|
|
1290
|
+
for (const w of pngResult.warnings)
|
|
1291
|
+
warnErr(`${id}: ${w}`);
|
|
1292
|
+
writeFileSync7(join10(outDir, `${id}.png`), pngResult.png);
|
|
1293
|
+
log(`${c.green("✓")} ${id} → ${join10(outDir, `${id}.png`)}`);
|
|
958
1294
|
}
|
|
959
1295
|
}
|
|
960
|
-
return last === 0 ? src : out + src.slice(last);
|
|
961
1296
|
};
|
|
962
|
-
var
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
for (;i < src.length && depth > 0; i++) {
|
|
966
|
-
const c2 = src[i];
|
|
967
|
-
if (c2 === "{")
|
|
968
|
-
depth++;
|
|
969
|
-
else if (c2 === "}")
|
|
970
|
-
depth--;
|
|
1297
|
+
var serve = async (ctx, args) => {
|
|
1298
|
+
if (!existsSync7(ctx.designDir)) {
|
|
1299
|
+
throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
|
|
971
1300
|
}
|
|
972
|
-
|
|
1301
|
+
let designServer;
|
|
1302
|
+
try {
|
|
1303
|
+
designServer = await import("lecodes-design/server");
|
|
1304
|
+
} catch {
|
|
1305
|
+
throw new CliError("The design canvas isn't installed. Run: npm i -D lecodes-design");
|
|
1306
|
+
}
|
|
1307
|
+
const port = Number(flagStr(args, "port")) || DEFAULT_PORT;
|
|
1308
|
+
const server = await designServer.startDesignServer({
|
|
1309
|
+
root: ctx.root,
|
|
1310
|
+
designDir: ctx.designDir,
|
|
1311
|
+
name: ctx.name,
|
|
1312
|
+
port,
|
|
1313
|
+
log: note,
|
|
1314
|
+
compileScreen: async (id) => ({
|
|
1315
|
+
js: await compileDesignScreen({
|
|
1316
|
+
root: ctx.root,
|
|
1317
|
+
manifest: ctx.manifest,
|
|
1318
|
+
name: ctx.name,
|
|
1319
|
+
designKey: ctx.designKey,
|
|
1320
|
+
screenId: id,
|
|
1321
|
+
assets: "server"
|
|
1322
|
+
}),
|
|
1323
|
+
warnings: []
|
|
1324
|
+
})
|
|
1325
|
+
});
|
|
1326
|
+
log(`${c.bold("LeCodes Design")} — ${ctx.name}`);
|
|
1327
|
+
log(`Canvas: ${c.bold(server.url)}`);
|
|
1328
|
+
log(`Watching: ${ctx.dirName}/ (screens hot-reload on save; Ctrl+C to stop)`);
|
|
1329
|
+
if (!flagBool(args, "no-open"))
|
|
1330
|
+
openBrowser(server.url);
|
|
1331
|
+
};
|
|
1332
|
+
var design = async (args) => {
|
|
1333
|
+
const sub = args._[0] ?? "serve";
|
|
1334
|
+
const ctx = getContext(args);
|
|
1335
|
+
if (sub === "serve")
|
|
1336
|
+
return serve(ctx, args);
|
|
1337
|
+
if (sub === "init")
|
|
1338
|
+
return init(ctx);
|
|
1339
|
+
if (sub === "snapshot")
|
|
1340
|
+
return snapshot(ctx, args);
|
|
1341
|
+
throw new CliError(`Unknown design subcommand "${sub}" (serve | init | snapshot).`);
|
|
973
1342
|
};
|
|
974
1343
|
|
|
975
|
-
//
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)");
|
|
985
|
-
};
|
|
986
|
-
var normalize = (path) => {
|
|
987
|
-
const parts = [];
|
|
988
|
-
for (const part of path.split("/")) {
|
|
989
|
-
if (!part || part === ".")
|
|
1344
|
+
// src/serverDiff.ts
|
|
1345
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
1346
|
+
import { basename as basename3 } from "node:path";
|
|
1347
|
+
var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
|
|
1348
|
+
var computeServerDiff = (project, scan) => {
|
|
1349
|
+
const pathById = buildPaths(project.assets);
|
|
1350
|
+
const serverByPath = new Map;
|
|
1351
|
+
for (const asset of project.assets) {
|
|
1352
|
+
if (asset.type === "folder")
|
|
990
1353
|
continue;
|
|
991
|
-
|
|
992
|
-
parts.pop();
|
|
993
|
-
else
|
|
994
|
-
parts.push(part);
|
|
1354
|
+
serverByPath.set(pathById.get(asset.id), asset);
|
|
995
1355
|
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1356
|
+
const localByPath = new Map(scan.files.map((f) => [f.path, f]));
|
|
1357
|
+
const entries = [];
|
|
1358
|
+
const paths = new Set([...serverByPath.keys(), ...localByPath.keys()]);
|
|
1359
|
+
for (const path of [...paths].sort()) {
|
|
1360
|
+
const server = serverByPath.get(path);
|
|
1361
|
+
const local = localByPath.get(path);
|
|
1362
|
+
if (local && !server) {
|
|
1363
|
+
const text = isTextKind(classifyKind(basename3(path)));
|
|
1364
|
+
entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync8(local.absPath, "utf8") : undefined });
|
|
1365
|
+
continue;
|
|
1366
|
+
}
|
|
1367
|
+
if (server && !local) {
|
|
1368
|
+
const text = isTextAsset(server.type);
|
|
1369
|
+
entries.push({ path, status: "removed", binary: !text, serverText: text ? server.text ?? "" : undefined, localText: "" });
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
if (server && local) {
|
|
1373
|
+
if (isTextAsset(server.type)) {
|
|
1374
|
+
const serverText = server.text ?? "";
|
|
1375
|
+
const localText = readFileSync8(local.absPath, "utf8");
|
|
1376
|
+
if (serverText !== localText)
|
|
1377
|
+
entries.push({ path, status: "modified", binary: false, serverText, localText });
|
|
1378
|
+
} else {
|
|
1379
|
+
const serverSize = server.file?.size;
|
|
1380
|
+
const localSize = Buffer.byteLength(readFileSync8(local.absPath));
|
|
1381
|
+
if (serverSize !== undefined && serverSize !== localSize) {
|
|
1382
|
+
entries.push({ path, status: "modified", binary: true });
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1004
1386
|
}
|
|
1005
|
-
return
|
|
1387
|
+
return entries;
|
|
1006
1388
|
};
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
var
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const p = join8(dir, name);
|
|
1017
|
-
if (statSync(p).isDirectory())
|
|
1018
|
-
walk(p);
|
|
1019
|
-
else if (isSdkSource(p))
|
|
1020
|
-
out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync7(p, "utf8");
|
|
1389
|
+
|
|
1390
|
+
// src/textDiff.ts
|
|
1391
|
+
var MAX_LINES = 4000;
|
|
1392
|
+
var lcsDiff = (a, b) => {
|
|
1393
|
+
const n = a.length, m = b.length;
|
|
1394
|
+
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
1395
|
+
for (let i2 = n - 1;i2 >= 0; i2--) {
|
|
1396
|
+
for (let j2 = m - 1;j2 >= 0; j2--) {
|
|
1397
|
+
dp[i2][j2] = a[i2] === b[j2] ? dp[i2 + 1][j2 + 1] + 1 : Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
|
|
1021
1398
|
}
|
|
1022
|
-
};
|
|
1023
|
-
walk(sdkSrc);
|
|
1024
|
-
sdkFilesCache.set(sdkSrc, out);
|
|
1025
|
-
return out;
|
|
1026
|
-
};
|
|
1027
|
-
var resolveChiselBin = () => {
|
|
1028
|
-
const override = process.env.LECODES_CHISEL_BIN;
|
|
1029
|
-
if (override) {
|
|
1030
|
-
if (existsSync5(override))
|
|
1031
|
-
return override;
|
|
1032
|
-
throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`);
|
|
1033
1399
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1400
|
+
const ops = [];
|
|
1401
|
+
let i = 0, j = 0;
|
|
1402
|
+
while (i < n && j < m) {
|
|
1403
|
+
if (a[i] === b[j]) {
|
|
1404
|
+
ops.push({ tag: " ", line: a[i] });
|
|
1405
|
+
i++;
|
|
1406
|
+
j++;
|
|
1407
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
1408
|
+
ops.push({ tag: "-", line: a[i] });
|
|
1409
|
+
i++;
|
|
1410
|
+
} else {
|
|
1411
|
+
ops.push({ tag: "+", line: b[j] });
|
|
1412
|
+
j++;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
while (i < n)
|
|
1416
|
+
ops.push({ tag: "-", line: a[i++] });
|
|
1417
|
+
while (j < m)
|
|
1418
|
+
ops.push({ tag: "+", line: b[j++] });
|
|
1419
|
+
return ops;
|
|
1040
1420
|
};
|
|
1041
|
-
var
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1421
|
+
var splitLines = (text) => {
|
|
1422
|
+
if (text === "")
|
|
1423
|
+
return [];
|
|
1424
|
+
const lines = text.split(`
|
|
1425
|
+
`);
|
|
1426
|
+
if (lines[lines.length - 1] === "")
|
|
1427
|
+
lines.pop();
|
|
1428
|
+
return lines;
|
|
1429
|
+
};
|
|
1430
|
+
var unifiedDiff = (oldText, newText, oldLabel, newLabel, context = 3) => {
|
|
1431
|
+
if (oldText === newText)
|
|
1432
|
+
return "";
|
|
1433
|
+
const a = splitLines(oldText);
|
|
1434
|
+
const b = splitLines(newText);
|
|
1435
|
+
if (a.length + b.length > MAX_LINES) {
|
|
1436
|
+
return c.dim(` (large file — ${a.length} → ${b.length} lines, diff omitted)`);
|
|
1437
|
+
}
|
|
1438
|
+
const ops = lcsDiff(a, b);
|
|
1439
|
+
const hunks = [];
|
|
1440
|
+
let aLine = 0, bLine = 0;
|
|
1441
|
+
let cur = null;
|
|
1442
|
+
let trailingContext = 0;
|
|
1443
|
+
for (let k = 0;k < ops.length; k++) {
|
|
1444
|
+
const op = ops[k];
|
|
1445
|
+
const isChange = op.tag !== " ";
|
|
1446
|
+
if (isChange) {
|
|
1447
|
+
if (!cur) {
|
|
1448
|
+
const back = [];
|
|
1449
|
+
let bi = k - 1;
|
|
1450
|
+
while (bi >= 0 && ops[bi].tag === " " && back.length < context) {
|
|
1451
|
+
back.unshift(ops[bi]);
|
|
1452
|
+
bi--;
|
|
1453
|
+
}
|
|
1454
|
+
cur = { aStart: aLine - back.length, bStart: bLine - back.length, lines: [...back] };
|
|
1455
|
+
}
|
|
1456
|
+
cur.lines.push(op);
|
|
1457
|
+
trailingContext = 0;
|
|
1458
|
+
} else if (cur) {
|
|
1459
|
+
cur.lines.push(op);
|
|
1460
|
+
if (++trailingContext >= context * 2) {
|
|
1461
|
+
cur.lines.length -= trailingContext - context;
|
|
1462
|
+
hunks.push(cur);
|
|
1463
|
+
cur = null;
|
|
1464
|
+
trailingContext = 0;
|
|
1465
|
+
}
|
|
1052
1466
|
}
|
|
1053
|
-
|
|
1467
|
+
if (op.tag !== "+")
|
|
1468
|
+
aLine++;
|
|
1469
|
+
if (op.tag !== "-")
|
|
1470
|
+
bLine++;
|
|
1054
1471
|
}
|
|
1055
|
-
if (
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
const input = JSON.stringify({
|
|
1060
|
-
files: { ...sdkFiles, ...projFiles },
|
|
1061
|
-
entry,
|
|
1062
|
-
inject: ["/__sdk/inject.ts"],
|
|
1063
|
-
format,
|
|
1064
|
-
minify,
|
|
1065
|
-
fuse: process.env.LECODES_CHISEL_FUSE === "1",
|
|
1066
|
-
define: CHISEL_DEFINE,
|
|
1067
|
-
keep: CHISEL_KEEP,
|
|
1068
|
-
sourcemap
|
|
1069
|
-
});
|
|
1070
|
-
const bin = resolveChiselBin();
|
|
1071
|
-
const r = spawnSync(bin, [], { input, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
1072
|
-
if (r.status === 2 || r.error) {
|
|
1073
|
-
throw new Error(`chisel failed: ${r.error?.message ?? r.stderr ?? "unknown error"}`);
|
|
1472
|
+
if (cur) {
|
|
1473
|
+
if (trailingContext > context)
|
|
1474
|
+
cur.lines.length -= trailingContext - context;
|
|
1475
|
+
hunks.push(cur);
|
|
1074
1476
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1477
|
+
const out = [c.bold(`--- ${oldLabel}`), c.bold(`+++ ${newLabel}`)];
|
|
1478
|
+
for (const h of hunks) {
|
|
1479
|
+
const aCount = h.lines.filter((o) => o.tag !== "+").length;
|
|
1480
|
+
const bCount = h.lines.filter((o) => o.tag !== "-").length;
|
|
1481
|
+
out.push(c.cyan(`@@ -${h.aStart + 1},${aCount} +${h.bStart + 1},${bCount} @@`));
|
|
1482
|
+
for (const o of h.lines) {
|
|
1483
|
+
if (o.tag === "+")
|
|
1484
|
+
out.push(c.green("+" + o.line));
|
|
1485
|
+
else if (o.tag === "-")
|
|
1486
|
+
out.push(c.red("-" + o.line));
|
|
1487
|
+
else
|
|
1488
|
+
out.push(c.dim(" " + o.line));
|
|
1489
|
+
}
|
|
1080
1490
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
if (parsed.code == null)
|
|
1084
|
-
throw new Error("chisel produced no code");
|
|
1085
|
-
return { code: parsed.code, map: parsed.map ?? null };
|
|
1491
|
+
return out.join(`
|
|
1492
|
+
`);
|
|
1086
1493
|
};
|
|
1087
|
-
var bundleProjectWithMap = async (entrypoint, files, format = "esm") => bundleProjectChisel(entrypoint, files, format, true);
|
|
1088
1494
|
|
|
1089
|
-
//
|
|
1090
|
-
var
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1495
|
+
// src/commands/diff.ts
|
|
1496
|
+
var diff = async (args) => {
|
|
1497
|
+
const root = findProjectRoot(process.cwd());
|
|
1498
|
+
const manifest = readManifest(root);
|
|
1499
|
+
const config = loadConfig();
|
|
1500
|
+
const apiUrl = requireApiUrl(config);
|
|
1501
|
+
const token = requireToken(config);
|
|
1502
|
+
info(c.dim("Comparing with the server…"));
|
|
1503
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
1504
|
+
const scan = scanProject(root, manifest);
|
|
1505
|
+
let entries = computeServerDiff(project, scan);
|
|
1506
|
+
if (args._.length > 0) {
|
|
1507
|
+
const keys = args._.map(toKey);
|
|
1508
|
+
entries = entries.filter((e) => keys.some((k) => e.path === k || e.path.startsWith(k + "/")));
|
|
1509
|
+
}
|
|
1510
|
+
if (entries.length === 0) {
|
|
1511
|
+
info(c.dim("No differences between local and server."));
|
|
1512
|
+
return;
|
|
1513
|
+
}
|
|
1514
|
+
if (flagBool(args, "name-only")) {
|
|
1515
|
+
for (const e of entries)
|
|
1516
|
+
log(statusLabel(e) + e.path + (e.binary ? c.dim(" (binary)") : ""));
|
|
1517
|
+
return;
|
|
1101
1518
|
}
|
|
1102
|
-
if (/_creator\.createGlb/.test(code))
|
|
1103
|
-
localAssets.add("uberarchive");
|
|
1104
|
-
if (/_creator\.captureImage/.test(code))
|
|
1105
|
-
localAssets.add("capture");
|
|
1106
|
-
if (localAssets.size > 0)
|
|
1107
|
-
header += `// preload: ${[...localAssets].join(";")}
|
|
1108
|
-
`;
|
|
1109
|
-
if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
|
|
1110
|
-
header += `// gl: disable
|
|
1111
|
-
`;
|
|
1112
|
-
else if (code.includes("_creator.createEngine"))
|
|
1113
|
-
header += `// gl: defer
|
|
1114
|
-
`;
|
|
1115
|
-
if (code.includes("_creator2d."))
|
|
1116
|
-
header += `// 2d: enable
|
|
1117
|
-
`;
|
|
1118
|
-
if (code.includes("_creator.createARController"))
|
|
1119
|
-
header += "// ar: enable";
|
|
1120
|
-
header += `
|
|
1121
|
-
`;
|
|
1122
|
-
return header;
|
|
1123
|
-
};
|
|
1124
|
-
|
|
1125
|
-
// ../sdk/src/compile/sourcemap.ts
|
|
1126
|
-
var offsetSourceMap = (mapJson, lines) => {
|
|
1127
|
-
if (lines <= 0)
|
|
1128
|
-
return mapJson;
|
|
1129
|
-
const map = JSON.parse(mapJson);
|
|
1130
|
-
map.mappings = ";".repeat(lines) + map.mappings;
|
|
1131
|
-
return JSON.stringify(map);
|
|
1132
|
-
};
|
|
1133
|
-
var inlineSourceMapComment = (mapJson) => {
|
|
1134
|
-
const b64 = Buffer.from(mapJson, "utf8").toString("base64");
|
|
1135
|
-
return `//# lecodesMap=data:application/json;charset=utf-8;base64,${b64}`;
|
|
1136
|
-
};
|
|
1137
|
-
|
|
1138
|
-
// ../sdk/src/compile/compileProject.ts
|
|
1139
|
-
var detectEntry = (entries) => {
|
|
1140
|
-
let entry = "";
|
|
1141
|
-
let max = -1;
|
|
1142
1519
|
for (const e of entries) {
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
entry = e.path;
|
|
1147
|
-
max = e.text.length;
|
|
1148
|
-
}
|
|
1520
|
+
if (e.binary) {
|
|
1521
|
+
log(statusLabel(e) + e.path + c.dim(" (binary — content not shown)"));
|
|
1522
|
+
continue;
|
|
1149
1523
|
}
|
|
1524
|
+
const d = unifiedDiff(e.serverText ?? "", e.localText ?? "", `server${e.path}`, `local${e.path}`);
|
|
1525
|
+
if (d)
|
|
1526
|
+
log(`
|
|
1527
|
+
` + d);
|
|
1150
1528
|
}
|
|
1151
|
-
return entry;
|
|
1152
1529
|
};
|
|
1153
|
-
var
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1530
|
+
var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
|
|
1531
|
+
|
|
1532
|
+
// src/commands/login.ts
|
|
1533
|
+
var login = async (args) => {
|
|
1534
|
+
const config = loadConfig();
|
|
1535
|
+
const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? config.apiUrl ?? DEFAULT_API);
|
|
1536
|
+
const webBase = normalizeApiUrl(flagStr(args, "web") ?? apiUrl);
|
|
1537
|
+
const token = flagStr(args, "token") ?? await browserLogin(webBase);
|
|
1538
|
+
const account = await getAccount(apiUrl, token);
|
|
1539
|
+
saveConfig({ apiUrl, token });
|
|
1540
|
+
success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`);
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
// src/commands/preview.ts
|
|
1544
|
+
import { writeFileSync as writeFileSync8 } from "node:fs";
|
|
1545
|
+
import { resolve as resolve3 } from "node:path";
|
|
1546
|
+
|
|
1547
|
+
// src/compile/collect.ts
|
|
1548
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
1549
|
+
import { basename as basename4 } from "node:path";
|
|
1550
|
+
var collectEntries = (root, manifest, project) => {
|
|
1551
|
+
const pathById = buildPaths(project.assets);
|
|
1552
|
+
const serverByPath = new Map;
|
|
1553
|
+
for (const asset of project.assets) {
|
|
1554
|
+
if (asset.type === "folder")
|
|
1555
|
+
continue;
|
|
1556
|
+
serverByPath.set(pathById.get(asset.id), { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] });
|
|
1157
1557
|
}
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
const
|
|
1164
|
-
if (
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
|
|
1558
|
+
const entries = [];
|
|
1559
|
+
const warnings = [];
|
|
1560
|
+
for (const file of scanLocal(root).files) {
|
|
1561
|
+
const type = manifest.files[file.path]?.type ?? classifyKind(basename4(file.path));
|
|
1562
|
+
if (type === "resource") {
|
|
1563
|
+
const src = serverByPath.get(file.path)?.fileSrc;
|
|
1564
|
+
if (!src)
|
|
1565
|
+
warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`);
|
|
1566
|
+
entries.push({ path: file.path, type, fileSrc: src, absPath: file.absPath });
|
|
1567
|
+
} else if (type === "shader") {
|
|
1568
|
+
const shaders = serverByPath.get(file.path)?.shaders;
|
|
1569
|
+
if (!shaders?.length)
|
|
1570
|
+
warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
|
|
1571
|
+
entries.push({ path: file.path, type, shaders });
|
|
1173
1572
|
} else {
|
|
1174
|
-
|
|
1573
|
+
entries.push({ path: file.path, type, text: readFileSync9(file.absPath, "utf8") });
|
|
1175
1574
|
}
|
|
1176
1575
|
}
|
|
1177
|
-
|
|
1178
|
-
if (opts.header === false) {
|
|
1179
|
-
return map ? `${code}
|
|
1180
|
-
${inlineSourceMapComment(map)}` : code;
|
|
1181
|
-
}
|
|
1182
|
-
const header = buildHeader(opts.name, code, { icon: opts.icon });
|
|
1183
|
-
if (!map)
|
|
1184
|
-
return header + code;
|
|
1185
|
-
const headerLines = (header.match(/\n/g) ?? []).length;
|
|
1186
|
-
return `${header}${code}
|
|
1187
|
-
${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`;
|
|
1188
|
-
};
|
|
1189
|
-
// src/compile/projectCompile.ts
|
|
1190
|
-
var here = dirname5(fileURLToPath2(import.meta.url));
|
|
1191
|
-
var vendoredInject = join9(here, "..", "runtime", "sdk", "inject.ts");
|
|
1192
|
-
if (existsSync6(vendoredInject))
|
|
1193
|
-
process.env.LECODES_SDK_INJECT = vendoredInject;
|
|
1194
|
-
var resolveChiselBin2 = () => {
|
|
1195
|
-
if (process.env.LECODES_CHISEL_BIN)
|
|
1196
|
-
return process.env.LECODES_CHISEL_BIN;
|
|
1197
|
-
try {
|
|
1198
|
-
return createRequire3(import.meta.url)("@letary/chisel").binaryPath() ?? null;
|
|
1199
|
-
} catch {
|
|
1200
|
-
return null;
|
|
1201
|
-
}
|
|
1576
|
+
return { entries, warnings };
|
|
1202
1577
|
};
|
|
1203
|
-
var chiselBin = resolveChiselBin2();
|
|
1204
|
-
if (chiselBin)
|
|
1205
|
-
process.env.LECODES_CHISEL_BIN = chiselBin;
|
|
1206
|
-
var compileProject2 = (opts) => compileProject({
|
|
1207
|
-
entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, shaders: e.shaders })),
|
|
1208
|
-
name: opts.name,
|
|
1209
|
-
publicUrl: opts.publicUrl,
|
|
1210
|
-
entryOverride: opts.entryOverride,
|
|
1211
|
-
header: opts.header,
|
|
1212
|
-
format: opts.format
|
|
1213
|
-
});
|
|
1214
1578
|
|
|
1215
1579
|
// src/commands/preview.ts
|
|
1216
1580
|
var preview = async (args) => {
|
|
@@ -1236,7 +1600,7 @@ var preview = async (args) => {
|
|
|
1236
1600
|
if (out && isNullSink(out)) {
|
|
1237
1601
|
note(`Discarded output (${out})`);
|
|
1238
1602
|
} else if (out) {
|
|
1239
|
-
|
|
1603
|
+
writeFileSync8(resolve3(out), js);
|
|
1240
1604
|
note(`Wrote ${out} (${js.length} bytes)`);
|
|
1241
1605
|
} else {
|
|
1242
1606
|
process.stdout.write(js);
|
|
@@ -1248,9 +1612,9 @@ var preview = async (args) => {
|
|
|
1248
1612
|
};
|
|
1249
1613
|
|
|
1250
1614
|
// src/commands/render.ts
|
|
1251
|
-
import { writeFileSync as
|
|
1615
|
+
import { writeFileSync as writeFileSync9 } from "node:fs";
|
|
1252
1616
|
import { resolve as resolve4 } from "node:path";
|
|
1253
|
-
var
|
|
1617
|
+
var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
|
|
1254
1618
|
var DEVICE_SAFE_AREA = {
|
|
1255
1619
|
none: [0, 0, 0, 0],
|
|
1256
1620
|
iphone: [47, 0, 34, 0],
|
|
@@ -1296,7 +1660,7 @@ var render = async (args) => {
|
|
|
1296
1660
|
const js = await compileProject2({
|
|
1297
1661
|
entries: compileEntries,
|
|
1298
1662
|
name: manifest.name,
|
|
1299
|
-
publicUrl: useLocalAssets ?
|
|
1663
|
+
publicUrl: useLocalAssets ? LOCAL_ASSET_SCHEME2 : publicUrlFlag ?? apiUrl,
|
|
1300
1664
|
entryOverride: flagStr(args, "entry"),
|
|
1301
1665
|
header: false,
|
|
1302
1666
|
format: "iife"
|
|
@@ -1345,7 +1709,7 @@ var render = async (args) => {
|
|
|
1345
1709
|
if (isNullSink(pngFlag)) {
|
|
1346
1710
|
note(`Discarded PNG (${pngFlag})`);
|
|
1347
1711
|
} else {
|
|
1348
|
-
|
|
1712
|
+
writeFileSync9(resolve4(pngFlag), png);
|
|
1349
1713
|
note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
|
|
1350
1714
|
}
|
|
1351
1715
|
return done((cb) => cb());
|
|
@@ -1360,7 +1724,7 @@ var render = async (args) => {
|
|
|
1360
1724
|
return done((cb) => cb());
|
|
1361
1725
|
}
|
|
1362
1726
|
if (out) {
|
|
1363
|
-
|
|
1727
|
+
writeFileSync9(resolve4(out), result.text);
|
|
1364
1728
|
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
1365
1729
|
return done((cb) => cb());
|
|
1366
1730
|
}
|
|
@@ -1371,8 +1735,8 @@ var render = async (args) => {
|
|
|
1371
1735
|
};
|
|
1372
1736
|
|
|
1373
1737
|
// src/commands/pull.ts
|
|
1374
|
-
import { existsSync as
|
|
1375
|
-
import { dirname as dirname6, join as
|
|
1738
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync8, rmSync, writeFileSync as writeFileSync10 } from "node:fs";
|
|
1739
|
+
import { dirname as dirname6, join as join11 } from "node:path";
|
|
1376
1740
|
var pull = async (args) => {
|
|
1377
1741
|
const root = findProjectRoot(process.cwd());
|
|
1378
1742
|
const manifest = readManifest(root);
|
|
@@ -1392,8 +1756,8 @@ var pull = async (args) => {
|
|
|
1392
1756
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
|
|
1393
1757
|
for (const path of Object.keys(manifest.files)) {
|
|
1394
1758
|
if (!remotePaths.has(path)) {
|
|
1395
|
-
const abs =
|
|
1396
|
-
if (
|
|
1759
|
+
const abs = join11(root, ...path.replace(/^\//, "").split("/"));
|
|
1760
|
+
if (existsSync8(abs))
|
|
1397
1761
|
rmSync(abs);
|
|
1398
1762
|
}
|
|
1399
1763
|
}
|
|
@@ -1431,18 +1795,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
1431
1795
|
const files = { ...manifest.files };
|
|
1432
1796
|
for (const asset of targets) {
|
|
1433
1797
|
const key = pathById.get(asset.id);
|
|
1434
|
-
const abs =
|
|
1435
|
-
|
|
1798
|
+
const abs = join11(root, ...key.replace(/^\//, "").split("/"));
|
|
1799
|
+
mkdirSync8(dirname6(abs), { recursive: true });
|
|
1436
1800
|
let sha;
|
|
1437
1801
|
if (asset.type === "file") {
|
|
1438
1802
|
if (!asset.file?.src)
|
|
1439
1803
|
continue;
|
|
1440
1804
|
const bytes = await downloadBlob(apiUrl, asset.file.src);
|
|
1441
|
-
|
|
1805
|
+
writeFileSync10(abs, bytes);
|
|
1442
1806
|
sha = sha256(bytes);
|
|
1443
1807
|
} else {
|
|
1444
1808
|
const text = asset.text ?? "";
|
|
1445
|
-
|
|
1809
|
+
writeFileSync10(abs, text);
|
|
1446
1810
|
sha = sha256(text);
|
|
1447
1811
|
}
|
|
1448
1812
|
files[key] = { id: asset.id, type: kindOf2(asset.type), sha };
|
|
@@ -1454,8 +1818,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
1454
1818
|
};
|
|
1455
1819
|
|
|
1456
1820
|
// src/commands/push.ts
|
|
1457
|
-
import { basename as
|
|
1458
|
-
import { readFileSync as
|
|
1821
|
+
import { basename as basename5 } from "node:path";
|
|
1822
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
1459
1823
|
var push = async (args) => {
|
|
1460
1824
|
const root = findProjectRoot(process.cwd());
|
|
1461
1825
|
const manifest = readManifest(root);
|
|
@@ -1509,14 +1873,14 @@ var push = async (args) => {
|
|
|
1509
1873
|
let uploaded = 0;
|
|
1510
1874
|
for (const file of scan.files) {
|
|
1511
1875
|
const source = sourceFor(file.path);
|
|
1512
|
-
const kind = source?.type ?? classifyKind(
|
|
1876
|
+
const kind = source?.type ?? classifyKind(basename5(file.path));
|
|
1513
1877
|
const entry = { path: file.path, type: kind };
|
|
1514
1878
|
if (source)
|
|
1515
1879
|
entry.assetId = source.id;
|
|
1516
1880
|
if (isTextKind(kind)) {
|
|
1517
|
-
entry.text =
|
|
1881
|
+
entry.text = readFileSync10(file.absPath, "utf8");
|
|
1518
1882
|
} else if (!source || source.sha !== file.sha) {
|
|
1519
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid,
|
|
1883
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename5(file.path), readFileSync10(file.absPath));
|
|
1520
1884
|
entry.blobFileId = blob.id;
|
|
1521
1885
|
uploaded++;
|
|
1522
1886
|
}
|
|
@@ -1640,13 +2004,25 @@ ${c.bold("Commands:")}
|
|
|
1640
2004
|
--settle <ms> wait for promises/timers/fetch before snapshot
|
|
1641
2005
|
--wait-network let data fetches hit the real network
|
|
1642
2006
|
--logs forward the app's console output (to stderr)
|
|
2007
|
+
design [serve] LeCodes Design — a live canvas of screens for AI-driven prototyping
|
|
2008
|
+
(needs the optional 'lecodes-design' package; works inside a
|
|
2009
|
+
cloned project or standalone in any folder)
|
|
2010
|
+
--port <n> dev-server port (default 4477)
|
|
2011
|
+
--dir <folder> design folder (default design/)
|
|
2012
|
+
--no-open don't open the browser
|
|
2013
|
+
design init Scaffold the design folder (screens/, shared/, meta.json, CLAUDE.md)
|
|
2014
|
+
design snapshot [screen…] Render design screens headless (needs 'lecodes-renderer')
|
|
2015
|
+
one screen → JSON to stdout; else files in design/.snapshots/
|
|
2016
|
+
--png also rasterize PNGs (needs @napi-rs/canvas)
|
|
2017
|
+
--out-dir <dir> output folder override
|
|
2018
|
+
--logs forward the screens' console output (to stderr)
|
|
1643
2019
|
|
|
1644
2020
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
1645
|
-
var commands = { login, create, clone, status, diff, pull, push, preview, render };
|
|
2021
|
+
var commands = { login, create, clone, status, diff, pull, push, preview, render, design };
|
|
1646
2022
|
var readVersion = () => {
|
|
1647
2023
|
try {
|
|
1648
2024
|
const here2 = dirname7(fileURLToPath3(import.meta.url));
|
|
1649
|
-
return JSON.parse(
|
|
2025
|
+
return JSON.parse(readFileSync11(join12(here2, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
1650
2026
|
} catch {
|
|
1651
2027
|
return "unknown";
|
|
1652
2028
|
}
|