lecodes-cli 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3005 -725
- package/package.json +2 -2
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/canvas/Canvas.ts +16 -0
- package/runtime/sdk/gl/Material.ts +4 -0
- package/runtime/sdk/inject.ts +7 -4
- package/runtime/sdk/plugins/service.ts +47 -0
- package/runtime/sdk/runtime/app.ts +21 -1
- package/runtime/sdk/runtime/appEvents.ts +25 -0
- package/runtime/sdk/runtime/service.ts +3 -2
- package/runtime/sdk/ui/NativeView.ts +2 -1
- package/runtime/sdk/ui/UI.ts +3 -1
- package/runtime/sdk/ui/UIButton.ts +10 -5
- package/runtime/sdk/ui/UIContainer.ts +16 -9
- package/runtime/sdk/ui/UIImage.ts +5 -2
- package/runtime/sdk/ui/UIInput.ts +75 -8
- package/runtime/sdk/ui/UIModal.ts +149 -0
- package/runtime/sdk/ui/UINode.ts +113 -50
- package/runtime/sdk/ui/UIPager.ts +297 -0
- package/runtime/sdk/ui/UIPopover.ts +106 -0
- package/runtime/sdk/ui/UIScreen.ts +11 -10
- package/runtime/sdk/ui/UIScrollable.ts +2 -1
- package/runtime/sdk/ui/UISpacer.ts +1 -1
- package/runtime/sdk/ui/UIText.ts +2 -1
- package/runtime/sdk/ui/UIVideo.ts +2 -1
- package/runtime/sdk/ui/UIVirtualizedList.ts +2 -1
- package/runtime/sdk/ui/UIWidget.ts +19 -22
- package/runtime/sdk/ui/theme.ts +66 -0
- package/runtime/sdk-types.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +0 -42
- package/runtime/sdk/compile/assetMacro.ts +0 -45
- package/runtime/sdk/compile/bundler.ts +0 -207
- package/runtime/sdk/compile/compileProject.ts +0 -113
- package/runtime/sdk/compile/header.ts +0 -46
- package/runtime/sdk/compile/index.ts +0 -18
- package/runtime/sdk/compile/libraryImports.ts +0 -47
- package/runtime/sdk/compile/sceneEditor.ts +0 -78
- package/runtime/sdk/compile/sourcemap.ts +0 -25
- package/runtime/sdk/kit/UITabs.ts +0 -105
- package/runtime/sdk/ui/UIScreenHost.ts +0 -213
package/dist/index.js
CHANGED
|
@@ -109,12 +109,15 @@ var hasImportStatement = (src) => src.includes("import") || src.includes("from")
|
|
|
109
109
|
if (isRelativeOrAbsolute(spec))
|
|
110
110
|
return spec;
|
|
111
111
|
const slash = spec.indexOf("/");
|
|
112
|
-
const
|
|
113
|
-
if (slugs.has(
|
|
112
|
+
const slug = slash === -1 ? spec : spec.slice(0, slash);
|
|
113
|
+
if (slugs.has(slug)) {
|
|
114
114
|
const sub = slash === -1 ? "/index" : spec.slice(slash);
|
|
115
|
-
return `/__lib/${
|
|
115
|
+
return `/__lib/${slug}${sub}`;
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
if (slug === "ui" || slug === "creator" || slug === "utils") {
|
|
118
|
+
throw new Error(`import from "${slug}": the SDK needs no imports — UIScreen, Scene, etc. are globals; remove this import line.`);
|
|
119
|
+
}
|
|
120
|
+
throw new Error(`Unknown library "${slug}" — add it in Settings → Libraries, or use a relative path like "./${slug}".`);
|
|
118
121
|
};
|
|
119
122
|
let out = src.replace(IMPORT_FROM_RE, (_m, boundary, head, quote, spec) => `${boundary}${head}${quote}${rewriteSpec(spec)}${quote}`);
|
|
120
123
|
out = out.replace(SIDE_EFFECT_RE, (m, boundary, head, quote, spec) => isRelativeOrAbsolute(spec) ? m : `${boundary}${head}${quote}${rewriteSpec(spec)}${quote}`);
|
|
@@ -126,18 +129,18 @@ var init_libraryImports = __esm(() => {
|
|
|
126
129
|
});
|
|
127
130
|
|
|
128
131
|
// ../sdk/src/compile/bundler.ts
|
|
129
|
-
import { existsSync as
|
|
132
|
+
import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync7, statSync } from "node:fs";
|
|
130
133
|
import { spawnSync } from "node:child_process";
|
|
131
134
|
import { createRequire as createRequire2 } from "node:module";
|
|
132
|
-
import { dirname as
|
|
135
|
+
import { dirname as dirname4, join as join7, relative as relative2 } from "node:path";
|
|
133
136
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
134
137
|
var resolveInject = () => {
|
|
135
138
|
const override = process.env.LECODES_SDK_INJECT;
|
|
136
|
-
if (override &&
|
|
139
|
+
if (override && existsSync4(override))
|
|
137
140
|
return override;
|
|
138
|
-
const here =
|
|
139
|
-
const src =
|
|
140
|
-
if (
|
|
141
|
+
const here = dirname4(fileURLToPath2(import.meta.url));
|
|
142
|
+
const src = join7(here, "..", "inject.ts");
|
|
143
|
+
if (existsSync4(src))
|
|
141
144
|
return src;
|
|
142
145
|
throw new Error("sdk: could not locate the inject source (LECODES_SDK_INJECT / src/inject.ts)");
|
|
143
146
|
}, normalize = (path) => {
|
|
@@ -156,19 +159,19 @@ var resolveInject = () => {
|
|
|
156
159
|
if (!inject.endsWith("inject.ts")) {
|
|
157
160
|
throw new Error(`chisel needs the SDK source tree (src/inject.ts); got ${inject}. Set LECODES_SDK_INJECT to the source.`);
|
|
158
161
|
}
|
|
159
|
-
return
|
|
162
|
+
return dirname4(inject);
|
|
160
163
|
}, isSdkSource = (path) => /\.[tj]s$/.test(path) && !path.endsWith(".d.ts") && !path.includes("/compile/") && !path.includes("/tests/") && !path.includes("/__tests__/"), sdkFilesCache, collectSdkFiles = (sdkSrc) => {
|
|
161
164
|
const cached = sdkFilesCache.get(sdkSrc);
|
|
162
165
|
if (cached)
|
|
163
166
|
return cached;
|
|
164
167
|
const out = {};
|
|
165
168
|
const walk = (dir) => {
|
|
166
|
-
for (const name of
|
|
167
|
-
const p =
|
|
169
|
+
for (const name of readdirSync2(dir)) {
|
|
170
|
+
const p = join7(dir, name);
|
|
168
171
|
if (statSync(p).isDirectory())
|
|
169
172
|
walk(p);
|
|
170
173
|
else if (isSdkSource(p))
|
|
171
|
-
out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] =
|
|
174
|
+
out["/__sdk/" + relative2(sdkSrc, p).split("\\").join("/")] = readFileSync7(p, "utf8");
|
|
172
175
|
}
|
|
173
176
|
};
|
|
174
177
|
walk(sdkSrc);
|
|
@@ -177,13 +180,13 @@ var resolveInject = () => {
|
|
|
177
180
|
}, resolveChiselBin = () => {
|
|
178
181
|
const override = process.env.LECODES_CHISEL_BIN;
|
|
179
182
|
if (override) {
|
|
180
|
-
if (
|
|
183
|
+
if (existsSync4(override))
|
|
181
184
|
return override;
|
|
182
185
|
throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`);
|
|
183
186
|
}
|
|
184
187
|
try {
|
|
185
188
|
const bin = createRequire2(import.meta.url)("@letary/chisel").binaryPath();
|
|
186
|
-
if (bin &&
|
|
189
|
+
if (bin && existsSync4(bin))
|
|
187
190
|
return bin;
|
|
188
191
|
} catch {}
|
|
189
192
|
throw new Error("chisel binary not found. Install @letary/chisel, or set LECODES_CHISEL_BIN.");
|
|
@@ -206,9 +209,9 @@ var resolveInject = () => {
|
|
|
206
209
|
}
|
|
207
210
|
if (!projFiles[entry])
|
|
208
211
|
throw new Error(`Entrypoint not found: ${entry}`);
|
|
209
|
-
for (const [
|
|
212
|
+
for (const [slug, libFiles] of Object.entries(libraries ?? {})) {
|
|
210
213
|
for (const [k, v] of Object.entries(libFiles)) {
|
|
211
|
-
const path = normalize(`/__lib/${
|
|
214
|
+
const path = normalize(`/__lib/${slug}/${k}`);
|
|
212
215
|
let src = v;
|
|
213
216
|
if (/\.[tj]sx?$/.test(path)) {
|
|
214
217
|
if (hasAssetMacro(src))
|
|
@@ -290,6 +293,9 @@ var buildHeader = (name, code, opts = {}) => {
|
|
|
290
293
|
localAssets.add("capture");
|
|
291
294
|
if (localAssets.size > 0)
|
|
292
295
|
header += `// preload: ${[...localAssets].join(";")}
|
|
296
|
+
`;
|
|
297
|
+
if (opts.fonts)
|
|
298
|
+
header += `// fonts: ${opts.fonts}
|
|
293
299
|
`;
|
|
294
300
|
if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
|
|
295
301
|
header += `// gl: disable
|
|
@@ -339,7 +345,7 @@ var detectEntry = (entries) => {
|
|
|
339
345
|
const files = {};
|
|
340
346
|
for (const e of opts.entries) {
|
|
341
347
|
if (e.type === "resource") {
|
|
342
|
-
if (opts.localAssets) {
|
|
348
|
+
if (opts.localAssets && !e.remote) {
|
|
343
349
|
const name = e.path.split(/[\\/]/).pop() ?? e.path;
|
|
344
350
|
files[e.path] = `export default ("id:" + _creatorUtils.fetchLocal(${JSON.stringify(name)}))`;
|
|
345
351
|
} else {
|
|
@@ -365,7 +371,7 @@ var detectEntry = (entries) => {
|
|
|
365
371
|
return map ? `${code}
|
|
366
372
|
${inlineSourceMapComment(map)}` : code;
|
|
367
373
|
}
|
|
368
|
-
const header = buildHeader(opts.name, code, { icon: opts.icon });
|
|
374
|
+
const header = buildHeader(opts.name, code, { icon: opts.icon, fonts: opts.fontsHeader });
|
|
369
375
|
if (!map)
|
|
370
376
|
return header + code;
|
|
371
377
|
const headerLines = (header.match(/\n/g) ?? []).length;
|
|
@@ -377,17 +383,17 @@ var init_compileProject = __esm(() => {
|
|
|
377
383
|
});
|
|
378
384
|
|
|
379
385
|
// ../sdk/src/compile/sceneEditor.ts
|
|
380
|
-
import { existsSync as
|
|
381
|
-
import { join as
|
|
386
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "node:fs";
|
|
387
|
+
import { join as join8 } from "node:path";
|
|
382
388
|
var SCENE_HARNESS_DIR = "./.scene-editor", SCENE_ENTRY_PATH = "./.scene-entry.ts", loadSceneHarness = (fallbackDir) => {
|
|
383
389
|
const override = process.env.LECODES_SCENE_HARNESS_DIR;
|
|
384
|
-
const dir = override &&
|
|
390
|
+
const dir = override && existsSync5(override) ? override : fallbackDir && existsSync5(fallbackDir) ? fallbackDir : null;
|
|
385
391
|
if (!dir)
|
|
386
392
|
throw new Error("scene-editor harness sources not found (set LECODES_SCENE_HARNESS_DIR)");
|
|
387
393
|
const out = {};
|
|
388
|
-
for (const f of
|
|
394
|
+
for (const f of readdirSync3(dir)) {
|
|
389
395
|
if (f.endsWith(".ts"))
|
|
390
|
-
out[f] =
|
|
396
|
+
out[f] = readFileSync8(join8(dir, f), "utf8");
|
|
391
397
|
}
|
|
392
398
|
return out;
|
|
393
399
|
}, sceneEditorEntries = (scenePath, harness, projectPaths = []) => {
|
|
@@ -417,25 +423,569 @@ export {}
|
|
|
417
423
|
};
|
|
418
424
|
var init_sceneEditor = () => {};
|
|
419
425
|
|
|
426
|
+
// ../sdk/src/compile/fontRegistry.ts
|
|
427
|
+
var DEFAULT_FONT_REGISTRY_BASE = "https://cdn.le.codes/fonts/v1", fontRegistryBase = () => typeof process !== "undefined" && process.env?.LECODES_FONT_REGISTRY?.trim() || DEFAULT_FONT_REGISTRY_BASE, faceFileName = (id, weight, italic, ext2) => `${id}-${weight}${italic ? "-italic" : ""}.${ext2}`, LATIN, CYR, OFL = "OFL-1.1", f = (family, category, scripts, weights, italics, defaultWeights) => ({ family, category, scripts, weights, italics, defaultWeights, license: OFL }), FONT_REGISTRY;
|
|
428
|
+
var init_fontRegistry = __esm(() => {
|
|
429
|
+
LATIN = ["latin"];
|
|
430
|
+
CYR = ["latin", "cyrillic"];
|
|
431
|
+
FONT_REGISTRY = {
|
|
432
|
+
inter: f("Inter", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
|
|
433
|
+
manrope: f("Manrope", "sans-serif", CYR, [200, 300, 400, 500, 600, 700, 800], [], [400, 500, 700]),
|
|
434
|
+
"golos-text": f("Golos Text", "sans-serif", CYR, [400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
|
|
435
|
+
onest: f("Onest", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [], [400, 500, 700]),
|
|
436
|
+
rubik: f("Rubik", "sans-serif", CYR, [300, 400, 500, 600, 700, 800, 900], [300, 400, 500, 600, 700, 800, 900], [400, 500, 700]),
|
|
437
|
+
nunito: f("Nunito", "sans-serif", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 600, 700]),
|
|
438
|
+
montserrat: f("Montserrat", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
|
|
439
|
+
"ibm-plex-sans": f("IBM Plex Sans", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700], [400, 700], [400, 500, 700]),
|
|
440
|
+
"pt-sans": f("PT Sans", "sans-serif", CYR, [400, 700], [400, 700], [400, 700]),
|
|
441
|
+
jost: f("Jost", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
|
|
442
|
+
"exo-2": f("Exo 2", "sans-serif", CYR, [100, 200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 500, 700]),
|
|
443
|
+
unbounded: f("Unbounded", "display", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [], [400, 700]),
|
|
444
|
+
"russo-one": f("Russo One", "display", CYR, [400], [], [400]),
|
|
445
|
+
oswald: f("Oswald", "display", CYR, [200, 300, 400, 500, 600, 700], [], [400, 700]),
|
|
446
|
+
comfortaa: f("Comfortaa", "display", CYR, [300, 400, 500, 600, 700], [], [400, 700]),
|
|
447
|
+
"space-grotesk": f("Space Grotesk", "display", LATIN, [300, 400, 500, 600, 700], [], [400, 700]),
|
|
448
|
+
"playfair-display": f("Playfair Display", "serif", CYR, [400, 500, 600, 700, 800, 900], [400, 700], [400, 700]),
|
|
449
|
+
lora: f("Lora", "serif", CYR, [400, 500, 600, 700], [400, 700], [400, 700]),
|
|
450
|
+
"pt-serif": f("PT Serif", "serif", CYR, [400, 700], [400, 700], [400, 700]),
|
|
451
|
+
cormorant: f("Cormorant", "serif", CYR, [300, 400, 500, 600, 700], [400, 700], [400, 700]),
|
|
452
|
+
literata: f("Literata", "serif", CYR, [200, 300, 400, 500, 600, 700, 800, 900], [400, 700], [400, 700]),
|
|
453
|
+
"jetbrains-mono": f("JetBrains Mono", "monospace", CYR, [100, 200, 300, 400, 500, 600, 700, 800], [400, 700], [400, 700]),
|
|
454
|
+
"fira-code": f("Fira Code", "monospace", CYR, [300, 400, 500, 600, 700], [], [400, 700]),
|
|
455
|
+
"ibm-plex-mono": f("IBM Plex Mono", "monospace", CYR, [100, 200, 300, 400, 500, 600, 700], [400, 700], [400, 700]),
|
|
456
|
+
caveat: f("Caveat", "handwriting", CYR, [400, 500, 600, 700], [], [400, 700]),
|
|
457
|
+
pacifico: f("Pacifico", "handwriting", CYR, [400], [], [400])
|
|
458
|
+
};
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// ../sdk/src/compile/sfnt.ts
|
|
462
|
+
var TAG_TTF = 65536, TAG_OTTO = 1330926671, TAG_TTCF = 1953784678, TAG_WOFF = 2001684038, TAG_WOF2 = 2001684018, parseSfnt = (bytes) => {
|
|
463
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
464
|
+
if (bytes.byteLength < 12)
|
|
465
|
+
throw new Error("not a font file (truncated)");
|
|
466
|
+
const version = dv.getUint32(0);
|
|
467
|
+
if (version === TAG_TTCF)
|
|
468
|
+
throw new Error("font collections (.ttc) aren't supported — export a single face");
|
|
469
|
+
if (version === TAG_WOFF || version === TAG_WOF2)
|
|
470
|
+
throw new Error("WOFF/WOFF2 project fonts aren't supported — use the .ttf/.otf");
|
|
471
|
+
if (version !== TAG_TTF && version !== TAG_OTTO)
|
|
472
|
+
throw new Error("not a TTF/OTF font file");
|
|
473
|
+
const numTables = dv.getUint16(4);
|
|
474
|
+
const tables = new Map;
|
|
475
|
+
for (let i = 0;i < numTables; i++) {
|
|
476
|
+
const rec = 12 + i * 16;
|
|
477
|
+
if (rec + 16 > bytes.byteLength)
|
|
478
|
+
throw new Error("not a font file (truncated table directory)");
|
|
479
|
+
const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3]);
|
|
480
|
+
tables.set(tag, { offset: dv.getUint32(rec + 8), length: dv.getUint32(rec + 12) });
|
|
481
|
+
}
|
|
482
|
+
const family = readFamily(dv, bytes, tables.get("name"));
|
|
483
|
+
if (!family)
|
|
484
|
+
throw new Error("no family name in the font's `name` table");
|
|
485
|
+
let weight = 400;
|
|
486
|
+
let italic = false;
|
|
487
|
+
const os2 = tables.get("OS/2");
|
|
488
|
+
if (os2 && os2.offset + 64 <= bytes.byteLength) {
|
|
489
|
+
weight = dv.getUint16(os2.offset + 4);
|
|
490
|
+
italic = (dv.getUint16(os2.offset + 62) & 1) === 1;
|
|
491
|
+
} else {
|
|
492
|
+
const head = tables.get("head");
|
|
493
|
+
if (head && head.offset + 46 <= bytes.byteLength) {
|
|
494
|
+
const macStyle = dv.getUint16(head.offset + 44);
|
|
495
|
+
if (macStyle & 1)
|
|
496
|
+
weight = 700;
|
|
497
|
+
italic = (macStyle & 2) === 2;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (weight < 1 || weight > 1000)
|
|
501
|
+
weight = 400;
|
|
502
|
+
return { family, weight, italic };
|
|
503
|
+
}, readFamily = (dv, bytes, name) => {
|
|
504
|
+
if (!name || name.offset + 6 > bytes.byteLength)
|
|
505
|
+
return "";
|
|
506
|
+
const base = name.offset;
|
|
507
|
+
const count = dv.getUint16(base + 2);
|
|
508
|
+
const stringOffset = base + dv.getUint16(base + 4);
|
|
509
|
+
let best = "";
|
|
510
|
+
let bestScore = -1;
|
|
511
|
+
for (let i = 0;i < count; i++) {
|
|
512
|
+
const rec = base + 6 + i * 12;
|
|
513
|
+
if (rec + 12 > bytes.byteLength)
|
|
514
|
+
break;
|
|
515
|
+
const platformID = dv.getUint16(rec);
|
|
516
|
+
const languageID = dv.getUint16(rec + 4);
|
|
517
|
+
const nameID = dv.getUint16(rec + 6);
|
|
518
|
+
const length = dv.getUint16(rec + 8);
|
|
519
|
+
const offset = stringOffset + dv.getUint16(rec + 10);
|
|
520
|
+
if (nameID !== 16 && nameID !== 1)
|
|
521
|
+
continue;
|
|
522
|
+
if (offset + length > bytes.byteLength)
|
|
523
|
+
continue;
|
|
524
|
+
let score = nameID === 16 ? 100 : 0;
|
|
525
|
+
if (platformID === 3)
|
|
526
|
+
score += languageID === 1033 ? 30 : 20;
|
|
527
|
+
else if (platformID === 0)
|
|
528
|
+
score += 10;
|
|
529
|
+
if (score <= bestScore)
|
|
530
|
+
continue;
|
|
531
|
+
const text = platformID === 1 ? decodeLatin1(bytes, offset, length) : decodeUtf16be(bytes, offset, length);
|
|
532
|
+
if (!text.trim())
|
|
533
|
+
continue;
|
|
534
|
+
best = text.trim();
|
|
535
|
+
bestScore = score;
|
|
536
|
+
}
|
|
537
|
+
return best;
|
|
538
|
+
}, decodeUtf16be = (bytes, offset, length) => {
|
|
539
|
+
let out = "";
|
|
540
|
+
for (let i = 0;i + 1 < length; i += 2)
|
|
541
|
+
out += String.fromCharCode(bytes[offset + i] << 8 | bytes[offset + i + 1]);
|
|
542
|
+
return out;
|
|
543
|
+
}, decodeLatin1 = (bytes, offset, length) => {
|
|
544
|
+
let out = "";
|
|
545
|
+
for (let i = 0;i < length; i++)
|
|
546
|
+
out += String.fromCharCode(bytes[offset + i]);
|
|
547
|
+
return out;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// ../sdk/src/compile/fontMacro.ts
|
|
551
|
+
var FONT_ID, CODE_FILE, DECLARES_FONT, isIdentStart = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$", isIdentPart = (c2) => isIdentStart(c2) || c2 >= "0" && c2 <= "9", skipString = (src, start) => {
|
|
552
|
+
const quote = src[start];
|
|
553
|
+
const n = src.length;
|
|
554
|
+
let i = start + 1;
|
|
555
|
+
while (i < n) {
|
|
556
|
+
const c2 = src[i];
|
|
557
|
+
if (c2 === "\\") {
|
|
558
|
+
i += 2;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (quote === "`" && c2 === "$" && src[i + 1] === "{") {
|
|
562
|
+
i = skipBraces(src, i + 1);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (c2 === quote)
|
|
566
|
+
return i + 1;
|
|
567
|
+
i++;
|
|
568
|
+
}
|
|
569
|
+
return n;
|
|
570
|
+
}, skipBraces = (src, open) => {
|
|
571
|
+
const n = src.length;
|
|
572
|
+
let depth = 0;
|
|
573
|
+
let i = open;
|
|
574
|
+
while (i < n) {
|
|
575
|
+
const c2 = src[i];
|
|
576
|
+
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
577
|
+
i = skipString(src, i);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
if (c2 === "{")
|
|
581
|
+
depth++;
|
|
582
|
+
else if (c2 === "}") {
|
|
583
|
+
depth--;
|
|
584
|
+
if (depth === 0)
|
|
585
|
+
return i + 1;
|
|
586
|
+
}
|
|
587
|
+
i++;
|
|
588
|
+
}
|
|
589
|
+
return n;
|
|
590
|
+
}, matchParen = (src, open) => {
|
|
591
|
+
const n = src.length;
|
|
592
|
+
let depth = 0;
|
|
593
|
+
let i = open;
|
|
594
|
+
while (i < n) {
|
|
595
|
+
const c2 = src[i];
|
|
596
|
+
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
597
|
+
i = skipString(src, i);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (c2 === "/" && src[i + 1] === "/") {
|
|
601
|
+
i += 2;
|
|
602
|
+
while (i < n && src[i] !== `
|
|
603
|
+
`)
|
|
604
|
+
i++;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (c2 === "/" && src[i + 1] === "*") {
|
|
608
|
+
i += 2;
|
|
609
|
+
while (i < n && !(src[i] === "*" && src[i + 1] === "/"))
|
|
610
|
+
i++;
|
|
611
|
+
i += 2;
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (c2 === "(")
|
|
615
|
+
depth++;
|
|
616
|
+
else if (c2 === ")") {
|
|
617
|
+
depth--;
|
|
618
|
+
if (depth === 0)
|
|
619
|
+
return i;
|
|
620
|
+
}
|
|
621
|
+
i++;
|
|
622
|
+
}
|
|
623
|
+
return -1;
|
|
624
|
+
}, splitTopLevel = (text, delim) => {
|
|
625
|
+
const parts = [];
|
|
626
|
+
const n = text.length;
|
|
627
|
+
let depth = 0;
|
|
628
|
+
let last = 0;
|
|
629
|
+
let i = 0;
|
|
630
|
+
while (i < n) {
|
|
631
|
+
const c2 = text[i];
|
|
632
|
+
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
633
|
+
i = skipString(text, i);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
if (c2 === "(" || c2 === "[" || c2 === "{")
|
|
637
|
+
depth++;
|
|
638
|
+
else if (c2 === ")" || c2 === "]" || c2 === "}")
|
|
639
|
+
depth = Math.max(0, depth - 1);
|
|
640
|
+
else if (depth === 0 && c2 === delim) {
|
|
641
|
+
parts.push(text.slice(last, i));
|
|
642
|
+
last = i + 1;
|
|
643
|
+
}
|
|
644
|
+
i++;
|
|
645
|
+
}
|
|
646
|
+
parts.push(text.slice(last));
|
|
647
|
+
return parts;
|
|
648
|
+
}, stringLiteralValue = (text) => {
|
|
649
|
+
const t = text.trim();
|
|
650
|
+
if (t.length < 2)
|
|
651
|
+
return null;
|
|
652
|
+
const q = t[0];
|
|
653
|
+
if (q !== '"' && q !== "'" && q !== "`" || t[t.length - 1] !== q)
|
|
654
|
+
return null;
|
|
655
|
+
const inner = t.slice(1, -1);
|
|
656
|
+
if (q === "`" && inner.includes("${"))
|
|
657
|
+
return null;
|
|
658
|
+
if (inner.includes(q) && !inner.includes("\\" + q))
|
|
659
|
+
return null;
|
|
660
|
+
return inner.replace(/\\(.)/g, "$1");
|
|
661
|
+
}, prevNonSpace = (src, i) => {
|
|
662
|
+
let k = i - 1;
|
|
663
|
+
while (k >= 0 && /\s/.test(src[k]))
|
|
664
|
+
k--;
|
|
665
|
+
return k >= 0 ? src[k] : "";
|
|
666
|
+
}, scanFontCalls = (source) => {
|
|
667
|
+
const calls = [];
|
|
668
|
+
const n = source.length;
|
|
669
|
+
let i = 0;
|
|
670
|
+
while (i < n) {
|
|
671
|
+
const c2 = source[i];
|
|
672
|
+
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
673
|
+
i = skipString(source, i);
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (c2 === "/" && source[i + 1] === "/") {
|
|
677
|
+
i += 2;
|
|
678
|
+
while (i < n && source[i] !== `
|
|
679
|
+
`)
|
|
680
|
+
i++;
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (c2 === "/" && source[i + 1] === "*") {
|
|
684
|
+
i += 2;
|
|
685
|
+
while (i < n && !(source[i] === "*" && source[i + 1] === "/"))
|
|
686
|
+
i++;
|
|
687
|
+
i += 2;
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
if (isIdentStart(c2)) {
|
|
691
|
+
let j = i + 1;
|
|
692
|
+
while (j < n && isIdentPart(source[j]))
|
|
693
|
+
j++;
|
|
694
|
+
if (source.slice(i, j) === "font" && prevNonSpace(source, i) !== ".") {
|
|
695
|
+
const call = parseCall(source, i, j);
|
|
696
|
+
if (call) {
|
|
697
|
+
calls.push(call);
|
|
698
|
+
i = call.end;
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
i = j;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
i++;
|
|
706
|
+
}
|
|
707
|
+
return calls;
|
|
708
|
+
}, parseCall = (src, start, identEnd) => {
|
|
709
|
+
const n = src.length;
|
|
710
|
+
let i = identEnd;
|
|
711
|
+
while (i < n && /\s/.test(src[i]))
|
|
712
|
+
i++;
|
|
713
|
+
if (src[i] !== "(")
|
|
714
|
+
return null;
|
|
715
|
+
const close = matchParen(src, i);
|
|
716
|
+
if (close === -1)
|
|
717
|
+
return null;
|
|
718
|
+
const call = { start, end: close + 1, arg: "", kind: "registry" };
|
|
719
|
+
const args = splitTopLevel(src.slice(i + 1, close), ",");
|
|
720
|
+
const arg = stringLiteralValue(args[0] ?? "");
|
|
721
|
+
if (arg === null) {
|
|
722
|
+
call.error = `font(...) needs a string-literal argument — a registry id ("manrope") or a project path ("./fonts/Brand.ttf"). Got \`${(args[0] ?? "").trim() || "()"}\`.`;
|
|
723
|
+
return call;
|
|
724
|
+
}
|
|
725
|
+
call.arg = arg;
|
|
726
|
+
if (arg.startsWith("./") || arg.startsWith("../")) {
|
|
727
|
+
call.kind = "file";
|
|
728
|
+
if (!/\.(ttf|otf)$/i.test(arg)) {
|
|
729
|
+
call.error = `font("${arg}"): project fonts must be .ttf/.otf files.`;
|
|
730
|
+
return call;
|
|
731
|
+
}
|
|
732
|
+
if (args.length > 1 && args.slice(1).some((a) => a.trim())) {
|
|
733
|
+
call.error = `font("${arg}"): a project font takes no options — weight and style come from the file itself.`;
|
|
734
|
+
return call;
|
|
735
|
+
}
|
|
736
|
+
return call;
|
|
737
|
+
}
|
|
738
|
+
if (arg === "serif" || arg === "sans-serif" || arg === "monospace" || arg === "monospaced") {
|
|
739
|
+
call.error = `font("${arg}"): system font families need no font() — pass the string straight to fontFamily, e.g. .style({ fontFamily: "${arg}" }).`;
|
|
740
|
+
return call;
|
|
741
|
+
}
|
|
742
|
+
if (!FONT_ID.test(arg)) {
|
|
743
|
+
call.error = `font("${arg}"): expected a registry id (lowercase [a-z0-9-], e.g. "manrope") or a relative path ("./fonts/Brand.ttf").`;
|
|
744
|
+
return call;
|
|
745
|
+
}
|
|
746
|
+
call.kind = "registry";
|
|
747
|
+
call.id = arg;
|
|
748
|
+
const optsText = args[1]?.trim();
|
|
749
|
+
if (optsText) {
|
|
750
|
+
const err = parseOptions(optsText, call);
|
|
751
|
+
if (err) {
|
|
752
|
+
call.error = err;
|
|
753
|
+
return call;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return call;
|
|
757
|
+
}, parseOptions = (optsText, call) => {
|
|
758
|
+
if (!optsText.startsWith("{") || !optsText.endsWith("}")) {
|
|
759
|
+
return `font("${call.arg}", opts): opts must be an object literal like { weights: [400, 700] }.`;
|
|
760
|
+
}
|
|
761
|
+
for (const prop of splitTopLevel(optsText.slice(1, -1), ",")) {
|
|
762
|
+
if (!prop.trim())
|
|
763
|
+
continue;
|
|
764
|
+
const kv = splitTopLevel(prop, ":");
|
|
765
|
+
if (kv.length < 2)
|
|
766
|
+
return `font("${call.arg}"): couldn't parse option \`${prop.trim()}\`.`;
|
|
767
|
+
const key = kv[0].trim().replace(/^["'`]|["'`]$/g, "");
|
|
768
|
+
const raw = kv.slice(1).join(":").trim();
|
|
769
|
+
if (key === "weights") {
|
|
770
|
+
const m = /^\[\s*(\d+(?:\s*,\s*\d+)*)\s*,?\s*\]$/.exec(raw);
|
|
771
|
+
if (!m)
|
|
772
|
+
return `font("${call.arg}"): weights must be a literal number array, e.g. { weights: [400, 700] }.`;
|
|
773
|
+
call.weights = m[1].split(",").map((s) => parseInt(s.trim(), 10));
|
|
774
|
+
} else if (key === "italic") {
|
|
775
|
+
if (raw !== "true" && raw !== "false")
|
|
776
|
+
return `font("${call.arg}"): italic must be a literal true/false.`;
|
|
777
|
+
call.italic = raw === "true";
|
|
778
|
+
} else {
|
|
779
|
+
return `font("${call.arg}"): unknown option "${key}" (did you mean weights/italic?).`;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return null;
|
|
783
|
+
}, norm = (p) => p.replace(/\\/g, "/").replace(/^\//, ""), dirOf = (p) => {
|
|
784
|
+
const n = norm(p);
|
|
785
|
+
const i = n.lastIndexOf("/");
|
|
786
|
+
return i === -1 ? "" : n.slice(0, i);
|
|
787
|
+
}, baseOf = (p) => {
|
|
788
|
+
const n = norm(p);
|
|
789
|
+
return n.slice(n.lastIndexOf("/") + 1);
|
|
790
|
+
}, resolveRel = (dir, rel) => {
|
|
791
|
+
const out = dir ? dir.split("/") : [];
|
|
792
|
+
for (const seg of rel.split("/")) {
|
|
793
|
+
if (!seg || seg === ".")
|
|
794
|
+
continue;
|
|
795
|
+
if (seg === "..") {
|
|
796
|
+
if (out.length === 0)
|
|
797
|
+
return "";
|
|
798
|
+
out.pop();
|
|
799
|
+
} else
|
|
800
|
+
out.push(seg);
|
|
801
|
+
}
|
|
802
|
+
return out.join("/");
|
|
803
|
+
}, relSpecifier = (fromDir, toPath) => {
|
|
804
|
+
const from = fromDir ? fromDir.split("/") : [];
|
|
805
|
+
const to = norm(toPath).split("/");
|
|
806
|
+
let common = 0;
|
|
807
|
+
while (common < from.length && common < to.length - 1 && from[common] === to[common])
|
|
808
|
+
common++;
|
|
809
|
+
const ups = from.length - common;
|
|
810
|
+
const rest = to.slice(common).join("/");
|
|
811
|
+
return ups === 0 ? `./${rest}` : `${"../".repeat(ups)}${rest}`;
|
|
812
|
+
}, faceKey = (family, weight, italic) => `${family}|${weight}|${italic ? "i" : "n"}`, applyFontMacro = async (entries, opts) => {
|
|
813
|
+
const scanned = entries.filter((e) => e.type === "text" && CODE_FILE.test(e.path) && typeof e.text === "string" && e.text.includes("font(")).filter((e) => !DECLARES_FONT.test(e.text)).map((e) => ({ entry: e, calls: scanFontCalls(e.text) })).filter((s) => s.calls.length > 0);
|
|
814
|
+
if (scanned.length === 0)
|
|
815
|
+
return [];
|
|
816
|
+
const errors = scanned.flatMap((s) => s.calls.filter((c2) => c2.error).map((c2) => `${s.entry.path}: ${c2.error}`));
|
|
817
|
+
if (errors.length)
|
|
818
|
+
throw new Error([...new Set(errors)].join(`
|
|
819
|
+
`));
|
|
820
|
+
const registryBase = (opts.registryBase ?? fontRegistryBase()).replace(/\/+$/, "");
|
|
821
|
+
const entryByPath = new Map(entries.map((e) => [norm(e.path), e]));
|
|
822
|
+
const faces = new Map;
|
|
823
|
+
const fileFaceCache = new Map;
|
|
824
|
+
const resolveCall = (entryPath, call) => {
|
|
825
|
+
if (call.kind === "file") {
|
|
826
|
+
const filePath = resolveRel(dirOf(entryPath), call.arg);
|
|
827
|
+
const target = filePath && entryByPath.get(filePath);
|
|
828
|
+
if (!target)
|
|
829
|
+
throw new Error(`${entryPath}: font("${call.arg}"): no such file in the project (resolved to "${filePath || call.arg}").`);
|
|
830
|
+
call.filePath = filePath;
|
|
831
|
+
let info2 = fileFaceCache.get(filePath);
|
|
832
|
+
if (!info2) {
|
|
833
|
+
if (!opts.readBytes)
|
|
834
|
+
throw new Error(`${entryPath}: font("${call.arg}"): project-font files aren't supported in this compile context.`);
|
|
835
|
+
const bytes = opts.readBytes(filePath);
|
|
836
|
+
if (!bytes)
|
|
837
|
+
throw new Error(`${entryPath}: font("${call.arg}"): couldn't read "${filePath}".`);
|
|
838
|
+
try {
|
|
839
|
+
info2 = parseSfnt(bytes);
|
|
840
|
+
} catch (e) {
|
|
841
|
+
throw new Error(`${entryPath}: font("${call.arg}"): ${e.message}`);
|
|
842
|
+
}
|
|
843
|
+
fileFaceCache.set(filePath, info2);
|
|
844
|
+
}
|
|
845
|
+
return { family: info2.family, callFaces: [{ family: info2.family, weight: info2.weight, italic: info2.italic }] };
|
|
846
|
+
}
|
|
847
|
+
const meta = FONT_REGISTRY[call.id];
|
|
848
|
+
if (!meta) {
|
|
849
|
+
const known = Object.keys(FONT_REGISTRY);
|
|
850
|
+
throw new Error(`${entryPath}: font("${call.id}"): unknown registry font. Available: ${known.slice(0, 10).join(", ")}${known.length > 10 ? ", …" : ""}`);
|
|
851
|
+
}
|
|
852
|
+
const weights = call.weights ?? meta.defaultWeights;
|
|
853
|
+
const badW = weights.filter((w) => !meta.weights.includes(w));
|
|
854
|
+
if (badW.length)
|
|
855
|
+
throw new Error(`${entryPath}: font("${call.id}"): no ${badW.join("/")} face — available weights: ${meta.weights.join(", ")}.`);
|
|
856
|
+
if (call.italic) {
|
|
857
|
+
const badI = weights.filter((w) => !meta.italics.includes(w));
|
|
858
|
+
if (badI.length) {
|
|
859
|
+
throw new Error(`${entryPath}: font("${call.id}"): no italic ${badI.join("/")} face${meta.italics.length ? ` — italics exist for: ${meta.italics.join(", ")}` : " — the family ships no italics"}.`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const callFaces = weights.flatMap((w) => call.italic ? [{ id: call.id, family: meta.family, weight: w, italic: false }, { id: call.id, family: meta.family, weight: w, italic: true }] : [{ id: call.id, family: meta.family, weight: w, italic: false }]);
|
|
863
|
+
return { family: meta.family, callFaces };
|
|
864
|
+
};
|
|
865
|
+
const pendingRegistry = new Map;
|
|
866
|
+
const work = [];
|
|
867
|
+
for (const { entry, calls } of scanned) {
|
|
868
|
+
const fw = { entry, calls: [], faceKeys: new Set };
|
|
869
|
+
for (const call of calls) {
|
|
870
|
+
const { family, callFaces } = resolveCall(norm(entry.path), call);
|
|
871
|
+
fw.calls.push({ call, family });
|
|
872
|
+
for (const cf of callFaces) {
|
|
873
|
+
const key = faceKey(cf.family, cf.weight, cf.italic);
|
|
874
|
+
fw.faceKeys.add(key);
|
|
875
|
+
if (faces.has(key))
|
|
876
|
+
continue;
|
|
877
|
+
if (cf.id === undefined) {
|
|
878
|
+
faces.set(key, { family: cf.family, weight: cf.weight, italic: cf.italic, source: { kind: "entry", path: call.filePath } });
|
|
879
|
+
} else {
|
|
880
|
+
pendingRegistry.set(key, cf);
|
|
881
|
+
faces.set(key, { family: cf.family, weight: cf.weight, italic: cf.italic, source: { kind: "cdn", urlBase: "" } });
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
work.push(fw);
|
|
886
|
+
}
|
|
887
|
+
for (const [key, face] of pendingRegistry) {
|
|
888
|
+
const ref = faces.get(key);
|
|
889
|
+
if (opts.mode === "cdn") {
|
|
890
|
+
ref.source = { kind: "cdn", urlBase: `${registryBase}/${face.id}/${face.id}-${face.weight}${face.italic ? "-italic" : ""}.` };
|
|
891
|
+
} else {
|
|
892
|
+
if (!opts.vendorFace)
|
|
893
|
+
throw new Error(`font("${face.id}"): registry fonts need vendoring in this compile context, but no vendor hook was provided.`);
|
|
894
|
+
const path = await opts.vendorFace({ ...face, fileName: faceFileName(face.id, face.weight, face.italic, "ttf") });
|
|
895
|
+
ref.source = { kind: "entry", path: norm(path) };
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
for (const fw of work) {
|
|
899
|
+
const entryPath = norm(fw.entry.path);
|
|
900
|
+
const dir = dirOf(entryPath);
|
|
901
|
+
const modBase = `.${baseOf(entryPath)}.__fonts.ts`;
|
|
902
|
+
const modPath = dir ? `${dir}/${modBase}` : modBase;
|
|
903
|
+
const idents = new Map;
|
|
904
|
+
for (const { family } of fw.calls) {
|
|
905
|
+
if (!idents.has(family))
|
|
906
|
+
idents.set(family, `__font_${idents.size}`);
|
|
907
|
+
}
|
|
908
|
+
let text = fw.entry.text;
|
|
909
|
+
for (const { call, family } of [...fw.calls].sort((a, b) => b.call.start - a.call.start)) {
|
|
910
|
+
text = text.slice(0, call.start) + idents.get(family) + text.slice(call.end);
|
|
911
|
+
}
|
|
912
|
+
text += `
|
|
913
|
+
import { ${[...idents.values()].join(", ")} } from ${JSON.stringify(`./${modBase}`)}`;
|
|
914
|
+
fw.entry.text = text;
|
|
915
|
+
const lines = [
|
|
916
|
+
"// Generated by the font() compile macro: registers this file's fonts. Imports hoist, so the",
|
|
917
|
+
"// faces are declared before the importing module's body runs. Hosts dedupe repeated faces."
|
|
918
|
+
];
|
|
919
|
+
const faceImports = new Map;
|
|
920
|
+
const body = [];
|
|
921
|
+
for (const key of [...fw.faceKeys].sort()) {
|
|
922
|
+
const ref = faces.get(key);
|
|
923
|
+
let urlExpr;
|
|
924
|
+
if (ref.source.kind === "cdn") {
|
|
925
|
+
urlExpr = `${JSON.stringify(ref.source.urlBase)} + (_creatorUI.fontFormat ?? "ttf")`;
|
|
926
|
+
} else {
|
|
927
|
+
let ident = faceImports.get(ref.source.path);
|
|
928
|
+
if (!ident) {
|
|
929
|
+
ident = `__face_${faceImports.size}`;
|
|
930
|
+
faceImports.set(ref.source.path, ident);
|
|
931
|
+
lines.push(`import ${ident} from ${JSON.stringify(relSpecifier(dir, ref.source.path))}`);
|
|
932
|
+
}
|
|
933
|
+
urlExpr = ident;
|
|
934
|
+
}
|
|
935
|
+
const optionsExpr = ref.italic ? `{ weight: ${ref.weight}, style: "italic" }` : `{ weight: ${ref.weight} }`;
|
|
936
|
+
body.push(`_creatorUI.registerFont(${JSON.stringify(ref.family)}, ${urlExpr}, ${optionsExpr}, () => {}, () => {})`);
|
|
937
|
+
}
|
|
938
|
+
for (const [family, ident] of idents)
|
|
939
|
+
lines.push(`export const ${ident} = ${JSON.stringify(family)}`);
|
|
940
|
+
lines.push(...body);
|
|
941
|
+
entries.push({ path: modPath, type: "text", text: lines.join(`
|
|
942
|
+
`) + `
|
|
943
|
+
` });
|
|
944
|
+
}
|
|
945
|
+
return [...faces.values()];
|
|
946
|
+
}, buildFontsHeaderValue = (faces, entryUrl) => faces.map((f2) => {
|
|
947
|
+
const tag = `${f2.family}@${f2.weight}${f2.italic ? "i" : ""}`;
|
|
948
|
+
if (f2.source.kind === "cdn")
|
|
949
|
+
return `${tag}=${f2.source.urlBase}{fmt}`;
|
|
950
|
+
const url = entryUrl?.(f2.source.path);
|
|
951
|
+
return url ? `${tag}=${url}` : null;
|
|
952
|
+
}).filter((s) => !!s).join(";");
|
|
953
|
+
var init_fontMacro = __esm(() => {
|
|
954
|
+
init_fontRegistry();
|
|
955
|
+
FONT_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
956
|
+
CODE_FILE = /\.[tj]sx?$/;
|
|
957
|
+
DECLARES_FONT = /\b(?:function|const|let|var|class)\s+font\b|\bimport\s+(?:\{[^}]*\bfont\b[^}]*\}|font)\s+from\b/;
|
|
958
|
+
});
|
|
959
|
+
|
|
420
960
|
// ../sdk/src/compile/index.ts
|
|
421
961
|
var exports_compile = {};
|
|
422
962
|
__export(exports_compile, {
|
|
423
963
|
sceneEditorEntries: () => sceneEditorEntries,
|
|
964
|
+
scanFontCalls: () => scanFontCalls,
|
|
965
|
+
parseSfnt: () => parseSfnt,
|
|
424
966
|
offsetSourceMap: () => offsetSourceMap,
|
|
425
967
|
loadSceneHarness: () => loadSceneHarness,
|
|
426
968
|
inlineSourceMapComment: () => inlineSourceMapComment,
|
|
969
|
+
fontRegistryBase: () => fontRegistryBase,
|
|
970
|
+
faceFileName: () => faceFileName,
|
|
427
971
|
detectProjectKind: () => detectProjectKind,
|
|
428
972
|
compileProject: () => compileProject,
|
|
429
973
|
bundleProjectWithMap: () => bundleProjectWithMap,
|
|
430
974
|
bundleProject: () => bundleProject,
|
|
431
975
|
buildHeader: () => buildHeader,
|
|
976
|
+
buildFontsHeaderValue: () => buildFontsHeaderValue,
|
|
977
|
+
applyFontMacro: () => applyFontMacro,
|
|
432
978
|
SCENE_HARNESS_DIR: () => SCENE_HARNESS_DIR,
|
|
433
|
-
SCENE_ENTRY_PATH: () => SCENE_ENTRY_PATH
|
|
979
|
+
SCENE_ENTRY_PATH: () => SCENE_ENTRY_PATH,
|
|
980
|
+
FONT_REGISTRY: () => FONT_REGISTRY,
|
|
981
|
+
DEFAULT_FONT_REGISTRY_BASE: () => DEFAULT_FONT_REGISTRY_BASE
|
|
434
982
|
});
|
|
435
983
|
var init_compile = __esm(() => {
|
|
436
984
|
init_bundler();
|
|
437
985
|
init_compileProject();
|
|
438
986
|
init_sceneEditor();
|
|
987
|
+
init_fontMacro();
|
|
988
|
+
init_fontRegistry();
|
|
439
989
|
});
|
|
440
990
|
|
|
441
991
|
// ../../node_modules/.bun/qrcode-terminal@0.12.0/node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
|
|
@@ -1457,7 +2007,7 @@ var require_main = __commonJS((exports, module) => {
|
|
|
1457
2007
|
// package.json
|
|
1458
2008
|
var package_default = {
|
|
1459
2009
|
name: "lecodes-cli",
|
|
1460
|
-
version: "0.
|
|
2010
|
+
version: "0.8.1",
|
|
1461
2011
|
description: "Clone, edit and push LeCodes projects from your machine.",
|
|
1462
2012
|
type: "module",
|
|
1463
2013
|
license: "MIT",
|
|
@@ -1507,16 +2057,22 @@ var package_default = {
|
|
|
1507
2057
|
},
|
|
1508
2058
|
devDependencies: {
|
|
1509
2059
|
"lecodes-design": "workspace:*",
|
|
1510
|
-
"lecodes-renderer": "
|
|
2060
|
+
"lecodes-renderer": "workspace:*",
|
|
1511
2061
|
"qrcode-terminal": "^0.12.0",
|
|
1512
2062
|
"lecodes-3d-editor": "workspace:*",
|
|
1513
2063
|
sdk: "workspace:*"
|
|
1514
2064
|
}
|
|
1515
2065
|
};
|
|
1516
2066
|
|
|
1517
|
-
// src/commands/
|
|
1518
|
-
import {
|
|
1519
|
-
import {
|
|
2067
|
+
// src/commands/app.ts
|
|
2068
|
+
import { execFileSync } from "node:child_process";
|
|
2069
|
+
import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync10, readdirSync as readdirSync4, rmSync, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
2070
|
+
import { homedir as homedir2 } from "node:os";
|
|
2071
|
+
import { basename as basename4, dirname as dirname7, join as join11, resolve as resolve2 } from "node:path";
|
|
2072
|
+
|
|
2073
|
+
// src/manifest.ts
|
|
2074
|
+
import { dirname, join } from "node:path";
|
|
2075
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
1520
2076
|
|
|
1521
2077
|
// src/util.ts
|
|
1522
2078
|
import { createHash } from "node:crypto";
|
|
@@ -1621,17 +2177,145 @@ var flagStr = (args, ...keys) => {
|
|
|
1621
2177
|
};
|
|
1622
2178
|
var flagBool = (args, ...keys) => keys.some((key) => args.flags[key] === true || args.flags[key] === "true");
|
|
1623
2179
|
|
|
1624
|
-
// src/
|
|
1625
|
-
var
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
if (
|
|
1631
|
-
return
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
2180
|
+
// src/manifest.ts
|
|
2181
|
+
var LECODES_DIR = ".lecodes";
|
|
2182
|
+
var manifestPath = (root) => join(root, LECODES_DIR, "manifest.json");
|
|
2183
|
+
var findProjectRoot = (start) => {
|
|
2184
|
+
let dir = start;
|
|
2185
|
+
while (true) {
|
|
2186
|
+
if (existsSync(manifestPath(dir)))
|
|
2187
|
+
return dir;
|
|
2188
|
+
const parent = dirname(dir);
|
|
2189
|
+
if (parent === dir)
|
|
2190
|
+
throw new CliError("Not inside a lecodes project (no .lecodes/manifest.json found).");
|
|
2191
|
+
dir = parent;
|
|
2192
|
+
}
|
|
2193
|
+
};
|
|
2194
|
+
var readManifest = (root) => {
|
|
2195
|
+
try {
|
|
2196
|
+
return JSON.parse(readFileSync(manifestPath(root), "utf8"));
|
|
2197
|
+
} catch {
|
|
2198
|
+
throw new CliError("Could not read .lecodes/manifest.json — is this a lecodes project?");
|
|
2199
|
+
}
|
|
2200
|
+
};
|
|
2201
|
+
var writeManifest = (root, manifest) => {
|
|
2202
|
+
mkdirSync(join(root, LECODES_DIR), { recursive: true });
|
|
2203
|
+
writeFileSync(manifestPath(root), JSON.stringify(manifest, null, 2));
|
|
2204
|
+
};
|
|
2205
|
+
|
|
2206
|
+
// src/ignore.ts
|
|
2207
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2208
|
+
import { join as join2 } from "node:path";
|
|
2209
|
+
var BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store"];
|
|
2210
|
+
var DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
|
|
2211
|
+
# Only untracked files are affected — assets already in the project stay tracked
|
|
2212
|
+
# even if they match a pattern. Built-in defaults (node_modules, _*, *.log) always apply.
|
|
2213
|
+
|
|
2214
|
+
# Render / preview outputs (the default \`lecodes render --png\` target)
|
|
2215
|
+
screenshot.png
|
|
2216
|
+
shots/
|
|
2217
|
+
|
|
2218
|
+
# Image & other binary assets ARE pushed by default — they're real project resources.
|
|
2219
|
+
# Uncomment a pattern below only if you want to keep some untracked ones out of pushes.
|
|
2220
|
+
# *.png
|
|
2221
|
+
|
|
2222
|
+
# Local notes & dumps (uncomment to ignore)
|
|
2223
|
+
# *.json
|
|
2224
|
+
# notes.md
|
|
2225
|
+
`;
|
|
2226
|
+
var translate = (glob) => {
|
|
2227
|
+
let re = "";
|
|
2228
|
+
for (let i = 0;i < glob.length; i++) {
|
|
2229
|
+
const ch = glob[i];
|
|
2230
|
+
if (ch === "*") {
|
|
2231
|
+
if (glob[i + 1] === "*") {
|
|
2232
|
+
re += ".*";
|
|
2233
|
+
i++;
|
|
2234
|
+
if (glob[i + 1] === "/")
|
|
2235
|
+
i++;
|
|
2236
|
+
} else
|
|
2237
|
+
re += "[^/]*";
|
|
2238
|
+
} else if (ch === "?")
|
|
2239
|
+
re += "[^/]";
|
|
2240
|
+
else if (ch === "/")
|
|
2241
|
+
re += "/";
|
|
2242
|
+
else if ("\\^$.|+()[]{}".includes(ch))
|
|
2243
|
+
re += "\\" + ch;
|
|
2244
|
+
else
|
|
2245
|
+
re += ch;
|
|
2246
|
+
}
|
|
2247
|
+
return re;
|
|
2248
|
+
};
|
|
2249
|
+
var compile = (raw) => {
|
|
2250
|
+
let p = raw.trim();
|
|
2251
|
+
if (!p || p.startsWith("#"))
|
|
2252
|
+
return null;
|
|
2253
|
+
let negate = false;
|
|
2254
|
+
if (p.startsWith("!")) {
|
|
2255
|
+
negate = true;
|
|
2256
|
+
p = p.slice(1);
|
|
2257
|
+
}
|
|
2258
|
+
let dirOnly = false;
|
|
2259
|
+
if (p.endsWith("/")) {
|
|
2260
|
+
dirOnly = true;
|
|
2261
|
+
p = p.slice(0, -1);
|
|
2262
|
+
}
|
|
2263
|
+
const anchored = p.startsWith("/") || p.includes("/");
|
|
2264
|
+
if (p.startsWith("/"))
|
|
2265
|
+
p = p.slice(1);
|
|
2266
|
+
if (!p)
|
|
2267
|
+
return null;
|
|
2268
|
+
const body = translate(p);
|
|
2269
|
+
const re = anchored ? new RegExp("^/" + body + "(/.*)?$") : new RegExp("(^|/)" + body + "(/.*)?$");
|
|
2270
|
+
return { re, negate, dirOnly };
|
|
2271
|
+
};
|
|
2272
|
+
var buildMatcher = (rules) => (key, isDir) => {
|
|
2273
|
+
let ignored = false;
|
|
2274
|
+
for (const r of rules) {
|
|
2275
|
+
if (r.dirOnly && !isDir)
|
|
2276
|
+
continue;
|
|
2277
|
+
if (r.re.test(key))
|
|
2278
|
+
ignored = !r.negate;
|
|
2279
|
+
}
|
|
2280
|
+
return ignored;
|
|
2281
|
+
};
|
|
2282
|
+
var buildGlobMatcher = (patterns) => {
|
|
2283
|
+
const rules = patterns.map((p) => compile(p.trim().replace(/\/+$/, ""))).filter((r) => r !== null);
|
|
2284
|
+
const match = buildMatcher(rules);
|
|
2285
|
+
return (path) => match(path.startsWith("/") ? path : "/" + path, false);
|
|
2286
|
+
};
|
|
2287
|
+
var loadIgnore = (root) => {
|
|
2288
|
+
const lines = [...BUILTIN_PATTERNS];
|
|
2289
|
+
const file = join2(root, ".lecodesignore");
|
|
2290
|
+
if (existsSync2(file)) {
|
|
2291
|
+
try {
|
|
2292
|
+
lines.push(...readFileSync2(file, "utf8").split(/\r?\n/));
|
|
2293
|
+
} catch {}
|
|
2294
|
+
}
|
|
2295
|
+
const rules = lines.map(compile).filter((r) => r !== null);
|
|
2296
|
+
return buildMatcher(rules);
|
|
2297
|
+
};
|
|
2298
|
+
var writeDefaultIgnore = (root) => {
|
|
2299
|
+
const file = join2(root, ".lecodesignore");
|
|
2300
|
+
if (!existsSync2(file))
|
|
2301
|
+
writeFileSync2(file, DEFAULT_IGNORE_FILE);
|
|
2302
|
+
};
|
|
2303
|
+
|
|
2304
|
+
// src/commands/compile.ts
|
|
2305
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
2306
|
+
import { basename as basename3, resolve } from "node:path";
|
|
2307
|
+
|
|
2308
|
+
// src/api.ts
|
|
2309
|
+
var errorMessage = (body, status) => {
|
|
2310
|
+
if (typeof body === "string" && body)
|
|
2311
|
+
return body;
|
|
2312
|
+
if (body && typeof body === "object") {
|
|
2313
|
+
const b = body;
|
|
2314
|
+
if (typeof b.error === "string")
|
|
2315
|
+
return b.error;
|
|
2316
|
+
if (b.error && typeof b.error === "object") {
|
|
2317
|
+
const first = Object.values(b.error)[0];
|
|
2318
|
+
if (typeof first === "string")
|
|
1635
2319
|
return first;
|
|
1636
2320
|
if (first && typeof first === "object" && typeof first.message === "string") {
|
|
1637
2321
|
return first.message;
|
|
@@ -1688,15 +2372,15 @@ var downloadBlob = async (apiUrl, src) => {
|
|
|
1688
2372
|
|
|
1689
2373
|
// src/config.ts
|
|
1690
2374
|
import { homedir } from "node:os";
|
|
1691
|
-
import { join } from "node:path";
|
|
1692
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2375
|
+
import { join as join3 } from "node:path";
|
|
2376
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
1693
2377
|
var DEFAULT_API = "https://le.codes";
|
|
1694
|
-
var CONFIG_DIR =
|
|
1695
|
-
var CONFIG_FILE =
|
|
2378
|
+
var CONFIG_DIR = join3(homedir(), ".lecodes");
|
|
2379
|
+
var CONFIG_FILE = join3(CONFIG_DIR, "config.json");
|
|
1696
2380
|
var loadConfig = () => {
|
|
1697
2381
|
let stored = {};
|
|
1698
2382
|
try {
|
|
1699
|
-
stored = JSON.parse(
|
|
2383
|
+
stored = JSON.parse(readFileSync3(CONFIG_FILE, "utf8"));
|
|
1700
2384
|
} catch {}
|
|
1701
2385
|
return {
|
|
1702
2386
|
apiUrl: process.env.LECODES_API ?? stored.apiUrl,
|
|
@@ -1704,8 +2388,8 @@ var loadConfig = () => {
|
|
|
1704
2388
|
};
|
|
1705
2389
|
};
|
|
1706
2390
|
var saveConfig = (config) => {
|
|
1707
|
-
|
|
1708
|
-
|
|
2391
|
+
mkdirSync2(CONFIG_DIR, { recursive: true });
|
|
2392
|
+
writeFileSync3(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
1709
2393
|
};
|
|
1710
2394
|
var normalizeApiUrl = (url) => url.trim().replace(/\/+$/, "");
|
|
1711
2395
|
var requireToken = (config) => {
|
|
@@ -1719,39 +2403,124 @@ var requireApiUrl = (config) => {
|
|
|
1719
2403
|
return normalizeApiUrl(config.apiUrl);
|
|
1720
2404
|
};
|
|
1721
2405
|
|
|
1722
|
-
// src/
|
|
1723
|
-
import {
|
|
1724
|
-
import {
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2406
|
+
// src/compile/collect.ts
|
|
2407
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
2408
|
+
import { basename } from "node:path";
|
|
2409
|
+
|
|
2410
|
+
// src/localFiles.ts
|
|
2411
|
+
import { readdirSync, readFileSync as readFileSync4 } from "node:fs";
|
|
2412
|
+
import { join as join4, relative } from "node:path";
|
|
2413
|
+
var SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
|
|
2414
|
+
var SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
|
|
2415
|
+
var TEXT_EXTS = new Set([
|
|
2416
|
+
"ts",
|
|
2417
|
+
"tsx",
|
|
2418
|
+
"js",
|
|
2419
|
+
"jsx",
|
|
2420
|
+
"mjs",
|
|
2421
|
+
"cjs",
|
|
2422
|
+
"json",
|
|
2423
|
+
"svg",
|
|
2424
|
+
"glsl",
|
|
2425
|
+
"vert",
|
|
2426
|
+
"frag",
|
|
2427
|
+
"txt",
|
|
2428
|
+
"md",
|
|
2429
|
+
"css",
|
|
2430
|
+
"html",
|
|
2431
|
+
"xml",
|
|
2432
|
+
"yml",
|
|
2433
|
+
"yaml",
|
|
2434
|
+
"csv"
|
|
2435
|
+
]);
|
|
2436
|
+
var ext = (name) => {
|
|
2437
|
+
const i = name.lastIndexOf(".");
|
|
2438
|
+
return i < 0 ? "" : name.slice(i + 1).toLowerCase();
|
|
1737
2439
|
};
|
|
1738
|
-
var
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2440
|
+
var classifyKind = (name) => {
|
|
2441
|
+
const e = ext(name);
|
|
2442
|
+
if (e === "mat")
|
|
2443
|
+
return "shader";
|
|
2444
|
+
if (e === "scene")
|
|
2445
|
+
return "scene";
|
|
2446
|
+
return TEXT_EXTS.has(e) ? "text" : "resource";
|
|
1744
2447
|
};
|
|
1745
|
-
var
|
|
1746
|
-
|
|
1747
|
-
|
|
2448
|
+
var isTextKind = (kind) => kind !== "resource";
|
|
2449
|
+
var scanLocal = (root, opts = {}) => {
|
|
2450
|
+
const { ignore, tracked } = opts;
|
|
2451
|
+
const files = [];
|
|
2452
|
+
const dirs = [];
|
|
2453
|
+
const ignored = [];
|
|
2454
|
+
const isTracked = (key) => tracked?.has(key) ?? false;
|
|
2455
|
+
const hasTrackedUnder = (key) => {
|
|
2456
|
+
if (!tracked)
|
|
2457
|
+
return false;
|
|
2458
|
+
const prefix = key + "/";
|
|
2459
|
+
for (const t of tracked)
|
|
2460
|
+
if (t.startsWith(prefix))
|
|
2461
|
+
return true;
|
|
2462
|
+
return false;
|
|
2463
|
+
};
|
|
2464
|
+
const walk = (dir, parentIgnored) => {
|
|
2465
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
2466
|
+
const abs = join4(dir, entry.name);
|
|
2467
|
+
const key = toKey(relative(root, abs));
|
|
2468
|
+
if (entry.isDirectory()) {
|
|
2469
|
+
if (SKIP_DIRS.has(entry.name))
|
|
2470
|
+
continue;
|
|
2471
|
+
const dirIgnored = parentIgnored || !!ignore && ignore(key, true);
|
|
2472
|
+
if (dirIgnored && !isTracked(key) && !hasTrackedUnder(key)) {
|
|
2473
|
+
ignored.push(key + "/");
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
dirs.push(key);
|
|
2477
|
+
walk(abs, dirIgnored);
|
|
2478
|
+
} else if (entry.isFile()) {
|
|
2479
|
+
if (SKIP_FILES.has(entry.name))
|
|
2480
|
+
continue;
|
|
2481
|
+
const fileIgnored = parentIgnored || !!ignore && ignore(key, false);
|
|
2482
|
+
if (fileIgnored && !isTracked(key)) {
|
|
2483
|
+
ignored.push(key);
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
files.push({ path: key, absPath: abs, sha: sha256(readFileSync4(abs)) });
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
};
|
|
2490
|
+
walk(root, false);
|
|
2491
|
+
return { files, dirs, ignored };
|
|
2492
|
+
};
|
|
2493
|
+
var scanProject = (root, manifest) => scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) });
|
|
2494
|
+
var diffLocal = (manifest, scan) => {
|
|
2495
|
+
const added = [];
|
|
2496
|
+
const modified = [];
|
|
2497
|
+
const localByPath = new Map(scan.files.map((f) => [f.path, f]));
|
|
2498
|
+
for (const file of scan.files) {
|
|
2499
|
+
const known = manifest.files[file.path];
|
|
2500
|
+
if (!known)
|
|
2501
|
+
added.push(file);
|
|
2502
|
+
else if (known.sha !== file.sha)
|
|
2503
|
+
modified.push(file);
|
|
2504
|
+
}
|
|
2505
|
+
const deleted = Object.keys(manifest.files).filter((path) => !localByPath.has(path));
|
|
2506
|
+
const moves = new Map;
|
|
2507
|
+
const claimed = new Set;
|
|
2508
|
+
for (const file of added) {
|
|
2509
|
+
const from = deleted.find((path) => !claimed.has(path) && manifest.files[path].sha === file.sha);
|
|
2510
|
+
if (from) {
|
|
2511
|
+
moves.set(file.path, from);
|
|
2512
|
+
claimed.add(from);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
return { added, modified, deleted, moves };
|
|
1748
2516
|
};
|
|
2517
|
+
var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
|
|
1749
2518
|
|
|
1750
2519
|
// src/project.ts
|
|
1751
|
-
import { dirname as dirname2, join as
|
|
1752
|
-
import { mkdirSync as mkdirSync3, writeFileSync as
|
|
2520
|
+
import { dirname as dirname2, join as join5 } from "node:path";
|
|
2521
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1753
2522
|
var kindOf = (type) => type === "file" ? "resource" : type;
|
|
1754
|
-
var absOf = (root, key) =>
|
|
2523
|
+
var absOf = (root, key) => join5(root, ...key.replace(/^\//, "").split("/"));
|
|
1755
2524
|
var buildPaths = (assets) => {
|
|
1756
2525
|
const pathById = new Map;
|
|
1757
2526
|
for (const asset of assets) {
|
|
@@ -1778,11 +2547,11 @@ var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
|
|
|
1778
2547
|
if (!asset.file?.src)
|
|
1779
2548
|
continue;
|
|
1780
2549
|
const bytes = await downloadBlob(apiUrl, asset.file.src);
|
|
1781
|
-
|
|
2550
|
+
writeFileSync4(abs, bytes);
|
|
1782
2551
|
sha = sha256(bytes);
|
|
1783
2552
|
} else {
|
|
1784
2553
|
const text = asset.text ?? "";
|
|
1785
|
-
|
|
2554
|
+
writeFileSync4(abs, text);
|
|
1786
2555
|
sha = sha256(text);
|
|
1787
2556
|
}
|
|
1788
2557
|
files[key] = { id: asset.id, type: kindOf(asset.type), sha };
|
|
@@ -1791,479 +2560,1723 @@ var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
|
|
|
1791
2560
|
return files;
|
|
1792
2561
|
};
|
|
1793
2562
|
|
|
1794
|
-
// src/
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
var distRoot = (() => {
|
|
1803
|
-
const candidates = [
|
|
1804
|
-
join4(dirname3(fileURLToPath(import.meta.url)), ".."),
|
|
1805
|
-
dirname3(process.execPath)
|
|
1806
|
-
];
|
|
1807
|
-
for (const c2 of candidates)
|
|
1808
|
-
if (existsSync2(join4(c2, "runtime")))
|
|
1809
|
-
return c2;
|
|
1810
|
-
return candidates[0];
|
|
1811
|
-
})();
|
|
1812
|
-
|
|
1813
|
-
// src/types.ts
|
|
1814
|
-
var TSCONFIG = {
|
|
1815
|
-
compilerOptions: {
|
|
1816
|
-
target: "ES2020",
|
|
1817
|
-
module: "ESNext",
|
|
1818
|
-
moduleResolution: "bundler",
|
|
1819
|
-
lib: ["ES2020"],
|
|
1820
|
-
types: [],
|
|
1821
|
-
allowJs: true,
|
|
1822
|
-
checkJs: false,
|
|
1823
|
-
strict: false,
|
|
1824
|
-
noEmit: true,
|
|
1825
|
-
skipLibCheck: true
|
|
1826
|
-
},
|
|
1827
|
-
include: [`${LECODES_DIR}/types/**/*.d.ts`, "**/*.ts", "**/*.js"],
|
|
1828
|
-
exclude: ["node_modules"]
|
|
1829
|
-
};
|
|
1830
|
-
var writeTypeBundle = (root, bundle) => {
|
|
1831
|
-
const typesDir = join5(root, LECODES_DIR, "types");
|
|
1832
|
-
for (const [path, text] of Object.entries(bundle)) {
|
|
1833
|
-
const dest = join5(typesDir, path);
|
|
1834
|
-
mkdirSync4(dirname4(dest), { recursive: true });
|
|
1835
|
-
writeFileSync4(dest, text);
|
|
2563
|
+
// src/compile/collect.ts
|
|
2564
|
+
var collectEntries = (root, manifest, project, opts = {}) => {
|
|
2565
|
+
const pathById = buildPaths(project.assets);
|
|
2566
|
+
const serverByPath = new Map;
|
|
2567
|
+
for (const asset of project.assets) {
|
|
2568
|
+
if (asset.type === "folder")
|
|
2569
|
+
continue;
|
|
2570
|
+
serverByPath.set(pathById.get(asset.id), { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] });
|
|
1836
2571
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
if (
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2572
|
+
const entries = [];
|
|
2573
|
+
const warnings = [];
|
|
2574
|
+
const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) });
|
|
2575
|
+
for (const file of scan.files) {
|
|
2576
|
+
const type = manifest.files[file.path]?.type ?? classifyKind(basename(file.path));
|
|
2577
|
+
if (type === "resource") {
|
|
2578
|
+
const src = serverByPath.get(file.path)?.fileSrc;
|
|
2579
|
+
if (!src && (opts.warnMissingUrl ?? true))
|
|
2580
|
+
warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`);
|
|
2581
|
+
entries.push({ path: file.path, type, fileSrc: src, absPath: file.absPath });
|
|
2582
|
+
} else if (type === "shader") {
|
|
2583
|
+
const shaders = serverByPath.get(file.path)?.shaders;
|
|
2584
|
+
if (!shaders?.length)
|
|
2585
|
+
warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
|
|
2586
|
+
entries.push({ path: file.path, type, shaders });
|
|
2587
|
+
} else {
|
|
2588
|
+
entries.push({ path: file.path, type, text: readFileSync5(file.absPath, "utf8") });
|
|
1851
2589
|
}
|
|
1852
2590
|
}
|
|
1853
|
-
return
|
|
1854
|
-
};
|
|
1855
|
-
var materializeTypesLocal = (root) => {
|
|
1856
|
-
const bundle = readLocalTypeBundle();
|
|
1857
|
-
if (!bundle)
|
|
1858
|
-
return false;
|
|
1859
|
-
writeTypeBundle(root, bundle);
|
|
1860
|
-
return true;
|
|
2591
|
+
return { entries, warnings };
|
|
1861
2592
|
};
|
|
1862
|
-
|
|
1863
|
-
|
|
2593
|
+
|
|
2594
|
+
// src/compile/collectLocal.ts
|
|
2595
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
2596
|
+
import { basename as basename2 } from "node:path";
|
|
2597
|
+
var collectLocalEntries = (root, manifest, assetBase) => {
|
|
2598
|
+
const entries = [];
|
|
2599
|
+
const warnings = [];
|
|
2600
|
+
const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
|
|
2601
|
+
for (const file of scan.files) {
|
|
2602
|
+
const type = manifest?.files[file.path]?.type ?? classifyKind(basename2(file.path));
|
|
2603
|
+
if (type === "resource") {
|
|
2604
|
+
entries.push({ path: file.path, type, fileSrc: assetBase + encodeURI(file.path), absPath: file.absPath });
|
|
2605
|
+
} else if (type === "shader") {
|
|
2606
|
+
warnings.push(`${file.path}: shaders compile on the server — push the project once for shaders to work in dev mode.`);
|
|
2607
|
+
entries.push({ path: file.path, type, shaders: [] });
|
|
2608
|
+
} else {
|
|
2609
|
+
entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
return { entries, warnings };
|
|
1864
2613
|
};
|
|
1865
2614
|
|
|
1866
|
-
// src/
|
|
1867
|
-
import { existsSync as
|
|
1868
|
-
import {
|
|
1869
|
-
|
|
1870
|
-
var DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
|
|
1871
|
-
# Only untracked files are affected — assets already in the project stay tracked
|
|
1872
|
-
# even if they match a pattern. Built-in defaults (node_modules, _*, *.log) always apply.
|
|
2615
|
+
// src/compile/projectCompile.ts
|
|
2616
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
2617
|
+
import { createRequire as createRequire3 } from "node:module";
|
|
2618
|
+
import { dirname as dirname5, join as join9 } from "node:path";
|
|
1873
2619
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
2620
|
+
// src/distRoot.ts
|
|
2621
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
2622
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
2623
|
+
import { fileURLToPath } from "node:url";
|
|
2624
|
+
var distRoot = (() => {
|
|
2625
|
+
const candidates = [
|
|
2626
|
+
join6(dirname3(fileURLToPath(import.meta.url)), ".."),
|
|
2627
|
+
dirname3(process.execPath)
|
|
2628
|
+
];
|
|
2629
|
+
for (const c2 of candidates)
|
|
2630
|
+
if (existsSync3(join6(c2, "runtime")))
|
|
2631
|
+
return c2;
|
|
2632
|
+
return candidates[0];
|
|
2633
|
+
})();
|
|
1877
2634
|
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2635
|
+
// src/compile/projectCompile.ts
|
|
2636
|
+
init_compile();
|
|
2637
|
+
var vendoredInject = join9(distRoot, "runtime", "sdk", "inject.ts");
|
|
2638
|
+
if (existsSync6(vendoredInject))
|
|
2639
|
+
process.env.LECODES_SDK_INJECT = vendoredInject;
|
|
2640
|
+
var resolveChiselBin2 = () => {
|
|
2641
|
+
if (process.env.LECODES_CHISEL_BIN)
|
|
2642
|
+
return process.env.LECODES_CHISEL_BIN;
|
|
2643
|
+
try {
|
|
2644
|
+
const fromPkg = createRequire3(import.meta.url)("@letary/chisel").binaryPath();
|
|
2645
|
+
if (fromPkg)
|
|
2646
|
+
return fromPkg;
|
|
2647
|
+
} catch {}
|
|
2648
|
+
const local = join9(dirname5(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel");
|
|
2649
|
+
return existsSync6(local) ? local : null;
|
|
2650
|
+
};
|
|
2651
|
+
var chiselBin = resolveChiselBin2();
|
|
2652
|
+
if (chiselBin)
|
|
2653
|
+
process.env.LECODES_CHISEL_BIN = chiselBin;
|
|
2654
|
+
var compileProject2 = (opts) => compileProject({
|
|
2655
|
+
entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, remote: e.remote, shaders: e.shaders })),
|
|
2656
|
+
name: opts.name,
|
|
2657
|
+
publicUrl: opts.publicUrl,
|
|
2658
|
+
entryOverride: opts.entryOverride,
|
|
2659
|
+
header: opts.header,
|
|
2660
|
+
fontsHeader: opts.fontsHeader,
|
|
2661
|
+
format: opts.format,
|
|
2662
|
+
localAssets: opts.localAssets,
|
|
2663
|
+
minify: opts.minify,
|
|
2664
|
+
sourcemap: opts.sourcemap,
|
|
2665
|
+
editor: opts.editor
|
|
2666
|
+
});
|
|
1881
2667
|
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2668
|
+
// src/compile/fonts.ts
|
|
2669
|
+
init_compile();
|
|
2670
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2671
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2672
|
+
import { dirname as dirname6, join as join10 } from "node:path";
|
|
2673
|
+
var applyProjectFonts = async (entries, opts) => {
|
|
2674
|
+
const registry = (opts.registry ?? fontRegistryBase()).replace(/\/+$/, "");
|
|
2675
|
+
const byPath = () => new Map(entries.map((e) => [e.path.replace(/^\//, ""), e]));
|
|
2676
|
+
const faces = await applyFontMacro(entries, {
|
|
2677
|
+
mode: opts.mode,
|
|
2678
|
+
registryBase: registry,
|
|
2679
|
+
readBytes: (entryPath) => {
|
|
2680
|
+
const entry = byPath().get(entryPath);
|
|
2681
|
+
const abs = entry?.absPath ?? join10(opts.root, entryPath);
|
|
2682
|
+
try {
|
|
2683
|
+
return new Uint8Array(readFileSync9(abs));
|
|
2684
|
+
} catch {
|
|
2685
|
+
return null;
|
|
2686
|
+
}
|
|
2687
|
+
},
|
|
2688
|
+
vendorFace: opts.mode === "vendor" ? async (face) => {
|
|
2689
|
+
const vendorDir = opts.vendorDir ?? opts.root;
|
|
2690
|
+
const rel = `assets/fonts/${face.fileName}`;
|
|
2691
|
+
const abs = join10(vendorDir, rel);
|
|
2692
|
+
const entryPath = `${opts.vendorPrefix ?? ""}/${rel}`;
|
|
2693
|
+
if (!existsSync7(abs))
|
|
2694
|
+
await downloadFace(registry, vendorDir, face, rel, abs);
|
|
2695
|
+
if (!byPath().has(entryPath.replace(/^\//, ""))) {
|
|
2696
|
+
entries.push(opts.makeResourceEntry?.(entryPath, abs) ?? { path: entryPath, type: "resource", absPath: abs });
|
|
2697
|
+
}
|
|
2698
|
+
return entryPath;
|
|
2699
|
+
} : undefined
|
|
2700
|
+
});
|
|
2701
|
+
if (faces.length === 0)
|
|
2702
|
+
return;
|
|
2703
|
+
return buildFontsHeaderValue(faces, (path) => {
|
|
2704
|
+
if (!opts.publicUrl || !/^https?:/i.test(opts.publicUrl))
|
|
2705
|
+
return null;
|
|
2706
|
+
const fileSrc = byPath().get(path.replace(/^\//, ""))?.fileSrc;
|
|
2707
|
+
return fileSrc ? opts.publicUrl + fileSrc : null;
|
|
2708
|
+
}) || undefined;
|
|
1908
2709
|
};
|
|
1909
|
-
var
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
p = p.slice(1);
|
|
1917
|
-
}
|
|
1918
|
-
let dirOnly = false;
|
|
1919
|
-
if (p.endsWith("/")) {
|
|
1920
|
-
dirOnly = true;
|
|
1921
|
-
p = p.slice(0, -1);
|
|
2710
|
+
var isHttp = (s) => /^https?:/i.test(s);
|
|
2711
|
+
var sha12 = (b) => createHash2("sha256").update(b).digest("hex").slice(0, 12);
|
|
2712
|
+
var registryHome = (registry) => registry.replace(/\/v\d+$/, "");
|
|
2713
|
+
var readRegistry = async (registry, rel) => {
|
|
2714
|
+
if (isHttp(registry)) {
|
|
2715
|
+
const res = await fetch(`${registry}/${rel}`);
|
|
2716
|
+
return res.ok ? new Uint8Array(await res.arrayBuffer()) : null;
|
|
1922
2717
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
if (!p)
|
|
2718
|
+
try {
|
|
2719
|
+
return new Uint8Array(readFileSync9(join10(registry, rel)));
|
|
2720
|
+
} catch {
|
|
1927
2721
|
return null;
|
|
1928
|
-
|
|
1929
|
-
const re = anchored ? new RegExp("^/" + body + "(/.*)?$") : new RegExp("(^|/)" + body + "(/.*)?$");
|
|
1930
|
-
return { re, negate, dirOnly };
|
|
2722
|
+
}
|
|
1931
2723
|
};
|
|
1932
|
-
var
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
if (
|
|
2724
|
+
var readLock = (path) => {
|
|
2725
|
+
try {
|
|
2726
|
+
const lock = JSON.parse(readFileSync9(path, "utf8"));
|
|
2727
|
+
if (lock && typeof lock === "object" && lock.faces && lock.families)
|
|
2728
|
+
return lock;
|
|
2729
|
+
} catch {}
|
|
2730
|
+
return null;
|
|
2731
|
+
};
|
|
2732
|
+
var serializeLock = (lock) => {
|
|
2733
|
+
const sortKeys = (rec) => Object.fromEntries(Object.keys(rec).sort().map((k) => [k, rec[k]]));
|
|
2734
|
+
return JSON.stringify({ registry: lock.registry, faces: sortKeys(lock.faces), families: sortKeys(lock.families) }, null, 2) + `
|
|
2735
|
+
`;
|
|
2736
|
+
};
|
|
2737
|
+
var renderNotices = (lock) => {
|
|
2738
|
+
const ids = [...new Set(Object.values(lock.faces).map((f2) => f2.id))].sort();
|
|
2739
|
+
const lines = [
|
|
2740
|
+
"# Font attribution & notices",
|
|
2741
|
+
"",
|
|
2742
|
+
"Fonts vendored from the font registry via `font(...)`. Keep this file in your repo / product.",
|
|
2743
|
+
""
|
|
2744
|
+
];
|
|
2745
|
+
for (const id of ids) {
|
|
2746
|
+
const l = lock.families[id];
|
|
2747
|
+
if (!l)
|
|
1936
2748
|
continue;
|
|
1937
|
-
|
|
1938
|
-
|
|
2749
|
+
let note2 = `- **${l.title || id}** — ${l.spdx}${l.url ? ` — ${l.url}` : ""}`;
|
|
2750
|
+
if (l.attribution)
|
|
2751
|
+
note2 += `
|
|
2752
|
+
- **Attribution required:** ${l.attribution}`;
|
|
2753
|
+
lines.push(note2);
|
|
1939
2754
|
}
|
|
1940
|
-
|
|
2755
|
+
lines.push("");
|
|
2756
|
+
return lines.join(`
|
|
2757
|
+
`);
|
|
1941
2758
|
};
|
|
1942
|
-
var
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2759
|
+
var writeIfChanged = (path, text) => {
|
|
2760
|
+
try {
|
|
2761
|
+
if (readFileSync9(path, "utf8") === text)
|
|
2762
|
+
return;
|
|
2763
|
+
} catch {}
|
|
2764
|
+
writeFileSync5(path, text);
|
|
2765
|
+
};
|
|
2766
|
+
var downloadFace = async (registry, vendorDir, face, rel, abs) => {
|
|
2767
|
+
const licRaw = await readRegistry(registry, "licenses.json");
|
|
2768
|
+
if (!licRaw)
|
|
2769
|
+
throw new Error(`font: can't reach the font registry (${registry}). Check your connection, or set LECODES_FONT_REGISTRY.`);
|
|
2770
|
+
const licenses = JSON.parse(new TextDecoder().decode(licRaw));
|
|
2771
|
+
const lic = licenses[face.id];
|
|
2772
|
+
if (!lic)
|
|
2773
|
+
throw new Error(`font("${face.id}"): the registry has no license entry for this family — it may not be published yet. Browse ${registryHome(registry)}.`);
|
|
2774
|
+
const bytes = await readRegistry(registry, `${face.id}/${face.fileName}`);
|
|
2775
|
+
if (!bytes)
|
|
2776
|
+
throw new Error(`font("${face.id}"): face "${face.fileName}" not found in the registry (${registryHome(registry)}).`);
|
|
2777
|
+
mkdirSync4(dirname6(abs), { recursive: true });
|
|
2778
|
+
writeFileSync5(abs, bytes);
|
|
2779
|
+
const lockPath = join10(vendorDir, "fonts.lock.json");
|
|
2780
|
+
const lock = readLock(lockPath) ?? { registry, faces: {}, families: {} };
|
|
2781
|
+
lock.registry = registry;
|
|
2782
|
+
lock.faces[`${face.id}@${face.weight}${face.italic ? "i" : ""}`] = { id: face.id, family: face.family, weight: face.weight, italic: face.italic, hash: sha12(bytes), spdx: lic.spdx, path: rel };
|
|
2783
|
+
lock.families[face.id] = lic;
|
|
2784
|
+
writeIfChanged(lockPath, serializeLock(lock));
|
|
2785
|
+
writeIfChanged(join10(vendorDir, "FONT-NOTICES.md"), renderNotices(lock));
|
|
2786
|
+
};
|
|
2787
|
+
|
|
2788
|
+
// src/commands/compile.ts
|
|
2789
|
+
var compile2 = async (args) => {
|
|
2790
|
+
await compileImpl(args);
|
|
2791
|
+
};
|
|
2792
|
+
var compileShell = (args, shell) => compileImpl(args, shell);
|
|
2793
|
+
var compileImpl = async (args, shell) => {
|
|
2794
|
+
let root;
|
|
2795
|
+
let manifest;
|
|
2796
|
+
try {
|
|
2797
|
+
root = findProjectRoot(process.cwd());
|
|
2798
|
+
manifest = readManifest(root);
|
|
2799
|
+
} catch {
|
|
2800
|
+
root = process.cwd();
|
|
2801
|
+
manifest = null;
|
|
1949
2802
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
2803
|
+
let entries;
|
|
2804
|
+
let warnings;
|
|
2805
|
+
let name;
|
|
2806
|
+
let publicUrl;
|
|
2807
|
+
let localAssets = false;
|
|
2808
|
+
if (manifest) {
|
|
2809
|
+
const config = loadConfig();
|
|
2810
|
+
const apiUrl = requireApiUrl(config);
|
|
2811
|
+
const token = requireToken(config);
|
|
2812
|
+
note("Resolving project assets…");
|
|
2813
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
2814
|
+
({ entries, warnings } = collectEntries(root, manifest, project, { warnMissingUrl: !shell }));
|
|
2815
|
+
name = manifest.name;
|
|
2816
|
+
publicUrl = flagStr(args, "public-url") ?? apiUrl;
|
|
2817
|
+
localAssets = !!shell;
|
|
2818
|
+
if (shell && shell.remotePatterns.length > 0) {
|
|
2819
|
+
const isRemote = buildGlobMatcher(shell.remotePatterns);
|
|
2820
|
+
for (const e of entries) {
|
|
2821
|
+
if (e.type !== "resource" || !isRemote(e.path))
|
|
2822
|
+
continue;
|
|
2823
|
+
if (!e.fileSrc) {
|
|
2824
|
+
warnErr(`${e.path} matches resources.remote but has no server URL yet — bundling it (push, then re-sync).`);
|
|
2825
|
+
continue;
|
|
2826
|
+
}
|
|
2827
|
+
e.remote = true;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
} else {
|
|
2831
|
+
const publicUrlFlag = flagStr(args, "public-url");
|
|
2832
|
+
localAssets = shell ? true : publicUrlFlag === undefined;
|
|
2833
|
+
publicUrl = publicUrlFlag ?? "";
|
|
2834
|
+
({ entries, warnings } = collectLocalEntries(root, null, ""));
|
|
2835
|
+
name = basename3(root);
|
|
2836
|
+
if (shell && shell.remotePatterns.length > 0) {
|
|
2837
|
+
warnErr("resources.remote needs a server-backed project (a streamed asset needs a server URL) — bundling everything.");
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
const fontsHeader = await applyProjectFonts(entries, manifest && !shell ? { mode: "cdn", root, publicUrl } : {
|
|
2841
|
+
mode: "vendor",
|
|
2842
|
+
root,
|
|
2843
|
+
vendorDir: root,
|
|
2844
|
+
vendorPrefix: "",
|
|
2845
|
+
publicUrl: shell ? "" : publicUrl,
|
|
2846
|
+
makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURI(path), absPath })
|
|
2847
|
+
});
|
|
2848
|
+
note("Compiling…");
|
|
2849
|
+
const js = await compileProject2({
|
|
2850
|
+
entries,
|
|
2851
|
+
name,
|
|
2852
|
+
publicUrl,
|
|
2853
|
+
entryOverride: flagStr(args, "entry"),
|
|
2854
|
+
header: !flagBool(args, "no-header"),
|
|
2855
|
+
localAssets,
|
|
2856
|
+
fontsHeader,
|
|
2857
|
+
minify: flagBool(args, "no-minify") ? false : undefined,
|
|
2858
|
+
sourcemap: flagBool(args, "no-sourcemap", "no-source-map") ? false : undefined
|
|
2859
|
+
});
|
|
2860
|
+
for (const w of warnings)
|
|
2861
|
+
warnErr(w);
|
|
2862
|
+
const out = flagStr(args, "o", "out");
|
|
2863
|
+
if (out && isNullSink(out)) {
|
|
2864
|
+
note(`Discarded output (${out})`);
|
|
2865
|
+
} else if (out) {
|
|
2866
|
+
writeFileSync6(resolve(out), js);
|
|
2867
|
+
note(`Wrote ${out} (${js.length} bytes)`);
|
|
2868
|
+
} else {
|
|
2869
|
+
process.stdout.write(js);
|
|
2870
|
+
if (!js.endsWith(`
|
|
2871
|
+
`))
|
|
2872
|
+
process.stdout.write(`
|
|
2873
|
+
`);
|
|
2874
|
+
}
|
|
2875
|
+
return entries.filter((e) => e.type === "resource").map((e) => ({
|
|
2876
|
+
path: e.path.replace(/^\//, ""),
|
|
2877
|
+
name: e.path.split(/[\\/]/).pop() ?? e.path,
|
|
2878
|
+
absPath: e.absPath,
|
|
2879
|
+
remote: !!e.remote
|
|
2880
|
+
}));
|
|
1952
2881
|
};
|
|
1953
|
-
var
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2882
|
+
var previewLegacy = async (args) => {
|
|
2883
|
+
warnErr("'lecodes preview' is now 'lecodes compile' — the old name still works.");
|
|
2884
|
+
await compile2(args);
|
|
2885
|
+
};
|
|
2886
|
+
|
|
2887
|
+
// src/commands/appTemplates.ts
|
|
2888
|
+
var pbxStr = (s) => s.replace(/[\\"]/g, (m) => "\\" + m);
|
|
2889
|
+
var pbxprojTemplate = ({ displayName, bundleId, teamId, version, buildNumber }) => {
|
|
2890
|
+
const target = `
|
|
2891
|
+
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
2892
|
+
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
2893
|
+
CODE_SIGN_STYLE = Automatic;
|
|
2894
|
+
CURRENT_PROJECT_VERSION = ${buildNumber};
|
|
2895
|
+
DEVELOPMENT_TEAM = "${pbxStr(teamId)}";
|
|
2896
|
+
GENERATE_INFOPLIST_FILE = YES;
|
|
2897
|
+
INFOPLIST_FILE = "App/Info.plist";
|
|
2898
|
+
INFOPLIST_KEY_CFBundleDisplayName = "${pbxStr(displayName)}";
|
|
2899
|
+
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
2900
|
+
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
|
2901
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
2902
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
2903
|
+
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
|
2904
|
+
LD_RUNPATH_SEARCH_PATHS = (
|
|
2905
|
+
"$(inherited)",
|
|
2906
|
+
"@executable_path/Frameworks",
|
|
2907
|
+
);
|
|
2908
|
+
MARKETING_VERSION = ${version};
|
|
2909
|
+
PRODUCT_BUNDLE_IDENTIFIER = "${pbxStr(bundleId)}";
|
|
2910
|
+
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
2911
|
+
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
|
2912
|
+
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
2913
|
+
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
|
2914
|
+
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
2915
|
+
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
2916
|
+
SWIFT_VERSION = 5.0;
|
|
2917
|
+
TARGETED_DEVICE_FAMILY = "1,2";`;
|
|
2918
|
+
const project = (debug) => `
|
|
2919
|
+
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
2920
|
+
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
2921
|
+
CLANG_ANALYZER_NONNULL = YES;
|
|
2922
|
+
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
2923
|
+
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
2924
|
+
CLANG_ENABLE_MODULES = YES;
|
|
2925
|
+
CLANG_ENABLE_OBJC_ARC = YES;
|
|
2926
|
+
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
2927
|
+
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
2928
|
+
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
2929
|
+
CLANG_WARN_COMMA = YES;
|
|
2930
|
+
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
2931
|
+
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
2932
|
+
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
2933
|
+
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
2934
|
+
CLANG_WARN_EMPTY_BODY = YES;
|
|
2935
|
+
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
2936
|
+
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
2937
|
+
CLANG_WARN_INT_CONVERSION = YES;
|
|
2938
|
+
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
2939
|
+
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
2940
|
+
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
2941
|
+
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
2942
|
+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
2943
|
+
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
2944
|
+
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
2945
|
+
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
2946
|
+
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
2947
|
+
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
2948
|
+
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
2949
|
+
COPY_PHASE_STRIP = NO;
|
|
2950
|
+
DEBUG_INFORMATION_FORMAT = ${debug ? "dwarf" : '"dwarf-with-dsym"'};
|
|
2951
|
+
DEVELOPMENT_TEAM = "${pbxStr(teamId)}";${debug ? "" : `
|
|
2952
|
+
ENABLE_NS_ASSERTIONS = NO;`}
|
|
2953
|
+
ENABLE_STRICT_OBJC_MSGSEND = YES;${debug ? `
|
|
2954
|
+
ENABLE_TESTABILITY = YES;` : ""}
|
|
2955
|
+
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
2956
|
+
GCC_C_LANGUAGE_STANDARD = gnu17;${debug ? `
|
|
2957
|
+
GCC_DYNAMIC_NO_PIC = NO;` : ""}
|
|
2958
|
+
GCC_NO_COMMON_BLOCKS = YES;${debug ? `
|
|
2959
|
+
GCC_OPTIMIZATION_LEVEL = 0;
|
|
2960
|
+
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
2961
|
+
"DEBUG=1",
|
|
2962
|
+
"$(inherited)",
|
|
2963
|
+
);` : ""}
|
|
2964
|
+
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
2965
|
+
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
2966
|
+
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
2967
|
+
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
2968
|
+
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
2969
|
+
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
2970
|
+
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
|
2971
|
+
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
2972
|
+
MTL_ENABLE_DEBUG_INFO = ${debug ? "INCLUDE_SOURCE" : "NO"};
|
|
2973
|
+
MTL_FAST_MATH = YES;${debug ? `
|
|
2974
|
+
ONLY_ACTIVE_ARCH = YES;` : ""}
|
|
2975
|
+
SDKROOT = iphoneos;${debug ? `
|
|
2976
|
+
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
|
2977
|
+
SWIFT_OPTIMIZATION_LEVEL = "-Onone";` : `
|
|
2978
|
+
SWIFT_COMPILATION_MODE = wholemodule;
|
|
2979
|
+
VALIDATE_PRODUCT = YES;`}`;
|
|
2980
|
+
return `// !$*UTF8*$!
|
|
2981
|
+
{
|
|
2982
|
+
archiveVersion = 1;
|
|
2983
|
+
classes = {
|
|
2984
|
+
};
|
|
2985
|
+
objectVersion = 77;
|
|
2986
|
+
objects = {
|
|
2987
|
+
|
|
2988
|
+
/* Begin PBXBuildFile section */
|
|
2989
|
+
B73550812FEBAC710005B4CC /* LeCodesRuntime in Frameworks */ = {isa = PBXBuildFile; productRef = B73550802FEBAC710005B4CC /* LeCodesRuntime */; };
|
|
2990
|
+
/* End PBXBuildFile section */
|
|
2991
|
+
|
|
2992
|
+
/* Begin PBXFileReference section */
|
|
2993
|
+
B73550672FEBAC0F0005B4CC /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
2994
|
+
/* End PBXFileReference section */
|
|
2995
|
+
|
|
2996
|
+
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
|
2997
|
+
B73550792FEBAC100005B4CC /* Exceptions for "App" folder in "App" target */ = {
|
|
2998
|
+
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
|
2999
|
+
membershipExceptions = (
|
|
3000
|
+
Info.plist,
|
|
3001
|
+
);
|
|
3002
|
+
target = B73550662FEBAC0F0005B4CC /* App */;
|
|
3003
|
+
};
|
|
3004
|
+
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
|
3005
|
+
|
|
3006
|
+
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
|
3007
|
+
B73550692FEBAC0F0005B4CC /* App */ = {
|
|
3008
|
+
isa = PBXFileSystemSynchronizedRootGroup;
|
|
3009
|
+
exceptions = (
|
|
3010
|
+
B73550792FEBAC100005B4CC /* Exceptions for "App" folder in "App" target */,
|
|
3011
|
+
);
|
|
3012
|
+
path = App;
|
|
3013
|
+
sourceTree = "<group>";
|
|
3014
|
+
};
|
|
3015
|
+
/* End PBXFileSystemSynchronizedRootGroup section */
|
|
3016
|
+
|
|
3017
|
+
/* Begin PBXFrameworksBuildPhase section */
|
|
3018
|
+
B73550642FEBAC0F0005B4CC /* Frameworks */ = {
|
|
3019
|
+
isa = PBXFrameworksBuildPhase;
|
|
3020
|
+
buildActionMask = 2147483647;
|
|
3021
|
+
files = (
|
|
3022
|
+
B73550812FEBAC710005B4CC /* LeCodesRuntime in Frameworks */,
|
|
3023
|
+
);
|
|
3024
|
+
runOnlyForDeploymentPostprocessing = 0;
|
|
3025
|
+
};
|
|
3026
|
+
/* End PBXFrameworksBuildPhase section */
|
|
3027
|
+
|
|
3028
|
+
/* Begin PBXGroup section */
|
|
3029
|
+
B735505E2FEBAC0F0005B4CC = {
|
|
3030
|
+
isa = PBXGroup;
|
|
3031
|
+
children = (
|
|
3032
|
+
B73550692FEBAC0F0005B4CC /* App */,
|
|
3033
|
+
B73550682FEBAC0F0005B4CC /* Products */,
|
|
3034
|
+
);
|
|
3035
|
+
sourceTree = "<group>";
|
|
3036
|
+
};
|
|
3037
|
+
B73550682FEBAC0F0005B4CC /* Products */ = {
|
|
3038
|
+
isa = PBXGroup;
|
|
3039
|
+
children = (
|
|
3040
|
+
B73550672FEBAC0F0005B4CC /* App.app */,
|
|
3041
|
+
);
|
|
3042
|
+
name = Products;
|
|
3043
|
+
sourceTree = "<group>";
|
|
3044
|
+
};
|
|
3045
|
+
/* End PBXGroup section */
|
|
3046
|
+
|
|
3047
|
+
/* Begin PBXNativeTarget section */
|
|
3048
|
+
B73550662FEBAC0F0005B4CC /* App */ = {
|
|
3049
|
+
isa = PBXNativeTarget;
|
|
3050
|
+
buildConfigurationList = B735507A2FEBAC100005B4CC /* Build configuration list for PBXNativeTarget "App" */;
|
|
3051
|
+
buildPhases = (
|
|
3052
|
+
B73550632FEBAC0F0005B4CC /* Sources */,
|
|
3053
|
+
B73550642FEBAC0F0005B4CC /* Frameworks */,
|
|
3054
|
+
B73550652FEBAC0F0005B4CC /* Resources */,
|
|
3055
|
+
);
|
|
3056
|
+
buildRules = (
|
|
3057
|
+
);
|
|
3058
|
+
dependencies = (
|
|
3059
|
+
);
|
|
3060
|
+
fileSystemSynchronizedGroups = (
|
|
3061
|
+
B73550692FEBAC0F0005B4CC /* App */,
|
|
3062
|
+
);
|
|
3063
|
+
name = App;
|
|
3064
|
+
packageProductDependencies = (
|
|
3065
|
+
B73550802FEBAC710005B4CC /* LeCodesRuntime */,
|
|
3066
|
+
);
|
|
3067
|
+
productName = App;
|
|
3068
|
+
productReference = B73550672FEBAC0F0005B4CC /* App.app */;
|
|
3069
|
+
productType = "com.apple.product-type.application";
|
|
3070
|
+
};
|
|
3071
|
+
/* End PBXNativeTarget section */
|
|
3072
|
+
|
|
3073
|
+
/* Begin PBXProject section */
|
|
3074
|
+
B735505F2FEBAC0F0005B4CC /* Project object */ = {
|
|
3075
|
+
isa = PBXProject;
|
|
3076
|
+
attributes = {
|
|
3077
|
+
BuildIndependentTargetsInParallel = 1;
|
|
3078
|
+
LastSwiftUpdateCheck = 2650;
|
|
3079
|
+
LastUpgradeCheck = 2650;
|
|
3080
|
+
TargetAttributes = {
|
|
3081
|
+
B73550662FEBAC0F0005B4CC = {
|
|
3082
|
+
CreatedOnToolsVersion = 26.5;
|
|
3083
|
+
};
|
|
3084
|
+
};
|
|
3085
|
+
};
|
|
3086
|
+
buildConfigurationList = B73550622FEBAC0F0005B4CC /* Build configuration list for PBXProject "App" */;
|
|
3087
|
+
developmentRegion = en;
|
|
3088
|
+
hasScannedForEncodings = 0;
|
|
3089
|
+
knownRegions = (
|
|
3090
|
+
en,
|
|
3091
|
+
Base,
|
|
3092
|
+
);
|
|
3093
|
+
mainGroup = B735505E2FEBAC0F0005B4CC;
|
|
3094
|
+
minimizedProjectReferenceProxies = 1;
|
|
3095
|
+
packageReferences = (
|
|
3096
|
+
B73550822FEBAC720005B4CC /* XCLocalSwiftPackageReference "LeCodesRuntime" */,
|
|
3097
|
+
);
|
|
3098
|
+
preferredProjectObjectVersion = 77;
|
|
3099
|
+
productRefGroup = B73550682FEBAC0F0005B4CC /* Products */;
|
|
3100
|
+
projectDirPath = "";
|
|
3101
|
+
projectRoot = "";
|
|
3102
|
+
targets = (
|
|
3103
|
+
B73550662FEBAC0F0005B4CC /* App */,
|
|
3104
|
+
);
|
|
3105
|
+
};
|
|
3106
|
+
/* End PBXProject section */
|
|
3107
|
+
|
|
3108
|
+
/* Begin PBXResourcesBuildPhase section */
|
|
3109
|
+
B73550652FEBAC0F0005B4CC /* Resources */ = {
|
|
3110
|
+
isa = PBXResourcesBuildPhase;
|
|
3111
|
+
buildActionMask = 2147483647;
|
|
3112
|
+
files = (
|
|
3113
|
+
);
|
|
3114
|
+
runOnlyForDeploymentPostprocessing = 0;
|
|
3115
|
+
};
|
|
3116
|
+
/* End PBXResourcesBuildPhase section */
|
|
3117
|
+
|
|
3118
|
+
/* Begin PBXSourcesBuildPhase section */
|
|
3119
|
+
B73550632FEBAC0F0005B4CC /* Sources */ = {
|
|
3120
|
+
isa = PBXSourcesBuildPhase;
|
|
3121
|
+
buildActionMask = 2147483647;
|
|
3122
|
+
files = (
|
|
3123
|
+
);
|
|
3124
|
+
runOnlyForDeploymentPostprocessing = 0;
|
|
3125
|
+
};
|
|
3126
|
+
/* End PBXSourcesBuildPhase section */
|
|
3127
|
+
|
|
3128
|
+
/* Begin XCBuildConfiguration section */
|
|
3129
|
+
B735507B2FEBAC100005B4CC /* Debug */ = {
|
|
3130
|
+
isa = XCBuildConfiguration;
|
|
3131
|
+
buildSettings = {${target}
|
|
3132
|
+
};
|
|
3133
|
+
name = Debug;
|
|
3134
|
+
};
|
|
3135
|
+
B735507C2FEBAC100005B4CC /* Release */ = {
|
|
3136
|
+
isa = XCBuildConfiguration;
|
|
3137
|
+
buildSettings = {${target}
|
|
3138
|
+
};
|
|
3139
|
+
name = Release;
|
|
3140
|
+
};
|
|
3141
|
+
B735507D2FEBAC100005B4CC /* Debug */ = {
|
|
3142
|
+
isa = XCBuildConfiguration;
|
|
3143
|
+
buildSettings = {${project(true)}
|
|
3144
|
+
};
|
|
3145
|
+
name = Debug;
|
|
3146
|
+
};
|
|
3147
|
+
B735507E2FEBAC100005B4CC /* Release */ = {
|
|
3148
|
+
isa = XCBuildConfiguration;
|
|
3149
|
+
buildSettings = {${project(false)}
|
|
3150
|
+
};
|
|
3151
|
+
name = Release;
|
|
3152
|
+
};
|
|
3153
|
+
/* End XCBuildConfiguration section */
|
|
3154
|
+
|
|
3155
|
+
/* Begin XCConfigurationList section */
|
|
3156
|
+
B73550622FEBAC0F0005B4CC /* Build configuration list for PBXProject "App" */ = {
|
|
3157
|
+
isa = XCConfigurationList;
|
|
3158
|
+
buildConfigurations = (
|
|
3159
|
+
B735507D2FEBAC100005B4CC /* Debug */,
|
|
3160
|
+
B735507E2FEBAC100005B4CC /* Release */,
|
|
3161
|
+
);
|
|
3162
|
+
defaultConfigurationIsVisible = 0;
|
|
3163
|
+
defaultConfigurationName = Release;
|
|
3164
|
+
};
|
|
3165
|
+
B735507A2FEBAC100005B4CC /* Build configuration list for PBXNativeTarget "App" */ = {
|
|
3166
|
+
isa = XCConfigurationList;
|
|
3167
|
+
buildConfigurations = (
|
|
3168
|
+
B735507B2FEBAC100005B4CC /* Debug */,
|
|
3169
|
+
B735507C2FEBAC100005B4CC /* Release */,
|
|
3170
|
+
);
|
|
3171
|
+
defaultConfigurationIsVisible = 0;
|
|
3172
|
+
defaultConfigurationName = Release;
|
|
3173
|
+
};
|
|
3174
|
+
/* End XCConfigurationList section */
|
|
3175
|
+
|
|
3176
|
+
/* Begin XCLocalSwiftPackageReference section */
|
|
3177
|
+
B73550822FEBAC720005B4CC /* XCLocalSwiftPackageReference "LeCodesRuntime" */ = {
|
|
3178
|
+
isa = XCLocalSwiftPackageReference;
|
|
3179
|
+
relativePath = LeCodesRuntime;
|
|
3180
|
+
};
|
|
3181
|
+
/* End XCLocalSwiftPackageReference section */
|
|
3182
|
+
|
|
3183
|
+
/* Begin XCSwiftPackageProductDependency section */
|
|
3184
|
+
B73550802FEBAC710005B4CC /* LeCodesRuntime */ = {
|
|
3185
|
+
isa = XCSwiftPackageProductDependency;
|
|
3186
|
+
productName = LeCodesRuntime;
|
|
3187
|
+
};
|
|
3188
|
+
/* End XCSwiftPackageProductDependency section */
|
|
3189
|
+
};
|
|
3190
|
+
rootObject = B735505F2FEBAC0F0005B4CC /* Project object */;
|
|
3191
|
+
}
|
|
3192
|
+
`;
|
|
1957
3193
|
};
|
|
3194
|
+
var SCHEME_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3195
|
+
<Scheme
|
|
3196
|
+
LastUpgradeVersion = "2650"
|
|
3197
|
+
version = "1.7">
|
|
3198
|
+
<BuildAction
|
|
3199
|
+
parallelizeBuildables = "YES"
|
|
3200
|
+
buildImplicitDependencies = "YES">
|
|
3201
|
+
<BuildActionEntries>
|
|
3202
|
+
<BuildActionEntry
|
|
3203
|
+
buildForTesting = "YES"
|
|
3204
|
+
buildForRunning = "YES"
|
|
3205
|
+
buildForProfiling = "YES"
|
|
3206
|
+
buildForArchiving = "YES"
|
|
3207
|
+
buildForAnalyzing = "YES">
|
|
3208
|
+
<BuildableReference
|
|
3209
|
+
BuildableIdentifier = "primary"
|
|
3210
|
+
BlueprintIdentifier = "B73550662FEBAC0F0005B4CC"
|
|
3211
|
+
BuildableName = "App.app"
|
|
3212
|
+
BlueprintName = "App"
|
|
3213
|
+
ReferencedContainer = "container:App.xcodeproj">
|
|
3214
|
+
</BuildableReference>
|
|
3215
|
+
</BuildActionEntry>
|
|
3216
|
+
</BuildActionEntries>
|
|
3217
|
+
</BuildAction>
|
|
3218
|
+
<TestAction
|
|
3219
|
+
buildConfiguration = "Debug"
|
|
3220
|
+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
3221
|
+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
3222
|
+
shouldUseLaunchSchemeArgsEnv = "YES">
|
|
3223
|
+
<Testables>
|
|
3224
|
+
</Testables>
|
|
3225
|
+
</TestAction>
|
|
3226
|
+
<LaunchAction
|
|
3227
|
+
buildConfiguration = "Debug"
|
|
3228
|
+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
3229
|
+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
3230
|
+
launchStyle = "0"
|
|
3231
|
+
useCustomWorkingDirectory = "NO"
|
|
3232
|
+
ignoresPersistentStateOnLaunch = "NO"
|
|
3233
|
+
debugDocumentVersioning = "YES"
|
|
3234
|
+
debugServiceExtension = "internal"
|
|
3235
|
+
allowLocationSimulation = "YES">
|
|
3236
|
+
<BuildableProductRunnable
|
|
3237
|
+
runnableDebuggingMode = "0">
|
|
3238
|
+
<BuildableReference
|
|
3239
|
+
BuildableIdentifier = "primary"
|
|
3240
|
+
BlueprintIdentifier = "B73550662FEBAC0F0005B4CC"
|
|
3241
|
+
BuildableName = "App.app"
|
|
3242
|
+
BlueprintName = "App"
|
|
3243
|
+
ReferencedContainer = "container:App.xcodeproj">
|
|
3244
|
+
</BuildableReference>
|
|
3245
|
+
</BuildableProductRunnable>
|
|
3246
|
+
</LaunchAction>
|
|
3247
|
+
<ProfileAction
|
|
3248
|
+
buildConfiguration = "Release"
|
|
3249
|
+
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
3250
|
+
savedToolIdentifier = ""
|
|
3251
|
+
useCustomWorkingDirectory = "NO"
|
|
3252
|
+
debugDocumentVersioning = "YES">
|
|
3253
|
+
<BuildableProductRunnable
|
|
3254
|
+
runnableDebuggingMode = "0">
|
|
3255
|
+
<BuildableReference
|
|
3256
|
+
BuildableIdentifier = "primary"
|
|
3257
|
+
BlueprintIdentifier = "B73550662FEBAC0F0005B4CC"
|
|
3258
|
+
BuildableName = "App.app"
|
|
3259
|
+
BlueprintName = "App"
|
|
3260
|
+
ReferencedContainer = "container:App.xcodeproj">
|
|
3261
|
+
</BuildableReference>
|
|
3262
|
+
</BuildableProductRunnable>
|
|
3263
|
+
</ProfileAction>
|
|
3264
|
+
<AnalyzeAction
|
|
3265
|
+
buildConfiguration = "Debug">
|
|
3266
|
+
</AnalyzeAction>
|
|
3267
|
+
<ArchiveAction
|
|
3268
|
+
buildConfiguration = "Release"
|
|
3269
|
+
revealArchiveInOrganizer = "YES">
|
|
3270
|
+
</ArchiveAction>
|
|
3271
|
+
</Scheme>
|
|
3272
|
+
`;
|
|
3273
|
+
var APP_DELEGATE = `//
|
|
3274
|
+
// AppDelegate.swift
|
|
3275
|
+
// Generated by lecodes app init — yours to edit.
|
|
3276
|
+
//
|
|
3277
|
+
|
|
3278
|
+
import UIKit
|
|
3279
|
+
|
|
3280
|
+
@main
|
|
3281
|
+
class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
3282
|
+
|
|
3283
|
+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
3284
|
+
return true
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
// MARK: UISceneSession Lifecycle
|
|
3288
|
+
|
|
3289
|
+
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
|
3290
|
+
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
`;
|
|
3294
|
+
var SCENE_DELEGATE = `//
|
|
3295
|
+
// SceneDelegate.swift
|
|
3296
|
+
// Generated by lecodes app init — yours to edit.
|
|
3297
|
+
//
|
|
3298
|
+
|
|
3299
|
+
import UIKit
|
|
3300
|
+
|
|
3301
|
+
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
3302
|
+
|
|
3303
|
+
var window: UIWindow?
|
|
3304
|
+
|
|
3305
|
+
func scene(
|
|
3306
|
+
_ scene: UIScene,
|
|
3307
|
+
willConnectTo session: UISceneSession,
|
|
3308
|
+
options connectionOptions: UIScene.ConnectionOptions
|
|
3309
|
+
) {
|
|
3310
|
+
guard let windowScene = (scene as? UIWindowScene) else { return }
|
|
3311
|
+
|
|
3312
|
+
let window = UIWindow(windowScene: windowScene)
|
|
3313
|
+
window.rootViewController = ViewController()
|
|
3314
|
+
self.window = window
|
|
3315
|
+
window.makeKeyAndVisible()
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
`;
|
|
3319
|
+
var VIEW_CONTROLLER = `//
|
|
3320
|
+
// ViewController.swift
|
|
3321
|
+
// Generated by lecodes app init — yours to edit.
|
|
3322
|
+
//
|
|
3323
|
+
// Runs the bundled app.js (produced by \`lecodes app sync\`) offline: everything the
|
|
3324
|
+
// script needs is provided by the SDK core (_creatorUI / _creatorUtils / console)
|
|
3325
|
+
// plus the plugins registered below via LeCodesRuntime.
|
|
3326
|
+
//
|
|
3327
|
+
|
|
3328
|
+
import UIKit
|
|
3329
|
+
import LeCodesRuntime
|
|
3330
|
+
|
|
3331
|
+
class ViewController: LeCodesViewController {
|
|
3332
|
+
|
|
3333
|
+
override func viewDidLoad() {
|
|
3334
|
+
super.viewDidLoad()
|
|
3335
|
+
|
|
3336
|
+
let lang = Locale.preferredLanguages.first.map { String($0.prefix(2)) } ?? "en"
|
|
3337
|
+
engine.setLanguage(lang)
|
|
3338
|
+
|
|
3339
|
+
// \`// preload:\` assets (images, fonts) resolve out of the app bundle.
|
|
3340
|
+
engine.setFetchLocal { path in
|
|
3341
|
+
let ns = path as NSString
|
|
3342
|
+
let ext = ns.pathExtension
|
|
3343
|
+
let name = ns.deletingPathExtension
|
|
3344
|
+
guard let url = Bundle.main.url(forResource: name, withExtension: ext.isEmpty ? nil : ext) else {
|
|
3345
|
+
return nil
|
|
3346
|
+
}
|
|
3347
|
+
return try? Data(contentsOf: url)
|
|
3348
|
+
}
|
|
1958
3349
|
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
3350
|
+
registerBundledPlugins(in: engine)
|
|
3351
|
+
|
|
3352
|
+
guard let url = Bundle.main.url(forResource: "app", withExtension: "js"),
|
|
3353
|
+
let code = try? String(contentsOf: url, encoding: .utf8) else {
|
|
3354
|
+
print("LeCodes: bundled app.js is missing — run \`lecodes app sync\`")
|
|
3355
|
+
return
|
|
3356
|
+
}
|
|
3357
|
+
engine.run(code)
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
`;
|
|
3361
|
+
var infoPlistTemplate = (pluginKeys) => {
|
|
3362
|
+
const keys = Object.keys(pluginKeys).sort();
|
|
3363
|
+
const pluginBlock = keys.length === 0 ? "" : `
|
|
3364
|
+
<!-- lecodes:plugins — merged from installed plugin manifests by \`lecodes app sync\` -->
|
|
3365
|
+
${keys.map((k) => ` <key>${k}</key>
|
|
3366
|
+
<string>${pluginKeys[k]}</string>`).join(`
|
|
3367
|
+
`)}`;
|
|
3368
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3369
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3370
|
+
<plist version="1.0">
|
|
3371
|
+
<dict>
|
|
3372
|
+
<key>UIApplicationSceneManifest</key>
|
|
3373
|
+
<dict>
|
|
3374
|
+
<key>UIApplicationSupportsMultipleScenes</key>
|
|
3375
|
+
<false/>
|
|
3376
|
+
<key>UISceneConfigurations</key>
|
|
3377
|
+
<dict>
|
|
3378
|
+
<key>UIWindowSceneSessionRoleApplication</key>
|
|
3379
|
+
<array>
|
|
3380
|
+
<dict>
|
|
3381
|
+
<key>UISceneConfigurationName</key>
|
|
3382
|
+
<string>Default Configuration</string>
|
|
3383
|
+
<key>UISceneDelegateClassName</key>
|
|
3384
|
+
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
|
3385
|
+
</dict>
|
|
3386
|
+
</array>
|
|
3387
|
+
</dict>
|
|
3388
|
+
</dict>${pluginBlock}
|
|
3389
|
+
</dict>
|
|
3390
|
+
</plist>
|
|
3391
|
+
`;
|
|
1968
3392
|
};
|
|
1969
|
-
var
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
3393
|
+
var LAUNCH_STORYBOARD = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
3394
|
+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
|
3395
|
+
<dependencies>
|
|
3396
|
+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
|
3397
|
+
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
3398
|
+
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
3399
|
+
</dependencies>
|
|
3400
|
+
<scenes>
|
|
3401
|
+
<!--View Controller-->
|
|
3402
|
+
<scene sceneID="EHf-IW-A2E">
|
|
3403
|
+
<objects>
|
|
3404
|
+
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
|
3405
|
+
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
|
3406
|
+
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
3407
|
+
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
3408
|
+
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
|
3409
|
+
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
|
3410
|
+
</view>
|
|
3411
|
+
</viewController>
|
|
3412
|
+
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
|
3413
|
+
</objects>
|
|
3414
|
+
<point key="canvasLocation" x="53" y="375"/>
|
|
3415
|
+
</scene>
|
|
3416
|
+
</scenes>
|
|
3417
|
+
</document>
|
|
3418
|
+
`;
|
|
3419
|
+
var ASSETS_ROOT_JSON = `{
|
|
3420
|
+
"info" : {
|
|
3421
|
+
"author" : "xcode",
|
|
3422
|
+
"version" : 1
|
|
1980
3423
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
3424
|
+
}
|
|
3425
|
+
`;
|
|
3426
|
+
var ACCENT_COLOR_JSON = `{
|
|
3427
|
+
"colors" : [
|
|
3428
|
+
{
|
|
3429
|
+
"idiom" : "universal"
|
|
3430
|
+
}
|
|
3431
|
+
],
|
|
3432
|
+
"info" : {
|
|
3433
|
+
"author" : "xcode",
|
|
3434
|
+
"version" : 1
|
|
1989
3435
|
}
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
3436
|
+
}
|
|
3437
|
+
`;
|
|
3438
|
+
var APP_ICON_JSON = `{
|
|
3439
|
+
"images" : [
|
|
3440
|
+
{
|
|
3441
|
+
"idiom" : "universal",
|
|
3442
|
+
"platform" : "ios",
|
|
3443
|
+
"size" : "1024x1024"
|
|
3444
|
+
},
|
|
3445
|
+
{
|
|
3446
|
+
"appearances" : [
|
|
3447
|
+
{
|
|
3448
|
+
"appearance" : "luminosity",
|
|
3449
|
+
"value" : "dark"
|
|
3450
|
+
}
|
|
3451
|
+
],
|
|
3452
|
+
"idiom" : "universal",
|
|
3453
|
+
"platform" : "ios",
|
|
3454
|
+
"size" : "1024x1024"
|
|
3455
|
+
},
|
|
3456
|
+
{
|
|
3457
|
+
"appearances" : [
|
|
3458
|
+
{
|
|
3459
|
+
"appearance" : "luminosity",
|
|
3460
|
+
"value" : "tinted"
|
|
3461
|
+
}
|
|
3462
|
+
],
|
|
3463
|
+
"idiom" : "universal",
|
|
3464
|
+
"platform" : "ios",
|
|
3465
|
+
"size" : "1024x1024"
|
|
3466
|
+
}
|
|
3467
|
+
],
|
|
3468
|
+
"info" : {
|
|
3469
|
+
"author" : "xcode",
|
|
3470
|
+
"version" : 1
|
|
3471
|
+
}
|
|
3472
|
+
}
|
|
3473
|
+
`;
|
|
3474
|
+
var runtimePackageTemplate = (sdkVersion, variant, plugins) => {
|
|
3475
|
+
const product = `LeCodesSDK-${variant}`;
|
|
3476
|
+
const pluginTargets = plugins.map((p) => `
|
|
3477
|
+
// plugin: ${p.id} (vendored — see plugins.lock.json)
|
|
3478
|
+
.target(
|
|
3479
|
+
name: "${p.module}",
|
|
3480
|
+
dependencies: [.product(name: "${product}", package: "lecodes-ios-sdk")],
|
|
3481
|
+
path: "Sources/${p.module}"
|
|
3482
|
+
),`).join("");
|
|
3483
|
+
const runtimeDeps = [`.product(name: "${product}", package: "lecodes-ios-sdk")`, ...plugins.map((p) => `"${p.module}"`)];
|
|
3484
|
+
return `// swift-tools-version:5.9
|
|
3485
|
+
//
|
|
3486
|
+
// GENERATED by lecodes — regenerated by \`lecodes app sync\`; do not edit.
|
|
3487
|
+
// Owns the app's native dependency graph: the LeCodesSDK variant (auto-picked from what
|
|
3488
|
+
// the compiled bundle uses, or forced via app.json ios.variant) and one vendored target
|
|
3489
|
+
// per installed plugin (see plugins.lock.json).
|
|
3490
|
+
|
|
3491
|
+
import PackageDescription
|
|
3492
|
+
|
|
3493
|
+
let package = Package(
|
|
3494
|
+
name: "LeCodesRuntime",
|
|
3495
|
+
platforms: [.iOS(.v15)],
|
|
3496
|
+
products: [
|
|
3497
|
+
.library(name: "LeCodesRuntime", targets: ["LeCodesRuntime"]),
|
|
3498
|
+
],
|
|
3499
|
+
dependencies: [
|
|
3500
|
+
.package(url: "https://github.com/letary/lecodes-ios-sdk.git", from: "${sdkVersion}"),
|
|
3501
|
+
],
|
|
3502
|
+
targets: [
|
|
3503
|
+
.target(
|
|
3504
|
+
name: "LeCodesRuntime",
|
|
3505
|
+
dependencies: [
|
|
3506
|
+
${runtimeDeps.join(`,
|
|
3507
|
+
`)},
|
|
3508
|
+
],
|
|
3509
|
+
path: "Sources/LeCodesRuntime"
|
|
3510
|
+
),${pluginTargets}
|
|
3511
|
+
]
|
|
3512
|
+
)
|
|
3513
|
+
`;
|
|
3514
|
+
};
|
|
3515
|
+
var runtimeSwiftTemplate = (plugins) => {
|
|
3516
|
+
const imports = plugins.map((p) => `import ${p.module}
|
|
3517
|
+
`).join("");
|
|
3518
|
+
const calls = plugins.length === 0 ? `
|
|
3519
|
+
` : plugins.map((p) => ` ${p.register}.register(in: engine)
|
|
3520
|
+
`).join("");
|
|
3521
|
+
return `//
|
|
3522
|
+
// LeCodesRuntime.swift
|
|
3523
|
+
// GENERATED by lecodes — regenerated by \`lecodes app sync\`; do not edit.
|
|
3524
|
+
//
|
|
3525
|
+
// Re-exports the SDK (so app code needs only \`import LeCodesRuntime\`) and registers
|
|
3526
|
+
// every plugin installed in app.json.
|
|
3527
|
+
//
|
|
3528
|
+
|
|
3529
|
+
@_exported import LeCodesSDK
|
|
3530
|
+
${imports}
|
|
3531
|
+
public func registerBundledPlugins(in engine: LeCodesEngine) {
|
|
3532
|
+
${calls}}
|
|
3533
|
+
`;
|
|
1995
3534
|
};
|
|
3535
|
+
var IOS_README = `# ios/ — native Xcode shell
|
|
1996
3536
|
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
3537
|
+
Generated by \`lecodes app init ios\` for the LeCodes project one folder up. The app runs the
|
|
3538
|
+
compiled bundle in \`App/Resources/app.js\` — fully offline, nothing fetched at launch. This
|
|
3539
|
+
folder is ignored by the platform (\`ios/\` in \`.lecodesignore\`) and is never pushed to LeCodes.
|
|
2000
3540
|
|
|
2001
|
-
|
|
2002
|
-
import { readFileSync as readFileSync6 } from "node:fs";
|
|
2003
|
-
import { basename } from "node:path";
|
|
3541
|
+
## Daily workflow
|
|
2004
3542
|
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
"
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
3543
|
+
Edit the app's code at the project root (\`main.ts\` …), then:
|
|
3544
|
+
|
|
3545
|
+
\`\`\`
|
|
3546
|
+
lecodes app sync # recompile app.js + assets into the shell, vendor plugins
|
|
3547
|
+
lecodes app open # open App.xcodeproj in Xcode — build & run from there
|
|
3548
|
+
\`\`\`
|
|
3549
|
+
|
|
3550
|
+
\`lecodes app doctor\` preflights builds; \`lecodes app build ios\` archives and exports an
|
|
3551
|
+
.ipa; \`lecodes app upload ios\` sends the archive to App Store Connect.
|
|
3552
|
+
|
|
3553
|
+
## Who owns what
|
|
3554
|
+
|
|
3555
|
+
Yours — \`lecodes app sync\` never touches these:
|
|
3556
|
+
|
|
3557
|
+
- \`App/*.swift\` (AppDelegate, SceneDelegate, ViewController) — customize freely, add files.
|
|
3558
|
+
App-local native code goes here too: add a Swift file that calls
|
|
3559
|
+
\`engine.registerCallback\` / \`registerService\`, and invoke it from ViewController.
|
|
3560
|
+
- \`App/Assets.xcassets/\` — drop a 1024×1024 PNG into AppIcon.appiconset/ for the app icon.
|
|
3561
|
+
- \`App/Base.lproj/LaunchScreen.storyboard\`
|
|
3562
|
+
|
|
3563
|
+
CLI-owned — regenerated on every sync, hand edits are overwritten:
|
|
3564
|
+
|
|
3565
|
+
- \`App/Resources/\` — the compiled app.js + every asset it references (fonts included), so
|
|
3566
|
+
the app runs fully offline. Globs in app.json \`"resources": {"remote": [...]}\` opt heavy
|
|
3567
|
+
assets (videos, big models) back into streaming from the server instead.
|
|
3568
|
+
- \`App/Info.plist\` — template + the installed plugins' usage keys
|
|
3569
|
+
- \`LeCodesRuntime/\` — local Swift package: the LeCodesSDK dependency, vendored plugin
|
|
3570
|
+
sources (Sources/<Module>/), and the registerBundledPlugins entry point
|
|
3571
|
+
- the version values inside \`App.xcodeproj/project.pbxproj\` (from app.json)
|
|
3572
|
+
- this README
|
|
3573
|
+
|
|
3574
|
+
## SDK variant
|
|
3575
|
+
|
|
3576
|
+
Sync links the smallest SDK product that covers what the compiled bundle actually uses:
|
|
3577
|
+
\`core\` (UI only), \`2d\` (+ 2D engine with physics), \`3d\` (+ 3D/AR engine with physics,
|
|
3578
|
+
~17 MB heavier), or \`full\` (everything). Adding your first 3D scene or 2D game to the
|
|
3579
|
+
project switches it automatically on the next sync; set \`ios.variant\` in \`../app.json\`
|
|
3580
|
+
to pin one instead.
|
|
3581
|
+
|
|
3582
|
+
## Config
|
|
3583
|
+
|
|
3584
|
+
Identity lives in \`../app.json\` (name, version, plugins, ios.bundleId / teamId / variant /
|
|
3585
|
+
buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are managed with
|
|
3586
|
+
\`lecodes app plugin add <id | github:owner/repo[@tag]>\` and pinned in \`../plugins.lock.json\`.
|
|
3587
|
+
`;
|
|
3588
|
+
|
|
3589
|
+
// src/commands/app.ts
|
|
3590
|
+
var SDK_PACKAGE_VERSION = "1.1.0";
|
|
3591
|
+
var SDK_VARIANTS = ["core", "2d", "3d", "full"];
|
|
3592
|
+
var FIRST_PARTY_PLUGINS = {
|
|
3593
|
+
camera: "letary/lecodes-plugins",
|
|
3594
|
+
"qr-scanner": "letary/lecodes-plugins"
|
|
2034
3595
|
};
|
|
2035
|
-
var
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
3596
|
+
var readJson = (path) => {
|
|
3597
|
+
if (!existsSync8(path))
|
|
3598
|
+
return null;
|
|
3599
|
+
try {
|
|
3600
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
3601
|
+
} catch {
|
|
3602
|
+
throw new CliError(`Invalid JSON in ${path}`);
|
|
3603
|
+
}
|
|
2042
3604
|
};
|
|
2043
|
-
var
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
return
|
|
2058
|
-
}
|
|
2059
|
-
const
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
|
|
3605
|
+
var writeJson = (path, value) => writeFileSync7(path, JSON.stringify(value, null, 2) + `
|
|
3606
|
+
`);
|
|
3607
|
+
var projectRoot = () => {
|
|
3608
|
+
try {
|
|
3609
|
+
return findProjectRoot(process.cwd());
|
|
3610
|
+
} catch {
|
|
3611
|
+
return process.cwd();
|
|
3612
|
+
}
|
|
3613
|
+
};
|
|
3614
|
+
var git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
3615
|
+
var ensureRepo = (repo, wantTag, lock) => {
|
|
3616
|
+
const override = process.env.LECODES_PLUGINS_DIR;
|
|
3617
|
+
if (override) {
|
|
3618
|
+
note(`Using local plugin checkout ${override} (LECODES_PLUGINS_DIR)`);
|
|
3619
|
+
return { dir: resolve2(override), tag: "local", commit: "local" };
|
|
3620
|
+
}
|
|
3621
|
+
const cache = join11(homedir2(), ".lecodes", "plugins", repo.replace("/", "__"));
|
|
3622
|
+
const url = `https://github.com/${repo}.git`;
|
|
3623
|
+
if (!existsSync8(join11(cache, ".git"))) {
|
|
3624
|
+
mkdirSync5(dirname7(cache), { recursive: true });
|
|
3625
|
+
note(`Cloning ${url}…`);
|
|
3626
|
+
git(dirname7(cache), "clone", "--quiet", url, cache);
|
|
3627
|
+
}
|
|
3628
|
+
let tag = lock?.tag ?? wantTag;
|
|
3629
|
+
let commit = lock?.commit;
|
|
3630
|
+
if (commit) {
|
|
3631
|
+
try {
|
|
3632
|
+
git(cache, "cat-file", "-e", `${commit}^{commit}`);
|
|
3633
|
+
} catch {
|
|
3634
|
+
git(cache, "fetch", "--quiet", "--tags", "origin");
|
|
3635
|
+
}
|
|
3636
|
+
} else {
|
|
3637
|
+
git(cache, "fetch", "--quiet", "--tags", "origin");
|
|
3638
|
+
if (!tag) {
|
|
3639
|
+
tag = git(cache, "tag", "--list", "v*", "--sort=-version:refname").split(`
|
|
3640
|
+
`)[0] ?? "";
|
|
3641
|
+
if (!tag)
|
|
3642
|
+
throw new CliError(`${repo} has no v* version tags to install from`);
|
|
3643
|
+
}
|
|
3644
|
+
commit = git(cache, "rev-list", "-n", "1", tag);
|
|
3645
|
+
}
|
|
3646
|
+
git(cache, "checkout", "--quiet", "--detach", commit);
|
|
3647
|
+
return { dir: cache, tag: tag ?? "unknown", commit };
|
|
3648
|
+
};
|
|
3649
|
+
var resolvePlugin = (id, spec, lock) => {
|
|
3650
|
+
let repo;
|
|
3651
|
+
let subdir;
|
|
3652
|
+
let wantTag = null;
|
|
3653
|
+
if (spec.startsWith("github:")) {
|
|
3654
|
+
const [repoPart, tagPart] = spec.slice("github:".length).split("@");
|
|
3655
|
+
repo = repoPart;
|
|
3656
|
+
wantTag = tagPart ?? null;
|
|
3657
|
+
subdir = "";
|
|
3658
|
+
} else {
|
|
3659
|
+
repo = FIRST_PARTY_PLUGINS[id];
|
|
3660
|
+
if (!repo) {
|
|
3661
|
+
throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use a github:owner/repo spec.`);
|
|
3662
|
+
}
|
|
3663
|
+
subdir = `plugins/${id}`;
|
|
3664
|
+
}
|
|
3665
|
+
const checkout = ensureRepo(repo, wantTag, lock?.repo === repo ? lock : undefined);
|
|
3666
|
+
const dir = subdir ? join11(checkout.dir, subdir) : checkout.dir;
|
|
3667
|
+
const manifest = readJson(join11(dir, "lecodes-plugin.json"));
|
|
3668
|
+
if (!manifest)
|
|
3669
|
+
throw new CliError(`Plugin "${id}": no lecodes-plugin.json in ${dir}`);
|
|
3670
|
+
if (manifest.id !== id)
|
|
3671
|
+
throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`);
|
|
3672
|
+
if (!manifest.ios)
|
|
3673
|
+
throw new CliError(`Plugin "${id}" does not support iOS (manifest "ios" is null)`);
|
|
3674
|
+
if (spec.startsWith("github:")) {
|
|
3675
|
+
const names = [...manifest.provides?.views ?? [], ...manifest.provides?.services ?? []];
|
|
3676
|
+
for (const name of names) {
|
|
3677
|
+
if (!name.includes("."))
|
|
3678
|
+
warn(`Plugin "${id}" provides bare channel name "${name}" — third-party names should be vendor-prefixed (e.g. "acme.${name}").`);
|
|
2083
3679
|
}
|
|
3680
|
+
}
|
|
3681
|
+
return {
|
|
3682
|
+
id,
|
|
3683
|
+
dir,
|
|
3684
|
+
manifest,
|
|
3685
|
+
lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" }
|
|
2084
3686
|
};
|
|
2085
|
-
walk(root, false);
|
|
2086
|
-
return { files, dirs, ignored };
|
|
2087
3687
|
};
|
|
2088
|
-
var
|
|
2089
|
-
var
|
|
2090
|
-
const
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
}
|
|
2100
|
-
const
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
3688
|
+
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app";
|
|
3689
|
+
var appInit = async (args) => {
|
|
3690
|
+
const platform = args._[1];
|
|
3691
|
+
if (platform !== "ios") {
|
|
3692
|
+
throw new CliError(platform ? `Unsupported platform "${platform}" — only "ios" for now.` : "Usage: lecodes app init ios [--name <display name>] [--bundle-id <id>] [--team <team id>] [--variant auto|core|2d|3d|full]");
|
|
3693
|
+
}
|
|
3694
|
+
const root = projectRoot();
|
|
3695
|
+
const iosDir = join11(root, "ios");
|
|
3696
|
+
if (existsSync8(iosDir)) {
|
|
3697
|
+
throw new CliError("ios/ already exists — run `lecodes app sync` to update it.");
|
|
3698
|
+
}
|
|
3699
|
+
const existing = readJson(join11(root, "app.json")) ?? {};
|
|
3700
|
+
const variant = flagStr(args, "variant") ?? existing.ios?.variant ?? "auto";
|
|
3701
|
+
if (variant !== "auto" && !SDK_VARIANTS.includes(variant)) {
|
|
3702
|
+
throw new CliError(`--variant must be auto, ${SDK_VARIANTS.join(", ")} (got "${variant}")`);
|
|
3703
|
+
}
|
|
3704
|
+
const name = flagStr(args, "name") ?? existing.name ?? basename4(root);
|
|
3705
|
+
const config = {
|
|
3706
|
+
...existing,
|
|
3707
|
+
name,
|
|
3708
|
+
version: existing.version ?? "1.0.0",
|
|
3709
|
+
plugins: existing.plugins ?? {},
|
|
3710
|
+
ios: {
|
|
3711
|
+
bundleId: flagStr(args, "bundle-id") ?? existing.ios?.bundleId ?? `com.example.${slugify(name)}`,
|
|
3712
|
+
teamId: flagStr(args, "team") ?? existing.ios?.teamId ?? "",
|
|
3713
|
+
variant,
|
|
3714
|
+
buildNumber: existing.ios?.buildNumber ?? 1
|
|
2108
3715
|
}
|
|
3716
|
+
};
|
|
3717
|
+
writeJson(join11(root, "app.json"), config);
|
|
3718
|
+
log(`${c.green("written")} app.json`);
|
|
3719
|
+
const ignorePath = join11(root, ".lecodesignore");
|
|
3720
|
+
const ignore = existsSync8(ignorePath) ? readFileSync10(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
|
|
3721
|
+
if (!/^ios\/$/m.test(ignore)) {
|
|
3722
|
+
writeFileSync7(ignorePath, ignore.replace(/\n*$/, `
|
|
3723
|
+
`) + `
|
|
3724
|
+
# Native app shells — generated by \`lecodes app init\`, never pushed to LeCodes.
|
|
3725
|
+
ios/
|
|
3726
|
+
`);
|
|
3727
|
+
log(`${c.green("updated")} .lecodesignore (+ ios/)`);
|
|
3728
|
+
}
|
|
3729
|
+
const identity = {
|
|
3730
|
+
displayName: name,
|
|
3731
|
+
bundleId: config.ios.bundleId,
|
|
3732
|
+
teamId: config.ios.teamId,
|
|
3733
|
+
version: config.version,
|
|
3734
|
+
buildNumber: config.ios.buildNumber
|
|
3735
|
+
};
|
|
3736
|
+
const write = (rel, content) => {
|
|
3737
|
+
const abs = join11(iosDir, rel);
|
|
3738
|
+
mkdirSync5(dirname7(abs), { recursive: true });
|
|
3739
|
+
writeFileSync7(abs, content);
|
|
3740
|
+
log(`${c.green("created")} ios/${rel}`);
|
|
3741
|
+
};
|
|
3742
|
+
write("README.md", IOS_README);
|
|
3743
|
+
write("App.xcodeproj/project.pbxproj", pbxprojTemplate(identity));
|
|
3744
|
+
write("App.xcodeproj/xcshareddata/xcschemes/App.xcscheme", SCHEME_XML);
|
|
3745
|
+
write("App/AppDelegate.swift", APP_DELEGATE);
|
|
3746
|
+
write("App/SceneDelegate.swift", SCENE_DELEGATE);
|
|
3747
|
+
write("App/ViewController.swift", VIEW_CONTROLLER);
|
|
3748
|
+
write("App/Info.plist", infoPlistTemplate({}));
|
|
3749
|
+
write("App/Base.lproj/LaunchScreen.storyboard", LAUNCH_STORYBOARD);
|
|
3750
|
+
write("App/Assets.xcassets/Contents.json", ASSETS_ROOT_JSON);
|
|
3751
|
+
write("App/Assets.xcassets/AccentColor.colorset/Contents.json", ACCENT_COLOR_JSON);
|
|
3752
|
+
write("App/Assets.xcassets/AppIcon.appiconset/Contents.json", APP_ICON_JSON);
|
|
3753
|
+
write("LeCodesRuntime/Package.swift", runtimePackageTemplate(SDK_PACKAGE_VERSION, variant === "auto" ? "core" : variant, []));
|
|
3754
|
+
write("LeCodesRuntime/Sources/LeCodesRuntime/LeCodesRuntime.swift", runtimeSwiftTemplate([]));
|
|
3755
|
+
log("");
|
|
3756
|
+
success(`iOS shell for ${c.bold(name)} ready.`);
|
|
3757
|
+
log(` ${c.bold("lecodes app sync")} embed the compiled bundle (+ plugins) into the shell`);
|
|
3758
|
+
log(` ${c.bold("open ios/App.xcodeproj")} then run on your device from Xcode`);
|
|
3759
|
+
log(` App icon: drop a 1024×1024 PNG into ios/App/Assets.xcassets/AppIcon.appiconset/`);
|
|
3760
|
+
};
|
|
3761
|
+
var detectEngines = (bundleJs) => {
|
|
3762
|
+
const lines = [];
|
|
3763
|
+
for (const line of bundleJs.split(`
|
|
3764
|
+
`)) {
|
|
3765
|
+
if (!line.startsWith("//"))
|
|
3766
|
+
break;
|
|
3767
|
+
lines.push(line);
|
|
3768
|
+
}
|
|
3769
|
+
const header = lines.join(`
|
|
3770
|
+
`);
|
|
3771
|
+
return { gl: !/^\/\/ gl: disable$/m.test(header), g2: /^\/\/ 2d: enable$/m.test(header) };
|
|
3772
|
+
};
|
|
3773
|
+
var resolveVariant = (requested, bundleJs) => {
|
|
3774
|
+
const uses = detectEngines(bundleJs);
|
|
3775
|
+
const detected = uses.gl && uses.g2 ? "full" : uses.gl ? "3d" : uses.g2 ? "2d" : "core";
|
|
3776
|
+
if (requested === "auto")
|
|
3777
|
+
return { variant: detected, detected };
|
|
3778
|
+
const variant = requested;
|
|
3779
|
+
const missing = [
|
|
3780
|
+
uses.gl && variant !== "3d" && variant !== "full" ? "3D" : null,
|
|
3781
|
+
uses.g2 && variant !== "2d" && variant !== "full" ? "2D" : null
|
|
3782
|
+
].filter(Boolean);
|
|
3783
|
+
if (missing.length > 0) {
|
|
3784
|
+
warn(`project uses the ${missing.join(" + ")} engine but ios.variant forces "${variant}" — those scenes won't run on device; remove ios.variant to auto-select "${detected}".`);
|
|
2109
3785
|
}
|
|
2110
|
-
return {
|
|
3786
|
+
return { variant, detected };
|
|
2111
3787
|
};
|
|
2112
|
-
var
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
3788
|
+
var ensureSdkResourcesBundle = async () => {
|
|
3789
|
+
const override = process.env.LECODES_SDK_RESOURCES;
|
|
3790
|
+
if (override) {
|
|
3791
|
+
note(`Using local resources bundle ${override} (LECODES_SDK_RESOURCES)`);
|
|
3792
|
+
return resolve2(override);
|
|
3793
|
+
}
|
|
3794
|
+
const dir = join11(homedir2(), ".lecodes", "sdk-resources", SDK_PACKAGE_VERSION);
|
|
3795
|
+
const bundle = join11(dir, "LeCodesSDKResources.bundle");
|
|
3796
|
+
if (existsSync8(bundle))
|
|
3797
|
+
return bundle;
|
|
3798
|
+
const url = `https://github.com/letary/lecodes-ios-sdk/releases/download/${SDK_PACKAGE_VERSION}/LeCodesSDKResources.bundle.zip`;
|
|
3799
|
+
note(`Downloading LeCodesSDKResources.bundle (SDK ${SDK_PACKAGE_VERSION})…`);
|
|
3800
|
+
const res = await fetch(url);
|
|
3801
|
+
if (!res.ok)
|
|
3802
|
+
throw new CliError(`Resources bundle download failed (HTTP ${res.status}) — ${url}`);
|
|
3803
|
+
mkdirSync5(dir, { recursive: true });
|
|
3804
|
+
const zip = join11(dir, "LeCodesSDKResources.bundle.zip");
|
|
3805
|
+
writeFileSync7(zip, new Uint8Array(await res.arrayBuffer()));
|
|
3806
|
+
execFileSync("ditto", ["-x", "-k", zip, dir]);
|
|
3807
|
+
rmSync(zip);
|
|
3808
|
+
if (!existsSync8(bundle))
|
|
3809
|
+
throw new CliError(`No LeCodesSDKResources.bundle inside the release asset ${url}`);
|
|
3810
|
+
return bundle;
|
|
3811
|
+
};
|
|
3812
|
+
var appSync = async (_args) => {
|
|
3813
|
+
const root = projectRoot();
|
|
3814
|
+
const config = readJson(join11(root, "app.json"));
|
|
3815
|
+
const iosDir = join11(root, "ios");
|
|
3816
|
+
if (!config?.ios || !existsSync8(iosDir)) {
|
|
3817
|
+
throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
|
|
3818
|
+
}
|
|
3819
|
+
const requestedVariant = config.ios.variant ?? "auto";
|
|
3820
|
+
if (requestedVariant !== "auto" && !SDK_VARIANTS.includes(requestedVariant)) {
|
|
3821
|
+
throw new CliError(`app.json ios.variant must be "auto" or one of ${SDK_VARIANTS.join(", ")} (got "${requestedVariant}").`);
|
|
3822
|
+
}
|
|
3823
|
+
const lockPath = join11(root, "plugins.lock.json");
|
|
3824
|
+
const oldLock = readJson(lockPath)?.plugins ?? {};
|
|
3825
|
+
const pluginSpecs = Object.entries(config.plugins ?? {});
|
|
3826
|
+
const runtimePlugins = [];
|
|
3827
|
+
const infoValues = {};
|
|
3828
|
+
const newLock = {};
|
|
3829
|
+
const sourcesDir = join11(iosDir, "LeCodesRuntime", "Sources");
|
|
3830
|
+
for (const [id, spec] of pluginSpecs) {
|
|
3831
|
+
const plugin = resolvePlugin(id, spec, oldLock[id]);
|
|
3832
|
+
const ios = plugin.manifest.ios;
|
|
3833
|
+
const module = ios.module;
|
|
3834
|
+
const register = ios.register;
|
|
3835
|
+
if (!module || !register)
|
|
3836
|
+
throw new CliError(`Plugin "${id}": manifest ios.module / ios.register missing`);
|
|
3837
|
+
const src = join11(plugin.dir, ios.sources ?? "ios");
|
|
3838
|
+
if (!existsSync8(src))
|
|
3839
|
+
throw new CliError(`Plugin "${id}": sources dir ${src} missing`);
|
|
3840
|
+
const dest = join11(sourcesDir, module);
|
|
3841
|
+
rmSync(dest, { recursive: true, force: true });
|
|
3842
|
+
cpSync(src, dest, { recursive: true });
|
|
3843
|
+
for (const [key, value] of Object.entries(ios.infoPlist ?? {})) {
|
|
3844
|
+
(infoValues[key] ??= []).push(value);
|
|
3845
|
+
}
|
|
3846
|
+
runtimePlugins.push({ id, module, register });
|
|
3847
|
+
newLock[id] = plugin.lock;
|
|
3848
|
+
log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`);
|
|
3849
|
+
}
|
|
3850
|
+
const keep = new Set(["LeCodesRuntime", ...runtimePlugins.map((p) => p.module)]);
|
|
3851
|
+
for (const entry of existsSync8(sourcesDir) ? readdirSync4(sourcesDir) : []) {
|
|
3852
|
+
if (!keep.has(entry) && statSync2(join11(sourcesDir, entry)).isDirectory()) {
|
|
3853
|
+
rmSync(join11(sourcesDir, entry), { recursive: true });
|
|
3854
|
+
log(`${c.yellow("removed")} ios/LeCodesRuntime/Sources/${entry} (plugin uninstalled)`);
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
const remotePatterns = config.resources?.remote ?? [];
|
|
3858
|
+
if (!Array.isArray(remotePatterns) || remotePatterns.some((p) => typeof p !== "string")) {
|
|
3859
|
+
throw new CliError('app.json "resources.remote" must be an array of glob strings (e.g. ["videos/**"]).');
|
|
3860
|
+
}
|
|
3861
|
+
const resourcesDir = join11(iosDir, "App", "Resources");
|
|
3862
|
+
rmSync(resourcesDir, { recursive: true, force: true });
|
|
3863
|
+
mkdirSync5(resourcesDir, { recursive: true });
|
|
3864
|
+
const resources = await compileShell({ _: [], flags: { o: join11(resourcesDir, "app.js") } }, { remotePatterns });
|
|
3865
|
+
const bundleJs = readFileSync10(join11(resourcesDir, "app.js"), "utf8");
|
|
3866
|
+
const { variant, detected } = resolveVariant(requestedVariant, bundleJs);
|
|
3867
|
+
const WHY = {
|
|
3868
|
+
core: "UI only",
|
|
3869
|
+
"2d": "uses the 2D engine",
|
|
3870
|
+
"3d": "uses the 3D engine",
|
|
3871
|
+
full: "uses the 3D + 2D engines"
|
|
3872
|
+
};
|
|
3873
|
+
log(`${c.green("variant")} ${variant}${requestedVariant === "auto" ? ` (auto: ${WHY[detected]})` : " (forced by ios.variant)"}`);
|
|
3874
|
+
const sdkProvided = new Set;
|
|
3875
|
+
if (variant === "3d" || variant === "full") {
|
|
3876
|
+
const sdkResources = await ensureSdkResourcesBundle();
|
|
3877
|
+
cpSync(sdkResources, join11(resourcesDir, "LeCodesSDKResources.bundle"), { recursive: true });
|
|
3878
|
+
for (const f2 of readdirSync4(sdkResources))
|
|
3879
|
+
if (f2 !== "Info.plist")
|
|
3880
|
+
sdkProvided.add(f2);
|
|
3881
|
+
log(`${c.green("resources")} LeCodesSDKResources.bundle (filament) → ios/App/Resources/`);
|
|
3882
|
+
}
|
|
3883
|
+
const preload = /^\/\/ preload: (.*)$/m.exec(bundleJs)?.[1].split(";").map((s) => s.trim()).filter(Boolean) ?? [];
|
|
3884
|
+
const IMPLICIT_PRELOADS = new Set(["uberarchive", "capture"]);
|
|
3885
|
+
const byName = new Map;
|
|
3886
|
+
for (const r of resources) {
|
|
3887
|
+
if (r.remote || !r.absPath)
|
|
2120
3888
|
continue;
|
|
2121
|
-
|
|
3889
|
+
const list = byName.get(r.name) ?? [];
|
|
3890
|
+
list.push(r);
|
|
3891
|
+
byName.set(r.name, list);
|
|
2122
3892
|
}
|
|
2123
|
-
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
if (!
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
} else if (type === "shader") {
|
|
2133
|
-
const shaders = serverByPath.get(file.path)?.shaders;
|
|
2134
|
-
if (!shaders?.length)
|
|
2135
|
-
warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
|
|
2136
|
-
entries.push({ path: file.path, type, shaders });
|
|
2137
|
-
} else {
|
|
2138
|
-
entries.push({ path: file.path, type, text: readFileSync6(file.absPath, "utf8") });
|
|
3893
|
+
let embedded = 0;
|
|
3894
|
+
for (const name of preload) {
|
|
3895
|
+
if (IMPLICIT_PRELOADS.has(name))
|
|
3896
|
+
continue;
|
|
3897
|
+
const candidates = byName.get(name) ?? [];
|
|
3898
|
+
if (candidates.length === 0) {
|
|
3899
|
+
if (!sdkProvided.has(name))
|
|
3900
|
+
warn(`bundle needs local asset "${name}" but no project file provides it — it won't load on device`);
|
|
3901
|
+
continue;
|
|
2139
3902
|
}
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
3903
|
+
if (candidates.length > 1)
|
|
3904
|
+
warn(`asset name collision: ${name} (${candidates.map((r) => r.path).join(", ")}) — embedding ${candidates[0].path}`);
|
|
3905
|
+
cpSync(candidates[0].absPath, join11(resourcesDir, name));
|
|
3906
|
+
embedded++;
|
|
3907
|
+
}
|
|
3908
|
+
if (embedded > 0)
|
|
3909
|
+
log(`${c.green("assets")} ${embedded} file(s) embedded → ios/App/Resources/`);
|
|
3910
|
+
const streamed = resources.filter((r) => r.remote);
|
|
3911
|
+
if (streamed.length > 0)
|
|
3912
|
+
log(`${c.green("remote")} ${streamed.length} resource(s) stay on the server (resources.remote): ${streamed.map((r) => r.path).join(", ")}`);
|
|
3913
|
+
writeFileSync7(join11(iosDir, "README.md"), IOS_README);
|
|
3914
|
+
writeFileSync7(join11(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(SDK_PACKAGE_VERSION, variant, runtimePlugins));
|
|
3915
|
+
writeFileSync7(join11(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins));
|
|
3916
|
+
const mergedKeys = {};
|
|
3917
|
+
for (const [key, values] of Object.entries(infoValues))
|
|
3918
|
+
mergedKeys[key] = [...new Set(values)].join(" ");
|
|
3919
|
+
writeFileSync7(join11(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys));
|
|
3920
|
+
if (pluginSpecs.length > 0 || existsSync8(lockPath))
|
|
3921
|
+
writeJson(lockPath, { plugins: newLock });
|
|
3922
|
+
const pbxPath = join11(iosDir, "App.xcodeproj", "project.pbxproj");
|
|
3923
|
+
const pbx = readFileSync10(pbxPath, "utf8").replace(/MARKETING_VERSION = [^;]+;/g, `MARKETING_VERSION = ${config.version ?? "1.0.0"};`).replace(/CURRENT_PROJECT_VERSION = [^;]+;/g, `CURRENT_PROJECT_VERSION = ${config.ios.buildNumber ?? 1};`);
|
|
3924
|
+
writeFileSync7(pbxPath, pbx);
|
|
3925
|
+
log("");
|
|
3926
|
+
success(`Shell synced: app.js + ${runtimePlugins.length} plugin(s), variant ${variant}, v${config.version ?? "1.0.0"} (${config.ios.buildNumber ?? 1}).`);
|
|
3927
|
+
note("Build & run from Xcode (open ios/App.xcodeproj) — user files in ios/App/*.swift were not touched.");
|
|
3928
|
+
};
|
|
3929
|
+
var appPlugin = async (args) => {
|
|
3930
|
+
const sub = args._[1];
|
|
3931
|
+
const root = projectRoot();
|
|
3932
|
+
const configPath = join11(root, "app.json");
|
|
3933
|
+
const config = readJson(configPath);
|
|
3934
|
+
if (!config)
|
|
3935
|
+
throw new CliError("No app.json here — run `lecodes app init ios` first.");
|
|
3936
|
+
config.plugins ??= {};
|
|
3937
|
+
if (sub === "list" || sub === undefined) {
|
|
3938
|
+
const entries = Object.entries(config.plugins);
|
|
3939
|
+
if (entries.length === 0) {
|
|
3940
|
+
log("No plugins installed.");
|
|
3941
|
+
return;
|
|
3942
|
+
}
|
|
3943
|
+
const lock = readJson(join11(root, "plugins.lock.json"))?.plugins ?? {};
|
|
3944
|
+
for (const [id2, spec] of entries) {
|
|
3945
|
+
const pin = lock[id2] ? c.dim(` ${lock[id2].repo} ${lock[id2].tag} (${lock[id2].commit.slice(0, 7)})`) : c.dim(" (not synced)");
|
|
3946
|
+
log(`${c.bold(id2)} ${spec}${pin}`);
|
|
3947
|
+
}
|
|
3948
|
+
return;
|
|
3949
|
+
}
|
|
3950
|
+
const id = args._[2];
|
|
3951
|
+
if (!id)
|
|
3952
|
+
throw new CliError(`Usage: lecodes app plugin ${sub} <id | github:owner/repo[@tag]>`);
|
|
3953
|
+
if (sub === "add") {
|
|
3954
|
+
if (id.startsWith("github:")) {
|
|
3955
|
+
const bare = id.slice("github:".length).split("@")[0].split("/").pop();
|
|
3956
|
+
config.plugins[bare] = id;
|
|
3957
|
+
log(`${c.green("added")} ${bare} → ${id} ${c.dim("(third-party native code — review it before shipping)")}`);
|
|
3958
|
+
} else if (FIRST_PARTY_PLUGINS[id]) {
|
|
3959
|
+
config.plugins[id] = "*";
|
|
3960
|
+
log(`${c.green("added")} ${id}`);
|
|
2158
3961
|
} else {
|
|
2159
|
-
|
|
3962
|
+
throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use github:owner/repo[@tag].`);
|
|
2160
3963
|
}
|
|
3964
|
+
writeJson(configPath, config);
|
|
3965
|
+
note("Run `lecodes app sync` to vendor it into the shell.");
|
|
3966
|
+
return;
|
|
2161
3967
|
}
|
|
2162
|
-
|
|
3968
|
+
if (sub === "remove") {
|
|
3969
|
+
if (!(id in config.plugins))
|
|
3970
|
+
throw new CliError(`Plugin "${id}" is not installed.`);
|
|
3971
|
+
delete config.plugins[id];
|
|
3972
|
+
writeJson(configPath, config);
|
|
3973
|
+
log(`${c.yellow("removed")} ${id}`);
|
|
3974
|
+
note("Run `lecodes app sync` to drop its sources from the shell.");
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3977
|
+
throw new CliError(`Unknown subcommand "plugin ${sub}" — use list, add or remove.`);
|
|
2163
3978
|
};
|
|
2164
|
-
|
|
2165
|
-
// src/compile/projectCompile.ts
|
|
2166
|
-
import { existsSync as existsSync8 } from "node:fs";
|
|
2167
|
-
import { createRequire as createRequire3 } from "node:module";
|
|
2168
|
-
import { dirname as dirname6, join as join10 } from "node:path";
|
|
2169
|
-
init_compile();
|
|
2170
|
-
var vendoredInject = join10(distRoot, "runtime", "sdk", "inject.ts");
|
|
2171
|
-
if (existsSync8(vendoredInject))
|
|
2172
|
-
process.env.LECODES_SDK_INJECT = vendoredInject;
|
|
2173
|
-
var resolveChiselBin2 = () => {
|
|
2174
|
-
if (process.env.LECODES_CHISEL_BIN)
|
|
2175
|
-
return process.env.LECODES_CHISEL_BIN;
|
|
3979
|
+
var runTool = (cwd, cmd, args) => {
|
|
2176
3980
|
try {
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
3981
|
+
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 });
|
|
3982
|
+
} catch (e) {
|
|
3983
|
+
const err = e;
|
|
3984
|
+
const tail = ((err.stdout ?? "") + `
|
|
3985
|
+
` + (err.stderr ?? "")).split(`
|
|
3986
|
+
`).filter((l) => l.trim()).slice(-25).join(`
|
|
3987
|
+
`);
|
|
3988
|
+
throw new CliError(`${cmd} ${args[0] ?? ""} failed:
|
|
3989
|
+
${tail}`);
|
|
3990
|
+
}
|
|
3991
|
+
};
|
|
3992
|
+
var exportOptionsPlist = (method, teamId, destination) => `<?xml version="1.0" encoding="UTF-8"?>
|
|
3993
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3994
|
+
<plist version="1.0">
|
|
3995
|
+
<dict>
|
|
3996
|
+
<key>method</key>
|
|
3997
|
+
<string>${method}</string>
|
|
3998
|
+
<key>signingStyle</key>
|
|
3999
|
+
<string>automatic</string>${teamId ? `
|
|
4000
|
+
<key>teamID</key>
|
|
4001
|
+
<string>${teamId}</string>` : ""}${destination ? `
|
|
4002
|
+
<key>destination</key>
|
|
4003
|
+
<string>${destination}</string>` : ""}
|
|
4004
|
+
</dict>
|
|
4005
|
+
</plist>
|
|
4006
|
+
`;
|
|
4007
|
+
var requireShell = () => {
|
|
4008
|
+
const root = projectRoot();
|
|
4009
|
+
const config = readJson(join11(root, "app.json"));
|
|
4010
|
+
if (!config?.ios || !existsSync8(join11(root, "ios"))) {
|
|
4011
|
+
throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
|
|
4012
|
+
}
|
|
4013
|
+
return { root, config };
|
|
4014
|
+
};
|
|
4015
|
+
var appDoctor = async (_args) => {
|
|
4016
|
+
const { root, config } = requireShell();
|
|
4017
|
+
let failed = 0;
|
|
4018
|
+
const check = (ok, label, hint = "", fatal = true) => {
|
|
4019
|
+
log(`${ok ? c.green("✓") : fatal ? c.red("✗") : c.yellow("!")} ${label}${ok || !hint ? "" : c.dim(" — " + hint)}`);
|
|
4020
|
+
if (!ok && fatal)
|
|
4021
|
+
failed++;
|
|
4022
|
+
};
|
|
4023
|
+
let xcodeVersion = "";
|
|
4024
|
+
try {
|
|
4025
|
+
xcodeVersion = runTool(root, "xcodebuild", ["-version"]).split(`
|
|
4026
|
+
`)[0];
|
|
2180
4027
|
} catch {}
|
|
2181
|
-
|
|
2182
|
-
|
|
4028
|
+
check(xcodeVersion !== "", `Xcode CLI (${xcodeVersion || "not found"})`, "install Xcode + command line tools");
|
|
4029
|
+
check(!!config.ios.teamId, "Development team set", "lecodes app init wrote app.json — fill ios.teamId", false);
|
|
4030
|
+
const iconDir = join11(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
|
|
4031
|
+
const hasIcon = existsSync8(iconDir) && readdirSync4(iconDir).some((f2) => f2.endsWith(".png"));
|
|
4032
|
+
check(hasIcon, "App icon present", "drop a 1024×1024 PNG into ios/App/Assets.xcassets/AppIcon.appiconset/ (App Store requires it)", false);
|
|
4033
|
+
const bundle = join11(root, "ios", "App", "Resources", "app.js");
|
|
4034
|
+
check(existsSync8(bundle), "Bundled app.js", "run `lecodes app sync`");
|
|
4035
|
+
const requestedVariant = config.ios.variant ?? "auto";
|
|
4036
|
+
const validVariant = requestedVariant === "auto" || SDK_VARIANTS.includes(requestedVariant);
|
|
4037
|
+
check(validVariant, `ios.variant "${requestedVariant}"`, `use "auto" or one of ${SDK_VARIANTS.join(", ")}`);
|
|
4038
|
+
if (existsSync8(bundle) && validVariant) {
|
|
4039
|
+
const { variant } = resolveVariant(requestedVariant, readFileSync10(bundle, "utf8"));
|
|
4040
|
+
const pkg = readFileSync10(join11(root, "ios", "LeCodesRuntime", "Package.swift"), "utf8");
|
|
4041
|
+
const needsResources = variant === "3d" || variant === "full";
|
|
4042
|
+
const hasResources = existsSync8(join11(root, "ios", "App", "Resources", "LeCodesSDKResources.bundle"));
|
|
4043
|
+
check(pkg.includes(`"LeCodesSDK-${variant}"`) && (!needsResources || hasResources), `SDK variant ${variant}${needsResources ? " (+ filament resources)" : ""}`, "run `lecodes app sync`");
|
|
4044
|
+
}
|
|
4045
|
+
const lock = readJson(join11(root, "plugins.lock.json"))?.plugins ?? {};
|
|
4046
|
+
const wanted = Object.keys(config.plugins ?? {});
|
|
4047
|
+
const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
|
|
4048
|
+
check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
|
|
4049
|
+
const asc = readJson(join11(homedir2(), ".lecodes", "asc.json"));
|
|
4050
|
+
check(!!asc?.keyPath, "App Store Connect API key configured", "pass --key-path/--key-id/--issuer-id to `lecodes app upload` once to store it", false);
|
|
4051
|
+
log("");
|
|
4052
|
+
if (failed > 0)
|
|
4053
|
+
throw new CliError(`${failed} check(s) failed.`);
|
|
4054
|
+
success("Ready to build.");
|
|
4055
|
+
};
|
|
4056
|
+
var ARCHIVE_PATH = "ios/build/App.xcarchive";
|
|
4057
|
+
var appBuild = async (args) => {
|
|
4058
|
+
const { root, config } = requireShell();
|
|
4059
|
+
const configuration = flagStr(args, "configuration") ?? "Release";
|
|
4060
|
+
const method = flagStr(args, "method") ?? "app-store-connect";
|
|
4061
|
+
if (!flagBool(args, "no-sync"))
|
|
4062
|
+
await appSync({ _: [], flags: {} });
|
|
4063
|
+
note(`Archiving (${configuration})… this takes a few minutes`);
|
|
4064
|
+
runTool(root, "xcodebuild", [
|
|
4065
|
+
"-project",
|
|
4066
|
+
"ios/App.xcodeproj",
|
|
4067
|
+
"-scheme",
|
|
4068
|
+
"App",
|
|
4069
|
+
"-destination",
|
|
4070
|
+
"generic/platform=iOS",
|
|
4071
|
+
"-configuration",
|
|
4072
|
+
configuration,
|
|
4073
|
+
"archive",
|
|
4074
|
+
"-archivePath",
|
|
4075
|
+
ARCHIVE_PATH,
|
|
4076
|
+
"-allowProvisioningUpdates"
|
|
4077
|
+
]);
|
|
4078
|
+
success(`Archive: ios/build/App.xcarchive`);
|
|
4079
|
+
if (flagBool(args, "no-export"))
|
|
4080
|
+
return;
|
|
4081
|
+
const optionsPath = join11(root, "ios", "build", "ExportOptions.plist");
|
|
4082
|
+
writeFileSync7(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
|
|
4083
|
+
note(`Exporting (.ipa, method ${method})…`);
|
|
4084
|
+
runTool(root, "xcodebuild", [
|
|
4085
|
+
"-exportArchive",
|
|
4086
|
+
"-archivePath",
|
|
4087
|
+
ARCHIVE_PATH,
|
|
4088
|
+
"-exportOptionsPlist",
|
|
4089
|
+
optionsPath,
|
|
4090
|
+
"-exportPath",
|
|
4091
|
+
"ios/build/export",
|
|
4092
|
+
"-allowProvisioningUpdates"
|
|
4093
|
+
]);
|
|
4094
|
+
success("Export: ios/build/export/ — upload with `lecodes app upload ios`");
|
|
4095
|
+
};
|
|
4096
|
+
var appUpload = async (args) => {
|
|
4097
|
+
const { root, config } = requireShell();
|
|
4098
|
+
if (!existsSync8(join11(root, ARCHIVE_PATH))) {
|
|
4099
|
+
throw new CliError("No archive at ios/build/App.xcarchive — run `lecodes app build ios` first.");
|
|
4100
|
+
}
|
|
4101
|
+
const ascPath = join11(homedir2(), ".lecodes", "asc.json");
|
|
4102
|
+
const stored = readJson(ascPath) ?? {};
|
|
4103
|
+
const keyPath = flagStr(args, "key-path") ?? stored.keyPath;
|
|
4104
|
+
const keyId = flagStr(args, "key-id") ?? stored.keyId;
|
|
4105
|
+
const issuerId = flagStr(args, "issuer-id") ?? stored.issuerId;
|
|
4106
|
+
if (!keyPath || !keyId || !issuerId) {
|
|
4107
|
+
throw new CliError(`App Store Connect API key needed: --key-path <AuthKey.p8> --key-id <id> --issuer-id <id>
|
|
4108
|
+
(create one at appstoreconnect.apple.com → Users and Access → Integrations; stored in ~/.lecodes/asc.json after first use)`);
|
|
4109
|
+
}
|
|
4110
|
+
if (!existsSync8(keyPath))
|
|
4111
|
+
throw new CliError(`API key file not found: ${keyPath}`);
|
|
4112
|
+
if (flagStr(args, "key-path")) {
|
|
4113
|
+
mkdirSync5(dirname7(ascPath), { recursive: true });
|
|
4114
|
+
writeJson(ascPath, { keyPath, keyId, issuerId });
|
|
4115
|
+
note("Stored the key reference in ~/.lecodes/asc.json");
|
|
4116
|
+
}
|
|
4117
|
+
const optionsPath = join11(root, "ios", "build", "UploadOptions.plist");
|
|
4118
|
+
writeFileSync7(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
|
|
4119
|
+
note("Uploading to App Store Connect… (processing continues on Apple's side)");
|
|
4120
|
+
runTool(root, "xcodebuild", [
|
|
4121
|
+
"-exportArchive",
|
|
4122
|
+
"-archivePath",
|
|
4123
|
+
ARCHIVE_PATH,
|
|
4124
|
+
"-exportOptionsPlist",
|
|
4125
|
+
optionsPath,
|
|
4126
|
+
"-exportPath",
|
|
4127
|
+
"ios/build/upload",
|
|
4128
|
+
"-authenticationKeyPath",
|
|
4129
|
+
resolve2(keyPath),
|
|
4130
|
+
"-authenticationKeyID",
|
|
4131
|
+
keyId,
|
|
4132
|
+
"-authenticationKeyIssuerID",
|
|
4133
|
+
issuerId,
|
|
4134
|
+
"-allowProvisioningUpdates"
|
|
4135
|
+
]);
|
|
4136
|
+
const configPath = join11(root, "app.json");
|
|
4137
|
+
const fresh = readJson(configPath);
|
|
4138
|
+
fresh.ios.buildNumber = (fresh.ios.buildNumber ?? 1) + 1;
|
|
4139
|
+
writeJson(configPath, fresh);
|
|
4140
|
+
success(`Uploaded build ${(fresh.ios.buildNumber ?? 2) - 1} — check App Store Connect / TestFlight once processing finishes.`);
|
|
4141
|
+
note(`ios.buildNumber bumped to ${fresh.ios.buildNumber} for the next build.`);
|
|
4142
|
+
};
|
|
4143
|
+
var appOpen = async (args) => {
|
|
4144
|
+
const root = projectRoot();
|
|
4145
|
+
const platform = args._[1] ?? "ios";
|
|
4146
|
+
if (platform === "ios") {
|
|
4147
|
+
const proj = join11(root, "ios", "App.xcodeproj");
|
|
4148
|
+
if (!existsSync8(proj))
|
|
4149
|
+
throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
|
|
4150
|
+
if (process.platform !== "darwin")
|
|
4151
|
+
throw new CliError("Opening Xcode requires macOS.");
|
|
4152
|
+
execFileSync("open", [proj], { stdio: "ignore" });
|
|
4153
|
+
success("Opened ios/App.xcodeproj in Xcode.");
|
|
4154
|
+
return;
|
|
4155
|
+
}
|
|
4156
|
+
if (platform === "android") {
|
|
4157
|
+
throw new CliError("Android shells don't exist yet — `lecodes app open` covers ios for now.");
|
|
4158
|
+
}
|
|
4159
|
+
throw new CliError(`Unknown platform "${platform}" — use ios.`);
|
|
4160
|
+
};
|
|
4161
|
+
var app = async (args) => {
|
|
4162
|
+
const sub = args._[0];
|
|
4163
|
+
if (sub === "init")
|
|
4164
|
+
return appInit(args);
|
|
4165
|
+
if (sub === "sync")
|
|
4166
|
+
return appSync(args);
|
|
4167
|
+
if (sub === "plugin")
|
|
4168
|
+
return appPlugin(args);
|
|
4169
|
+
if (sub === "doctor")
|
|
4170
|
+
return appDoctor(args);
|
|
4171
|
+
if (sub === "build")
|
|
4172
|
+
return appBuild(args);
|
|
4173
|
+
if (sub === "upload")
|
|
4174
|
+
return appUpload(args);
|
|
4175
|
+
if (sub === "open")
|
|
4176
|
+
return appOpen(args);
|
|
4177
|
+
throw new CliError("Usage: lecodes app <init ios | sync | open | plugin list|add|remove | doctor | build ios | upload ios> — see `lecodes help`.");
|
|
2183
4178
|
};
|
|
2184
|
-
var chiselBin = resolveChiselBin2();
|
|
2185
|
-
if (chiselBin)
|
|
2186
|
-
process.env.LECODES_CHISEL_BIN = chiselBin;
|
|
2187
|
-
var compileProject2 = (opts) => compileProject({
|
|
2188
|
-
entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, shaders: e.shaders })),
|
|
2189
|
-
name: opts.name,
|
|
2190
|
-
publicUrl: opts.publicUrl,
|
|
2191
|
-
entryOverride: opts.entryOverride,
|
|
2192
|
-
header: opts.header,
|
|
2193
|
-
format: opts.format,
|
|
2194
|
-
localAssets: opts.localAssets,
|
|
2195
|
-
minify: opts.minify,
|
|
2196
|
-
sourcemap: opts.sourcemap,
|
|
2197
|
-
editor: opts.editor
|
|
2198
|
-
});
|
|
2199
4179
|
|
|
2200
|
-
// src/commands/
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
const
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
({ entries, warnings } = collectLocalEntries(root, null, publicUrl));
|
|
2230
|
-
name = basename3(root);
|
|
4180
|
+
// src/commands/clone.ts
|
|
4181
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5 } from "node:fs";
|
|
4182
|
+
import { resolve as resolve3 } from "node:path";
|
|
4183
|
+
|
|
4184
|
+
// src/types.ts
|
|
4185
|
+
import { dirname as dirname8, join as join12 } from "node:path";
|
|
4186
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
|
|
4187
|
+
var TSCONFIG = {
|
|
4188
|
+
compilerOptions: {
|
|
4189
|
+
target: "ES2020",
|
|
4190
|
+
module: "ESNext",
|
|
4191
|
+
moduleResolution: "bundler",
|
|
4192
|
+
lib: ["ES2020"],
|
|
4193
|
+
types: [],
|
|
4194
|
+
allowJs: true,
|
|
4195
|
+
checkJs: false,
|
|
4196
|
+
strict: false,
|
|
4197
|
+
noEmit: true,
|
|
4198
|
+
skipLibCheck: true
|
|
4199
|
+
},
|
|
4200
|
+
include: [`${LECODES_DIR}/types/**/*.d.ts`, "**/*.ts", "**/*.js"],
|
|
4201
|
+
exclude: ["node_modules"]
|
|
4202
|
+
};
|
|
4203
|
+
var writeTypeBundle = (root, bundle) => {
|
|
4204
|
+
const typesDir = join12(root, LECODES_DIR, "types");
|
|
4205
|
+
for (const [path, text] of Object.entries(bundle)) {
|
|
4206
|
+
const dest = join12(typesDir, path);
|
|
4207
|
+
mkdirSync6(dirname8(dest), { recursive: true });
|
|
4208
|
+
writeFileSync8(dest, text);
|
|
2231
4209
|
}
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
note(`Discarded output (${out})`);
|
|
2248
|
-
} else if (out) {
|
|
2249
|
-
writeFileSync6(resolve2(out), js);
|
|
2250
|
-
note(`Wrote ${out} (${js.length} bytes)`);
|
|
2251
|
-
} else {
|
|
2252
|
-
process.stdout.write(js);
|
|
2253
|
-
if (!js.endsWith(`
|
|
2254
|
-
`))
|
|
2255
|
-
process.stdout.write(`
|
|
2256
|
-
`);
|
|
4210
|
+
};
|
|
4211
|
+
var materializeTypes = async (root, apiUrl) => {
|
|
4212
|
+
writeTypeBundle(root, await getTypes(apiUrl));
|
|
4213
|
+
};
|
|
4214
|
+
var readLocalTypeBundle = () => {
|
|
4215
|
+
const candidates = [
|
|
4216
|
+
join12(distRoot, "runtime", "sdk-types.json"),
|
|
4217
|
+
join12(distRoot, "..", "sdk", "dist", "types.json")
|
|
4218
|
+
];
|
|
4219
|
+
for (const path of candidates) {
|
|
4220
|
+
if (existsSync9(path)) {
|
|
4221
|
+
try {
|
|
4222
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
4223
|
+
} catch {}
|
|
4224
|
+
}
|
|
2257
4225
|
}
|
|
4226
|
+
return null;
|
|
2258
4227
|
};
|
|
2259
|
-
var
|
|
2260
|
-
|
|
2261
|
-
|
|
4228
|
+
var materializeTypesLocal = (root) => {
|
|
4229
|
+
const bundle = readLocalTypeBundle();
|
|
4230
|
+
if (!bundle)
|
|
4231
|
+
return false;
|
|
4232
|
+
writeTypeBundle(root, bundle);
|
|
4233
|
+
return true;
|
|
4234
|
+
};
|
|
4235
|
+
var writeTsconfig = (root) => {
|
|
4236
|
+
writeFileSync8(join12(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
|
|
4237
|
+
};
|
|
4238
|
+
|
|
4239
|
+
// src/commands/clone.ts
|
|
4240
|
+
var parseUuid = (input) => {
|
|
4241
|
+
if (!input)
|
|
4242
|
+
throw new CliError("Usage: lecodes clone <project-uuid|url> [directory]");
|
|
4243
|
+
if (!input.includes("/"))
|
|
4244
|
+
return input;
|
|
4245
|
+
const clean = input.split(/[?#]/)[0].replace(/\/+$/, "");
|
|
4246
|
+
const match = clean.match(/\/(?:projects|view)\/([^/]+)/);
|
|
4247
|
+
return match ? match[1] : clean.slice(clean.lastIndexOf("/") + 1);
|
|
4248
|
+
};
|
|
4249
|
+
var slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
4250
|
+
var clone = async (args) => {
|
|
4251
|
+
const config = loadConfig();
|
|
4252
|
+
const apiUrl = requireApiUrl(config);
|
|
4253
|
+
const token = requireToken(config);
|
|
4254
|
+
const uuid = parseUuid(args._[0]);
|
|
4255
|
+
const project = await getProject(apiUrl, token, uuid);
|
|
4256
|
+
const dir = args._[1] ?? slug(project.name);
|
|
4257
|
+
const root = resolve3(dir);
|
|
4258
|
+
if (existsSync10(root) && readdirSync5(root).length > 0) {
|
|
4259
|
+
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
4260
|
+
}
|
|
4261
|
+
mkdirSync7(root, { recursive: true });
|
|
4262
|
+
info(c.dim(`Fetching project ${c.bold(project.name)}…`));
|
|
4263
|
+
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
4264
|
+
endProgress();
|
|
4265
|
+
if (!flagBool(args, "no-types")) {
|
|
4266
|
+
info(c.dim("Fetching type definitions…"));
|
|
4267
|
+
await materializeTypes(root, apiUrl);
|
|
4268
|
+
writeTsconfig(root);
|
|
4269
|
+
}
|
|
4270
|
+
writeDefaultIgnore(root);
|
|
4271
|
+
const commits = await getCommits(apiUrl, token, uuid);
|
|
4272
|
+
writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
|
|
4273
|
+
success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`);
|
|
4274
|
+
info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``));
|
|
2262
4275
|
};
|
|
2263
4276
|
|
|
2264
4277
|
// src/commands/create.ts
|
|
2265
|
-
import { existsSync as
|
|
2266
|
-
import { join as
|
|
4278
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
4279
|
+
import { join as join13, resolve as resolve4 } from "node:path";
|
|
2267
4280
|
var slug2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
2268
4281
|
var STARTER = `// Welcome to le.codes — no imports needed, the whole SDK is available as globals.
|
|
2269
4282
|
const scene = new Scene2D({ background: '#10131a' })
|
|
@@ -2281,18 +4294,18 @@ var create = async (args) => {
|
|
|
2281
4294
|
if (!name)
|
|
2282
4295
|
throw new CliError("Usage: lecodes create <name> [directory]");
|
|
2283
4296
|
const dir = args._[1] ?? slug2(name);
|
|
2284
|
-
const root =
|
|
2285
|
-
if (
|
|
4297
|
+
const root = resolve4(dir);
|
|
4298
|
+
if (existsSync11(root) && readdirSync6(root).length > 0) {
|
|
2286
4299
|
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
2287
4300
|
}
|
|
2288
|
-
|
|
4301
|
+
mkdirSync8(root, { recursive: true });
|
|
2289
4302
|
info(c.dim(`Creating project ${c.bold(name)}…`));
|
|
2290
4303
|
const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
|
|
2291
4304
|
info(c.dim("Fetching type definitions…"));
|
|
2292
4305
|
await materializeTypes(root, apiUrl);
|
|
2293
4306
|
writeTsconfig(root);
|
|
2294
4307
|
writeDefaultIgnore(root);
|
|
2295
|
-
|
|
4308
|
+
writeFileSync9(join13(root, "main.ts"), STARTER);
|
|
2296
4309
|
const commits = await getCommits(apiUrl, token, uuid);
|
|
2297
4310
|
writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
|
|
2298
4311
|
success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
|
|
@@ -2300,22 +4313,22 @@ var create = async (args) => {
|
|
|
2300
4313
|
};
|
|
2301
4314
|
|
|
2302
4315
|
// src/commands/design.ts
|
|
2303
|
-
import { existsSync as
|
|
2304
|
-
import { basename as
|
|
4316
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync14, readdirSync as readdirSync7, writeFileSync as writeFileSync11, appendFileSync } from "node:fs";
|
|
4317
|
+
import { basename as basename6, join as join15 } from "node:path";
|
|
2305
4318
|
|
|
2306
4319
|
// src/compile/designCompile.ts
|
|
2307
|
-
import { readFileSync as
|
|
2308
|
-
import { basename as
|
|
4320
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
4321
|
+
import { basename as basename5 } from "node:path";
|
|
2309
4322
|
|
|
2310
4323
|
// src/compile/assetIcons.ts
|
|
2311
|
-
import { createHash as
|
|
2312
|
-
import { existsSync as
|
|
2313
|
-
import { dirname as
|
|
4324
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4325
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "node:fs";
|
|
4326
|
+
import { dirname as dirname9, join as join14 } from "node:path";
|
|
2314
4327
|
var DEFAULT_REGISTRY = process.env.LECODES_ICON_REGISTRY?.trim() || "https://icon-registry.jt3.ru/v1";
|
|
2315
4328
|
var ICON_ID = /^([a-z0-9]+(?:-[a-z0-9]+)*):([a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
|
2316
|
-
var
|
|
2317
|
-
var
|
|
2318
|
-
var
|
|
4329
|
+
var isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$";
|
|
4330
|
+
var isIdentPart2 = (c2) => isIdentStart2(c2) || c2 >= "0" && c2 <= "9";
|
|
4331
|
+
var skipString2 = (src, start) => {
|
|
2319
4332
|
const quote = src[start];
|
|
2320
4333
|
const n = src.length;
|
|
2321
4334
|
let i = start + 1;
|
|
@@ -2326,7 +4339,7 @@ var skipString = (src, start) => {
|
|
|
2326
4339
|
continue;
|
|
2327
4340
|
}
|
|
2328
4341
|
if (quote === "`" && c2 === "$" && src[i + 1] === "{") {
|
|
2329
|
-
i =
|
|
4342
|
+
i = skipBraces2(src, i + 1);
|
|
2330
4343
|
continue;
|
|
2331
4344
|
}
|
|
2332
4345
|
if (c2 === quote)
|
|
@@ -2335,14 +4348,14 @@ var skipString = (src, start) => {
|
|
|
2335
4348
|
}
|
|
2336
4349
|
return n;
|
|
2337
4350
|
};
|
|
2338
|
-
var
|
|
4351
|
+
var skipBraces2 = (src, open) => {
|
|
2339
4352
|
const n = src.length;
|
|
2340
4353
|
let depth = 0;
|
|
2341
4354
|
let i = open;
|
|
2342
4355
|
while (i < n) {
|
|
2343
4356
|
const c2 = src[i];
|
|
2344
4357
|
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
2345
|
-
i =
|
|
4358
|
+
i = skipString2(src, i);
|
|
2346
4359
|
continue;
|
|
2347
4360
|
}
|
|
2348
4361
|
if (c2 === "{")
|
|
@@ -2356,14 +4369,14 @@ var skipBraces = (src, open) => {
|
|
|
2356
4369
|
}
|
|
2357
4370
|
return n;
|
|
2358
4371
|
};
|
|
2359
|
-
var
|
|
4372
|
+
var matchParen2 = (src, open) => {
|
|
2360
4373
|
const n = src.length;
|
|
2361
4374
|
let depth = 0;
|
|
2362
4375
|
let i = open;
|
|
2363
4376
|
while (i < n) {
|
|
2364
4377
|
const c2 = src[i];
|
|
2365
4378
|
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
2366
|
-
i =
|
|
4379
|
+
i = skipString2(src, i);
|
|
2367
4380
|
continue;
|
|
2368
4381
|
}
|
|
2369
4382
|
if (c2 === "/" && src[i + 1] === "/") {
|
|
@@ -2391,7 +4404,7 @@ var matchParen = (src, open) => {
|
|
|
2391
4404
|
}
|
|
2392
4405
|
return -1;
|
|
2393
4406
|
};
|
|
2394
|
-
var
|
|
4407
|
+
var splitTopLevel2 = (text, delim) => {
|
|
2395
4408
|
const parts = [];
|
|
2396
4409
|
const n = text.length;
|
|
2397
4410
|
let depth = 0;
|
|
@@ -2400,7 +4413,7 @@ var splitTopLevel = (text, delim) => {
|
|
|
2400
4413
|
while (i < n) {
|
|
2401
4414
|
const c2 = text[i];
|
|
2402
4415
|
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
2403
|
-
i =
|
|
4416
|
+
i = skipString2(text, i);
|
|
2404
4417
|
continue;
|
|
2405
4418
|
}
|
|
2406
4419
|
if (c2 === "(" || c2 === "[" || c2 === "{")
|
|
@@ -2416,7 +4429,7 @@ var splitTopLevel = (text, delim) => {
|
|
|
2416
4429
|
parts.push(text.slice(last));
|
|
2417
4430
|
return parts;
|
|
2418
4431
|
};
|
|
2419
|
-
var
|
|
4432
|
+
var stringLiteralValue2 = (text) => {
|
|
2420
4433
|
const t = text.trim();
|
|
2421
4434
|
if (t.length < 2)
|
|
2422
4435
|
return null;
|
|
@@ -2437,7 +4450,7 @@ var scanAssetIconCalls = (source) => {
|
|
|
2437
4450
|
while (i < n) {
|
|
2438
4451
|
const c2 = source[i];
|
|
2439
4452
|
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
2440
|
-
i =
|
|
4453
|
+
i = skipString2(source, i);
|
|
2441
4454
|
continue;
|
|
2442
4455
|
}
|
|
2443
4456
|
if (c2 === "/" && source[i + 1] === "/") {
|
|
@@ -2454,12 +4467,12 @@ var scanAssetIconCalls = (source) => {
|
|
|
2454
4467
|
i += 2;
|
|
2455
4468
|
continue;
|
|
2456
4469
|
}
|
|
2457
|
-
if (
|
|
4470
|
+
if (isIdentStart2(c2)) {
|
|
2458
4471
|
let j = i + 1;
|
|
2459
|
-
while (j < n &&
|
|
4472
|
+
while (j < n && isIdentPart2(source[j]))
|
|
2460
4473
|
j++;
|
|
2461
|
-
if (source.slice(i, j) === "assetIcon" &&
|
|
2462
|
-
const call =
|
|
4474
|
+
if (source.slice(i, j) === "assetIcon" && prevNonSpace2(source, i) !== ".") {
|
|
4475
|
+
const call = parseCall2(source, i, j);
|
|
2463
4476
|
if (call) {
|
|
2464
4477
|
calls.push(call);
|
|
2465
4478
|
i = call.end;
|
|
@@ -2473,25 +4486,25 @@ var scanAssetIconCalls = (source) => {
|
|
|
2473
4486
|
}
|
|
2474
4487
|
return calls;
|
|
2475
4488
|
};
|
|
2476
|
-
var
|
|
4489
|
+
var prevNonSpace2 = (src, i) => {
|
|
2477
4490
|
let k = i - 1;
|
|
2478
4491
|
while (k >= 0 && /\s/.test(src[k]))
|
|
2479
4492
|
k--;
|
|
2480
4493
|
return k >= 0 ? src[k] : "";
|
|
2481
4494
|
};
|
|
2482
|
-
var
|
|
4495
|
+
var parseCall2 = (src, start, identEnd) => {
|
|
2483
4496
|
const n = src.length;
|
|
2484
4497
|
let i = identEnd;
|
|
2485
4498
|
while (i < n && /\s/.test(src[i]))
|
|
2486
4499
|
i++;
|
|
2487
4500
|
if (src[i] !== "(")
|
|
2488
4501
|
return null;
|
|
2489
|
-
const close =
|
|
4502
|
+
const close = matchParen2(src, i);
|
|
2490
4503
|
if (close === -1)
|
|
2491
4504
|
return null;
|
|
2492
4505
|
const call = { start, end: close + 1, id: "", pack: "", name: "" };
|
|
2493
|
-
const args =
|
|
2494
|
-
const id =
|
|
4506
|
+
const args = splitTopLevel2(src.slice(i + 1, close), ",");
|
|
4507
|
+
const id = stringLiteralValue2(args[0] ?? "");
|
|
2495
4508
|
if (id === null) {
|
|
2496
4509
|
call.error = `assetIcon(...) needs a string-literal id like "lucide:bell" (got \`${(args[0] ?? "").trim() || "()"}\`).`;
|
|
2497
4510
|
return call;
|
|
@@ -2522,8 +4535,8 @@ var extractColor = (optsText) => {
|
|
|
2522
4535
|
if (!optsText.startsWith("{") || !optsText.endsWith("}")) {
|
|
2523
4536
|
return { error: `assetIcon(id, opts): opts must be an object literal like { color: "#5b8cff" }.` };
|
|
2524
4537
|
}
|
|
2525
|
-
for (const prop of
|
|
2526
|
-
const kv =
|
|
4538
|
+
for (const prop of splitTopLevel2(optsText.slice(1, -1), ",")) {
|
|
4539
|
+
const kv = splitTopLevel2(prop, ":");
|
|
2527
4540
|
if (kv.length < 2)
|
|
2528
4541
|
continue;
|
|
2529
4542
|
const key = kv[0].trim().replace(/^["'`]|["'`]$/g, "");
|
|
@@ -2532,7 +4545,7 @@ var extractColor = (optsText) => {
|
|
|
2532
4545
|
const raw = kv.slice(1).join(":").trim();
|
|
2533
4546
|
if (!raw)
|
|
2534
4547
|
continue;
|
|
2535
|
-
const literal =
|
|
4548
|
+
const literal = stringLiteralValue2(raw);
|
|
2536
4549
|
return literal !== null ? { bake: literal } : { tint: raw };
|
|
2537
4550
|
}
|
|
2538
4551
|
return {};
|
|
@@ -2553,34 +4566,34 @@ var iconExpr = (svg, call) => {
|
|
|
2553
4566
|
return call.tint ? `Object.assign(${src}, { tintColor: (${call.tint}) })` : src;
|
|
2554
4567
|
};
|
|
2555
4568
|
var escapeTemplate = (s) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
2556
|
-
var
|
|
2557
|
-
var
|
|
2558
|
-
var
|
|
4569
|
+
var isHttp2 = (s) => /^https?:/i.test(s);
|
|
4570
|
+
var sha122 = (s) => createHash3("sha256").update(s).digest("hex").slice(0, 12);
|
|
4571
|
+
var registryHome2 = (registry) => registry.replace(/\/v\d+$/, "");
|
|
2559
4572
|
var readText = async (registry, rel) => {
|
|
2560
|
-
if (
|
|
4573
|
+
if (isHttp2(registry)) {
|
|
2561
4574
|
const res = await fetch(`${registry}/${rel}`);
|
|
2562
4575
|
return res.ok ? await res.text() : null;
|
|
2563
4576
|
}
|
|
2564
4577
|
try {
|
|
2565
|
-
return
|
|
4578
|
+
return readFileSync12(join14(registry, rel), "utf8");
|
|
2566
4579
|
} catch {
|
|
2567
4580
|
return null;
|
|
2568
4581
|
}
|
|
2569
4582
|
};
|
|
2570
|
-
var
|
|
4583
|
+
var readLock2 = (path) => {
|
|
2571
4584
|
try {
|
|
2572
|
-
const lock = JSON.parse(
|
|
4585
|
+
const lock = JSON.parse(readFileSync12(path, "utf8"));
|
|
2573
4586
|
if (lock && typeof lock === "object" && lock.icons && lock.packs)
|
|
2574
4587
|
return lock;
|
|
2575
4588
|
} catch {}
|
|
2576
4589
|
return null;
|
|
2577
4590
|
};
|
|
2578
|
-
var
|
|
4591
|
+
var serializeLock2 = (lock) => {
|
|
2579
4592
|
const sortKeys = (rec) => Object.fromEntries(Object.keys(rec).sort().map((k) => [k, rec[k]]));
|
|
2580
4593
|
return JSON.stringify({ registry: lock.registry, icons: sortKeys(lock.icons), packs: sortKeys(lock.packs) }, null, 2) + `
|
|
2581
4594
|
`;
|
|
2582
4595
|
};
|
|
2583
|
-
var
|
|
4596
|
+
var renderNotices2 = (lock) => {
|
|
2584
4597
|
const packs = [...new Set(Object.values(lock.icons).map((i) => i.pack))].sort();
|
|
2585
4598
|
const lines = [
|
|
2586
4599
|
"# Icon attribution & notices",
|
|
@@ -2605,29 +4618,29 @@ var renderNotices = (lock) => {
|
|
|
2605
4618
|
return lines.join(`
|
|
2606
4619
|
`);
|
|
2607
4620
|
};
|
|
2608
|
-
var
|
|
2609
|
-
const current =
|
|
4621
|
+
var writeIfChanged2 = (path, text) => {
|
|
4622
|
+
const current = existsSync12(path) ? safeRead(path) : null;
|
|
2610
4623
|
if (current === text)
|
|
2611
4624
|
return false;
|
|
2612
|
-
|
|
4625
|
+
writeFileSync10(path, text);
|
|
2613
4626
|
return true;
|
|
2614
4627
|
};
|
|
2615
4628
|
var safeRead = (path) => {
|
|
2616
4629
|
try {
|
|
2617
|
-
return
|
|
4630
|
+
return readFileSync12(path, "utf8");
|
|
2618
4631
|
} catch {
|
|
2619
4632
|
return null;
|
|
2620
4633
|
}
|
|
2621
4634
|
};
|
|
2622
4635
|
var vendorIcons = async (designDir, ids, opts = {}) => {
|
|
2623
4636
|
const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
|
|
2624
|
-
const lockPath =
|
|
2625
|
-
const lock =
|
|
4637
|
+
const lockPath = join14(designDir, "icons.lock.json");
|
|
4638
|
+
const lock = readLock2(lockPath) ?? { registry, icons: {}, packs: {} };
|
|
2626
4639
|
const svgById = new Map;
|
|
2627
4640
|
const missing = [];
|
|
2628
4641
|
for (const id of ids) {
|
|
2629
4642
|
const li = lock.icons[id];
|
|
2630
|
-
const cached = li && safeRead(
|
|
4643
|
+
const cached = li && safeRead(join14(designDir, li.path));
|
|
2631
4644
|
if (cached)
|
|
2632
4645
|
svgById.set(id, cached);
|
|
2633
4646
|
else
|
|
@@ -2643,23 +4656,23 @@ var vendorIcons = async (designDir, ids, opts = {}) => {
|
|
|
2643
4656
|
const [, pack, name] = m;
|
|
2644
4657
|
const lic = licenses[pack];
|
|
2645
4658
|
if (!lic)
|
|
2646
|
-
throw new Error(`assetIcon: unknown icon pack "${pack}" (in "${id}"). Browse packs at ${
|
|
4659
|
+
throw new Error(`assetIcon: unknown icon pack "${pack}" (in "${id}"). Browse packs at ${registryHome2(registry)}.`);
|
|
2647
4660
|
const svg = await readText(registry, `${pack}/${name}.svg`);
|
|
2648
4661
|
if (svg === null)
|
|
2649
|
-
throw new Error(`assetIcon: icon "${id}" not found in the registry. Check the name at ${
|
|
4662
|
+
throw new Error(`assetIcon: icon "${id}" not found in the registry. Check the name at ${registryHome2(registry)}.`);
|
|
2650
4663
|
const rel = `assets/icons/${pack}/${name}.svg`;
|
|
2651
|
-
const abs =
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
lock.icons[id] = { pack, name, hash:
|
|
4664
|
+
const abs = join14(designDir, rel);
|
|
4665
|
+
mkdirSync9(dirname9(abs), { recursive: true });
|
|
4666
|
+
writeFileSync10(abs, svg);
|
|
4667
|
+
lock.icons[id] = { pack, name, hash: sha122(svg), spdx: lic.spdx, path: rel };
|
|
2655
4668
|
lock.packs[pack] = { spdx: lic.spdx, title: lic.title, url: lic.url, attribution: lic.attribution, modifiedNotice: lic.modifiedNotice };
|
|
2656
4669
|
svgById.set(id, svg);
|
|
2657
4670
|
}
|
|
2658
4671
|
lock.registry = registry;
|
|
2659
|
-
|
|
4672
|
+
writeIfChanged2(lockPath, serializeLock2(lock));
|
|
2660
4673
|
}
|
|
2661
4674
|
if (Object.keys(lock.icons).length)
|
|
2662
|
-
|
|
4675
|
+
writeIfChanged2(join14(designDir, "ICON-NOTICES.md"), renderNotices2(lock));
|
|
2663
4676
|
return svgById;
|
|
2664
4677
|
};
|
|
2665
4678
|
var applyAssetIcons = async (entries, designDir, opts = {}) => {
|
|
@@ -2683,7 +4696,7 @@ var compileDesignScreen = async (opts) => {
|
|
|
2683
4696
|
const entries = [];
|
|
2684
4697
|
const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
|
|
2685
4698
|
for (const file of scan.files) {
|
|
2686
|
-
const type = manifest?.files[file.path]?.type ?? classifyKind(
|
|
4699
|
+
const type = manifest?.files[file.path]?.type ?? classifyKind(basename5(file.path));
|
|
2687
4700
|
if (type === "resource") {
|
|
2688
4701
|
entries.push({
|
|
2689
4702
|
path: file.path,
|
|
@@ -2694,7 +4707,7 @@ var compileDesignScreen = async (opts) => {
|
|
|
2694
4707
|
} else if (type === "shader") {
|
|
2695
4708
|
entries.push({ path: file.path, type, shaders: [] });
|
|
2696
4709
|
} else {
|
|
2697
|
-
entries.push({ path: file.path, type, text:
|
|
4710
|
+
entries.push({ path: file.path, type, text: readFileSync13(file.absPath, "utf8") });
|
|
2698
4711
|
}
|
|
2699
4712
|
}
|
|
2700
4713
|
const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()";
|
|
@@ -2708,6 +4721,18 @@ var compileDesignScreen = async (opts) => {
|
|
|
2708
4721
|
`
|
|
2709
4722
|
});
|
|
2710
4723
|
await applyAssetIcons(entries, designDir);
|
|
4724
|
+
await applyProjectFonts(entries, {
|
|
4725
|
+
mode: "vendor",
|
|
4726
|
+
root,
|
|
4727
|
+
vendorDir: designDir,
|
|
4728
|
+
vendorPrefix: designKey,
|
|
4729
|
+
makeResourceEntry: (path, absPath) => ({
|
|
4730
|
+
path,
|
|
4731
|
+
type: "resource",
|
|
4732
|
+
fileSrc: opts.assets === "disk" ? encodeURIComponent(absPath) : encodeURI(path),
|
|
4733
|
+
absPath
|
|
4734
|
+
})
|
|
4735
|
+
});
|
|
2711
4736
|
return compileProject2({
|
|
2712
4737
|
entries,
|
|
2713
4738
|
name: opts.name,
|
|
@@ -2754,10 +4779,10 @@ var openBrowser = (url) => {
|
|
|
2754
4779
|
};
|
|
2755
4780
|
var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
|
|
2756
4781
|
const state = randomBytes(16).toString("hex");
|
|
2757
|
-
let
|
|
4782
|
+
let resolve5;
|
|
2758
4783
|
let reject;
|
|
2759
4784
|
const tokenPromise = new Promise((res, rej) => {
|
|
2760
|
-
|
|
4785
|
+
resolve5 = res;
|
|
2761
4786
|
reject = rej;
|
|
2762
4787
|
});
|
|
2763
4788
|
const server = createServer((req, res) => {
|
|
@@ -2775,7 +4800,7 @@ var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser
|
|
|
2775
4800
|
}
|
|
2776
4801
|
res.writeHead(200, { "content-type": "text/html" });
|
|
2777
4802
|
res.end(SUCCESS_HTML);
|
|
2778
|
-
|
|
4803
|
+
resolve5(token);
|
|
2779
4804
|
});
|
|
2780
4805
|
await new Promise((res, rej) => {
|
|
2781
4806
|
server.once("error", rej);
|
|
@@ -2839,6 +4864,9 @@ Behaviour that isn't visible in a single screen — validation, permissions, not
|
|
|
2839
4864
|
cases — and the decisions you've made and why, so they aren't re-litigated later.
|
|
2840
4865
|
`;
|
|
2841
4866
|
var TOKENS_TS = `// Design tokens shared by every screen. Colors are hex strings (the engine parses hex only).
|
|
4867
|
+
// App font: one theme() call here sets the default text font everywhere. font("id") installs a
|
|
4868
|
+
// registry family (vendored at compile time) and returns its name — no loading code needed.
|
|
4869
|
+
// theme({ fontFamily: font("manrope") })
|
|
2842
4870
|
export const colors = {
|
|
2843
4871
|
bg: "#101114",
|
|
2844
4872
|
card: "#1c1e24",
|
|
@@ -2852,6 +4880,57 @@ export const spacing = { xs: 4, sm: 8, md: 16, lg: 24 }
|
|
|
2852
4880
|
|
|
2853
4881
|
export const radius = { sm: 8, md: 12, lg: 20 }
|
|
2854
4882
|
`;
|
|
4883
|
+
var TABS_TS = `// The app's tab bar, declared ONCE — the design canvas discovers it from this file.
|
|
4884
|
+
//
|
|
4885
|
+
// export const mainTabs = defineTabs({
|
|
4886
|
+
// home: { label: "Home", icon: assetIcon("lucide:house") },
|
|
4887
|
+
// news: { label: "News", icon: assetIcon("lucide:newspaper") },
|
|
4888
|
+
// })
|
|
4889
|
+
//
|
|
4890
|
+
// The KEYS are screen ids — each tab's root screen (screens/<key>.ts), in tab order, literals only.
|
|
4891
|
+
// Every screen that shows the bar mounts it with a literal tab id — tab roots AND pushed details
|
|
4892
|
+
// that keep the bar:
|
|
4893
|
+
//
|
|
4894
|
+
// export default () => mainTabs.screen("news", [ ...content ])
|
|
4895
|
+
//
|
|
4896
|
+
// A screen that doesn't mount it is a full-screen push (bar hidden). Never draw meta.json edges
|
|
4897
|
+
// between tab screens — the bar already navigates; the board renders it as a rail + lanes.
|
|
4898
|
+
// Restyle the bar below freely: only the defineTabs({ keys }) declaration and the
|
|
4899
|
+
// .screen("<tab>", …) / .bar("<tab>") call shapes are parsed.
|
|
4900
|
+
|
|
4901
|
+
type TabDef = { label: string, icon?: { svg: string, tintColor: string | null } }
|
|
4902
|
+
|
|
4903
|
+
// Theme-variable driven — theme({ primaryColor, mutedColor, tabbarBg, screenBg }) anywhere
|
|
4904
|
+
// restyles every screen's bar at once; the fallbacks keep an untouched scaffold looking right.
|
|
4905
|
+
const ACTIVE = "var(--primaryColor, #5b8cff)"
|
|
4906
|
+
const INACTIVE = "var(--mutedColor, #8a919e)"
|
|
4907
|
+
const BAR_BG = "var(--tabbarBg, #15171c)"
|
|
4908
|
+
const SCREEN_BG = "var(--screenBg, #101114)"
|
|
4909
|
+
|
|
4910
|
+
export const defineTabs = <T extends Record<string, TabDef>>(tabs: T) => {
|
|
4911
|
+
const ids = Object.keys(tabs) as (keyof T & string)[]
|
|
4912
|
+
|
|
4913
|
+
/** The bar alone — mount it yourself when the screen owns its whole layout. */
|
|
4914
|
+
const bar = (active: keyof T & string) =>
|
|
4915
|
+
UIRow(ids.map((id) => {
|
|
4916
|
+
const color = id === active ? ACTIVE : INACTIVE
|
|
4917
|
+
return UIButton([
|
|
4918
|
+
...(tabs[id].icon ? [UIImage(tabs[id].icon!).style({ width: 22, height: 22, tintColor: color })] : []),
|
|
4919
|
+
UIText(tabs[id].label).style({ fontSize: 10, color }),
|
|
4920
|
+
]).style({ name: \`tab-\${id}\`, flexDirection: "column", gap: 3, flexGrow: 1, flexBasis: 0, pt: 8, pb: 6 })
|
|
4921
|
+
})).style({ bgColor: BAR_BG, pb: "safe-bottom" })
|
|
4922
|
+
|
|
4923
|
+
/** A screen that sits ON the bar (the common case): [ content, bar ] — the same column the real
|
|
4924
|
+
* app composes as [ UIPager, bar ]. */
|
|
4925
|
+
const screen = (active: keyof T & string, children: UINodeChild[]) =>
|
|
4926
|
+
UIScreen([
|
|
4927
|
+
UIColumn(children).style({ flexGrow: 1, p: 16, pt: "safe-top" }),
|
|
4928
|
+
bar(active),
|
|
4929
|
+
]).style({ bgColor: SCREEN_BG })
|
|
4930
|
+
|
|
4931
|
+
return { ids, bar, screen }
|
|
4932
|
+
}
|
|
4933
|
+
`;
|
|
2855
4934
|
var HOME_SCREEN_TS = `import { colors, spacing, radius } from "../shared/tokens"
|
|
2856
4935
|
|
|
2857
4936
|
// Mock data: the domain shown on realistic values. Inline, per screen.
|
|
@@ -2930,6 +5009,12 @@ positions or descriptions.
|
|
|
2930
5009
|
|
|
2931
5010
|
- An edge's optional \`activator\` names the element (its \`name\` style option) that triggers the
|
|
2932
5011
|
transition. Give a \`name\` to interactive elements an edge refers to — nothing else needs names.
|
|
5012
|
+
- **Edges are forward intent only.** Never draw back/dismiss/logout edges — the navigation stack
|
|
5013
|
+
implies return paths. And never draw edges between tab screens — the tab bar already navigates
|
|
5014
|
+
(see "The tab bar" below); the board flags bar-implied edges for deletion.
|
|
5015
|
+
- **An edge from a screen to itself** (\`"from": "home", "to": "home@empty"\`) is a *state link*: the
|
|
5016
|
+
board draws it as a chip on the activator element (clicking it switches the tile's state), never
|
|
5017
|
+
as an arrow. Use it to record "tapping X puts this screen into state Y".
|
|
2933
5018
|
- **Group each screen** you create by its user-facing area — \`"group": "Onboarding"\` — 2–6 screens
|
|
2934
5019
|
per group (Onboarding, Checkout, Settings…). Groups render as labeled regions and \`arrange\` lays
|
|
2935
5020
|
each out as its own sub-flow, so grouping keeps big boards legible. Semantic clustering is your
|
|
@@ -2977,6 +5062,25 @@ keep in the chat.
|
|
|
2977
5062
|
typing a component's return value or a container's children — with no imports. Trust the types;
|
|
2978
5063
|
don't fall back to \`any\`.
|
|
2979
5064
|
|
|
5065
|
+
## The tab bar
|
|
5066
|
+
|
|
5067
|
+
Most apps have one — declare it ONCE, in source, and never draw edges for it:
|
|
5068
|
+
|
|
5069
|
+
- \`shared/tabs.ts\` ships a \`defineTabs\` helper. Declare the bar there:
|
|
5070
|
+
\`export const mainTabs = defineTabs({ home: { label: "Home", icon: assetIcon("lucide:house") }, … })\`
|
|
5071
|
+
— the **keys are screen ids** (each tab's root screen, in tab order), literals only.
|
|
5072
|
+
- Every screen that shows the bar mounts it with a **literal** tab id:
|
|
5073
|
+
\`export default () => mainTabs.screen("news", [ …content ])\` — tab roots AND pushed detail
|
|
5074
|
+
screens that keep the bar (the news lane's article screen mounts \`"news"\` too). A screen that
|
|
5075
|
+
doesn't mount it is a full-screen push (bar hidden), e.g. a photo viewer.
|
|
5076
|
+
- The canvas discovers all of this from the source (like screen states) and draws the bar as a
|
|
5077
|
+
**rail + lane bands** — tab switching needs NO edges, and \`design_state\` serves the discovered
|
|
5078
|
+
\`hubs\`. In the live prototype, tapping a tab button jumps the board to that tab's root screen.
|
|
5079
|
+
- Style through theme variables: the bar reads \`var(--primaryColor)\`, \`var(--mutedColor)\`,
|
|
5080
|
+
\`var(--tabbarBg)\`, \`var(--screenBg)\` (with fallbacks) — one \`theme({ primaryColor: … })\` call
|
|
5081
|
+
restyles it app-wide. You may restyle \`shared/tabs.ts\` itself; keep the \`defineTabs({ … })\`
|
|
5082
|
+
declaration and \`.screen("<tab>", …)\` call shapes intact (the canvas parses them).
|
|
5083
|
+
|
|
2980
5084
|
## Screen states (variants)
|
|
2981
5085
|
|
|
2982
5086
|
A screen often has more than one look — a list vs. its empty state, a form vs. its error state.
|
|
@@ -2984,10 +5088,9 @@ Instead of a second file per variant, make the default export a **function of a
|
|
|
2984
5088
|
type is a string-literal union** — the union IS the state list (no separate schema):
|
|
2985
5089
|
|
|
2986
5090
|
\`\`\`ts
|
|
2987
|
-
export default (state: "default" | "empty" = "default") =>
|
|
5091
|
+
export default (state: "default" | "empty" = "default") => mainTabs.screen("home", [
|
|
2988
5092
|
Header(child),
|
|
2989
5093
|
state === "empty" ? EmptyState("Nothing here yet") : UIScrollable(items.map(Card)),
|
|
2990
|
-
TabBar(tabs, "home"),
|
|
2991
5094
|
])
|
|
2992
5095
|
\`\`\`
|
|
2993
5096
|
|
|
@@ -3005,6 +5108,35 @@ export default (state: "default" | "empty" = "default") => UIScreen([
|
|
|
3005
5108
|
\`"home@empty"\`) — still one node per screen. \`lecodes design check\` errors on an edge pointing at
|
|
3006
5109
|
a state the screen doesn't declare.
|
|
3007
5110
|
|
|
5111
|
+
## Overlays (sheets & dialogs)
|
|
5112
|
+
|
|
5113
|
+
A bottom sheet or confirm dialog is a **\`UIWidget\`**, not an absolutely-positioned child of the
|
|
5114
|
+
screen. Its \`overlayColor\` is the scrim — a real modal layer: it blocks everything beneath it
|
|
5115
|
+
(taps AND the canvas's hover/hints), and a tap on it fires \`onOverlayTap\`. \`show()\` it from the
|
|
5116
|
+
state branch instead of embedding it:
|
|
5117
|
+
|
|
5118
|
+
\`\`\`ts
|
|
5119
|
+
const FilterSheet = () => {
|
|
5120
|
+
const sheet = UIWidget(
|
|
5121
|
+
{ left: 0, right: 0, bottom: 0, p: 16, gap: 8, overlayColor: "#0008" },
|
|
5122
|
+
[ SheetCard([ … ]), PrimaryButton("Apply", { name: "apply" }) ],
|
|
5123
|
+
)
|
|
5124
|
+
return sheet.onOverlayTap(() => sheet.hide())
|
|
5125
|
+
}
|
|
5126
|
+
|
|
5127
|
+
export default (state: "default" | "filters" = "default") => {
|
|
5128
|
+
if (state === "filters") FilterSheet().show()
|
|
5129
|
+
return UIScreen([ … ])
|
|
5130
|
+
}
|
|
5131
|
+
\`\`\`
|
|
5132
|
+
|
|
5133
|
+
- Size the widget box to the **content only** (bottom-anchor a sheet, or center a dialog card
|
|
5134
|
+
inside a full-inset box) — the scrim is the \`overlayColor\`, painted full-screen behind it.
|
|
5135
|
+
- A tap lands **outside the widget box** → \`onOverlayTap\` (dismiss a sheet there). A confirm
|
|
5136
|
+
dialog that demands an explicit choice uses a full-inset box and no \`onOverlayTap\`.
|
|
5137
|
+
- Headless snapshots render widgets as \`overlays\` in the JSON and composite them in the PNG,
|
|
5138
|
+
and the edge \`activator\` names on widget elements (\`"apply"\`, \`"confirm"\`) work as usual.
|
|
5139
|
+
|
|
3008
5140
|
## Icons
|
|
3009
5141
|
|
|
3010
5142
|
Use real icons instead of emoji. \`assetIcon("pack:name")\` is a **compile-time macro**: it vendors
|
|
@@ -3027,6 +5159,28 @@ UIImage(assetIcon("lucide:check")).style({ width: 20, height: 20, tintColor: col
|
|
|
3027
5159
|
(attribution) — **commit these**; they make rebuilds offline. An unknown \`pack:name\` is a clear
|
|
3028
5160
|
compile error.
|
|
3029
5161
|
|
|
5162
|
+
## Fonts
|
|
5163
|
+
|
|
5164
|
+
Real typography sells a design. \`font("id")\` installs a font from the registry AND returns its
|
|
5165
|
+
family name — no loading code, no awaiting, ever:
|
|
5166
|
+
|
|
5167
|
+
\`\`\`ts
|
|
5168
|
+
theme({ fontFamily: font("manrope") }) // app font, in shared/tokens.ts
|
|
5169
|
+
UIText("Hero").style({ fontFamily: font("unbounded"), fontSize: 34 }) // display accents per node
|
|
5170
|
+
font("ibm-plex-sans", { weights: [400, 600] }) // pick exact weights
|
|
5171
|
+
\`\`\`
|
|
5172
|
+
|
|
5173
|
+
- Registry ids (all OFL, cyrillic-capable unless noted):
|
|
5174
|
+
sans \`inter manrope golos-text onest rubik nunito montserrat ibm-plex-sans pt-sans jost exo-2\`;
|
|
5175
|
+
display \`unbounded russo-one oswald comfortaa space-grotesk\` (latin only);
|
|
5176
|
+
serif \`playfair-display lora pt-serif cormorant literata\`;
|
|
5177
|
+
mono \`jetbrains-mono fira-code ibm-plex-mono\`; handwriting \`caveat pacifico\`.
|
|
5178
|
+
- Default weights are ~400/500/700 — install the weights you use (\`fontWeight: 600\` wants a 600
|
|
5179
|
+
face). The id must be a **string literal**; an unknown one is a clear compile error.
|
|
5180
|
+
- Your own file works too: \`font("./fonts/Brand.ttf")\` — family/weight are read from the file.
|
|
5181
|
+
- Vendored faces land in \`assets/fonts/\`, tracked by \`fonts.lock.json\` + \`FONT-NOTICES.md\` —
|
|
5182
|
+
**commit these**; they make rebuilds offline and they're what the platform board renders from.
|
|
5183
|
+
|
|
3030
5184
|
## See what you built
|
|
3031
5185
|
|
|
3032
5186
|
The developer watches the canvas live (screens hot-reload on save). To see a screen yourself:
|
|
@@ -3077,6 +5231,15 @@ var DESIGN_MACROS_DTS = `// Generated by \`lecodes design init\`. Globals the de
|
|
|
3077
5231
|
* string literal (resolved, and the SVG vendored, at compile time).
|
|
3078
5232
|
*/
|
|
3079
5233
|
declare function assetIcon(id: string, opts?: { color?: string }): { readonly svg: string, tintColor: string | null }
|
|
5234
|
+
|
|
5235
|
+
/**
|
|
5236
|
+
* Install a font and get its family name back — using a font IS declaring it. A registry id
|
|
5237
|
+
* (\`font("manrope")\`) vendors faces from the font registry at compile time ({ weights, italic }
|
|
5238
|
+
* narrows the set); a project path (\`font("./fonts/Brand.ttf")\`) reads family/weight/italic from
|
|
5239
|
+
* the file itself. The argument must be a string literal. No awaiting — pass the result straight
|
|
5240
|
+
* to \`theme({ fontFamily: font("manrope") })\` or a \`fontFamily\` style.
|
|
5241
|
+
*/
|
|
5242
|
+
declare function font(id: string, opts?: { weights?: number[], italic?: boolean }): string
|
|
3080
5243
|
`;
|
|
3081
5244
|
var README_HINT = `# design/
|
|
3082
5245
|
|
|
@@ -3101,33 +5264,33 @@ var getContext = (args) => {
|
|
|
3101
5264
|
return {
|
|
3102
5265
|
root,
|
|
3103
5266
|
manifest,
|
|
3104
|
-
name: manifest?.name ??
|
|
5267
|
+
name: manifest?.name ?? basename6(root),
|
|
3105
5268
|
dirName,
|
|
3106
|
-
designDir:
|
|
5269
|
+
designDir: join15(root, dirName),
|
|
3107
5270
|
designKey: "/" + dirName.replace(/\\/g, "/")
|
|
3108
5271
|
};
|
|
3109
5272
|
};
|
|
3110
5273
|
var listScreens = (designDir) => {
|
|
3111
|
-
const dir =
|
|
3112
|
-
if (!
|
|
5274
|
+
const dir = join15(designDir, "screens");
|
|
5275
|
+
if (!existsSync13(dir))
|
|
3113
5276
|
return [];
|
|
3114
|
-
return
|
|
5277
|
+
return readdirSync7(dir).filter((f2) => f2.endsWith(".ts") && !f2.startsWith(".")).map((f2) => f2.slice(0, -3)).sort();
|
|
3115
5278
|
};
|
|
3116
5279
|
var readMetaSizes = (designDir) => {
|
|
3117
5280
|
try {
|
|
3118
|
-
const meta = JSON.parse(
|
|
5281
|
+
const meta = JSON.parse(readFileSync14(join15(designDir, "meta.json"), "utf8"));
|
|
3119
5282
|
return meta?.screens ?? {};
|
|
3120
5283
|
} catch {
|
|
3121
5284
|
return {};
|
|
3122
5285
|
}
|
|
3123
5286
|
};
|
|
3124
5287
|
var writeMcpConfig = (ctx, port) => {
|
|
3125
|
-
const path =
|
|
5288
|
+
const path = join15(ctx.root, ".mcp.json");
|
|
3126
5289
|
const entry = { type: "http", url: `http://localhost:${port}/mcp` };
|
|
3127
5290
|
let config = {};
|
|
3128
|
-
if (
|
|
5291
|
+
if (existsSync13(path)) {
|
|
3129
5292
|
try {
|
|
3130
|
-
config = JSON.parse(
|
|
5293
|
+
config = JSON.parse(readFileSync14(path, "utf8"));
|
|
3131
5294
|
} catch {
|
|
3132
5295
|
warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
|
|
3133
5296
|
return;
|
|
@@ -3140,7 +5303,7 @@ var writeMcpConfig = (ctx, port) => {
|
|
|
3140
5303
|
return;
|
|
3141
5304
|
}
|
|
3142
5305
|
config.mcpServers["lecodes-design"] = entry;
|
|
3143
|
-
|
|
5306
|
+
writeFileSync11(path, JSON.stringify(config, null, 2) + `
|
|
3144
5307
|
`);
|
|
3145
5308
|
log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
|
|
3146
5309
|
};
|
|
@@ -3150,25 +5313,26 @@ var init = async (ctx, args) => {
|
|
|
3150
5313
|
["spec.md", SPEC_MD],
|
|
3151
5314
|
["README.md", README_HINT],
|
|
3152
5315
|
["CLAUDE.md", CLAUDE_MD],
|
|
3153
|
-
[
|
|
3154
|
-
[
|
|
5316
|
+
[join15("shared", "tokens.ts"), TOKENS_TS],
|
|
5317
|
+
[join15("shared", "tabs.ts"), TABS_TS],
|
|
5318
|
+
[join15("screens", "home.ts"), HOME_SCREEN_TS]
|
|
3155
5319
|
];
|
|
3156
|
-
|
|
3157
|
-
|
|
5320
|
+
mkdirSync10(join15(ctx.designDir, "screens"), { recursive: true });
|
|
5321
|
+
mkdirSync10(join15(ctx.designDir, "shared"), { recursive: true });
|
|
3158
5322
|
let created = 0;
|
|
3159
5323
|
for (const [rel, content] of files) {
|
|
3160
|
-
const abs =
|
|
3161
|
-
if (
|
|
5324
|
+
const abs = join15(ctx.designDir, rel);
|
|
5325
|
+
if (existsSync13(abs)) {
|
|
3162
5326
|
note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
|
|
3163
5327
|
continue;
|
|
3164
5328
|
}
|
|
3165
|
-
|
|
5329
|
+
writeFileSync11(abs, content);
|
|
3166
5330
|
log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
|
|
3167
5331
|
created++;
|
|
3168
5332
|
}
|
|
3169
|
-
const ignorePath =
|
|
5333
|
+
const ignorePath = join15(ctx.root, ".lecodesignore");
|
|
3170
5334
|
const ignoreLine = `${ctx.dirName}/.snapshots/`;
|
|
3171
|
-
const current =
|
|
5335
|
+
const current = existsSync13(ignorePath) ? readFileSync14(ignorePath, "utf8") : "";
|
|
3172
5336
|
if (!current.split(/\r?\n/).includes(ignoreLine)) {
|
|
3173
5337
|
appendFileSync(ignorePath, (current && !current.endsWith(`
|
|
3174
5338
|
`) ? `
|
|
@@ -3179,8 +5343,8 @@ var init = async (ctx, args) => {
|
|
|
3179
5343
|
writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
|
|
3180
5344
|
const wroteTypes = materializeTypesLocal(ctx.designDir);
|
|
3181
5345
|
writeTsconfig(ctx.designDir);
|
|
3182
|
-
|
|
3183
|
-
|
|
5346
|
+
mkdirSync10(join15(ctx.designDir, ".lecodes", "types"), { recursive: true });
|
|
5347
|
+
writeFileSync11(join15(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
|
|
3184
5348
|
log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
|
|
3185
5349
|
if (!wroteTypes)
|
|
3186
5350
|
warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
|
|
@@ -3229,16 +5393,16 @@ var snapshot = async (ctx, args) => {
|
|
|
3229
5393
|
killer.unref?.();
|
|
3230
5394
|
const png = flagBool(args, "png") || flagStr(args, "png") !== undefined;
|
|
3231
5395
|
const sizes = readMetaSizes(ctx.designDir);
|
|
3232
|
-
const outDir = flagStr(args, "out-dir") ??
|
|
5396
|
+
const outDir = flagStr(args, "out-dir") ?? join15(ctx.designDir, ".snapshots");
|
|
3233
5397
|
const single = targets.length === 1 && requested.length === 1 && !png && !allStates && !flagStr(args, "out-dir");
|
|
3234
5398
|
const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
|
|
3235
5399
|
if (!single)
|
|
3236
|
-
|
|
5400
|
+
mkdirSync10(outDir, { recursive: true });
|
|
3237
5401
|
for (const id of targets) {
|
|
3238
5402
|
const [width, height] = sizes[id]?.size ?? [390, 844];
|
|
3239
5403
|
let plan = [{ state: undefined, bases: [id] }];
|
|
3240
5404
|
if (discover) {
|
|
3241
|
-
const { states, canonical } = discover(
|
|
5405
|
+
const { states, canonical } = discover(readFileSync14(join15(ctx.designDir, "screens", `${id}.ts`), "utf8"));
|
|
3242
5406
|
if (stateFlag !== undefined) {
|
|
3243
5407
|
if (!states.includes(stateFlag)) {
|
|
3244
5408
|
if (requested.length === 1)
|
|
@@ -3279,18 +5443,18 @@ var snapshot = async (ctx, args) => {
|
|
|
3279
5443
|
for (const w of pngResult.warnings)
|
|
3280
5444
|
warnErr(`${tag}: ${w}`);
|
|
3281
5445
|
for (const base of bases) {
|
|
3282
|
-
|
|
3283
|
-
log(`${c.green("✓")} ${tag} → ${
|
|
5446
|
+
writeFileSync11(join15(outDir, `${base}.json`), result.text);
|
|
5447
|
+
log(`${c.green("✓")} ${tag} → ${join15(outDir, `${base}.json`)}`);
|
|
3284
5448
|
if (pngResult) {
|
|
3285
|
-
|
|
3286
|
-
log(`${c.green("✓")} ${tag} → ${
|
|
5449
|
+
writeFileSync11(join15(outDir, `${base}.png`), pngResult.png);
|
|
5450
|
+
log(`${c.green("✓")} ${tag} → ${join15(outDir, `${base}.png`)}`);
|
|
3287
5451
|
}
|
|
3288
5452
|
}
|
|
3289
5453
|
}
|
|
3290
5454
|
}
|
|
3291
5455
|
};
|
|
3292
5456
|
var serve = async (ctx, args) => {
|
|
3293
|
-
if (!
|
|
5457
|
+
if (!existsSync13(ctx.designDir)) {
|
|
3294
5458
|
throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
|
|
3295
5459
|
}
|
|
3296
5460
|
let designServer;
|
|
@@ -3363,7 +5527,7 @@ var serve = async (ctx, args) => {
|
|
|
3363
5527
|
openBrowser(server.url);
|
|
3364
5528
|
};
|
|
3365
5529
|
var arrange = async (ctx, args) => {
|
|
3366
|
-
if (!
|
|
5530
|
+
if (!existsSync13(ctx.designDir)) {
|
|
3367
5531
|
throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
|
|
3368
5532
|
}
|
|
3369
5533
|
let designServer;
|
|
@@ -3399,7 +5563,7 @@ var share = async (ctx, args) => {
|
|
|
3399
5563
|
note("Turn it off with: lecodes design share --off");
|
|
3400
5564
|
};
|
|
3401
5565
|
var check = async (ctx, args) => {
|
|
3402
|
-
if (!
|
|
5566
|
+
if (!existsSync13(ctx.designDir)) {
|
|
3403
5567
|
throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
|
|
3404
5568
|
}
|
|
3405
5569
|
let designServer;
|
|
@@ -3436,6 +5600,14 @@ var check = async (ctx, args) => {
|
|
|
3436
5600
|
log(` ${c.yellow("!")} ${s.id}: ${s.statesWarning}`);
|
|
3437
5601
|
log("");
|
|
3438
5602
|
}
|
|
5603
|
+
if (state.hubs.length) {
|
|
5604
|
+
log(c.bold("Tab bars"));
|
|
5605
|
+
for (const h of state.hubs) {
|
|
5606
|
+
const mounted = Object.keys(h.lanes).length;
|
|
5607
|
+
log(` ${h.name} (${h.file}): ${h.tabs.join(" · ")} — ${mounted} screen${mounted === 1 ? "" : "s"} on the bar`);
|
|
5608
|
+
}
|
|
5609
|
+
log("");
|
|
5610
|
+
}
|
|
3439
5611
|
const hard = state.warnings.length;
|
|
3440
5612
|
if (hard === 0) {
|
|
3441
5613
|
const n = state.screens.length;
|
|
@@ -3464,15 +5636,15 @@ var design = async (args) => {
|
|
|
3464
5636
|
};
|
|
3465
5637
|
|
|
3466
5638
|
// src/commands/dev.ts
|
|
3467
|
-
import { existsSync as
|
|
5639
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync15, watch, writeFileSync as writeFileSync12 } from "node:fs";
|
|
3468
5640
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
3469
5641
|
import { networkInterfaces } from "node:os";
|
|
3470
|
-
import { basename as
|
|
5642
|
+
import { basename as basename7, join as join17 } from "node:path";
|
|
3471
5643
|
|
|
3472
5644
|
// src/dev/devServer.ts
|
|
3473
5645
|
import { createServer as createServer2 } from "node:http";
|
|
3474
|
-
import { createReadStream, existsSync as
|
|
3475
|
-
import { join as
|
|
5646
|
+
import { createReadStream, existsSync as existsSync14, statSync as statSync3 } from "node:fs";
|
|
5647
|
+
import { join as join16 } from "node:path";
|
|
3476
5648
|
|
|
3477
5649
|
// src/dev/clientTemplates.ts
|
|
3478
5650
|
var DEV_SCREEN_TS = `const title = "LeCodes Dev"
|
|
@@ -3587,9 +5759,9 @@ var renderPrelude = (p) => DEV_PRELUDE_JS.split("__DEV_ORIGIN__").join(p.origin)
|
|
|
3587
5759
|
var renderDevScreen = (name) => DEV_SCREEN_TS.split("__DEV_NAME__").join(JSON.stringify(name));
|
|
3588
5760
|
|
|
3589
5761
|
// src/dev/wsServer.ts
|
|
3590
|
-
import { createHash as
|
|
5762
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3591
5763
|
var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
3592
|
-
var acceptKey = (key) =>
|
|
5764
|
+
var acceptKey = (key) => createHash4("sha1").update(key + WS_GUID).digest("base64");
|
|
3593
5765
|
var encodeTextFrame = (text) => {
|
|
3594
5766
|
const payload = Buffer.from(text, "utf8");
|
|
3595
5767
|
const len = payload.length;
|
|
@@ -3736,14 +5908,14 @@ var sendText = (res, status, type, body) => {
|
|
|
3736
5908
|
res.end(body);
|
|
3737
5909
|
};
|
|
3738
5910
|
var sendJson = (res, status, body) => sendText(res, status, "application/json", JSON.stringify(body));
|
|
3739
|
-
var readBody = (req) => new Promise((
|
|
5911
|
+
var readBody = (req) => new Promise((resolve5) => {
|
|
3740
5912
|
let body = "";
|
|
3741
5913
|
req.on("data", (c2) => {
|
|
3742
5914
|
if (body.length < 256 * 1024)
|
|
3743
5915
|
body += c2.toString("utf8");
|
|
3744
5916
|
});
|
|
3745
|
-
req.on("end", () =>
|
|
3746
|
-
req.on("error", () =>
|
|
5917
|
+
req.on("end", () => resolve5(body));
|
|
5918
|
+
req.on("error", () => resolve5(body));
|
|
3747
5919
|
});
|
|
3748
5920
|
var startDevServer = async (options) => {
|
|
3749
5921
|
const { root, manifest, name, origin, token, entryOverride } = options;
|
|
@@ -3756,7 +5928,15 @@ var startDevServer = async (options) => {
|
|
|
3756
5928
|
const compileOnce = async () => {
|
|
3757
5929
|
try {
|
|
3758
5930
|
const { entries, warnings } = collectLocalEntries(root, manifest, `${base}/assets`);
|
|
3759
|
-
const
|
|
5931
|
+
const fontsHeader = await applyProjectFonts(entries, {
|
|
5932
|
+
mode: "vendor",
|
|
5933
|
+
root,
|
|
5934
|
+
vendorDir: root,
|
|
5935
|
+
vendorPrefix: "",
|
|
5936
|
+
publicUrl: origin,
|
|
5937
|
+
makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: `${base}/assets` + encodeURI(path), absPath })
|
|
5938
|
+
});
|
|
5939
|
+
const js = await compileProject2({ entries, name, publicUrl: origin, entryOverride, header: true, fontsHeader });
|
|
3760
5940
|
const changed = bundleJs === null || stripVolatile(js) !== stripVolatile(bundleJs);
|
|
3761
5941
|
if (changed) {
|
|
3762
5942
|
bundleJs = js;
|
|
@@ -3828,8 +6008,8 @@ var startDevServer = async (options) => {
|
|
|
3828
6008
|
} catch {
|
|
3829
6009
|
return sendText(res, 400, "text/plain", "Bad path");
|
|
3830
6010
|
}
|
|
3831
|
-
const abs =
|
|
3832
|
-
if (!
|
|
6011
|
+
const abs = join16(root, key.slice(1));
|
|
6012
|
+
if (!existsSync14(abs) || !statSync3(abs).isFile())
|
|
3833
6013
|
return sendText(res, 404, "text/plain", "Not found");
|
|
3834
6014
|
res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
|
|
3835
6015
|
createReadStream(abs).pipe(res);
|
|
@@ -3872,11 +6052,11 @@ var startDevServer = async (options) => {
|
|
|
3872
6052
|
if (conn)
|
|
3873
6053
|
clients.add(conn);
|
|
3874
6054
|
});
|
|
3875
|
-
await new Promise((
|
|
6055
|
+
await new Promise((resolve5, reject) => {
|
|
3876
6056
|
server.once("error", reject);
|
|
3877
6057
|
server.listen(options.port, options.bind ?? "0.0.0.0", () => {
|
|
3878
6058
|
server.removeListener("error", reject);
|
|
3879
|
-
|
|
6059
|
+
resolve5();
|
|
3880
6060
|
});
|
|
3881
6061
|
});
|
|
3882
6062
|
const address = server.address();
|
|
@@ -3912,24 +6092,24 @@ var pickLanAddress = () => {
|
|
|
3912
6092
|
return null;
|
|
3913
6093
|
};
|
|
3914
6094
|
var loadDevToken = (root) => {
|
|
3915
|
-
const path =
|
|
6095
|
+
const path = join17(root, LECODES_DIR, "dev.json");
|
|
3916
6096
|
try {
|
|
3917
|
-
const token2 = JSON.parse(
|
|
6097
|
+
const token2 = JSON.parse(readFileSync15(path, "utf8")).token;
|
|
3918
6098
|
if (token2 && /^[a-z0-9]{8,}$/.test(token2))
|
|
3919
6099
|
return token2;
|
|
3920
6100
|
} catch {}
|
|
3921
6101
|
const token = randomBytes2(6).toString("hex");
|
|
3922
|
-
|
|
3923
|
-
|
|
6102
|
+
mkdirSync11(join17(root, LECODES_DIR), { recursive: true });
|
|
6103
|
+
writeFileSync12(path, JSON.stringify({ token }, null, 2) + `
|
|
3924
6104
|
`);
|
|
3925
6105
|
return token;
|
|
3926
6106
|
};
|
|
3927
6107
|
var printQr = async (url) => {
|
|
3928
6108
|
try {
|
|
3929
6109
|
const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
|
|
3930
|
-
await new Promise((
|
|
6110
|
+
await new Promise((resolve5) => qrcode.generate(url, { small: true }, (out) => {
|
|
3931
6111
|
log(out);
|
|
3932
|
-
|
|
6112
|
+
resolve5();
|
|
3933
6113
|
}));
|
|
3934
6114
|
} catch {
|
|
3935
6115
|
note("(couldn't render a QR code — open the URL below on the device)");
|
|
@@ -3945,7 +6125,7 @@ var dev = async (args) => {
|
|
|
3945
6125
|
root = process.cwd();
|
|
3946
6126
|
manifest = null;
|
|
3947
6127
|
}
|
|
3948
|
-
const name = manifest?.name ??
|
|
6128
|
+
const name = manifest?.name ?? basename7(root);
|
|
3949
6129
|
if (typeof globalThis.Bun !== "undefined") {
|
|
3950
6130
|
warnErr("Running under Bun: the websocket reload channel doesn't work here — run `lecodes dev` with Node (or use ?t=poll on the device URL).");
|
|
3951
6131
|
}
|
|
@@ -4032,15 +6212,15 @@ ${result.error}`);
|
|
|
4032
6212
|
await printQr(server.url);
|
|
4033
6213
|
log(`Scan the QR with the LeCodes app, or open: ${c.bold(server.url)}`);
|
|
4034
6214
|
note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`);
|
|
4035
|
-
if (
|
|
6215
|
+
if (existsSync15(join17(root, LECODES_DIR, "manifest.json")))
|
|
4036
6216
|
note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
|
|
4037
6217
|
log("");
|
|
4038
6218
|
log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
|
|
4039
6219
|
};
|
|
4040
6220
|
|
|
4041
6221
|
// src/serverDiff.ts
|
|
4042
|
-
import { readFileSync as
|
|
4043
|
-
import { basename as
|
|
6222
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
6223
|
+
import { basename as basename8 } from "node:path";
|
|
4044
6224
|
var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
|
|
4045
6225
|
var computeServerDiff = (project, scan) => {
|
|
4046
6226
|
const pathById = buildPaths(project.assets);
|
|
@@ -4050,15 +6230,15 @@ var computeServerDiff = (project, scan) => {
|
|
|
4050
6230
|
continue;
|
|
4051
6231
|
serverByPath.set(pathById.get(asset.id), asset);
|
|
4052
6232
|
}
|
|
4053
|
-
const localByPath = new Map(scan.files.map((
|
|
6233
|
+
const localByPath = new Map(scan.files.map((f2) => [f2.path, f2]));
|
|
4054
6234
|
const entries = [];
|
|
4055
6235
|
const paths = new Set([...serverByPath.keys(), ...localByPath.keys()]);
|
|
4056
6236
|
for (const path of [...paths].sort()) {
|
|
4057
6237
|
const server = serverByPath.get(path);
|
|
4058
6238
|
const local = localByPath.get(path);
|
|
4059
6239
|
if (local && !server) {
|
|
4060
|
-
const text = isTextKind(classifyKind(
|
|
4061
|
-
entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ?
|
|
6240
|
+
const text = isTextKind(classifyKind(basename8(path)));
|
|
6241
|
+
entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync16(local.absPath, "utf8") : undefined });
|
|
4062
6242
|
continue;
|
|
4063
6243
|
}
|
|
4064
6244
|
if (server && !local) {
|
|
@@ -4069,12 +6249,12 @@ var computeServerDiff = (project, scan) => {
|
|
|
4069
6249
|
if (server && local) {
|
|
4070
6250
|
if (isTextAsset(server.type)) {
|
|
4071
6251
|
const serverText = server.text ?? "";
|
|
4072
|
-
const localText =
|
|
6252
|
+
const localText = readFileSync16(local.absPath, "utf8");
|
|
4073
6253
|
if (serverText !== localText)
|
|
4074
6254
|
entries.push({ path, status: "modified", binary: false, serverText, localText });
|
|
4075
6255
|
} else {
|
|
4076
6256
|
const serverSize = server.file?.size;
|
|
4077
|
-
const localSize = Buffer.byteLength(
|
|
6257
|
+
const localSize = Buffer.byteLength(readFileSync16(local.absPath));
|
|
4078
6258
|
if (serverSize !== undefined && serverSize !== localSize) {
|
|
4079
6259
|
entries.push({ path, status: "modified", binary: true });
|
|
4080
6260
|
}
|
|
@@ -4227,8 +6407,8 @@ var diff = async (args) => {
|
|
|
4227
6407
|
var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
|
|
4228
6408
|
|
|
4229
6409
|
// src/commands/init.ts
|
|
4230
|
-
import { existsSync as
|
|
4231
|
-
import { basename as
|
|
6410
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync12, writeFileSync as writeFileSync13 } from "node:fs";
|
|
6411
|
+
import { basename as basename9, dirname as dirname10, join as join18, resolve as resolve5 } from "node:path";
|
|
4232
6412
|
var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
|
|
4233
6413
|
// Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
|
|
4234
6414
|
|
|
@@ -4264,24 +6444,55 @@ const screen = UIScreen(
|
|
|
4264
6444
|
|
|
4265
6445
|
screen.open()
|
|
4266
6446
|
`;
|
|
6447
|
+
var readmeTemplate = (name) => `# ${name}
|
|
6448
|
+
|
|
6449
|
+
A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
|
|
6450
|
+
no imports needed. \`main.ts\` is the entry point.
|
|
6451
|
+
|
|
6452
|
+
## Run it
|
|
6453
|
+
|
|
6454
|
+
\`\`\`
|
|
6455
|
+
lecodes dev # live on your phone: scan the QR with the LeCodes app, hot reload
|
|
6456
|
+
lecodes compile -o app.js # bundle to a single self-contained JS file
|
|
6457
|
+
\`\`\`
|
|
6458
|
+
|
|
6459
|
+
## Go native
|
|
6460
|
+
|
|
6461
|
+
\`\`\`
|
|
6462
|
+
lecodes app init ios # generate a native Xcode shell in ios/
|
|
6463
|
+
lecodes app sync # embed the compiled bundle + plugins into it
|
|
6464
|
+
lecodes app open # open it in Xcode, build & run
|
|
6465
|
+
\`\`\`
|
|
6466
|
+
|
|
6467
|
+
## Layout
|
|
6468
|
+
|
|
6469
|
+
- \`main.ts\` — the app. Add more top-level .ts files freely; the largest export-free one
|
|
6470
|
+
is picked as the entry point.
|
|
6471
|
+
- \`assets/\` — images/fonts the app references (created on demand)
|
|
6472
|
+
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit)
|
|
6473
|
+
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
6474
|
+
|
|
6475
|
+
Full command list: \`lecodes --help\`.
|
|
6476
|
+
`;
|
|
4267
6477
|
var init2 = async (args) => {
|
|
4268
6478
|
const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
|
|
4269
|
-
const root =
|
|
4270
|
-
const name =
|
|
4271
|
-
|
|
6479
|
+
const root = resolve5(dirArg);
|
|
6480
|
+
const name = basename9(root);
|
|
6481
|
+
mkdirSync12(root, { recursive: true });
|
|
4272
6482
|
let created = 0;
|
|
4273
6483
|
const write = (rel, content) => {
|
|
4274
|
-
const abs =
|
|
4275
|
-
if (
|
|
6484
|
+
const abs = join18(root, rel);
|
|
6485
|
+
if (existsSync16(abs)) {
|
|
4276
6486
|
note(`kept ${rel}`);
|
|
4277
6487
|
return;
|
|
4278
6488
|
}
|
|
4279
|
-
|
|
4280
|
-
|
|
6489
|
+
mkdirSync12(dirname10(abs), { recursive: true });
|
|
6490
|
+
writeFileSync13(abs, content);
|
|
4281
6491
|
log(`${c.green("created")} ${rel}`);
|
|
4282
6492
|
created++;
|
|
4283
6493
|
};
|
|
4284
6494
|
write("main.ts", STARTER2);
|
|
6495
|
+
write("README.md", readmeTemplate(name));
|
|
4285
6496
|
write(".lecodesignore", DEFAULT_IGNORE_FILE);
|
|
4286
6497
|
const wroteTypes = materializeTypesLocal(root);
|
|
4287
6498
|
writeTsconfig(root);
|
|
@@ -4308,20 +6519,20 @@ var login = async (args) => {
|
|
|
4308
6519
|
};
|
|
4309
6520
|
|
|
4310
6521
|
// src/commands/render.ts
|
|
4311
|
-
import { writeFileSync as
|
|
4312
|
-
import { resolve as
|
|
6522
|
+
import { writeFileSync as writeFileSync14 } from "node:fs";
|
|
6523
|
+
import { resolve as resolve6 } from "node:path";
|
|
4313
6524
|
var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
|
|
4314
6525
|
var DEVICE_SAFE_AREA = {
|
|
4315
|
-
none: [0, 0, 0, 0],
|
|
4316
|
-
iphone: [
|
|
4317
|
-
android: [24, 0,
|
|
6526
|
+
none: { insets: [0, 0, 0, 0], bars: [] },
|
|
6527
|
+
iphone: { insets: [59, 0, 34, 0], bars: [] },
|
|
6528
|
+
android: { insets: [24, 0, 48, 0], bars: ["top", "bottom"] }
|
|
4318
6529
|
};
|
|
4319
6530
|
var resolveSafeArea = (args) => {
|
|
4320
6531
|
const explicit = flagStr(args, "safe-area");
|
|
4321
6532
|
if (explicit) {
|
|
4322
6533
|
const n = explicit.split(",").map((s) => Number(s.trim()));
|
|
4323
6534
|
if (n.length === 4 && n.every((v) => Number.isFinite(v)))
|
|
4324
|
-
return n;
|
|
6535
|
+
return { safeArea: n, safeAreaBars: [] };
|
|
4325
6536
|
throw new CliError(`--safe-area expects "top,right,bottom,left" (4 numbers), got "${explicit}".`);
|
|
4326
6537
|
}
|
|
4327
6538
|
const device = flagStr(args, "device");
|
|
@@ -4329,7 +6540,7 @@ var resolveSafeArea = (args) => {
|
|
|
4329
6540
|
const preset = DEVICE_SAFE_AREA[device];
|
|
4330
6541
|
if (!preset)
|
|
4331
6542
|
throw new CliError(`Unknown --device "${device}". Options: ${Object.keys(DEVICE_SAFE_AREA).join(", ")}.`);
|
|
4332
|
-
return preset;
|
|
6543
|
+
return { safeArea: preset.insets, safeAreaBars: preset.bars };
|
|
4333
6544
|
}
|
|
4334
6545
|
return;
|
|
4335
6546
|
};
|
|
@@ -4352,6 +6563,13 @@ var render = async (args) => {
|
|
|
4352
6563
|
const useLocalAssets = !publicUrlFlag && !flagBool(args, "remote-assets");
|
|
4353
6564
|
const compileEntries = useLocalAssets ? entries.map((e) => e.type === "resource" && e.absPath ? { ...e, fileSrc: encodeURIComponent(e.absPath) } : e) : entries;
|
|
4354
6565
|
const warnings = useLocalAssets ? collectWarnings.filter((w) => !w.includes("No server URL")) : collectWarnings;
|
|
6566
|
+
await applyProjectFonts(compileEntries, useLocalAssets ? {
|
|
6567
|
+
mode: "vendor",
|
|
6568
|
+
root,
|
|
6569
|
+
vendorDir: root,
|
|
6570
|
+
vendorPrefix: "",
|
|
6571
|
+
makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURIComponent(absPath), absPath })
|
|
6572
|
+
} : { mode: "cdn", root });
|
|
4355
6573
|
note("Compiling…");
|
|
4356
6574
|
const js = await compileProject2({
|
|
4357
6575
|
entries: compileEntries,
|
|
@@ -4384,7 +6602,7 @@ var render = async (args) => {
|
|
|
4384
6602
|
localAssets: useLocalAssets,
|
|
4385
6603
|
timeMs: timeStr !== undefined ? Number(timeStr) : undefined,
|
|
4386
6604
|
afterAnimations: !flagBool(args, "no-after-animations"),
|
|
4387
|
-
|
|
6605
|
+
...resolveSafeArea(args),
|
|
4388
6606
|
onConsole
|
|
4389
6607
|
};
|
|
4390
6608
|
const done = (flush) => flush(() => {
|
|
@@ -4405,7 +6623,7 @@ var render = async (args) => {
|
|
|
4405
6623
|
if (isNullSink(pngFlag)) {
|
|
4406
6624
|
note(`Discarded PNG (${pngFlag})`);
|
|
4407
6625
|
} else {
|
|
4408
|
-
|
|
6626
|
+
writeFileSync14(resolve6(pngFlag), png);
|
|
4409
6627
|
note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
|
|
4410
6628
|
}
|
|
4411
6629
|
return done((cb) => cb());
|
|
@@ -4420,7 +6638,7 @@ var render = async (args) => {
|
|
|
4420
6638
|
return done((cb) => cb());
|
|
4421
6639
|
}
|
|
4422
6640
|
if (out) {
|
|
4423
|
-
|
|
6641
|
+
writeFileSync14(resolve6(out), result.text);
|
|
4424
6642
|
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
4425
6643
|
return done((cb) => cb());
|
|
4426
6644
|
}
|
|
@@ -4431,8 +6649,8 @@ var render = async (args) => {
|
|
|
4431
6649
|
};
|
|
4432
6650
|
|
|
4433
6651
|
// src/commands/pull.ts
|
|
4434
|
-
import { existsSync as
|
|
4435
|
-
import { dirname as
|
|
6652
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync13, rmSync as rmSync2, writeFileSync as writeFileSync15 } from "node:fs";
|
|
6653
|
+
import { dirname as dirname11, join as join19 } from "node:path";
|
|
4436
6654
|
var pull = async (args) => {
|
|
4437
6655
|
const root = findProjectRoot(process.cwd());
|
|
4438
6656
|
const manifest = readManifest(root);
|
|
@@ -4452,9 +6670,9 @@ var pull = async (args) => {
|
|
|
4452
6670
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
|
|
4453
6671
|
for (const path of Object.keys(manifest.files)) {
|
|
4454
6672
|
if (!remotePaths.has(path)) {
|
|
4455
|
-
const abs =
|
|
4456
|
-
if (
|
|
4457
|
-
|
|
6673
|
+
const abs = join19(root, ...path.replace(/^\//, "").split("/"));
|
|
6674
|
+
if (existsSync17(abs))
|
|
6675
|
+
rmSync2(abs);
|
|
4458
6676
|
}
|
|
4459
6677
|
}
|
|
4460
6678
|
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
@@ -4491,18 +6709,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
4491
6709
|
const files = { ...manifest.files };
|
|
4492
6710
|
for (const asset of targets) {
|
|
4493
6711
|
const key = pathById.get(asset.id);
|
|
4494
|
-
const abs =
|
|
4495
|
-
|
|
6712
|
+
const abs = join19(root, ...key.replace(/^\//, "").split("/"));
|
|
6713
|
+
mkdirSync13(dirname11(abs), { recursive: true });
|
|
4496
6714
|
let sha;
|
|
4497
6715
|
if (asset.type === "file") {
|
|
4498
6716
|
if (!asset.file?.src)
|
|
4499
6717
|
continue;
|
|
4500
6718
|
const bytes = await downloadBlob(apiUrl, asset.file.src);
|
|
4501
|
-
|
|
6719
|
+
writeFileSync15(abs, bytes);
|
|
4502
6720
|
sha = sha256(bytes);
|
|
4503
6721
|
} else {
|
|
4504
6722
|
const text = asset.text ?? "";
|
|
4505
|
-
|
|
6723
|
+
writeFileSync15(abs, text);
|
|
4506
6724
|
sha = sha256(text);
|
|
4507
6725
|
}
|
|
4508
6726
|
files[key] = { id: asset.id, type: kindOf2(asset.type), sha };
|
|
@@ -4514,8 +6732,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
4514
6732
|
};
|
|
4515
6733
|
|
|
4516
6734
|
// src/commands/push.ts
|
|
4517
|
-
import { basename as
|
|
4518
|
-
import { readFileSync as
|
|
6735
|
+
import { basename as basename10 } from "node:path";
|
|
6736
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
4519
6737
|
var push = async (args) => {
|
|
4520
6738
|
const root = findProjectRoot(process.cwd());
|
|
4521
6739
|
const manifest = readManifest(root);
|
|
@@ -4569,14 +6787,14 @@ var push = async (args) => {
|
|
|
4569
6787
|
let uploaded = 0;
|
|
4570
6788
|
for (const file of scan.files) {
|
|
4571
6789
|
const source = sourceFor(file.path);
|
|
4572
|
-
const kind = source?.type ?? classifyKind(
|
|
6790
|
+
const kind = source?.type ?? classifyKind(basename10(file.path));
|
|
4573
6791
|
const entry = { path: file.path, type: kind };
|
|
4574
6792
|
if (source)
|
|
4575
6793
|
entry.assetId = source.id;
|
|
4576
6794
|
if (isTextKind(kind)) {
|
|
4577
|
-
entry.text =
|
|
6795
|
+
entry.text = readFileSync17(file.absPath, "utf8");
|
|
4578
6796
|
} else if (!source || source.sha !== file.sha) {
|
|
4579
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid,
|
|
6797
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename10(file.path), readFileSync17(file.absPath));
|
|
4580
6798
|
entry.blobFileId = blob.id;
|
|
4581
6799
|
uploaded++;
|
|
4582
6800
|
}
|
|
@@ -4592,7 +6810,7 @@ var push = async (args) => {
|
|
|
4592
6810
|
reportPublish(res);
|
|
4593
6811
|
return;
|
|
4594
6812
|
}
|
|
4595
|
-
const shaByPath = new Map(scan.files.map((
|
|
6813
|
+
const shaByPath = new Map(scan.files.map((f2) => [f2.path, f2.sha]));
|
|
4596
6814
|
const newFiles = {};
|
|
4597
6815
|
for (const node of res.nodes) {
|
|
4598
6816
|
newFiles[node.path] = { id: node.id, type: node.type, sha: shaByPath.get(node.path) ?? "" };
|
|
@@ -4629,22 +6847,22 @@ var reportPublish = (res) => {
|
|
|
4629
6847
|
};
|
|
4630
6848
|
|
|
4631
6849
|
// src/commands/scene.ts
|
|
4632
|
-
import { existsSync as
|
|
4633
|
-
import { basename as
|
|
6850
|
+
import { existsSync as existsSync19, readFileSync as readFileSync19, readdirSync as readdirSync8, statSync as statSync4, writeFileSync as writeFileSync16 } from "node:fs";
|
|
6851
|
+
import { basename as basename11, dirname as dirname12, join as join21, resolve as resolve7 } from "node:path";
|
|
4634
6852
|
|
|
4635
6853
|
// src/compile/sceneCompile.ts
|
|
4636
|
-
import { existsSync as
|
|
4637
|
-
import { join as
|
|
6854
|
+
import { existsSync as existsSync18, readFileSync as readFileSync18 } from "node:fs";
|
|
6855
|
+
import { join as join20 } from "node:path";
|
|
4638
6856
|
var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
|
|
4639
6857
|
var rel = (key) => key.replace(/^\//, "");
|
|
4640
6858
|
var resolveHarness = (loadSceneHarness2) => {
|
|
4641
|
-
const inRepo =
|
|
6859
|
+
const inRepo = join20(distRoot, "..", "projects", "src", "scene-editor");
|
|
4642
6860
|
try {
|
|
4643
|
-
return loadSceneHarness2(
|
|
6861
|
+
return loadSceneHarness2(existsSync18(inRepo) ? inRepo : undefined);
|
|
4644
6862
|
} catch {
|
|
4645
|
-
const vendored =
|
|
4646
|
-
if (
|
|
4647
|
-
return JSON.parse(
|
|
6863
|
+
const vendored = join20(distRoot, "runtime", "scene-harness.json");
|
|
6864
|
+
if (existsSync18(vendored))
|
|
6865
|
+
return JSON.parse(readFileSync18(vendored, "utf8"));
|
|
4648
6866
|
throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
|
|
4649
6867
|
}
|
|
4650
6868
|
};
|
|
@@ -4656,7 +6874,7 @@ var compileSceneEditor = async (opts) => {
|
|
|
4656
6874
|
for (const file of scan.files) {
|
|
4657
6875
|
const path = "./" + rel(file.path);
|
|
4658
6876
|
if (TEXT_EXT.test(path)) {
|
|
4659
|
-
entries.push({ path, type: "text", text:
|
|
6877
|
+
entries.push({ path, type: "text", text: readFileSync18(file.absPath, "utf8") });
|
|
4660
6878
|
} else {
|
|
4661
6879
|
entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path) });
|
|
4662
6880
|
}
|
|
@@ -4696,24 +6914,24 @@ var getContext2 = (args) => {
|
|
|
4696
6914
|
manifest = null;
|
|
4697
6915
|
const pathArg = args._[0];
|
|
4698
6916
|
if (pathArg) {
|
|
4699
|
-
const abs =
|
|
4700
|
-
if (
|
|
6917
|
+
const abs = resolve7(process.cwd(), pathArg);
|
|
6918
|
+
if (existsSync19(abs) && statSync4(abs).isDirectory())
|
|
4701
6919
|
root = abs;
|
|
4702
|
-
else if (abs.endsWith(".scene.ts") &&
|
|
4703
|
-
root =
|
|
6920
|
+
else if (abs.endsWith(".scene.ts") && existsSync19(abs))
|
|
6921
|
+
root = dirname12(abs);
|
|
4704
6922
|
else
|
|
4705
6923
|
throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
|
|
4706
6924
|
}
|
|
4707
6925
|
}
|
|
4708
|
-
return { root, manifest, name: manifest?.name ??
|
|
6926
|
+
return { root, manifest, name: manifest?.name ?? basename11(root) };
|
|
4709
6927
|
};
|
|
4710
6928
|
var writeMcpConfig2 = (root, port) => {
|
|
4711
|
-
const path =
|
|
6929
|
+
const path = join21(root, ".mcp.json");
|
|
4712
6930
|
const entry = { type: "http", url: `http://localhost:${port}/mcp` };
|
|
4713
6931
|
let config = {};
|
|
4714
|
-
if (
|
|
6932
|
+
if (existsSync19(path)) {
|
|
4715
6933
|
try {
|
|
4716
|
-
config = JSON.parse(
|
|
6934
|
+
config = JSON.parse(readFileSync19(path, "utf8"));
|
|
4717
6935
|
} catch {
|
|
4718
6936
|
warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
|
|
4719
6937
|
return;
|
|
@@ -4726,17 +6944,17 @@ var writeMcpConfig2 = (root, port) => {
|
|
|
4726
6944
|
return;
|
|
4727
6945
|
}
|
|
4728
6946
|
config.mcpServers["lecodes-scene"] = entry;
|
|
4729
|
-
|
|
6947
|
+
writeFileSync16(path, JSON.stringify(config, null, 2) + `
|
|
4730
6948
|
`);
|
|
4731
6949
|
note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
|
|
4732
6950
|
};
|
|
4733
6951
|
var countScenes = (dir) => {
|
|
4734
6952
|
let n = 0;
|
|
4735
|
-
for (const name of
|
|
6953
|
+
for (const name of readdirSync8(dir)) {
|
|
4736
6954
|
if (name === "node_modules" || name.startsWith("."))
|
|
4737
6955
|
continue;
|
|
4738
|
-
const full =
|
|
4739
|
-
if (
|
|
6956
|
+
const full = join21(dir, name);
|
|
6957
|
+
if (statSync4(full).isDirectory())
|
|
4740
6958
|
n += countScenes(full);
|
|
4741
6959
|
else if (name.endsWith(".scene.ts"))
|
|
4742
6960
|
n++;
|
|
@@ -4788,12 +7006,12 @@ var status = async (_args) => {
|
|
|
4788
7006
|
info(c.dim("Working tree clean — nothing to push.") + ignoredNote);
|
|
4789
7007
|
return;
|
|
4790
7008
|
}
|
|
4791
|
-
for (const
|
|
4792
|
-
if (!diff2.moves.has(
|
|
4793
|
-
log(c.green(` added ${
|
|
7009
|
+
for (const f2 of diff2.added) {
|
|
7010
|
+
if (!diff2.moves.has(f2.path))
|
|
7011
|
+
log(c.green(` added ${f2.path}`));
|
|
4794
7012
|
}
|
|
4795
|
-
for (const
|
|
4796
|
-
log(c.yellow(` modified ${
|
|
7013
|
+
for (const f2 of diff2.modified)
|
|
7014
|
+
log(c.yellow(` modified ${f2.path}`));
|
|
4797
7015
|
for (const [to, from] of diff2.moves)
|
|
4798
7016
|
log(c.cyan(` moved ${from} → ${to}`));
|
|
4799
7017
|
const movedFrom = new Set(diff2.moves.values());
|
|
@@ -4806,21 +7024,22 @@ var status = async (_args) => {
|
|
|
4806
7024
|
};
|
|
4807
7025
|
|
|
4808
7026
|
// src/index.ts
|
|
4809
|
-
import { existsSync as
|
|
4810
|
-
import {
|
|
7027
|
+
import { existsSync as existsSync21, readFileSync as readFileSync21, realpathSync as realpathSync2 } from "node:fs";
|
|
7028
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
7029
|
+
import { delimiter as delimiter2, dirname as dirname14, join as join23 } from "node:path";
|
|
4811
7030
|
|
|
4812
7031
|
// src/commands/install.ts
|
|
4813
|
-
import { execFileSync } from "node:child_process";
|
|
4814
|
-
import { appendFileSync as appendFileSync2, chmodSync, cpSync, existsSync as
|
|
4815
|
-
import { homedir as
|
|
4816
|
-
import { basename as
|
|
7032
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
7033
|
+
import { appendFileSync as appendFileSync2, chmodSync, cpSync as cpSync2, existsSync as existsSync20, mkdirSync as mkdirSync14, readFileSync as readFileSync20, realpathSync, rmSync as rmSync3, symlinkSync } from "node:fs";
|
|
7034
|
+
import { homedir as homedir3 } from "node:os";
|
|
7035
|
+
import { basename as basename12, delimiter, dirname as dirname13, join as join22, resolve as resolve8, sep } from "node:path";
|
|
4817
7036
|
var isWin = process.platform === "win32";
|
|
4818
|
-
var HOME =
|
|
7037
|
+
var HOME = homedir3();
|
|
4819
7038
|
var safeRealpath = (p) => {
|
|
4820
7039
|
try {
|
|
4821
7040
|
return realpathSync(p);
|
|
4822
7041
|
} catch {
|
|
4823
|
-
return
|
|
7042
|
+
return resolve8(p);
|
|
4824
7043
|
}
|
|
4825
7044
|
};
|
|
4826
7045
|
var onPath = (dir) => {
|
|
@@ -4836,25 +7055,25 @@ var onPath = (dir) => {
|
|
|
4836
7055
|
});
|
|
4837
7056
|
};
|
|
4838
7057
|
var install = async (args) => {
|
|
4839
|
-
const bundle =
|
|
4840
|
-
const exeName =
|
|
7058
|
+
const bundle = dirname13(process.execPath);
|
|
7059
|
+
const exeName = basename12(process.execPath);
|
|
4841
7060
|
const chiselName = isWin ? "chisel.exe" : "chisel";
|
|
4842
|
-
if (!
|
|
7061
|
+
if (!existsSync20(join22(bundle, "runtime")) || !existsSync20(join22(bundle, chiselName))) {
|
|
4843
7062
|
throw new CliError("`lecodes install` only works from the packaged release build (the archive containing lecodes + " + "chisel + runtime/). You appear to be running from source or an npm install — install globally " + "with your package manager there instead.");
|
|
4844
7063
|
}
|
|
4845
|
-
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((
|
|
7064
|
+
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f2) => existsSync20(join22(bundle, f2)));
|
|
4846
7065
|
if (isWin)
|
|
4847
7066
|
return installWindows(args, bundle, companions);
|
|
4848
7067
|
return installUnix(args, bundle, companions);
|
|
4849
7068
|
};
|
|
4850
7069
|
var copyBundle = (bundle, target, companions) => {
|
|
4851
|
-
|
|
7070
|
+
mkdirSync14(target, { recursive: true });
|
|
4852
7071
|
if (safeRealpath(target) === safeRealpath(bundle))
|
|
4853
7072
|
return;
|
|
4854
|
-
for (const
|
|
4855
|
-
const dest =
|
|
4856
|
-
|
|
4857
|
-
|
|
7073
|
+
for (const f2 of companions) {
|
|
7074
|
+
const dest = join22(target, f2);
|
|
7075
|
+
rmSync3(dest, { recursive: true, force: true });
|
|
7076
|
+
cpSync2(join22(bundle, f2), dest, { recursive: true });
|
|
4858
7077
|
}
|
|
4859
7078
|
};
|
|
4860
7079
|
var permError = (e, dir, system) => {
|
|
@@ -4866,41 +7085,41 @@ var permError = (e, dir, system) => {
|
|
|
4866
7085
|
};
|
|
4867
7086
|
var installUnix = (args, bundle, companions) => {
|
|
4868
7087
|
const system = flagBool(args, "system");
|
|
4869
|
-
const libDir =
|
|
4870
|
-
const binDir =
|
|
4871
|
-
const link =
|
|
7088
|
+
const libDir = resolve8(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join22(HOME, ".local", "lib", "lecodes-cli")));
|
|
7089
|
+
const binDir = resolve8(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join22(HOME, ".local", "bin")));
|
|
7090
|
+
const link = join22(binDir, "lecodes");
|
|
4872
7091
|
try {
|
|
4873
7092
|
copyBundle(bundle, libDir, companions);
|
|
4874
|
-
chmodSync(
|
|
4875
|
-
chmodSync(
|
|
7093
|
+
chmodSync(join22(libDir, "lecodes"), 493);
|
|
7094
|
+
chmodSync(join22(libDir, "chisel"), 493);
|
|
4876
7095
|
} catch (e) {
|
|
4877
7096
|
throw permError(e, libDir, system);
|
|
4878
7097
|
}
|
|
4879
7098
|
try {
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
symlinkSync(
|
|
7099
|
+
mkdirSync14(binDir, { recursive: true });
|
|
7100
|
+
rmSync3(link, { force: true });
|
|
7101
|
+
symlinkSync(join22(libDir, "lecodes"), link);
|
|
4883
7102
|
} catch (e) {
|
|
4884
7103
|
throw permError(e, binDir, system);
|
|
4885
7104
|
}
|
|
4886
7105
|
success(`Installed to ${c.bold(libDir)}`);
|
|
4887
|
-
log(` ${c.dim(link + " → " +
|
|
7106
|
+
log(` ${c.dim(link + " → " + join22(libDir, "lecodes"))}`);
|
|
4888
7107
|
finishPathUnix(args, binDir);
|
|
4889
7108
|
};
|
|
4890
7109
|
var shellRc = () => {
|
|
4891
|
-
const shell =
|
|
7110
|
+
const shell = basename12(process.env.SHELL ?? "");
|
|
4892
7111
|
if (shell === "zsh")
|
|
4893
|
-
return { file:
|
|
7112
|
+
return { file: join22(HOME, ".zshrc"), name: "~/.zshrc" };
|
|
4894
7113
|
if (shell === "bash")
|
|
4895
|
-
return { file:
|
|
4896
|
-
return { file:
|
|
7114
|
+
return { file: join22(HOME, ".bashrc"), name: "~/.bashrc" };
|
|
7115
|
+
return { file: join22(HOME, ".profile"), name: "~/.profile" };
|
|
4897
7116
|
};
|
|
4898
7117
|
var PATH_MARKER = "# Added by `lecodes install`";
|
|
4899
7118
|
var appendPathToShellRc = (binDir) => {
|
|
4900
7119
|
const { file, name } = shellRc();
|
|
4901
7120
|
const line = `export PATH="${binDir}:$PATH"`;
|
|
4902
7121
|
try {
|
|
4903
|
-
const existing =
|
|
7122
|
+
const existing = existsSync20(file) ? readFileSync20(file, "utf8") : "";
|
|
4904
7123
|
if (existing.includes(line))
|
|
4905
7124
|
return name;
|
|
4906
7125
|
appendFileSync2(file, `${existing && !existing.endsWith(`
|
|
@@ -4938,7 +7157,7 @@ var finishPathUnix = (args, binDir) => {
|
|
|
4938
7157
|
log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
|
|
4939
7158
|
};
|
|
4940
7159
|
var installWindows = (args, bundle, companions) => {
|
|
4941
|
-
const target =
|
|
7160
|
+
const target = resolve8(flagStr(args, "dir") ?? join22(process.env.LOCALAPPDATA ?? join22(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
|
|
4942
7161
|
try {
|
|
4943
7162
|
copyBundle(bundle, target, companions);
|
|
4944
7163
|
} catch (e) {
|
|
@@ -4966,7 +7185,7 @@ var installWindows = (args, bundle, companions) => {
|
|
|
4966
7185
|
var addToUserPathWindows = (dir) => {
|
|
4967
7186
|
const script = "$d = $env:LECODES_INSTALL_DIR;" + "$p = [Environment]::GetEnvironmentVariable('Path','User'); if (-not $p) { $p = '' };" + "$parts = @($p.Split(';') | Where-Object { $_ -ne '' });" + "if ($parts -notcontains $d) { [Environment]::SetEnvironmentVariable('Path', (@($parts + $d) -join ';'), 'User') }";
|
|
4968
7187
|
try {
|
|
4969
|
-
|
|
7188
|
+
execFileSync2("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
|
|
4970
7189
|
return true;
|
|
4971
7190
|
} catch {
|
|
4972
7191
|
return false;
|
|
@@ -4975,9 +7194,9 @@ var addToUserPathWindows = (dir) => {
|
|
|
4975
7194
|
|
|
4976
7195
|
// src/index.ts
|
|
4977
7196
|
var STANDALONE = (() => {
|
|
4978
|
-
const dir =
|
|
7197
|
+
const dir = dirname14(process.execPath);
|
|
4979
7198
|
const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
|
|
4980
|
-
return
|
|
7199
|
+
return existsSync21(join23(dir, "runtime")) && existsSync21(join23(dir, chisel));
|
|
4981
7200
|
})();
|
|
4982
7201
|
var INSTALLED = STANDALONE && (() => {
|
|
4983
7202
|
const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
|
|
@@ -4992,8 +7211,8 @@ var INSTALLED = STANDALONE && (() => {
|
|
|
4992
7211
|
if (!d)
|
|
4993
7212
|
return false;
|
|
4994
7213
|
try {
|
|
4995
|
-
const cand =
|
|
4996
|
-
return
|
|
7214
|
+
const cand = join23(d, exe);
|
|
7215
|
+
return existsSync21(cand) && realpathSync2(cand) === self;
|
|
4997
7216
|
} catch {
|
|
4998
7217
|
return false;
|
|
4999
7218
|
}
|
|
@@ -5080,12 +7299,73 @@ ${c.bold("Commands:")}
|
|
|
5080
7299
|
--port <n> editor-server port (default 4488)
|
|
5081
7300
|
--no-open don't open the browser
|
|
5082
7301
|
--no-mcp don't register the MCP server in .mcp.json
|
|
7302
|
+
app init ios Generate a native Xcode shell in <project>/ios/ (offline-first:
|
|
7303
|
+
runs the bundled app.js; ios/ is never pushed to LeCodes)
|
|
7304
|
+
--name <s> display name (default: app.json / folder name)
|
|
7305
|
+
--bundle-id <id> iOS bundle identifier
|
|
7306
|
+
--team <id> Apple development team id
|
|
7307
|
+
--variant <v> SDK variant: auto (default — sync picks the
|
|
7308
|
+
smallest of core/2d/3d/full covering what
|
|
7309
|
+
the bundle uses), or one of those to pin
|
|
7310
|
+
app sync Make the shell match the project: compile the bundle + embed
|
|
7311
|
+
its assets into ios/App/Resources/ (offline by default, also
|
|
7312
|
+
for server projects; app.json "resources": {"remote": [globs]}
|
|
7313
|
+
keeps heavy assets streaming from the server), pick the SDK
|
|
7314
|
+
variant from the bundle's engine use, vendor app.json
|
|
7315
|
+
plugins (pinned in plugins.lock.json), regenerate the
|
|
7316
|
+
CLI-owned shell files
|
|
7317
|
+
app plugin <list|add|remove> Manage native plugins in app.json (then run 'lecodes app sync')
|
|
7318
|
+
add takes a known id (camera, qr-scanner) or github:owner/repo[@tag]
|
|
7319
|
+
app open [ios] Open the native shell in its IDE (Xcode; Android Studio later)
|
|
7320
|
+
app doctor Preflight checks for building/uploading the shell
|
|
7321
|
+
app build ios Sync, archive and export the .ipa (automatic signing)
|
|
7322
|
+
--configuration <c> build configuration (default Release)
|
|
7323
|
+
--method <m> export method (default app-store-connect)
|
|
7324
|
+
--no-export stop after the archive
|
|
7325
|
+
--no-sync skip the lecodes app sync step
|
|
7326
|
+
app upload ios Upload the archive to App Store Connect (then bumps buildNumber)
|
|
7327
|
+
--key-path <p8> --key-id <id> --issuer-id <id>
|
|
7328
|
+
ASC API key; stored in ~/.lecodes/asc.json
|
|
5083
7329
|
|
|
5084
7330
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
5085
|
-
var commands = { init: init2, login, create, clone, status, diff, pull, push, compile: compile2, preview: previewLegacy, render, design, scene, dev };
|
|
7331
|
+
var commands = { init: init2, login, create, clone, status, diff, pull, push, compile: compile2, preview: previewLegacy, render, design, scene, dev, app };
|
|
5086
7332
|
if (STANDALONE)
|
|
5087
7333
|
commands.install = install;
|
|
5088
7334
|
var readVersion = () => package_default.version ?? "unknown";
|
|
7335
|
+
var OPTIONAL_PEERS = [
|
|
7336
|
+
["lecodes-renderer", "lecodes-renderer/headless"],
|
|
7337
|
+
["lecodes-design", "lecodes-design/server"],
|
|
7338
|
+
["lecodes-3d-editor", "lecodes-3d-editor/server"]
|
|
7339
|
+
];
|
|
7340
|
+
var peerVersion = (name, probe) => {
|
|
7341
|
+
try {
|
|
7342
|
+
const require2 = createRequire4(import.meta.url);
|
|
7343
|
+
let dir = dirname14(require2.resolve(probe));
|
|
7344
|
+
for (let depth = 0;depth < 16; depth++) {
|
|
7345
|
+
const manifest = join23(dir, "package.json");
|
|
7346
|
+
if (existsSync21(manifest)) {
|
|
7347
|
+
const parsed = JSON.parse(readFileSync21(manifest, "utf8"));
|
|
7348
|
+
if (parsed.name === name)
|
|
7349
|
+
return parsed.version ?? "unknown";
|
|
7350
|
+
}
|
|
7351
|
+
const parent = dirname14(dir);
|
|
7352
|
+
if (parent === dir)
|
|
7353
|
+
break;
|
|
7354
|
+
dir = parent;
|
|
7355
|
+
}
|
|
7356
|
+
} catch {}
|
|
7357
|
+
return null;
|
|
7358
|
+
};
|
|
7359
|
+
var printVersion = () => {
|
|
7360
|
+
const rows = [["lecodes-cli", readVersion()]];
|
|
7361
|
+
for (const [name, probe] of OPTIONAL_PEERS) {
|
|
7362
|
+
const version = peerVersion(name, probe);
|
|
7363
|
+
rows.push([name, version ?? c.dim("not installed")]);
|
|
7364
|
+
}
|
|
7365
|
+
const width = Math.max(...rows.map(([label]) => label.length));
|
|
7366
|
+
for (const [label, value] of rows)
|
|
7367
|
+
log(`${label.padEnd(width)} ${value}`);
|
|
7368
|
+
};
|
|
5089
7369
|
var main = async () => {
|
|
5090
7370
|
const argv = process.argv.slice(2);
|
|
5091
7371
|
const command = argv[0];
|
|
@@ -5094,7 +7374,7 @@ var main = async () => {
|
|
|
5094
7374
|
return;
|
|
5095
7375
|
}
|
|
5096
7376
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
5097
|
-
|
|
7377
|
+
printVersion();
|
|
5098
7378
|
return;
|
|
5099
7379
|
}
|
|
5100
7380
|
const run = commands[command];
|