lecodes-cli 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2909 -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.0",
|
|
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,1630 @@ 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
|
-
const body = translate(p);
|
|
1929
|
-
const re = anchored ? new RegExp("^/" + body + "(/.*)?$") : new RegExp("(^|/)" + body + "(/.*)?$");
|
|
1930
|
-
return { re, negate, dirOnly };
|
|
1931
|
-
};
|
|
1932
|
-
var buildMatcher = (rules) => (key, isDir) => {
|
|
1933
|
-
let ignored = false;
|
|
1934
|
-
for (const r of rules) {
|
|
1935
|
-
if (r.dirOnly && !isDir)
|
|
1936
|
-
continue;
|
|
1937
|
-
if (r.re.test(key))
|
|
1938
|
-
ignored = !r.negate;
|
|
1939
2722
|
}
|
|
1940
|
-
return ignored;
|
|
1941
2723
|
};
|
|
1942
|
-
var
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
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)
|
|
2748
|
+
continue;
|
|
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);
|
|
1949
2754
|
}
|
|
1950
|
-
|
|
1951
|
-
return
|
|
2755
|
+
lines.push("");
|
|
2756
|
+
return lines.join(`
|
|
2757
|
+
`);
|
|
1952
2758
|
};
|
|
1953
|
-
var
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
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));
|
|
1957
2786
|
};
|
|
1958
2787
|
|
|
1959
|
-
// src/commands/
|
|
1960
|
-
var
|
|
1961
|
-
|
|
1962
|
-
throw new CliError("Usage: lecodes clone <project-uuid|url> [directory]");
|
|
1963
|
-
if (!input.includes("/"))
|
|
1964
|
-
return input;
|
|
1965
|
-
const clean = input.split(/[?#]/)[0].replace(/\/+$/, "");
|
|
1966
|
-
const match = clean.match(/\/(?:projects|view)\/([^/]+)/);
|
|
1967
|
-
return match ? match[1] : clean.slice(clean.lastIndexOf("/") + 1);
|
|
2788
|
+
// src/commands/compile.ts
|
|
2789
|
+
var compile2 = async (args) => {
|
|
2790
|
+
await compileImpl(args);
|
|
1968
2791
|
};
|
|
1969
|
-
var
|
|
1970
|
-
var
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
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;
|
|
1980
2802
|
}
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
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
|
+
}
|
|
1989
2839
|
}
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
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
|
+
}));
|
|
2881
|
+
};
|
|
2882
|
+
var previewLegacy = async (args) => {
|
|
2883
|
+
warnErr("'lecodes preview' is now 'lecodes compile' — the old name still works.");
|
|
2884
|
+
await compile2(args);
|
|
1995
2885
|
};
|
|
1996
2886
|
|
|
1997
|
-
// src/commands/
|
|
1998
|
-
|
|
1999
|
-
|
|
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
|
+
`;
|
|
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
|
+
//
|
|
2000
3277
|
|
|
2001
|
-
|
|
2002
|
-
import { readFileSync as readFileSync6 } from "node:fs";
|
|
2003
|
-
import { basename } from "node:path";
|
|
3278
|
+
import UIKit
|
|
2004
3279
|
|
|
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
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
var
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
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)
|
|
2070
3348
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
ignored.push(key);
|
|
2079
|
-
continue;
|
|
3349
|
+
|
|
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
|
|
2080
3356
|
}
|
|
2081
|
-
|
|
2082
|
-
}
|
|
3357
|
+
engine.run(code)
|
|
2083
3358
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
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
|
+
`;
|
|
2087
3392
|
};
|
|
2088
|
-
var
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
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
|
|
2099
3423
|
}
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
moves.set(file.path, from);
|
|
2107
|
-
claimed.add(from);
|
|
3424
|
+
}
|
|
3425
|
+
`;
|
|
3426
|
+
var ACCENT_COLOR_JSON = `{
|
|
3427
|
+
"colors" : [
|
|
3428
|
+
{
|
|
3429
|
+
"idiom" : "universal"
|
|
2108
3430
|
}
|
|
3431
|
+
],
|
|
3432
|
+
"info" : {
|
|
3433
|
+
"author" : "xcode",
|
|
3434
|
+
"version" : 1
|
|
2109
3435
|
}
|
|
2110
|
-
|
|
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 (app.json ios.variant)
|
|
3488
|
+
// and one vendored target per installed plugin (see plugins.lock.json).
|
|
3489
|
+
|
|
3490
|
+
import PackageDescription
|
|
3491
|
+
|
|
3492
|
+
let package = Package(
|
|
3493
|
+
name: "LeCodesRuntime",
|
|
3494
|
+
platforms: [.iOS(.v15)],
|
|
3495
|
+
products: [
|
|
3496
|
+
.library(name: "LeCodesRuntime", targets: ["LeCodesRuntime"]),
|
|
3497
|
+
],
|
|
3498
|
+
dependencies: [
|
|
3499
|
+
.package(url: "https://github.com/letary/lecodes-ios-sdk.git", from: "${sdkVersion}"),
|
|
3500
|
+
],
|
|
3501
|
+
targets: [
|
|
3502
|
+
.target(
|
|
3503
|
+
name: "LeCodesRuntime",
|
|
3504
|
+
dependencies: [
|
|
3505
|
+
${runtimeDeps.join(`,
|
|
3506
|
+
`)},
|
|
3507
|
+
],
|
|
3508
|
+
path: "Sources/LeCodesRuntime"
|
|
3509
|
+
),${pluginTargets}
|
|
3510
|
+
]
|
|
3511
|
+
)
|
|
3512
|
+
`;
|
|
2111
3513
|
};
|
|
2112
|
-
var
|
|
3514
|
+
var runtimeSwiftTemplate = (plugins) => {
|
|
3515
|
+
const imports = plugins.map((p) => `import ${p.module}
|
|
3516
|
+
`).join("");
|
|
3517
|
+
const calls = plugins.length === 0 ? `
|
|
3518
|
+
` : plugins.map((p) => ` ${p.register}.register(in: engine)
|
|
3519
|
+
`).join("");
|
|
3520
|
+
return `//
|
|
3521
|
+
// LeCodesRuntime.swift
|
|
3522
|
+
// GENERATED by lecodes — regenerated by \`lecodes app sync\`; do not edit.
|
|
3523
|
+
//
|
|
3524
|
+
// Re-exports the SDK (so app code needs only \`import LeCodesRuntime\`) and registers
|
|
3525
|
+
// every plugin installed in app.json.
|
|
3526
|
+
//
|
|
3527
|
+
|
|
3528
|
+
@_exported import LeCodesSDK
|
|
3529
|
+
${imports}
|
|
3530
|
+
public func registerBundledPlugins(in engine: LeCodesEngine) {
|
|
3531
|
+
${calls}}
|
|
3532
|
+
`;
|
|
3533
|
+
};
|
|
3534
|
+
var IOS_README = `# ios/ — native Xcode shell
|
|
2113
3535
|
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
3536
|
+
Generated by \`lecodes app init ios\` for the LeCodes project one folder up. The app runs the
|
|
3537
|
+
compiled bundle in \`App/Resources/app.js\` — fully offline, nothing fetched at launch. This
|
|
3538
|
+
folder is ignored by the platform (\`ios/\` in \`.lecodesignore\`) and is never pushed to LeCodes.
|
|
3539
|
+
|
|
3540
|
+
## Daily workflow
|
|
3541
|
+
|
|
3542
|
+
Edit the app's code at the project root (\`main.ts\` …), then:
|
|
3543
|
+
|
|
3544
|
+
\`\`\`
|
|
3545
|
+
lecodes app sync # recompile app.js + assets into the shell, vendor plugins
|
|
3546
|
+
lecodes app open # open App.xcodeproj in Xcode — build & run from there
|
|
3547
|
+
\`\`\`
|
|
3548
|
+
|
|
3549
|
+
\`lecodes app doctor\` preflights builds; \`lecodes app build ios\` archives and exports an
|
|
3550
|
+
.ipa; \`lecodes app upload ios\` sends the archive to App Store Connect.
|
|
3551
|
+
|
|
3552
|
+
## Who owns what
|
|
3553
|
+
|
|
3554
|
+
Yours — \`lecodes app sync\` never touches these:
|
|
3555
|
+
|
|
3556
|
+
- \`App/*.swift\` (AppDelegate, SceneDelegate, ViewController) — customize freely, add files.
|
|
3557
|
+
App-local native code goes here too: add a Swift file that calls
|
|
3558
|
+
\`engine.registerCallback\` / \`registerService\`, and invoke it from ViewController.
|
|
3559
|
+
- \`App/Assets.xcassets/\` — drop a 1024×1024 PNG into AppIcon.appiconset/ for the app icon.
|
|
3560
|
+
- \`App/Base.lproj/LaunchScreen.storyboard\`
|
|
3561
|
+
|
|
3562
|
+
CLI-owned — regenerated on every sync, hand edits are overwritten:
|
|
3563
|
+
|
|
3564
|
+
- \`App/Resources/\` — the compiled app.js + every asset it references (fonts included), so
|
|
3565
|
+
the app runs fully offline. Globs in app.json \`"resources": {"remote": [...]}\` opt heavy
|
|
3566
|
+
assets (videos, big models) back into streaming from the server instead.
|
|
3567
|
+
- \`App/Info.plist\` — template + the installed plugins' usage keys
|
|
3568
|
+
- \`LeCodesRuntime/\` — local Swift package: the LeCodesSDK dependency, vendored plugin
|
|
3569
|
+
sources (Sources/<Module>/), and the registerBundledPlugins entry point
|
|
3570
|
+
- the version values inside \`App.xcodeproj/project.pbxproj\` (from app.json)
|
|
3571
|
+
- this README
|
|
3572
|
+
|
|
3573
|
+
## Config
|
|
3574
|
+
|
|
3575
|
+
Identity lives in \`../app.json\` (name, version, plugins, ios.bundleId / teamId / variant /
|
|
3576
|
+
buildNumber) — edit it there, then re-run \`lecodes app sync\`. Plugins are managed with
|
|
3577
|
+
\`lecodes app plugin add <id | github:owner/repo[@tag]>\` and pinned in \`../plugins.lock.json\`.
|
|
3578
|
+
`;
|
|
3579
|
+
|
|
3580
|
+
// src/commands/app.ts
|
|
3581
|
+
var SDK_PACKAGE_VERSION = "1.0.4";
|
|
3582
|
+
var FIRST_PARTY_PLUGINS = {
|
|
3583
|
+
camera: "letary/lecodes-plugins",
|
|
3584
|
+
"qr-scanner": "letary/lecodes-plugins"
|
|
3585
|
+
};
|
|
3586
|
+
var readJson = (path) => {
|
|
3587
|
+
if (!existsSync8(path))
|
|
3588
|
+
return null;
|
|
3589
|
+
try {
|
|
3590
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
3591
|
+
} catch {
|
|
3592
|
+
throw new CliError(`Invalid JSON in ${path}`);
|
|
2122
3593
|
}
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
3594
|
+
};
|
|
3595
|
+
var writeJson = (path, value) => writeFileSync7(path, JSON.stringify(value, null, 2) + `
|
|
3596
|
+
`);
|
|
3597
|
+
var projectRoot = () => {
|
|
3598
|
+
try {
|
|
3599
|
+
return findProjectRoot(process.cwd());
|
|
3600
|
+
} catch {
|
|
3601
|
+
return process.cwd();
|
|
3602
|
+
}
|
|
3603
|
+
};
|
|
3604
|
+
var git = (cwd, ...args) => execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
3605
|
+
var ensureRepo = (repo, wantTag, lock) => {
|
|
3606
|
+
const override = process.env.LECODES_PLUGINS_DIR;
|
|
3607
|
+
if (override) {
|
|
3608
|
+
note(`Using local plugin checkout ${override} (LECODES_PLUGINS_DIR)`);
|
|
3609
|
+
return { dir: resolve2(override), tag: "local", commit: "local" };
|
|
3610
|
+
}
|
|
3611
|
+
const cache = join11(homedir2(), ".lecodes", "plugins", repo.replace("/", "__"));
|
|
3612
|
+
const url = `https://github.com/${repo}.git`;
|
|
3613
|
+
if (!existsSync8(join11(cache, ".git"))) {
|
|
3614
|
+
mkdirSync5(dirname7(cache), { recursive: true });
|
|
3615
|
+
note(`Cloning ${url}…`);
|
|
3616
|
+
git(dirname7(cache), "clone", "--quiet", url, cache);
|
|
3617
|
+
}
|
|
3618
|
+
let tag = lock?.tag ?? wantTag;
|
|
3619
|
+
let commit = lock?.commit;
|
|
3620
|
+
if (commit) {
|
|
3621
|
+
try {
|
|
3622
|
+
git(cache, "cat-file", "-e", `${commit}^{commit}`);
|
|
3623
|
+
} catch {
|
|
3624
|
+
git(cache, "fetch", "--quiet", "--tags", "origin");
|
|
3625
|
+
}
|
|
3626
|
+
} else {
|
|
3627
|
+
git(cache, "fetch", "--quiet", "--tags", "origin");
|
|
3628
|
+
if (!tag) {
|
|
3629
|
+
tag = git(cache, "tag", "--list", "v*", "--sort=-version:refname").split(`
|
|
3630
|
+
`)[0] ?? "";
|
|
3631
|
+
if (!tag)
|
|
3632
|
+
throw new CliError(`${repo} has no v* version tags to install from`);
|
|
3633
|
+
}
|
|
3634
|
+
commit = git(cache, "rev-list", "-n", "1", tag);
|
|
3635
|
+
}
|
|
3636
|
+
git(cache, "checkout", "--quiet", "--detach", commit);
|
|
3637
|
+
return { dir: cache, tag: tag ?? "unknown", commit };
|
|
3638
|
+
};
|
|
3639
|
+
var resolvePlugin = (id, spec, lock) => {
|
|
3640
|
+
let repo;
|
|
3641
|
+
let subdir;
|
|
3642
|
+
let wantTag = null;
|
|
3643
|
+
if (spec.startsWith("github:")) {
|
|
3644
|
+
const [repoPart, tagPart] = spec.slice("github:".length).split("@");
|
|
3645
|
+
repo = repoPart;
|
|
3646
|
+
wantTag = tagPart ?? null;
|
|
3647
|
+
subdir = "";
|
|
3648
|
+
} else {
|
|
3649
|
+
repo = FIRST_PARTY_PLUGINS[id];
|
|
3650
|
+
if (!repo) {
|
|
3651
|
+
throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use a github:owner/repo spec.`);
|
|
3652
|
+
}
|
|
3653
|
+
subdir = `plugins/${id}`;
|
|
3654
|
+
}
|
|
3655
|
+
const checkout = ensureRepo(repo, wantTag, lock?.repo === repo ? lock : undefined);
|
|
3656
|
+
const dir = subdir ? join11(checkout.dir, subdir) : checkout.dir;
|
|
3657
|
+
const manifest = readJson(join11(dir, "lecodes-plugin.json"));
|
|
3658
|
+
if (!manifest)
|
|
3659
|
+
throw new CliError(`Plugin "${id}": no lecodes-plugin.json in ${dir}`);
|
|
3660
|
+
if (manifest.id !== id)
|
|
3661
|
+
throw new CliError(`Plugin "${id}": manifest declares id "${manifest.id}"`);
|
|
3662
|
+
if (!manifest.ios)
|
|
3663
|
+
throw new CliError(`Plugin "${id}" does not support iOS (manifest "ios" is null)`);
|
|
3664
|
+
if (spec.startsWith("github:")) {
|
|
3665
|
+
const names = [...manifest.provides?.views ?? [], ...manifest.provides?.services ?? []];
|
|
3666
|
+
for (const name of names) {
|
|
3667
|
+
if (!name.includes("."))
|
|
3668
|
+
warn(`Plugin "${id}" provides bare channel name "${name}" — third-party names should be vendor-prefixed (e.g. "acme.${name}").`);
|
|
2139
3669
|
}
|
|
2140
3670
|
}
|
|
2141
|
-
return {
|
|
3671
|
+
return {
|
|
3672
|
+
id,
|
|
3673
|
+
dir,
|
|
3674
|
+
manifest,
|
|
3675
|
+
lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" }
|
|
3676
|
+
};
|
|
2142
3677
|
};
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
const
|
|
2150
|
-
const
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
3678
|
+
var slugify = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "app";
|
|
3679
|
+
var appInit = async (args) => {
|
|
3680
|
+
const platform = args._[1];
|
|
3681
|
+
if (platform !== "ios") {
|
|
3682
|
+
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 core|full]");
|
|
3683
|
+
}
|
|
3684
|
+
const root = projectRoot();
|
|
3685
|
+
const iosDir = join11(root, "ios");
|
|
3686
|
+
if (existsSync8(iosDir)) {
|
|
3687
|
+
throw new CliError("ios/ already exists — run `lecodes app sync` to update it.");
|
|
3688
|
+
}
|
|
3689
|
+
const existing = readJson(join11(root, "app.json")) ?? {};
|
|
3690
|
+
const variant = flagStr(args, "variant") ?? existing.ios?.variant ?? "core";
|
|
3691
|
+
if (variant !== "core" && variant !== "full")
|
|
3692
|
+
throw new CliError(`--variant must be "core" or "full" (got "${variant}")`);
|
|
3693
|
+
const name = flagStr(args, "name") ?? existing.name ?? basename4(root);
|
|
3694
|
+
const config = {
|
|
3695
|
+
...existing,
|
|
3696
|
+
name,
|
|
3697
|
+
version: existing.version ?? "1.0.0",
|
|
3698
|
+
plugins: existing.plugins ?? {},
|
|
3699
|
+
ios: {
|
|
3700
|
+
bundleId: flagStr(args, "bundle-id") ?? existing.ios?.bundleId ?? `com.example.${slugify(name)}`,
|
|
3701
|
+
teamId: flagStr(args, "team") ?? existing.ios?.teamId ?? "",
|
|
3702
|
+
variant,
|
|
3703
|
+
buildNumber: existing.ios?.buildNumber ?? 1
|
|
3704
|
+
}
|
|
3705
|
+
};
|
|
3706
|
+
writeJson(join11(root, "app.json"), config);
|
|
3707
|
+
log(`${c.green("written")} app.json`);
|
|
3708
|
+
const ignorePath = join11(root, ".lecodesignore");
|
|
3709
|
+
const ignore = existsSync8(ignorePath) ? readFileSync10(ignorePath, "utf8") : DEFAULT_IGNORE_FILE;
|
|
3710
|
+
if (!/^ios\/$/m.test(ignore)) {
|
|
3711
|
+
writeFileSync7(ignorePath, ignore.replace(/\n*$/, `
|
|
3712
|
+
`) + `
|
|
3713
|
+
# Native app shells — generated by \`lecodes app init\`, never pushed to LeCodes.
|
|
3714
|
+
ios/
|
|
3715
|
+
`);
|
|
3716
|
+
log(`${c.green("updated")} .lecodesignore (+ ios/)`);
|
|
3717
|
+
}
|
|
3718
|
+
const identity = {
|
|
3719
|
+
displayName: name,
|
|
3720
|
+
bundleId: config.ios.bundleId,
|
|
3721
|
+
teamId: config.ios.teamId,
|
|
3722
|
+
version: config.version,
|
|
3723
|
+
buildNumber: config.ios.buildNumber
|
|
3724
|
+
};
|
|
3725
|
+
const write = (rel, content) => {
|
|
3726
|
+
const abs = join11(iosDir, rel);
|
|
3727
|
+
mkdirSync5(dirname7(abs), { recursive: true });
|
|
3728
|
+
writeFileSync7(abs, content);
|
|
3729
|
+
log(`${c.green("created")} ios/${rel}`);
|
|
3730
|
+
};
|
|
3731
|
+
write("README.md", IOS_README);
|
|
3732
|
+
write("App.xcodeproj/project.pbxproj", pbxprojTemplate(identity));
|
|
3733
|
+
write("App.xcodeproj/xcshareddata/xcschemes/App.xcscheme", SCHEME_XML);
|
|
3734
|
+
write("App/AppDelegate.swift", APP_DELEGATE);
|
|
3735
|
+
write("App/SceneDelegate.swift", SCENE_DELEGATE);
|
|
3736
|
+
write("App/ViewController.swift", VIEW_CONTROLLER);
|
|
3737
|
+
write("App/Info.plist", infoPlistTemplate({}));
|
|
3738
|
+
write("App/Base.lproj/LaunchScreen.storyboard", LAUNCH_STORYBOARD);
|
|
3739
|
+
write("App/Assets.xcassets/Contents.json", ASSETS_ROOT_JSON);
|
|
3740
|
+
write("App/Assets.xcassets/AccentColor.colorset/Contents.json", ACCENT_COLOR_JSON);
|
|
3741
|
+
write("App/Assets.xcassets/AppIcon.appiconset/Contents.json", APP_ICON_JSON);
|
|
3742
|
+
write("LeCodesRuntime/Package.swift", runtimePackageTemplate(SDK_PACKAGE_VERSION, variant, []));
|
|
3743
|
+
write("LeCodesRuntime/Sources/LeCodesRuntime/LeCodesRuntime.swift", runtimeSwiftTemplate([]));
|
|
3744
|
+
log("");
|
|
3745
|
+
success(`iOS shell for ${c.bold(name)} ready.`);
|
|
3746
|
+
log(` ${c.bold("lecodes app sync")} embed the compiled bundle (+ plugins) into the shell`);
|
|
3747
|
+
log(` ${c.bold("open ios/App.xcodeproj")} then run on your device from Xcode`);
|
|
3748
|
+
log(` App icon: drop a 1024×1024 PNG into ios/App/Assets.xcassets/AppIcon.appiconset/`);
|
|
3749
|
+
};
|
|
3750
|
+
var appSync = async (_args) => {
|
|
3751
|
+
const root = projectRoot();
|
|
3752
|
+
const config = readJson(join11(root, "app.json"));
|
|
3753
|
+
const iosDir = join11(root, "ios");
|
|
3754
|
+
if (!config?.ios || !existsSync8(iosDir)) {
|
|
3755
|
+
throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
|
|
3756
|
+
}
|
|
3757
|
+
const variant = config.ios.variant === "full" ? "full" : "core";
|
|
3758
|
+
const lockPath = join11(root, "plugins.lock.json");
|
|
3759
|
+
const oldLock = readJson(lockPath)?.plugins ?? {};
|
|
3760
|
+
const pluginSpecs = Object.entries(config.plugins ?? {});
|
|
3761
|
+
const runtimePlugins = [];
|
|
3762
|
+
const infoValues = {};
|
|
3763
|
+
const newLock = {};
|
|
3764
|
+
const sourcesDir = join11(iosDir, "LeCodesRuntime", "Sources");
|
|
3765
|
+
for (const [id, spec] of pluginSpecs) {
|
|
3766
|
+
const plugin = resolvePlugin(id, spec, oldLock[id]);
|
|
3767
|
+
const ios = plugin.manifest.ios;
|
|
3768
|
+
const module = ios.module;
|
|
3769
|
+
const register = ios.register;
|
|
3770
|
+
if (!module || !register)
|
|
3771
|
+
throw new CliError(`Plugin "${id}": manifest ios.module / ios.register missing`);
|
|
3772
|
+
const src = join11(plugin.dir, ios.sources ?? "ios");
|
|
3773
|
+
if (!existsSync8(src))
|
|
3774
|
+
throw new CliError(`Plugin "${id}": sources dir ${src} missing`);
|
|
3775
|
+
const dest = join11(sourcesDir, module);
|
|
3776
|
+
rmSync(dest, { recursive: true, force: true });
|
|
3777
|
+
cpSync(src, dest, { recursive: true });
|
|
3778
|
+
for (const [key, value] of Object.entries(ios.infoPlist ?? {})) {
|
|
3779
|
+
(infoValues[key] ??= []).push(value);
|
|
3780
|
+
}
|
|
3781
|
+
runtimePlugins.push({ id, module, register });
|
|
3782
|
+
newLock[id] = plugin.lock;
|
|
3783
|
+
log(`${c.green("plugin")} ${id}@${plugin.lock.version} (${plugin.lock.tag} → Sources/${module})`);
|
|
3784
|
+
}
|
|
3785
|
+
const keep = new Set(["LeCodesRuntime", ...runtimePlugins.map((p) => p.module)]);
|
|
3786
|
+
for (const entry of existsSync8(sourcesDir) ? readdirSync4(sourcesDir) : []) {
|
|
3787
|
+
if (!keep.has(entry) && statSync2(join11(sourcesDir, entry)).isDirectory()) {
|
|
3788
|
+
rmSync(join11(sourcesDir, entry), { recursive: true });
|
|
3789
|
+
log(`${c.yellow("removed")} ios/LeCodesRuntime/Sources/${entry} (plugin uninstalled)`);
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
writeFileSync7(join11(iosDir, "README.md"), IOS_README);
|
|
3793
|
+
writeFileSync7(join11(iosDir, "LeCodesRuntime", "Package.swift"), runtimePackageTemplate(SDK_PACKAGE_VERSION, variant, runtimePlugins));
|
|
3794
|
+
writeFileSync7(join11(sourcesDir, "LeCodesRuntime", "LeCodesRuntime.swift"), runtimeSwiftTemplate(runtimePlugins));
|
|
3795
|
+
const mergedKeys = {};
|
|
3796
|
+
for (const [key, values] of Object.entries(infoValues))
|
|
3797
|
+
mergedKeys[key] = [...new Set(values)].join(" ");
|
|
3798
|
+
writeFileSync7(join11(iosDir, "App", "Info.plist"), infoPlistTemplate(mergedKeys));
|
|
3799
|
+
if (pluginSpecs.length > 0 || existsSync8(lockPath))
|
|
3800
|
+
writeJson(lockPath, { plugins: newLock });
|
|
3801
|
+
const pbxPath = join11(iosDir, "App.xcodeproj", "project.pbxproj");
|
|
3802
|
+
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};`);
|
|
3803
|
+
writeFileSync7(pbxPath, pbx);
|
|
3804
|
+
const remotePatterns = config.resources?.remote ?? [];
|
|
3805
|
+
if (!Array.isArray(remotePatterns) || remotePatterns.some((p) => typeof p !== "string")) {
|
|
3806
|
+
throw new CliError('app.json "resources.remote" must be an array of glob strings (e.g. ["videos/**"]).');
|
|
3807
|
+
}
|
|
3808
|
+
const resourcesDir = join11(iosDir, "App", "Resources");
|
|
3809
|
+
rmSync(resourcesDir, { recursive: true, force: true });
|
|
3810
|
+
mkdirSync5(resourcesDir, { recursive: true });
|
|
3811
|
+
const resources = await compileShell({ _: [], flags: { o: join11(resourcesDir, "app.js") } }, { remotePatterns });
|
|
3812
|
+
const bundleJs = readFileSync10(join11(resourcesDir, "app.js"), "utf8");
|
|
3813
|
+
const preload = /^\/\/ preload: (.*)$/m.exec(bundleJs)?.[1].split(";").map((s) => s.trim()).filter(Boolean) ?? [];
|
|
3814
|
+
const IMPLICIT_PRELOADS = new Set(["uberarchive", "capture"]);
|
|
3815
|
+
const byName = new Map;
|
|
3816
|
+
for (const r of resources) {
|
|
3817
|
+
if (r.remote || !r.absPath)
|
|
3818
|
+
continue;
|
|
3819
|
+
const list = byName.get(r.name) ?? [];
|
|
3820
|
+
list.push(r);
|
|
3821
|
+
byName.set(r.name, list);
|
|
3822
|
+
}
|
|
3823
|
+
let embedded = 0;
|
|
3824
|
+
for (const name of preload) {
|
|
3825
|
+
if (IMPLICIT_PRELOADS.has(name))
|
|
3826
|
+
continue;
|
|
3827
|
+
const candidates = byName.get(name) ?? [];
|
|
3828
|
+
if (candidates.length === 0) {
|
|
3829
|
+
warn(`bundle needs local asset "${name}" but no project file provides it — it won't load on device`);
|
|
3830
|
+
continue;
|
|
3831
|
+
}
|
|
3832
|
+
if (candidates.length > 1)
|
|
3833
|
+
warn(`asset name collision: ${name} (${candidates.map((r) => r.path).join(", ")}) — embedding ${candidates[0].path}`);
|
|
3834
|
+
cpSync(candidates[0].absPath, join11(resourcesDir, name));
|
|
3835
|
+
embedded++;
|
|
3836
|
+
}
|
|
3837
|
+
if (embedded > 0)
|
|
3838
|
+
log(`${c.green("assets")} ${embedded} file(s) embedded → ios/App/Resources/`);
|
|
3839
|
+
const streamed = resources.filter((r) => r.remote);
|
|
3840
|
+
if (streamed.length > 0)
|
|
3841
|
+
log(`${c.green("remote")} ${streamed.length} resource(s) stay on the server (resources.remote): ${streamed.map((r) => r.path).join(", ")}`);
|
|
3842
|
+
log("");
|
|
3843
|
+
success(`Shell synced: app.js + ${runtimePlugins.length} plugin(s), variant ${variant}, v${config.version ?? "1.0.0"} (${config.ios.buildNumber ?? 1}).`);
|
|
3844
|
+
note("Build & run from Xcode (open ios/App.xcodeproj) — user files in ios/App/*.swift were not touched.");
|
|
3845
|
+
};
|
|
3846
|
+
var appPlugin = async (args) => {
|
|
3847
|
+
const sub = args._[1];
|
|
3848
|
+
const root = projectRoot();
|
|
3849
|
+
const configPath = join11(root, "app.json");
|
|
3850
|
+
const config = readJson(configPath);
|
|
3851
|
+
if (!config)
|
|
3852
|
+
throw new CliError("No app.json here — run `lecodes app init ios` first.");
|
|
3853
|
+
config.plugins ??= {};
|
|
3854
|
+
if (sub === "list" || sub === undefined) {
|
|
3855
|
+
const entries = Object.entries(config.plugins);
|
|
3856
|
+
if (entries.length === 0) {
|
|
3857
|
+
log("No plugins installed.");
|
|
3858
|
+
return;
|
|
3859
|
+
}
|
|
3860
|
+
const lock = readJson(join11(root, "plugins.lock.json"))?.plugins ?? {};
|
|
3861
|
+
for (const [id2, spec] of entries) {
|
|
3862
|
+
const pin = lock[id2] ? c.dim(` ${lock[id2].repo} ${lock[id2].tag} (${lock[id2].commit.slice(0, 7)})`) : c.dim(" (not synced)");
|
|
3863
|
+
log(`${c.bold(id2)} ${spec}${pin}`);
|
|
3864
|
+
}
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
const id = args._[2];
|
|
3868
|
+
if (!id)
|
|
3869
|
+
throw new CliError(`Usage: lecodes app plugin ${sub} <id | github:owner/repo[@tag]>`);
|
|
3870
|
+
if (sub === "add") {
|
|
3871
|
+
if (id.startsWith("github:")) {
|
|
3872
|
+
const bare = id.slice("github:".length).split("@")[0].split("/").pop();
|
|
3873
|
+
config.plugins[bare] = id;
|
|
3874
|
+
log(`${c.green("added")} ${bare} → ${id} ${c.dim("(third-party native code — review it before shipping)")}`);
|
|
3875
|
+
} else if (FIRST_PARTY_PLUGINS[id]) {
|
|
3876
|
+
config.plugins[id] = "*";
|
|
3877
|
+
log(`${c.green("added")} ${id}`);
|
|
2158
3878
|
} else {
|
|
2159
|
-
|
|
3879
|
+
throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use github:owner/repo[@tag].`);
|
|
2160
3880
|
}
|
|
3881
|
+
writeJson(configPath, config);
|
|
3882
|
+
note("Run `lecodes app sync` to vendor it into the shell.");
|
|
3883
|
+
return;
|
|
2161
3884
|
}
|
|
2162
|
-
|
|
3885
|
+
if (sub === "remove") {
|
|
3886
|
+
if (!(id in config.plugins))
|
|
3887
|
+
throw new CliError(`Plugin "${id}" is not installed.`);
|
|
3888
|
+
delete config.plugins[id];
|
|
3889
|
+
writeJson(configPath, config);
|
|
3890
|
+
log(`${c.yellow("removed")} ${id}`);
|
|
3891
|
+
note("Run `lecodes app sync` to drop its sources from the shell.");
|
|
3892
|
+
return;
|
|
3893
|
+
}
|
|
3894
|
+
throw new CliError(`Unknown subcommand "plugin ${sub}" — use list, add or remove.`);
|
|
2163
3895
|
};
|
|
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;
|
|
3896
|
+
var runTool = (cwd, cmd, args) => {
|
|
2176
3897
|
try {
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
3898
|
+
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], maxBuffer: 128 * 1024 * 1024 });
|
|
3899
|
+
} catch (e) {
|
|
3900
|
+
const err = e;
|
|
3901
|
+
const tail = ((err.stdout ?? "") + `
|
|
3902
|
+
` + (err.stderr ?? "")).split(`
|
|
3903
|
+
`).filter((l) => l.trim()).slice(-25).join(`
|
|
3904
|
+
`);
|
|
3905
|
+
throw new CliError(`${cmd} ${args[0] ?? ""} failed:
|
|
3906
|
+
${tail}`);
|
|
3907
|
+
}
|
|
3908
|
+
};
|
|
3909
|
+
var exportOptionsPlist = (method, teamId, destination) => `<?xml version="1.0" encoding="UTF-8"?>
|
|
3910
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3911
|
+
<plist version="1.0">
|
|
3912
|
+
<dict>
|
|
3913
|
+
<key>method</key>
|
|
3914
|
+
<string>${method}</string>
|
|
3915
|
+
<key>signingStyle</key>
|
|
3916
|
+
<string>automatic</string>${teamId ? `
|
|
3917
|
+
<key>teamID</key>
|
|
3918
|
+
<string>${teamId}</string>` : ""}${destination ? `
|
|
3919
|
+
<key>destination</key>
|
|
3920
|
+
<string>${destination}</string>` : ""}
|
|
3921
|
+
</dict>
|
|
3922
|
+
</plist>
|
|
3923
|
+
`;
|
|
3924
|
+
var requireShell = () => {
|
|
3925
|
+
const root = projectRoot();
|
|
3926
|
+
const config = readJson(join11(root, "app.json"));
|
|
3927
|
+
if (!config?.ios || !existsSync8(join11(root, "ios"))) {
|
|
3928
|
+
throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
|
|
3929
|
+
}
|
|
3930
|
+
return { root, config };
|
|
3931
|
+
};
|
|
3932
|
+
var appDoctor = async (_args) => {
|
|
3933
|
+
const { root, config } = requireShell();
|
|
3934
|
+
let failed = 0;
|
|
3935
|
+
const check = (ok, label, hint = "", fatal = true) => {
|
|
3936
|
+
log(`${ok ? c.green("✓") : fatal ? c.red("✗") : c.yellow("!")} ${label}${ok || !hint ? "" : c.dim(" — " + hint)}`);
|
|
3937
|
+
if (!ok && fatal)
|
|
3938
|
+
failed++;
|
|
3939
|
+
};
|
|
3940
|
+
let xcodeVersion = "";
|
|
3941
|
+
try {
|
|
3942
|
+
xcodeVersion = runTool(root, "xcodebuild", ["-version"]).split(`
|
|
3943
|
+
`)[0];
|
|
2180
3944
|
} catch {}
|
|
2181
|
-
|
|
2182
|
-
|
|
3945
|
+
check(xcodeVersion !== "", `Xcode CLI (${xcodeVersion || "not found"})`, "install Xcode + command line tools");
|
|
3946
|
+
check(!!config.ios.teamId, "Development team set", "lecodes app init wrote app.json — fill ios.teamId", false);
|
|
3947
|
+
const iconDir = join11(root, "ios", "App", "Assets.xcassets", "AppIcon.appiconset");
|
|
3948
|
+
const hasIcon = existsSync8(iconDir) && readdirSync4(iconDir).some((f2) => f2.endsWith(".png"));
|
|
3949
|
+
check(hasIcon, "App icon present", "drop a 1024×1024 PNG into ios/App/Assets.xcassets/AppIcon.appiconset/ (App Store requires it)", false);
|
|
3950
|
+
const bundle = join11(root, "ios", "App", "Resources", "app.js");
|
|
3951
|
+
check(existsSync8(bundle), "Bundled app.js", "run `lecodes app sync`");
|
|
3952
|
+
const lock = readJson(join11(root, "plugins.lock.json"))?.plugins ?? {};
|
|
3953
|
+
const wanted = Object.keys(config.plugins ?? {});
|
|
3954
|
+
const inSync = wanted.every((id) => lock[id]) && Object.keys(lock).every((id) => wanted.includes(id));
|
|
3955
|
+
check(wanted.length === 0 || inSync, "Plugins synced", "run `lecodes app sync`");
|
|
3956
|
+
const asc = readJson(join11(homedir2(), ".lecodes", "asc.json"));
|
|
3957
|
+
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);
|
|
3958
|
+
log("");
|
|
3959
|
+
if (failed > 0)
|
|
3960
|
+
throw new CliError(`${failed} check(s) failed.`);
|
|
3961
|
+
success("Ready to build.");
|
|
3962
|
+
};
|
|
3963
|
+
var ARCHIVE_PATH = "ios/build/App.xcarchive";
|
|
3964
|
+
var appBuild = async (args) => {
|
|
3965
|
+
const { root, config } = requireShell();
|
|
3966
|
+
const configuration = flagStr(args, "configuration") ?? "Release";
|
|
3967
|
+
const method = flagStr(args, "method") ?? "app-store-connect";
|
|
3968
|
+
if (!flagBool(args, "no-sync"))
|
|
3969
|
+
await appSync({ _: [], flags: {} });
|
|
3970
|
+
note(`Archiving (${configuration})… this takes a few minutes`);
|
|
3971
|
+
runTool(root, "xcodebuild", [
|
|
3972
|
+
"-project",
|
|
3973
|
+
"ios/App.xcodeproj",
|
|
3974
|
+
"-scheme",
|
|
3975
|
+
"App",
|
|
3976
|
+
"-destination",
|
|
3977
|
+
"generic/platform=iOS",
|
|
3978
|
+
"-configuration",
|
|
3979
|
+
configuration,
|
|
3980
|
+
"archive",
|
|
3981
|
+
"-archivePath",
|
|
3982
|
+
ARCHIVE_PATH,
|
|
3983
|
+
"-allowProvisioningUpdates"
|
|
3984
|
+
]);
|
|
3985
|
+
success(`Archive: ios/build/App.xcarchive`);
|
|
3986
|
+
if (flagBool(args, "no-export"))
|
|
3987
|
+
return;
|
|
3988
|
+
const optionsPath = join11(root, "ios", "build", "ExportOptions.plist");
|
|
3989
|
+
writeFileSync7(optionsPath, exportOptionsPlist(method, config.ios.teamId ?? ""));
|
|
3990
|
+
note(`Exporting (.ipa, method ${method})…`);
|
|
3991
|
+
runTool(root, "xcodebuild", [
|
|
3992
|
+
"-exportArchive",
|
|
3993
|
+
"-archivePath",
|
|
3994
|
+
ARCHIVE_PATH,
|
|
3995
|
+
"-exportOptionsPlist",
|
|
3996
|
+
optionsPath,
|
|
3997
|
+
"-exportPath",
|
|
3998
|
+
"ios/build/export",
|
|
3999
|
+
"-allowProvisioningUpdates"
|
|
4000
|
+
]);
|
|
4001
|
+
success("Export: ios/build/export/ — upload with `lecodes app upload ios`");
|
|
4002
|
+
};
|
|
4003
|
+
var appUpload = async (args) => {
|
|
4004
|
+
const { root, config } = requireShell();
|
|
4005
|
+
if (!existsSync8(join11(root, ARCHIVE_PATH))) {
|
|
4006
|
+
throw new CliError("No archive at ios/build/App.xcarchive — run `lecodes app build ios` first.");
|
|
4007
|
+
}
|
|
4008
|
+
const ascPath = join11(homedir2(), ".lecodes", "asc.json");
|
|
4009
|
+
const stored = readJson(ascPath) ?? {};
|
|
4010
|
+
const keyPath = flagStr(args, "key-path") ?? stored.keyPath;
|
|
4011
|
+
const keyId = flagStr(args, "key-id") ?? stored.keyId;
|
|
4012
|
+
const issuerId = flagStr(args, "issuer-id") ?? stored.issuerId;
|
|
4013
|
+
if (!keyPath || !keyId || !issuerId) {
|
|
4014
|
+
throw new CliError(`App Store Connect API key needed: --key-path <AuthKey.p8> --key-id <id> --issuer-id <id>
|
|
4015
|
+
(create one at appstoreconnect.apple.com → Users and Access → Integrations; stored in ~/.lecodes/asc.json after first use)`);
|
|
4016
|
+
}
|
|
4017
|
+
if (!existsSync8(keyPath))
|
|
4018
|
+
throw new CliError(`API key file not found: ${keyPath}`);
|
|
4019
|
+
if (flagStr(args, "key-path")) {
|
|
4020
|
+
mkdirSync5(dirname7(ascPath), { recursive: true });
|
|
4021
|
+
writeJson(ascPath, { keyPath, keyId, issuerId });
|
|
4022
|
+
note("Stored the key reference in ~/.lecodes/asc.json");
|
|
4023
|
+
}
|
|
4024
|
+
const optionsPath = join11(root, "ios", "build", "UploadOptions.plist");
|
|
4025
|
+
writeFileSync7(optionsPath, exportOptionsPlist("app-store-connect", config.ios.teamId ?? "", "upload"));
|
|
4026
|
+
note("Uploading to App Store Connect… (processing continues on Apple's side)");
|
|
4027
|
+
runTool(root, "xcodebuild", [
|
|
4028
|
+
"-exportArchive",
|
|
4029
|
+
"-archivePath",
|
|
4030
|
+
ARCHIVE_PATH,
|
|
4031
|
+
"-exportOptionsPlist",
|
|
4032
|
+
optionsPath,
|
|
4033
|
+
"-exportPath",
|
|
4034
|
+
"ios/build/upload",
|
|
4035
|
+
"-authenticationKeyPath",
|
|
4036
|
+
resolve2(keyPath),
|
|
4037
|
+
"-authenticationKeyID",
|
|
4038
|
+
keyId,
|
|
4039
|
+
"-authenticationKeyIssuerID",
|
|
4040
|
+
issuerId,
|
|
4041
|
+
"-allowProvisioningUpdates"
|
|
4042
|
+
]);
|
|
4043
|
+
const configPath = join11(root, "app.json");
|
|
4044
|
+
const fresh = readJson(configPath);
|
|
4045
|
+
fresh.ios.buildNumber = (fresh.ios.buildNumber ?? 1) + 1;
|
|
4046
|
+
writeJson(configPath, fresh);
|
|
4047
|
+
success(`Uploaded build ${(fresh.ios.buildNumber ?? 2) - 1} — check App Store Connect / TestFlight once processing finishes.`);
|
|
4048
|
+
note(`ios.buildNumber bumped to ${fresh.ios.buildNumber} for the next build.`);
|
|
4049
|
+
};
|
|
4050
|
+
var appOpen = async (args) => {
|
|
4051
|
+
const root = projectRoot();
|
|
4052
|
+
const platform = args._[1] ?? "ios";
|
|
4053
|
+
if (platform === "ios") {
|
|
4054
|
+
const proj = join11(root, "ios", "App.xcodeproj");
|
|
4055
|
+
if (!existsSync8(proj))
|
|
4056
|
+
throw new CliError("No iOS shell here — run `lecodes app init ios` first.");
|
|
4057
|
+
if (process.platform !== "darwin")
|
|
4058
|
+
throw new CliError("Opening Xcode requires macOS.");
|
|
4059
|
+
execFileSync("open", [proj], { stdio: "ignore" });
|
|
4060
|
+
success("Opened ios/App.xcodeproj in Xcode.");
|
|
4061
|
+
return;
|
|
4062
|
+
}
|
|
4063
|
+
if (platform === "android") {
|
|
4064
|
+
throw new CliError("Android shells don't exist yet — `lecodes app open` covers ios for now.");
|
|
4065
|
+
}
|
|
4066
|
+
throw new CliError(`Unknown platform "${platform}" — use ios.`);
|
|
4067
|
+
};
|
|
4068
|
+
var app = async (args) => {
|
|
4069
|
+
const sub = args._[0];
|
|
4070
|
+
if (sub === "init")
|
|
4071
|
+
return appInit(args);
|
|
4072
|
+
if (sub === "sync")
|
|
4073
|
+
return appSync(args);
|
|
4074
|
+
if (sub === "plugin")
|
|
4075
|
+
return appPlugin(args);
|
|
4076
|
+
if (sub === "doctor")
|
|
4077
|
+
return appDoctor(args);
|
|
4078
|
+
if (sub === "build")
|
|
4079
|
+
return appBuild(args);
|
|
4080
|
+
if (sub === "upload")
|
|
4081
|
+
return appUpload(args);
|
|
4082
|
+
if (sub === "open")
|
|
4083
|
+
return appOpen(args);
|
|
4084
|
+
throw new CliError("Usage: lecodes app <init ios | sync | open | plugin list|add|remove | doctor | build ios | upload ios> — see `lecodes help`.");
|
|
2183
4085
|
};
|
|
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
4086
|
|
|
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);
|
|
4087
|
+
// src/commands/clone.ts
|
|
4088
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync5 } from "node:fs";
|
|
4089
|
+
import { resolve as resolve3 } from "node:path";
|
|
4090
|
+
|
|
4091
|
+
// src/types.ts
|
|
4092
|
+
import { dirname as dirname8, join as join12 } from "node:path";
|
|
4093
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "node:fs";
|
|
4094
|
+
var TSCONFIG = {
|
|
4095
|
+
compilerOptions: {
|
|
4096
|
+
target: "ES2020",
|
|
4097
|
+
module: "ESNext",
|
|
4098
|
+
moduleResolution: "bundler",
|
|
4099
|
+
lib: ["ES2020"],
|
|
4100
|
+
types: [],
|
|
4101
|
+
allowJs: true,
|
|
4102
|
+
checkJs: false,
|
|
4103
|
+
strict: false,
|
|
4104
|
+
noEmit: true,
|
|
4105
|
+
skipLibCheck: true
|
|
4106
|
+
},
|
|
4107
|
+
include: [`${LECODES_DIR}/types/**/*.d.ts`, "**/*.ts", "**/*.js"],
|
|
4108
|
+
exclude: ["node_modules"]
|
|
4109
|
+
};
|
|
4110
|
+
var writeTypeBundle = (root, bundle) => {
|
|
4111
|
+
const typesDir = join12(root, LECODES_DIR, "types");
|
|
4112
|
+
for (const [path, text] of Object.entries(bundle)) {
|
|
4113
|
+
const dest = join12(typesDir, path);
|
|
4114
|
+
mkdirSync6(dirname8(dest), { recursive: true });
|
|
4115
|
+
writeFileSync8(dest, text);
|
|
2231
4116
|
}
|
|
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
|
-
`);
|
|
4117
|
+
};
|
|
4118
|
+
var materializeTypes = async (root, apiUrl) => {
|
|
4119
|
+
writeTypeBundle(root, await getTypes(apiUrl));
|
|
4120
|
+
};
|
|
4121
|
+
var readLocalTypeBundle = () => {
|
|
4122
|
+
const candidates = [
|
|
4123
|
+
join12(distRoot, "runtime", "sdk-types.json"),
|
|
4124
|
+
join12(distRoot, "..", "sdk", "dist", "types.json")
|
|
4125
|
+
];
|
|
4126
|
+
for (const path of candidates) {
|
|
4127
|
+
if (existsSync9(path)) {
|
|
4128
|
+
try {
|
|
4129
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
4130
|
+
} catch {}
|
|
4131
|
+
}
|
|
2257
4132
|
}
|
|
4133
|
+
return null;
|
|
2258
4134
|
};
|
|
2259
|
-
var
|
|
2260
|
-
|
|
2261
|
-
|
|
4135
|
+
var materializeTypesLocal = (root) => {
|
|
4136
|
+
const bundle = readLocalTypeBundle();
|
|
4137
|
+
if (!bundle)
|
|
4138
|
+
return false;
|
|
4139
|
+
writeTypeBundle(root, bundle);
|
|
4140
|
+
return true;
|
|
4141
|
+
};
|
|
4142
|
+
var writeTsconfig = (root) => {
|
|
4143
|
+
writeFileSync8(join12(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
|
|
4144
|
+
};
|
|
4145
|
+
|
|
4146
|
+
// src/commands/clone.ts
|
|
4147
|
+
var parseUuid = (input) => {
|
|
4148
|
+
if (!input)
|
|
4149
|
+
throw new CliError("Usage: lecodes clone <project-uuid|url> [directory]");
|
|
4150
|
+
if (!input.includes("/"))
|
|
4151
|
+
return input;
|
|
4152
|
+
const clean = input.split(/[?#]/)[0].replace(/\/+$/, "");
|
|
4153
|
+
const match = clean.match(/\/(?:projects|view)\/([^/]+)/);
|
|
4154
|
+
return match ? match[1] : clean.slice(clean.lastIndexOf("/") + 1);
|
|
4155
|
+
};
|
|
4156
|
+
var slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
4157
|
+
var clone = async (args) => {
|
|
4158
|
+
const config = loadConfig();
|
|
4159
|
+
const apiUrl = requireApiUrl(config);
|
|
4160
|
+
const token = requireToken(config);
|
|
4161
|
+
const uuid = parseUuid(args._[0]);
|
|
4162
|
+
const project = await getProject(apiUrl, token, uuid);
|
|
4163
|
+
const dir = args._[1] ?? slug(project.name);
|
|
4164
|
+
const root = resolve3(dir);
|
|
4165
|
+
if (existsSync10(root) && readdirSync5(root).length > 0) {
|
|
4166
|
+
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
4167
|
+
}
|
|
4168
|
+
mkdirSync7(root, { recursive: true });
|
|
4169
|
+
info(c.dim(`Fetching project ${c.bold(project.name)}…`));
|
|
4170
|
+
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
4171
|
+
endProgress();
|
|
4172
|
+
if (!flagBool(args, "no-types")) {
|
|
4173
|
+
info(c.dim("Fetching type definitions…"));
|
|
4174
|
+
await materializeTypes(root, apiUrl);
|
|
4175
|
+
writeTsconfig(root);
|
|
4176
|
+
}
|
|
4177
|
+
writeDefaultIgnore(root);
|
|
4178
|
+
const commits = await getCommits(apiUrl, token, uuid);
|
|
4179
|
+
writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
|
|
4180
|
+
success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`);
|
|
4181
|
+
info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``));
|
|
2262
4182
|
};
|
|
2263
4183
|
|
|
2264
4184
|
// src/commands/create.ts
|
|
2265
|
-
import { existsSync as
|
|
2266
|
-
import { join as
|
|
4185
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readdirSync as readdirSync6, writeFileSync as writeFileSync9 } from "node:fs";
|
|
4186
|
+
import { join as join13, resolve as resolve4 } from "node:path";
|
|
2267
4187
|
var slug2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
2268
4188
|
var STARTER = `// Welcome to le.codes — no imports needed, the whole SDK is available as globals.
|
|
2269
4189
|
const scene = new Scene2D({ background: '#10131a' })
|
|
@@ -2281,18 +4201,18 @@ var create = async (args) => {
|
|
|
2281
4201
|
if (!name)
|
|
2282
4202
|
throw new CliError("Usage: lecodes create <name> [directory]");
|
|
2283
4203
|
const dir = args._[1] ?? slug2(name);
|
|
2284
|
-
const root =
|
|
2285
|
-
if (
|
|
4204
|
+
const root = resolve4(dir);
|
|
4205
|
+
if (existsSync11(root) && readdirSync6(root).length > 0) {
|
|
2286
4206
|
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
2287
4207
|
}
|
|
2288
|
-
|
|
4208
|
+
mkdirSync8(root, { recursive: true });
|
|
2289
4209
|
info(c.dim(`Creating project ${c.bold(name)}…`));
|
|
2290
4210
|
const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
|
|
2291
4211
|
info(c.dim("Fetching type definitions…"));
|
|
2292
4212
|
await materializeTypes(root, apiUrl);
|
|
2293
4213
|
writeTsconfig(root);
|
|
2294
4214
|
writeDefaultIgnore(root);
|
|
2295
|
-
|
|
4215
|
+
writeFileSync9(join13(root, "main.ts"), STARTER);
|
|
2296
4216
|
const commits = await getCommits(apiUrl, token, uuid);
|
|
2297
4217
|
writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
|
|
2298
4218
|
success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
|
|
@@ -2300,22 +4220,22 @@ var create = async (args) => {
|
|
|
2300
4220
|
};
|
|
2301
4221
|
|
|
2302
4222
|
// src/commands/design.ts
|
|
2303
|
-
import { existsSync as
|
|
2304
|
-
import { basename as
|
|
4223
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync10, readFileSync as readFileSync14, readdirSync as readdirSync7, writeFileSync as writeFileSync11, appendFileSync } from "node:fs";
|
|
4224
|
+
import { basename as basename6, join as join15 } from "node:path";
|
|
2305
4225
|
|
|
2306
4226
|
// src/compile/designCompile.ts
|
|
2307
|
-
import { readFileSync as
|
|
2308
|
-
import { basename as
|
|
4227
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
4228
|
+
import { basename as basename5 } from "node:path";
|
|
2309
4229
|
|
|
2310
4230
|
// src/compile/assetIcons.ts
|
|
2311
|
-
import { createHash as
|
|
2312
|
-
import { existsSync as
|
|
2313
|
-
import { dirname as
|
|
4231
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
4232
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "node:fs";
|
|
4233
|
+
import { dirname as dirname9, join as join14 } from "node:path";
|
|
2314
4234
|
var DEFAULT_REGISTRY = process.env.LECODES_ICON_REGISTRY?.trim() || "https://icon-registry.jt3.ru/v1";
|
|
2315
4235
|
var ICON_ID = /^([a-z0-9]+(?:-[a-z0-9]+)*):([a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
|
2316
|
-
var
|
|
2317
|
-
var
|
|
2318
|
-
var
|
|
4236
|
+
var isIdentStart2 = (c2) => c2 >= "A" && c2 <= "Z" || c2 >= "a" && c2 <= "z" || c2 === "_" || c2 === "$";
|
|
4237
|
+
var isIdentPart2 = (c2) => isIdentStart2(c2) || c2 >= "0" && c2 <= "9";
|
|
4238
|
+
var skipString2 = (src, start) => {
|
|
2319
4239
|
const quote = src[start];
|
|
2320
4240
|
const n = src.length;
|
|
2321
4241
|
let i = start + 1;
|
|
@@ -2326,7 +4246,7 @@ var skipString = (src, start) => {
|
|
|
2326
4246
|
continue;
|
|
2327
4247
|
}
|
|
2328
4248
|
if (quote === "`" && c2 === "$" && src[i + 1] === "{") {
|
|
2329
|
-
i =
|
|
4249
|
+
i = skipBraces2(src, i + 1);
|
|
2330
4250
|
continue;
|
|
2331
4251
|
}
|
|
2332
4252
|
if (c2 === quote)
|
|
@@ -2335,14 +4255,14 @@ var skipString = (src, start) => {
|
|
|
2335
4255
|
}
|
|
2336
4256
|
return n;
|
|
2337
4257
|
};
|
|
2338
|
-
var
|
|
4258
|
+
var skipBraces2 = (src, open) => {
|
|
2339
4259
|
const n = src.length;
|
|
2340
4260
|
let depth = 0;
|
|
2341
4261
|
let i = open;
|
|
2342
4262
|
while (i < n) {
|
|
2343
4263
|
const c2 = src[i];
|
|
2344
4264
|
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
2345
|
-
i =
|
|
4265
|
+
i = skipString2(src, i);
|
|
2346
4266
|
continue;
|
|
2347
4267
|
}
|
|
2348
4268
|
if (c2 === "{")
|
|
@@ -2356,14 +4276,14 @@ var skipBraces = (src, open) => {
|
|
|
2356
4276
|
}
|
|
2357
4277
|
return n;
|
|
2358
4278
|
};
|
|
2359
|
-
var
|
|
4279
|
+
var matchParen2 = (src, open) => {
|
|
2360
4280
|
const n = src.length;
|
|
2361
4281
|
let depth = 0;
|
|
2362
4282
|
let i = open;
|
|
2363
4283
|
while (i < n) {
|
|
2364
4284
|
const c2 = src[i];
|
|
2365
4285
|
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
2366
|
-
i =
|
|
4286
|
+
i = skipString2(src, i);
|
|
2367
4287
|
continue;
|
|
2368
4288
|
}
|
|
2369
4289
|
if (c2 === "/" && src[i + 1] === "/") {
|
|
@@ -2391,7 +4311,7 @@ var matchParen = (src, open) => {
|
|
|
2391
4311
|
}
|
|
2392
4312
|
return -1;
|
|
2393
4313
|
};
|
|
2394
|
-
var
|
|
4314
|
+
var splitTopLevel2 = (text, delim) => {
|
|
2395
4315
|
const parts = [];
|
|
2396
4316
|
const n = text.length;
|
|
2397
4317
|
let depth = 0;
|
|
@@ -2400,7 +4320,7 @@ var splitTopLevel = (text, delim) => {
|
|
|
2400
4320
|
while (i < n) {
|
|
2401
4321
|
const c2 = text[i];
|
|
2402
4322
|
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
2403
|
-
i =
|
|
4323
|
+
i = skipString2(text, i);
|
|
2404
4324
|
continue;
|
|
2405
4325
|
}
|
|
2406
4326
|
if (c2 === "(" || c2 === "[" || c2 === "{")
|
|
@@ -2416,7 +4336,7 @@ var splitTopLevel = (text, delim) => {
|
|
|
2416
4336
|
parts.push(text.slice(last));
|
|
2417
4337
|
return parts;
|
|
2418
4338
|
};
|
|
2419
|
-
var
|
|
4339
|
+
var stringLiteralValue2 = (text) => {
|
|
2420
4340
|
const t = text.trim();
|
|
2421
4341
|
if (t.length < 2)
|
|
2422
4342
|
return null;
|
|
@@ -2437,7 +4357,7 @@ var scanAssetIconCalls = (source) => {
|
|
|
2437
4357
|
while (i < n) {
|
|
2438
4358
|
const c2 = source[i];
|
|
2439
4359
|
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
2440
|
-
i =
|
|
4360
|
+
i = skipString2(source, i);
|
|
2441
4361
|
continue;
|
|
2442
4362
|
}
|
|
2443
4363
|
if (c2 === "/" && source[i + 1] === "/") {
|
|
@@ -2454,12 +4374,12 @@ var scanAssetIconCalls = (source) => {
|
|
|
2454
4374
|
i += 2;
|
|
2455
4375
|
continue;
|
|
2456
4376
|
}
|
|
2457
|
-
if (
|
|
4377
|
+
if (isIdentStart2(c2)) {
|
|
2458
4378
|
let j = i + 1;
|
|
2459
|
-
while (j < n &&
|
|
4379
|
+
while (j < n && isIdentPart2(source[j]))
|
|
2460
4380
|
j++;
|
|
2461
|
-
if (source.slice(i, j) === "assetIcon" &&
|
|
2462
|
-
const call =
|
|
4381
|
+
if (source.slice(i, j) === "assetIcon" && prevNonSpace2(source, i) !== ".") {
|
|
4382
|
+
const call = parseCall2(source, i, j);
|
|
2463
4383
|
if (call) {
|
|
2464
4384
|
calls.push(call);
|
|
2465
4385
|
i = call.end;
|
|
@@ -2473,25 +4393,25 @@ var scanAssetIconCalls = (source) => {
|
|
|
2473
4393
|
}
|
|
2474
4394
|
return calls;
|
|
2475
4395
|
};
|
|
2476
|
-
var
|
|
4396
|
+
var prevNonSpace2 = (src, i) => {
|
|
2477
4397
|
let k = i - 1;
|
|
2478
4398
|
while (k >= 0 && /\s/.test(src[k]))
|
|
2479
4399
|
k--;
|
|
2480
4400
|
return k >= 0 ? src[k] : "";
|
|
2481
4401
|
};
|
|
2482
|
-
var
|
|
4402
|
+
var parseCall2 = (src, start, identEnd) => {
|
|
2483
4403
|
const n = src.length;
|
|
2484
4404
|
let i = identEnd;
|
|
2485
4405
|
while (i < n && /\s/.test(src[i]))
|
|
2486
4406
|
i++;
|
|
2487
4407
|
if (src[i] !== "(")
|
|
2488
4408
|
return null;
|
|
2489
|
-
const close =
|
|
4409
|
+
const close = matchParen2(src, i);
|
|
2490
4410
|
if (close === -1)
|
|
2491
4411
|
return null;
|
|
2492
4412
|
const call = { start, end: close + 1, id: "", pack: "", name: "" };
|
|
2493
|
-
const args =
|
|
2494
|
-
const id =
|
|
4413
|
+
const args = splitTopLevel2(src.slice(i + 1, close), ",");
|
|
4414
|
+
const id = stringLiteralValue2(args[0] ?? "");
|
|
2495
4415
|
if (id === null) {
|
|
2496
4416
|
call.error = `assetIcon(...) needs a string-literal id like "lucide:bell" (got \`${(args[0] ?? "").trim() || "()"}\`).`;
|
|
2497
4417
|
return call;
|
|
@@ -2522,8 +4442,8 @@ var extractColor = (optsText) => {
|
|
|
2522
4442
|
if (!optsText.startsWith("{") || !optsText.endsWith("}")) {
|
|
2523
4443
|
return { error: `assetIcon(id, opts): opts must be an object literal like { color: "#5b8cff" }.` };
|
|
2524
4444
|
}
|
|
2525
|
-
for (const prop of
|
|
2526
|
-
const kv =
|
|
4445
|
+
for (const prop of splitTopLevel2(optsText.slice(1, -1), ",")) {
|
|
4446
|
+
const kv = splitTopLevel2(prop, ":");
|
|
2527
4447
|
if (kv.length < 2)
|
|
2528
4448
|
continue;
|
|
2529
4449
|
const key = kv[0].trim().replace(/^["'`]|["'`]$/g, "");
|
|
@@ -2532,7 +4452,7 @@ var extractColor = (optsText) => {
|
|
|
2532
4452
|
const raw = kv.slice(1).join(":").trim();
|
|
2533
4453
|
if (!raw)
|
|
2534
4454
|
continue;
|
|
2535
|
-
const literal =
|
|
4455
|
+
const literal = stringLiteralValue2(raw);
|
|
2536
4456
|
return literal !== null ? { bake: literal } : { tint: raw };
|
|
2537
4457
|
}
|
|
2538
4458
|
return {};
|
|
@@ -2553,34 +4473,34 @@ var iconExpr = (svg, call) => {
|
|
|
2553
4473
|
return call.tint ? `Object.assign(${src}, { tintColor: (${call.tint}) })` : src;
|
|
2554
4474
|
};
|
|
2555
4475
|
var escapeTemplate = (s) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
2556
|
-
var
|
|
2557
|
-
var
|
|
2558
|
-
var
|
|
4476
|
+
var isHttp2 = (s) => /^https?:/i.test(s);
|
|
4477
|
+
var sha122 = (s) => createHash3("sha256").update(s).digest("hex").slice(0, 12);
|
|
4478
|
+
var registryHome2 = (registry) => registry.replace(/\/v\d+$/, "");
|
|
2559
4479
|
var readText = async (registry, rel) => {
|
|
2560
|
-
if (
|
|
4480
|
+
if (isHttp2(registry)) {
|
|
2561
4481
|
const res = await fetch(`${registry}/${rel}`);
|
|
2562
4482
|
return res.ok ? await res.text() : null;
|
|
2563
4483
|
}
|
|
2564
4484
|
try {
|
|
2565
|
-
return
|
|
4485
|
+
return readFileSync12(join14(registry, rel), "utf8");
|
|
2566
4486
|
} catch {
|
|
2567
4487
|
return null;
|
|
2568
4488
|
}
|
|
2569
4489
|
};
|
|
2570
|
-
var
|
|
4490
|
+
var readLock2 = (path) => {
|
|
2571
4491
|
try {
|
|
2572
|
-
const lock = JSON.parse(
|
|
4492
|
+
const lock = JSON.parse(readFileSync12(path, "utf8"));
|
|
2573
4493
|
if (lock && typeof lock === "object" && lock.icons && lock.packs)
|
|
2574
4494
|
return lock;
|
|
2575
4495
|
} catch {}
|
|
2576
4496
|
return null;
|
|
2577
4497
|
};
|
|
2578
|
-
var
|
|
4498
|
+
var serializeLock2 = (lock) => {
|
|
2579
4499
|
const sortKeys = (rec) => Object.fromEntries(Object.keys(rec).sort().map((k) => [k, rec[k]]));
|
|
2580
4500
|
return JSON.stringify({ registry: lock.registry, icons: sortKeys(lock.icons), packs: sortKeys(lock.packs) }, null, 2) + `
|
|
2581
4501
|
`;
|
|
2582
4502
|
};
|
|
2583
|
-
var
|
|
4503
|
+
var renderNotices2 = (lock) => {
|
|
2584
4504
|
const packs = [...new Set(Object.values(lock.icons).map((i) => i.pack))].sort();
|
|
2585
4505
|
const lines = [
|
|
2586
4506
|
"# Icon attribution & notices",
|
|
@@ -2605,29 +4525,29 @@ var renderNotices = (lock) => {
|
|
|
2605
4525
|
return lines.join(`
|
|
2606
4526
|
`);
|
|
2607
4527
|
};
|
|
2608
|
-
var
|
|
2609
|
-
const current =
|
|
4528
|
+
var writeIfChanged2 = (path, text) => {
|
|
4529
|
+
const current = existsSync12(path) ? safeRead(path) : null;
|
|
2610
4530
|
if (current === text)
|
|
2611
4531
|
return false;
|
|
2612
|
-
|
|
4532
|
+
writeFileSync10(path, text);
|
|
2613
4533
|
return true;
|
|
2614
4534
|
};
|
|
2615
4535
|
var safeRead = (path) => {
|
|
2616
4536
|
try {
|
|
2617
|
-
return
|
|
4537
|
+
return readFileSync12(path, "utf8");
|
|
2618
4538
|
} catch {
|
|
2619
4539
|
return null;
|
|
2620
4540
|
}
|
|
2621
4541
|
};
|
|
2622
4542
|
var vendorIcons = async (designDir, ids, opts = {}) => {
|
|
2623
4543
|
const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
|
|
2624
|
-
const lockPath =
|
|
2625
|
-
const lock =
|
|
4544
|
+
const lockPath = join14(designDir, "icons.lock.json");
|
|
4545
|
+
const lock = readLock2(lockPath) ?? { registry, icons: {}, packs: {} };
|
|
2626
4546
|
const svgById = new Map;
|
|
2627
4547
|
const missing = [];
|
|
2628
4548
|
for (const id of ids) {
|
|
2629
4549
|
const li = lock.icons[id];
|
|
2630
|
-
const cached = li && safeRead(
|
|
4550
|
+
const cached = li && safeRead(join14(designDir, li.path));
|
|
2631
4551
|
if (cached)
|
|
2632
4552
|
svgById.set(id, cached);
|
|
2633
4553
|
else
|
|
@@ -2643,23 +4563,23 @@ var vendorIcons = async (designDir, ids, opts = {}) => {
|
|
|
2643
4563
|
const [, pack, name] = m;
|
|
2644
4564
|
const lic = licenses[pack];
|
|
2645
4565
|
if (!lic)
|
|
2646
|
-
throw new Error(`assetIcon: unknown icon pack "${pack}" (in "${id}"). Browse packs at ${
|
|
4566
|
+
throw new Error(`assetIcon: unknown icon pack "${pack}" (in "${id}"). Browse packs at ${registryHome2(registry)}.`);
|
|
2647
4567
|
const svg = await readText(registry, `${pack}/${name}.svg`);
|
|
2648
4568
|
if (svg === null)
|
|
2649
|
-
throw new Error(`assetIcon: icon "${id}" not found in the registry. Check the name at ${
|
|
4569
|
+
throw new Error(`assetIcon: icon "${id}" not found in the registry. Check the name at ${registryHome2(registry)}.`);
|
|
2650
4570
|
const rel = `assets/icons/${pack}/${name}.svg`;
|
|
2651
|
-
const abs =
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
lock.icons[id] = { pack, name, hash:
|
|
4571
|
+
const abs = join14(designDir, rel);
|
|
4572
|
+
mkdirSync9(dirname9(abs), { recursive: true });
|
|
4573
|
+
writeFileSync10(abs, svg);
|
|
4574
|
+
lock.icons[id] = { pack, name, hash: sha122(svg), spdx: lic.spdx, path: rel };
|
|
2655
4575
|
lock.packs[pack] = { spdx: lic.spdx, title: lic.title, url: lic.url, attribution: lic.attribution, modifiedNotice: lic.modifiedNotice };
|
|
2656
4576
|
svgById.set(id, svg);
|
|
2657
4577
|
}
|
|
2658
4578
|
lock.registry = registry;
|
|
2659
|
-
|
|
4579
|
+
writeIfChanged2(lockPath, serializeLock2(lock));
|
|
2660
4580
|
}
|
|
2661
4581
|
if (Object.keys(lock.icons).length)
|
|
2662
|
-
|
|
4582
|
+
writeIfChanged2(join14(designDir, "ICON-NOTICES.md"), renderNotices2(lock));
|
|
2663
4583
|
return svgById;
|
|
2664
4584
|
};
|
|
2665
4585
|
var applyAssetIcons = async (entries, designDir, opts = {}) => {
|
|
@@ -2683,7 +4603,7 @@ var compileDesignScreen = async (opts) => {
|
|
|
2683
4603
|
const entries = [];
|
|
2684
4604
|
const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
|
|
2685
4605
|
for (const file of scan.files) {
|
|
2686
|
-
const type = manifest?.files[file.path]?.type ?? classifyKind(
|
|
4606
|
+
const type = manifest?.files[file.path]?.type ?? classifyKind(basename5(file.path));
|
|
2687
4607
|
if (type === "resource") {
|
|
2688
4608
|
entries.push({
|
|
2689
4609
|
path: file.path,
|
|
@@ -2694,7 +4614,7 @@ var compileDesignScreen = async (opts) => {
|
|
|
2694
4614
|
} else if (type === "shader") {
|
|
2695
4615
|
entries.push({ path: file.path, type, shaders: [] });
|
|
2696
4616
|
} else {
|
|
2697
|
-
entries.push({ path: file.path, type, text:
|
|
4617
|
+
entries.push({ path: file.path, type, text: readFileSync13(file.absPath, "utf8") });
|
|
2698
4618
|
}
|
|
2699
4619
|
}
|
|
2700
4620
|
const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()";
|
|
@@ -2708,6 +4628,18 @@ var compileDesignScreen = async (opts) => {
|
|
|
2708
4628
|
`
|
|
2709
4629
|
});
|
|
2710
4630
|
await applyAssetIcons(entries, designDir);
|
|
4631
|
+
await applyProjectFonts(entries, {
|
|
4632
|
+
mode: "vendor",
|
|
4633
|
+
root,
|
|
4634
|
+
vendorDir: designDir,
|
|
4635
|
+
vendorPrefix: designKey,
|
|
4636
|
+
makeResourceEntry: (path, absPath) => ({
|
|
4637
|
+
path,
|
|
4638
|
+
type: "resource",
|
|
4639
|
+
fileSrc: opts.assets === "disk" ? encodeURIComponent(absPath) : encodeURI(path),
|
|
4640
|
+
absPath
|
|
4641
|
+
})
|
|
4642
|
+
});
|
|
2711
4643
|
return compileProject2({
|
|
2712
4644
|
entries,
|
|
2713
4645
|
name: opts.name,
|
|
@@ -2754,10 +4686,10 @@ var openBrowser = (url) => {
|
|
|
2754
4686
|
};
|
|
2755
4687
|
var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
|
|
2756
4688
|
const state = randomBytes(16).toString("hex");
|
|
2757
|
-
let
|
|
4689
|
+
let resolve5;
|
|
2758
4690
|
let reject;
|
|
2759
4691
|
const tokenPromise = new Promise((res, rej) => {
|
|
2760
|
-
|
|
4692
|
+
resolve5 = res;
|
|
2761
4693
|
reject = rej;
|
|
2762
4694
|
});
|
|
2763
4695
|
const server = createServer((req, res) => {
|
|
@@ -2775,7 +4707,7 @@ var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser
|
|
|
2775
4707
|
}
|
|
2776
4708
|
res.writeHead(200, { "content-type": "text/html" });
|
|
2777
4709
|
res.end(SUCCESS_HTML);
|
|
2778
|
-
|
|
4710
|
+
resolve5(token);
|
|
2779
4711
|
});
|
|
2780
4712
|
await new Promise((res, rej) => {
|
|
2781
4713
|
server.once("error", rej);
|
|
@@ -2839,6 +4771,9 @@ Behaviour that isn't visible in a single screen — validation, permissions, not
|
|
|
2839
4771
|
cases — and the decisions you've made and why, so they aren't re-litigated later.
|
|
2840
4772
|
`;
|
|
2841
4773
|
var TOKENS_TS = `// Design tokens shared by every screen. Colors are hex strings (the engine parses hex only).
|
|
4774
|
+
// App font: one theme() call here sets the default text font everywhere. font("id") installs a
|
|
4775
|
+
// registry family (vendored at compile time) and returns its name — no loading code needed.
|
|
4776
|
+
// theme({ fontFamily: font("manrope") })
|
|
2842
4777
|
export const colors = {
|
|
2843
4778
|
bg: "#101114",
|
|
2844
4779
|
card: "#1c1e24",
|
|
@@ -2852,6 +4787,57 @@ export const spacing = { xs: 4, sm: 8, md: 16, lg: 24 }
|
|
|
2852
4787
|
|
|
2853
4788
|
export const radius = { sm: 8, md: 12, lg: 20 }
|
|
2854
4789
|
`;
|
|
4790
|
+
var TABS_TS = `// The app's tab bar, declared ONCE — the design canvas discovers it from this file.
|
|
4791
|
+
//
|
|
4792
|
+
// export const mainTabs = defineTabs({
|
|
4793
|
+
// home: { label: "Home", icon: assetIcon("lucide:house") },
|
|
4794
|
+
// news: { label: "News", icon: assetIcon("lucide:newspaper") },
|
|
4795
|
+
// })
|
|
4796
|
+
//
|
|
4797
|
+
// The KEYS are screen ids — each tab's root screen (screens/<key>.ts), in tab order, literals only.
|
|
4798
|
+
// Every screen that shows the bar mounts it with a literal tab id — tab roots AND pushed details
|
|
4799
|
+
// that keep the bar:
|
|
4800
|
+
//
|
|
4801
|
+
// export default () => mainTabs.screen("news", [ ...content ])
|
|
4802
|
+
//
|
|
4803
|
+
// A screen that doesn't mount it is a full-screen push (bar hidden). Never draw meta.json edges
|
|
4804
|
+
// between tab screens — the bar already navigates; the board renders it as a rail + lanes.
|
|
4805
|
+
// Restyle the bar below freely: only the defineTabs({ keys }) declaration and the
|
|
4806
|
+
// .screen("<tab>", …) / .bar("<tab>") call shapes are parsed.
|
|
4807
|
+
|
|
4808
|
+
type TabDef = { label: string, icon?: { svg: string, tintColor: string | null } }
|
|
4809
|
+
|
|
4810
|
+
// Theme-variable driven — theme({ primaryColor, mutedColor, tabbarBg, screenBg }) anywhere
|
|
4811
|
+
// restyles every screen's bar at once; the fallbacks keep an untouched scaffold looking right.
|
|
4812
|
+
const ACTIVE = "var(--primaryColor, #5b8cff)"
|
|
4813
|
+
const INACTIVE = "var(--mutedColor, #8a919e)"
|
|
4814
|
+
const BAR_BG = "var(--tabbarBg, #15171c)"
|
|
4815
|
+
const SCREEN_BG = "var(--screenBg, #101114)"
|
|
4816
|
+
|
|
4817
|
+
export const defineTabs = <T extends Record<string, TabDef>>(tabs: T) => {
|
|
4818
|
+
const ids = Object.keys(tabs) as (keyof T & string)[]
|
|
4819
|
+
|
|
4820
|
+
/** The bar alone — mount it yourself when the screen owns its whole layout. */
|
|
4821
|
+
const bar = (active: keyof T & string) =>
|
|
4822
|
+
UIRow(ids.map((id) => {
|
|
4823
|
+
const color = id === active ? ACTIVE : INACTIVE
|
|
4824
|
+
return UIButton([
|
|
4825
|
+
...(tabs[id].icon ? [UIImage(tabs[id].icon!).style({ width: 22, height: 22, tintColor: color })] : []),
|
|
4826
|
+
UIText(tabs[id].label).style({ fontSize: 10, color }),
|
|
4827
|
+
]).style({ name: \`tab-\${id}\`, flexDirection: "column", gap: 3, flexGrow: 1, flexBasis: 0, pt: 8, pb: 6 })
|
|
4828
|
+
})).style({ bgColor: BAR_BG, pb: "safe-bottom" })
|
|
4829
|
+
|
|
4830
|
+
/** A screen that sits ON the bar (the common case): [ content, bar ] — the same column the real
|
|
4831
|
+
* app composes as [ UIPager, bar ]. */
|
|
4832
|
+
const screen = (active: keyof T & string, children: UINodeChild[]) =>
|
|
4833
|
+
UIScreen([
|
|
4834
|
+
UIColumn(children).style({ flexGrow: 1, p: 16, pt: "safe-top" }),
|
|
4835
|
+
bar(active),
|
|
4836
|
+
]).style({ bgColor: SCREEN_BG })
|
|
4837
|
+
|
|
4838
|
+
return { ids, bar, screen }
|
|
4839
|
+
}
|
|
4840
|
+
`;
|
|
2855
4841
|
var HOME_SCREEN_TS = `import { colors, spacing, radius } from "../shared/tokens"
|
|
2856
4842
|
|
|
2857
4843
|
// Mock data: the domain shown on realistic values. Inline, per screen.
|
|
@@ -2930,6 +4916,12 @@ positions or descriptions.
|
|
|
2930
4916
|
|
|
2931
4917
|
- An edge's optional \`activator\` names the element (its \`name\` style option) that triggers the
|
|
2932
4918
|
transition. Give a \`name\` to interactive elements an edge refers to — nothing else needs names.
|
|
4919
|
+
- **Edges are forward intent only.** Never draw back/dismiss/logout edges — the navigation stack
|
|
4920
|
+
implies return paths. And never draw edges between tab screens — the tab bar already navigates
|
|
4921
|
+
(see "The tab bar" below); the board flags bar-implied edges for deletion.
|
|
4922
|
+
- **An edge from a screen to itself** (\`"from": "home", "to": "home@empty"\`) is a *state link*: the
|
|
4923
|
+
board draws it as a chip on the activator element (clicking it switches the tile's state), never
|
|
4924
|
+
as an arrow. Use it to record "tapping X puts this screen into state Y".
|
|
2933
4925
|
- **Group each screen** you create by its user-facing area — \`"group": "Onboarding"\` — 2–6 screens
|
|
2934
4926
|
per group (Onboarding, Checkout, Settings…). Groups render as labeled regions and \`arrange\` lays
|
|
2935
4927
|
each out as its own sub-flow, so grouping keeps big boards legible. Semantic clustering is your
|
|
@@ -2977,6 +4969,25 @@ keep in the chat.
|
|
|
2977
4969
|
typing a component's return value or a container's children — with no imports. Trust the types;
|
|
2978
4970
|
don't fall back to \`any\`.
|
|
2979
4971
|
|
|
4972
|
+
## The tab bar
|
|
4973
|
+
|
|
4974
|
+
Most apps have one — declare it ONCE, in source, and never draw edges for it:
|
|
4975
|
+
|
|
4976
|
+
- \`shared/tabs.ts\` ships a \`defineTabs\` helper. Declare the bar there:
|
|
4977
|
+
\`export const mainTabs = defineTabs({ home: { label: "Home", icon: assetIcon("lucide:house") }, … })\`
|
|
4978
|
+
— the **keys are screen ids** (each tab's root screen, in tab order), literals only.
|
|
4979
|
+
- Every screen that shows the bar mounts it with a **literal** tab id:
|
|
4980
|
+
\`export default () => mainTabs.screen("news", [ …content ])\` — tab roots AND pushed detail
|
|
4981
|
+
screens that keep the bar (the news lane's article screen mounts \`"news"\` too). A screen that
|
|
4982
|
+
doesn't mount it is a full-screen push (bar hidden), e.g. a photo viewer.
|
|
4983
|
+
- The canvas discovers all of this from the source (like screen states) and draws the bar as a
|
|
4984
|
+
**rail + lane bands** — tab switching needs NO edges, and \`design_state\` serves the discovered
|
|
4985
|
+
\`hubs\`. In the live prototype, tapping a tab button jumps the board to that tab's root screen.
|
|
4986
|
+
- Style through theme variables: the bar reads \`var(--primaryColor)\`, \`var(--mutedColor)\`,
|
|
4987
|
+
\`var(--tabbarBg)\`, \`var(--screenBg)\` (with fallbacks) — one \`theme({ primaryColor: … })\` call
|
|
4988
|
+
restyles it app-wide. You may restyle \`shared/tabs.ts\` itself; keep the \`defineTabs({ … })\`
|
|
4989
|
+
declaration and \`.screen("<tab>", …)\` call shapes intact (the canvas parses them).
|
|
4990
|
+
|
|
2980
4991
|
## Screen states (variants)
|
|
2981
4992
|
|
|
2982
4993
|
A screen often has more than one look — a list vs. its empty state, a form vs. its error state.
|
|
@@ -2984,10 +4995,9 @@ Instead of a second file per variant, make the default export a **function of a
|
|
|
2984
4995
|
type is a string-literal union** — the union IS the state list (no separate schema):
|
|
2985
4996
|
|
|
2986
4997
|
\`\`\`ts
|
|
2987
|
-
export default (state: "default" | "empty" = "default") =>
|
|
4998
|
+
export default (state: "default" | "empty" = "default") => mainTabs.screen("home", [
|
|
2988
4999
|
Header(child),
|
|
2989
5000
|
state === "empty" ? EmptyState("Nothing here yet") : UIScrollable(items.map(Card)),
|
|
2990
|
-
TabBar(tabs, "home"),
|
|
2991
5001
|
])
|
|
2992
5002
|
\`\`\`
|
|
2993
5003
|
|
|
@@ -3005,6 +5015,35 @@ export default (state: "default" | "empty" = "default") => UIScreen([
|
|
|
3005
5015
|
\`"home@empty"\`) — still one node per screen. \`lecodes design check\` errors on an edge pointing at
|
|
3006
5016
|
a state the screen doesn't declare.
|
|
3007
5017
|
|
|
5018
|
+
## Overlays (sheets & dialogs)
|
|
5019
|
+
|
|
5020
|
+
A bottom sheet or confirm dialog is a **\`UIWidget\`**, not an absolutely-positioned child of the
|
|
5021
|
+
screen. Its \`overlayColor\` is the scrim — a real modal layer: it blocks everything beneath it
|
|
5022
|
+
(taps AND the canvas's hover/hints), and a tap on it fires \`onOverlayTap\`. \`show()\` it from the
|
|
5023
|
+
state branch instead of embedding it:
|
|
5024
|
+
|
|
5025
|
+
\`\`\`ts
|
|
5026
|
+
const FilterSheet = () => {
|
|
5027
|
+
const sheet = UIWidget(
|
|
5028
|
+
{ left: 0, right: 0, bottom: 0, p: 16, gap: 8, overlayColor: "#0008" },
|
|
5029
|
+
[ SheetCard([ … ]), PrimaryButton("Apply", { name: "apply" }) ],
|
|
5030
|
+
)
|
|
5031
|
+
return sheet.onOverlayTap(() => sheet.hide())
|
|
5032
|
+
}
|
|
5033
|
+
|
|
5034
|
+
export default (state: "default" | "filters" = "default") => {
|
|
5035
|
+
if (state === "filters") FilterSheet().show()
|
|
5036
|
+
return UIScreen([ … ])
|
|
5037
|
+
}
|
|
5038
|
+
\`\`\`
|
|
5039
|
+
|
|
5040
|
+
- Size the widget box to the **content only** (bottom-anchor a sheet, or center a dialog card
|
|
5041
|
+
inside a full-inset box) — the scrim is the \`overlayColor\`, painted full-screen behind it.
|
|
5042
|
+
- A tap lands **outside the widget box** → \`onOverlayTap\` (dismiss a sheet there). A confirm
|
|
5043
|
+
dialog that demands an explicit choice uses a full-inset box and no \`onOverlayTap\`.
|
|
5044
|
+
- Headless snapshots render widgets as \`overlays\` in the JSON and composite them in the PNG,
|
|
5045
|
+
and the edge \`activator\` names on widget elements (\`"apply"\`, \`"confirm"\`) work as usual.
|
|
5046
|
+
|
|
3008
5047
|
## Icons
|
|
3009
5048
|
|
|
3010
5049
|
Use real icons instead of emoji. \`assetIcon("pack:name")\` is a **compile-time macro**: it vendors
|
|
@@ -3027,6 +5066,28 @@ UIImage(assetIcon("lucide:check")).style({ width: 20, height: 20, tintColor: col
|
|
|
3027
5066
|
(attribution) — **commit these**; they make rebuilds offline. An unknown \`pack:name\` is a clear
|
|
3028
5067
|
compile error.
|
|
3029
5068
|
|
|
5069
|
+
## Fonts
|
|
5070
|
+
|
|
5071
|
+
Real typography sells a design. \`font("id")\` installs a font from the registry AND returns its
|
|
5072
|
+
family name — no loading code, no awaiting, ever:
|
|
5073
|
+
|
|
5074
|
+
\`\`\`ts
|
|
5075
|
+
theme({ fontFamily: font("manrope") }) // app font, in shared/tokens.ts
|
|
5076
|
+
UIText("Hero").style({ fontFamily: font("unbounded"), fontSize: 34 }) // display accents per node
|
|
5077
|
+
font("ibm-plex-sans", { weights: [400, 600] }) // pick exact weights
|
|
5078
|
+
\`\`\`
|
|
5079
|
+
|
|
5080
|
+
- Registry ids (all OFL, cyrillic-capable unless noted):
|
|
5081
|
+
sans \`inter manrope golos-text onest rubik nunito montserrat ibm-plex-sans pt-sans jost exo-2\`;
|
|
5082
|
+
display \`unbounded russo-one oswald comfortaa space-grotesk\` (latin only);
|
|
5083
|
+
serif \`playfair-display lora pt-serif cormorant literata\`;
|
|
5084
|
+
mono \`jetbrains-mono fira-code ibm-plex-mono\`; handwriting \`caveat pacifico\`.
|
|
5085
|
+
- Default weights are ~400/500/700 — install the weights you use (\`fontWeight: 600\` wants a 600
|
|
5086
|
+
face). The id must be a **string literal**; an unknown one is a clear compile error.
|
|
5087
|
+
- Your own file works too: \`font("./fonts/Brand.ttf")\` — family/weight are read from the file.
|
|
5088
|
+
- Vendored faces land in \`assets/fonts/\`, tracked by \`fonts.lock.json\` + \`FONT-NOTICES.md\` —
|
|
5089
|
+
**commit these**; they make rebuilds offline and they're what the platform board renders from.
|
|
5090
|
+
|
|
3030
5091
|
## See what you built
|
|
3031
5092
|
|
|
3032
5093
|
The developer watches the canvas live (screens hot-reload on save). To see a screen yourself:
|
|
@@ -3077,6 +5138,15 @@ var DESIGN_MACROS_DTS = `// Generated by \`lecodes design init\`. Globals the de
|
|
|
3077
5138
|
* string literal (resolved, and the SVG vendored, at compile time).
|
|
3078
5139
|
*/
|
|
3079
5140
|
declare function assetIcon(id: string, opts?: { color?: string }): { readonly svg: string, tintColor: string | null }
|
|
5141
|
+
|
|
5142
|
+
/**
|
|
5143
|
+
* Install a font and get its family name back — using a font IS declaring it. A registry id
|
|
5144
|
+
* (\`font("manrope")\`) vendors faces from the font registry at compile time ({ weights, italic }
|
|
5145
|
+
* narrows the set); a project path (\`font("./fonts/Brand.ttf")\`) reads family/weight/italic from
|
|
5146
|
+
* the file itself. The argument must be a string literal. No awaiting — pass the result straight
|
|
5147
|
+
* to \`theme({ fontFamily: font("manrope") })\` or a \`fontFamily\` style.
|
|
5148
|
+
*/
|
|
5149
|
+
declare function font(id: string, opts?: { weights?: number[], italic?: boolean }): string
|
|
3080
5150
|
`;
|
|
3081
5151
|
var README_HINT = `# design/
|
|
3082
5152
|
|
|
@@ -3101,33 +5171,33 @@ var getContext = (args) => {
|
|
|
3101
5171
|
return {
|
|
3102
5172
|
root,
|
|
3103
5173
|
manifest,
|
|
3104
|
-
name: manifest?.name ??
|
|
5174
|
+
name: manifest?.name ?? basename6(root),
|
|
3105
5175
|
dirName,
|
|
3106
|
-
designDir:
|
|
5176
|
+
designDir: join15(root, dirName),
|
|
3107
5177
|
designKey: "/" + dirName.replace(/\\/g, "/")
|
|
3108
5178
|
};
|
|
3109
5179
|
};
|
|
3110
5180
|
var listScreens = (designDir) => {
|
|
3111
|
-
const dir =
|
|
3112
|
-
if (!
|
|
5181
|
+
const dir = join15(designDir, "screens");
|
|
5182
|
+
if (!existsSync13(dir))
|
|
3113
5183
|
return [];
|
|
3114
|
-
return
|
|
5184
|
+
return readdirSync7(dir).filter((f2) => f2.endsWith(".ts") && !f2.startsWith(".")).map((f2) => f2.slice(0, -3)).sort();
|
|
3115
5185
|
};
|
|
3116
5186
|
var readMetaSizes = (designDir) => {
|
|
3117
5187
|
try {
|
|
3118
|
-
const meta = JSON.parse(
|
|
5188
|
+
const meta = JSON.parse(readFileSync14(join15(designDir, "meta.json"), "utf8"));
|
|
3119
5189
|
return meta?.screens ?? {};
|
|
3120
5190
|
} catch {
|
|
3121
5191
|
return {};
|
|
3122
5192
|
}
|
|
3123
5193
|
};
|
|
3124
5194
|
var writeMcpConfig = (ctx, port) => {
|
|
3125
|
-
const path =
|
|
5195
|
+
const path = join15(ctx.root, ".mcp.json");
|
|
3126
5196
|
const entry = { type: "http", url: `http://localhost:${port}/mcp` };
|
|
3127
5197
|
let config = {};
|
|
3128
|
-
if (
|
|
5198
|
+
if (existsSync13(path)) {
|
|
3129
5199
|
try {
|
|
3130
|
-
config = JSON.parse(
|
|
5200
|
+
config = JSON.parse(readFileSync14(path, "utf8"));
|
|
3131
5201
|
} catch {
|
|
3132
5202
|
warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
|
|
3133
5203
|
return;
|
|
@@ -3140,7 +5210,7 @@ var writeMcpConfig = (ctx, port) => {
|
|
|
3140
5210
|
return;
|
|
3141
5211
|
}
|
|
3142
5212
|
config.mcpServers["lecodes-design"] = entry;
|
|
3143
|
-
|
|
5213
|
+
writeFileSync11(path, JSON.stringify(config, null, 2) + `
|
|
3144
5214
|
`);
|
|
3145
5215
|
log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
|
|
3146
5216
|
};
|
|
@@ -3150,25 +5220,26 @@ var init = async (ctx, args) => {
|
|
|
3150
5220
|
["spec.md", SPEC_MD],
|
|
3151
5221
|
["README.md", README_HINT],
|
|
3152
5222
|
["CLAUDE.md", CLAUDE_MD],
|
|
3153
|
-
[
|
|
3154
|
-
[
|
|
5223
|
+
[join15("shared", "tokens.ts"), TOKENS_TS],
|
|
5224
|
+
[join15("shared", "tabs.ts"), TABS_TS],
|
|
5225
|
+
[join15("screens", "home.ts"), HOME_SCREEN_TS]
|
|
3155
5226
|
];
|
|
3156
|
-
|
|
3157
|
-
|
|
5227
|
+
mkdirSync10(join15(ctx.designDir, "screens"), { recursive: true });
|
|
5228
|
+
mkdirSync10(join15(ctx.designDir, "shared"), { recursive: true });
|
|
3158
5229
|
let created = 0;
|
|
3159
5230
|
for (const [rel, content] of files) {
|
|
3160
|
-
const abs =
|
|
3161
|
-
if (
|
|
5231
|
+
const abs = join15(ctx.designDir, rel);
|
|
5232
|
+
if (existsSync13(abs)) {
|
|
3162
5233
|
note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
|
|
3163
5234
|
continue;
|
|
3164
5235
|
}
|
|
3165
|
-
|
|
5236
|
+
writeFileSync11(abs, content);
|
|
3166
5237
|
log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
|
|
3167
5238
|
created++;
|
|
3168
5239
|
}
|
|
3169
|
-
const ignorePath =
|
|
5240
|
+
const ignorePath = join15(ctx.root, ".lecodesignore");
|
|
3170
5241
|
const ignoreLine = `${ctx.dirName}/.snapshots/`;
|
|
3171
|
-
const current =
|
|
5242
|
+
const current = existsSync13(ignorePath) ? readFileSync14(ignorePath, "utf8") : "";
|
|
3172
5243
|
if (!current.split(/\r?\n/).includes(ignoreLine)) {
|
|
3173
5244
|
appendFileSync(ignorePath, (current && !current.endsWith(`
|
|
3174
5245
|
`) ? `
|
|
@@ -3179,8 +5250,8 @@ var init = async (ctx, args) => {
|
|
|
3179
5250
|
writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
|
|
3180
5251
|
const wroteTypes = materializeTypesLocal(ctx.designDir);
|
|
3181
5252
|
writeTsconfig(ctx.designDir);
|
|
3182
|
-
|
|
3183
|
-
|
|
5253
|
+
mkdirSync10(join15(ctx.designDir, ".lecodes", "types"), { recursive: true });
|
|
5254
|
+
writeFileSync11(join15(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
|
|
3184
5255
|
log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
|
|
3185
5256
|
if (!wroteTypes)
|
|
3186
5257
|
warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
|
|
@@ -3229,16 +5300,16 @@ var snapshot = async (ctx, args) => {
|
|
|
3229
5300
|
killer.unref?.();
|
|
3230
5301
|
const png = flagBool(args, "png") || flagStr(args, "png") !== undefined;
|
|
3231
5302
|
const sizes = readMetaSizes(ctx.designDir);
|
|
3232
|
-
const outDir = flagStr(args, "out-dir") ??
|
|
5303
|
+
const outDir = flagStr(args, "out-dir") ?? join15(ctx.designDir, ".snapshots");
|
|
3233
5304
|
const single = targets.length === 1 && requested.length === 1 && !png && !allStates && !flagStr(args, "out-dir");
|
|
3234
5305
|
const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
|
|
3235
5306
|
if (!single)
|
|
3236
|
-
|
|
5307
|
+
mkdirSync10(outDir, { recursive: true });
|
|
3237
5308
|
for (const id of targets) {
|
|
3238
5309
|
const [width, height] = sizes[id]?.size ?? [390, 844];
|
|
3239
5310
|
let plan = [{ state: undefined, bases: [id] }];
|
|
3240
5311
|
if (discover) {
|
|
3241
|
-
const { states, canonical } = discover(
|
|
5312
|
+
const { states, canonical } = discover(readFileSync14(join15(ctx.designDir, "screens", `${id}.ts`), "utf8"));
|
|
3242
5313
|
if (stateFlag !== undefined) {
|
|
3243
5314
|
if (!states.includes(stateFlag)) {
|
|
3244
5315
|
if (requested.length === 1)
|
|
@@ -3279,18 +5350,18 @@ var snapshot = async (ctx, args) => {
|
|
|
3279
5350
|
for (const w of pngResult.warnings)
|
|
3280
5351
|
warnErr(`${tag}: ${w}`);
|
|
3281
5352
|
for (const base of bases) {
|
|
3282
|
-
|
|
3283
|
-
log(`${c.green("✓")} ${tag} → ${
|
|
5353
|
+
writeFileSync11(join15(outDir, `${base}.json`), result.text);
|
|
5354
|
+
log(`${c.green("✓")} ${tag} → ${join15(outDir, `${base}.json`)}`);
|
|
3284
5355
|
if (pngResult) {
|
|
3285
|
-
|
|
3286
|
-
log(`${c.green("✓")} ${tag} → ${
|
|
5356
|
+
writeFileSync11(join15(outDir, `${base}.png`), pngResult.png);
|
|
5357
|
+
log(`${c.green("✓")} ${tag} → ${join15(outDir, `${base}.png`)}`);
|
|
3287
5358
|
}
|
|
3288
5359
|
}
|
|
3289
5360
|
}
|
|
3290
5361
|
}
|
|
3291
5362
|
};
|
|
3292
5363
|
var serve = async (ctx, args) => {
|
|
3293
|
-
if (!
|
|
5364
|
+
if (!existsSync13(ctx.designDir)) {
|
|
3294
5365
|
throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
|
|
3295
5366
|
}
|
|
3296
5367
|
let designServer;
|
|
@@ -3363,7 +5434,7 @@ var serve = async (ctx, args) => {
|
|
|
3363
5434
|
openBrowser(server.url);
|
|
3364
5435
|
};
|
|
3365
5436
|
var arrange = async (ctx, args) => {
|
|
3366
|
-
if (!
|
|
5437
|
+
if (!existsSync13(ctx.designDir)) {
|
|
3367
5438
|
throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
|
|
3368
5439
|
}
|
|
3369
5440
|
let designServer;
|
|
@@ -3399,7 +5470,7 @@ var share = async (ctx, args) => {
|
|
|
3399
5470
|
note("Turn it off with: lecodes design share --off");
|
|
3400
5471
|
};
|
|
3401
5472
|
var check = async (ctx, args) => {
|
|
3402
|
-
if (!
|
|
5473
|
+
if (!existsSync13(ctx.designDir)) {
|
|
3403
5474
|
throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
|
|
3404
5475
|
}
|
|
3405
5476
|
let designServer;
|
|
@@ -3436,6 +5507,14 @@ var check = async (ctx, args) => {
|
|
|
3436
5507
|
log(` ${c.yellow("!")} ${s.id}: ${s.statesWarning}`);
|
|
3437
5508
|
log("");
|
|
3438
5509
|
}
|
|
5510
|
+
if (state.hubs.length) {
|
|
5511
|
+
log(c.bold("Tab bars"));
|
|
5512
|
+
for (const h of state.hubs) {
|
|
5513
|
+
const mounted = Object.keys(h.lanes).length;
|
|
5514
|
+
log(` ${h.name} (${h.file}): ${h.tabs.join(" · ")} — ${mounted} screen${mounted === 1 ? "" : "s"} on the bar`);
|
|
5515
|
+
}
|
|
5516
|
+
log("");
|
|
5517
|
+
}
|
|
3439
5518
|
const hard = state.warnings.length;
|
|
3440
5519
|
if (hard === 0) {
|
|
3441
5520
|
const n = state.screens.length;
|
|
@@ -3464,15 +5543,15 @@ var design = async (args) => {
|
|
|
3464
5543
|
};
|
|
3465
5544
|
|
|
3466
5545
|
// src/commands/dev.ts
|
|
3467
|
-
import { existsSync as
|
|
5546
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync15, watch, writeFileSync as writeFileSync12 } from "node:fs";
|
|
3468
5547
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
3469
5548
|
import { networkInterfaces } from "node:os";
|
|
3470
|
-
import { basename as
|
|
5549
|
+
import { basename as basename7, join as join17 } from "node:path";
|
|
3471
5550
|
|
|
3472
5551
|
// src/dev/devServer.ts
|
|
3473
5552
|
import { createServer as createServer2 } from "node:http";
|
|
3474
|
-
import { createReadStream, existsSync as
|
|
3475
|
-
import { join as
|
|
5553
|
+
import { createReadStream, existsSync as existsSync14, statSync as statSync3 } from "node:fs";
|
|
5554
|
+
import { join as join16 } from "node:path";
|
|
3476
5555
|
|
|
3477
5556
|
// src/dev/clientTemplates.ts
|
|
3478
5557
|
var DEV_SCREEN_TS = `const title = "LeCodes Dev"
|
|
@@ -3587,9 +5666,9 @@ var renderPrelude = (p) => DEV_PRELUDE_JS.split("__DEV_ORIGIN__").join(p.origin)
|
|
|
3587
5666
|
var renderDevScreen = (name) => DEV_SCREEN_TS.split("__DEV_NAME__").join(JSON.stringify(name));
|
|
3588
5667
|
|
|
3589
5668
|
// src/dev/wsServer.ts
|
|
3590
|
-
import { createHash as
|
|
5669
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3591
5670
|
var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
3592
|
-
var acceptKey = (key) =>
|
|
5671
|
+
var acceptKey = (key) => createHash4("sha1").update(key + WS_GUID).digest("base64");
|
|
3593
5672
|
var encodeTextFrame = (text) => {
|
|
3594
5673
|
const payload = Buffer.from(text, "utf8");
|
|
3595
5674
|
const len = payload.length;
|
|
@@ -3736,14 +5815,14 @@ var sendText = (res, status, type, body) => {
|
|
|
3736
5815
|
res.end(body);
|
|
3737
5816
|
};
|
|
3738
5817
|
var sendJson = (res, status, body) => sendText(res, status, "application/json", JSON.stringify(body));
|
|
3739
|
-
var readBody = (req) => new Promise((
|
|
5818
|
+
var readBody = (req) => new Promise((resolve5) => {
|
|
3740
5819
|
let body = "";
|
|
3741
5820
|
req.on("data", (c2) => {
|
|
3742
5821
|
if (body.length < 256 * 1024)
|
|
3743
5822
|
body += c2.toString("utf8");
|
|
3744
5823
|
});
|
|
3745
|
-
req.on("end", () =>
|
|
3746
|
-
req.on("error", () =>
|
|
5824
|
+
req.on("end", () => resolve5(body));
|
|
5825
|
+
req.on("error", () => resolve5(body));
|
|
3747
5826
|
});
|
|
3748
5827
|
var startDevServer = async (options) => {
|
|
3749
5828
|
const { root, manifest, name, origin, token, entryOverride } = options;
|
|
@@ -3756,7 +5835,15 @@ var startDevServer = async (options) => {
|
|
|
3756
5835
|
const compileOnce = async () => {
|
|
3757
5836
|
try {
|
|
3758
5837
|
const { entries, warnings } = collectLocalEntries(root, manifest, `${base}/assets`);
|
|
3759
|
-
const
|
|
5838
|
+
const fontsHeader = await applyProjectFonts(entries, {
|
|
5839
|
+
mode: "vendor",
|
|
5840
|
+
root,
|
|
5841
|
+
vendorDir: root,
|
|
5842
|
+
vendorPrefix: "",
|
|
5843
|
+
publicUrl: origin,
|
|
5844
|
+
makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: `${base}/assets` + encodeURI(path), absPath })
|
|
5845
|
+
});
|
|
5846
|
+
const js = await compileProject2({ entries, name, publicUrl: origin, entryOverride, header: true, fontsHeader });
|
|
3760
5847
|
const changed = bundleJs === null || stripVolatile(js) !== stripVolatile(bundleJs);
|
|
3761
5848
|
if (changed) {
|
|
3762
5849
|
bundleJs = js;
|
|
@@ -3828,8 +5915,8 @@ var startDevServer = async (options) => {
|
|
|
3828
5915
|
} catch {
|
|
3829
5916
|
return sendText(res, 400, "text/plain", "Bad path");
|
|
3830
5917
|
}
|
|
3831
|
-
const abs =
|
|
3832
|
-
if (!
|
|
5918
|
+
const abs = join16(root, key.slice(1));
|
|
5919
|
+
if (!existsSync14(abs) || !statSync3(abs).isFile())
|
|
3833
5920
|
return sendText(res, 404, "text/plain", "Not found");
|
|
3834
5921
|
res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
|
|
3835
5922
|
createReadStream(abs).pipe(res);
|
|
@@ -3872,11 +5959,11 @@ var startDevServer = async (options) => {
|
|
|
3872
5959
|
if (conn)
|
|
3873
5960
|
clients.add(conn);
|
|
3874
5961
|
});
|
|
3875
|
-
await new Promise((
|
|
5962
|
+
await new Promise((resolve5, reject) => {
|
|
3876
5963
|
server.once("error", reject);
|
|
3877
5964
|
server.listen(options.port, options.bind ?? "0.0.0.0", () => {
|
|
3878
5965
|
server.removeListener("error", reject);
|
|
3879
|
-
|
|
5966
|
+
resolve5();
|
|
3880
5967
|
});
|
|
3881
5968
|
});
|
|
3882
5969
|
const address = server.address();
|
|
@@ -3912,24 +5999,24 @@ var pickLanAddress = () => {
|
|
|
3912
5999
|
return null;
|
|
3913
6000
|
};
|
|
3914
6001
|
var loadDevToken = (root) => {
|
|
3915
|
-
const path =
|
|
6002
|
+
const path = join17(root, LECODES_DIR, "dev.json");
|
|
3916
6003
|
try {
|
|
3917
|
-
const token2 = JSON.parse(
|
|
6004
|
+
const token2 = JSON.parse(readFileSync15(path, "utf8")).token;
|
|
3918
6005
|
if (token2 && /^[a-z0-9]{8,}$/.test(token2))
|
|
3919
6006
|
return token2;
|
|
3920
6007
|
} catch {}
|
|
3921
6008
|
const token = randomBytes2(6).toString("hex");
|
|
3922
|
-
|
|
3923
|
-
|
|
6009
|
+
mkdirSync11(join17(root, LECODES_DIR), { recursive: true });
|
|
6010
|
+
writeFileSync12(path, JSON.stringify({ token }, null, 2) + `
|
|
3924
6011
|
`);
|
|
3925
6012
|
return token;
|
|
3926
6013
|
};
|
|
3927
6014
|
var printQr = async (url) => {
|
|
3928
6015
|
try {
|
|
3929
6016
|
const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
|
|
3930
|
-
await new Promise((
|
|
6017
|
+
await new Promise((resolve5) => qrcode.generate(url, { small: true }, (out) => {
|
|
3931
6018
|
log(out);
|
|
3932
|
-
|
|
6019
|
+
resolve5();
|
|
3933
6020
|
}));
|
|
3934
6021
|
} catch {
|
|
3935
6022
|
note("(couldn't render a QR code — open the URL below on the device)");
|
|
@@ -3945,7 +6032,7 @@ var dev = async (args) => {
|
|
|
3945
6032
|
root = process.cwd();
|
|
3946
6033
|
manifest = null;
|
|
3947
6034
|
}
|
|
3948
|
-
const name = manifest?.name ??
|
|
6035
|
+
const name = manifest?.name ?? basename7(root);
|
|
3949
6036
|
if (typeof globalThis.Bun !== "undefined") {
|
|
3950
6037
|
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
6038
|
}
|
|
@@ -4032,15 +6119,15 @@ ${result.error}`);
|
|
|
4032
6119
|
await printQr(server.url);
|
|
4033
6120
|
log(`Scan the QR with the LeCodes app, or open: ${c.bold(server.url)}`);
|
|
4034
6121
|
note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`);
|
|
4035
|
-
if (
|
|
6122
|
+
if (existsSync15(join17(root, LECODES_DIR, "manifest.json")))
|
|
4036
6123
|
note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
|
|
4037
6124
|
log("");
|
|
4038
6125
|
log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
|
|
4039
6126
|
};
|
|
4040
6127
|
|
|
4041
6128
|
// src/serverDiff.ts
|
|
4042
|
-
import { readFileSync as
|
|
4043
|
-
import { basename as
|
|
6129
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
6130
|
+
import { basename as basename8 } from "node:path";
|
|
4044
6131
|
var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
|
|
4045
6132
|
var computeServerDiff = (project, scan) => {
|
|
4046
6133
|
const pathById = buildPaths(project.assets);
|
|
@@ -4050,15 +6137,15 @@ var computeServerDiff = (project, scan) => {
|
|
|
4050
6137
|
continue;
|
|
4051
6138
|
serverByPath.set(pathById.get(asset.id), asset);
|
|
4052
6139
|
}
|
|
4053
|
-
const localByPath = new Map(scan.files.map((
|
|
6140
|
+
const localByPath = new Map(scan.files.map((f2) => [f2.path, f2]));
|
|
4054
6141
|
const entries = [];
|
|
4055
6142
|
const paths = new Set([...serverByPath.keys(), ...localByPath.keys()]);
|
|
4056
6143
|
for (const path of [...paths].sort()) {
|
|
4057
6144
|
const server = serverByPath.get(path);
|
|
4058
6145
|
const local = localByPath.get(path);
|
|
4059
6146
|
if (local && !server) {
|
|
4060
|
-
const text = isTextKind(classifyKind(
|
|
4061
|
-
entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ?
|
|
6147
|
+
const text = isTextKind(classifyKind(basename8(path)));
|
|
6148
|
+
entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync16(local.absPath, "utf8") : undefined });
|
|
4062
6149
|
continue;
|
|
4063
6150
|
}
|
|
4064
6151
|
if (server && !local) {
|
|
@@ -4069,12 +6156,12 @@ var computeServerDiff = (project, scan) => {
|
|
|
4069
6156
|
if (server && local) {
|
|
4070
6157
|
if (isTextAsset(server.type)) {
|
|
4071
6158
|
const serverText = server.text ?? "";
|
|
4072
|
-
const localText =
|
|
6159
|
+
const localText = readFileSync16(local.absPath, "utf8");
|
|
4073
6160
|
if (serverText !== localText)
|
|
4074
6161
|
entries.push({ path, status: "modified", binary: false, serverText, localText });
|
|
4075
6162
|
} else {
|
|
4076
6163
|
const serverSize = server.file?.size;
|
|
4077
|
-
const localSize = Buffer.byteLength(
|
|
6164
|
+
const localSize = Buffer.byteLength(readFileSync16(local.absPath));
|
|
4078
6165
|
if (serverSize !== undefined && serverSize !== localSize) {
|
|
4079
6166
|
entries.push({ path, status: "modified", binary: true });
|
|
4080
6167
|
}
|
|
@@ -4227,8 +6314,8 @@ var diff = async (args) => {
|
|
|
4227
6314
|
var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
|
|
4228
6315
|
|
|
4229
6316
|
// src/commands/init.ts
|
|
4230
|
-
import { existsSync as
|
|
4231
|
-
import { basename as
|
|
6317
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync12, writeFileSync as writeFileSync13 } from "node:fs";
|
|
6318
|
+
import { basename as basename9, dirname as dirname10, join as join18, resolve as resolve5 } from "node:path";
|
|
4232
6319
|
var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
|
|
4233
6320
|
// Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
|
|
4234
6321
|
|
|
@@ -4264,24 +6351,55 @@ const screen = UIScreen(
|
|
|
4264
6351
|
|
|
4265
6352
|
screen.open()
|
|
4266
6353
|
`;
|
|
6354
|
+
var readmeTemplate = (name) => `# ${name}
|
|
6355
|
+
|
|
6356
|
+
A LeCodes app. The whole SDK is available as globals (\`UIScreen\`, \`UIText\`, \`Scene2D\`, …) —
|
|
6357
|
+
no imports needed. \`main.ts\` is the entry point.
|
|
6358
|
+
|
|
6359
|
+
## Run it
|
|
6360
|
+
|
|
6361
|
+
\`\`\`
|
|
6362
|
+
lecodes dev # live on your phone: scan the QR with the LeCodes app, hot reload
|
|
6363
|
+
lecodes compile -o app.js # bundle to a single self-contained JS file
|
|
6364
|
+
\`\`\`
|
|
6365
|
+
|
|
6366
|
+
## Go native
|
|
6367
|
+
|
|
6368
|
+
\`\`\`
|
|
6369
|
+
lecodes app init ios # generate a native Xcode shell in ios/
|
|
6370
|
+
lecodes app sync # embed the compiled bundle + plugins into it
|
|
6371
|
+
lecodes app open # open it in Xcode, build & run
|
|
6372
|
+
\`\`\`
|
|
6373
|
+
|
|
6374
|
+
## Layout
|
|
6375
|
+
|
|
6376
|
+
- \`main.ts\` — the app. Add more top-level .ts files freely; the largest export-free one
|
|
6377
|
+
is picked as the entry point.
|
|
6378
|
+
- \`assets/\` — images/fonts the app references (created on demand)
|
|
6379
|
+
- \`tsconfig.json\` + \`.lecodes/types/\` — generated IDE types (rewritten by the CLI; don't edit)
|
|
6380
|
+
- \`.lecodesignore\` — files a future \`lecodes push\` would skip
|
|
6381
|
+
|
|
6382
|
+
Full command list: \`lecodes --help\`.
|
|
6383
|
+
`;
|
|
4267
6384
|
var init2 = async (args) => {
|
|
4268
6385
|
const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
|
|
4269
|
-
const root =
|
|
4270
|
-
const name =
|
|
4271
|
-
|
|
6386
|
+
const root = resolve5(dirArg);
|
|
6387
|
+
const name = basename9(root);
|
|
6388
|
+
mkdirSync12(root, { recursive: true });
|
|
4272
6389
|
let created = 0;
|
|
4273
6390
|
const write = (rel, content) => {
|
|
4274
|
-
const abs =
|
|
4275
|
-
if (
|
|
6391
|
+
const abs = join18(root, rel);
|
|
6392
|
+
if (existsSync16(abs)) {
|
|
4276
6393
|
note(`kept ${rel}`);
|
|
4277
6394
|
return;
|
|
4278
6395
|
}
|
|
4279
|
-
|
|
4280
|
-
|
|
6396
|
+
mkdirSync12(dirname10(abs), { recursive: true });
|
|
6397
|
+
writeFileSync13(abs, content);
|
|
4281
6398
|
log(`${c.green("created")} ${rel}`);
|
|
4282
6399
|
created++;
|
|
4283
6400
|
};
|
|
4284
6401
|
write("main.ts", STARTER2);
|
|
6402
|
+
write("README.md", readmeTemplate(name));
|
|
4285
6403
|
write(".lecodesignore", DEFAULT_IGNORE_FILE);
|
|
4286
6404
|
const wroteTypes = materializeTypesLocal(root);
|
|
4287
6405
|
writeTsconfig(root);
|
|
@@ -4308,20 +6426,20 @@ var login = async (args) => {
|
|
|
4308
6426
|
};
|
|
4309
6427
|
|
|
4310
6428
|
// src/commands/render.ts
|
|
4311
|
-
import { writeFileSync as
|
|
4312
|
-
import { resolve as
|
|
6429
|
+
import { writeFileSync as writeFileSync14 } from "node:fs";
|
|
6430
|
+
import { resolve as resolve6 } from "node:path";
|
|
4313
6431
|
var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
|
|
4314
6432
|
var DEVICE_SAFE_AREA = {
|
|
4315
|
-
none: [0, 0, 0, 0],
|
|
4316
|
-
iphone: [
|
|
4317
|
-
android: [24, 0,
|
|
6433
|
+
none: { insets: [0, 0, 0, 0], bars: [] },
|
|
6434
|
+
iphone: { insets: [59, 0, 34, 0], bars: [] },
|
|
6435
|
+
android: { insets: [24, 0, 48, 0], bars: ["top", "bottom"] }
|
|
4318
6436
|
};
|
|
4319
6437
|
var resolveSafeArea = (args) => {
|
|
4320
6438
|
const explicit = flagStr(args, "safe-area");
|
|
4321
6439
|
if (explicit) {
|
|
4322
6440
|
const n = explicit.split(",").map((s) => Number(s.trim()));
|
|
4323
6441
|
if (n.length === 4 && n.every((v) => Number.isFinite(v)))
|
|
4324
|
-
return n;
|
|
6442
|
+
return { safeArea: n, safeAreaBars: [] };
|
|
4325
6443
|
throw new CliError(`--safe-area expects "top,right,bottom,left" (4 numbers), got "${explicit}".`);
|
|
4326
6444
|
}
|
|
4327
6445
|
const device = flagStr(args, "device");
|
|
@@ -4329,7 +6447,7 @@ var resolveSafeArea = (args) => {
|
|
|
4329
6447
|
const preset = DEVICE_SAFE_AREA[device];
|
|
4330
6448
|
if (!preset)
|
|
4331
6449
|
throw new CliError(`Unknown --device "${device}". Options: ${Object.keys(DEVICE_SAFE_AREA).join(", ")}.`);
|
|
4332
|
-
return preset;
|
|
6450
|
+
return { safeArea: preset.insets, safeAreaBars: preset.bars };
|
|
4333
6451
|
}
|
|
4334
6452
|
return;
|
|
4335
6453
|
};
|
|
@@ -4352,6 +6470,13 @@ var render = async (args) => {
|
|
|
4352
6470
|
const useLocalAssets = !publicUrlFlag && !flagBool(args, "remote-assets");
|
|
4353
6471
|
const compileEntries = useLocalAssets ? entries.map((e) => e.type === "resource" && e.absPath ? { ...e, fileSrc: encodeURIComponent(e.absPath) } : e) : entries;
|
|
4354
6472
|
const warnings = useLocalAssets ? collectWarnings.filter((w) => !w.includes("No server URL")) : collectWarnings;
|
|
6473
|
+
await applyProjectFonts(compileEntries, useLocalAssets ? {
|
|
6474
|
+
mode: "vendor",
|
|
6475
|
+
root,
|
|
6476
|
+
vendorDir: root,
|
|
6477
|
+
vendorPrefix: "",
|
|
6478
|
+
makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURIComponent(absPath), absPath })
|
|
6479
|
+
} : { mode: "cdn", root });
|
|
4355
6480
|
note("Compiling…");
|
|
4356
6481
|
const js = await compileProject2({
|
|
4357
6482
|
entries: compileEntries,
|
|
@@ -4384,7 +6509,7 @@ var render = async (args) => {
|
|
|
4384
6509
|
localAssets: useLocalAssets,
|
|
4385
6510
|
timeMs: timeStr !== undefined ? Number(timeStr) : undefined,
|
|
4386
6511
|
afterAnimations: !flagBool(args, "no-after-animations"),
|
|
4387
|
-
|
|
6512
|
+
...resolveSafeArea(args),
|
|
4388
6513
|
onConsole
|
|
4389
6514
|
};
|
|
4390
6515
|
const done = (flush) => flush(() => {
|
|
@@ -4405,7 +6530,7 @@ var render = async (args) => {
|
|
|
4405
6530
|
if (isNullSink(pngFlag)) {
|
|
4406
6531
|
note(`Discarded PNG (${pngFlag})`);
|
|
4407
6532
|
} else {
|
|
4408
|
-
|
|
6533
|
+
writeFileSync14(resolve6(pngFlag), png);
|
|
4409
6534
|
note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
|
|
4410
6535
|
}
|
|
4411
6536
|
return done((cb) => cb());
|
|
@@ -4420,7 +6545,7 @@ var render = async (args) => {
|
|
|
4420
6545
|
return done((cb) => cb());
|
|
4421
6546
|
}
|
|
4422
6547
|
if (out) {
|
|
4423
|
-
|
|
6548
|
+
writeFileSync14(resolve6(out), result.text);
|
|
4424
6549
|
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
4425
6550
|
return done((cb) => cb());
|
|
4426
6551
|
}
|
|
@@ -4431,8 +6556,8 @@ var render = async (args) => {
|
|
|
4431
6556
|
};
|
|
4432
6557
|
|
|
4433
6558
|
// src/commands/pull.ts
|
|
4434
|
-
import { existsSync as
|
|
4435
|
-
import { dirname as
|
|
6559
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync13, rmSync as rmSync2, writeFileSync as writeFileSync15 } from "node:fs";
|
|
6560
|
+
import { dirname as dirname11, join as join19 } from "node:path";
|
|
4436
6561
|
var pull = async (args) => {
|
|
4437
6562
|
const root = findProjectRoot(process.cwd());
|
|
4438
6563
|
const manifest = readManifest(root);
|
|
@@ -4452,9 +6577,9 @@ var pull = async (args) => {
|
|
|
4452
6577
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
|
|
4453
6578
|
for (const path of Object.keys(manifest.files)) {
|
|
4454
6579
|
if (!remotePaths.has(path)) {
|
|
4455
|
-
const abs =
|
|
4456
|
-
if (
|
|
4457
|
-
|
|
6580
|
+
const abs = join19(root, ...path.replace(/^\//, "").split("/"));
|
|
6581
|
+
if (existsSync17(abs))
|
|
6582
|
+
rmSync2(abs);
|
|
4458
6583
|
}
|
|
4459
6584
|
}
|
|
4460
6585
|
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
@@ -4491,18 +6616,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
4491
6616
|
const files = { ...manifest.files };
|
|
4492
6617
|
for (const asset of targets) {
|
|
4493
6618
|
const key = pathById.get(asset.id);
|
|
4494
|
-
const abs =
|
|
4495
|
-
|
|
6619
|
+
const abs = join19(root, ...key.replace(/^\//, "").split("/"));
|
|
6620
|
+
mkdirSync13(dirname11(abs), { recursive: true });
|
|
4496
6621
|
let sha;
|
|
4497
6622
|
if (asset.type === "file") {
|
|
4498
6623
|
if (!asset.file?.src)
|
|
4499
6624
|
continue;
|
|
4500
6625
|
const bytes = await downloadBlob(apiUrl, asset.file.src);
|
|
4501
|
-
|
|
6626
|
+
writeFileSync15(abs, bytes);
|
|
4502
6627
|
sha = sha256(bytes);
|
|
4503
6628
|
} else {
|
|
4504
6629
|
const text = asset.text ?? "";
|
|
4505
|
-
|
|
6630
|
+
writeFileSync15(abs, text);
|
|
4506
6631
|
sha = sha256(text);
|
|
4507
6632
|
}
|
|
4508
6633
|
files[key] = { id: asset.id, type: kindOf2(asset.type), sha };
|
|
@@ -4514,8 +6639,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
4514
6639
|
};
|
|
4515
6640
|
|
|
4516
6641
|
// src/commands/push.ts
|
|
4517
|
-
import { basename as
|
|
4518
|
-
import { readFileSync as
|
|
6642
|
+
import { basename as basename10 } from "node:path";
|
|
6643
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
4519
6644
|
var push = async (args) => {
|
|
4520
6645
|
const root = findProjectRoot(process.cwd());
|
|
4521
6646
|
const manifest = readManifest(root);
|
|
@@ -4569,14 +6694,14 @@ var push = async (args) => {
|
|
|
4569
6694
|
let uploaded = 0;
|
|
4570
6695
|
for (const file of scan.files) {
|
|
4571
6696
|
const source = sourceFor(file.path);
|
|
4572
|
-
const kind = source?.type ?? classifyKind(
|
|
6697
|
+
const kind = source?.type ?? classifyKind(basename10(file.path));
|
|
4573
6698
|
const entry = { path: file.path, type: kind };
|
|
4574
6699
|
if (source)
|
|
4575
6700
|
entry.assetId = source.id;
|
|
4576
6701
|
if (isTextKind(kind)) {
|
|
4577
|
-
entry.text =
|
|
6702
|
+
entry.text = readFileSync17(file.absPath, "utf8");
|
|
4578
6703
|
} else if (!source || source.sha !== file.sha) {
|
|
4579
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid,
|
|
6704
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename10(file.path), readFileSync17(file.absPath));
|
|
4580
6705
|
entry.blobFileId = blob.id;
|
|
4581
6706
|
uploaded++;
|
|
4582
6707
|
}
|
|
@@ -4592,7 +6717,7 @@ var push = async (args) => {
|
|
|
4592
6717
|
reportPublish(res);
|
|
4593
6718
|
return;
|
|
4594
6719
|
}
|
|
4595
|
-
const shaByPath = new Map(scan.files.map((
|
|
6720
|
+
const shaByPath = new Map(scan.files.map((f2) => [f2.path, f2.sha]));
|
|
4596
6721
|
const newFiles = {};
|
|
4597
6722
|
for (const node of res.nodes) {
|
|
4598
6723
|
newFiles[node.path] = { id: node.id, type: node.type, sha: shaByPath.get(node.path) ?? "" };
|
|
@@ -4629,22 +6754,22 @@ var reportPublish = (res) => {
|
|
|
4629
6754
|
};
|
|
4630
6755
|
|
|
4631
6756
|
// src/commands/scene.ts
|
|
4632
|
-
import { existsSync as
|
|
4633
|
-
import { basename as
|
|
6757
|
+
import { existsSync as existsSync19, readFileSync as readFileSync19, readdirSync as readdirSync8, statSync as statSync4, writeFileSync as writeFileSync16 } from "node:fs";
|
|
6758
|
+
import { basename as basename11, dirname as dirname12, join as join21, resolve as resolve7 } from "node:path";
|
|
4634
6759
|
|
|
4635
6760
|
// src/compile/sceneCompile.ts
|
|
4636
|
-
import { existsSync as
|
|
4637
|
-
import { join as
|
|
6761
|
+
import { existsSync as existsSync18, readFileSync as readFileSync18 } from "node:fs";
|
|
6762
|
+
import { join as join20 } from "node:path";
|
|
4638
6763
|
var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
|
|
4639
6764
|
var rel = (key) => key.replace(/^\//, "");
|
|
4640
6765
|
var resolveHarness = (loadSceneHarness2) => {
|
|
4641
|
-
const inRepo =
|
|
6766
|
+
const inRepo = join20(distRoot, "..", "projects", "src", "scene-editor");
|
|
4642
6767
|
try {
|
|
4643
|
-
return loadSceneHarness2(
|
|
6768
|
+
return loadSceneHarness2(existsSync18(inRepo) ? inRepo : undefined);
|
|
4644
6769
|
} catch {
|
|
4645
|
-
const vendored =
|
|
4646
|
-
if (
|
|
4647
|
-
return JSON.parse(
|
|
6770
|
+
const vendored = join20(distRoot, "runtime", "scene-harness.json");
|
|
6771
|
+
if (existsSync18(vendored))
|
|
6772
|
+
return JSON.parse(readFileSync18(vendored, "utf8"));
|
|
4648
6773
|
throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
|
|
4649
6774
|
}
|
|
4650
6775
|
};
|
|
@@ -4656,7 +6781,7 @@ var compileSceneEditor = async (opts) => {
|
|
|
4656
6781
|
for (const file of scan.files) {
|
|
4657
6782
|
const path = "./" + rel(file.path);
|
|
4658
6783
|
if (TEXT_EXT.test(path)) {
|
|
4659
|
-
entries.push({ path, type: "text", text:
|
|
6784
|
+
entries.push({ path, type: "text", text: readFileSync18(file.absPath, "utf8") });
|
|
4660
6785
|
} else {
|
|
4661
6786
|
entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path) });
|
|
4662
6787
|
}
|
|
@@ -4696,24 +6821,24 @@ var getContext2 = (args) => {
|
|
|
4696
6821
|
manifest = null;
|
|
4697
6822
|
const pathArg = args._[0];
|
|
4698
6823
|
if (pathArg) {
|
|
4699
|
-
const abs =
|
|
4700
|
-
if (
|
|
6824
|
+
const abs = resolve7(process.cwd(), pathArg);
|
|
6825
|
+
if (existsSync19(abs) && statSync4(abs).isDirectory())
|
|
4701
6826
|
root = abs;
|
|
4702
|
-
else if (abs.endsWith(".scene.ts") &&
|
|
4703
|
-
root =
|
|
6827
|
+
else if (abs.endsWith(".scene.ts") && existsSync19(abs))
|
|
6828
|
+
root = dirname12(abs);
|
|
4704
6829
|
else
|
|
4705
6830
|
throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
|
|
4706
6831
|
}
|
|
4707
6832
|
}
|
|
4708
|
-
return { root, manifest, name: manifest?.name ??
|
|
6833
|
+
return { root, manifest, name: manifest?.name ?? basename11(root) };
|
|
4709
6834
|
};
|
|
4710
6835
|
var writeMcpConfig2 = (root, port) => {
|
|
4711
|
-
const path =
|
|
6836
|
+
const path = join21(root, ".mcp.json");
|
|
4712
6837
|
const entry = { type: "http", url: `http://localhost:${port}/mcp` };
|
|
4713
6838
|
let config = {};
|
|
4714
|
-
if (
|
|
6839
|
+
if (existsSync19(path)) {
|
|
4715
6840
|
try {
|
|
4716
|
-
config = JSON.parse(
|
|
6841
|
+
config = JSON.parse(readFileSync19(path, "utf8"));
|
|
4717
6842
|
} catch {
|
|
4718
6843
|
warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
|
|
4719
6844
|
return;
|
|
@@ -4726,17 +6851,17 @@ var writeMcpConfig2 = (root, port) => {
|
|
|
4726
6851
|
return;
|
|
4727
6852
|
}
|
|
4728
6853
|
config.mcpServers["lecodes-scene"] = entry;
|
|
4729
|
-
|
|
6854
|
+
writeFileSync16(path, JSON.stringify(config, null, 2) + `
|
|
4730
6855
|
`);
|
|
4731
6856
|
note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
|
|
4732
6857
|
};
|
|
4733
6858
|
var countScenes = (dir) => {
|
|
4734
6859
|
let n = 0;
|
|
4735
|
-
for (const name of
|
|
6860
|
+
for (const name of readdirSync8(dir)) {
|
|
4736
6861
|
if (name === "node_modules" || name.startsWith("."))
|
|
4737
6862
|
continue;
|
|
4738
|
-
const full =
|
|
4739
|
-
if (
|
|
6863
|
+
const full = join21(dir, name);
|
|
6864
|
+
if (statSync4(full).isDirectory())
|
|
4740
6865
|
n += countScenes(full);
|
|
4741
6866
|
else if (name.endsWith(".scene.ts"))
|
|
4742
6867
|
n++;
|
|
@@ -4788,12 +6913,12 @@ var status = async (_args) => {
|
|
|
4788
6913
|
info(c.dim("Working tree clean — nothing to push.") + ignoredNote);
|
|
4789
6914
|
return;
|
|
4790
6915
|
}
|
|
4791
|
-
for (const
|
|
4792
|
-
if (!diff2.moves.has(
|
|
4793
|
-
log(c.green(` added ${
|
|
6916
|
+
for (const f2 of diff2.added) {
|
|
6917
|
+
if (!diff2.moves.has(f2.path))
|
|
6918
|
+
log(c.green(` added ${f2.path}`));
|
|
4794
6919
|
}
|
|
4795
|
-
for (const
|
|
4796
|
-
log(c.yellow(` modified ${
|
|
6920
|
+
for (const f2 of diff2.modified)
|
|
6921
|
+
log(c.yellow(` modified ${f2.path}`));
|
|
4797
6922
|
for (const [to, from] of diff2.moves)
|
|
4798
6923
|
log(c.cyan(` moved ${from} → ${to}`));
|
|
4799
6924
|
const movedFrom = new Set(diff2.moves.values());
|
|
@@ -4806,21 +6931,22 @@ var status = async (_args) => {
|
|
|
4806
6931
|
};
|
|
4807
6932
|
|
|
4808
6933
|
// src/index.ts
|
|
4809
|
-
import { existsSync as
|
|
4810
|
-
import {
|
|
6934
|
+
import { existsSync as existsSync21, readFileSync as readFileSync21, realpathSync as realpathSync2 } from "node:fs";
|
|
6935
|
+
import { createRequire as createRequire4 } from "node:module";
|
|
6936
|
+
import { delimiter as delimiter2, dirname as dirname14, join as join23 } from "node:path";
|
|
4811
6937
|
|
|
4812
6938
|
// 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
|
|
6939
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
6940
|
+
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";
|
|
6941
|
+
import { homedir as homedir3 } from "node:os";
|
|
6942
|
+
import { basename as basename12, delimiter, dirname as dirname13, join as join22, resolve as resolve8, sep } from "node:path";
|
|
4817
6943
|
var isWin = process.platform === "win32";
|
|
4818
|
-
var HOME =
|
|
6944
|
+
var HOME = homedir3();
|
|
4819
6945
|
var safeRealpath = (p) => {
|
|
4820
6946
|
try {
|
|
4821
6947
|
return realpathSync(p);
|
|
4822
6948
|
} catch {
|
|
4823
|
-
return
|
|
6949
|
+
return resolve8(p);
|
|
4824
6950
|
}
|
|
4825
6951
|
};
|
|
4826
6952
|
var onPath = (dir) => {
|
|
@@ -4836,25 +6962,25 @@ var onPath = (dir) => {
|
|
|
4836
6962
|
});
|
|
4837
6963
|
};
|
|
4838
6964
|
var install = async (args) => {
|
|
4839
|
-
const bundle =
|
|
4840
|
-
const exeName =
|
|
6965
|
+
const bundle = dirname13(process.execPath);
|
|
6966
|
+
const exeName = basename12(process.execPath);
|
|
4841
6967
|
const chiselName = isWin ? "chisel.exe" : "chisel";
|
|
4842
|
-
if (!
|
|
6968
|
+
if (!existsSync20(join22(bundle, "runtime")) || !existsSync20(join22(bundle, chiselName))) {
|
|
4843
6969
|
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
6970
|
}
|
|
4845
|
-
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((
|
|
6971
|
+
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f2) => existsSync20(join22(bundle, f2)));
|
|
4846
6972
|
if (isWin)
|
|
4847
6973
|
return installWindows(args, bundle, companions);
|
|
4848
6974
|
return installUnix(args, bundle, companions);
|
|
4849
6975
|
};
|
|
4850
6976
|
var copyBundle = (bundle, target, companions) => {
|
|
4851
|
-
|
|
6977
|
+
mkdirSync14(target, { recursive: true });
|
|
4852
6978
|
if (safeRealpath(target) === safeRealpath(bundle))
|
|
4853
6979
|
return;
|
|
4854
|
-
for (const
|
|
4855
|
-
const dest =
|
|
4856
|
-
|
|
4857
|
-
|
|
6980
|
+
for (const f2 of companions) {
|
|
6981
|
+
const dest = join22(target, f2);
|
|
6982
|
+
rmSync3(dest, { recursive: true, force: true });
|
|
6983
|
+
cpSync2(join22(bundle, f2), dest, { recursive: true });
|
|
4858
6984
|
}
|
|
4859
6985
|
};
|
|
4860
6986
|
var permError = (e, dir, system) => {
|
|
@@ -4866,41 +6992,41 @@ var permError = (e, dir, system) => {
|
|
|
4866
6992
|
};
|
|
4867
6993
|
var installUnix = (args, bundle, companions) => {
|
|
4868
6994
|
const system = flagBool(args, "system");
|
|
4869
|
-
const libDir =
|
|
4870
|
-
const binDir =
|
|
4871
|
-
const link =
|
|
6995
|
+
const libDir = resolve8(flagStr(args, "dir") ?? (system ? "/usr/local/lib/lecodes-cli" : join22(HOME, ".local", "lib", "lecodes-cli")));
|
|
6996
|
+
const binDir = resolve8(flagStr(args, "bin") ?? (system ? "/usr/local/bin" : join22(HOME, ".local", "bin")));
|
|
6997
|
+
const link = join22(binDir, "lecodes");
|
|
4872
6998
|
try {
|
|
4873
6999
|
copyBundle(bundle, libDir, companions);
|
|
4874
|
-
chmodSync(
|
|
4875
|
-
chmodSync(
|
|
7000
|
+
chmodSync(join22(libDir, "lecodes"), 493);
|
|
7001
|
+
chmodSync(join22(libDir, "chisel"), 493);
|
|
4876
7002
|
} catch (e) {
|
|
4877
7003
|
throw permError(e, libDir, system);
|
|
4878
7004
|
}
|
|
4879
7005
|
try {
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
symlinkSync(
|
|
7006
|
+
mkdirSync14(binDir, { recursive: true });
|
|
7007
|
+
rmSync3(link, { force: true });
|
|
7008
|
+
symlinkSync(join22(libDir, "lecodes"), link);
|
|
4883
7009
|
} catch (e) {
|
|
4884
7010
|
throw permError(e, binDir, system);
|
|
4885
7011
|
}
|
|
4886
7012
|
success(`Installed to ${c.bold(libDir)}`);
|
|
4887
|
-
log(` ${c.dim(link + " → " +
|
|
7013
|
+
log(` ${c.dim(link + " → " + join22(libDir, "lecodes"))}`);
|
|
4888
7014
|
finishPathUnix(args, binDir);
|
|
4889
7015
|
};
|
|
4890
7016
|
var shellRc = () => {
|
|
4891
|
-
const shell =
|
|
7017
|
+
const shell = basename12(process.env.SHELL ?? "");
|
|
4892
7018
|
if (shell === "zsh")
|
|
4893
|
-
return { file:
|
|
7019
|
+
return { file: join22(HOME, ".zshrc"), name: "~/.zshrc" };
|
|
4894
7020
|
if (shell === "bash")
|
|
4895
|
-
return { file:
|
|
4896
|
-
return { file:
|
|
7021
|
+
return { file: join22(HOME, ".bashrc"), name: "~/.bashrc" };
|
|
7022
|
+
return { file: join22(HOME, ".profile"), name: "~/.profile" };
|
|
4897
7023
|
};
|
|
4898
7024
|
var PATH_MARKER = "# Added by `lecodes install`";
|
|
4899
7025
|
var appendPathToShellRc = (binDir) => {
|
|
4900
7026
|
const { file, name } = shellRc();
|
|
4901
7027
|
const line = `export PATH="${binDir}:$PATH"`;
|
|
4902
7028
|
try {
|
|
4903
|
-
const existing =
|
|
7029
|
+
const existing = existsSync20(file) ? readFileSync20(file, "utf8") : "";
|
|
4904
7030
|
if (existing.includes(line))
|
|
4905
7031
|
return name;
|
|
4906
7032
|
appendFileSync2(file, `${existing && !existing.endsWith(`
|
|
@@ -4938,7 +7064,7 @@ var finishPathUnix = (args, binDir) => {
|
|
|
4938
7064
|
log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
|
|
4939
7065
|
};
|
|
4940
7066
|
var installWindows = (args, bundle, companions) => {
|
|
4941
|
-
const target =
|
|
7067
|
+
const target = resolve8(flagStr(args, "dir") ?? join22(process.env.LOCALAPPDATA ?? join22(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
|
|
4942
7068
|
try {
|
|
4943
7069
|
copyBundle(bundle, target, companions);
|
|
4944
7070
|
} catch (e) {
|
|
@@ -4966,7 +7092,7 @@ var installWindows = (args, bundle, companions) => {
|
|
|
4966
7092
|
var addToUserPathWindows = (dir) => {
|
|
4967
7093
|
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
7094
|
try {
|
|
4969
|
-
|
|
7095
|
+
execFileSync2("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
|
|
4970
7096
|
return true;
|
|
4971
7097
|
} catch {
|
|
4972
7098
|
return false;
|
|
@@ -4975,9 +7101,9 @@ var addToUserPathWindows = (dir) => {
|
|
|
4975
7101
|
|
|
4976
7102
|
// src/index.ts
|
|
4977
7103
|
var STANDALONE = (() => {
|
|
4978
|
-
const dir =
|
|
7104
|
+
const dir = dirname14(process.execPath);
|
|
4979
7105
|
const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
|
|
4980
|
-
return
|
|
7106
|
+
return existsSync21(join23(dir, "runtime")) && existsSync21(join23(dir, chisel));
|
|
4981
7107
|
})();
|
|
4982
7108
|
var INSTALLED = STANDALONE && (() => {
|
|
4983
7109
|
const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
|
|
@@ -4992,8 +7118,8 @@ var INSTALLED = STANDALONE && (() => {
|
|
|
4992
7118
|
if (!d)
|
|
4993
7119
|
return false;
|
|
4994
7120
|
try {
|
|
4995
|
-
const cand =
|
|
4996
|
-
return
|
|
7121
|
+
const cand = join23(d, exe);
|
|
7122
|
+
return existsSync21(cand) && realpathSync2(cand) === self;
|
|
4997
7123
|
} catch {
|
|
4998
7124
|
return false;
|
|
4999
7125
|
}
|
|
@@ -5080,12 +7206,70 @@ ${c.bold("Commands:")}
|
|
|
5080
7206
|
--port <n> editor-server port (default 4488)
|
|
5081
7207
|
--no-open don't open the browser
|
|
5082
7208
|
--no-mcp don't register the MCP server in .mcp.json
|
|
7209
|
+
app init ios Generate a native Xcode shell in <project>/ios/ (offline-first:
|
|
7210
|
+
runs the bundled app.js; ios/ is never pushed to LeCodes)
|
|
7211
|
+
--name <s> display name (default: app.json / folder name)
|
|
7212
|
+
--bundle-id <id> iOS bundle identifier
|
|
7213
|
+
--team <id> Apple development team id
|
|
7214
|
+
--variant <v> SDK variant: core (default) or full (3D/AR)
|
|
7215
|
+
app sync Make the shell match the project: compile the bundle + embed
|
|
7216
|
+
its assets into ios/App/Resources/ (offline by default, also
|
|
7217
|
+
for server projects; app.json "resources": {"remote": [globs]}
|
|
7218
|
+
keeps heavy assets streaming from the server), vendor app.json
|
|
7219
|
+
plugins (pinned in plugins.lock.json), regenerate the
|
|
7220
|
+
CLI-owned shell files
|
|
7221
|
+
app plugin <list|add|remove> Manage native plugins in app.json (then run 'lecodes app sync')
|
|
7222
|
+
add takes a known id (camera, qr-scanner) or github:owner/repo[@tag]
|
|
7223
|
+
app open [ios] Open the native shell in its IDE (Xcode; Android Studio later)
|
|
7224
|
+
app doctor Preflight checks for building/uploading the shell
|
|
7225
|
+
app build ios Sync, archive and export the .ipa (automatic signing)
|
|
7226
|
+
--configuration <c> build configuration (default Release)
|
|
7227
|
+
--method <m> export method (default app-store-connect)
|
|
7228
|
+
--no-export stop after the archive
|
|
7229
|
+
--no-sync skip the lecodes app sync step
|
|
7230
|
+
app upload ios Upload the archive to App Store Connect (then bumps buildNumber)
|
|
7231
|
+
--key-path <p8> --key-id <id> --issuer-id <id>
|
|
7232
|
+
ASC API key; stored in ~/.lecodes/asc.json
|
|
5083
7233
|
|
|
5084
7234
|
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 };
|
|
7235
|
+
var commands = { init: init2, login, create, clone, status, diff, pull, push, compile: compile2, preview: previewLegacy, render, design, scene, dev, app };
|
|
5086
7236
|
if (STANDALONE)
|
|
5087
7237
|
commands.install = install;
|
|
5088
7238
|
var readVersion = () => package_default.version ?? "unknown";
|
|
7239
|
+
var OPTIONAL_PEERS = [
|
|
7240
|
+
["lecodes-renderer", "lecodes-renderer/headless"],
|
|
7241
|
+
["lecodes-design", "lecodes-design/server"],
|
|
7242
|
+
["lecodes-3d-editor", "lecodes-3d-editor/server"]
|
|
7243
|
+
];
|
|
7244
|
+
var peerVersion = (name, probe) => {
|
|
7245
|
+
try {
|
|
7246
|
+
const require2 = createRequire4(import.meta.url);
|
|
7247
|
+
let dir = dirname14(require2.resolve(probe));
|
|
7248
|
+
for (let depth = 0;depth < 16; depth++) {
|
|
7249
|
+
const manifest = join23(dir, "package.json");
|
|
7250
|
+
if (existsSync21(manifest)) {
|
|
7251
|
+
const parsed = JSON.parse(readFileSync21(manifest, "utf8"));
|
|
7252
|
+
if (parsed.name === name)
|
|
7253
|
+
return parsed.version ?? "unknown";
|
|
7254
|
+
}
|
|
7255
|
+
const parent = dirname14(dir);
|
|
7256
|
+
if (parent === dir)
|
|
7257
|
+
break;
|
|
7258
|
+
dir = parent;
|
|
7259
|
+
}
|
|
7260
|
+
} catch {}
|
|
7261
|
+
return null;
|
|
7262
|
+
};
|
|
7263
|
+
var printVersion = () => {
|
|
7264
|
+
const rows = [["lecodes-cli", readVersion()]];
|
|
7265
|
+
for (const [name, probe] of OPTIONAL_PEERS) {
|
|
7266
|
+
const version = peerVersion(name, probe);
|
|
7267
|
+
rows.push([name, version ?? c.dim("not installed")]);
|
|
7268
|
+
}
|
|
7269
|
+
const width = Math.max(...rows.map(([label]) => label.length));
|
|
7270
|
+
for (const [label, value] of rows)
|
|
7271
|
+
log(`${label.padEnd(width)} ${value}`);
|
|
7272
|
+
};
|
|
5089
7273
|
var main = async () => {
|
|
5090
7274
|
const argv = process.argv.slice(2);
|
|
5091
7275
|
const command = argv[0];
|
|
@@ -5094,7 +7278,7 @@ var main = async () => {
|
|
|
5094
7278
|
return;
|
|
5095
7279
|
}
|
|
5096
7280
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
5097
|
-
|
|
7281
|
+
printVersion();
|
|
5098
7282
|
return;
|
|
5099
7283
|
}
|
|
5100
7284
|
const run = commands[command];
|