lecodes-cli 0.7.1 → 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/README.md +3 -3
- package/dist/index.js +2941 -745
- package/package.json +4 -4
- 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 +8 -5
- 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 +126 -55
- 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 -205
- package/runtime/sdk/compile/compileProject.ts +0 -108
- 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))
|
|
@@ -249,7 +252,7 @@ var resolveInject = () => {
|
|
|
249
252
|
if (parsed.code == null)
|
|
250
253
|
throw new Error("chisel produced no code");
|
|
251
254
|
return { code: parsed.code, map: parsed.map ?? null };
|
|
252
|
-
}, bundleProject = async (entrypoint, files, format = "esm") => (await bundleProjectChisel(entrypoint, files, format, false)).code, bundleProjectWithMap = async (entrypoint, files, format = "esm", minify, editor, libraries) => bundleProjectChisel(entrypoint, files, format,
|
|
255
|
+
}, bundleProject = async (entrypoint, files, format = "esm") => (await bundleProjectChisel(entrypoint, files, format, false)).code, bundleProjectWithMap = async (entrypoint, files, format = "esm", minify, editor, libraries, sourcemap = true) => bundleProjectChisel(entrypoint, files, format, sourcemap, minify, editor, libraries);
|
|
253
256
|
var init_bundler = __esm(() => {
|
|
254
257
|
init_assetMacro();
|
|
255
258
|
init_aspectMacro();
|
|
@@ -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 {
|
|
@@ -360,12 +366,12 @@ var detectEntry = (entries) => {
|
|
|
360
366
|
files[e.path] = e.text ?? "";
|
|
361
367
|
}
|
|
362
368
|
}
|
|
363
|
-
const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries);
|
|
369
|
+
const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm", opts.minify, opts.editor, opts.libraries, opts.sourcemap ?? true);
|
|
364
370
|
if (opts.header === false) {
|
|
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",
|
|
@@ -1490,8 +2040,8 @@ var package_default = {
|
|
|
1490
2040
|
"@letary/chisel": "^0.4.1"
|
|
1491
2041
|
},
|
|
1492
2042
|
peerDependencies: {
|
|
1493
|
-
"lecodes-design": "^0.3.
|
|
1494
|
-
"lecodes-renderer": "^0.6.
|
|
2043
|
+
"lecodes-design": "^0.3.2",
|
|
2044
|
+
"lecodes-renderer": "^0.6.2",
|
|
1495
2045
|
"lecodes-3d-editor": "^0.1.0"
|
|
1496
2046
|
},
|
|
1497
2047
|
peerDependenciesMeta: {
|
|
@@ -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,27 +2560,1538 @@ 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
|
-
|
|
1803
|
-
const
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
if (
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
})
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
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 ?? [] });
|
|
2571
|
+
}
|
|
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") });
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
return { entries, warnings };
|
|
2592
|
+
};
|
|
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 };
|
|
2613
|
+
};
|
|
2614
|
+
|
|
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";
|
|
2619
|
+
|
|
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
|
+
})();
|
|
2634
|
+
|
|
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
|
+
});
|
|
2667
|
+
|
|
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;
|
|
2709
|
+
};
|
|
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;
|
|
2717
|
+
}
|
|
2718
|
+
try {
|
|
2719
|
+
return new Uint8Array(readFileSync9(join10(registry, rel)));
|
|
2720
|
+
} catch {
|
|
2721
|
+
return null;
|
|
2722
|
+
}
|
|
2723
|
+
};
|
|
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);
|
|
2754
|
+
}
|
|
2755
|
+
lines.push("");
|
|
2756
|
+
return lines.join(`
|
|
2757
|
+
`);
|
|
2758
|
+
};
|
|
2759
|
+
var writeIfChanged = (path, text) => {
|
|
2760
|
+
try {
|
|
2761
|
+
if (readFileSync9(path, "utf8") === text)
|
|
2762
|
+
return;
|
|
2763
|
+
} catch {}
|
|
2764
|
+
writeFileSync5(path, text);
|
|
2765
|
+
};
|
|
2766
|
+
var downloadFace = async (registry, vendorDir, face, rel, abs) => {
|
|
2767
|
+
const licRaw = await readRegistry(registry, "licenses.json");
|
|
2768
|
+
if (!licRaw)
|
|
2769
|
+
throw new Error(`font: can't reach the font registry (${registry}). Check your connection, or set LECODES_FONT_REGISTRY.`);
|
|
2770
|
+
const licenses = JSON.parse(new TextDecoder().decode(licRaw));
|
|
2771
|
+
const lic = licenses[face.id];
|
|
2772
|
+
if (!lic)
|
|
2773
|
+
throw new Error(`font("${face.id}"): the registry has no license entry for this family — it may not be published yet. Browse ${registryHome(registry)}.`);
|
|
2774
|
+
const bytes = await readRegistry(registry, `${face.id}/${face.fileName}`);
|
|
2775
|
+
if (!bytes)
|
|
2776
|
+
throw new Error(`font("${face.id}"): face "${face.fileName}" not found in the registry (${registryHome(registry)}).`);
|
|
2777
|
+
mkdirSync4(dirname6(abs), { recursive: true });
|
|
2778
|
+
writeFileSync5(abs, bytes);
|
|
2779
|
+
const lockPath = join10(vendorDir, "fonts.lock.json");
|
|
2780
|
+
const lock = readLock(lockPath) ?? { registry, faces: {}, families: {} };
|
|
2781
|
+
lock.registry = registry;
|
|
2782
|
+
lock.faces[`${face.id}@${face.weight}${face.italic ? "i" : ""}`] = { id: face.id, family: face.family, weight: face.weight, italic: face.italic, hash: sha12(bytes), spdx: lic.spdx, path: rel };
|
|
2783
|
+
lock.families[face.id] = lic;
|
|
2784
|
+
writeIfChanged(lockPath, serializeLock(lock));
|
|
2785
|
+
writeIfChanged(join10(vendorDir, "FONT-NOTICES.md"), renderNotices(lock));
|
|
2786
|
+
};
|
|
2787
|
+
|
|
2788
|
+
// src/commands/compile.ts
|
|
2789
|
+
var compile2 = async (args) => {
|
|
2790
|
+
await compileImpl(args);
|
|
2791
|
+
};
|
|
2792
|
+
var compileShell = (args, shell) => compileImpl(args, shell);
|
|
2793
|
+
var compileImpl = async (args, shell) => {
|
|
2794
|
+
let root;
|
|
2795
|
+
let manifest;
|
|
2796
|
+
try {
|
|
2797
|
+
root = findProjectRoot(process.cwd());
|
|
2798
|
+
manifest = readManifest(root);
|
|
2799
|
+
} catch {
|
|
2800
|
+
root = process.cwd();
|
|
2801
|
+
manifest = null;
|
|
2802
|
+
}
|
|
2803
|
+
let entries;
|
|
2804
|
+
let warnings;
|
|
2805
|
+
let name;
|
|
2806
|
+
let publicUrl;
|
|
2807
|
+
let localAssets = false;
|
|
2808
|
+
if (manifest) {
|
|
2809
|
+
const config = loadConfig();
|
|
2810
|
+
const apiUrl = requireApiUrl(config);
|
|
2811
|
+
const token = requireToken(config);
|
|
2812
|
+
note("Resolving project assets…");
|
|
2813
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
2814
|
+
({ entries, warnings } = collectEntries(root, manifest, project, { warnMissingUrl: !shell }));
|
|
2815
|
+
name = manifest.name;
|
|
2816
|
+
publicUrl = flagStr(args, "public-url") ?? apiUrl;
|
|
2817
|
+
localAssets = !!shell;
|
|
2818
|
+
if (shell && shell.remotePatterns.length > 0) {
|
|
2819
|
+
const isRemote = buildGlobMatcher(shell.remotePatterns);
|
|
2820
|
+
for (const e of entries) {
|
|
2821
|
+
if (e.type !== "resource" || !isRemote(e.path))
|
|
2822
|
+
continue;
|
|
2823
|
+
if (!e.fileSrc) {
|
|
2824
|
+
warnErr(`${e.path} matches resources.remote but has no server URL yet — bundling it (push, then re-sync).`);
|
|
2825
|
+
continue;
|
|
2826
|
+
}
|
|
2827
|
+
e.remote = true;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
} else {
|
|
2831
|
+
const publicUrlFlag = flagStr(args, "public-url");
|
|
2832
|
+
localAssets = shell ? true : publicUrlFlag === undefined;
|
|
2833
|
+
publicUrl = publicUrlFlag ?? "";
|
|
2834
|
+
({ entries, warnings } = collectLocalEntries(root, null, ""));
|
|
2835
|
+
name = basename3(root);
|
|
2836
|
+
if (shell && shell.remotePatterns.length > 0) {
|
|
2837
|
+
warnErr("resources.remote needs a server-backed project (a streamed asset needs a server URL) — bundling everything.");
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
const fontsHeader = await applyProjectFonts(entries, manifest && !shell ? { mode: "cdn", root, publicUrl } : {
|
|
2841
|
+
mode: "vendor",
|
|
2842
|
+
root,
|
|
2843
|
+
vendorDir: root,
|
|
2844
|
+
vendorPrefix: "",
|
|
2845
|
+
publicUrl: shell ? "" : publicUrl,
|
|
2846
|
+
makeResourceEntry: (path, absPath) => ({ path, type: "resource", fileSrc: encodeURI(path), absPath })
|
|
2847
|
+
});
|
|
2848
|
+
note("Compiling…");
|
|
2849
|
+
const js = await compileProject2({
|
|
2850
|
+
entries,
|
|
2851
|
+
name,
|
|
2852
|
+
publicUrl,
|
|
2853
|
+
entryOverride: flagStr(args, "entry"),
|
|
2854
|
+
header: !flagBool(args, "no-header"),
|
|
2855
|
+
localAssets,
|
|
2856
|
+
fontsHeader,
|
|
2857
|
+
minify: flagBool(args, "no-minify") ? false : undefined,
|
|
2858
|
+
sourcemap: flagBool(args, "no-sourcemap", "no-source-map") ? false : undefined
|
|
2859
|
+
});
|
|
2860
|
+
for (const w of warnings)
|
|
2861
|
+
warnErr(w);
|
|
2862
|
+
const out = flagStr(args, "o", "out");
|
|
2863
|
+
if (out && isNullSink(out)) {
|
|
2864
|
+
note(`Discarded output (${out})`);
|
|
2865
|
+
} else if (out) {
|
|
2866
|
+
writeFileSync6(resolve(out), js);
|
|
2867
|
+
note(`Wrote ${out} (${js.length} bytes)`);
|
|
2868
|
+
} else {
|
|
2869
|
+
process.stdout.write(js);
|
|
2870
|
+
if (!js.endsWith(`
|
|
2871
|
+
`))
|
|
2872
|
+
process.stdout.write(`
|
|
2873
|
+
`);
|
|
2874
|
+
}
|
|
2875
|
+
return entries.filter((e) => e.type === "resource").map((e) => ({
|
|
2876
|
+
path: e.path.replace(/^\//, ""),
|
|
2877
|
+
name: e.path.split(/[\\/]/).pop() ?? e.path,
|
|
2878
|
+
absPath: e.absPath,
|
|
2879
|
+
remote: !!e.remote
|
|
2880
|
+
}));
|
|
2881
|
+
};
|
|
2882
|
+
var previewLegacy = async (args) => {
|
|
2883
|
+
warnErr("'lecodes preview' is now 'lecodes compile' — the old name still works.");
|
|
2884
|
+
await compile2(args);
|
|
2885
|
+
};
|
|
2886
|
+
|
|
2887
|
+
// src/commands/appTemplates.ts
|
|
2888
|
+
var pbxStr = (s) => s.replace(/[\\"]/g, (m) => "\\" + m);
|
|
2889
|
+
var pbxprojTemplate = ({ displayName, bundleId, teamId, version, buildNumber }) => {
|
|
2890
|
+
const target = `
|
|
2891
|
+
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
|
2892
|
+
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
|
2893
|
+
CODE_SIGN_STYLE = Automatic;
|
|
2894
|
+
CURRENT_PROJECT_VERSION = ${buildNumber};
|
|
2895
|
+
DEVELOPMENT_TEAM = "${pbxStr(teamId)}";
|
|
2896
|
+
GENERATE_INFOPLIST_FILE = YES;
|
|
2897
|
+
INFOPLIST_FILE = "App/Info.plist";
|
|
2898
|
+
INFOPLIST_KEY_CFBundleDisplayName = "${pbxStr(displayName)}";
|
|
2899
|
+
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
|
2900
|
+
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
|
2901
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
2902
|
+
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
|
2903
|
+
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
|
2904
|
+
LD_RUNPATH_SEARCH_PATHS = (
|
|
2905
|
+
"$(inherited)",
|
|
2906
|
+
"@executable_path/Frameworks",
|
|
2907
|
+
);
|
|
2908
|
+
MARKETING_VERSION = ${version};
|
|
2909
|
+
PRODUCT_BUNDLE_IDENTIFIER = "${pbxStr(bundleId)}";
|
|
2910
|
+
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
2911
|
+
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
|
2912
|
+
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
|
2913
|
+
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
|
|
2914
|
+
SWIFT_EMIT_LOC_STRINGS = YES;
|
|
2915
|
+
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
|
2916
|
+
SWIFT_VERSION = 5.0;
|
|
2917
|
+
TARGETED_DEVICE_FAMILY = "1,2";`;
|
|
2918
|
+
const project = (debug) => `
|
|
2919
|
+
ALWAYS_SEARCH_USER_PATHS = NO;
|
|
2920
|
+
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
|
2921
|
+
CLANG_ANALYZER_NONNULL = YES;
|
|
2922
|
+
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
|
2923
|
+
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
|
2924
|
+
CLANG_ENABLE_MODULES = YES;
|
|
2925
|
+
CLANG_ENABLE_OBJC_ARC = YES;
|
|
2926
|
+
CLANG_ENABLE_OBJC_WEAK = YES;
|
|
2927
|
+
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
|
2928
|
+
CLANG_WARN_BOOL_CONVERSION = YES;
|
|
2929
|
+
CLANG_WARN_COMMA = YES;
|
|
2930
|
+
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
|
2931
|
+
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
|
2932
|
+
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
|
2933
|
+
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
|
2934
|
+
CLANG_WARN_EMPTY_BODY = YES;
|
|
2935
|
+
CLANG_WARN_ENUM_CONVERSION = YES;
|
|
2936
|
+
CLANG_WARN_INFINITE_RECURSION = YES;
|
|
2937
|
+
CLANG_WARN_INT_CONVERSION = YES;
|
|
2938
|
+
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
|
2939
|
+
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
|
2940
|
+
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
|
2941
|
+
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
|
2942
|
+
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
|
2943
|
+
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
|
2944
|
+
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
|
2945
|
+
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
|
2946
|
+
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
|
2947
|
+
CLANG_WARN_UNREACHABLE_CODE = YES;
|
|
2948
|
+
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
|
2949
|
+
COPY_PHASE_STRIP = NO;
|
|
2950
|
+
DEBUG_INFORMATION_FORMAT = ${debug ? "dwarf" : '"dwarf-with-dsym"'};
|
|
2951
|
+
DEVELOPMENT_TEAM = "${pbxStr(teamId)}";${debug ? "" : `
|
|
2952
|
+
ENABLE_NS_ASSERTIONS = NO;`}
|
|
2953
|
+
ENABLE_STRICT_OBJC_MSGSEND = YES;${debug ? `
|
|
2954
|
+
ENABLE_TESTABILITY = YES;` : ""}
|
|
2955
|
+
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
|
2956
|
+
GCC_C_LANGUAGE_STANDARD = gnu17;${debug ? `
|
|
2957
|
+
GCC_DYNAMIC_NO_PIC = NO;` : ""}
|
|
2958
|
+
GCC_NO_COMMON_BLOCKS = YES;${debug ? `
|
|
2959
|
+
GCC_OPTIMIZATION_LEVEL = 0;
|
|
2960
|
+
GCC_PREPROCESSOR_DEFINITIONS = (
|
|
2961
|
+
"DEBUG=1",
|
|
2962
|
+
"$(inherited)",
|
|
2963
|
+
);` : ""}
|
|
2964
|
+
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
|
2965
|
+
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
|
2966
|
+
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
|
2967
|
+
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
|
2968
|
+
GCC_WARN_UNUSED_FUNCTION = YES;
|
|
2969
|
+
GCC_WARN_UNUSED_VARIABLE = YES;
|
|
2970
|
+
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
|
2971
|
+
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
|
2972
|
+
MTL_ENABLE_DEBUG_INFO = ${debug ? "INCLUDE_SOURCE" : "NO"};
|
|
2973
|
+
MTL_FAST_MATH = YES;${debug ? `
|
|
2974
|
+
ONLY_ACTIVE_ARCH = YES;` : ""}
|
|
2975
|
+
SDKROOT = iphoneos;${debug ? `
|
|
2976
|
+
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
|
2977
|
+
SWIFT_OPTIMIZATION_LEVEL = "-Onone";` : `
|
|
2978
|
+
SWIFT_COMPILATION_MODE = wholemodule;
|
|
2979
|
+
VALIDATE_PRODUCT = YES;`}`;
|
|
2980
|
+
return `// !$*UTF8*$!
|
|
2981
|
+
{
|
|
2982
|
+
archiveVersion = 1;
|
|
2983
|
+
classes = {
|
|
2984
|
+
};
|
|
2985
|
+
objectVersion = 77;
|
|
2986
|
+
objects = {
|
|
2987
|
+
|
|
2988
|
+
/* Begin PBXBuildFile section */
|
|
2989
|
+
B73550812FEBAC710005B4CC /* LeCodesRuntime in Frameworks */ = {isa = PBXBuildFile; productRef = B73550802FEBAC710005B4CC /* LeCodesRuntime */; };
|
|
2990
|
+
/* End PBXBuildFile section */
|
|
2991
|
+
|
|
2992
|
+
/* Begin PBXFileReference section */
|
|
2993
|
+
B73550672FEBAC0F0005B4CC /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
2994
|
+
/* End PBXFileReference section */
|
|
2995
|
+
|
|
2996
|
+
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
|
2997
|
+
B73550792FEBAC100005B4CC /* Exceptions for "App" folder in "App" target */ = {
|
|
2998
|
+
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
|
2999
|
+
membershipExceptions = (
|
|
3000
|
+
Info.plist,
|
|
3001
|
+
);
|
|
3002
|
+
target = B73550662FEBAC0F0005B4CC /* App */;
|
|
3003
|
+
};
|
|
3004
|
+
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
|
3005
|
+
|
|
3006
|
+
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
|
3007
|
+
B73550692FEBAC0F0005B4CC /* App */ = {
|
|
3008
|
+
isa = PBXFileSystemSynchronizedRootGroup;
|
|
3009
|
+
exceptions = (
|
|
3010
|
+
B73550792FEBAC100005B4CC /* Exceptions for "App" folder in "App" target */,
|
|
3011
|
+
);
|
|
3012
|
+
path = App;
|
|
3013
|
+
sourceTree = "<group>";
|
|
3014
|
+
};
|
|
3015
|
+
/* End PBXFileSystemSynchronizedRootGroup section */
|
|
3016
|
+
|
|
3017
|
+
/* Begin PBXFrameworksBuildPhase section */
|
|
3018
|
+
B73550642FEBAC0F0005B4CC /* Frameworks */ = {
|
|
3019
|
+
isa = PBXFrameworksBuildPhase;
|
|
3020
|
+
buildActionMask = 2147483647;
|
|
3021
|
+
files = (
|
|
3022
|
+
B73550812FEBAC710005B4CC /* LeCodesRuntime in Frameworks */,
|
|
3023
|
+
);
|
|
3024
|
+
runOnlyForDeploymentPostprocessing = 0;
|
|
3025
|
+
};
|
|
3026
|
+
/* End PBXFrameworksBuildPhase section */
|
|
3027
|
+
|
|
3028
|
+
/* Begin PBXGroup section */
|
|
3029
|
+
B735505E2FEBAC0F0005B4CC = {
|
|
3030
|
+
isa = PBXGroup;
|
|
3031
|
+
children = (
|
|
3032
|
+
B73550692FEBAC0F0005B4CC /* App */,
|
|
3033
|
+
B73550682FEBAC0F0005B4CC /* Products */,
|
|
3034
|
+
);
|
|
3035
|
+
sourceTree = "<group>";
|
|
3036
|
+
};
|
|
3037
|
+
B73550682FEBAC0F0005B4CC /* Products */ = {
|
|
3038
|
+
isa = PBXGroup;
|
|
3039
|
+
children = (
|
|
3040
|
+
B73550672FEBAC0F0005B4CC /* App.app */,
|
|
3041
|
+
);
|
|
3042
|
+
name = Products;
|
|
3043
|
+
sourceTree = "<group>";
|
|
3044
|
+
};
|
|
3045
|
+
/* End PBXGroup section */
|
|
3046
|
+
|
|
3047
|
+
/* Begin PBXNativeTarget section */
|
|
3048
|
+
B73550662FEBAC0F0005B4CC /* App */ = {
|
|
3049
|
+
isa = PBXNativeTarget;
|
|
3050
|
+
buildConfigurationList = B735507A2FEBAC100005B4CC /* Build configuration list for PBXNativeTarget "App" */;
|
|
3051
|
+
buildPhases = (
|
|
3052
|
+
B73550632FEBAC0F0005B4CC /* Sources */,
|
|
3053
|
+
B73550642FEBAC0F0005B4CC /* Frameworks */,
|
|
3054
|
+
B73550652FEBAC0F0005B4CC /* Resources */,
|
|
3055
|
+
);
|
|
3056
|
+
buildRules = (
|
|
3057
|
+
);
|
|
3058
|
+
dependencies = (
|
|
3059
|
+
);
|
|
3060
|
+
fileSystemSynchronizedGroups = (
|
|
3061
|
+
B73550692FEBAC0F0005B4CC /* App */,
|
|
3062
|
+
);
|
|
3063
|
+
name = App;
|
|
3064
|
+
packageProductDependencies = (
|
|
3065
|
+
B73550802FEBAC710005B4CC /* LeCodesRuntime */,
|
|
3066
|
+
);
|
|
3067
|
+
productName = App;
|
|
3068
|
+
productReference = B73550672FEBAC0F0005B4CC /* App.app */;
|
|
3069
|
+
productType = "com.apple.product-type.application";
|
|
3070
|
+
};
|
|
3071
|
+
/* End PBXNativeTarget section */
|
|
3072
|
+
|
|
3073
|
+
/* Begin PBXProject section */
|
|
3074
|
+
B735505F2FEBAC0F0005B4CC /* Project object */ = {
|
|
3075
|
+
isa = PBXProject;
|
|
3076
|
+
attributes = {
|
|
3077
|
+
BuildIndependentTargetsInParallel = 1;
|
|
3078
|
+
LastSwiftUpdateCheck = 2650;
|
|
3079
|
+
LastUpgradeCheck = 2650;
|
|
3080
|
+
TargetAttributes = {
|
|
3081
|
+
B73550662FEBAC0F0005B4CC = {
|
|
3082
|
+
CreatedOnToolsVersion = 26.5;
|
|
3083
|
+
};
|
|
3084
|
+
};
|
|
3085
|
+
};
|
|
3086
|
+
buildConfigurationList = B73550622FEBAC0F0005B4CC /* Build configuration list for PBXProject "App" */;
|
|
3087
|
+
developmentRegion = en;
|
|
3088
|
+
hasScannedForEncodings = 0;
|
|
3089
|
+
knownRegions = (
|
|
3090
|
+
en,
|
|
3091
|
+
Base,
|
|
3092
|
+
);
|
|
3093
|
+
mainGroup = B735505E2FEBAC0F0005B4CC;
|
|
3094
|
+
minimizedProjectReferenceProxies = 1;
|
|
3095
|
+
packageReferences = (
|
|
3096
|
+
B73550822FEBAC720005B4CC /* XCLocalSwiftPackageReference "LeCodesRuntime" */,
|
|
3097
|
+
);
|
|
3098
|
+
preferredProjectObjectVersion = 77;
|
|
3099
|
+
productRefGroup = B73550682FEBAC0F0005B4CC /* Products */;
|
|
3100
|
+
projectDirPath = "";
|
|
3101
|
+
projectRoot = "";
|
|
3102
|
+
targets = (
|
|
3103
|
+
B73550662FEBAC0F0005B4CC /* App */,
|
|
3104
|
+
);
|
|
3105
|
+
};
|
|
3106
|
+
/* End PBXProject section */
|
|
3107
|
+
|
|
3108
|
+
/* Begin PBXResourcesBuildPhase section */
|
|
3109
|
+
B73550652FEBAC0F0005B4CC /* Resources */ = {
|
|
3110
|
+
isa = PBXResourcesBuildPhase;
|
|
3111
|
+
buildActionMask = 2147483647;
|
|
3112
|
+
files = (
|
|
3113
|
+
);
|
|
3114
|
+
runOnlyForDeploymentPostprocessing = 0;
|
|
3115
|
+
};
|
|
3116
|
+
/* End PBXResourcesBuildPhase section */
|
|
3117
|
+
|
|
3118
|
+
/* Begin PBXSourcesBuildPhase section */
|
|
3119
|
+
B73550632FEBAC0F0005B4CC /* Sources */ = {
|
|
3120
|
+
isa = PBXSourcesBuildPhase;
|
|
3121
|
+
buildActionMask = 2147483647;
|
|
3122
|
+
files = (
|
|
3123
|
+
);
|
|
3124
|
+
runOnlyForDeploymentPostprocessing = 0;
|
|
3125
|
+
};
|
|
3126
|
+
/* End PBXSourcesBuildPhase section */
|
|
3127
|
+
|
|
3128
|
+
/* Begin XCBuildConfiguration section */
|
|
3129
|
+
B735507B2FEBAC100005B4CC /* Debug */ = {
|
|
3130
|
+
isa = XCBuildConfiguration;
|
|
3131
|
+
buildSettings = {${target}
|
|
3132
|
+
};
|
|
3133
|
+
name = Debug;
|
|
3134
|
+
};
|
|
3135
|
+
B735507C2FEBAC100005B4CC /* Release */ = {
|
|
3136
|
+
isa = XCBuildConfiguration;
|
|
3137
|
+
buildSettings = {${target}
|
|
3138
|
+
};
|
|
3139
|
+
name = Release;
|
|
3140
|
+
};
|
|
3141
|
+
B735507D2FEBAC100005B4CC /* Debug */ = {
|
|
3142
|
+
isa = XCBuildConfiguration;
|
|
3143
|
+
buildSettings = {${project(true)}
|
|
3144
|
+
};
|
|
3145
|
+
name = Debug;
|
|
3146
|
+
};
|
|
3147
|
+
B735507E2FEBAC100005B4CC /* Release */ = {
|
|
3148
|
+
isa = XCBuildConfiguration;
|
|
3149
|
+
buildSettings = {${project(false)}
|
|
3150
|
+
};
|
|
3151
|
+
name = Release;
|
|
3152
|
+
};
|
|
3153
|
+
/* End XCBuildConfiguration section */
|
|
3154
|
+
|
|
3155
|
+
/* Begin XCConfigurationList section */
|
|
3156
|
+
B73550622FEBAC0F0005B4CC /* Build configuration list for PBXProject "App" */ = {
|
|
3157
|
+
isa = XCConfigurationList;
|
|
3158
|
+
buildConfigurations = (
|
|
3159
|
+
B735507D2FEBAC100005B4CC /* Debug */,
|
|
3160
|
+
B735507E2FEBAC100005B4CC /* Release */,
|
|
3161
|
+
);
|
|
3162
|
+
defaultConfigurationIsVisible = 0;
|
|
3163
|
+
defaultConfigurationName = Release;
|
|
3164
|
+
};
|
|
3165
|
+
B735507A2FEBAC100005B4CC /* Build configuration list for PBXNativeTarget "App" */ = {
|
|
3166
|
+
isa = XCConfigurationList;
|
|
3167
|
+
buildConfigurations = (
|
|
3168
|
+
B735507B2FEBAC100005B4CC /* Debug */,
|
|
3169
|
+
B735507C2FEBAC100005B4CC /* Release */,
|
|
3170
|
+
);
|
|
3171
|
+
defaultConfigurationIsVisible = 0;
|
|
3172
|
+
defaultConfigurationName = Release;
|
|
3173
|
+
};
|
|
3174
|
+
/* End XCConfigurationList section */
|
|
3175
|
+
|
|
3176
|
+
/* Begin XCLocalSwiftPackageReference section */
|
|
3177
|
+
B73550822FEBAC720005B4CC /* XCLocalSwiftPackageReference "LeCodesRuntime" */ = {
|
|
3178
|
+
isa = XCLocalSwiftPackageReference;
|
|
3179
|
+
relativePath = LeCodesRuntime;
|
|
3180
|
+
};
|
|
3181
|
+
/* End XCLocalSwiftPackageReference section */
|
|
3182
|
+
|
|
3183
|
+
/* Begin XCSwiftPackageProductDependency section */
|
|
3184
|
+
B73550802FEBAC710005B4CC /* LeCodesRuntime */ = {
|
|
3185
|
+
isa = XCSwiftPackageProductDependency;
|
|
3186
|
+
productName = LeCodesRuntime;
|
|
3187
|
+
};
|
|
3188
|
+
/* End XCSwiftPackageProductDependency section */
|
|
3189
|
+
};
|
|
3190
|
+
rootObject = B735505F2FEBAC0F0005B4CC /* Project object */;
|
|
3191
|
+
}
|
|
3192
|
+
`;
|
|
3193
|
+
};
|
|
3194
|
+
var SCHEME_XML = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3195
|
+
<Scheme
|
|
3196
|
+
LastUpgradeVersion = "2650"
|
|
3197
|
+
version = "1.7">
|
|
3198
|
+
<BuildAction
|
|
3199
|
+
parallelizeBuildables = "YES"
|
|
3200
|
+
buildImplicitDependencies = "YES">
|
|
3201
|
+
<BuildActionEntries>
|
|
3202
|
+
<BuildActionEntry
|
|
3203
|
+
buildForTesting = "YES"
|
|
3204
|
+
buildForRunning = "YES"
|
|
3205
|
+
buildForProfiling = "YES"
|
|
3206
|
+
buildForArchiving = "YES"
|
|
3207
|
+
buildForAnalyzing = "YES">
|
|
3208
|
+
<BuildableReference
|
|
3209
|
+
BuildableIdentifier = "primary"
|
|
3210
|
+
BlueprintIdentifier = "B73550662FEBAC0F0005B4CC"
|
|
3211
|
+
BuildableName = "App.app"
|
|
3212
|
+
BlueprintName = "App"
|
|
3213
|
+
ReferencedContainer = "container:App.xcodeproj">
|
|
3214
|
+
</BuildableReference>
|
|
3215
|
+
</BuildActionEntry>
|
|
3216
|
+
</BuildActionEntries>
|
|
3217
|
+
</BuildAction>
|
|
3218
|
+
<TestAction
|
|
3219
|
+
buildConfiguration = "Debug"
|
|
3220
|
+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
3221
|
+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
3222
|
+
shouldUseLaunchSchemeArgsEnv = "YES">
|
|
3223
|
+
<Testables>
|
|
3224
|
+
</Testables>
|
|
3225
|
+
</TestAction>
|
|
3226
|
+
<LaunchAction
|
|
3227
|
+
buildConfiguration = "Debug"
|
|
3228
|
+
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
|
3229
|
+
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
|
3230
|
+
launchStyle = "0"
|
|
3231
|
+
useCustomWorkingDirectory = "NO"
|
|
3232
|
+
ignoresPersistentStateOnLaunch = "NO"
|
|
3233
|
+
debugDocumentVersioning = "YES"
|
|
3234
|
+
debugServiceExtension = "internal"
|
|
3235
|
+
allowLocationSimulation = "YES">
|
|
3236
|
+
<BuildableProductRunnable
|
|
3237
|
+
runnableDebuggingMode = "0">
|
|
3238
|
+
<BuildableReference
|
|
3239
|
+
BuildableIdentifier = "primary"
|
|
3240
|
+
BlueprintIdentifier = "B73550662FEBAC0F0005B4CC"
|
|
3241
|
+
BuildableName = "App.app"
|
|
3242
|
+
BlueprintName = "App"
|
|
3243
|
+
ReferencedContainer = "container:App.xcodeproj">
|
|
3244
|
+
</BuildableReference>
|
|
3245
|
+
</BuildableProductRunnable>
|
|
3246
|
+
</LaunchAction>
|
|
3247
|
+
<ProfileAction
|
|
3248
|
+
buildConfiguration = "Release"
|
|
3249
|
+
shouldUseLaunchSchemeArgsEnv = "YES"
|
|
3250
|
+
savedToolIdentifier = ""
|
|
3251
|
+
useCustomWorkingDirectory = "NO"
|
|
3252
|
+
debugDocumentVersioning = "YES">
|
|
3253
|
+
<BuildableProductRunnable
|
|
3254
|
+
runnableDebuggingMode = "0">
|
|
3255
|
+
<BuildableReference
|
|
3256
|
+
BuildableIdentifier = "primary"
|
|
3257
|
+
BlueprintIdentifier = "B73550662FEBAC0F0005B4CC"
|
|
3258
|
+
BuildableName = "App.app"
|
|
3259
|
+
BlueprintName = "App"
|
|
3260
|
+
ReferencedContainer = "container:App.xcodeproj">
|
|
3261
|
+
</BuildableReference>
|
|
3262
|
+
</BuildableProductRunnable>
|
|
3263
|
+
</ProfileAction>
|
|
3264
|
+
<AnalyzeAction
|
|
3265
|
+
buildConfiguration = "Debug">
|
|
3266
|
+
</AnalyzeAction>
|
|
3267
|
+
<ArchiveAction
|
|
3268
|
+
buildConfiguration = "Release"
|
|
3269
|
+
revealArchiveInOrganizer = "YES">
|
|
3270
|
+
</ArchiveAction>
|
|
3271
|
+
</Scheme>
|
|
3272
|
+
`;
|
|
3273
|
+
var APP_DELEGATE = `//
|
|
3274
|
+
// AppDelegate.swift
|
|
3275
|
+
// Generated by lecodes app init — yours to edit.
|
|
3276
|
+
//
|
|
3277
|
+
|
|
3278
|
+
import UIKit
|
|
3279
|
+
|
|
3280
|
+
@main
|
|
3281
|
+
class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
3282
|
+
|
|
3283
|
+
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
3284
|
+
return true
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
// MARK: UISceneSession Lifecycle
|
|
3288
|
+
|
|
3289
|
+
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
|
3290
|
+
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
`;
|
|
3294
|
+
var SCENE_DELEGATE = `//
|
|
3295
|
+
// SceneDelegate.swift
|
|
3296
|
+
// Generated by lecodes app init — yours to edit.
|
|
3297
|
+
//
|
|
3298
|
+
|
|
3299
|
+
import UIKit
|
|
3300
|
+
|
|
3301
|
+
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
|
3302
|
+
|
|
3303
|
+
var window: UIWindow?
|
|
3304
|
+
|
|
3305
|
+
func scene(
|
|
3306
|
+
_ scene: UIScene,
|
|
3307
|
+
willConnectTo session: UISceneSession,
|
|
3308
|
+
options connectionOptions: UIScene.ConnectionOptions
|
|
3309
|
+
) {
|
|
3310
|
+
guard let windowScene = (scene as? UIWindowScene) else { return }
|
|
3311
|
+
|
|
3312
|
+
let window = UIWindow(windowScene: windowScene)
|
|
3313
|
+
window.rootViewController = ViewController()
|
|
3314
|
+
self.window = window
|
|
3315
|
+
window.makeKeyAndVisible()
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
`;
|
|
3319
|
+
var VIEW_CONTROLLER = `//
|
|
3320
|
+
// ViewController.swift
|
|
3321
|
+
// Generated by lecodes app init — yours to edit.
|
|
3322
|
+
//
|
|
3323
|
+
// Runs the bundled app.js (produced by \`lecodes app sync\`) offline: everything the
|
|
3324
|
+
// script needs is provided by the SDK core (_creatorUI / _creatorUtils / console)
|
|
3325
|
+
// plus the plugins registered below via LeCodesRuntime.
|
|
3326
|
+
//
|
|
3327
|
+
|
|
3328
|
+
import UIKit
|
|
3329
|
+
import LeCodesRuntime
|
|
3330
|
+
|
|
3331
|
+
class ViewController: LeCodesViewController {
|
|
3332
|
+
|
|
3333
|
+
override func viewDidLoad() {
|
|
3334
|
+
super.viewDidLoad()
|
|
3335
|
+
|
|
3336
|
+
let lang = Locale.preferredLanguages.first.map { String($0.prefix(2)) } ?? "en"
|
|
3337
|
+
engine.setLanguage(lang)
|
|
3338
|
+
|
|
3339
|
+
// \`// preload:\` assets (images, fonts) resolve out of the app bundle.
|
|
3340
|
+
engine.setFetchLocal { path in
|
|
3341
|
+
let ns = path as NSString
|
|
3342
|
+
let ext = ns.pathExtension
|
|
3343
|
+
let name = ns.deletingPathExtension
|
|
3344
|
+
guard let url = Bundle.main.url(forResource: name, withExtension: ext.isEmpty ? nil : ext) else {
|
|
3345
|
+
return nil
|
|
3346
|
+
}
|
|
3347
|
+
return try? Data(contentsOf: url)
|
|
3348
|
+
}
|
|
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
|
|
3356
|
+
}
|
|
3357
|
+
engine.run(code)
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
`;
|
|
3361
|
+
var infoPlistTemplate = (pluginKeys) => {
|
|
3362
|
+
const keys = Object.keys(pluginKeys).sort();
|
|
3363
|
+
const pluginBlock = keys.length === 0 ? "" : `
|
|
3364
|
+
<!-- lecodes:plugins — merged from installed plugin manifests by \`lecodes app sync\` -->
|
|
3365
|
+
${keys.map((k) => ` <key>${k}</key>
|
|
3366
|
+
<string>${pluginKeys[k]}</string>`).join(`
|
|
3367
|
+
`)}`;
|
|
3368
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3369
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3370
|
+
<plist version="1.0">
|
|
3371
|
+
<dict>
|
|
3372
|
+
<key>UIApplicationSceneManifest</key>
|
|
3373
|
+
<dict>
|
|
3374
|
+
<key>UIApplicationSupportsMultipleScenes</key>
|
|
3375
|
+
<false/>
|
|
3376
|
+
<key>UISceneConfigurations</key>
|
|
3377
|
+
<dict>
|
|
3378
|
+
<key>UIWindowSceneSessionRoleApplication</key>
|
|
3379
|
+
<array>
|
|
3380
|
+
<dict>
|
|
3381
|
+
<key>UISceneConfigurationName</key>
|
|
3382
|
+
<string>Default Configuration</string>
|
|
3383
|
+
<key>UISceneDelegateClassName</key>
|
|
3384
|
+
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
|
3385
|
+
</dict>
|
|
3386
|
+
</array>
|
|
3387
|
+
</dict>
|
|
3388
|
+
</dict>${pluginBlock}
|
|
3389
|
+
</dict>
|
|
3390
|
+
</plist>
|
|
3391
|
+
`;
|
|
3392
|
+
};
|
|
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
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
`;
|
|
3426
|
+
var ACCENT_COLOR_JSON = `{
|
|
3427
|
+
"colors" : [
|
|
3428
|
+
{
|
|
3429
|
+
"idiom" : "universal"
|
|
3430
|
+
}
|
|
3431
|
+
],
|
|
3432
|
+
"info" : {
|
|
3433
|
+
"author" : "xcode",
|
|
3434
|
+
"version" : 1
|
|
3435
|
+
}
|
|
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
|
+
`;
|
|
3513
|
+
};
|
|
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
|
|
3535
|
+
|
|
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}`);
|
|
3593
|
+
}
|
|
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}").`);
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
return {
|
|
3672
|
+
id,
|
|
3673
|
+
dir,
|
|
3674
|
+
manifest,
|
|
3675
|
+
lock: { repo, tag: checkout.tag, commit: checkout.commit, version: manifest.version ?? "unknown" }
|
|
3676
|
+
};
|
|
3677
|
+
};
|
|
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}`);
|
|
3878
|
+
} else {
|
|
3879
|
+
throw new CliError(`Unknown plugin "${id}" — known: ${Object.keys(FIRST_PARTY_PLUGINS).join(", ")}, or use github:owner/repo[@tag].`);
|
|
3880
|
+
}
|
|
3881
|
+
writeJson(configPath, config);
|
|
3882
|
+
note("Run `lecodes app sync` to vendor it into the shell.");
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
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.`);
|
|
3895
|
+
};
|
|
3896
|
+
var runTool = (cwd, cmd, args) => {
|
|
3897
|
+
try {
|
|
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];
|
|
3944
|
+
} catch {}
|
|
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`.");
|
|
4085
|
+
};
|
|
4086
|
+
|
|
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 = {
|
|
1815
4095
|
compilerOptions: {
|
|
1816
4096
|
target: "ES2020",
|
|
1817
4097
|
module: "ESNext",
|
|
@@ -1828,11 +4108,11 @@ var TSCONFIG = {
|
|
|
1828
4108
|
exclude: ["node_modules"]
|
|
1829
4109
|
};
|
|
1830
4110
|
var writeTypeBundle = (root, bundle) => {
|
|
1831
|
-
const typesDir =
|
|
4111
|
+
const typesDir = join12(root, LECODES_DIR, "types");
|
|
1832
4112
|
for (const [path, text] of Object.entries(bundle)) {
|
|
1833
|
-
const dest =
|
|
1834
|
-
|
|
1835
|
-
|
|
4113
|
+
const dest = join12(typesDir, path);
|
|
4114
|
+
mkdirSync6(dirname8(dest), { recursive: true });
|
|
4115
|
+
writeFileSync8(dest, text);
|
|
1836
4116
|
}
|
|
1837
4117
|
};
|
|
1838
4118
|
var materializeTypes = async (root, apiUrl) => {
|
|
@@ -1840,13 +4120,13 @@ var materializeTypes = async (root, apiUrl) => {
|
|
|
1840
4120
|
};
|
|
1841
4121
|
var readLocalTypeBundle = () => {
|
|
1842
4122
|
const candidates = [
|
|
1843
|
-
|
|
1844
|
-
|
|
4123
|
+
join12(distRoot, "runtime", "sdk-types.json"),
|
|
4124
|
+
join12(distRoot, "..", "sdk", "dist", "types.json")
|
|
1845
4125
|
];
|
|
1846
4126
|
for (const path of candidates) {
|
|
1847
|
-
if (
|
|
4127
|
+
if (existsSync9(path)) {
|
|
1848
4128
|
try {
|
|
1849
|
-
return JSON.parse(
|
|
4129
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
1850
4130
|
} catch {}
|
|
1851
4131
|
}
|
|
1852
4132
|
}
|
|
@@ -1860,100 +4140,7 @@ var materializeTypesLocal = (root) => {
|
|
|
1860
4140
|
return true;
|
|
1861
4141
|
};
|
|
1862
4142
|
var writeTsconfig = (root) => {
|
|
1863
|
-
|
|
1864
|
-
};
|
|
1865
|
-
|
|
1866
|
-
// src/ignore.ts
|
|
1867
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1868
|
-
import { join as join6 } from "node:path";
|
|
1869
|
-
var BUILTIN_PATTERNS = ["node_modules/", ".git/", "_*", "*.log", ".DS_Store"];
|
|
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.
|
|
1873
|
-
|
|
1874
|
-
# Render / preview outputs (the default \`lecodes render --png\` target)
|
|
1875
|
-
screenshot.png
|
|
1876
|
-
shots/
|
|
1877
|
-
|
|
1878
|
-
# Image & other binary assets ARE pushed by default — they're real project resources.
|
|
1879
|
-
# Uncomment a pattern below only if you want to keep some untracked ones out of pushes.
|
|
1880
|
-
# *.png
|
|
1881
|
-
|
|
1882
|
-
# Local notes & dumps (uncomment to ignore)
|
|
1883
|
-
# *.json
|
|
1884
|
-
# notes.md
|
|
1885
|
-
`;
|
|
1886
|
-
var translate = (glob) => {
|
|
1887
|
-
let re = "";
|
|
1888
|
-
for (let i = 0;i < glob.length; i++) {
|
|
1889
|
-
const ch = glob[i];
|
|
1890
|
-
if (ch === "*") {
|
|
1891
|
-
if (glob[i + 1] === "*") {
|
|
1892
|
-
re += ".*";
|
|
1893
|
-
i++;
|
|
1894
|
-
if (glob[i + 1] === "/")
|
|
1895
|
-
i++;
|
|
1896
|
-
} else
|
|
1897
|
-
re += "[^/]*";
|
|
1898
|
-
} else if (ch === "?")
|
|
1899
|
-
re += "[^/]";
|
|
1900
|
-
else if (ch === "/")
|
|
1901
|
-
re += "/";
|
|
1902
|
-
else if ("\\^$.|+()[]{}".includes(ch))
|
|
1903
|
-
re += "\\" + ch;
|
|
1904
|
-
else
|
|
1905
|
-
re += ch;
|
|
1906
|
-
}
|
|
1907
|
-
return re;
|
|
1908
|
-
};
|
|
1909
|
-
var compile = (raw) => {
|
|
1910
|
-
let p = raw.trim();
|
|
1911
|
-
if (!p || p.startsWith("#"))
|
|
1912
|
-
return null;
|
|
1913
|
-
let negate = false;
|
|
1914
|
-
if (p.startsWith("!")) {
|
|
1915
|
-
negate = true;
|
|
1916
|
-
p = p.slice(1);
|
|
1917
|
-
}
|
|
1918
|
-
let dirOnly = false;
|
|
1919
|
-
if (p.endsWith("/")) {
|
|
1920
|
-
dirOnly = true;
|
|
1921
|
-
p = p.slice(0, -1);
|
|
1922
|
-
}
|
|
1923
|
-
const anchored = p.startsWith("/") || p.includes("/");
|
|
1924
|
-
if (p.startsWith("/"))
|
|
1925
|
-
p = p.slice(1);
|
|
1926
|
-
if (!p)
|
|
1927
|
-
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
|
-
}
|
|
1940
|
-
return ignored;
|
|
1941
|
-
};
|
|
1942
|
-
var loadIgnore = (root) => {
|
|
1943
|
-
const lines = [...BUILTIN_PATTERNS];
|
|
1944
|
-
const file = join6(root, ".lecodesignore");
|
|
1945
|
-
if (existsSync4(file)) {
|
|
1946
|
-
try {
|
|
1947
|
-
lines.push(...readFileSync4(file, "utf8").split(/\r?\n/));
|
|
1948
|
-
} catch {}
|
|
1949
|
-
}
|
|
1950
|
-
const rules = lines.map(compile).filter((r) => r !== null);
|
|
1951
|
-
return buildMatcher(rules);
|
|
1952
|
-
};
|
|
1953
|
-
var writeDefaultIgnore = (root) => {
|
|
1954
|
-
const file = join6(root, ".lecodesignore");
|
|
1955
|
-
if (!existsSync4(file))
|
|
1956
|
-
writeFileSync5(file, DEFAULT_IGNORE_FILE);
|
|
4143
|
+
writeFileSync8(join12(root, "tsconfig.json"), JSON.stringify(TSCONFIG, null, 2));
|
|
1957
4144
|
};
|
|
1958
4145
|
|
|
1959
4146
|
// src/commands/clone.ts
|
|
@@ -1974,224 +4161,81 @@ var clone = async (args) => {
|
|
|
1974
4161
|
const uuid = parseUuid(args._[0]);
|
|
1975
4162
|
const project = await getProject(apiUrl, token, uuid);
|
|
1976
4163
|
const dir = args._[1] ?? slug(project.name);
|
|
1977
|
-
const root =
|
|
1978
|
-
if (
|
|
1979
|
-
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
1980
|
-
}
|
|
1981
|
-
mkdirSync5(root, { recursive: true });
|
|
1982
|
-
info(c.dim(`Fetching project ${c.bold(project.name)}…`));
|
|
1983
|
-
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
1984
|
-
endProgress();
|
|
1985
|
-
if (!flagBool(args, "no-types")) {
|
|
1986
|
-
info(c.dim("Fetching type definitions…"));
|
|
1987
|
-
await materializeTypes(root, apiUrl);
|
|
1988
|
-
writeTsconfig(root);
|
|
1989
|
-
}
|
|
1990
|
-
writeDefaultIgnore(root);
|
|
1991
|
-
const commits = await getCommits(apiUrl, token, uuid);
|
|
1992
|
-
writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
|
|
1993
|
-
success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`);
|
|
1994
|
-
info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``));
|
|
1995
|
-
};
|
|
1996
|
-
|
|
1997
|
-
// src/commands/create.ts
|
|
1998
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1999
|
-
import { join as join7, resolve as resolve2 } from "node:path";
|
|
2000
|
-
var slug2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
2001
|
-
var STARTER = `// Welcome to le.codes — no imports needed, the whole SDK is available as globals.
|
|
2002
|
-
const scene = new Scene2D({ background: '#10131a' })
|
|
2003
|
-
scene.open()
|
|
2004
|
-
|
|
2005
|
-
setLoop((dt) => {
|
|
2006
|
-
// your per-frame logic — dt is seconds since the previous frame
|
|
2007
|
-
})
|
|
2008
|
-
`;
|
|
2009
|
-
var create = async (args) => {
|
|
2010
|
-
const config = loadConfig();
|
|
2011
|
-
const apiUrl = requireApiUrl(config);
|
|
2012
|
-
const token = requireToken(config);
|
|
2013
|
-
const name = args._[0];
|
|
2014
|
-
if (!name)
|
|
2015
|
-
throw new CliError("Usage: lecodes create <name> [directory]");
|
|
2016
|
-
const dir = args._[1] ?? slug2(name);
|
|
2017
|
-
const root = resolve2(dir);
|
|
2018
|
-
if (existsSync6(root) && readdirSync2(root).length > 0) {
|
|
2019
|
-
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
2020
|
-
}
|
|
2021
|
-
mkdirSync6(root, { recursive: true });
|
|
2022
|
-
info(c.dim(`Creating project ${c.bold(name)}…`));
|
|
2023
|
-
const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
|
|
2024
|
-
info(c.dim("Fetching type definitions…"));
|
|
2025
|
-
await materializeTypes(root, apiUrl);
|
|
2026
|
-
writeTsconfig(root);
|
|
2027
|
-
writeDefaultIgnore(root);
|
|
2028
|
-
writeFileSync6(join7(root, "main.ts"), STARTER);
|
|
2029
|
-
const commits = await getCommits(apiUrl, token, uuid);
|
|
2030
|
-
writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
|
|
2031
|
-
success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
|
|
2032
|
-
info(c.dim(` cd ${dir} — edit main.ts, then \`lecodes push -m "first commit"\` to save it`));
|
|
2033
|
-
};
|
|
2034
|
-
|
|
2035
|
-
// src/commands/design.ts
|
|
2036
|
-
import { existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync10, readdirSync as readdirSync6, writeFileSync as writeFileSync8, appendFileSync } from "node:fs";
|
|
2037
|
-
import { basename as basename2, join as join13 } from "node:path";
|
|
2038
|
-
|
|
2039
|
-
// src/compile/designCompile.ts
|
|
2040
|
-
import { readFileSync as readFileSync9 } from "node:fs";
|
|
2041
|
-
import { basename } from "node:path";
|
|
2042
|
-
|
|
2043
|
-
// src/localFiles.ts
|
|
2044
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
2045
|
-
import { join as join8, relative } from "node:path";
|
|
2046
|
-
var SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
|
|
2047
|
-
var SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
|
|
2048
|
-
var TEXT_EXTS = new Set([
|
|
2049
|
-
"ts",
|
|
2050
|
-
"tsx",
|
|
2051
|
-
"js",
|
|
2052
|
-
"jsx",
|
|
2053
|
-
"mjs",
|
|
2054
|
-
"cjs",
|
|
2055
|
-
"json",
|
|
2056
|
-
"svg",
|
|
2057
|
-
"glsl",
|
|
2058
|
-
"vert",
|
|
2059
|
-
"frag",
|
|
2060
|
-
"txt",
|
|
2061
|
-
"md",
|
|
2062
|
-
"css",
|
|
2063
|
-
"html",
|
|
2064
|
-
"xml",
|
|
2065
|
-
"yml",
|
|
2066
|
-
"yaml",
|
|
2067
|
-
"csv"
|
|
2068
|
-
]);
|
|
2069
|
-
var ext = (name) => {
|
|
2070
|
-
const i = name.lastIndexOf(".");
|
|
2071
|
-
return i < 0 ? "" : name.slice(i + 1).toLowerCase();
|
|
2072
|
-
};
|
|
2073
|
-
var classifyKind = (name) => {
|
|
2074
|
-
const e = ext(name);
|
|
2075
|
-
if (e === "mat")
|
|
2076
|
-
return "shader";
|
|
2077
|
-
if (e === "scene")
|
|
2078
|
-
return "scene";
|
|
2079
|
-
return TEXT_EXTS.has(e) ? "text" : "resource";
|
|
2080
|
-
};
|
|
2081
|
-
var isTextKind = (kind) => kind !== "resource";
|
|
2082
|
-
var scanLocal = (root, opts = {}) => {
|
|
2083
|
-
const { ignore, tracked } = opts;
|
|
2084
|
-
const files = [];
|
|
2085
|
-
const dirs = [];
|
|
2086
|
-
const ignored = [];
|
|
2087
|
-
const isTracked = (key) => tracked?.has(key) ?? false;
|
|
2088
|
-
const hasTrackedUnder = (key) => {
|
|
2089
|
-
if (!tracked)
|
|
2090
|
-
return false;
|
|
2091
|
-
const prefix = key + "/";
|
|
2092
|
-
for (const t of tracked)
|
|
2093
|
-
if (t.startsWith(prefix))
|
|
2094
|
-
return true;
|
|
2095
|
-
return false;
|
|
2096
|
-
};
|
|
2097
|
-
const walk = (dir, parentIgnored) => {
|
|
2098
|
-
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
2099
|
-
const abs = join8(dir, entry.name);
|
|
2100
|
-
const key = toKey(relative(root, abs));
|
|
2101
|
-
if (entry.isDirectory()) {
|
|
2102
|
-
if (SKIP_DIRS.has(entry.name))
|
|
2103
|
-
continue;
|
|
2104
|
-
const dirIgnored = parentIgnored || !!ignore && ignore(key, true);
|
|
2105
|
-
if (dirIgnored && !isTracked(key) && !hasTrackedUnder(key)) {
|
|
2106
|
-
ignored.push(key + "/");
|
|
2107
|
-
continue;
|
|
2108
|
-
}
|
|
2109
|
-
dirs.push(key);
|
|
2110
|
-
walk(abs, dirIgnored);
|
|
2111
|
-
} else if (entry.isFile()) {
|
|
2112
|
-
if (SKIP_FILES.has(entry.name))
|
|
2113
|
-
continue;
|
|
2114
|
-
const fileIgnored = parentIgnored || !!ignore && ignore(key, false);
|
|
2115
|
-
if (fileIgnored && !isTracked(key)) {
|
|
2116
|
-
ignored.push(key);
|
|
2117
|
-
continue;
|
|
2118
|
-
}
|
|
2119
|
-
files.push({ path: key, absPath: abs, sha: sha256(readFileSync5(abs)) });
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
};
|
|
2123
|
-
walk(root, false);
|
|
2124
|
-
return { files, dirs, ignored };
|
|
2125
|
-
};
|
|
2126
|
-
var scanProject = (root, manifest) => scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest.files)) });
|
|
2127
|
-
var diffLocal = (manifest, scan) => {
|
|
2128
|
-
const added = [];
|
|
2129
|
-
const modified = [];
|
|
2130
|
-
const localByPath = new Map(scan.files.map((f) => [f.path, f]));
|
|
2131
|
-
for (const file of scan.files) {
|
|
2132
|
-
const known = manifest.files[file.path];
|
|
2133
|
-
if (!known)
|
|
2134
|
-
added.push(file);
|
|
2135
|
-
else if (known.sha !== file.sha)
|
|
2136
|
-
modified.push(file);
|
|
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.`);
|
|
2137
4167
|
}
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
const
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
}
|
|
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);
|
|
2147
4176
|
}
|
|
2148
|
-
|
|
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"\``));
|
|
2149
4182
|
};
|
|
2150
|
-
var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
|
|
2151
4183
|
|
|
2152
|
-
// src/
|
|
2153
|
-
import { existsSync as
|
|
2154
|
-
import {
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
const
|
|
2169
|
-
|
|
4184
|
+
// src/commands/create.ts
|
|
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";
|
|
4187
|
+
var slug2 = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
4188
|
+
var STARTER = `// Welcome to le.codes — no imports needed, the whole SDK is available as globals.
|
|
4189
|
+
const scene = new Scene2D({ background: '#10131a' })
|
|
4190
|
+
scene.open()
|
|
4191
|
+
|
|
4192
|
+
setLoop((dt) => {
|
|
4193
|
+
// your per-frame logic — dt is seconds since the previous frame
|
|
4194
|
+
})
|
|
4195
|
+
`;
|
|
4196
|
+
var create = async (args) => {
|
|
4197
|
+
const config = loadConfig();
|
|
4198
|
+
const apiUrl = requireApiUrl(config);
|
|
4199
|
+
const token = requireToken(config);
|
|
4200
|
+
const name = args._[0];
|
|
4201
|
+
if (!name)
|
|
4202
|
+
throw new CliError("Usage: lecodes create <name> [directory]");
|
|
4203
|
+
const dir = args._[1] ?? slug2(name);
|
|
4204
|
+
const root = resolve4(dir);
|
|
4205
|
+
if (existsSync11(root) && readdirSync6(root).length > 0) {
|
|
4206
|
+
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
4207
|
+
}
|
|
4208
|
+
mkdirSync8(root, { recursive: true });
|
|
4209
|
+
info(c.dim(`Creating project ${c.bold(name)}…`));
|
|
4210
|
+
const { uuid } = await createProject(apiUrl, token, name, flagStr(args, "team"));
|
|
4211
|
+
info(c.dim("Fetching type definitions…"));
|
|
4212
|
+
await materializeTypes(root, apiUrl);
|
|
4213
|
+
writeTsconfig(root);
|
|
4214
|
+
writeDefaultIgnore(root);
|
|
4215
|
+
writeFileSync9(join13(root, "main.ts"), STARTER);
|
|
4216
|
+
const commits = await getCommits(apiUrl, token, uuid);
|
|
4217
|
+
writeManifest(root, { uuid, apiUrl, name, baseHeadId: commits.headId, files: {} });
|
|
4218
|
+
success(`Created ${c.bold(name)} in ${c.bold(dir)}/`);
|
|
4219
|
+
info(c.dim(` cd ${dir} — edit main.ts, then \`lecodes push -m "first commit"\` to save it`));
|
|
2170
4220
|
};
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
header: opts.header,
|
|
2180
|
-
format: opts.format,
|
|
2181
|
-
localAssets: opts.localAssets,
|
|
2182
|
-
minify: opts.minify,
|
|
2183
|
-
editor: opts.editor
|
|
2184
|
-
});
|
|
4221
|
+
|
|
4222
|
+
// src/commands/design.ts
|
|
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";
|
|
4225
|
+
|
|
4226
|
+
// src/compile/designCompile.ts
|
|
4227
|
+
import { readFileSync as readFileSync13 } from "node:fs";
|
|
4228
|
+
import { basename as basename5 } from "node:path";
|
|
2185
4229
|
|
|
2186
4230
|
// src/compile/assetIcons.ts
|
|
2187
|
-
import { createHash as
|
|
2188
|
-
import { existsSync as
|
|
2189
|
-
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";
|
|
2190
4234
|
var DEFAULT_REGISTRY = process.env.LECODES_ICON_REGISTRY?.trim() || "https://icon-registry.jt3.ru/v1";
|
|
2191
4235
|
var ICON_ID = /^([a-z0-9]+(?:-[a-z0-9]+)*):([a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
|
2192
|
-
var
|
|
2193
|
-
var
|
|
2194
|
-
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) => {
|
|
2195
4239
|
const quote = src[start];
|
|
2196
4240
|
const n = src.length;
|
|
2197
4241
|
let i = start + 1;
|
|
@@ -2202,7 +4246,7 @@ var skipString = (src, start) => {
|
|
|
2202
4246
|
continue;
|
|
2203
4247
|
}
|
|
2204
4248
|
if (quote === "`" && c2 === "$" && src[i + 1] === "{") {
|
|
2205
|
-
i =
|
|
4249
|
+
i = skipBraces2(src, i + 1);
|
|
2206
4250
|
continue;
|
|
2207
4251
|
}
|
|
2208
4252
|
if (c2 === quote)
|
|
@@ -2211,14 +4255,14 @@ var skipString = (src, start) => {
|
|
|
2211
4255
|
}
|
|
2212
4256
|
return n;
|
|
2213
4257
|
};
|
|
2214
|
-
var
|
|
4258
|
+
var skipBraces2 = (src, open) => {
|
|
2215
4259
|
const n = src.length;
|
|
2216
4260
|
let depth = 0;
|
|
2217
4261
|
let i = open;
|
|
2218
4262
|
while (i < n) {
|
|
2219
4263
|
const c2 = src[i];
|
|
2220
4264
|
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
2221
|
-
i =
|
|
4265
|
+
i = skipString2(src, i);
|
|
2222
4266
|
continue;
|
|
2223
4267
|
}
|
|
2224
4268
|
if (c2 === "{")
|
|
@@ -2232,14 +4276,14 @@ var skipBraces = (src, open) => {
|
|
|
2232
4276
|
}
|
|
2233
4277
|
return n;
|
|
2234
4278
|
};
|
|
2235
|
-
var
|
|
4279
|
+
var matchParen2 = (src, open) => {
|
|
2236
4280
|
const n = src.length;
|
|
2237
4281
|
let depth = 0;
|
|
2238
4282
|
let i = open;
|
|
2239
4283
|
while (i < n) {
|
|
2240
4284
|
const c2 = src[i];
|
|
2241
4285
|
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
2242
|
-
i =
|
|
4286
|
+
i = skipString2(src, i);
|
|
2243
4287
|
continue;
|
|
2244
4288
|
}
|
|
2245
4289
|
if (c2 === "/" && src[i + 1] === "/") {
|
|
@@ -2267,7 +4311,7 @@ var matchParen = (src, open) => {
|
|
|
2267
4311
|
}
|
|
2268
4312
|
return -1;
|
|
2269
4313
|
};
|
|
2270
|
-
var
|
|
4314
|
+
var splitTopLevel2 = (text, delim) => {
|
|
2271
4315
|
const parts = [];
|
|
2272
4316
|
const n = text.length;
|
|
2273
4317
|
let depth = 0;
|
|
@@ -2276,7 +4320,7 @@ var splitTopLevel = (text, delim) => {
|
|
|
2276
4320
|
while (i < n) {
|
|
2277
4321
|
const c2 = text[i];
|
|
2278
4322
|
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
2279
|
-
i =
|
|
4323
|
+
i = skipString2(text, i);
|
|
2280
4324
|
continue;
|
|
2281
4325
|
}
|
|
2282
4326
|
if (c2 === "(" || c2 === "[" || c2 === "{")
|
|
@@ -2292,7 +4336,7 @@ var splitTopLevel = (text, delim) => {
|
|
|
2292
4336
|
parts.push(text.slice(last));
|
|
2293
4337
|
return parts;
|
|
2294
4338
|
};
|
|
2295
|
-
var
|
|
4339
|
+
var stringLiteralValue2 = (text) => {
|
|
2296
4340
|
const t = text.trim();
|
|
2297
4341
|
if (t.length < 2)
|
|
2298
4342
|
return null;
|
|
@@ -2313,7 +4357,7 @@ var scanAssetIconCalls = (source) => {
|
|
|
2313
4357
|
while (i < n) {
|
|
2314
4358
|
const c2 = source[i];
|
|
2315
4359
|
if (c2 === '"' || c2 === "'" || c2 === "`") {
|
|
2316
|
-
i =
|
|
4360
|
+
i = skipString2(source, i);
|
|
2317
4361
|
continue;
|
|
2318
4362
|
}
|
|
2319
4363
|
if (c2 === "/" && source[i + 1] === "/") {
|
|
@@ -2330,12 +4374,12 @@ var scanAssetIconCalls = (source) => {
|
|
|
2330
4374
|
i += 2;
|
|
2331
4375
|
continue;
|
|
2332
4376
|
}
|
|
2333
|
-
if (
|
|
4377
|
+
if (isIdentStart2(c2)) {
|
|
2334
4378
|
let j = i + 1;
|
|
2335
|
-
while (j < n &&
|
|
4379
|
+
while (j < n && isIdentPart2(source[j]))
|
|
2336
4380
|
j++;
|
|
2337
|
-
if (source.slice(i, j) === "assetIcon" &&
|
|
2338
|
-
const call =
|
|
4381
|
+
if (source.slice(i, j) === "assetIcon" && prevNonSpace2(source, i) !== ".") {
|
|
4382
|
+
const call = parseCall2(source, i, j);
|
|
2339
4383
|
if (call) {
|
|
2340
4384
|
calls.push(call);
|
|
2341
4385
|
i = call.end;
|
|
@@ -2349,25 +4393,25 @@ var scanAssetIconCalls = (source) => {
|
|
|
2349
4393
|
}
|
|
2350
4394
|
return calls;
|
|
2351
4395
|
};
|
|
2352
|
-
var
|
|
4396
|
+
var prevNonSpace2 = (src, i) => {
|
|
2353
4397
|
let k = i - 1;
|
|
2354
4398
|
while (k >= 0 && /\s/.test(src[k]))
|
|
2355
4399
|
k--;
|
|
2356
4400
|
return k >= 0 ? src[k] : "";
|
|
2357
4401
|
};
|
|
2358
|
-
var
|
|
4402
|
+
var parseCall2 = (src, start, identEnd) => {
|
|
2359
4403
|
const n = src.length;
|
|
2360
4404
|
let i = identEnd;
|
|
2361
4405
|
while (i < n && /\s/.test(src[i]))
|
|
2362
4406
|
i++;
|
|
2363
4407
|
if (src[i] !== "(")
|
|
2364
4408
|
return null;
|
|
2365
|
-
const close =
|
|
4409
|
+
const close = matchParen2(src, i);
|
|
2366
4410
|
if (close === -1)
|
|
2367
4411
|
return null;
|
|
2368
4412
|
const call = { start, end: close + 1, id: "", pack: "", name: "" };
|
|
2369
|
-
const args =
|
|
2370
|
-
const id =
|
|
4413
|
+
const args = splitTopLevel2(src.slice(i + 1, close), ",");
|
|
4414
|
+
const id = stringLiteralValue2(args[0] ?? "");
|
|
2371
4415
|
if (id === null) {
|
|
2372
4416
|
call.error = `assetIcon(...) needs a string-literal id like "lucide:bell" (got \`${(args[0] ?? "").trim() || "()"}\`).`;
|
|
2373
4417
|
return call;
|
|
@@ -2398,8 +4442,8 @@ var extractColor = (optsText) => {
|
|
|
2398
4442
|
if (!optsText.startsWith("{") || !optsText.endsWith("}")) {
|
|
2399
4443
|
return { error: `assetIcon(id, opts): opts must be an object literal like { color: "#5b8cff" }.` };
|
|
2400
4444
|
}
|
|
2401
|
-
for (const prop of
|
|
2402
|
-
const kv =
|
|
4445
|
+
for (const prop of splitTopLevel2(optsText.slice(1, -1), ",")) {
|
|
4446
|
+
const kv = splitTopLevel2(prop, ":");
|
|
2403
4447
|
if (kv.length < 2)
|
|
2404
4448
|
continue;
|
|
2405
4449
|
const key = kv[0].trim().replace(/^["'`]|["'`]$/g, "");
|
|
@@ -2408,7 +4452,7 @@ var extractColor = (optsText) => {
|
|
|
2408
4452
|
const raw = kv.slice(1).join(":").trim();
|
|
2409
4453
|
if (!raw)
|
|
2410
4454
|
continue;
|
|
2411
|
-
const literal =
|
|
4455
|
+
const literal = stringLiteralValue2(raw);
|
|
2412
4456
|
return literal !== null ? { bake: literal } : { tint: raw };
|
|
2413
4457
|
}
|
|
2414
4458
|
return {};
|
|
@@ -2429,34 +4473,34 @@ var iconExpr = (svg, call) => {
|
|
|
2429
4473
|
return call.tint ? `Object.assign(${src}, { tintColor: (${call.tint}) })` : src;
|
|
2430
4474
|
};
|
|
2431
4475
|
var escapeTemplate = (s) => s.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
2432
|
-
var
|
|
2433
|
-
var
|
|
2434
|
-
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+$/, "");
|
|
2435
4479
|
var readText = async (registry, rel) => {
|
|
2436
|
-
if (
|
|
4480
|
+
if (isHttp2(registry)) {
|
|
2437
4481
|
const res = await fetch(`${registry}/${rel}`);
|
|
2438
4482
|
return res.ok ? await res.text() : null;
|
|
2439
4483
|
}
|
|
2440
4484
|
try {
|
|
2441
|
-
return
|
|
4485
|
+
return readFileSync12(join14(registry, rel), "utf8");
|
|
2442
4486
|
} catch {
|
|
2443
4487
|
return null;
|
|
2444
4488
|
}
|
|
2445
4489
|
};
|
|
2446
|
-
var
|
|
4490
|
+
var readLock2 = (path) => {
|
|
2447
4491
|
try {
|
|
2448
|
-
const lock = JSON.parse(
|
|
4492
|
+
const lock = JSON.parse(readFileSync12(path, "utf8"));
|
|
2449
4493
|
if (lock && typeof lock === "object" && lock.icons && lock.packs)
|
|
2450
4494
|
return lock;
|
|
2451
4495
|
} catch {}
|
|
2452
4496
|
return null;
|
|
2453
4497
|
};
|
|
2454
|
-
var
|
|
4498
|
+
var serializeLock2 = (lock) => {
|
|
2455
4499
|
const sortKeys = (rec) => Object.fromEntries(Object.keys(rec).sort().map((k) => [k, rec[k]]));
|
|
2456
4500
|
return JSON.stringify({ registry: lock.registry, icons: sortKeys(lock.icons), packs: sortKeys(lock.packs) }, null, 2) + `
|
|
2457
4501
|
`;
|
|
2458
4502
|
};
|
|
2459
|
-
var
|
|
4503
|
+
var renderNotices2 = (lock) => {
|
|
2460
4504
|
const packs = [...new Set(Object.values(lock.icons).map((i) => i.pack))].sort();
|
|
2461
4505
|
const lines = [
|
|
2462
4506
|
"# Icon attribution & notices",
|
|
@@ -2481,29 +4525,29 @@ var renderNotices = (lock) => {
|
|
|
2481
4525
|
return lines.join(`
|
|
2482
4526
|
`);
|
|
2483
4527
|
};
|
|
2484
|
-
var
|
|
2485
|
-
const current =
|
|
4528
|
+
var writeIfChanged2 = (path, text) => {
|
|
4529
|
+
const current = existsSync12(path) ? safeRead(path) : null;
|
|
2486
4530
|
if (current === text)
|
|
2487
4531
|
return false;
|
|
2488
|
-
|
|
4532
|
+
writeFileSync10(path, text);
|
|
2489
4533
|
return true;
|
|
2490
4534
|
};
|
|
2491
4535
|
var safeRead = (path) => {
|
|
2492
4536
|
try {
|
|
2493
|
-
return
|
|
4537
|
+
return readFileSync12(path, "utf8");
|
|
2494
4538
|
} catch {
|
|
2495
4539
|
return null;
|
|
2496
4540
|
}
|
|
2497
4541
|
};
|
|
2498
4542
|
var vendorIcons = async (designDir, ids, opts = {}) => {
|
|
2499
4543
|
const registry = (opts.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
|
|
2500
|
-
const lockPath =
|
|
2501
|
-
const lock =
|
|
4544
|
+
const lockPath = join14(designDir, "icons.lock.json");
|
|
4545
|
+
const lock = readLock2(lockPath) ?? { registry, icons: {}, packs: {} };
|
|
2502
4546
|
const svgById = new Map;
|
|
2503
4547
|
const missing = [];
|
|
2504
4548
|
for (const id of ids) {
|
|
2505
4549
|
const li = lock.icons[id];
|
|
2506
|
-
const cached = li && safeRead(
|
|
4550
|
+
const cached = li && safeRead(join14(designDir, li.path));
|
|
2507
4551
|
if (cached)
|
|
2508
4552
|
svgById.set(id, cached);
|
|
2509
4553
|
else
|
|
@@ -2519,23 +4563,23 @@ var vendorIcons = async (designDir, ids, opts = {}) => {
|
|
|
2519
4563
|
const [, pack, name] = m;
|
|
2520
4564
|
const lic = licenses[pack];
|
|
2521
4565
|
if (!lic)
|
|
2522
|
-
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)}.`);
|
|
2523
4567
|
const svg = await readText(registry, `${pack}/${name}.svg`);
|
|
2524
4568
|
if (svg === null)
|
|
2525
|
-
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)}.`);
|
|
2526
4570
|
const rel = `assets/icons/${pack}/${name}.svg`;
|
|
2527
|
-
const abs =
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
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 };
|
|
2531
4575
|
lock.packs[pack] = { spdx: lic.spdx, title: lic.title, url: lic.url, attribution: lic.attribution, modifiedNotice: lic.modifiedNotice };
|
|
2532
4576
|
svgById.set(id, svg);
|
|
2533
4577
|
}
|
|
2534
4578
|
lock.registry = registry;
|
|
2535
|
-
|
|
4579
|
+
writeIfChanged2(lockPath, serializeLock2(lock));
|
|
2536
4580
|
}
|
|
2537
4581
|
if (Object.keys(lock.icons).length)
|
|
2538
|
-
|
|
4582
|
+
writeIfChanged2(join14(designDir, "ICON-NOTICES.md"), renderNotices2(lock));
|
|
2539
4583
|
return svgById;
|
|
2540
4584
|
};
|
|
2541
4585
|
var applyAssetIcons = async (entries, designDir, opts = {}) => {
|
|
@@ -2559,7 +4603,7 @@ var compileDesignScreen = async (opts) => {
|
|
|
2559
4603
|
const entries = [];
|
|
2560
4604
|
const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
|
|
2561
4605
|
for (const file of scan.files) {
|
|
2562
|
-
const type = manifest?.files[file.path]?.type ?? classifyKind(
|
|
4606
|
+
const type = manifest?.files[file.path]?.type ?? classifyKind(basename5(file.path));
|
|
2563
4607
|
if (type === "resource") {
|
|
2564
4608
|
entries.push({
|
|
2565
4609
|
path: file.path,
|
|
@@ -2570,7 +4614,7 @@ var compileDesignScreen = async (opts) => {
|
|
|
2570
4614
|
} else if (type === "shader") {
|
|
2571
4615
|
entries.push({ path: file.path, type, shaders: [] });
|
|
2572
4616
|
} else {
|
|
2573
|
-
entries.push({ path: file.path, type, text:
|
|
4617
|
+
entries.push({ path: file.path, type, text: readFileSync13(file.absPath, "utf8") });
|
|
2574
4618
|
}
|
|
2575
4619
|
}
|
|
2576
4620
|
const invoke = state !== undefined ? `screen(${JSON.stringify(state)})` : "screen()";
|
|
@@ -2584,6 +4628,18 @@ var compileDesignScreen = async (opts) => {
|
|
|
2584
4628
|
`
|
|
2585
4629
|
});
|
|
2586
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
|
+
});
|
|
2587
4643
|
return compileProject2({
|
|
2588
4644
|
entries,
|
|
2589
4645
|
name: opts.name,
|
|
@@ -2630,10 +4686,10 @@ var openBrowser = (url) => {
|
|
|
2630
4686
|
};
|
|
2631
4687
|
var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
|
|
2632
4688
|
const state = randomBytes(16).toString("hex");
|
|
2633
|
-
let
|
|
4689
|
+
let resolve5;
|
|
2634
4690
|
let reject;
|
|
2635
4691
|
const tokenPromise = new Promise((res, rej) => {
|
|
2636
|
-
|
|
4692
|
+
resolve5 = res;
|
|
2637
4693
|
reject = rej;
|
|
2638
4694
|
});
|
|
2639
4695
|
const server = createServer((req, res) => {
|
|
@@ -2651,7 +4707,7 @@ var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser
|
|
|
2651
4707
|
}
|
|
2652
4708
|
res.writeHead(200, { "content-type": "text/html" });
|
|
2653
4709
|
res.end(SUCCESS_HTML);
|
|
2654
|
-
|
|
4710
|
+
resolve5(token);
|
|
2655
4711
|
});
|
|
2656
4712
|
await new Promise((res, rej) => {
|
|
2657
4713
|
server.once("error", rej);
|
|
@@ -2715,6 +4771,9 @@ Behaviour that isn't visible in a single screen — validation, permissions, not
|
|
|
2715
4771
|
cases — and the decisions you've made and why, so they aren't re-litigated later.
|
|
2716
4772
|
`;
|
|
2717
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") })
|
|
2718
4777
|
export const colors = {
|
|
2719
4778
|
bg: "#101114",
|
|
2720
4779
|
card: "#1c1e24",
|
|
@@ -2728,6 +4787,57 @@ export const spacing = { xs: 4, sm: 8, md: 16, lg: 24 }
|
|
|
2728
4787
|
|
|
2729
4788
|
export const radius = { sm: 8, md: 12, lg: 20 }
|
|
2730
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
|
+
`;
|
|
2731
4841
|
var HOME_SCREEN_TS = `import { colors, spacing, radius } from "../shared/tokens"
|
|
2732
4842
|
|
|
2733
4843
|
// Mock data: the domain shown on realistic values. Inline, per screen.
|
|
@@ -2806,6 +4916,12 @@ positions or descriptions.
|
|
|
2806
4916
|
|
|
2807
4917
|
- An edge's optional \`activator\` names the element (its \`name\` style option) that triggers the
|
|
2808
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".
|
|
2809
4925
|
- **Group each screen** you create by its user-facing area — \`"group": "Onboarding"\` — 2–6 screens
|
|
2810
4926
|
per group (Onboarding, Checkout, Settings…). Groups render as labeled regions and \`arrange\` lays
|
|
2811
4927
|
each out as its own sub-flow, so grouping keeps big boards legible. Semantic clustering is your
|
|
@@ -2853,6 +4969,25 @@ keep in the chat.
|
|
|
2853
4969
|
typing a component's return value or a container's children — with no imports. Trust the types;
|
|
2854
4970
|
don't fall back to \`any\`.
|
|
2855
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
|
+
|
|
2856
4991
|
## Screen states (variants)
|
|
2857
4992
|
|
|
2858
4993
|
A screen often has more than one look — a list vs. its empty state, a form vs. its error state.
|
|
@@ -2860,10 +4995,9 @@ Instead of a second file per variant, make the default export a **function of a
|
|
|
2860
4995
|
type is a string-literal union** — the union IS the state list (no separate schema):
|
|
2861
4996
|
|
|
2862
4997
|
\`\`\`ts
|
|
2863
|
-
export default (state: "default" | "empty" = "default") =>
|
|
4998
|
+
export default (state: "default" | "empty" = "default") => mainTabs.screen("home", [
|
|
2864
4999
|
Header(child),
|
|
2865
5000
|
state === "empty" ? EmptyState("Nothing here yet") : UIScrollable(items.map(Card)),
|
|
2866
|
-
TabBar(tabs, "home"),
|
|
2867
5001
|
])
|
|
2868
5002
|
\`\`\`
|
|
2869
5003
|
|
|
@@ -2881,6 +5015,35 @@ export default (state: "default" | "empty" = "default") => UIScreen([
|
|
|
2881
5015
|
\`"home@empty"\`) — still one node per screen. \`lecodes design check\` errors on an edge pointing at
|
|
2882
5016
|
a state the screen doesn't declare.
|
|
2883
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
|
+
|
|
2884
5047
|
## Icons
|
|
2885
5048
|
|
|
2886
5049
|
Use real icons instead of emoji. \`assetIcon("pack:name")\` is a **compile-time macro**: it vendors
|
|
@@ -2903,6 +5066,28 @@ UIImage(assetIcon("lucide:check")).style({ width: 20, height: 20, tintColor: col
|
|
|
2903
5066
|
(attribution) — **commit these**; they make rebuilds offline. An unknown \`pack:name\` is a clear
|
|
2904
5067
|
compile error.
|
|
2905
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
|
+
|
|
2906
5091
|
## See what you built
|
|
2907
5092
|
|
|
2908
5093
|
The developer watches the canvas live (screens hot-reload on save). To see a screen yourself:
|
|
@@ -2953,6 +5138,15 @@ var DESIGN_MACROS_DTS = `// Generated by \`lecodes design init\`. Globals the de
|
|
|
2953
5138
|
* string literal (resolved, and the SVG vendored, at compile time).
|
|
2954
5139
|
*/
|
|
2955
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
|
|
2956
5150
|
`;
|
|
2957
5151
|
var README_HINT = `# design/
|
|
2958
5152
|
|
|
@@ -2977,33 +5171,33 @@ var getContext = (args) => {
|
|
|
2977
5171
|
return {
|
|
2978
5172
|
root,
|
|
2979
5173
|
manifest,
|
|
2980
|
-
name: manifest?.name ??
|
|
5174
|
+
name: manifest?.name ?? basename6(root),
|
|
2981
5175
|
dirName,
|
|
2982
|
-
designDir:
|
|
5176
|
+
designDir: join15(root, dirName),
|
|
2983
5177
|
designKey: "/" + dirName.replace(/\\/g, "/")
|
|
2984
5178
|
};
|
|
2985
5179
|
};
|
|
2986
5180
|
var listScreens = (designDir) => {
|
|
2987
|
-
const dir =
|
|
2988
|
-
if (!
|
|
5181
|
+
const dir = join15(designDir, "screens");
|
|
5182
|
+
if (!existsSync13(dir))
|
|
2989
5183
|
return [];
|
|
2990
|
-
return
|
|
5184
|
+
return readdirSync7(dir).filter((f2) => f2.endsWith(".ts") && !f2.startsWith(".")).map((f2) => f2.slice(0, -3)).sort();
|
|
2991
5185
|
};
|
|
2992
5186
|
var readMetaSizes = (designDir) => {
|
|
2993
5187
|
try {
|
|
2994
|
-
const meta = JSON.parse(
|
|
5188
|
+
const meta = JSON.parse(readFileSync14(join15(designDir, "meta.json"), "utf8"));
|
|
2995
5189
|
return meta?.screens ?? {};
|
|
2996
5190
|
} catch {
|
|
2997
5191
|
return {};
|
|
2998
5192
|
}
|
|
2999
5193
|
};
|
|
3000
5194
|
var writeMcpConfig = (ctx, port) => {
|
|
3001
|
-
const path =
|
|
5195
|
+
const path = join15(ctx.root, ".mcp.json");
|
|
3002
5196
|
const entry = { type: "http", url: `http://localhost:${port}/mcp` };
|
|
3003
5197
|
let config = {};
|
|
3004
|
-
if (
|
|
5198
|
+
if (existsSync13(path)) {
|
|
3005
5199
|
try {
|
|
3006
|
-
config = JSON.parse(
|
|
5200
|
+
config = JSON.parse(readFileSync14(path, "utf8"));
|
|
3007
5201
|
} catch {
|
|
3008
5202
|
warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-design server by hand.");
|
|
3009
5203
|
return;
|
|
@@ -3016,7 +5210,7 @@ var writeMcpConfig = (ctx, port) => {
|
|
|
3016
5210
|
return;
|
|
3017
5211
|
}
|
|
3018
5212
|
config.mcpServers["lecodes-design"] = entry;
|
|
3019
|
-
|
|
5213
|
+
writeFileSync11(path, JSON.stringify(config, null, 2) + `
|
|
3020
5214
|
`);
|
|
3021
5215
|
log(`${c.green(existing ? "updated" : "created")} .mcp.json (lecodes-design MCP → ${entry.url})`);
|
|
3022
5216
|
};
|
|
@@ -3026,25 +5220,26 @@ var init = async (ctx, args) => {
|
|
|
3026
5220
|
["spec.md", SPEC_MD],
|
|
3027
5221
|
["README.md", README_HINT],
|
|
3028
5222
|
["CLAUDE.md", CLAUDE_MD],
|
|
3029
|
-
[
|
|
3030
|
-
[
|
|
5223
|
+
[join15("shared", "tokens.ts"), TOKENS_TS],
|
|
5224
|
+
[join15("shared", "tabs.ts"), TABS_TS],
|
|
5225
|
+
[join15("screens", "home.ts"), HOME_SCREEN_TS]
|
|
3031
5226
|
];
|
|
3032
|
-
|
|
3033
|
-
|
|
5227
|
+
mkdirSync10(join15(ctx.designDir, "screens"), { recursive: true });
|
|
5228
|
+
mkdirSync10(join15(ctx.designDir, "shared"), { recursive: true });
|
|
3034
5229
|
let created = 0;
|
|
3035
5230
|
for (const [rel, content] of files) {
|
|
3036
|
-
const abs =
|
|
3037
|
-
if (
|
|
5231
|
+
const abs = join15(ctx.designDir, rel);
|
|
5232
|
+
if (existsSync13(abs)) {
|
|
3038
5233
|
note(`kept ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
|
|
3039
5234
|
continue;
|
|
3040
5235
|
}
|
|
3041
|
-
|
|
5236
|
+
writeFileSync11(abs, content);
|
|
3042
5237
|
log(`${c.green("created")} ${ctx.dirName}/${rel.replace(/\\/g, "/")}`);
|
|
3043
5238
|
created++;
|
|
3044
5239
|
}
|
|
3045
|
-
const ignorePath =
|
|
5240
|
+
const ignorePath = join15(ctx.root, ".lecodesignore");
|
|
3046
5241
|
const ignoreLine = `${ctx.dirName}/.snapshots/`;
|
|
3047
|
-
const current =
|
|
5242
|
+
const current = existsSync13(ignorePath) ? readFileSync14(ignorePath, "utf8") : "";
|
|
3048
5243
|
if (!current.split(/\r?\n/).includes(ignoreLine)) {
|
|
3049
5244
|
appendFileSync(ignorePath, (current && !current.endsWith(`
|
|
3050
5245
|
`) ? `
|
|
@@ -3055,8 +5250,8 @@ var init = async (ctx, args) => {
|
|
|
3055
5250
|
writeMcpConfig(ctx, Number(flagStr(args, "port")) || DEFAULT_PORT);
|
|
3056
5251
|
const wroteTypes = materializeTypesLocal(ctx.designDir);
|
|
3057
5252
|
writeTsconfig(ctx.designDir);
|
|
3058
|
-
|
|
3059
|
-
|
|
5253
|
+
mkdirSync10(join15(ctx.designDir, ".lecodes", "types"), { recursive: true });
|
|
5254
|
+
writeFileSync11(join15(ctx.designDir, ".lecodes", "types", "design-macros.d.ts"), DESIGN_MACROS_DTS);
|
|
3060
5255
|
log(`${c.green("created")} ${ctx.dirName}/tsconfig.json${wroteTypes ? ` + ${ctx.dirName}/.lecodes/types/` : ""} (IDE types)`);
|
|
3061
5256
|
if (!wroteTypes)
|
|
3062
5257
|
warnErr("Bundled SDK types not found — tsconfig written, but globals may show unresolved until the CLI is rebuilt with vendored types.");
|
|
@@ -3105,16 +5300,16 @@ var snapshot = async (ctx, args) => {
|
|
|
3105
5300
|
killer.unref?.();
|
|
3106
5301
|
const png = flagBool(args, "png") || flagStr(args, "png") !== undefined;
|
|
3107
5302
|
const sizes = readMetaSizes(ctx.designDir);
|
|
3108
|
-
const outDir = flagStr(args, "out-dir") ??
|
|
5303
|
+
const outDir = flagStr(args, "out-dir") ?? join15(ctx.designDir, ".snapshots");
|
|
3109
5304
|
const single = targets.length === 1 && requested.length === 1 && !png && !allStates && !flagStr(args, "out-dir");
|
|
3110
5305
|
const onConsole = flagBool(args, "logs") ? (level, text) => logErr(`${c.dim(`[${level}]`)} ${text}`) : undefined;
|
|
3111
5306
|
if (!single)
|
|
3112
|
-
|
|
5307
|
+
mkdirSync10(outDir, { recursive: true });
|
|
3113
5308
|
for (const id of targets) {
|
|
3114
5309
|
const [width, height] = sizes[id]?.size ?? [390, 844];
|
|
3115
5310
|
let plan = [{ state: undefined, bases: [id] }];
|
|
3116
5311
|
if (discover) {
|
|
3117
|
-
const { states, canonical } = discover(
|
|
5312
|
+
const { states, canonical } = discover(readFileSync14(join15(ctx.designDir, "screens", `${id}.ts`), "utf8"));
|
|
3118
5313
|
if (stateFlag !== undefined) {
|
|
3119
5314
|
if (!states.includes(stateFlag)) {
|
|
3120
5315
|
if (requested.length === 1)
|
|
@@ -3155,18 +5350,18 @@ var snapshot = async (ctx, args) => {
|
|
|
3155
5350
|
for (const w of pngResult.warnings)
|
|
3156
5351
|
warnErr(`${tag}: ${w}`);
|
|
3157
5352
|
for (const base of bases) {
|
|
3158
|
-
|
|
3159
|
-
log(`${c.green("✓")} ${tag} → ${
|
|
5353
|
+
writeFileSync11(join15(outDir, `${base}.json`), result.text);
|
|
5354
|
+
log(`${c.green("✓")} ${tag} → ${join15(outDir, `${base}.json`)}`);
|
|
3160
5355
|
if (pngResult) {
|
|
3161
|
-
|
|
3162
|
-
log(`${c.green("✓")} ${tag} → ${
|
|
5356
|
+
writeFileSync11(join15(outDir, `${base}.png`), pngResult.png);
|
|
5357
|
+
log(`${c.green("✓")} ${tag} → ${join15(outDir, `${base}.png`)}`);
|
|
3163
5358
|
}
|
|
3164
5359
|
}
|
|
3165
5360
|
}
|
|
3166
5361
|
}
|
|
3167
5362
|
};
|
|
3168
5363
|
var serve = async (ctx, args) => {
|
|
3169
|
-
if (!
|
|
5364
|
+
if (!existsSync13(ctx.designDir)) {
|
|
3170
5365
|
throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
|
|
3171
5366
|
}
|
|
3172
5367
|
let designServer;
|
|
@@ -3239,7 +5434,7 @@ var serve = async (ctx, args) => {
|
|
|
3239
5434
|
openBrowser(server.url);
|
|
3240
5435
|
};
|
|
3241
5436
|
var arrange = async (ctx, args) => {
|
|
3242
|
-
if (!
|
|
5437
|
+
if (!existsSync13(ctx.designDir)) {
|
|
3243
5438
|
throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
|
|
3244
5439
|
}
|
|
3245
5440
|
let designServer;
|
|
@@ -3275,7 +5470,7 @@ var share = async (ctx, args) => {
|
|
|
3275
5470
|
note("Turn it off with: lecodes design share --off");
|
|
3276
5471
|
};
|
|
3277
5472
|
var check = async (ctx, args) => {
|
|
3278
|
-
if (!
|
|
5473
|
+
if (!existsSync13(ctx.designDir)) {
|
|
3279
5474
|
throw new CliError(`No ${ctx.dirName}/ folder here. Run "lecodes design init" to scaffold it.`);
|
|
3280
5475
|
}
|
|
3281
5476
|
let designServer;
|
|
@@ -3312,6 +5507,14 @@ var check = async (ctx, args) => {
|
|
|
3312
5507
|
log(` ${c.yellow("!")} ${s.id}: ${s.statesWarning}`);
|
|
3313
5508
|
log("");
|
|
3314
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
|
+
}
|
|
3315
5518
|
const hard = state.warnings.length;
|
|
3316
5519
|
if (hard === 0) {
|
|
3317
5520
|
const n = state.screens.length;
|
|
@@ -3340,36 +5543,15 @@ var design = async (args) => {
|
|
|
3340
5543
|
};
|
|
3341
5544
|
|
|
3342
5545
|
// src/commands/dev.ts
|
|
3343
|
-
import { existsSync as
|
|
5546
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync15, watch, writeFileSync as writeFileSync12 } from "node:fs";
|
|
3344
5547
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
3345
5548
|
import { networkInterfaces } from "node:os";
|
|
3346
|
-
import { basename as
|
|
5549
|
+
import { basename as basename7, join as join17 } from "node:path";
|
|
3347
5550
|
|
|
3348
5551
|
// src/dev/devServer.ts
|
|
3349
5552
|
import { createServer as createServer2 } from "node:http";
|
|
3350
|
-
import { createReadStream, existsSync as
|
|
3351
|
-
import { join as
|
|
3352
|
-
|
|
3353
|
-
// src/compile/collectLocal.ts
|
|
3354
|
-
import { readFileSync as readFileSync11 } from "node:fs";
|
|
3355
|
-
import { basename as basename3 } from "node:path";
|
|
3356
|
-
var collectLocalEntries = (root, manifest, assetBase) => {
|
|
3357
|
-
const entries = [];
|
|
3358
|
-
const warnings = [];
|
|
3359
|
-
const scan = scanLocal(root, { ignore: loadIgnore(root), tracked: new Set(Object.keys(manifest?.files ?? {})) });
|
|
3360
|
-
for (const file of scan.files) {
|
|
3361
|
-
const type = manifest?.files[file.path]?.type ?? classifyKind(basename3(file.path));
|
|
3362
|
-
if (type === "resource") {
|
|
3363
|
-
entries.push({ path: file.path, type, fileSrc: assetBase + encodeURI(file.path), absPath: file.absPath });
|
|
3364
|
-
} else if (type === "shader") {
|
|
3365
|
-
warnings.push(`${file.path}: shaders compile on the server — push the project once for shaders to work in dev mode.`);
|
|
3366
|
-
entries.push({ path: file.path, type, shaders: [] });
|
|
3367
|
-
} else {
|
|
3368
|
-
entries.push({ path: file.path, type, text: readFileSync11(file.absPath, "utf8") });
|
|
3369
|
-
}
|
|
3370
|
-
}
|
|
3371
|
-
return { entries, warnings };
|
|
3372
|
-
};
|
|
5553
|
+
import { createReadStream, existsSync as existsSync14, statSync as statSync3 } from "node:fs";
|
|
5554
|
+
import { join as join16 } from "node:path";
|
|
3373
5555
|
|
|
3374
5556
|
// src/dev/clientTemplates.ts
|
|
3375
5557
|
var DEV_SCREEN_TS = `const title = "LeCodes Dev"
|
|
@@ -3484,9 +5666,9 @@ var renderPrelude = (p) => DEV_PRELUDE_JS.split("__DEV_ORIGIN__").join(p.origin)
|
|
|
3484
5666
|
var renderDevScreen = (name) => DEV_SCREEN_TS.split("__DEV_NAME__").join(JSON.stringify(name));
|
|
3485
5667
|
|
|
3486
5668
|
// src/dev/wsServer.ts
|
|
3487
|
-
import { createHash as
|
|
5669
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
3488
5670
|
var WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
3489
|
-
var acceptKey = (key) =>
|
|
5671
|
+
var acceptKey = (key) => createHash4("sha1").update(key + WS_GUID).digest("base64");
|
|
3490
5672
|
var encodeTextFrame = (text) => {
|
|
3491
5673
|
const payload = Buffer.from(text, "utf8");
|
|
3492
5674
|
const len = payload.length;
|
|
@@ -3633,14 +5815,14 @@ var sendText = (res, status, type, body) => {
|
|
|
3633
5815
|
res.end(body);
|
|
3634
5816
|
};
|
|
3635
5817
|
var sendJson = (res, status, body) => sendText(res, status, "application/json", JSON.stringify(body));
|
|
3636
|
-
var readBody = (req) => new Promise((
|
|
5818
|
+
var readBody = (req) => new Promise((resolve5) => {
|
|
3637
5819
|
let body = "";
|
|
3638
5820
|
req.on("data", (c2) => {
|
|
3639
5821
|
if (body.length < 256 * 1024)
|
|
3640
5822
|
body += c2.toString("utf8");
|
|
3641
5823
|
});
|
|
3642
|
-
req.on("end", () =>
|
|
3643
|
-
req.on("error", () =>
|
|
5824
|
+
req.on("end", () => resolve5(body));
|
|
5825
|
+
req.on("error", () => resolve5(body));
|
|
3644
5826
|
});
|
|
3645
5827
|
var startDevServer = async (options) => {
|
|
3646
5828
|
const { root, manifest, name, origin, token, entryOverride } = options;
|
|
@@ -3653,7 +5835,15 @@ var startDevServer = async (options) => {
|
|
|
3653
5835
|
const compileOnce = async () => {
|
|
3654
5836
|
try {
|
|
3655
5837
|
const { entries, warnings } = collectLocalEntries(root, manifest, `${base}/assets`);
|
|
3656
|
-
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 });
|
|
3657
5847
|
const changed = bundleJs === null || stripVolatile(js) !== stripVolatile(bundleJs);
|
|
3658
5848
|
if (changed) {
|
|
3659
5849
|
bundleJs = js;
|
|
@@ -3725,8 +5915,8 @@ var startDevServer = async (options) => {
|
|
|
3725
5915
|
} catch {
|
|
3726
5916
|
return sendText(res, 400, "text/plain", "Bad path");
|
|
3727
5917
|
}
|
|
3728
|
-
const abs =
|
|
3729
|
-
if (!
|
|
5918
|
+
const abs = join16(root, key.slice(1));
|
|
5919
|
+
if (!existsSync14(abs) || !statSync3(abs).isFile())
|
|
3730
5920
|
return sendText(res, 404, "text/plain", "Not found");
|
|
3731
5921
|
res.writeHead(200, { "content-type": contentType(key), "cache-control": "no-store" });
|
|
3732
5922
|
createReadStream(abs).pipe(res);
|
|
@@ -3769,11 +5959,11 @@ var startDevServer = async (options) => {
|
|
|
3769
5959
|
if (conn)
|
|
3770
5960
|
clients.add(conn);
|
|
3771
5961
|
});
|
|
3772
|
-
await new Promise((
|
|
5962
|
+
await new Promise((resolve5, reject) => {
|
|
3773
5963
|
server.once("error", reject);
|
|
3774
5964
|
server.listen(options.port, options.bind ?? "0.0.0.0", () => {
|
|
3775
5965
|
server.removeListener("error", reject);
|
|
3776
|
-
|
|
5966
|
+
resolve5();
|
|
3777
5967
|
});
|
|
3778
5968
|
});
|
|
3779
5969
|
const address = server.address();
|
|
@@ -3809,24 +5999,24 @@ var pickLanAddress = () => {
|
|
|
3809
5999
|
return null;
|
|
3810
6000
|
};
|
|
3811
6001
|
var loadDevToken = (root) => {
|
|
3812
|
-
const path =
|
|
6002
|
+
const path = join17(root, LECODES_DIR, "dev.json");
|
|
3813
6003
|
try {
|
|
3814
|
-
const token2 = JSON.parse(
|
|
6004
|
+
const token2 = JSON.parse(readFileSync15(path, "utf8")).token;
|
|
3815
6005
|
if (token2 && /^[a-z0-9]{8,}$/.test(token2))
|
|
3816
6006
|
return token2;
|
|
3817
6007
|
} catch {}
|
|
3818
6008
|
const token = randomBytes2(6).toString("hex");
|
|
3819
|
-
|
|
3820
|
-
|
|
6009
|
+
mkdirSync11(join17(root, LECODES_DIR), { recursive: true });
|
|
6010
|
+
writeFileSync12(path, JSON.stringify({ token }, null, 2) + `
|
|
3821
6011
|
`);
|
|
3822
6012
|
return token;
|
|
3823
6013
|
};
|
|
3824
6014
|
var printQr = async (url) => {
|
|
3825
6015
|
try {
|
|
3826
6016
|
const qrcode = (await Promise.resolve().then(() => __toESM(require_main(), 1))).default;
|
|
3827
|
-
await new Promise((
|
|
6017
|
+
await new Promise((resolve5) => qrcode.generate(url, { small: true }, (out) => {
|
|
3828
6018
|
log(out);
|
|
3829
|
-
|
|
6019
|
+
resolve5();
|
|
3830
6020
|
}));
|
|
3831
6021
|
} catch {
|
|
3832
6022
|
note("(couldn't render a QR code — open the URL below on the device)");
|
|
@@ -3842,7 +6032,7 @@ var dev = async (args) => {
|
|
|
3842
6032
|
root = process.cwd();
|
|
3843
6033
|
manifest = null;
|
|
3844
6034
|
}
|
|
3845
|
-
const name = manifest?.name ??
|
|
6035
|
+
const name = manifest?.name ?? basename7(root);
|
|
3846
6036
|
if (typeof globalThis.Bun !== "undefined") {
|
|
3847
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).");
|
|
3848
6038
|
}
|
|
@@ -3929,15 +6119,15 @@ ${result.error}`);
|
|
|
3929
6119
|
await printQr(server.url);
|
|
3930
6120
|
log(`Scan the QR with the LeCodes app, or open: ${c.bold(server.url)}`);
|
|
3931
6121
|
note(`Older desktop builds (no websocket bridge): ${server.url}?t=poll`);
|
|
3932
|
-
if (
|
|
6122
|
+
if (existsSync15(join17(root, LECODES_DIR, "manifest.json")))
|
|
3933
6123
|
note(`Token stored in ${LECODES_DIR}/dev.json (delete it to rotate the URL).`);
|
|
3934
6124
|
log("");
|
|
3935
6125
|
log(`Watching ${root} — save a file to hot-reload. Ctrl+C to stop.`);
|
|
3936
6126
|
};
|
|
3937
6127
|
|
|
3938
6128
|
// src/serverDiff.ts
|
|
3939
|
-
import { readFileSync as
|
|
3940
|
-
import { basename as
|
|
6129
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
6130
|
+
import { basename as basename8 } from "node:path";
|
|
3941
6131
|
var isTextAsset = (t) => t === "text" || t === "shader" || t === "scene";
|
|
3942
6132
|
var computeServerDiff = (project, scan) => {
|
|
3943
6133
|
const pathById = buildPaths(project.assets);
|
|
@@ -3947,15 +6137,15 @@ var computeServerDiff = (project, scan) => {
|
|
|
3947
6137
|
continue;
|
|
3948
6138
|
serverByPath.set(pathById.get(asset.id), asset);
|
|
3949
6139
|
}
|
|
3950
|
-
const localByPath = new Map(scan.files.map((
|
|
6140
|
+
const localByPath = new Map(scan.files.map((f2) => [f2.path, f2]));
|
|
3951
6141
|
const entries = [];
|
|
3952
6142
|
const paths = new Set([...serverByPath.keys(), ...localByPath.keys()]);
|
|
3953
6143
|
for (const path of [...paths].sort()) {
|
|
3954
6144
|
const server = serverByPath.get(path);
|
|
3955
6145
|
const local = localByPath.get(path);
|
|
3956
6146
|
if (local && !server) {
|
|
3957
|
-
const text = isTextKind(classifyKind(
|
|
3958
|
-
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 });
|
|
3959
6149
|
continue;
|
|
3960
6150
|
}
|
|
3961
6151
|
if (server && !local) {
|
|
@@ -3966,12 +6156,12 @@ var computeServerDiff = (project, scan) => {
|
|
|
3966
6156
|
if (server && local) {
|
|
3967
6157
|
if (isTextAsset(server.type)) {
|
|
3968
6158
|
const serverText = server.text ?? "";
|
|
3969
|
-
const localText =
|
|
6159
|
+
const localText = readFileSync16(local.absPath, "utf8");
|
|
3970
6160
|
if (serverText !== localText)
|
|
3971
6161
|
entries.push({ path, status: "modified", binary: false, serverText, localText });
|
|
3972
6162
|
} else {
|
|
3973
6163
|
const serverSize = server.file?.size;
|
|
3974
|
-
const localSize = Buffer.byteLength(
|
|
6164
|
+
const localSize = Buffer.byteLength(readFileSync16(local.absPath));
|
|
3975
6165
|
if (serverSize !== undefined && serverSize !== localSize) {
|
|
3976
6166
|
entries.push({ path, status: "modified", binary: true });
|
|
3977
6167
|
}
|
|
@@ -4124,10 +6314,10 @@ var diff = async (args) => {
|
|
|
4124
6314
|
var statusLabel = (e) => e.status === "added" ? c.green(" added ") : e.status === "removed" ? c.red(" removed ") : c.yellow(" modified ");
|
|
4125
6315
|
|
|
4126
6316
|
// src/commands/init.ts
|
|
4127
|
-
import { existsSync as
|
|
4128
|
-
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";
|
|
4129
6319
|
var STARTER2 = `// Welcome to LeCodes — the whole SDK is available as globals, no imports needed.
|
|
4130
|
-
// Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes
|
|
6320
|
+
// Run it live on a device with \`lecodes dev\`, or bundle it with \`lecodes compile -o app.js\`.
|
|
4131
6321
|
|
|
4132
6322
|
let count = 0
|
|
4133
6323
|
|
|
@@ -4161,24 +6351,55 @@ const screen = UIScreen(
|
|
|
4161
6351
|
|
|
4162
6352
|
screen.open()
|
|
4163
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
|
+
`;
|
|
4164
6384
|
var init2 = async (args) => {
|
|
4165
6385
|
const dirArg = (args._[0] ?? ".").replace(/[/\\]+$/, "") || ".";
|
|
4166
|
-
const root =
|
|
4167
|
-
const name =
|
|
4168
|
-
|
|
6386
|
+
const root = resolve5(dirArg);
|
|
6387
|
+
const name = basename9(root);
|
|
6388
|
+
mkdirSync12(root, { recursive: true });
|
|
4169
6389
|
let created = 0;
|
|
4170
6390
|
const write = (rel, content) => {
|
|
4171
|
-
const abs =
|
|
4172
|
-
if (
|
|
6391
|
+
const abs = join18(root, rel);
|
|
6392
|
+
if (existsSync16(abs)) {
|
|
4173
6393
|
note(`kept ${rel}`);
|
|
4174
6394
|
return;
|
|
4175
6395
|
}
|
|
4176
|
-
|
|
4177
|
-
|
|
6396
|
+
mkdirSync12(dirname10(abs), { recursive: true });
|
|
6397
|
+
writeFileSync13(abs, content);
|
|
4178
6398
|
log(`${c.green("created")} ${rel}`);
|
|
4179
6399
|
created++;
|
|
4180
6400
|
};
|
|
4181
6401
|
write("main.ts", STARTER2);
|
|
6402
|
+
write("README.md", readmeTemplate(name));
|
|
4182
6403
|
write(".lecodesignore", DEFAULT_IGNORE_FILE);
|
|
4183
6404
|
const wroteTypes = materializeTypesLocal(root);
|
|
4184
6405
|
writeTsconfig(root);
|
|
@@ -4190,7 +6411,7 @@ var init2 = async (args) => {
|
|
|
4190
6411
|
success(created ? `Local project ${c.bold(name)} ready${dirArg === "." ? "" : ` in ${c.bold(dirArg)}/`}.` : `Project ${c.bold(name)} already initialized.`);
|
|
4191
6412
|
const cd = dirArg === "." ? "" : `cd ${dirArg} && `;
|
|
4192
6413
|
log(` ${c.bold(cd + "lecodes dev")} run it on a device with hot reload`);
|
|
4193
|
-
log(` ${c.bold(cd + "lecodes
|
|
6414
|
+
log(` ${c.bold(cd + "lecodes compile -o app.js")} bundle it for LeCodes Desktop`);
|
|
4194
6415
|
};
|
|
4195
6416
|
|
|
4196
6417
|
// src/commands/login.ts
|
|
@@ -4204,115 +6425,21 @@ var login = async (args) => {
|
|
|
4204
6425
|
success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`);
|
|
4205
6426
|
};
|
|
4206
6427
|
|
|
4207
|
-
// src/commands/preview.ts
|
|
4208
|
-
import { writeFileSync as writeFileSync11 } from "node:fs";
|
|
4209
|
-
import { basename as basename8, resolve as resolve4 } from "node:path";
|
|
4210
|
-
|
|
4211
|
-
// src/compile/collect.ts
|
|
4212
|
-
import { readFileSync as readFileSync14 } from "node:fs";
|
|
4213
|
-
import { basename as basename7 } from "node:path";
|
|
4214
|
-
var collectEntries = (root, manifest, project) => {
|
|
4215
|
-
const pathById = buildPaths(project.assets);
|
|
4216
|
-
const serverByPath = new Map;
|
|
4217
|
-
for (const asset of project.assets) {
|
|
4218
|
-
if (asset.type === "folder")
|
|
4219
|
-
continue;
|
|
4220
|
-
serverByPath.set(pathById.get(asset.id), { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] });
|
|
4221
|
-
}
|
|
4222
|
-
const entries = [];
|
|
4223
|
-
const warnings = [];
|
|
4224
|
-
for (const file of scanLocal(root).files) {
|
|
4225
|
-
const type = manifest.files[file.path]?.type ?? classifyKind(basename7(file.path));
|
|
4226
|
-
if (type === "resource") {
|
|
4227
|
-
const src = serverByPath.get(file.path)?.fileSrc;
|
|
4228
|
-
if (!src)
|
|
4229
|
-
warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`);
|
|
4230
|
-
entries.push({ path: file.path, type, fileSrc: src, absPath: file.absPath });
|
|
4231
|
-
} else if (type === "shader") {
|
|
4232
|
-
const shaders = serverByPath.get(file.path)?.shaders;
|
|
4233
|
-
if (!shaders?.length)
|
|
4234
|
-
warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
|
|
4235
|
-
entries.push({ path: file.path, type, shaders });
|
|
4236
|
-
} else {
|
|
4237
|
-
entries.push({ path: file.path, type, text: readFileSync14(file.absPath, "utf8") });
|
|
4238
|
-
}
|
|
4239
|
-
}
|
|
4240
|
-
return { entries, warnings };
|
|
4241
|
-
};
|
|
4242
|
-
|
|
4243
|
-
// src/commands/preview.ts
|
|
4244
|
-
var preview = async (args) => {
|
|
4245
|
-
let root;
|
|
4246
|
-
let manifest;
|
|
4247
|
-
try {
|
|
4248
|
-
root = findProjectRoot(process.cwd());
|
|
4249
|
-
manifest = readManifest(root);
|
|
4250
|
-
} catch {
|
|
4251
|
-
root = process.cwd();
|
|
4252
|
-
manifest = null;
|
|
4253
|
-
}
|
|
4254
|
-
let entries;
|
|
4255
|
-
let warnings;
|
|
4256
|
-
let name;
|
|
4257
|
-
let publicUrl;
|
|
4258
|
-
let localAssets = false;
|
|
4259
|
-
if (manifest) {
|
|
4260
|
-
const config = loadConfig();
|
|
4261
|
-
const apiUrl = requireApiUrl(config);
|
|
4262
|
-
const token = requireToken(config);
|
|
4263
|
-
note("Resolving project assets…");
|
|
4264
|
-
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
4265
|
-
({ entries, warnings } = collectEntries(root, manifest, project));
|
|
4266
|
-
name = manifest.name;
|
|
4267
|
-
publicUrl = flagStr(args, "public-url") ?? apiUrl;
|
|
4268
|
-
} else {
|
|
4269
|
-
const publicUrlFlag = flagStr(args, "public-url");
|
|
4270
|
-
localAssets = publicUrlFlag === undefined;
|
|
4271
|
-
publicUrl = publicUrlFlag ?? "";
|
|
4272
|
-
({ entries, warnings } = collectLocalEntries(root, null, publicUrl));
|
|
4273
|
-
name = basename8(root);
|
|
4274
|
-
}
|
|
4275
|
-
note("Compiling…");
|
|
4276
|
-
const js = await compileProject2({
|
|
4277
|
-
entries,
|
|
4278
|
-
name,
|
|
4279
|
-
publicUrl,
|
|
4280
|
-
entryOverride: flagStr(args, "entry"),
|
|
4281
|
-
header: !flagBool(args, "no-header"),
|
|
4282
|
-
localAssets
|
|
4283
|
-
});
|
|
4284
|
-
for (const w of warnings)
|
|
4285
|
-
warnErr(w);
|
|
4286
|
-
const out = flagStr(args, "o", "out");
|
|
4287
|
-
if (out && isNullSink(out)) {
|
|
4288
|
-
note(`Discarded output (${out})`);
|
|
4289
|
-
} else if (out) {
|
|
4290
|
-
writeFileSync11(resolve4(out), js);
|
|
4291
|
-
note(`Wrote ${out} (${js.length} bytes)`);
|
|
4292
|
-
} else {
|
|
4293
|
-
process.stdout.write(js);
|
|
4294
|
-
if (!js.endsWith(`
|
|
4295
|
-
`))
|
|
4296
|
-
process.stdout.write(`
|
|
4297
|
-
`);
|
|
4298
|
-
}
|
|
4299
|
-
};
|
|
4300
|
-
|
|
4301
6428
|
// src/commands/render.ts
|
|
4302
|
-
import { writeFileSync as
|
|
4303
|
-
import { resolve as
|
|
6429
|
+
import { writeFileSync as writeFileSync14 } from "node:fs";
|
|
6430
|
+
import { resolve as resolve6 } from "node:path";
|
|
4304
6431
|
var LOCAL_ASSET_SCHEME2 = "lecodesfile://";
|
|
4305
6432
|
var DEVICE_SAFE_AREA = {
|
|
4306
|
-
none: [0, 0, 0, 0],
|
|
4307
|
-
iphone: [
|
|
4308
|
-
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"] }
|
|
4309
6436
|
};
|
|
4310
6437
|
var resolveSafeArea = (args) => {
|
|
4311
6438
|
const explicit = flagStr(args, "safe-area");
|
|
4312
6439
|
if (explicit) {
|
|
4313
6440
|
const n = explicit.split(",").map((s) => Number(s.trim()));
|
|
4314
6441
|
if (n.length === 4 && n.every((v) => Number.isFinite(v)))
|
|
4315
|
-
return n;
|
|
6442
|
+
return { safeArea: n, safeAreaBars: [] };
|
|
4316
6443
|
throw new CliError(`--safe-area expects "top,right,bottom,left" (4 numbers), got "${explicit}".`);
|
|
4317
6444
|
}
|
|
4318
6445
|
const device = flagStr(args, "device");
|
|
@@ -4320,7 +6447,7 @@ var resolveSafeArea = (args) => {
|
|
|
4320
6447
|
const preset = DEVICE_SAFE_AREA[device];
|
|
4321
6448
|
if (!preset)
|
|
4322
6449
|
throw new CliError(`Unknown --device "${device}". Options: ${Object.keys(DEVICE_SAFE_AREA).join(", ")}.`);
|
|
4323
|
-
return preset;
|
|
6450
|
+
return { safeArea: preset.insets, safeAreaBars: preset.bars };
|
|
4324
6451
|
}
|
|
4325
6452
|
return;
|
|
4326
6453
|
};
|
|
@@ -4343,6 +6470,13 @@ var render = async (args) => {
|
|
|
4343
6470
|
const useLocalAssets = !publicUrlFlag && !flagBool(args, "remote-assets");
|
|
4344
6471
|
const compileEntries = useLocalAssets ? entries.map((e) => e.type === "resource" && e.absPath ? { ...e, fileSrc: encodeURIComponent(e.absPath) } : e) : entries;
|
|
4345
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 });
|
|
4346
6480
|
note("Compiling…");
|
|
4347
6481
|
const js = await compileProject2({
|
|
4348
6482
|
entries: compileEntries,
|
|
@@ -4375,7 +6509,7 @@ var render = async (args) => {
|
|
|
4375
6509
|
localAssets: useLocalAssets,
|
|
4376
6510
|
timeMs: timeStr !== undefined ? Number(timeStr) : undefined,
|
|
4377
6511
|
afterAnimations: !flagBool(args, "no-after-animations"),
|
|
4378
|
-
|
|
6512
|
+
...resolveSafeArea(args),
|
|
4379
6513
|
onConsole
|
|
4380
6514
|
};
|
|
4381
6515
|
const done = (flush) => flush(() => {
|
|
@@ -4396,7 +6530,7 @@ var render = async (args) => {
|
|
|
4396
6530
|
if (isNullSink(pngFlag)) {
|
|
4397
6531
|
note(`Discarded PNG (${pngFlag})`);
|
|
4398
6532
|
} else {
|
|
4399
|
-
|
|
6533
|
+
writeFileSync14(resolve6(pngFlag), png);
|
|
4400
6534
|
note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
|
|
4401
6535
|
}
|
|
4402
6536
|
return done((cb) => cb());
|
|
@@ -4411,7 +6545,7 @@ var render = async (args) => {
|
|
|
4411
6545
|
return done((cb) => cb());
|
|
4412
6546
|
}
|
|
4413
6547
|
if (out) {
|
|
4414
|
-
|
|
6548
|
+
writeFileSync14(resolve6(out), result.text);
|
|
4415
6549
|
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
4416
6550
|
return done((cb) => cb());
|
|
4417
6551
|
}
|
|
@@ -4422,8 +6556,8 @@ var render = async (args) => {
|
|
|
4422
6556
|
};
|
|
4423
6557
|
|
|
4424
6558
|
// src/commands/pull.ts
|
|
4425
|
-
import { existsSync as
|
|
4426
|
-
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";
|
|
4427
6561
|
var pull = async (args) => {
|
|
4428
6562
|
const root = findProjectRoot(process.cwd());
|
|
4429
6563
|
const manifest = readManifest(root);
|
|
@@ -4443,9 +6577,9 @@ var pull = async (args) => {
|
|
|
4443
6577
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
|
|
4444
6578
|
for (const path of Object.keys(manifest.files)) {
|
|
4445
6579
|
if (!remotePaths.has(path)) {
|
|
4446
|
-
const abs =
|
|
4447
|
-
if (
|
|
4448
|
-
|
|
6580
|
+
const abs = join19(root, ...path.replace(/^\//, "").split("/"));
|
|
6581
|
+
if (existsSync17(abs))
|
|
6582
|
+
rmSync2(abs);
|
|
4449
6583
|
}
|
|
4450
6584
|
}
|
|
4451
6585
|
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
@@ -4482,18 +6616,18 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
4482
6616
|
const files = { ...manifest.files };
|
|
4483
6617
|
for (const asset of targets) {
|
|
4484
6618
|
const key = pathById.get(asset.id);
|
|
4485
|
-
const abs =
|
|
4486
|
-
|
|
6619
|
+
const abs = join19(root, ...key.replace(/^\//, "").split("/"));
|
|
6620
|
+
mkdirSync13(dirname11(abs), { recursive: true });
|
|
4487
6621
|
let sha;
|
|
4488
6622
|
if (asset.type === "file") {
|
|
4489
6623
|
if (!asset.file?.src)
|
|
4490
6624
|
continue;
|
|
4491
6625
|
const bytes = await downloadBlob(apiUrl, asset.file.src);
|
|
4492
|
-
|
|
6626
|
+
writeFileSync15(abs, bytes);
|
|
4493
6627
|
sha = sha256(bytes);
|
|
4494
6628
|
} else {
|
|
4495
6629
|
const text = asset.text ?? "";
|
|
4496
|
-
|
|
6630
|
+
writeFileSync15(abs, text);
|
|
4497
6631
|
sha = sha256(text);
|
|
4498
6632
|
}
|
|
4499
6633
|
files[key] = { id: asset.id, type: kindOf2(asset.type), sha };
|
|
@@ -4505,8 +6639,8 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
4505
6639
|
};
|
|
4506
6640
|
|
|
4507
6641
|
// src/commands/push.ts
|
|
4508
|
-
import { basename as
|
|
4509
|
-
import { readFileSync as
|
|
6642
|
+
import { basename as basename10 } from "node:path";
|
|
6643
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
4510
6644
|
var push = async (args) => {
|
|
4511
6645
|
const root = findProjectRoot(process.cwd());
|
|
4512
6646
|
const manifest = readManifest(root);
|
|
@@ -4560,14 +6694,14 @@ var push = async (args) => {
|
|
|
4560
6694
|
let uploaded = 0;
|
|
4561
6695
|
for (const file of scan.files) {
|
|
4562
6696
|
const source = sourceFor(file.path);
|
|
4563
|
-
const kind = source?.type ?? classifyKind(
|
|
6697
|
+
const kind = source?.type ?? classifyKind(basename10(file.path));
|
|
4564
6698
|
const entry = { path: file.path, type: kind };
|
|
4565
6699
|
if (source)
|
|
4566
6700
|
entry.assetId = source.id;
|
|
4567
6701
|
if (isTextKind(kind)) {
|
|
4568
|
-
entry.text =
|
|
6702
|
+
entry.text = readFileSync17(file.absPath, "utf8");
|
|
4569
6703
|
} else if (!source || source.sha !== file.sha) {
|
|
4570
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid,
|
|
6704
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename10(file.path), readFileSync17(file.absPath));
|
|
4571
6705
|
entry.blobFileId = blob.id;
|
|
4572
6706
|
uploaded++;
|
|
4573
6707
|
}
|
|
@@ -4583,7 +6717,7 @@ var push = async (args) => {
|
|
|
4583
6717
|
reportPublish(res);
|
|
4584
6718
|
return;
|
|
4585
6719
|
}
|
|
4586
|
-
const shaByPath = new Map(scan.files.map((
|
|
6720
|
+
const shaByPath = new Map(scan.files.map((f2) => [f2.path, f2.sha]));
|
|
4587
6721
|
const newFiles = {};
|
|
4588
6722
|
for (const node of res.nodes) {
|
|
4589
6723
|
newFiles[node.path] = { id: node.id, type: node.type, sha: shaByPath.get(node.path) ?? "" };
|
|
@@ -4620,22 +6754,22 @@ var reportPublish = (res) => {
|
|
|
4620
6754
|
};
|
|
4621
6755
|
|
|
4622
6756
|
// src/commands/scene.ts
|
|
4623
|
-
import { existsSync as
|
|
4624
|
-
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";
|
|
4625
6759
|
|
|
4626
6760
|
// src/compile/sceneCompile.ts
|
|
4627
|
-
import { existsSync as
|
|
4628
|
-
import { join as
|
|
6761
|
+
import { existsSync as existsSync18, readFileSync as readFileSync18 } from "node:fs";
|
|
6762
|
+
import { join as join20 } from "node:path";
|
|
4629
6763
|
var TEXT_EXT = /\.(ts|js|json|svg|txt|md)$/i;
|
|
4630
6764
|
var rel = (key) => key.replace(/^\//, "");
|
|
4631
6765
|
var resolveHarness = (loadSceneHarness2) => {
|
|
4632
|
-
const inRepo =
|
|
6766
|
+
const inRepo = join20(distRoot, "..", "projects", "src", "scene-editor");
|
|
4633
6767
|
try {
|
|
4634
|
-
return loadSceneHarness2(
|
|
6768
|
+
return loadSceneHarness2(existsSync18(inRepo) ? inRepo : undefined);
|
|
4635
6769
|
} catch {
|
|
4636
|
-
const vendored =
|
|
4637
|
-
if (
|
|
4638
|
-
return JSON.parse(
|
|
6770
|
+
const vendored = join20(distRoot, "runtime", "scene-harness.json");
|
|
6771
|
+
if (existsSync18(vendored))
|
|
6772
|
+
return JSON.parse(readFileSync18(vendored, "utf8"));
|
|
4639
6773
|
throw new CliError("scene-editor harness sources not found — reinstall the CLI or set LECODES_SCENE_HARNESS_DIR.");
|
|
4640
6774
|
}
|
|
4641
6775
|
};
|
|
@@ -4647,7 +6781,7 @@ var compileSceneEditor = async (opts) => {
|
|
|
4647
6781
|
for (const file of scan.files) {
|
|
4648
6782
|
const path = "./" + rel(file.path);
|
|
4649
6783
|
if (TEXT_EXT.test(path)) {
|
|
4650
|
-
entries.push({ path, type: "text", text:
|
|
6784
|
+
entries.push({ path, type: "text", text: readFileSync18(file.absPath, "utf8") });
|
|
4651
6785
|
} else {
|
|
4652
6786
|
entries.push({ path, type: "resource", fileSrc: "/project/" + rel(file.path) });
|
|
4653
6787
|
}
|
|
@@ -4687,24 +6821,24 @@ var getContext2 = (args) => {
|
|
|
4687
6821
|
manifest = null;
|
|
4688
6822
|
const pathArg = args._[0];
|
|
4689
6823
|
if (pathArg) {
|
|
4690
|
-
const abs =
|
|
4691
|
-
if (
|
|
6824
|
+
const abs = resolve7(process.cwd(), pathArg);
|
|
6825
|
+
if (existsSync19(abs) && statSync4(abs).isDirectory())
|
|
4692
6826
|
root = abs;
|
|
4693
|
-
else if (abs.endsWith(".scene.ts") &&
|
|
4694
|
-
root =
|
|
6827
|
+
else if (abs.endsWith(".scene.ts") && existsSync19(abs))
|
|
6828
|
+
root = dirname12(abs);
|
|
4695
6829
|
else
|
|
4696
6830
|
throw new CliError(`No such folder or .scene.ts: ${pathArg}`);
|
|
4697
6831
|
}
|
|
4698
6832
|
}
|
|
4699
|
-
return { root, manifest, name: manifest?.name ??
|
|
6833
|
+
return { root, manifest, name: manifest?.name ?? basename11(root) };
|
|
4700
6834
|
};
|
|
4701
6835
|
var writeMcpConfig2 = (root, port) => {
|
|
4702
|
-
const path =
|
|
6836
|
+
const path = join21(root, ".mcp.json");
|
|
4703
6837
|
const entry = { type: "http", url: `http://localhost:${port}/mcp` };
|
|
4704
6838
|
let config = {};
|
|
4705
|
-
if (
|
|
6839
|
+
if (existsSync19(path)) {
|
|
4706
6840
|
try {
|
|
4707
|
-
config = JSON.parse(
|
|
6841
|
+
config = JSON.parse(readFileSync19(path, "utf8"));
|
|
4708
6842
|
} catch {
|
|
4709
6843
|
warnErr(".mcp.json exists but isn't valid JSON — left it untouched. Add the lecodes-scene server by hand.");
|
|
4710
6844
|
return;
|
|
@@ -4717,17 +6851,17 @@ var writeMcpConfig2 = (root, port) => {
|
|
|
4717
6851
|
return;
|
|
4718
6852
|
}
|
|
4719
6853
|
config.mcpServers["lecodes-scene"] = entry;
|
|
4720
|
-
|
|
6854
|
+
writeFileSync16(path, JSON.stringify(config, null, 2) + `
|
|
4721
6855
|
`);
|
|
4722
6856
|
note(`${existing ? "updated" : "created"} .mcp.json (lecodes-scene MCP → ${entry.url})`);
|
|
4723
6857
|
};
|
|
4724
6858
|
var countScenes = (dir) => {
|
|
4725
6859
|
let n = 0;
|
|
4726
|
-
for (const name of
|
|
6860
|
+
for (const name of readdirSync8(dir)) {
|
|
4727
6861
|
if (name === "node_modules" || name.startsWith("."))
|
|
4728
6862
|
continue;
|
|
4729
|
-
const full =
|
|
4730
|
-
if (
|
|
6863
|
+
const full = join21(dir, name);
|
|
6864
|
+
if (statSync4(full).isDirectory())
|
|
4731
6865
|
n += countScenes(full);
|
|
4732
6866
|
else if (name.endsWith(".scene.ts"))
|
|
4733
6867
|
n++;
|
|
@@ -4779,12 +6913,12 @@ var status = async (_args) => {
|
|
|
4779
6913
|
info(c.dim("Working tree clean — nothing to push.") + ignoredNote);
|
|
4780
6914
|
return;
|
|
4781
6915
|
}
|
|
4782
|
-
for (const
|
|
4783
|
-
if (!diff2.moves.has(
|
|
4784
|
-
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}`));
|
|
4785
6919
|
}
|
|
4786
|
-
for (const
|
|
4787
|
-
log(c.yellow(` modified ${
|
|
6920
|
+
for (const f2 of diff2.modified)
|
|
6921
|
+
log(c.yellow(` modified ${f2.path}`));
|
|
4788
6922
|
for (const [to, from] of diff2.moves)
|
|
4789
6923
|
log(c.cyan(` moved ${from} → ${to}`));
|
|
4790
6924
|
const movedFrom = new Set(diff2.moves.values());
|
|
@@ -4797,21 +6931,22 @@ var status = async (_args) => {
|
|
|
4797
6931
|
};
|
|
4798
6932
|
|
|
4799
6933
|
// src/index.ts
|
|
4800
|
-
import { existsSync as
|
|
4801
|
-
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";
|
|
4802
6937
|
|
|
4803
6938
|
// src/commands/install.ts
|
|
4804
|
-
import { execFileSync } from "node:child_process";
|
|
4805
|
-
import { appendFileSync as appendFileSync2, chmodSync, cpSync, existsSync as
|
|
4806
|
-
import { homedir as
|
|
4807
|
-
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";
|
|
4808
6943
|
var isWin = process.platform === "win32";
|
|
4809
|
-
var HOME =
|
|
6944
|
+
var HOME = homedir3();
|
|
4810
6945
|
var safeRealpath = (p) => {
|
|
4811
6946
|
try {
|
|
4812
6947
|
return realpathSync(p);
|
|
4813
6948
|
} catch {
|
|
4814
|
-
return
|
|
6949
|
+
return resolve8(p);
|
|
4815
6950
|
}
|
|
4816
6951
|
};
|
|
4817
6952
|
var onPath = (dir) => {
|
|
@@ -4827,25 +6962,25 @@ var onPath = (dir) => {
|
|
|
4827
6962
|
});
|
|
4828
6963
|
};
|
|
4829
6964
|
var install = async (args) => {
|
|
4830
|
-
const bundle =
|
|
4831
|
-
const exeName =
|
|
6965
|
+
const bundle = dirname13(process.execPath);
|
|
6966
|
+
const exeName = basename12(process.execPath);
|
|
4832
6967
|
const chiselName = isWin ? "chisel.exe" : "chisel";
|
|
4833
|
-
if (!
|
|
6968
|
+
if (!existsSync20(join22(bundle, "runtime")) || !existsSync20(join22(bundle, chiselName))) {
|
|
4834
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.");
|
|
4835
6970
|
}
|
|
4836
|
-
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((
|
|
6971
|
+
const companions = [exeName, chiselName, "runtime", "README.txt"].filter((f2) => existsSync20(join22(bundle, f2)));
|
|
4837
6972
|
if (isWin)
|
|
4838
6973
|
return installWindows(args, bundle, companions);
|
|
4839
6974
|
return installUnix(args, bundle, companions);
|
|
4840
6975
|
};
|
|
4841
6976
|
var copyBundle = (bundle, target, companions) => {
|
|
4842
|
-
|
|
6977
|
+
mkdirSync14(target, { recursive: true });
|
|
4843
6978
|
if (safeRealpath(target) === safeRealpath(bundle))
|
|
4844
6979
|
return;
|
|
4845
|
-
for (const
|
|
4846
|
-
const dest =
|
|
4847
|
-
|
|
4848
|
-
|
|
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 });
|
|
4849
6984
|
}
|
|
4850
6985
|
};
|
|
4851
6986
|
var permError = (e, dir, system) => {
|
|
@@ -4857,41 +6992,41 @@ var permError = (e, dir, system) => {
|
|
|
4857
6992
|
};
|
|
4858
6993
|
var installUnix = (args, bundle, companions) => {
|
|
4859
6994
|
const system = flagBool(args, "system");
|
|
4860
|
-
const libDir =
|
|
4861
|
-
const binDir =
|
|
4862
|
-
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");
|
|
4863
6998
|
try {
|
|
4864
6999
|
copyBundle(bundle, libDir, companions);
|
|
4865
|
-
chmodSync(
|
|
4866
|
-
chmodSync(
|
|
7000
|
+
chmodSync(join22(libDir, "lecodes"), 493);
|
|
7001
|
+
chmodSync(join22(libDir, "chisel"), 493);
|
|
4867
7002
|
} catch (e) {
|
|
4868
7003
|
throw permError(e, libDir, system);
|
|
4869
7004
|
}
|
|
4870
7005
|
try {
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
symlinkSync(
|
|
7006
|
+
mkdirSync14(binDir, { recursive: true });
|
|
7007
|
+
rmSync3(link, { force: true });
|
|
7008
|
+
symlinkSync(join22(libDir, "lecodes"), link);
|
|
4874
7009
|
} catch (e) {
|
|
4875
7010
|
throw permError(e, binDir, system);
|
|
4876
7011
|
}
|
|
4877
7012
|
success(`Installed to ${c.bold(libDir)}`);
|
|
4878
|
-
log(` ${c.dim(link + " → " +
|
|
7013
|
+
log(` ${c.dim(link + " → " + join22(libDir, "lecodes"))}`);
|
|
4879
7014
|
finishPathUnix(args, binDir);
|
|
4880
7015
|
};
|
|
4881
7016
|
var shellRc = () => {
|
|
4882
|
-
const shell =
|
|
7017
|
+
const shell = basename12(process.env.SHELL ?? "");
|
|
4883
7018
|
if (shell === "zsh")
|
|
4884
|
-
return { file:
|
|
7019
|
+
return { file: join22(HOME, ".zshrc"), name: "~/.zshrc" };
|
|
4885
7020
|
if (shell === "bash")
|
|
4886
|
-
return { file:
|
|
4887
|
-
return { file:
|
|
7021
|
+
return { file: join22(HOME, ".bashrc"), name: "~/.bashrc" };
|
|
7022
|
+
return { file: join22(HOME, ".profile"), name: "~/.profile" };
|
|
4888
7023
|
};
|
|
4889
7024
|
var PATH_MARKER = "# Added by `lecodes install`";
|
|
4890
7025
|
var appendPathToShellRc = (binDir) => {
|
|
4891
7026
|
const { file, name } = shellRc();
|
|
4892
7027
|
const line = `export PATH="${binDir}:$PATH"`;
|
|
4893
7028
|
try {
|
|
4894
|
-
const existing =
|
|
7029
|
+
const existing = existsSync20(file) ? readFileSync20(file, "utf8") : "";
|
|
4895
7030
|
if (existing.includes(line))
|
|
4896
7031
|
return name;
|
|
4897
7032
|
appendFileSync2(file, `${existing && !existing.endsWith(`
|
|
@@ -4929,7 +7064,7 @@ var finishPathUnix = (args, binDir) => {
|
|
|
4929
7064
|
log(` • or install system-wide (no PATH edit): ${c.bold("sudo lecodes install --system")}`);
|
|
4930
7065
|
};
|
|
4931
7066
|
var installWindows = (args, bundle, companions) => {
|
|
4932
|
-
const target =
|
|
7067
|
+
const target = resolve8(flagStr(args, "dir") ?? join22(process.env.LOCALAPPDATA ?? join22(HOME, "AppData", "Local"), "Programs", "lecodes-cli"));
|
|
4933
7068
|
try {
|
|
4934
7069
|
copyBundle(bundle, target, companions);
|
|
4935
7070
|
} catch (e) {
|
|
@@ -4957,7 +7092,7 @@ var installWindows = (args, bundle, companions) => {
|
|
|
4957
7092
|
var addToUserPathWindows = (dir) => {
|
|
4958
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') }";
|
|
4959
7094
|
try {
|
|
4960
|
-
|
|
7095
|
+
execFileSync2("powershell", ["-NoProfile", "-NonInteractive", "-Command", script], { stdio: "ignore", env: { ...process.env, LECODES_INSTALL_DIR: dir } });
|
|
4961
7096
|
return true;
|
|
4962
7097
|
} catch {
|
|
4963
7098
|
return false;
|
|
@@ -4966,9 +7101,9 @@ var addToUserPathWindows = (dir) => {
|
|
|
4966
7101
|
|
|
4967
7102
|
// src/index.ts
|
|
4968
7103
|
var STANDALONE = (() => {
|
|
4969
|
-
const dir =
|
|
7104
|
+
const dir = dirname14(process.execPath);
|
|
4970
7105
|
const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
|
|
4971
|
-
return
|
|
7106
|
+
return existsSync21(join23(dir, "runtime")) && existsSync21(join23(dir, chisel));
|
|
4972
7107
|
})();
|
|
4973
7108
|
var INSTALLED = STANDALONE && (() => {
|
|
4974
7109
|
const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
|
|
@@ -4983,8 +7118,8 @@ var INSTALLED = STANDALONE && (() => {
|
|
|
4983
7118
|
if (!d)
|
|
4984
7119
|
return false;
|
|
4985
7120
|
try {
|
|
4986
|
-
const cand =
|
|
4987
|
-
return
|
|
7121
|
+
const cand = join23(d, exe);
|
|
7122
|
+
return existsSync21(cand) && realpathSync2(cand) === self;
|
|
4988
7123
|
} catch {
|
|
4989
7124
|
return false;
|
|
4990
7125
|
}
|
|
@@ -5004,7 +7139,7 @@ ${c.bold("Usage:")} lecodes <command> [options]
|
|
|
5004
7139
|
${c.bold("Commands:")}
|
|
5005
7140
|
init [dir] Scaffold a new LOCAL project (no login/server): a starter
|
|
5006
7141
|
main.ts, IDE types + tsconfig, and a .lecodesignore.
|
|
5007
|
-
Run it with 'lecodes dev' or 'lecodes
|
|
7142
|
+
Run it with 'lecodes dev' or 'lecodes compile'.${INSTALL_HELP}
|
|
5008
7143
|
login Authorize in the browser and store an access token
|
|
5009
7144
|
--token <pat> store a token from the web UI instead
|
|
5010
7145
|
--api <url> API origin (default https://le.codes)
|
|
@@ -5034,10 +7169,13 @@ ${c.bold("Commands:")}
|
|
|
5034
7169
|
--host <ip> LAN address to advertise (default: auto-detect)
|
|
5035
7170
|
--entry <path> override the entrypoint
|
|
5036
7171
|
--no-qr don't render the QR code
|
|
5037
|
-
|
|
7172
|
+
compile Compile the local project to JS (prints to stdout)
|
|
7173
|
+
(was 'lecodes preview' — the old name still works)
|
|
5038
7174
|
-o, --out <file> write to a file instead of stdout
|
|
5039
7175
|
--entry <path> override the entrypoint
|
|
5040
7176
|
--no-header omit the metadata header
|
|
7177
|
+
--no-minify keep the bundle readable (no compress/mangle)
|
|
7178
|
+
--no-sourcemap don't generate/inline the source map
|
|
5041
7179
|
--public-url <url> asset URL prefix (default: the API origin)
|
|
5042
7180
|
render Render the local UI project to a semantic JSON description
|
|
5043
7181
|
(needs the optional 'lecodes-renderer' package; UI projects only)
|
|
@@ -5068,12 +7206,70 @@ ${c.bold("Commands:")}
|
|
|
5068
7206
|
--port <n> editor-server port (default 4488)
|
|
5069
7207
|
--no-open don't open the browser
|
|
5070
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
|
|
5071
7233
|
|
|
5072
7234
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
5073
|
-
var commands = { init: init2, login, create, clone, status, diff, pull, push, preview, 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 };
|
|
5074
7236
|
if (STANDALONE)
|
|
5075
7237
|
commands.install = install;
|
|
5076
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
|
+
};
|
|
5077
7273
|
var main = async () => {
|
|
5078
7274
|
const argv = process.argv.slice(2);
|
|
5079
7275
|
const command = argv[0];
|
|
@@ -5082,7 +7278,7 @@ var main = async () => {
|
|
|
5082
7278
|
return;
|
|
5083
7279
|
}
|
|
5084
7280
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
5085
|
-
|
|
7281
|
+
printVersion();
|
|
5086
7282
|
return;
|
|
5087
7283
|
}
|
|
5088
7284
|
const run = commands[command];
|