lecodes-cli 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/index.js +510 -46
- package/package.json +18 -2
- package/runtime/gl-matrix/common.js +54 -0
- package/runtime/gl-matrix/header.js +12 -0
- package/runtime/gl-matrix/index.js +11 -0
- package/runtime/gl-matrix/mat2.js +425 -0
- package/runtime/gl-matrix/mat2d.js +476 -0
- package/runtime/gl-matrix/mat3.js +765 -0
- package/runtime/gl-matrix/mat4.js +1947 -0
- package/runtime/gl-matrix/quat.js +737 -0
- package/runtime/gl-matrix/quat2.js +822 -0
- package/runtime/gl-matrix/vec2.js +620 -0
- package/runtime/gl-matrix/vec3.js +791 -0
- package/runtime/gl-matrix/vec4.js +652 -0
- package/runtime/worker/animate.js +304 -0
- package/runtime/worker/chunk-4ZN32GYS.js +34 -0
- package/runtime/worker/chunk-G5ZA3LKB.js +52 -0
- package/runtime/worker/chunk-MI3UXE5H.js +1395 -0
- package/runtime/worker/creator.js +930 -0
- package/runtime/worker/globals.js +208 -0
- package/runtime/worker/ui.js +763 -0
- package/runtime/worker/utils.js +16 -0
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
2
4
|
|
|
3
5
|
// src/commands/clone.ts
|
|
4
6
|
import { existsSync as existsSync2, mkdirSync as mkdirSync5, readdirSync } from "node:fs";
|
|
@@ -21,6 +23,25 @@ var log = (msg = "") => process.stdout.write(msg + `
|
|
|
21
23
|
`);
|
|
22
24
|
var info = (msg) => log(msg);
|
|
23
25
|
var success = (msg) => log(c.green("✓ ") + msg);
|
|
26
|
+
var logErr = (msg = "") => process.stderr.write(msg + `
|
|
27
|
+
`);
|
|
28
|
+
var note = (msg) => logErr(c.dim(msg));
|
|
29
|
+
var warnErr = (msg) => logErr(c.yellow("! ") + msg);
|
|
30
|
+
var ellipsize = (s, max) => s.length <= max ? s : "…" + s.slice(s.length - max + 1);
|
|
31
|
+
var progress = (done, total, label = "") => {
|
|
32
|
+
if (!process.stdout.isTTY)
|
|
33
|
+
return;
|
|
34
|
+
const pct = total > 0 ? Math.round(done / total * 100) : 100;
|
|
35
|
+
process.stdout.clearLine(0);
|
|
36
|
+
process.stdout.cursorTo(0);
|
|
37
|
+
process.stdout.write(` ${c.cyan(`${pct}`.padStart(3) + "%")} ${c.dim(`(${done}/${total})`)} ${c.dim(ellipsize(label, 48))}`);
|
|
38
|
+
};
|
|
39
|
+
var endProgress = () => {
|
|
40
|
+
if (process.stdout.isTTY)
|
|
41
|
+
process.stdout.write(`
|
|
42
|
+
`);
|
|
43
|
+
};
|
|
44
|
+
|
|
24
45
|
class CliError extends Error {
|
|
25
46
|
}
|
|
26
47
|
var sha256 = (data) => createHash("sha256").update(data).digest("hex");
|
|
@@ -38,16 +59,6 @@ var prompt = (question, fallback) => new Promise((resolve) => {
|
|
|
38
59
|
resolve(answer.trim() || fallback || "");
|
|
39
60
|
});
|
|
40
61
|
});
|
|
41
|
-
var promptHidden = (question) => new Promise((resolve) => {
|
|
42
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
|
43
|
-
rl.question(`${question}: `, (answer) => {
|
|
44
|
-
rl.close();
|
|
45
|
-
process.stdout.write(`
|
|
46
|
-
`);
|
|
47
|
-
resolve(answer.trim());
|
|
48
|
-
});
|
|
49
|
-
rl._writeToOutput = () => {};
|
|
50
|
-
});
|
|
51
62
|
var parseArgs = (argv) => {
|
|
52
63
|
const out = { _: [], flags: {} };
|
|
53
64
|
for (let i = 0;i < argv.length; i++) {
|
|
@@ -133,8 +144,6 @@ var apiRequest = async (apiUrl, path, opts = {}) => {
|
|
|
133
144
|
throw new CliError(errorMessage(parsed, resp.status));
|
|
134
145
|
return parsed;
|
|
135
146
|
};
|
|
136
|
-
var login = (apiUrl, login2, password) => apiRequest(apiUrl, "/account/login", { body: { login: login2, password } });
|
|
137
|
-
var createAccessToken = (apiUrl, jwt, name) => apiRequest(apiUrl, "/account/tokens", { token: jwt, body: { name } });
|
|
138
147
|
var getAccount = (apiUrl, token) => apiRequest(apiUrl, "/account", { token });
|
|
139
148
|
var getProject = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}`, { token });
|
|
140
149
|
var getCommits = (apiUrl, token, uuid) => apiRequest(apiUrl, `/projects/${uuid}/commits`, { token });
|
|
@@ -226,9 +235,11 @@ var buildPaths = (assets) => {
|
|
|
226
235
|
}
|
|
227
236
|
return pathById;
|
|
228
237
|
};
|
|
229
|
-
var materializeAssets = async (root, project, apiUrl, token) => {
|
|
238
|
+
var materializeAssets = async (root, project, apiUrl, token, onProgress) => {
|
|
230
239
|
const pathById = buildPaths(project.assets);
|
|
231
240
|
const files = {};
|
|
241
|
+
const total = project.assets.filter((a) => a.type !== "folder").length;
|
|
242
|
+
let done = 0;
|
|
232
243
|
for (const asset of project.assets) {
|
|
233
244
|
const key = pathById.get(asset.id);
|
|
234
245
|
const abs = absOf(root, key);
|
|
@@ -250,6 +261,7 @@ var materializeAssets = async (root, project, apiUrl, token) => {
|
|
|
250
261
|
sha = sha256(text);
|
|
251
262
|
}
|
|
252
263
|
files[key] = { id: asset.id, type: kindOf(asset.type), sha };
|
|
264
|
+
onProgress?.(++done, total, key);
|
|
253
265
|
}
|
|
254
266
|
return files;
|
|
255
267
|
};
|
|
@@ -322,8 +334,9 @@ var parseUuid = (input) => {
|
|
|
322
334
|
throw new CliError("Usage: lecodes clone <project-uuid|url> [directory]");
|
|
323
335
|
if (!input.includes("/"))
|
|
324
336
|
return input;
|
|
325
|
-
const
|
|
326
|
-
|
|
337
|
+
const clean = input.split(/[?#]/)[0].replace(/\/+$/, "");
|
|
338
|
+
const match = clean.match(/\/(?:projects|view)\/([^/]+)/);
|
|
339
|
+
return match ? match[1] : clean.slice(clean.lastIndexOf("/") + 1);
|
|
327
340
|
};
|
|
328
341
|
var slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "project";
|
|
329
342
|
var clone = async (args) => {
|
|
@@ -331,7 +344,6 @@ var clone = async (args) => {
|
|
|
331
344
|
const apiUrl = requireApiUrl(config);
|
|
332
345
|
const token = requireToken(config);
|
|
333
346
|
const uuid = parseUuid(args._[0]);
|
|
334
|
-
info(c.dim(`Fetching project ${uuid}…`));
|
|
335
347
|
const project = await getProject(apiUrl, token, uuid);
|
|
336
348
|
const dir = args._[1] ?? slug(project.name);
|
|
337
349
|
const root = resolve(dir);
|
|
@@ -339,8 +351,9 @@ var clone = async (args) => {
|
|
|
339
351
|
throw new CliError(`Directory "${dir}" already exists and is not empty.`);
|
|
340
352
|
}
|
|
341
353
|
mkdirSync5(root, { recursive: true });
|
|
342
|
-
info(c.dim(`
|
|
343
|
-
const files = await materializeAssets(root, project, apiUrl, token);
|
|
354
|
+
info(c.dim(`Fetching project ${c.bold(project.name)}…`));
|
|
355
|
+
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
356
|
+
endProgress();
|
|
344
357
|
if (!flagBool(args, "no-types")) {
|
|
345
358
|
info(c.dim("Fetching type definitions…"));
|
|
346
359
|
await materializeTypes(root, apiUrl);
|
|
@@ -348,32 +361,106 @@ var clone = async (args) => {
|
|
|
348
361
|
}
|
|
349
362
|
const commits = await getCommits(apiUrl, token, uuid);
|
|
350
363
|
writeManifest(root, { uuid, apiUrl, name: project.name, baseHeadId: commits.headId, files });
|
|
351
|
-
success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}
|
|
364
|
+
success(`Cloned ${c.bold(project.name)} into ${c.bold(dir)}/ (${Object.keys(files).length} files)`);
|
|
352
365
|
info(c.dim(` cd ${dir} — edit, then \`lecodes push -m "your message"\``));
|
|
353
366
|
};
|
|
354
367
|
|
|
355
|
-
// src/
|
|
368
|
+
// src/browserAuth.ts
|
|
369
|
+
import { createServer } from "node:http";
|
|
370
|
+
import { spawn } from "node:child_process";
|
|
371
|
+
import { randomBytes } from "node:crypto";
|
|
356
372
|
import { hostname } from "node:os";
|
|
357
|
-
var
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
373
|
+
var SUCCESS_HTML = `<!doctype html><meta charset="utf-8"><title>lecodes</title>
|
|
374
|
+
<body style="font-family:system-ui,sans-serif;background:#0f1115;color:#e6e6e6;display:grid;place-items:center;height:100vh;margin:0">
|
|
375
|
+
<div style="text-align:center;max-width:420px;padding:32px">
|
|
376
|
+
<h1 style="font-size:20px;margin:0 0 8px">✓ Authorized</h1>
|
|
377
|
+
<p style="color:#9aa0aa;margin:0;font-size:14px">You can close this tab and return to your terminal.</p></div>`;
|
|
378
|
+
var errorHtml = (msg) => `<!doctype html><meta charset="utf-8"><title>lecodes</title>
|
|
379
|
+
<body style="font-family:system-ui,sans-serif;background:#0f1115;color:#e6e6e6;display:grid;place-items:center;height:100vh;margin:0">
|
|
380
|
+
<div style="text-align:center;max-width:420px;padding:32px">
|
|
381
|
+
<h1 style="font-size:20px;margin:0 0 8px">Authorization failed</h1>
|
|
382
|
+
<p style="color:#9aa0aa;margin:0;font-size:14px">${msg}</p></div>`;
|
|
383
|
+
var openBrowser = (url) => {
|
|
384
|
+
let cmd;
|
|
385
|
+
let args;
|
|
386
|
+
if (process.platform === "win32") {
|
|
387
|
+
cmd = "rundll32";
|
|
388
|
+
args = ["url.dll,FileProtocolHandler", url];
|
|
389
|
+
} else if (process.platform === "darwin") {
|
|
390
|
+
cmd = "open";
|
|
391
|
+
args = [url];
|
|
392
|
+
} else {
|
|
393
|
+
cmd = "xdg-open";
|
|
394
|
+
args = [url];
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
398
|
+
child.on("error", () => {});
|
|
399
|
+
child.unref();
|
|
400
|
+
} catch {}
|
|
401
|
+
};
|
|
402
|
+
var browserLogin = async (webBase, timeoutMs = 5 * 60 * 1000, open = openBrowser) => {
|
|
403
|
+
const state = randomBytes(16).toString("hex");
|
|
404
|
+
let resolve2;
|
|
405
|
+
let reject;
|
|
406
|
+
const tokenPromise = new Promise((res, rej) => {
|
|
407
|
+
resolve2 = res;
|
|
408
|
+
reject = rej;
|
|
409
|
+
});
|
|
410
|
+
const server = createServer((req, res) => {
|
|
411
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
412
|
+
if (url.pathname !== "/") {
|
|
413
|
+
res.writeHead(404);
|
|
414
|
+
res.end();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const token = url.searchParams.get("token");
|
|
418
|
+
if (!token || url.searchParams.get("state") !== state) {
|
|
419
|
+
res.writeHead(400, { "content-type": "text/html" });
|
|
420
|
+
res.end(errorHtml("Invalid or expired authorization."));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
424
|
+
res.end(SUCCESS_HTML);
|
|
425
|
+
resolve2(token);
|
|
426
|
+
});
|
|
427
|
+
await new Promise((res, rej) => {
|
|
428
|
+
server.once("error", rej);
|
|
429
|
+
server.listen(0, "127.0.0.1", res);
|
|
430
|
+
});
|
|
431
|
+
const port = server.address().port;
|
|
432
|
+
const timer = setTimeout(() => reject(new CliError("Timed out waiting for browser authorization.")), timeoutMs);
|
|
433
|
+
const authUrl = `${webBase}/app/cli-auth?port=${port}&state=${state}&name=${encodeURIComponent(hostname())}`;
|
|
434
|
+
info("Opening your browser to authorize the CLI…");
|
|
435
|
+
log(c.dim(" " + authUrl));
|
|
436
|
+
log(c.dim(" (waiting — finish in the browser, or press Ctrl+C to cancel)"));
|
|
437
|
+
open(authUrl);
|
|
438
|
+
try {
|
|
439
|
+
return await tokenPromise;
|
|
440
|
+
} finally {
|
|
441
|
+
clearTimeout(timer);
|
|
442
|
+
server.close();
|
|
368
443
|
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// src/commands/login.ts
|
|
447
|
+
var login = async (args) => {
|
|
448
|
+
const config = loadConfig();
|
|
449
|
+
const apiUrl = normalizeApiUrl(flagStr(args, "api") ?? config.apiUrl ?? DEFAULT_API);
|
|
450
|
+
const webBase = normalizeApiUrl(flagStr(args, "web") ?? apiUrl);
|
|
451
|
+
const token = flagStr(args, "token") ?? await browserLogin(webBase);
|
|
369
452
|
const account = await getAccount(apiUrl, token);
|
|
370
453
|
saveConfig({ apiUrl, token });
|
|
371
454
|
success(`Logged in as ${c.bold(account.email)} on ${apiUrl}`);
|
|
372
455
|
};
|
|
373
456
|
|
|
374
|
-
// src/commands/
|
|
375
|
-
import {
|
|
376
|
-
import {
|
|
457
|
+
// src/commands/preview.ts
|
|
458
|
+
import { writeFileSync as writeFileSync5 } from "node:fs";
|
|
459
|
+
import { resolve as resolve2 } from "node:path";
|
|
460
|
+
|
|
461
|
+
// src/compile/collect.ts
|
|
462
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
463
|
+
import { basename } from "node:path";
|
|
377
464
|
|
|
378
465
|
// src/localFiles.ts
|
|
379
466
|
import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -462,7 +549,369 @@ var diffLocal = (manifest, scan) => {
|
|
|
462
549
|
};
|
|
463
550
|
var hasChanges = (diff) => diff.added.length + diff.modified.length + diff.deleted.length > 0;
|
|
464
551
|
|
|
552
|
+
// src/compile/collect.ts
|
|
553
|
+
var collectEntries = (root, manifest, project) => {
|
|
554
|
+
const pathById = buildPaths(project.assets);
|
|
555
|
+
const serverByPath = new Map;
|
|
556
|
+
for (const asset of project.assets) {
|
|
557
|
+
if (asset.type === "folder")
|
|
558
|
+
continue;
|
|
559
|
+
serverByPath.set(pathById.get(asset.id), { fileSrc: asset.file?.src ?? undefined, shaders: asset.shaders ?? [] });
|
|
560
|
+
}
|
|
561
|
+
const entries = [];
|
|
562
|
+
const warnings = [];
|
|
563
|
+
for (const file of scanLocal(root).files) {
|
|
564
|
+
const type = manifest.files[file.path]?.type ?? classifyKind(basename(file.path));
|
|
565
|
+
if (type === "resource") {
|
|
566
|
+
const src = serverByPath.get(file.path)?.fileSrc;
|
|
567
|
+
if (!src)
|
|
568
|
+
warnings.push(`No server URL for ${file.path} — push it so its URL resolves.`);
|
|
569
|
+
entries.push({ path: file.path, type, fileSrc: src });
|
|
570
|
+
} else if (type === "shader") {
|
|
571
|
+
const shaders = serverByPath.get(file.path)?.shaders;
|
|
572
|
+
if (!shaders?.length)
|
|
573
|
+
warnings.push(`No compiled shader for ${file.path} — push it to compile the shader.`);
|
|
574
|
+
entries.push({ path: file.path, type, shaders });
|
|
575
|
+
} else {
|
|
576
|
+
entries.push({ path: file.path, type, text: readFileSync4(file.absPath, "utf8") });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return { entries, warnings };
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// src/compile/bundler.ts
|
|
583
|
+
import * as esbuild from "esbuild";
|
|
584
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
585
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
586
|
+
import { fileURLToPath } from "node:url";
|
|
587
|
+
|
|
588
|
+
// src/compile/assetMacro.ts
|
|
589
|
+
var ASSET_RE = /\basset\(\s*(["'])((?:[^"'\\]|\\.)*)\1\s*\)/g;
|
|
590
|
+
var hasAssetMacro = (src) => src.includes("asset(");
|
|
591
|
+
var desugarAssetMacros = (src, filePath) => {
|
|
592
|
+
const byPath = new Map;
|
|
593
|
+
let i = 0;
|
|
594
|
+
const out = src.replace(ASSET_RE, (_match, _quote, path) => {
|
|
595
|
+
let name = byPath.get(path);
|
|
596
|
+
if (!name) {
|
|
597
|
+
name = `__asset_${i++}`;
|
|
598
|
+
byPath.set(path, name);
|
|
599
|
+
}
|
|
600
|
+
return name;
|
|
601
|
+
});
|
|
602
|
+
if (/\basset\(/.test(out)) {
|
|
603
|
+
throw new Error(`asset() expects a string literal, found a dynamic argument in: ${filePath}`);
|
|
604
|
+
}
|
|
605
|
+
if (byPath.size === 0)
|
|
606
|
+
return src;
|
|
607
|
+
const header = [...byPath].map(([path, name]) => `import ${name} from ${JSON.stringify(path)};`).join(`
|
|
608
|
+
`);
|
|
609
|
+
return header + `
|
|
610
|
+
` + out;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// src/compile/bundler.ts
|
|
614
|
+
var GL_MATRIX_HEADER = `
|
|
615
|
+
import * as glMatrix from "gl-matrix/common";
|
|
616
|
+
import * as mat2 from "gl-matrix/mat2";
|
|
617
|
+
import * as mat2d from "gl-matrix/mat2d";
|
|
618
|
+
import * as mat3 from "gl-matrix/mat3";
|
|
619
|
+
import * as mat4 from "gl-matrix/mat4";
|
|
620
|
+
import * as quat from "gl-matrix/quat";
|
|
621
|
+
import * as quat2 from "gl-matrix/quat2";
|
|
622
|
+
import * as vec2 from "gl-matrix/vec2";
|
|
623
|
+
import * as vec3 from "gl-matrix/vec3";
|
|
624
|
+
import * as vec4 from "gl-matrix/vec4";
|
|
625
|
+
`;
|
|
626
|
+
var resolveRuntime = () => {
|
|
627
|
+
const here = dirname3(fileURLToPath(import.meta.url));
|
|
628
|
+
const vendored = join6(here, "..", "runtime");
|
|
629
|
+
if (existsSync3(join6(vendored, "worker", "creator.js"))) {
|
|
630
|
+
const w = join6(vendored, "worker");
|
|
631
|
+
return {
|
|
632
|
+
alias: {
|
|
633
|
+
creator: join6(w, "creator.js"),
|
|
634
|
+
ui: join6(w, "ui.js"),
|
|
635
|
+
animate: join6(w, "animate.js"),
|
|
636
|
+
utils: join6(w, "utils.js"),
|
|
637
|
+
"gl-matrix": join6(vendored, "gl-matrix")
|
|
638
|
+
},
|
|
639
|
+
inject: [join6(w, "globals.js")]
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
const repoRoot = join6(here, "..", "..", "..", "..");
|
|
643
|
+
const ws = join6(repoRoot, "packages", "worker", "src");
|
|
644
|
+
if (existsSync3(join6(ws, "index.ts"))) {
|
|
645
|
+
return {
|
|
646
|
+
alias: {
|
|
647
|
+
creator: join6(ws, "index.ts"),
|
|
648
|
+
ui: join6(ws, "ui.ts"),
|
|
649
|
+
animate: join6(ws, "animate.ts"),
|
|
650
|
+
utils: join6(ws, "utils.ts"),
|
|
651
|
+
"gl-matrix": join6(repoRoot, "packages", "backend", "src", "assets", "dependencies", "gl-matrix")
|
|
652
|
+
},
|
|
653
|
+
inject: [join6(ws, "globals.ts")]
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
throw new CliError("Could not locate the compile runtime — try reinstalling lecodes-cli.");
|
|
657
|
+
};
|
|
658
|
+
function normalize(path) {
|
|
659
|
+
const parts = [];
|
|
660
|
+
for (const part of path.split("/")) {
|
|
661
|
+
if (!part || part === ".")
|
|
662
|
+
continue;
|
|
663
|
+
if (part === "..")
|
|
664
|
+
parts.pop();
|
|
665
|
+
else
|
|
666
|
+
parts.push(part);
|
|
667
|
+
}
|
|
668
|
+
return "/" + parts.join("/");
|
|
669
|
+
}
|
|
670
|
+
function dirOf(path) {
|
|
671
|
+
const i = path.lastIndexOf("/");
|
|
672
|
+
return i <= 0 ? "/" : path.slice(0, i);
|
|
673
|
+
}
|
|
674
|
+
var hasExtension = (path) => /\.[a-zA-Z0-9]+$/.test(path);
|
|
675
|
+
function resolveBase(importer, specifier) {
|
|
676
|
+
if (specifier.startsWith("/"))
|
|
677
|
+
return normalize(specifier);
|
|
678
|
+
if (specifier.startsWith("./") || specifier.startsWith("../"))
|
|
679
|
+
return normalize(dirOf(importer) + "/" + specifier);
|
|
680
|
+
throw new Error(`Bare import is not allowed here: ${specifier}`);
|
|
681
|
+
}
|
|
682
|
+
function resolveWithExtensions(files, importer, specifier) {
|
|
683
|
+
const base = resolveBase(importer, specifier);
|
|
684
|
+
if (hasExtension(base)) {
|
|
685
|
+
if (files.has(base))
|
|
686
|
+
return base;
|
|
687
|
+
throw new Error(`Module not found: ${base}`);
|
|
688
|
+
}
|
|
689
|
+
for (const candidate of [base, `${base}.ts`, `${base}.js`]) {
|
|
690
|
+
if (files.has(candidate))
|
|
691
|
+
return candidate;
|
|
692
|
+
}
|
|
693
|
+
throw new Error(`Module not found: ${specifier} from ${importer}`);
|
|
694
|
+
}
|
|
695
|
+
var getLoader = (path) => path.endsWith(".ts") ? "ts" : "js";
|
|
696
|
+
async function buildInMemory(entrypoint, inputFiles, format = "esm") {
|
|
697
|
+
const runtime = resolveRuntime();
|
|
698
|
+
const files = new Map(Object.entries(inputFiles).map(([k, v]) => [normalize(k), v]));
|
|
699
|
+
const entry = normalize(entrypoint);
|
|
700
|
+
if (!files.has(entry))
|
|
701
|
+
throw new CliError(`Entrypoint not found: ${entry}`);
|
|
702
|
+
const plugin = {
|
|
703
|
+
name: "memfs",
|
|
704
|
+
setup(build2) {
|
|
705
|
+
build2.onResolve({ filter: /^entry$/ }, () => ({ path: entry, namespace: "mem" }));
|
|
706
|
+
build2.onResolve({ filter: /.*/, namespace: "mem" }, (args) => {
|
|
707
|
+
const isRelative = args.path.startsWith("./") || args.path.startsWith("../");
|
|
708
|
+
const isAbsolute = args.path.startsWith("/");
|
|
709
|
+
if (!isRelative && !isAbsolute)
|
|
710
|
+
return null;
|
|
711
|
+
try {
|
|
712
|
+
return { path: resolveWithExtensions(files, args.importer || entry, args.path), namespace: "mem" };
|
|
713
|
+
} catch (error) {
|
|
714
|
+
return { errors: [{ text: error instanceof Error ? error.message : String(error) }] };
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
build2.onLoad({ filter: /.*/, namespace: "mem" }, (args) => {
|
|
718
|
+
const contents = files.get(args.path);
|
|
719
|
+
if (contents == null)
|
|
720
|
+
return { errors: [{ text: `Module not found: ${args.path}` }] };
|
|
721
|
+
const finalContents = /\.[tj]sx?$/.test(args.path) && hasAssetMacro(contents) ? desugarAssetMacros(contents, args.path) : contents;
|
|
722
|
+
return { contents: finalContents, loader: getLoader(args.path), resolveDir: dirOf(args.path) };
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
const result = await esbuild.build({
|
|
727
|
+
stdin: { contents: `import "entry";`, loader: "js", sourcefile: "bootstrap.js" },
|
|
728
|
+
alias: runtime.alias,
|
|
729
|
+
define: { RAD2DEG: "57.29577951308232", DEG2RAD: "0.017453292519943295" },
|
|
730
|
+
inject: runtime.inject,
|
|
731
|
+
bundle: true,
|
|
732
|
+
write: false,
|
|
733
|
+
format,
|
|
734
|
+
platform: "browser",
|
|
735
|
+
plugins: [plugin],
|
|
736
|
+
charset: "utf8"
|
|
737
|
+
});
|
|
738
|
+
const output = result.outputFiles?.[0];
|
|
739
|
+
if (!output)
|
|
740
|
+
throw new CliError("Compilation produced no output");
|
|
741
|
+
return output.text;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/compile/projectCompile.ts
|
|
745
|
+
var buildHeader = (name, code) => {
|
|
746
|
+
let header = `// name: ${name.slice(0, 20)}
|
|
747
|
+
`;
|
|
748
|
+
header += `// last-updated: ${new Date().toISOString()}
|
|
749
|
+
`;
|
|
750
|
+
const localAssets = new Set;
|
|
751
|
+
for (const m of code.matchAll(/_creatorUtils\.fetchLocal\((.+?)\)/g)) {
|
|
752
|
+
localAssets.add(m[1].replaceAll(/['"]/g, "").trim());
|
|
753
|
+
}
|
|
754
|
+
if (/_creator\.createGlb/.test(code))
|
|
755
|
+
localAssets.add("uberarchive");
|
|
756
|
+
if (/_creator\.captureImage/.test(code))
|
|
757
|
+
localAssets.add("capture");
|
|
758
|
+
if (localAssets.size > 0)
|
|
759
|
+
header += `// preload: ${[...localAssets].join(";")}
|
|
760
|
+
`;
|
|
761
|
+
if (!code.includes("_creator.createScene") && !code.includes("_creator.createEntity"))
|
|
762
|
+
header += `// gl: disable
|
|
763
|
+
`;
|
|
764
|
+
else if (code.includes("_creator.createEngine"))
|
|
765
|
+
header += `// gl: defer
|
|
766
|
+
`;
|
|
767
|
+
if (code.includes("_creator.createARController"))
|
|
768
|
+
header += "// ar: enable";
|
|
769
|
+
header += `
|
|
770
|
+
`;
|
|
771
|
+
return header;
|
|
772
|
+
};
|
|
773
|
+
var detectEntry = (entries) => {
|
|
774
|
+
let entry = "";
|
|
775
|
+
let max = -1;
|
|
776
|
+
for (const e of entries) {
|
|
777
|
+
const ext2 = e.path.split(".").at(-1)?.toLowerCase();
|
|
778
|
+
if (e.type === "text" && e.text !== undefined && !e.text.includes("export") && (ext2 === "ts" || ext2 === "js")) {
|
|
779
|
+
if (e.text.length > max) {
|
|
780
|
+
entry = e.path;
|
|
781
|
+
max = e.text.length;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return entry;
|
|
786
|
+
};
|
|
787
|
+
var compileProject = async (opts) => {
|
|
788
|
+
const entry = opts.entryOverride ?? detectEntry(opts.entries);
|
|
789
|
+
if (!entry) {
|
|
790
|
+
throw new CliError("Couldn't find an entrypoint (a top-level .ts/.js file with no `export`). Pass --entry <path>.");
|
|
791
|
+
}
|
|
792
|
+
const files = {};
|
|
793
|
+
for (const e of opts.entries) {
|
|
794
|
+
if (e.type === "resource") {
|
|
795
|
+
files[e.path] = `export default "${opts.publicUrl}${e.fileSrc ?? ""}"`;
|
|
796
|
+
} else if (e.type === "shader") {
|
|
797
|
+
const shaders = e.shaders ?? [];
|
|
798
|
+
if (shaders.length > 2) {
|
|
799
|
+
files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.subBackend ?? _creator.backend}")}\``;
|
|
800
|
+
} else if (shaders.length > 0) {
|
|
801
|
+
files[e.path] = `export default \`${opts.publicUrl}${shaders[0].src.replace("_opengl", "_${_creator.backend}")}\``;
|
|
802
|
+
} else {
|
|
803
|
+
files[e.path] = `export default ""`;
|
|
804
|
+
}
|
|
805
|
+
} else if (e.path.toLowerCase().endsWith(".svg")) {
|
|
806
|
+
files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`;
|
|
807
|
+
} else {
|
|
808
|
+
files[e.path] = GL_MATRIX_HEADER + (e.text ?? "");
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const code = await buildInMemory(entry, files, opts.format ?? "esm");
|
|
812
|
+
return opts.header === false ? code : buildHeader(opts.name, code) + code;
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// src/commands/preview.ts
|
|
816
|
+
var preview = async (args) => {
|
|
817
|
+
const root = findProjectRoot(process.cwd());
|
|
818
|
+
const manifest = readManifest(root);
|
|
819
|
+
const config = loadConfig();
|
|
820
|
+
const apiUrl = requireApiUrl(config);
|
|
821
|
+
const token = requireToken(config);
|
|
822
|
+
note("Resolving project assets…");
|
|
823
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
824
|
+
const { entries, warnings } = collectEntries(root, manifest, project);
|
|
825
|
+
note("Compiling…");
|
|
826
|
+
const js = await compileProject({
|
|
827
|
+
entries,
|
|
828
|
+
name: manifest.name,
|
|
829
|
+
publicUrl: flagStr(args, "public-url") ?? apiUrl,
|
|
830
|
+
entryOverride: flagStr(args, "entry"),
|
|
831
|
+
header: !flagBool(args, "no-header")
|
|
832
|
+
});
|
|
833
|
+
for (const w of warnings)
|
|
834
|
+
warnErr(w);
|
|
835
|
+
const out = flagStr(args, "o", "out");
|
|
836
|
+
if (out) {
|
|
837
|
+
writeFileSync5(resolve2(out), js);
|
|
838
|
+
note(`Wrote ${out} (${js.length} bytes)`);
|
|
839
|
+
} else {
|
|
840
|
+
process.stdout.write(js);
|
|
841
|
+
if (!js.endsWith(`
|
|
842
|
+
`))
|
|
843
|
+
process.stdout.write(`
|
|
844
|
+
`);
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// src/commands/render.ts
|
|
849
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
850
|
+
import { resolve as resolve3 } from "node:path";
|
|
851
|
+
var render = async (args) => {
|
|
852
|
+
const root = findProjectRoot(process.cwd());
|
|
853
|
+
const manifest = readManifest(root);
|
|
854
|
+
const config = loadConfig();
|
|
855
|
+
const apiUrl = requireApiUrl(config);
|
|
856
|
+
const token = requireToken(config);
|
|
857
|
+
note("Resolving project assets…");
|
|
858
|
+
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
859
|
+
const { entries, warnings } = collectEntries(root, manifest, project);
|
|
860
|
+
note("Compiling…");
|
|
861
|
+
const js = await compileProject({
|
|
862
|
+
entries,
|
|
863
|
+
name: manifest.name,
|
|
864
|
+
publicUrl: flagStr(args, "public-url") ?? apiUrl,
|
|
865
|
+
entryOverride: flagStr(args, "entry"),
|
|
866
|
+
header: false,
|
|
867
|
+
format: "iife"
|
|
868
|
+
});
|
|
869
|
+
if (js.includes("_creator.createScene") || js.includes("_creator.createEntity")) {
|
|
870
|
+
warnings.push("This looks like a 3D/GL project — only the UI layer is rendered.");
|
|
871
|
+
}
|
|
872
|
+
let renderer;
|
|
873
|
+
try {
|
|
874
|
+
renderer = await import("lecodes-render");
|
|
875
|
+
} catch {
|
|
876
|
+
throw new CliError("The renderer isn't installed. Run: npm i -D lecodes-render");
|
|
877
|
+
}
|
|
878
|
+
const width = Number(flagStr(args, "width")) || 390;
|
|
879
|
+
const height = Number(flagStr(args, "height")) || 844;
|
|
880
|
+
const pngFlag = flagStr(args, "png") ?? (flagBool(args, "png") ? "screenshot.png" : undefined);
|
|
881
|
+
if (pngFlag !== undefined) {
|
|
882
|
+
note("Rendering PNG…");
|
|
883
|
+
const { png, warnings: pngWarnings } = await renderer.renderToPng(js, {
|
|
884
|
+
width,
|
|
885
|
+
height,
|
|
886
|
+
scale: Number(flagStr(args, "scale")) || 2
|
|
887
|
+
});
|
|
888
|
+
const pngPath = resolve3(pngFlag);
|
|
889
|
+
writeFileSync6(pngPath, png);
|
|
890
|
+
for (const w of [...warnings, ...pngWarnings])
|
|
891
|
+
warnErr(w);
|
|
892
|
+
note(`Wrote ${pngFlag} (${png.length} bytes, ${width}x${height})`);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
note("Rendering…");
|
|
896
|
+
const result = await renderer.renderToJson(js, { width, height });
|
|
897
|
+
for (const w of [...warnings, ...result.warnings])
|
|
898
|
+
warnErr(w);
|
|
899
|
+
const out = flagStr(args, "o", "out");
|
|
900
|
+
if (out) {
|
|
901
|
+
writeFileSync6(resolve3(out), result.text);
|
|
902
|
+
note(`Wrote ${out} (${result.text.length} bytes)`);
|
|
903
|
+
} else {
|
|
904
|
+
process.stdout.write(result.text);
|
|
905
|
+
if (!result.text.endsWith(`
|
|
906
|
+
`))
|
|
907
|
+
process.stdout.write(`
|
|
908
|
+
`);
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
|
|
465
912
|
// src/commands/pull.ts
|
|
913
|
+
import { existsSync as existsSync4, rmSync } from "node:fs";
|
|
914
|
+
import { join as join7 } from "node:path";
|
|
466
915
|
var pull = async (args) => {
|
|
467
916
|
const root = findProjectRoot(process.cwd());
|
|
468
917
|
const manifest = readManifest(root);
|
|
@@ -477,12 +926,13 @@ var pull = async (args) => {
|
|
|
477
926
|
const remotePaths = new Set([...buildPaths(project.assets).entries()].filter(([id]) => project.assets.find((a) => a.id === id)?.type !== "folder").map(([, path]) => path));
|
|
478
927
|
for (const path of Object.keys(manifest.files)) {
|
|
479
928
|
if (!remotePaths.has(path)) {
|
|
480
|
-
const abs =
|
|
481
|
-
if (
|
|
929
|
+
const abs = join7(root, ...path.replace(/^\//, "").split("/"));
|
|
930
|
+
if (existsSync4(abs))
|
|
482
931
|
rmSync(abs);
|
|
483
932
|
}
|
|
484
933
|
}
|
|
485
|
-
const files = await materializeAssets(root, project, apiUrl, token);
|
|
934
|
+
const files = await materializeAssets(root, project, apiUrl, token, progress);
|
|
935
|
+
endProgress();
|
|
486
936
|
if (!flagBool(args, "no-types")) {
|
|
487
937
|
await materializeTypes(root, apiUrl);
|
|
488
938
|
writeTsconfig(root);
|
|
@@ -493,8 +943,8 @@ var pull = async (args) => {
|
|
|
493
943
|
};
|
|
494
944
|
|
|
495
945
|
// src/commands/push.ts
|
|
496
|
-
import { basename } from "node:path";
|
|
497
|
-
import { readFileSync as
|
|
946
|
+
import { basename as basename2 } from "node:path";
|
|
947
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
498
948
|
var push = async (args) => {
|
|
499
949
|
const root = findProjectRoot(process.cwd());
|
|
500
950
|
const manifest = readManifest(root);
|
|
@@ -519,14 +969,14 @@ var push = async (args) => {
|
|
|
519
969
|
let uploaded = 0;
|
|
520
970
|
for (const file of scan.files) {
|
|
521
971
|
const source = sourceFor(file.path);
|
|
522
|
-
const kind = source?.type ?? classifyKind(
|
|
972
|
+
const kind = source?.type ?? classifyKind(basename2(file.path));
|
|
523
973
|
const entry = { path: file.path, type: kind };
|
|
524
974
|
if (source)
|
|
525
975
|
entry.assetId = source.id;
|
|
526
976
|
if (isTextKind(kind)) {
|
|
527
|
-
entry.text =
|
|
977
|
+
entry.text = readFileSync5(file.absPath, "utf8");
|
|
528
978
|
} else if (!source || source.sha !== file.sha) {
|
|
529
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid,
|
|
979
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename2(file.path), readFileSync5(file.absPath));
|
|
530
980
|
entry.blobFileId = blob.id;
|
|
531
981
|
uploaded++;
|
|
532
982
|
}
|
|
@@ -586,9 +1036,10 @@ var HELP = `${c.bold("lecodes")} — clone, edit and push LeCodes projects from
|
|
|
586
1036
|
${c.bold("Usage:")} lecodes <command> [options]
|
|
587
1037
|
|
|
588
1038
|
${c.bold("Commands:")}
|
|
589
|
-
login
|
|
590
|
-
--token <pat>
|
|
591
|
-
--api <url>
|
|
1039
|
+
login Authorize in the browser and store an access token
|
|
1040
|
+
--token <pat> store a token from the web UI instead
|
|
1041
|
+
--api <url> API origin (default https://le.codes)
|
|
1042
|
+
--web <url> app origin for the browser flow
|
|
592
1043
|
clone <uuid|url> [dir] Download a project's files + types into a new folder
|
|
593
1044
|
--no-types skip the .d.ts / tsconfig generation
|
|
594
1045
|
status Show local changes vs the last sync
|
|
@@ -597,9 +1048,22 @@ ${c.bold("Commands:")}
|
|
|
597
1048
|
push Push local changes as one new checkpoint
|
|
598
1049
|
-m, --message checkpoint message
|
|
599
1050
|
--force push even if the server moved on
|
|
1051
|
+
preview Compile the local project to JS (prints to stdout)
|
|
1052
|
+
-o, --out <file> write to a file instead of stdout
|
|
1053
|
+
--entry <path> override the entrypoint
|
|
1054
|
+
--no-header omit the metadata header
|
|
1055
|
+
--public-url <url> asset URL prefix (default: the API origin)
|
|
1056
|
+
render Render the local UI project to a semantic JSON description
|
|
1057
|
+
(needs the optional 'lecodes-render' package; UI projects only)
|
|
1058
|
+
-o, --out <file> write JSON to a file instead of stdout
|
|
1059
|
+
--png [file] rasterize a PNG (default screenshot.png;
|
|
1060
|
+
needs @napi-rs/canvas)
|
|
1061
|
+
--scale <n> PNG device-pixel scale (default 2)
|
|
1062
|
+
--width/--height viewport size (default 390x844)
|
|
1063
|
+
--entry <path> override the entrypoint
|
|
600
1064
|
|
|
601
1065
|
Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKEN).`;
|
|
602
|
-
var commands = { login
|
|
1066
|
+
var commands = { login, clone, status, pull, push, preview, render };
|
|
603
1067
|
var main = async () => {
|
|
604
1068
|
const argv = process.argv.slice(2);
|
|
605
1069
|
const command = argv[0];
|