lecodes-cli 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1013 -127
- package/package.json +13 -12
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/assets/{inspect-C7x78PNs.js → inspect-DdJ50Kun.js} +2 -2
- package/runtime/web/player.js +18 -18
- package/runtime/web-lite/assets/canvas-C6G_Pb2Q.js +1 -0
- package/runtime/web-lite/assets/pager-O712dZv9.js +1 -0
- package/runtime/web-lite/assets/roboto-cyrillic-DVBNKCCH.woff2 +0 -0
- package/runtime/web-lite/assets/roboto-cyrillic-ext-1gPz9dK9.woff2 +0 -0
- package/runtime/web-lite/assets/roboto-greek-DVLYEgdx.woff2 +0 -0
- package/runtime/web-lite/assets/roboto-greek-ext-D98D44EL.woff2 +0 -0
- package/runtime/web-lite/assets/roboto-latin-cAJwcdBQ.woff2 +0 -0
- package/runtime/web-lite/assets/roboto-latin-ext-t610Sqxq.woff2 +0 -0
- package/runtime/web-lite/assets/roboto-rest-C7JkyNHT.woff2 +0 -0
- package/runtime/web-lite/assets/roboto-vietnamese-w-p-nKZZ.woff2 +0 -0
- package/runtime/web-lite/assets/tween-t_XDy3Nd.js +1 -0
- package/runtime/web-lite/assets/vlist-gEB8Sn0S.js +1 -0
- package/runtime/web-lite/features.json +51 -0
- package/runtime/web-lite/index.html +23 -0
- package/runtime/web-lite/lite.js +249 -0
- package/src/commands/design/comments.ts +1 -1
- package/src/commands/design/context.ts +1 -1
- package/src/commands/design/index.ts +1 -1
- package/src/commands/dev.ts +17 -53
- package/src/commands/index.ts +2 -0
- package/src/commands/test.ts +55 -6
- package/src/commands/update/index.ts +8 -5
- package/src/commands/web.ts +171 -0
- package/src/compile/fonts.ts +5 -2
- package/src/dev/devServer.ts +20 -1
- package/src/dev/watchProject.ts +53 -0
- package/src/dev/webRunner.ts +10 -0
- package/src/hosts/archives.ts +66 -0
- package/src/hosts/liteBrowser.mjs +86 -0
- package/src/hosts/liteRunner.ts +386 -0
- package/src/hosts/peerInstall.ts +12 -8
- package/src/web/build.ts +194 -0
- package/src/web/preview.ts +50 -0
package/dist/index.js
CHANGED
|
@@ -52,28 +52,29 @@ var package_default;
|
|
|
52
52
|
var init_package = __esm(() => {
|
|
53
53
|
package_default = {
|
|
54
54
|
name: "lecodes-cli",
|
|
55
|
-
version: "2.0.
|
|
55
|
+
version: "2.0.4",
|
|
56
56
|
dependencies: {
|
|
57
57
|
"@letary/chisel": "^0.13.0",
|
|
58
58
|
jimp: "^1.6.1",
|
|
59
59
|
"lecodes-sdk": "workspace:*"
|
|
60
60
|
},
|
|
61
61
|
devDependencies: {
|
|
62
|
-
"lecodes-design": "
|
|
62
|
+
"lecodes-design": "https://storage.yandexcloud.net/le-codes/releases/packages/lecodes-design/lecodes-design-0.11.0.tgz",
|
|
63
63
|
"lecodes-headless": "workspace:*",
|
|
64
64
|
"qrcode-terminal": "^0.12.0",
|
|
65
|
-
"lecodes-3d-editor": "
|
|
65
|
+
"lecodes-3d-editor": "https://storage.yandexcloud.net/le-codes/releases/packages/lecodes-3d-editor/lecodes-3d-editor-0.9.0.tgz",
|
|
66
66
|
"lecodes-assets": "workspace:*",
|
|
67
67
|
"lecodes-creator-nav": "workspace:*",
|
|
68
68
|
"lecodes-gl-lite": "workspace:*",
|
|
69
69
|
"lecodes-web-frame": "workspace:*",
|
|
70
70
|
"lecodes-web-player": "workspace:*",
|
|
71
|
-
"lecodes-web-shared": "workspace:*"
|
|
71
|
+
"lecodes-web-shared": "workspace:*",
|
|
72
|
+
"lecodes-web-lite": "workspace:*"
|
|
72
73
|
},
|
|
73
74
|
peerDependencies: {
|
|
74
|
-
"lecodes-design": "^0.
|
|
75
|
-
"lecodes-headless": "^2.0.
|
|
76
|
-
"lecodes-3d-editor": "^0.
|
|
75
|
+
"lecodes-design": "^0.11.0",
|
|
76
|
+
"lecodes-headless": "^2.0.4",
|
|
77
|
+
"lecodes-3d-editor": "^0.9.0",
|
|
77
78
|
"lecodes-assets": "^0.2.1"
|
|
78
79
|
},
|
|
79
80
|
bin: {
|
|
@@ -126,7 +127,7 @@ var init_package = __esm(() => {
|
|
|
126
127
|
});
|
|
127
128
|
|
|
128
129
|
// ../../sdk/src/version.ts
|
|
129
|
-
var SDK_VERSION = "2.0.
|
|
130
|
+
var SDK_VERSION = "2.0.4", sdkMajor = (version) => {
|
|
130
131
|
const m = version?.match(/^(\d+)\.\d+\.\d+/);
|
|
131
132
|
return m ? Number(m[1]) : null;
|
|
132
133
|
}, bundleSdkVersion = (head) => head.slice(0, 2000).match(/^\/\/ sdk: (\d+\.\d+\.\d+\S*)/m)?.[1] ?? null, bundleRunsOn = (bundleVersion, hostVersion) => sdkMajor(bundleVersion) !== null && sdkMajor(bundleVersion) === sdkMajor(hostVersion);
|
|
@@ -1349,6 +1350,7 @@ var init_chisel = __esm(() => {
|
|
|
1349
1350
|
"remove",
|
|
1350
1351
|
"setContent",
|
|
1351
1352
|
"keepAlive",
|
|
1353
|
+
"url",
|
|
1352
1354
|
"attachTo",
|
|
1353
1355
|
"detents",
|
|
1354
1356
|
"dismissible",
|
|
@@ -171787,10 +171789,10 @@ var applyProjectFonts = async (entries, opts) => {
|
|
|
171787
171789
|
if (faces.length === 0)
|
|
171788
171790
|
return;
|
|
171789
171791
|
return buildFontsHeaderValue(faces, (path) => {
|
|
171790
|
-
if (!opts.publicUrl || !/^https?:/i.test(opts.publicUrl))
|
|
171792
|
+
if (!opts.relative && (!opts.publicUrl || !/^https?:/i.test(opts.publicUrl)))
|
|
171791
171793
|
return null;
|
|
171792
171794
|
const fileSrc = byPath().get(path.replace(/^\//, ""))?.fileSrc;
|
|
171793
|
-
return fileSrc ? opts.publicUrl + fileSrc : null;
|
|
171795
|
+
return fileSrc ? (opts.publicUrl ?? "") + fileSrc : null;
|
|
171794
171796
|
}) || undefined;
|
|
171795
171797
|
}, isHttp = (s) => /^https?:/i.test(s), sha12 = (b) => createHash2("sha256").update(b).digest("hex").slice(0, 12), registryHome = (registry) => registry.replace(/\/v\d+$/, ""), readRegistry = async (registry, rel) => {
|
|
171796
171798
|
if (isHttp(registry)) {
|
|
@@ -212716,6 +212718,53 @@ var init_peers = __esm(() => {
|
|
|
212716
212718
|
];
|
|
212717
212719
|
});
|
|
212718
212720
|
|
|
212721
|
+
// src/hosts/archives.ts
|
|
212722
|
+
var ARCHIVED_PEERS, BASE = "https://storage.yandexcloud.net/le-codes/releases/packages", packagesBase = () => (process.env.LECODES_PACKAGES_URL || BASE).replace(/\/$/, ""), archiveUrl = (name, version) => `${packagesBase()}/${name}/${name}-${version}.tgz`, numbers = (version) => version.split(/[.-]/).slice(0, 3).map(Number), byVersion = (a2, b) => {
|
|
212723
|
+
const pa = numbers(a2), pb = numbers(b);
|
|
212724
|
+
for (let i2 = 0;i2 < 3; i2++) {
|
|
212725
|
+
const d = (pa[i2] ?? 0) - (pb[i2] ?? 0);
|
|
212726
|
+
if (d)
|
|
212727
|
+
return d;
|
|
212728
|
+
}
|
|
212729
|
+
return 0;
|
|
212730
|
+
}, caretTakes = (floor, version) => {
|
|
212731
|
+
if (!/^\d+\.\d+\.\d+$/.test(version) || byVersion(version, floor) < 0)
|
|
212732
|
+
return false;
|
|
212733
|
+
const f5 = numbers(floor), v = numbers(version);
|
|
212734
|
+
const held = f5[0] ? 1 : f5[1] ? 2 : 3;
|
|
212735
|
+
for (let i2 = 0;i2 < held; i2++)
|
|
212736
|
+
if (f5[i2] !== v[i2])
|
|
212737
|
+
return false;
|
|
212738
|
+
return true;
|
|
212739
|
+
}, asked, versionsOut = (name) => {
|
|
212740
|
+
const url = `${packagesBase()}/${name}/index.json`;
|
|
212741
|
+
let answer = asked.get(url);
|
|
212742
|
+
if (!answer) {
|
|
212743
|
+
answer = fetch(`${url}?t=${Date.now()}`, { signal: AbortSignal.timeout(4000) }).then(async (res) => res.ok ? (await res.json()).versions ?? [] : []).catch(() => []);
|
|
212744
|
+
asked.set(url, answer);
|
|
212745
|
+
}
|
|
212746
|
+
return answer;
|
|
212747
|
+
}, archivedVersion = async (name) => {
|
|
212748
|
+
if (!ARCHIVED_PEERS.has(name))
|
|
212749
|
+
return null;
|
|
212750
|
+
if (name === "lecodes-headless")
|
|
212751
|
+
return package_default.version;
|
|
212752
|
+
const range = package_default.peerDependencies?.[name];
|
|
212753
|
+
const floor = range?.replace(/^[\^~>=\s]+/, "");
|
|
212754
|
+
if (!floor || !/^\d+\.\d+\.\d+/.test(floor))
|
|
212755
|
+
return null;
|
|
212756
|
+
const newest = (await versionsOut(name)).filter((version) => caretTakes(floor, version)).sort(byVersion).pop();
|
|
212757
|
+
return newest ?? floor;
|
|
212758
|
+
}, archiveSpec = async (name) => {
|
|
212759
|
+
const version = await archivedVersion(name);
|
|
212760
|
+
return version ? archiveUrl(name, version) : null;
|
|
212761
|
+
};
|
|
212762
|
+
var init_archives = __esm(() => {
|
|
212763
|
+
init_package();
|
|
212764
|
+
ARCHIVED_PEERS = new Set(["lecodes-headless", "lecodes-design", "lecodes-3d-editor"]);
|
|
212765
|
+
asked = new Map;
|
|
212766
|
+
});
|
|
212767
|
+
|
|
212719
212768
|
// src/hosts/peerInstall.ts
|
|
212720
212769
|
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
212721
212770
|
import { existsSync as existsSync23, mkdirSync as mkdirSync15, readFileSync as readFileSync19, writeFileSync as writeFileSync18 } from "node:fs";
|
|
@@ -212734,7 +212783,7 @@ async function loadPeer(name, subpath, opts) {
|
|
|
212734
212783
|
if (opts.install === false)
|
|
212735
212784
|
return null;
|
|
212736
212785
|
if (peerVersionGlobal(name, spec) !== null)
|
|
212737
|
-
throw new CliError(`${name} is installed but "${spec}" can't be loaded from it — reinstall it: npm install -g ${peerSpec(name)}`);
|
|
212786
|
+
throw new CliError(`${name} is installed but "${spec}" can't be loaded from it — reinstall it: npm install -g ${await peerSpec(name)}`);
|
|
212738
212787
|
await installPeers(companions(name), opts.for);
|
|
212739
212788
|
}
|
|
212740
212789
|
return await import(pathToFileURL(resolveEntry(dir, subpath)).href);
|
|
@@ -212745,9 +212794,10 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson2 = (path2) => {
|
|
|
212745
212794
|
} catch {
|
|
212746
212795
|
return null;
|
|
212747
212796
|
}
|
|
212748
|
-
}, peerSpec = (name) => {
|
|
212749
|
-
|
|
212750
|
-
|
|
212797
|
+
}, peerSpec = async (name) => {
|
|
212798
|
+
const archive = await archiveSpec(name);
|
|
212799
|
+
if (archive)
|
|
212800
|
+
return archive;
|
|
212751
212801
|
if (name === "@napi-rs/canvas") {
|
|
212752
212802
|
const renderer = readJson2(join24(peerDirInPrefix("lecodes-headless"), "package.json"));
|
|
212753
212803
|
const range2 = renderer?.peerDependencies?.["@napi-rs/canvas"] ?? renderer?.optionalDependencies?.["@napi-rs/canvas"] ?? CANVAS_RANGE_FALLBACK;
|
|
@@ -212774,7 +212824,7 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson2 = (path2) => {
|
|
|
212774
212824
|
`);
|
|
212775
212825
|
}
|
|
212776
212826
|
const ordered = [...names3].sort((a2, b) => a2 === "lecodes-headless" ? -1 : b === "lecodes-headless" ? 1 : 0);
|
|
212777
|
-
const specsNow = ordered.map(peerSpec);
|
|
212827
|
+
const specsNow = await Promise.all(ordered.map(peerSpec));
|
|
212778
212828
|
const yes = opts.yes === true || !!process.env.LECODES_YES;
|
|
212779
212829
|
if (!yes) {
|
|
212780
212830
|
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
@@ -212791,7 +212841,7 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson2 = (path2) => {
|
|
|
212791
212841
|
if (!tool)
|
|
212792
212842
|
throw new CliError(`Neither npm nor bun is on PATH, so ${names3.join(" + ")} can't be installed automatically. Install one, or run on a machine with Node: ${manualCommand(dir, specsNow)}`);
|
|
212793
212843
|
for (const name of ordered) {
|
|
212794
|
-
const spec = peerSpec(name);
|
|
212844
|
+
const spec = await peerSpec(name);
|
|
212795
212845
|
const argv = tool === "npm" ? ["install", "--prefix", dir, "--no-audit", "--no-fund", "--loglevel=error", spec] : ["add", "--cwd", dir, spec];
|
|
212796
212846
|
note(`$ ${tool} ${argv.join(" ")}`);
|
|
212797
212847
|
const r2 = spawnSync6(tool, argv, { stdio: "inherit", shell: process.platform === "win32", timeout: 10 * 60000 });
|
|
@@ -212840,6 +212890,7 @@ var CANVAS_RANGE_FALLBACK = "^0.1.65", readJson2 = (path2) => {
|
|
|
212840
212890
|
var init_peerInstall = __esm(() => {
|
|
212841
212891
|
init_package();
|
|
212842
212892
|
init_peers();
|
|
212893
|
+
init_archives();
|
|
212843
212894
|
init_cli();
|
|
212844
212895
|
});
|
|
212845
212896
|
|
|
@@ -213140,7 +213191,7 @@ var DEFAULT_DIR = "design", DEFAULT_PORT = 4477, dirFlag, getContext = (dir) =>
|
|
|
213140
213191
|
const mod = await loadPeer("lecodes-design", "server", { for: "lecodes design" });
|
|
213141
213192
|
const missing = needs.filter((name) => typeof mod[name] !== "function");
|
|
213142
213193
|
if (missing.length) {
|
|
213143
|
-
throw new CliError(`This needs a newer lecodes-design — the installed one has no \`${missing[0]}\`. Run:
|
|
213194
|
+
throw new CliError(`This needs a newer lecodes-design — the installed one has no \`${missing[0]}\`. Run: lecodes update`);
|
|
213144
213195
|
}
|
|
213145
213196
|
return mod;
|
|
213146
213197
|
}, requirePlatform = (ctx, verb) => {
|
|
@@ -213256,7 +213307,7 @@ var aiFlag, cliAuthor = (ai) => ai || process.env.CLAUDECODE ? { author: "Claude
|
|
|
213256
213307
|
if (!platform)
|
|
213257
213308
|
return null;
|
|
213258
213309
|
if (typeof designServer.mirrorThreads !== "function") {
|
|
213259
|
-
note("Comment sync needs a newer lecodes-design (
|
|
213310
|
+
note("Comment sync needs a newer lecodes-design (run: lecodes update) — serving without it.");
|
|
213260
213311
|
return null;
|
|
213261
213312
|
}
|
|
213262
213313
|
let failures = 0;
|
|
@@ -213570,7 +213621,7 @@ var init_snapshot = __esm(() => {
|
|
|
213570
213621
|
});
|
|
213571
213622
|
});
|
|
213572
213623
|
|
|
213573
|
-
// ../../node_modules/.bun/lecodes-design@0.
|
|
213624
|
+
// ../../node_modules/.bun/lecodes-design@https+++storage.yandexcloud.net+le-codes+releases+packages+lecodes-design+lecodes-design-0.11.0.tgz+a0cdf6553a4c6042/node_modules/lecodes-design/src/core/concept.ts
|
|
213574
213625
|
var SPEC_TEMPLATE = `# Concept
|
|
213575
213626
|
|
|
213576
213627
|
The narrative half of this design. \`meta.json\` + the canvas hold the *visual/flow* half (screens,
|
|
@@ -213606,7 +213657,7 @@ Behaviour that isn't visible in a single screen — validation, permissions, not
|
|
|
213606
213657
|
cases — and the decisions you've made and why, so they aren't re-litigated later.
|
|
213607
213658
|
`;
|
|
213608
213659
|
|
|
213609
|
-
// ../../node_modules/.bun/lecodes-design@0.
|
|
213660
|
+
// ../../node_modules/.bun/lecodes-design@https+++storage.yandexcloud.net+le-codes+releases+packages+lecodes-design+lecodes-design-0.11.0.tgz+a0cdf6553a4c6042/node_modules/lecodes-design/src/templates.ts
|
|
213610
213661
|
var SPEC_MD, META_JSON = `{
|
|
213611
213662
|
"screens": {
|
|
213612
213663
|
"home": { "pos": [0, 0], "desc": "Example screen — replace me", "role": "list" }
|
|
@@ -213703,9 +213754,9 @@ export default UIScreen([
|
|
|
213703
213754
|
mb: spacing.lg,
|
|
213704
213755
|
}),
|
|
213705
213756
|
]).style({ bgColor: colors.bg, p: spacing.md, pt: "safe-top" })
|
|
213706
|
-
`, CLAUDE_MD = `#
|
|
213757
|
+
`, CLAUDE_MD = `# le.codes Design — screen prototyping canvas
|
|
213707
213758
|
|
|
213708
|
-
This folder is a **design canvas** for a
|
|
213759
|
+
This folder is a **design canvas** for a le.codes app: each file in \`screens/\` is one live,
|
|
213709
213760
|
executable screen shown on a spatial canvas in the browser (\`lecodes design\` runs the dev server).
|
|
213710
213761
|
You (Claude) write the screens; the developer views them on the canvas and gives feedback.
|
|
213711
213762
|
This is an intent-capture stage — screens run on inline mock data, before any backend exists.
|
|
@@ -213811,7 +213862,7 @@ keep in the chat.
|
|
|
213811
213862
|
(entities + fields ARE the future schema — make them meaningful, not "Lorem ipsum").
|
|
213812
213863
|
- **Reuse via shared/.** Design tokens and reusable components live in \`shared/\`. Use them in
|
|
213813
213864
|
every screen. Promoting a repeated pattern into \`shared/\` is an explicit, deliberate step.
|
|
213814
|
-
- The
|
|
213865
|
+
- The le.codes UI API (\`UIScreen\`, \`UIColumn\`, \`UIRow\`, \`UIBox\`, \`UIText\`, \`UIImage\`,
|
|
213815
213866
|
\`UIButton\`, \`UIInput\`, \`UISpacer\`, \`UIScrollable\`, …) is **global** — no imports needed.
|
|
213816
213867
|
Colors must be hex strings. This folder ships a \`tsconfig.json\` + bundled SDK types
|
|
213817
213868
|
(\`.lecodes/types/\`), so the editor resolves those globals — and \`UINode\` / \`UINodeChild\` for
|
|
@@ -214033,7 +214084,7 @@ declare function assetIcon(id: string, opts?: { color?: string }): { readonly sv
|
|
|
214033
214084
|
declare function font(id: string, opts?: { weights?: number[], italic?: boolean }): string
|
|
214034
214085
|
`, README_HINT = `# design/
|
|
214035
214086
|
|
|
214036
|
-
|
|
214087
|
+
le.codes Design canvas — run \`lecodes design\` here and open the printed URL.
|
|
214037
214088
|
See CLAUDE.md for the conventions (it's written for AI assistants, but it's the spec).
|
|
214038
214089
|
`;
|
|
214039
214090
|
var init_templates = __esm(() => {
|
|
@@ -214114,7 +214165,7 @@ var firstLine = (body, max = 64) => {
|
|
|
214114
214165
|
const sync3 = flags["no-sync"] ? null : startCommentSync(ctx, designServer);
|
|
214115
214166
|
log(`${c.bold("LeCodes Design")} — ${ctx.name}`);
|
|
214116
214167
|
log(`Canvas: ${c.bold(server2.url)}`);
|
|
214117
|
-
log(`MCP: ${c.bold(server2.url + "/mcp")}${renderer ? "" : c.dim(" (render_screen off —
|
|
214168
|
+
log(`MCP: ${c.bold(server2.url + "/mcp")}${renderer ? "" : c.dim(" (render_screen off — lecodes-headless is not installed: `lecodes render` installs it)")}`);
|
|
214118
214169
|
log(`Watching: ${ctx.dirName}/ (screens hot-reload on save; Ctrl+C to stop)`);
|
|
214119
214170
|
if (sync3)
|
|
214120
214171
|
log(`Comments: ${c.dim("in sync with the platform — replies and resolves go back automatically")}`);
|
|
@@ -215391,8 +215442,8 @@ var require_main = __commonJS((exports, module) => {
|
|
|
215391
215442
|
// src/index.ts
|
|
215392
215443
|
init_package();
|
|
215393
215444
|
init_cli();
|
|
215394
|
-
import { existsSync as
|
|
215395
|
-
import { delimiter as delimiter3, dirname as
|
|
215445
|
+
import { existsSync as existsSync53, realpathSync as realpathSync2 } from "node:fs";
|
|
215446
|
+
import { delimiter as delimiter3, dirname as dirname27, join as join53 } from "node:path";
|
|
215396
215447
|
|
|
215397
215448
|
// src/commands/app/index.ts
|
|
215398
215449
|
init_manifest();
|
|
@@ -222047,8 +222098,7 @@ var desktop_default = defineCommand({
|
|
|
222047
222098
|
|
|
222048
222099
|
// src/commands/dev.ts
|
|
222049
222100
|
init_manifest();
|
|
222050
|
-
|
|
222051
|
-
import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync26, watch, writeFileSync as writeFileSync24 } from "node:fs";
|
|
222101
|
+
import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync26, writeFileSync as writeFileSync24 } from "node:fs";
|
|
222052
222102
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
222053
222103
|
import { networkInterfaces } from "node:os";
|
|
222054
222104
|
import { basename as basename13, join as join35 } from "node:path";
|
|
@@ -222457,6 +222507,7 @@ var startDevServer = async (options) => {
|
|
|
222457
222507
|
` + renderPrelude({ origin: reqOrigin, base, transport, v: v2 });
|
|
222458
222508
|
};
|
|
222459
222509
|
const web = options.web ?? null;
|
|
222510
|
+
const lite = options.lite ?? null;
|
|
222460
222511
|
const sendBuilt = (res, dir, rel) => {
|
|
222461
222512
|
if (rel.split("/").some((seg) => seg === ".." || seg === "" || seg === "."))
|
|
222462
222513
|
return sendText(res, 400, "text/plain", "Bad path");
|
|
@@ -222518,6 +222569,18 @@ var startDevServer = async (options) => {
|
|
|
222518
222569
|
return redirect(`${base}/web/${query()}`);
|
|
222519
222570
|
return sendBuilt(res, web.page, rel === "/" ? "index.html" : rel.slice(1));
|
|
222520
222571
|
}
|
|
222572
|
+
if (route === "/lite" || route.startsWith("/lite/")) {
|
|
222573
|
+
if (!lite)
|
|
222574
|
+
return sendText(res, 404, "text/plain", "No Web Lite page with this CLI (lecodes-web-lite's dist).");
|
|
222575
|
+
const rel = route.slice("/lite".length);
|
|
222576
|
+
if (rel === "" || rel === "/" && !url.searchParams.has("dev")) {
|
|
222577
|
+
url.searchParams.set("dev", "..");
|
|
222578
|
+
res.writeHead(302, { location: `${base}/lite/?${url.searchParams.toString()}${url.hash}` });
|
|
222579
|
+
res.end();
|
|
222580
|
+
return;
|
|
222581
|
+
}
|
|
222582
|
+
return sendBuilt(res, lite, rel === "/" ? "index.html" : rel.slice(1));
|
|
222583
|
+
}
|
|
222521
222584
|
if (route === "/version") {
|
|
222522
222585
|
return sendJson(res, 200, { v });
|
|
222523
222586
|
}
|
|
@@ -222601,6 +222664,7 @@ var startDevServer = async (options) => {
|
|
|
222601
222664
|
return {
|
|
222602
222665
|
url: `${origin}${base}/dev.js`,
|
|
222603
222666
|
webUrl: web ? `${origin}${base}/web/` : null,
|
|
222667
|
+
liteUrl: lite ? `${origin}${base}/lite/` : null,
|
|
222604
222668
|
base,
|
|
222605
222669
|
port: boundPort,
|
|
222606
222670
|
v: () => v,
|
|
@@ -222636,6 +222700,15 @@ var PAGE = {
|
|
|
222636
222700
|
build: ["-c", "vite.page.config.ts"],
|
|
222637
222701
|
sources: ["hosts/web-frame/src", "hosts/web-frame/page", "hosts/web-player/src/protocol.ts"]
|
|
222638
222702
|
};
|
|
222703
|
+
var LITE = {
|
|
222704
|
+
pkg: "lecodes-web-lite",
|
|
222705
|
+
dist: "dist",
|
|
222706
|
+
entry: "lite.js",
|
|
222707
|
+
vendored: "web-lite",
|
|
222708
|
+
build: [],
|
|
222709
|
+
sources: ["hosts/web-lite/src", "hosts/web-lite/index.html", "renderers/web-lite/src", "renderers/web-shared/src", "hosts/web-player/src/dev.ts"]
|
|
222710
|
+
};
|
|
222711
|
+
var resolveLiteRunner = (log2 = () => {}) => resolve16(LITE, log2);
|
|
222639
222712
|
var resolveWebRunner = (log2 = () => {}) => {
|
|
222640
222713
|
const player = resolve16(PLAYER, log2);
|
|
222641
222714
|
return player ? { player, page: resolve16(PAGE, log2) } : null;
|
|
@@ -222689,6 +222762,59 @@ var newest = (paths) => {
|
|
|
222689
222762
|
return latest;
|
|
222690
222763
|
};
|
|
222691
222764
|
|
|
222765
|
+
// src/dev/watchProject.ts
|
|
222766
|
+
init_ignore();
|
|
222767
|
+
init_paths();
|
|
222768
|
+
import { watch } from "node:fs";
|
|
222769
|
+
var DEBOUNCE_MS = 120;
|
|
222770
|
+
var SKIP_DIRS2 = new Set([".lecodes", ".git", "node_modules", "dist"]);
|
|
222771
|
+
var SKIP_FILES2 = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
|
|
222772
|
+
var watchProject = (root, onChange, log2) => {
|
|
222773
|
+
const ignore = loadIgnore(root);
|
|
222774
|
+
const counts = (file) => {
|
|
222775
|
+
if (file === null)
|
|
222776
|
+
return true;
|
|
222777
|
+
const parts = file.replace(/\\/g, "/").split("/");
|
|
222778
|
+
if (parts.some((p2) => SKIP_DIRS2.has(p2)))
|
|
222779
|
+
return false;
|
|
222780
|
+
if (SKIP_FILES2.has(parts[parts.length - 1]))
|
|
222781
|
+
return false;
|
|
222782
|
+
try {
|
|
222783
|
+
if (ignore(toKey(file), false))
|
|
222784
|
+
return false;
|
|
222785
|
+
} catch {
|
|
222786
|
+
return false;
|
|
222787
|
+
}
|
|
222788
|
+
return true;
|
|
222789
|
+
};
|
|
222790
|
+
let timer = null;
|
|
222791
|
+
let watcher = null;
|
|
222792
|
+
try {
|
|
222793
|
+
watcher = watch(root, { recursive: true }, (_type, file) => {
|
|
222794
|
+
const name = file === null ? null : String(file);
|
|
222795
|
+
log2.debug?.(`[watch] event: ${name ?? "(null)"} → ${counts(name) ? "recompile" : "skip"}`);
|
|
222796
|
+
if (!counts(name))
|
|
222797
|
+
return;
|
|
222798
|
+
if (timer)
|
|
222799
|
+
clearTimeout(timer);
|
|
222800
|
+
timer = setTimeout(() => {
|
|
222801
|
+
timer = null;
|
|
222802
|
+
onChange();
|
|
222803
|
+
}, DEBOUNCE_MS);
|
|
222804
|
+
});
|
|
222805
|
+
} catch {
|
|
222806
|
+
log2.warn("fs.watch(recursive) unavailable on this platform — hot reload disabled (restart to pick up changes).");
|
|
222807
|
+
}
|
|
222808
|
+
watcher?.on("error", (e2) => log2.warn(`Watcher error — hot reload may have stopped (restart the command): ${e2.message}`));
|
|
222809
|
+
return {
|
|
222810
|
+
close() {
|
|
222811
|
+
if (timer)
|
|
222812
|
+
clearTimeout(timer);
|
|
222813
|
+
watcher?.close();
|
|
222814
|
+
}
|
|
222815
|
+
};
|
|
222816
|
+
};
|
|
222817
|
+
|
|
222692
222818
|
// src/commands/dev.ts
|
|
222693
222819
|
init_browserAuth();
|
|
222694
222820
|
|
|
@@ -222982,11 +223108,7 @@ var launchOnIos = (opts) => {
|
|
|
222982
223108
|
// src/commands/dev.ts
|
|
222983
223109
|
init_cli();
|
|
222984
223110
|
init_shared();
|
|
222985
|
-
init_paths();
|
|
222986
223111
|
var DEFAULT_PORT2 = 4499;
|
|
222987
|
-
var DEBOUNCE_MS = 120;
|
|
222988
|
-
var WATCH_SKIP_DIRS = new Set([".lecodes", ".git", "node_modules"]);
|
|
222989
|
-
var WATCH_SKIP_FILES = new Set(["tsconfig.json", "jsconfig.json", ".gitignore", ".lecodesignore", ".DS_Store"]);
|
|
222990
223112
|
var VIRTUAL_IFACE = /wsl|docker|hyper-v|vethernet|virtualbox|vmware|tap|tun|zerotier|tailscale|loopback/i;
|
|
222991
223113
|
var lanCandidates = () => {
|
|
222992
223114
|
const nets = networkInterfaces();
|
|
@@ -223106,54 +223228,18 @@ var dev_default = defineCommand({
|
|
|
223106
223228
|
warnErr(`Compile failed — fix it and save; devices connect once it compiles:
|
|
223107
223229
|
${first.error}`);
|
|
223108
223230
|
}
|
|
223109
|
-
const
|
|
223110
|
-
|
|
223111
|
-
if (
|
|
223112
|
-
|
|
223113
|
-
const parts = file.replace(/\\/g, "/").split("/");
|
|
223114
|
-
if (parts.some((p2) => WATCH_SKIP_DIRS.has(p2)))
|
|
223115
|
-
return false;
|
|
223116
|
-
if (WATCH_SKIP_FILES.has(parts[parts.length - 1]))
|
|
223117
|
-
return false;
|
|
223118
|
-
try {
|
|
223119
|
-
const key = toKey(file);
|
|
223120
|
-
if (ignore(key, false))
|
|
223121
|
-
return false;
|
|
223122
|
-
} catch {
|
|
223123
|
-
return false;
|
|
223124
|
-
}
|
|
223125
|
-
return true;
|
|
223126
|
-
};
|
|
223127
|
-
const debug2 = process.env.LECODES_DEV_DEBUG === "1";
|
|
223128
|
-
let timer = null;
|
|
223129
|
-
const onChange = (file) => {
|
|
223130
|
-
if (debug2)
|
|
223131
|
-
note(`[watch] event: ${file ?? "(null)"} → ${shouldRecompile(file) ? "recompile" : "skip"}`);
|
|
223132
|
-
if (!shouldRecompile(file))
|
|
223133
|
-
return;
|
|
223134
|
-
if (timer)
|
|
223135
|
-
clearTimeout(timer);
|
|
223136
|
-
timer = setTimeout(async () => {
|
|
223137
|
-
timer = null;
|
|
223138
|
-
const result = await server2.recompile();
|
|
223139
|
-
if (!result.ok) {
|
|
223140
|
-
warnErr(`Compile failed (devices keep the last good version):
|
|
223231
|
+
const watcher = watchProject(root, async () => {
|
|
223232
|
+
const result = await server2.recompile();
|
|
223233
|
+
if (!result.ok) {
|
|
223234
|
+
warnErr(`Compile failed (devices keep the last good version):
|
|
223141
223235
|
${result.error}`);
|
|
223142
|
-
|
|
223143
|
-
|
|
223144
|
-
|
|
223145
|
-
|
|
223146
|
-
|
|
223147
|
-
|
|
223148
|
-
|
|
223149
|
-
};
|
|
223150
|
-
let watcher = null;
|
|
223151
|
-
try {
|
|
223152
|
-
watcher = watch(root, { recursive: true }, (_type, file) => onChange(file));
|
|
223153
|
-
} catch {
|
|
223154
|
-
warnErr("fs.watch(recursive) unavailable on this platform — hot reload disabled (restart to pick up changes).");
|
|
223155
|
-
}
|
|
223156
|
-
watcher?.on("error", (e2) => warnErr(`Watcher error — hot reload may have stopped (restart lecodes dev): ${e2.message}`));
|
|
223236
|
+
} else if (result.changed) {
|
|
223237
|
+
const devices = server2.clientCount();
|
|
223238
|
+
note(`✓ v${result.v} → ${devices} device${devices === 1 ? "" : "s"}`);
|
|
223239
|
+
for (const w of result.warnings)
|
|
223240
|
+
warnErr(w);
|
|
223241
|
+
}
|
|
223242
|
+
}, { warn: warnErr, debug: process.env.LECODES_DEV_DEBUG === "1" ? note : undefined });
|
|
223157
223243
|
log("");
|
|
223158
223244
|
log(`${c.bold("LeCodes Dev")} — ${name}`);
|
|
223159
223245
|
log("");
|
|
@@ -223186,14 +223272,14 @@ ${result.error}`);
|
|
|
223186
223272
|
session2 = launchOnAndroid({ port, url: androidUrl, serial: flags.serial, apk: flags.apk, packageName: flags.package });
|
|
223187
223273
|
} catch (e2) {
|
|
223188
223274
|
server2.close();
|
|
223189
|
-
watcher
|
|
223275
|
+
watcher.close();
|
|
223190
223276
|
throw e2;
|
|
223191
223277
|
}
|
|
223192
223278
|
note(`Android: ${session2.serial} → ${androidUrl} (adb reverse; device logs stream here as [device:log])`);
|
|
223193
223279
|
const finish = (code) => {
|
|
223194
223280
|
session2.stop();
|
|
223195
223281
|
server2.close();
|
|
223196
|
-
watcher
|
|
223282
|
+
watcher.close();
|
|
223197
223283
|
process.exit(code);
|
|
223198
223284
|
};
|
|
223199
223285
|
process.on("SIGINT", () => {
|
|
@@ -223228,14 +223314,14 @@ ${crashes.join(`
|
|
|
223228
223314
|
session2 = launchOnIos({ simulatorUrl: `http://127.0.0.1:${port}${path2}`, deviceUrl, udid: flags.udid, app: flags.app, bundleId: flags.bundle });
|
|
223229
223315
|
} catch (e2) {
|
|
223230
223316
|
server2.close();
|
|
223231
|
-
watcher
|
|
223317
|
+
watcher.close();
|
|
223232
223318
|
throw e2;
|
|
223233
223319
|
}
|
|
223234
223320
|
note(`iOS: ${session2.name} → ${session2.kind === "simulator" ? `http://127.0.0.1:${port}${path2}` : deviceUrl} (device logs stream here as [device:log])`);
|
|
223235
223321
|
const finish = (code) => {
|
|
223236
223322
|
session2.stop();
|
|
223237
223323
|
server2.close();
|
|
223238
|
-
watcher
|
|
223324
|
+
watcher.close();
|
|
223239
223325
|
process.exit(code);
|
|
223240
223326
|
};
|
|
223241
223327
|
process.on("SIGINT", () => {
|
|
@@ -230583,15 +230669,15 @@ var parsePlatforms = (raw) => {
|
|
|
230583
230669
|
if (raw === undefined)
|
|
230584
230670
|
return;
|
|
230585
230671
|
const known = allShaderPlatforms();
|
|
230586
|
-
const
|
|
230587
|
-
if (
|
|
230672
|
+
const asked2 = raw.split(",").map((p2) => p2.trim()).filter(Boolean);
|
|
230673
|
+
if (asked2.length === 0)
|
|
230588
230674
|
throw new CliError(`--platform needs at least one target. Known: ${known.join(", ")}`);
|
|
230589
|
-
const bad =
|
|
230675
|
+
const bad = asked2.filter((p2) => !known.includes(p2));
|
|
230590
230676
|
if (bad.length > 0) {
|
|
230591
230677
|
throw new CliError(`Unknown shader platform${bad.length > 1 ? "s" : ""}: ${bad.join(", ")}
|
|
230592
230678
|
Known: ${known.join(", ")}`);
|
|
230593
230679
|
}
|
|
230594
|
-
return
|
|
230680
|
+
return asked2;
|
|
230595
230681
|
};
|
|
230596
230682
|
var compileShaders = async (platforms) => {
|
|
230597
230683
|
const { root, shaders } = collectShaders();
|
|
@@ -230703,8 +230789,391 @@ var status_default = defineCommand({
|
|
|
230703
230789
|
});
|
|
230704
230790
|
|
|
230705
230791
|
// src/commands/test.ts
|
|
230706
|
-
import { existsSync as
|
|
230707
|
-
import { dirname as
|
|
230792
|
+
import { existsSync as existsSync48, mkdirSync as mkdirSync32, readFileSync as readFileSync40, readdirSync as readdirSync21 } from "node:fs";
|
|
230793
|
+
import { dirname as dirname24, resolve as resolve28 } from "node:path";
|
|
230794
|
+
init_manifest();
|
|
230795
|
+
|
|
230796
|
+
// src/hosts/liteRunner.ts
|
|
230797
|
+
init_cli();
|
|
230798
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
230799
|
+
import { existsSync as existsSync47, mkdirSync as mkdirSync31, writeFileSync as writeFileSync34 } from "node:fs";
|
|
230800
|
+
import { createServer as createServer3 } from "node:net";
|
|
230801
|
+
import { dirname as dirname23, join as join48 } from "node:path";
|
|
230802
|
+
import { createInterface as createInterface2 } from "node:readline";
|
|
230803
|
+
init_distRoot();
|
|
230804
|
+
init_peers();
|
|
230805
|
+
var FRAME_MS2 = 16;
|
|
230806
|
+
var LOG_SLACK_MS = 3000;
|
|
230807
|
+
var NO_NODE = "--lite needs Node on the PATH: the browser is driven from a Node process.";
|
|
230808
|
+
var NO_PLAYWRIGHT = "--lite runs the flows in a browser and needs Playwright: `npm install -D playwright` in the project, then `npx playwright install chromium` once.";
|
|
230809
|
+
var startDriver = async () => {
|
|
230810
|
+
const script = join48(distRoot, "src", "hosts", "liteBrowser.mjs");
|
|
230811
|
+
if (!existsSync47(script))
|
|
230812
|
+
throw new CliError("--lite is not available in this build of the CLI (its browser driver ships with the npm package).");
|
|
230813
|
+
const child = spawn6("node", [script], { stdio: ["pipe", "pipe", "inherit"] });
|
|
230814
|
+
const waiting = new Map;
|
|
230815
|
+
let onConsole = () => {};
|
|
230816
|
+
let next = 1;
|
|
230817
|
+
let dead = null;
|
|
230818
|
+
const fail = (error) => {
|
|
230819
|
+
dead = dead ?? error;
|
|
230820
|
+
for (const w of waiting.values())
|
|
230821
|
+
w.reject(dead);
|
|
230822
|
+
waiting.clear();
|
|
230823
|
+
};
|
|
230824
|
+
child.on("error", () => fail(new CliError(NO_NODE)));
|
|
230825
|
+
child.on("exit", () => fail(new Error("the browser's process ended")));
|
|
230826
|
+
createInterface2({ input: child.stdout }).on("line", (line) => {
|
|
230827
|
+
let message;
|
|
230828
|
+
try {
|
|
230829
|
+
message = JSON.parse(line);
|
|
230830
|
+
} catch {
|
|
230831
|
+
return;
|
|
230832
|
+
}
|
|
230833
|
+
if (message.event === "console") {
|
|
230834
|
+
onConsole(message.level ?? "log", message.text ?? "");
|
|
230835
|
+
return;
|
|
230836
|
+
}
|
|
230837
|
+
const w = message.id !== undefined ? waiting.get(message.id) : undefined;
|
|
230838
|
+
if (!w)
|
|
230839
|
+
return;
|
|
230840
|
+
waiting.delete(message.id);
|
|
230841
|
+
if (message.ok)
|
|
230842
|
+
w.resolve(message.value);
|
|
230843
|
+
else
|
|
230844
|
+
w.reject(new Error(message.error ?? "failed"));
|
|
230845
|
+
});
|
|
230846
|
+
const call = (op, args = {}) => new Promise((resolve28, reject) => {
|
|
230847
|
+
if (dead) {
|
|
230848
|
+
reject(dead);
|
|
230849
|
+
return;
|
|
230850
|
+
}
|
|
230851
|
+
const id = next++;
|
|
230852
|
+
waiting.set(id, { resolve: resolve28, reject });
|
|
230853
|
+
child.stdin.write(JSON.stringify({ id, op, args }) + `
|
|
230854
|
+
`);
|
|
230855
|
+
});
|
|
230856
|
+
const from = [
|
|
230857
|
+
join48(process.cwd(), "package.json"),
|
|
230858
|
+
join48(distRoot, "package.json"),
|
|
230859
|
+
join48(peersDir(), "package.json"),
|
|
230860
|
+
join48(distRoot, "..", "..", "engines", "gl-lite", "package.json")
|
|
230861
|
+
];
|
|
230862
|
+
try {
|
|
230863
|
+
await call("launch", { from });
|
|
230864
|
+
} catch (e2) {
|
|
230865
|
+
child.kill();
|
|
230866
|
+
if (e2 instanceof CliError)
|
|
230867
|
+
throw e2;
|
|
230868
|
+
const message = e2 instanceof Error ? e2.message : String(e2);
|
|
230869
|
+
throw new CliError(message === "NO_PLAYWRIGHT" ? NO_PLAYWRIGHT : `Chromium did not start (${message}) — run "npx playwright install chromium" once.`);
|
|
230870
|
+
}
|
|
230871
|
+
return {
|
|
230872
|
+
open(width, height, listener) {
|
|
230873
|
+
onConsole = listener;
|
|
230874
|
+
return call("open", { width, height });
|
|
230875
|
+
},
|
|
230876
|
+
goto: (url) => call("goto", { url }),
|
|
230877
|
+
waitFor: (expression, timeout) => call("waitFor", { expression, timeout }),
|
|
230878
|
+
evaluate: (expression) => call("evaluate", { expression }),
|
|
230879
|
+
timeout: (ms) => call("timeout", { ms }),
|
|
230880
|
+
mouse: (action, x2, y2) => call("mouse", { action, x: x2, y: y2 }),
|
|
230881
|
+
keyboard: (action, text2) => call("keyboard", { action, text: text2 }),
|
|
230882
|
+
screenshot: (path2, clip) => call("screenshot", { path: path2, clip }),
|
|
230883
|
+
async close() {
|
|
230884
|
+
await call("close").catch(() => {});
|
|
230885
|
+
child.kill();
|
|
230886
|
+
}
|
|
230887
|
+
};
|
|
230888
|
+
};
|
|
230889
|
+
var freePort = () => new Promise((resolve28, reject) => {
|
|
230890
|
+
const probe = createServer3();
|
|
230891
|
+
probe.once("error", reject);
|
|
230892
|
+
probe.listen(0, "127.0.0.1", () => {
|
|
230893
|
+
const address = probe.address();
|
|
230894
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
230895
|
+
probe.close(() => resolve28(port));
|
|
230896
|
+
});
|
|
230897
|
+
});
|
|
230898
|
+
var startLiteSession = async (options) => {
|
|
230899
|
+
const port = await freePort();
|
|
230900
|
+
const server2 = await startDevServer({
|
|
230901
|
+
root: options.root,
|
|
230902
|
+
manifest: options.manifest,
|
|
230903
|
+
name: options.name,
|
|
230904
|
+
lite: options.lite,
|
|
230905
|
+
origin: `http://127.0.0.1:${port}`,
|
|
230906
|
+
bind: "127.0.0.1",
|
|
230907
|
+
port,
|
|
230908
|
+
token: "test",
|
|
230909
|
+
entryOverride: options.entry
|
|
230910
|
+
});
|
|
230911
|
+
const compiled = await server2.recompile();
|
|
230912
|
+
if (!compiled.ok) {
|
|
230913
|
+
server2.close();
|
|
230914
|
+
throw new CliError(`Compile failed:
|
|
230915
|
+
${compiled.error}`);
|
|
230916
|
+
}
|
|
230917
|
+
let driver;
|
|
230918
|
+
try {
|
|
230919
|
+
driver = await startDriver();
|
|
230920
|
+
} catch (e2) {
|
|
230921
|
+
server2.close();
|
|
230922
|
+
throw e2;
|
|
230923
|
+
}
|
|
230924
|
+
return {
|
|
230925
|
+
run: (plan, runOptions) => runPlan(driver, `${server2.liteUrl}?dev=..&test`, plan, runOptions),
|
|
230926
|
+
async close() {
|
|
230927
|
+
await driver.close();
|
|
230928
|
+
server2.close();
|
|
230929
|
+
}
|
|
230930
|
+
};
|
|
230931
|
+
};
|
|
230932
|
+
var parseArgs2 = (rest) => {
|
|
230933
|
+
const out = [];
|
|
230934
|
+
let i2 = 0;
|
|
230935
|
+
while (i2 < rest.length) {
|
|
230936
|
+
if (rest[i2] === " ") {
|
|
230937
|
+
i2++;
|
|
230938
|
+
continue;
|
|
230939
|
+
}
|
|
230940
|
+
if (rest[i2] === '"') {
|
|
230941
|
+
let text2 = "";
|
|
230942
|
+
i2++;
|
|
230943
|
+
while (i2 < rest.length && rest[i2] !== '"') {
|
|
230944
|
+
if (rest[i2] === "\\")
|
|
230945
|
+
i2++;
|
|
230946
|
+
text2 += rest[i2] ?? "";
|
|
230947
|
+
i2++;
|
|
230948
|
+
}
|
|
230949
|
+
i2++;
|
|
230950
|
+
out.push(text2);
|
|
230951
|
+
continue;
|
|
230952
|
+
}
|
|
230953
|
+
const end = rest.indexOf(" ", i2);
|
|
230954
|
+
const token = rest.slice(i2, end < 0 ? rest.length : end);
|
|
230955
|
+
const n2 = Number(token);
|
|
230956
|
+
out.push(Number.isFinite(n2) && token !== "" ? n2 : token);
|
|
230957
|
+
i2 = end < 0 ? rest.length : end;
|
|
230958
|
+
}
|
|
230959
|
+
return out;
|
|
230960
|
+
};
|
|
230961
|
+
var unwire = (text2) => text2.replace(/\\(n|\\)/g, (_, c4) => c4 === "n" ? `
|
|
230962
|
+
` : "\\");
|
|
230963
|
+
var LEVEL = { log: "DEBUG", debug: "DEBUG", info: "INFO", warning: "WARN", warn: "WARN", error: "ERROR" };
|
|
230964
|
+
var ON_SHOW = ".lc-root > .lc:not(.lc-covered)";
|
|
230965
|
+
var FRAME = "new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(() => r(null))))";
|
|
230966
|
+
var FILL_VALUE = `(f) => f.type === "email" ? "test@example.com" : f.type === "tel" ? "5550100" : f.inputMode === "numeric" || f.inputMode === "decimal" ? "1" : f.type === "date" ? "2026-01-01" : f.type === "time" ? "12:00" : "test"`;
|
|
230967
|
+
var runPlan = async (driver, url, plan, options) => {
|
|
230968
|
+
const output = [];
|
|
230969
|
+
const seen = [];
|
|
230970
|
+
const say = (line) => {
|
|
230971
|
+
output.push(line);
|
|
230972
|
+
};
|
|
230973
|
+
await driver.open(options.width, options.height, (level, text2) => {
|
|
230974
|
+
seen.push(text2);
|
|
230975
|
+
say(`[${LEVEL[level] ?? "DEBUG"}] ${text2}`);
|
|
230976
|
+
options.onConsole?.(level, text2);
|
|
230977
|
+
});
|
|
230978
|
+
const expectOf = new Map;
|
|
230979
|
+
for (const step of plan.steps)
|
|
230980
|
+
if (step.expectLog !== undefined)
|
|
230981
|
+
for (const line of step.lines)
|
|
230982
|
+
expectOf.set(line, step.expectLog);
|
|
230983
|
+
const dump = () => driver.evaluate("window.lecodesTest.dump()");
|
|
230984
|
+
const rectOf = (id) => driver.evaluate(`window.lecodesTest.rect(${id})`);
|
|
230985
|
+
const nodeOf = async (name) => {
|
|
230986
|
+
const node = findDumpTarget(parseTreeDump(await dump()), name);
|
|
230987
|
+
if (!node)
|
|
230988
|
+
throw new Error(`no element "${name}" on the screen`);
|
|
230989
|
+
return node;
|
|
230990
|
+
};
|
|
230991
|
+
const pointOf = async (args, at2) => {
|
|
230992
|
+
const first = args[at2];
|
|
230993
|
+
if (typeof first === "number")
|
|
230994
|
+
return { point: [first, Number(args[at2 + 1])], next: at2 + 2 };
|
|
230995
|
+
const rect = await rectOf((await nodeOf(String(first))).id);
|
|
230996
|
+
if (!rect || rect[2] === 0 || rect[3] === 0)
|
|
230997
|
+
throw new Error(`"${first}" is not laid out`);
|
|
230998
|
+
return { point: [rect[0] + rect[2] / 2, rect[1] + rect[3] / 2], next: at2 + 1 };
|
|
230999
|
+
};
|
|
231000
|
+
const frame = () => driver.evaluate(FRAME);
|
|
231001
|
+
const waitFor = async (ms, line) => {
|
|
231002
|
+
const expected = expectOf.get(line);
|
|
231003
|
+
if (expected === undefined) {
|
|
231004
|
+
await driver.timeout(ms);
|
|
231005
|
+
return;
|
|
231006
|
+
}
|
|
231007
|
+
const until = Date.now() + ms + LOG_SLACK_MS;
|
|
231008
|
+
while (Date.now() < until) {
|
|
231009
|
+
if (seen.some((text2) => text2.includes(expected)))
|
|
231010
|
+
return;
|
|
231011
|
+
await driver.timeout(FRAME_MS2);
|
|
231012
|
+
}
|
|
231013
|
+
};
|
|
231014
|
+
let clip = null;
|
|
231015
|
+
const play = async (op, rest, line) => {
|
|
231016
|
+
const args = parseArgs2(rest);
|
|
231017
|
+
switch (op) {
|
|
231018
|
+
case "frames":
|
|
231019
|
+
await waitFor(Math.max(0, Number(args[0])) * FRAME_MS2, line);
|
|
231020
|
+
return;
|
|
231021
|
+
case "wait":
|
|
231022
|
+
await waitFor(Math.max(0, Number(args[0])), line);
|
|
231023
|
+
return;
|
|
231024
|
+
case "tap": {
|
|
231025
|
+
const { point } = await pointOf(args, 0);
|
|
231026
|
+
await driver.mouse("move", point[0], point[1]);
|
|
231027
|
+
await driver.mouse("down");
|
|
231028
|
+
await driver.mouse("up");
|
|
231029
|
+
await frame();
|
|
231030
|
+
return;
|
|
231031
|
+
}
|
|
231032
|
+
case "hold": {
|
|
231033
|
+
const { point, next } = await pointOf(args, 0);
|
|
231034
|
+
await driver.mouse("move", point[0], point[1]);
|
|
231035
|
+
await driver.mouse("down");
|
|
231036
|
+
await driver.timeout(Number(args[next]) || 0);
|
|
231037
|
+
await driver.mouse("up");
|
|
231038
|
+
await frame();
|
|
231039
|
+
return;
|
|
231040
|
+
}
|
|
231041
|
+
case "drag": {
|
|
231042
|
+
const from = await pointOf(args, 0);
|
|
231043
|
+
const to2 = await pointOf(args, from.next);
|
|
231044
|
+
const ms = Number(args[to2.next]) || 300;
|
|
231045
|
+
const steps = Math.max(2, Math.round(ms / FRAME_MS2));
|
|
231046
|
+
await driver.mouse("move", from.point[0], from.point[1]);
|
|
231047
|
+
await driver.mouse("down");
|
|
231048
|
+
for (let i2 = 1;i2 <= steps; i2++) {
|
|
231049
|
+
const t2 = i2 / steps;
|
|
231050
|
+
await driver.mouse("move", from.point[0] + (to2.point[0] - from.point[0]) * t2, from.point[1] + (to2.point[1] - from.point[1]) * t2);
|
|
231051
|
+
await driver.timeout(ms / steps);
|
|
231052
|
+
}
|
|
231053
|
+
await driver.mouse("up");
|
|
231054
|
+
await frame();
|
|
231055
|
+
return;
|
|
231056
|
+
}
|
|
231057
|
+
case "mouseto": {
|
|
231058
|
+
const { point } = await pointOf(args, 0);
|
|
231059
|
+
await driver.mouse("move", point[0], point[1]);
|
|
231060
|
+
await frame();
|
|
231061
|
+
return;
|
|
231062
|
+
}
|
|
231063
|
+
case "type":
|
|
231064
|
+
await driver.keyboard("type", unwire(rest));
|
|
231065
|
+
await frame();
|
|
231066
|
+
return;
|
|
231067
|
+
case "fill": {
|
|
231068
|
+
const value = rest === "" ? undefined : unwire(rest);
|
|
231069
|
+
const count = await driver.evaluate(`(() => {
|
|
231070
|
+
window.__lecodesFill = [...document.querySelectorAll("${ON_SHOW} input.lc, ${ON_SHOW} textarea.lc")].filter((f) => f.value === "")
|
|
231071
|
+
return window.__lecodesFill.length
|
|
231072
|
+
})()`);
|
|
231073
|
+
for (let i2 = 0;i2 < count; i2++) {
|
|
231074
|
+
const text2 = value ?? await driver.evaluate(`(${FILL_VALUE})(window.__lecodesFill[${i2}])`);
|
|
231075
|
+
await driver.evaluate(`window.__lecodesFill[${i2}].focus()`);
|
|
231076
|
+
await driver.keyboard("type", text2);
|
|
231077
|
+
}
|
|
231078
|
+
await frame();
|
|
231079
|
+
return;
|
|
231080
|
+
}
|
|
231081
|
+
case "scroll": {
|
|
231082
|
+
const by = Number(args[0]);
|
|
231083
|
+
const name = typeof args[1] === "string" ? args[1] : null;
|
|
231084
|
+
const target = name === null ? `document.querySelector("${ON_SHOW} .lc-scrollable, ${ON_SHOW} .lc-vlist")` : `document.querySelector('[data-node="${(await nodeOf(name)).id}"]')`;
|
|
231085
|
+
const found = await driver.evaluate(`(() => {
|
|
231086
|
+
const el = ${target}
|
|
231087
|
+
if (!el) return false
|
|
231088
|
+
const style = getComputedStyle(el)
|
|
231089
|
+
if (style.overflowX !== "hidden" && style.overflowY === "hidden") el.scrollLeft += ${by}
|
|
231090
|
+
else el.scrollTop += ${by}
|
|
231091
|
+
return true
|
|
231092
|
+
})()`);
|
|
231093
|
+
if (!found)
|
|
231094
|
+
throw new Error(name === null ? "no scrollable on the screen" : `"${name}" is not there`);
|
|
231095
|
+
await frame();
|
|
231096
|
+
return;
|
|
231097
|
+
}
|
|
231098
|
+
case "key": {
|
|
231099
|
+
await driver.keyboard("down", String(args[0]));
|
|
231100
|
+
await driver.timeout(Number(args[1]) || 0);
|
|
231101
|
+
await driver.keyboard("up", String(args[0]));
|
|
231102
|
+
return;
|
|
231103
|
+
}
|
|
231104
|
+
case "keydown":
|
|
231105
|
+
await driver.keyboard("down", String(args[0]));
|
|
231106
|
+
return;
|
|
231107
|
+
case "keyup":
|
|
231108
|
+
await driver.keyboard("up", String(args[0]));
|
|
231109
|
+
return;
|
|
231110
|
+
case "wheel":
|
|
231111
|
+
await driver.mouse("wheel", Number(args[0]) * 100, Number(args[1]) * 100);
|
|
231112
|
+
await frame();
|
|
231113
|
+
return;
|
|
231114
|
+
case "dump": {
|
|
231115
|
+
const file = rest.trim();
|
|
231116
|
+
mkdirSync31(dirname23(file), { recursive: true });
|
|
231117
|
+
writeFileSync34(file, await dump());
|
|
231118
|
+
return;
|
|
231119
|
+
}
|
|
231120
|
+
case "clip": {
|
|
231121
|
+
if (rest.trim() === "off") {
|
|
231122
|
+
clip = null;
|
|
231123
|
+
return;
|
|
231124
|
+
}
|
|
231125
|
+
if (typeof args[0] === "number") {
|
|
231126
|
+
clip = { x: args[0], y: Number(args[1]), width: Number(args[2]), height: Number(args[3]) };
|
|
231127
|
+
return;
|
|
231128
|
+
}
|
|
231129
|
+
const rect = await rectOf((await nodeOf(String(args[0]))).id);
|
|
231130
|
+
if (!rect)
|
|
231131
|
+
throw new Error(`"${args[0]}" is not laid out`);
|
|
231132
|
+
clip = { x: rect[0], y: rect[1], width: rect[2], height: rect[3] };
|
|
231133
|
+
return;
|
|
231134
|
+
}
|
|
231135
|
+
case "shot": {
|
|
231136
|
+
const file = rest.trim();
|
|
231137
|
+
mkdirSync31(dirname23(file), { recursive: true });
|
|
231138
|
+
await driver.screenshot(file, clip);
|
|
231139
|
+
return;
|
|
231140
|
+
}
|
|
231141
|
+
default:
|
|
231142
|
+
throw new Error(`"${op}" is not available on Web Lite (it has no 3D scene, no gamepad)`);
|
|
231143
|
+
}
|
|
231144
|
+
};
|
|
231145
|
+
try {
|
|
231146
|
+
await driver.goto(url);
|
|
231147
|
+
await driver.waitFor("window.lecodesTest && window.lecodesTest.ready === true", 20000);
|
|
231148
|
+
say("[script] loaded");
|
|
231149
|
+
const lines = plan.text.split(`
|
|
231150
|
+
`);
|
|
231151
|
+
for (let i2 = 0;i2 < lines.length; i2++) {
|
|
231152
|
+
const text2 = lines[i2].trim();
|
|
231153
|
+
if (text2 === "" || text2.startsWith("#"))
|
|
231154
|
+
continue;
|
|
231155
|
+
const space = text2.indexOf(" ");
|
|
231156
|
+
const op = space < 0 ? text2 : text2.slice(0, space);
|
|
231157
|
+
const rest = space < 0 ? "" : text2.slice(space + 1);
|
|
231158
|
+
try {
|
|
231159
|
+
await play(op, rest, i2 + 1);
|
|
231160
|
+
say(`[script] #${i2 + 1} done ${op}`);
|
|
231161
|
+
} catch (e2) {
|
|
231162
|
+
say(`[script] #${i2 + 1} error ${op}: ${e2 instanceof Error ? e2.message.split(`
|
|
231163
|
+
`)[0] : String(e2)}`);
|
|
231164
|
+
break;
|
|
231165
|
+
}
|
|
231166
|
+
}
|
|
231167
|
+
say("[script] end");
|
|
231168
|
+
} catch (e2) {
|
|
231169
|
+
say(`[ERROR] ${e2 instanceof Error ? e2.message.split(`
|
|
231170
|
+
`)[0] : String(e2)}`);
|
|
231171
|
+
}
|
|
231172
|
+
return output.join(`
|
|
231173
|
+
`);
|
|
231174
|
+
};
|
|
231175
|
+
|
|
231176
|
+
// src/commands/test.ts
|
|
230708
231177
|
init_manifest();
|
|
230709
231178
|
init_cli();
|
|
230710
231179
|
init_shared();
|
|
@@ -230782,7 +231251,7 @@ var readFlowFile = (abs, displayName) => {
|
|
|
230782
231251
|
name: file.name ?? displayName,
|
|
230783
231252
|
kind: "file",
|
|
230784
231253
|
steps: file.steps,
|
|
230785
|
-
fixtures: resolveFixtures(file.fixtures,
|
|
231254
|
+
fixtures: resolveFixtures(file.fixtures, dirname24(abs)),
|
|
230786
231255
|
width: file.width,
|
|
230787
231256
|
height: file.height,
|
|
230788
231257
|
settle: file.settle,
|
|
@@ -230802,6 +231271,7 @@ var testFlags = {
|
|
|
230802
231271
|
...viewportFlags,
|
|
230803
231272
|
...bundleFlags,
|
|
230804
231273
|
...desktopRenderFlags,
|
|
231274
|
+
lite: bool("run the file scenarios on Web Lite, in a browser (needs Playwright): the same steps, targets and expects; the clock is the browser's, so a step that expects a log line waits for it; design flows stay headless"),
|
|
230805
231275
|
desktop: bool("run the file scenarios through the NATIVE host: the same steps, targets and expects as headless (expect.audio and the json output excepted); design flows stay headless. --frames / --fixed-dt as in render --desktop")
|
|
230806
231276
|
};
|
|
230807
231277
|
var test_default = defineCommand({
|
|
@@ -230817,6 +231287,11 @@ Design flows read tests/fixtures.json automatically (canned fetch responses) so
|
|
|
230817
231287
|
});
|
|
230818
231288
|
var runTests = async (args, flags) => {
|
|
230819
231289
|
const sim = flags.sim;
|
|
231290
|
+
const lite = flags.lite;
|
|
231291
|
+
if (lite && (flags.desktop || sim))
|
|
231292
|
+
throw new CliError("--lite is a host of its own — drop --desktop / --sim.");
|
|
231293
|
+
if (lite && flags.flows)
|
|
231294
|
+
throw new CliError("--flows needs the headless renderer (design flows read expect.screen and fill inputs) — drop --lite.");
|
|
230820
231295
|
const desktop = flags.desktop || sim;
|
|
230821
231296
|
if (desktop && flags.flows)
|
|
230822
231297
|
throw new CliError(`--flows needs the headless renderer (design flows read expect.screen and fill inputs) — drop --${sim ? "sim" : "desktop"}.`);
|
|
@@ -230828,7 +231303,7 @@ var runTests = async (args, flags) => {
|
|
|
230828
231303
|
}
|
|
230829
231304
|
const desktopRenderer = desktop ? resolveDesktopRenderer(desktopRoot, flags.renderer) : undefined;
|
|
230830
231305
|
const native = desktop ? await buildBundle(bundleOptions(flags), { localAssets: true, desktopRenderer }) : null;
|
|
230831
|
-
const headless = desktop ? null : await compileHeadlessBundle({ entry: flags.entry, publicUrl: flags["public-url"], remoteAssets: flags["remote-assets"], waitNetwork: flags["wait-network"] });
|
|
231306
|
+
const headless = desktop || lite ? null : await compileHeadlessBundle({ entry: flags.entry, publicUrl: flags["public-url"], remoteAssets: flags["remote-assets"], waitNetwork: flags["wait-network"] });
|
|
230832
231307
|
const warnings = headless?.warnings ?? [];
|
|
230833
231308
|
const useLocalAssets = headless?.useLocalAssets ?? false;
|
|
230834
231309
|
const js = headless?.js ?? "";
|
|
@@ -230842,7 +231317,7 @@ var runTests = async (args, flags) => {
|
|
|
230842
231317
|
root = process.cwd();
|
|
230843
231318
|
}
|
|
230844
231319
|
}
|
|
230845
|
-
const renderer = desktop ? null : await requireRenderer();
|
|
231320
|
+
const renderer = desktop || lite ? null : await requireRenderer();
|
|
230846
231321
|
const nativeFrames = flags.frames || 60;
|
|
230847
231322
|
const nativeFixedDt = flags["fixed-dt"] ?? 16;
|
|
230848
231323
|
if (nativeFixedDt < 0 || nativeFixedDt > 1000)
|
|
@@ -230856,28 +231331,28 @@ var runTests = async (args, flags) => {
|
|
|
230856
231331
|
if (args.length > 0) {
|
|
230857
231332
|
for (const given of args) {
|
|
230858
231333
|
const candidates = [resolve28(given), resolve28(testsDir, given), resolve28(testsDir, `${given}.flow.json`)];
|
|
230859
|
-
const abs = candidates.find((p2) =>
|
|
231334
|
+
const abs = candidates.find((p2) => existsSync48(p2));
|
|
230860
231335
|
if (!abs)
|
|
230861
231336
|
throw new CliError(`No such test: "${given}" (looked in ${testsDir}).`);
|
|
230862
231337
|
cases.push(readFlowFile(abs, given));
|
|
230863
231338
|
}
|
|
230864
|
-
} else if (
|
|
231339
|
+
} else if (existsSync48(testsDir)) {
|
|
230865
231340
|
for (const f5 of readdirSync21(testsDir).filter((f6) => f6.endsWith(".flow.json")).sort()) {
|
|
230866
231341
|
cases.push(readFlowFile(resolve28(testsDir, f5), f5));
|
|
230867
231342
|
}
|
|
230868
231343
|
}
|
|
230869
231344
|
const defaultPack = resolve28(testsDir, "fixtures.json");
|
|
230870
|
-
const flowFixtures = flags.fixtures !== undefined ? resolveFixtures(flags.fixtures, ".") :
|
|
231345
|
+
const flowFixtures = flags.fixtures !== undefined ? resolveFixtures(flags.fixtures, ".") : existsSync48(defaultPack) ? resolveFixtures("fixtures.json", testsDir) : undefined;
|
|
230871
231346
|
const flowsOnly = flags.flows;
|
|
230872
231347
|
const wantFlows = !flags["no-flows"] && args.length === 0;
|
|
230873
231348
|
if (flowsOnly)
|
|
230874
231349
|
cases.length = 0;
|
|
230875
231350
|
let skipped = [];
|
|
230876
231351
|
let entryScreen = null;
|
|
230877
|
-
if (desktop && wantFlows &&
|
|
230878
|
-
note("Design-derived flows skipped with --desktop (they read expect.screen and fill inputs; run `lecodes test` headless for them).");
|
|
231352
|
+
if ((desktop || lite) && wantFlows && existsSync48(designMetaPath)) {
|
|
231353
|
+
note("Design-derived flows skipped with --desktop / --lite (they read expect.screen and fill inputs; run `lecodes test` headless for them).");
|
|
230879
231354
|
}
|
|
230880
|
-
if (!desktop && (wantFlows || flowsOnly) &&
|
|
231355
|
+
if (!desktop && !lite && (wantFlows || flowsOnly) && existsSync48(designMetaPath)) {
|
|
230881
231356
|
let meta;
|
|
230882
231357
|
try {
|
|
230883
231358
|
meta = JSON.parse(readFileSync40(designMetaPath, "utf8"));
|
|
@@ -230917,7 +231392,7 @@ var runTests = async (args, flags) => {
|
|
|
230917
231392
|
const reportCase = (result) => {
|
|
230918
231393
|
const skippedCount = result.steps.reduce((n2, s2) => n2 + (s2.skipped?.length ?? 0), 0);
|
|
230919
231394
|
if (result.ok) {
|
|
230920
|
-
out(`${c.green("✓")} ${result.name}${skippedCount > 0 ? c.dim(` (${skippedCount}
|
|
231395
|
+
out(`${c.green("✓")} ${result.name}${skippedCount > 0 ? c.dim(` (${skippedCount} check${skippedCount === 1 ? "" : "s"} skipped on this host)`) : ""}`);
|
|
230921
231396
|
} else {
|
|
230922
231397
|
out(`${c.red("✗")} ${result.name}`);
|
|
230923
231398
|
if (result.error)
|
|
@@ -230936,8 +231411,8 @@ var runTests = async (args, flags) => {
|
|
|
230936
231411
|
const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: nativeFrames, sim });
|
|
230937
231412
|
for (const st of plan.steps)
|
|
230938
231413
|
if (st.screenshot)
|
|
230939
|
-
|
|
230940
|
-
|
|
231414
|
+
mkdirSync32(dirname24(st.screenshot), { recursive: true });
|
|
231415
|
+
mkdirSync32(caseDir, { recursive: true });
|
|
230941
231416
|
const finalPng = resolve28(caseDir, "final.png");
|
|
230942
231417
|
const { output } = await runDesktopRender(native.js, native.resources, {
|
|
230943
231418
|
renderer: desktopRenderer,
|
|
@@ -230952,7 +231427,7 @@ var runTests = async (args, flags) => {
|
|
|
230952
231427
|
script: plan.text,
|
|
230953
231428
|
fixedDtMs: nativeFixedDt
|
|
230954
231429
|
});
|
|
230955
|
-
const r2 = evaluateDesktopRun(plan, output, (path2) =>
|
|
231430
|
+
const r2 = evaluateDesktopRun(plan, output, (path2) => existsSync48(path2) ? readFileSync40(path2, "utf8") : null);
|
|
230956
231431
|
if (!r2.loaded)
|
|
230957
231432
|
throw new CliError("This desktop host doesn't support scripted input (CREATOR_SCRIPT). Update it: lecodes desktop update (or point LECODES_DESKTOP_EXE at a newer build).");
|
|
230958
231433
|
return {
|
|
@@ -230964,8 +231439,48 @@ var runTests = async (args, flags) => {
|
|
|
230964
231439
|
artifacts: r2.ok ? [] : [finalPng]
|
|
230965
231440
|
};
|
|
230966
231441
|
};
|
|
231442
|
+
let liteSession = null;
|
|
231443
|
+
if (lite) {
|
|
231444
|
+
const page = resolveLiteRunner(note);
|
|
231445
|
+
if (!page)
|
|
231446
|
+
throw new CliError("No Web Lite page found: this CLI has no runtime/web-lite (a monorepo checkout needs `bun run build` in hosts/web-lite).");
|
|
231447
|
+
let manifest = null;
|
|
231448
|
+
try {
|
|
231449
|
+
manifest = readManifest(root);
|
|
231450
|
+
} catch {}
|
|
231451
|
+
liteSession = await startLiteSession({ root, manifest, name: manifest?.name ?? "app", lite: page, entry: flags.entry });
|
|
231452
|
+
}
|
|
231453
|
+
const runLiteCase = async (kase) => {
|
|
231454
|
+
const caseDir = resolve28(artifactsRoot, slug(kase.name));
|
|
231455
|
+
const plan = translateScenario(kase.steps, { outDir: caseDir, headroomFrames: 0 });
|
|
231456
|
+
mkdirSync32(caseDir, { recursive: true });
|
|
231457
|
+
const output = await liteSession.run(plan, {
|
|
231458
|
+
width: flags.width || kase.width || projectViewport?.width || 390,
|
|
231459
|
+
height: flags.height || kase.height || projectViewport?.height || 844,
|
|
231460
|
+
onConsole
|
|
231461
|
+
});
|
|
231462
|
+
const r2 = evaluateDesktopRun(plan, output, (path2) => existsSync48(path2) ? readFileSync40(path2, "utf8") : null);
|
|
231463
|
+
return {
|
|
231464
|
+
name: kase.name,
|
|
231465
|
+
kind: kase.kind,
|
|
231466
|
+
ok: r2.ok,
|
|
231467
|
+
steps: r2.steps.map((s2) => ({ desc: s2.desc, ok: s2.ok, error: s2.error, skipped: s2.skipped.length > 0 ? s2.skipped : undefined })),
|
|
231468
|
+
error: !r2.loaded ? "the page did not load the app" : r2.ended ? undefined : "the page stopped before the script finished",
|
|
231469
|
+
artifacts: []
|
|
231470
|
+
};
|
|
231471
|
+
};
|
|
230967
231472
|
for (const kase of cases) {
|
|
230968
231473
|
let result;
|
|
231474
|
+
if (lite) {
|
|
231475
|
+
try {
|
|
231476
|
+
result = await runLiteCase(kase);
|
|
231477
|
+
} catch (e2) {
|
|
231478
|
+
result = { name: kase.name, kind: kase.kind, ok: false, steps: [], error: e2 instanceof Error ? e2.message : String(e2), artifacts: [] };
|
|
231479
|
+
}
|
|
231480
|
+
results.push(result);
|
|
231481
|
+
reportCase(result);
|
|
231482
|
+
continue;
|
|
231483
|
+
}
|
|
230969
231484
|
if (desktop) {
|
|
230970
231485
|
try {
|
|
230971
231486
|
result = await runNativeCase(kase);
|
|
@@ -231039,6 +231554,7 @@ var runTests = async (args, flags) => {
|
|
|
231039
231554
|
}, null, 2) + `
|
|
231040
231555
|
`);
|
|
231041
231556
|
}
|
|
231557
|
+
await liteSession?.close();
|
|
231042
231558
|
clearTimeout(killer);
|
|
231043
231559
|
process.exit(failed > 0 ? 1 : 0);
|
|
231044
231560
|
};
|
|
@@ -231047,9 +231563,9 @@ var runTests = async (args, flags) => {
|
|
|
231047
231563
|
init_peerInstall();
|
|
231048
231564
|
import { createRequire as createRequire8 } from "node:module";
|
|
231049
231565
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
231050
|
-
import { existsSync as
|
|
231566
|
+
import { existsSync as existsSync49, mkdtempSync as mkdtempSync6, readdirSync as readdirSync22, rmSync as rmSync13, statSync as statSync19 } from "node:fs";
|
|
231051
231567
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
231052
|
-
import { extname, join as
|
|
231568
|
+
import { extname, join as join49, relative as relative12, resolve as resolve29 } from "node:path";
|
|
231053
231569
|
init_cli();
|
|
231054
231570
|
init_paths();
|
|
231055
231571
|
var DEFAULT_SIZE = 256;
|
|
@@ -231061,13 +231577,13 @@ var findKtx = () => {
|
|
|
231061
231577
|
process.env.LECODES_KTX,
|
|
231062
231578
|
process.env.KTX_CLI,
|
|
231063
231579
|
process.env.KTX_TOKTX?.replace(/toktx(\.exe)?$/i, exe),
|
|
231064
|
-
process.platform === "win32" ?
|
|
231065
|
-
process.platform === "win32" ?
|
|
231580
|
+
process.platform === "win32" ? join49(process.env.LOCALAPPDATA ?? "", "Programs", "KTX-Software", "bin", exe) : null,
|
|
231581
|
+
process.platform === "win32" ? join49(process.env.ProgramFiles ?? "", "KTX-Software", "bin", exe) : null,
|
|
231066
231582
|
"/usr/local/bin/ktx",
|
|
231067
231583
|
"/opt/homebrew/bin/ktx"
|
|
231068
231584
|
].filter((p2) => !!p2);
|
|
231069
231585
|
for (const path2 of candidates) {
|
|
231070
|
-
if (
|
|
231586
|
+
if (existsSync49(path2))
|
|
231071
231587
|
return path2;
|
|
231072
231588
|
}
|
|
231073
231589
|
try {
|
|
@@ -231099,7 +231615,7 @@ var ktxToPng = (ktx, file, out) => {
|
|
|
231099
231615
|
run4(["extract", "--transcode", "rgba8", "--level", "0", file, out]);
|
|
231100
231616
|
} catch (e2) {
|
|
231101
231617
|
run4(["extract", "--level", "0", file, out]);
|
|
231102
|
-
if (!
|
|
231618
|
+
if (!existsSync49(out))
|
|
231103
231619
|
throw e2;
|
|
231104
231620
|
}
|
|
231105
231621
|
};
|
|
@@ -231108,7 +231624,7 @@ var collect = (dir) => {
|
|
|
231108
231624
|
for (const name of readdirSync22(dir)) {
|
|
231109
231625
|
if (name === "node_modules" || name.startsWith("."))
|
|
231110
231626
|
continue;
|
|
231111
|
-
const full =
|
|
231627
|
+
const full = join49(dir, name);
|
|
231112
231628
|
if (statSync19(full).isDirectory())
|
|
231113
231629
|
out.push(...collect(full));
|
|
231114
231630
|
else if (IMAGE_EXT.test(name))
|
|
@@ -231135,7 +231651,7 @@ var thumbs = async (targets, size, force) => {
|
|
|
231135
231651
|
if (targets.length > 0) {
|
|
231136
231652
|
for (const arg of targets) {
|
|
231137
231653
|
const abs = resolve29(process.cwd(), arg);
|
|
231138
|
-
if (!
|
|
231654
|
+
if (!existsSync49(abs))
|
|
231139
231655
|
throw new CliError(`No such file or folder: ${arg}`);
|
|
231140
231656
|
if (statSync19(abs).isDirectory())
|
|
231141
231657
|
files.push(...collect(abs));
|
|
@@ -231157,7 +231673,7 @@ var thumbs = async (targets, size, force) => {
|
|
|
231157
231673
|
const ktx = files.some((f5) => f5.toLowerCase().endsWith(".ktx2")) ? findKtx() : null;
|
|
231158
231674
|
const sharp = sharpResizer();
|
|
231159
231675
|
const resize = sharp ?? jimpResizer();
|
|
231160
|
-
const tmp = mkdtempSync6(
|
|
231676
|
+
const tmp = mkdtempSync6(join49(tmpdir6(), "lecodes-thumbs-"));
|
|
231161
231677
|
let made = 0, fresh = 0;
|
|
231162
231678
|
const skipped = [];
|
|
231163
231679
|
try {
|
|
@@ -231183,7 +231699,7 @@ var thumbs = async (targets, size, force) => {
|
|
|
231183
231699
|
skipped.push(`${asset} (KTX2 needs KTX-Software's \`ktx\` — set LECODES_KTX or put it on PATH)`);
|
|
231184
231700
|
continue;
|
|
231185
231701
|
}
|
|
231186
|
-
source =
|
|
231702
|
+
source = join49(tmp, `${i2}.png`);
|
|
231187
231703
|
ktxToPng(ktx, file, source);
|
|
231188
231704
|
}
|
|
231189
231705
|
sceneServer.writePreviewFile(root, asset, await resize(source, size));
|
|
@@ -231203,7 +231719,7 @@ var thumbs = async (targets, size, force) => {
|
|
|
231203
231719
|
if (rendered.length > 0) {
|
|
231204
231720
|
warnErr(`Models, materials and prefabs are RENDERED, not decoded — the scene editor does those: ${rendered.join(", ")}`);
|
|
231205
231721
|
}
|
|
231206
|
-
note(`Cache: ${
|
|
231722
|
+
note(`Cache: ${join49(root, ".lecodes", "preview")} (re-runs skip everything unchanged)`);
|
|
231207
231723
|
};
|
|
231208
231724
|
|
|
231209
231725
|
// src/commands/update/index.ts
|
|
@@ -231211,10 +231727,11 @@ init_package();
|
|
|
231211
231727
|
init_cli();
|
|
231212
231728
|
init_peers();
|
|
231213
231729
|
init_peerInstall();
|
|
231730
|
+
init_archives();
|
|
231214
231731
|
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
231215
|
-
import { existsSync as
|
|
231732
|
+
import { existsSync as existsSync50, readdirSync as readdirSync23 } from "node:fs";
|
|
231216
231733
|
import { homedir as homedir13 } from "node:os";
|
|
231217
|
-
import { dirname as
|
|
231734
|
+
import { dirname as dirname25, join as join50, resolve as resolve31 } from "node:path";
|
|
231218
231735
|
|
|
231219
231736
|
// src/commands/update/types.ts
|
|
231220
231737
|
init_api();
|
|
@@ -231286,9 +231803,9 @@ var findProjectDir = (start) => {
|
|
|
231286
231803
|
const home = resolve31(homedir13());
|
|
231287
231804
|
let dir = resolve31(start);
|
|
231288
231805
|
while (true) {
|
|
231289
|
-
if (dir !== home && (
|
|
231806
|
+
if (dir !== home && (existsSync50(join50(dir, "app.json")) || existsSync50(join50(dir, ".lecodes", "manifest.json")) || existsSync50(join50(dir, ".lecodes", "types"))))
|
|
231290
231807
|
return dir;
|
|
231291
|
-
const parent =
|
|
231808
|
+
const parent = dirname25(dir);
|
|
231292
231809
|
if (parent === dir)
|
|
231293
231810
|
return null;
|
|
231294
231811
|
dir = parent;
|
|
@@ -231323,7 +231840,7 @@ var semverNewer2 = (a2, b) => {
|
|
|
231323
231840
|
return 0;
|
|
231324
231841
|
};
|
|
231325
231842
|
var FROM_SOURCE = /[\\/]src[\\/]commands[\\/]/.test(import.meta.url);
|
|
231326
|
-
var STANDALONE =
|
|
231843
|
+
var STANDALONE = existsSync50(join50(dirname25(process.execPath), "runtime")) && existsSync50(join50(dirname25(process.execPath), process.platform === "win32" ? "chisel.exe" : "chisel"));
|
|
231327
231844
|
var update_default = defineCommand({
|
|
231328
231845
|
name: "update",
|
|
231329
231846
|
summary: "Move everything the project depends on to its newest published state",
|
|
@@ -231383,7 +231900,7 @@ ${c.bold(name)} ${c.dim(what)}`);
|
|
|
231383
231900
|
return ["skipped", "not inside a project"];
|
|
231384
231901
|
if (flags["no-shells"])
|
|
231385
231902
|
return ["skipped", "--no-shells"];
|
|
231386
|
-
const present = ["ios", "android"].filter((d) =>
|
|
231903
|
+
const present = ["ios", "android"].filter((d) => existsSync50(join50(root, d)) && readdirSync23(join50(root, d)).length > 0);
|
|
231387
231904
|
if (present.length === 0)
|
|
231388
231905
|
return ["skipped", "no ios/ or android/ shell in this project (`lecodes app init ios|android`)"];
|
|
231389
231906
|
await runSubcommand(app_default, "update");
|
|
@@ -231398,7 +231915,7 @@ ${c.bold(name)} ${c.dim(what)}`);
|
|
|
231398
231915
|
for (const [name, probe] of OPTIONAL_PEERS) {
|
|
231399
231916
|
const where = peerSource(name, probe);
|
|
231400
231917
|
const installed = peerVersion(name, probe);
|
|
231401
|
-
const latest = !installed ? null :
|
|
231918
|
+
const latest = !installed ? null : await archivedVersion(name) ?? await npmLatest(name);
|
|
231402
231919
|
rows.push({ name, installed, latest, where });
|
|
231403
231920
|
}
|
|
231404
231921
|
rows.push({ name: "lecodes-cli", installed: package_default.version, latest: await npmLatest("lecodes-cli"), where: "global" });
|
|
@@ -231412,7 +231929,7 @@ ${c.bold(name)} ${c.dim(what)}`);
|
|
|
231412
231929
|
if (behind.length === 0)
|
|
231413
231930
|
return "ok";
|
|
231414
231931
|
const prefixBehind = behind.filter((r2) => r2.where === "prefix");
|
|
231415
|
-
const globalSpecs = behind.filter((r2) => r2.where !== "prefix").map((r2) => r2.name + "@latest");
|
|
231932
|
+
const globalSpecs = await Promise.all(behind.filter((r2) => r2.where !== "prefix").map(async (r2) => await archiveSpec(r2.name) ?? r2.name + "@latest"));
|
|
231416
231933
|
if (flags.check) {
|
|
231417
231934
|
return ["updated", behind.length + " behind — run: lecodes update" + (globalSpecs.length ? " (global: npm install -g " + globalSpecs.join(" ") + ")" : "")];
|
|
231418
231935
|
}
|
|
@@ -231430,7 +231947,7 @@ ${c.bold(name)} ${c.dim(what)}`);
|
|
|
231430
231947
|
const r2 = spawnSync10("npm", ["install", "-g", ...toInstall], { stdio: "inherit", shell: true });
|
|
231431
231948
|
if (r2.status !== 0)
|
|
231432
231949
|
throw new CliError("npm install -g exited with " + (r2.status ?? "a signal"));
|
|
231433
|
-
done.push(...
|
|
231950
|
+
done.push(...behind.filter((r3) => r3.where !== "prefix" && (!STANDALONE || r3.name !== "lecodes-cli")).map((r3) => r3.name));
|
|
231434
231951
|
}
|
|
231435
231952
|
if (done.length === 0)
|
|
231436
231953
|
return ["skipped", "only the standalone CLI itself is behind — download the release"];
|
|
@@ -231449,11 +231966,380 @@ ${c.bold("Summary")}`);
|
|
|
231449
231966
|
}
|
|
231450
231967
|
});
|
|
231451
231968
|
|
|
231969
|
+
// src/commands/web.ts
|
|
231970
|
+
init_manifest();
|
|
231971
|
+
import { basename as basename25, relative as relative13 } from "node:path";
|
|
231972
|
+
init_browserAuth();
|
|
231973
|
+
init_cli();
|
|
231974
|
+
|
|
231975
|
+
// src/web/build.ts
|
|
231976
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
231977
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync51, mkdirSync as mkdirSync33, readFileSync as readFileSync41, readdirSync as readdirSync24, rmSync as rmSync14, statSync as statSync20, writeFileSync as writeFileSync35 } from "node:fs";
|
|
231978
|
+
import { dirname as dirname26, extname as extname2, join as join51, resolve as resolve32 } from "node:path";
|
|
231979
|
+
init_fonts();
|
|
231980
|
+
init_assetIcons();
|
|
231981
|
+
init_projectCompile();
|
|
231982
|
+
init_cli();
|
|
231983
|
+
var escapeHtml = (text2) => text2.replace(/[&<>"]/g, (c4) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c4]);
|
|
231984
|
+
var normalBase = (base) => {
|
|
231985
|
+
const b = (base ?? "/").trim() || "/";
|
|
231986
|
+
return (b.startsWith("/") ? b : `/${b}`).replace(/\/*$/, "/");
|
|
231987
|
+
};
|
|
231988
|
+
var walk = (dir, prefix = "") => {
|
|
231989
|
+
const out = [];
|
|
231990
|
+
for (const name of readdirSync24(dir)) {
|
|
231991
|
+
const abs = join51(dir, name);
|
|
231992
|
+
const stat = statSync20(abs);
|
|
231993
|
+
if (stat.isDirectory())
|
|
231994
|
+
out.push(...walk(abs, `${prefix}${name}/`));
|
|
231995
|
+
else
|
|
231996
|
+
out.push({ path: prefix + name, bytes: stat.size });
|
|
231997
|
+
}
|
|
231998
|
+
return out;
|
|
231999
|
+
};
|
|
232000
|
+
var buildWeb = async (options) => {
|
|
232001
|
+
const { root, manifest, name, lite } = options;
|
|
232002
|
+
const out = resolve32(root, options.out);
|
|
232003
|
+
if (out === resolve32(root) || resolve32(root).startsWith(out + "\\") || resolve32(root).startsWith(out + "/")) {
|
|
232004
|
+
throw new CliError(`The output folder is cleaned before a build: "${options.out}" holds the project itself.`);
|
|
232005
|
+
}
|
|
232006
|
+
const config = readAppConfigAt(root)?.web ?? {};
|
|
232007
|
+
const mode = config.urls === "path" ? "path" : "hash";
|
|
232008
|
+
const base = normalBase(config.base);
|
|
232009
|
+
const warnings = [];
|
|
232010
|
+
const assetUrl = (path2) => "assets" + encodeURI(path2);
|
|
232011
|
+
const collected = collectLocalEntries(root, manifest, "assets");
|
|
232012
|
+
const entries = collected.entries.filter((e2) => {
|
|
232013
|
+
const abs = e2.absPath ? resolve32(e2.absPath) : null;
|
|
232014
|
+
return !(abs && (abs === out || abs.startsWith(out + "\\") || abs.startsWith(out + "/")));
|
|
232015
|
+
});
|
|
232016
|
+
for (const w of collected.warnings)
|
|
232017
|
+
if (!/shaders compile on the server/.test(w))
|
|
232018
|
+
warnings.push(w);
|
|
232019
|
+
const fontsHeader = await applyProjectFonts(entries, {
|
|
232020
|
+
mode: "vendor",
|
|
232021
|
+
root,
|
|
232022
|
+
vendorDir: root,
|
|
232023
|
+
vendorPrefix: "",
|
|
232024
|
+
publicUrl: "",
|
|
232025
|
+
relative: true,
|
|
232026
|
+
makeResourceEntry: (path2, absPath) => ({ path: path2, type: "resource", fileSrc: assetUrl(path2), absPath })
|
|
232027
|
+
});
|
|
232028
|
+
const iconsHeader = await applyAppAssetIcons(entries);
|
|
232029
|
+
if (!options.entry) {
|
|
232030
|
+
const ambiguous = ambiguousEntryWarning(entries);
|
|
232031
|
+
if (ambiguous)
|
|
232032
|
+
warnings.push(ambiguous);
|
|
232033
|
+
}
|
|
232034
|
+
const compiled = await compileProject2({
|
|
232035
|
+
entries,
|
|
232036
|
+
name,
|
|
232037
|
+
publicUrl: "",
|
|
232038
|
+
entryOverride: options.entry,
|
|
232039
|
+
header: true,
|
|
232040
|
+
fontsHeader,
|
|
232041
|
+
iconsHeader,
|
|
232042
|
+
sourcemap: options.sourcemap === true,
|
|
232043
|
+
minify: options.minify,
|
|
232044
|
+
onWarning: (w) => warnings.push(w)
|
|
232045
|
+
});
|
|
232046
|
+
const js = compiled.replace(/^\/\/ last-updated: .*$/m, "// last-updated: -");
|
|
232047
|
+
const header = js.slice(0, 2000);
|
|
232048
|
+
if (!/^\/\/ gl: disable/m.test(header) || /^\/\/ 2d: enable/m.test(header)) {
|
|
232049
|
+
warnings.push("This project uses 3D or 2D scenes, which Web Lite does not show yet: the page runs its UI, a scene's calls fail. (`lecodes dev --web` runs the whole app on the web player.)");
|
|
232050
|
+
}
|
|
232051
|
+
rmSync14(out, { recursive: true, force: true });
|
|
232052
|
+
mkdirSync33(out, { recursive: true });
|
|
232053
|
+
cpSync6(lite, out, { recursive: true });
|
|
232054
|
+
const table2 = join51(out, "features.json");
|
|
232055
|
+
if (existsSync51(table2)) {
|
|
232056
|
+
const { features, fonts } = JSON.parse(readFileSync41(table2, "utf8"));
|
|
232057
|
+
if (config.font === "system")
|
|
232058
|
+
for (const file of fonts ?? [])
|
|
232059
|
+
rmSync14(join51(out, file), { force: true });
|
|
232060
|
+
for (const feature of features) {
|
|
232061
|
+
if (feature.uses.some((name2) => js.includes(name2)))
|
|
232062
|
+
continue;
|
|
232063
|
+
for (const file of feature.files)
|
|
232064
|
+
rmSync14(join51(out, file), { force: true });
|
|
232065
|
+
}
|
|
232066
|
+
rmSync14(table2);
|
|
232067
|
+
}
|
|
232068
|
+
const bundle = `app.${createHash9("sha256").update(js).digest("hex").slice(0, 10)}.js`;
|
|
232069
|
+
writeFileSync35(join51(out, bundle), js);
|
|
232070
|
+
for (const entry of entries) {
|
|
232071
|
+
if (entry.type !== "resource" || !entry.absPath || !entry.fileSrc)
|
|
232072
|
+
continue;
|
|
232073
|
+
if (!js.includes(entry.fileSrc))
|
|
232074
|
+
continue;
|
|
232075
|
+
const to2 = join51(out, "assets", ...entry.path.split("/").filter(Boolean));
|
|
232076
|
+
if (existsSync51(to2)) {
|
|
232077
|
+
warnings.push(`${entry.path}: a file of the page has this name in the built folder — the project's file was not copied`);
|
|
232078
|
+
continue;
|
|
232079
|
+
}
|
|
232080
|
+
mkdirSync33(dirname26(to2), { recursive: true });
|
|
232081
|
+
copyFileSync2(entry.absPath, to2);
|
|
232082
|
+
}
|
|
232083
|
+
const app = readAppConfigAt(root);
|
|
232084
|
+
let html = readFileSync41(join51(lite, "index.html"), "utf8");
|
|
232085
|
+
const head = [];
|
|
232086
|
+
head.push(`<meta name="theme-color" content="${escapeHtml(config.themeColor ?? "#000000")}" />`);
|
|
232087
|
+
if (app?.icon) {
|
|
232088
|
+
const icon = resolve32(root, app.icon);
|
|
232089
|
+
if (existsSync51(icon)) {
|
|
232090
|
+
const file = `icon${extname2(icon).toLowerCase() || ".png"}`;
|
|
232091
|
+
copyFileSync2(icon, join51(out, file));
|
|
232092
|
+
head.push(`<link rel="icon" href="${file}" />`, `<link rel="apple-touch-icon" href="${file}" />`);
|
|
232093
|
+
} else {
|
|
232094
|
+
warnings.push(`app.json icon "${app.icon}" is not there — the page has no icon`);
|
|
232095
|
+
}
|
|
232096
|
+
}
|
|
232097
|
+
if (mode === "path")
|
|
232098
|
+
head.unshift(`<base href="${escapeHtml(base)}" />`);
|
|
232099
|
+
const attrs = [
|
|
232100
|
+
`data-src="${bundle}"`,
|
|
232101
|
+
...mode === "path" ? [`data-urls="path"`, `data-base="${escapeHtml(base)}"`] : [],
|
|
232102
|
+
...config.font === "system" ? [`data-font="system"`] : []
|
|
232103
|
+
].join(" ");
|
|
232104
|
+
const replace = (from, to2) => {
|
|
232105
|
+
const next = html.replace(from, to2);
|
|
232106
|
+
if (next === html)
|
|
232107
|
+
throw new CliError(`The page of Web Lite has no ${String(from)} to fill — this CLI and its page are of different versions.`);
|
|
232108
|
+
html = next;
|
|
232109
|
+
};
|
|
232110
|
+
replace(/<title>[^<]*<\/title>/, `<title>${escapeHtml(config.title ?? app?.name ?? name)}</title>`);
|
|
232111
|
+
replace(`<div id="app"></div>`, `<div id="app" ${attrs}></div>`);
|
|
232112
|
+
replace("</head>", ` ${head.join(`
|
|
232113
|
+
`)}
|
|
232114
|
+
</head>`);
|
|
232115
|
+
if (config.lang)
|
|
232116
|
+
replace(/<html lang="[^"]*">/, `<html lang="${escapeHtml(config.lang)}">`);
|
|
232117
|
+
writeFileSync35(join51(out, "index.html"), html);
|
|
232118
|
+
if (mode === "path")
|
|
232119
|
+
writeFileSync35(join51(out, "404.html"), html);
|
|
232120
|
+
return { out, bundle, files: walk(out), warnings };
|
|
232121
|
+
};
|
|
232122
|
+
|
|
232123
|
+
// src/web/preview.ts
|
|
232124
|
+
import { createReadStream as createReadStream2, existsSync as existsSync52, statSync as statSync21 } from "node:fs";
|
|
232125
|
+
import { createServer as createServer4 } from "node:http";
|
|
232126
|
+
import { join as join52, resolve as resolve33 } from "node:path";
|
|
232127
|
+
var startPreview = async (dir, port) => {
|
|
232128
|
+
const root = resolve33(dir);
|
|
232129
|
+
const fallback = existsSync52(join52(root, "404.html")) ? join52(root, "index.html") : null;
|
|
232130
|
+
const server2 = createServer4((req, res) => {
|
|
232131
|
+
let path2;
|
|
232132
|
+
try {
|
|
232133
|
+
path2 = decodeURIComponent(new URL(req.url ?? "/", "http://localhost").pathname);
|
|
232134
|
+
} catch {
|
|
232135
|
+
res.writeHead(400);
|
|
232136
|
+
res.end();
|
|
232137
|
+
return;
|
|
232138
|
+
}
|
|
232139
|
+
const parts = path2.split("/").filter(Boolean);
|
|
232140
|
+
if (parts.some((p2) => p2 === ".." || p2 === ".")) {
|
|
232141
|
+
res.writeHead(400);
|
|
232142
|
+
res.end();
|
|
232143
|
+
return;
|
|
232144
|
+
}
|
|
232145
|
+
let file = join52(root, ...parts);
|
|
232146
|
+
if (existsSync52(file) && statSync21(file).isDirectory())
|
|
232147
|
+
file = join52(file, "index.html");
|
|
232148
|
+
let status3 = 200;
|
|
232149
|
+
if (!existsSync52(file) || !statSync21(file).isFile()) {
|
|
232150
|
+
if (!fallback || /\.[a-z0-9]+$/i.test(parts[parts.length - 1] ?? "")) {
|
|
232151
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
232152
|
+
res.end("Not found");
|
|
232153
|
+
return;
|
|
232154
|
+
}
|
|
232155
|
+
file = fallback;
|
|
232156
|
+
}
|
|
232157
|
+
res.writeHead(status3, {
|
|
232158
|
+
"content-type": contentType(file),
|
|
232159
|
+
"cache-control": /[.-][0-9a-f]{8,}\.[a-z0-9]+$/i.test(file) ? "max-age=31536000, immutable" : "no-cache"
|
|
232160
|
+
});
|
|
232161
|
+
createReadStream2(file).pipe(res);
|
|
232162
|
+
});
|
|
232163
|
+
await new Promise((done, reject) => {
|
|
232164
|
+
server2.once("error", reject);
|
|
232165
|
+
server2.listen(port, "0.0.0.0", () => {
|
|
232166
|
+
server2.removeListener("error", reject);
|
|
232167
|
+
done();
|
|
232168
|
+
});
|
|
232169
|
+
});
|
|
232170
|
+
const address = server2.address();
|
|
232171
|
+
const bound = typeof address === "object" && address !== null ? address.port : port;
|
|
232172
|
+
return { url: `http://localhost:${bound}/`, port: bound, close: () => server2.close() };
|
|
232173
|
+
};
|
|
232174
|
+
|
|
232175
|
+
// src/commands/web.ts
|
|
232176
|
+
init_shared();
|
|
232177
|
+
var DEFAULT_PORT4 = 4488;
|
|
232178
|
+
var DEFAULT_OUT = "dist/web";
|
|
232179
|
+
var project = () => {
|
|
232180
|
+
let root;
|
|
232181
|
+
let manifest;
|
|
232182
|
+
try {
|
|
232183
|
+
root = findProjectRoot(process.cwd());
|
|
232184
|
+
manifest = readManifest(root);
|
|
232185
|
+
} catch {
|
|
232186
|
+
root = process.cwd();
|
|
232187
|
+
manifest = null;
|
|
232188
|
+
}
|
|
232189
|
+
return { root, manifest, name: manifest?.name ?? basename25(root) };
|
|
232190
|
+
};
|
|
232191
|
+
var litePage = () => {
|
|
232192
|
+
const lite = resolveLiteRunner(note);
|
|
232193
|
+
if (!lite)
|
|
232194
|
+
throw new CliError("No Web Lite page found: this CLI has no runtime/web-lite (a monorepo checkout needs `bun run build` in hosts/web-lite).");
|
|
232195
|
+
return lite;
|
|
232196
|
+
};
|
|
232197
|
+
var kb = (bytes) => bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KB` : `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
232198
|
+
var dev = defineCommand({
|
|
232199
|
+
name: "dev",
|
|
232200
|
+
summary: "Open the project as a web page, reloaded on every save",
|
|
232201
|
+
description: "Serves the project's bundle and assets with the page of Web Lite around them; a save recompiles and runs the app again, the page's console output and errors stream back here. The address's hash is the route the app is opened with (#/product/42 → app.launchUrl).",
|
|
232202
|
+
flags: {
|
|
232203
|
+
port: num("server port", { default: DEFAULT_PORT4 }),
|
|
232204
|
+
entry: bundleFlags.entry,
|
|
232205
|
+
"no-open": bool("don't open the browser")
|
|
232206
|
+
},
|
|
232207
|
+
examples: ["lecodes web dev", "lecodes web dev --port 8080 --no-open"],
|
|
232208
|
+
run: async ({ flags }) => {
|
|
232209
|
+
const { root, manifest, name } = project();
|
|
232210
|
+
const lite = litePage();
|
|
232211
|
+
const port = flags.port || DEFAULT_PORT4;
|
|
232212
|
+
const lan = lanCandidates()[0]?.address;
|
|
232213
|
+
const origin = `http://localhost:${port}`;
|
|
232214
|
+
const server2 = await startDevServer({
|
|
232215
|
+
root,
|
|
232216
|
+
manifest,
|
|
232217
|
+
name,
|
|
232218
|
+
origin,
|
|
232219
|
+
port,
|
|
232220
|
+
token: loadDevToken(root),
|
|
232221
|
+
entryOverride: flags.entry,
|
|
232222
|
+
lite,
|
|
232223
|
+
log: note
|
|
232224
|
+
}).catch((e2) => {
|
|
232225
|
+
const msg = e2 instanceof Error ? e2.message : String(e2);
|
|
232226
|
+
throw new CliError(msg.includes("EADDRINUSE") ? `Port ${port} is already in use — pass --port <n>.` : msg);
|
|
232227
|
+
});
|
|
232228
|
+
note("Compiling…");
|
|
232229
|
+
const first = await server2.recompile();
|
|
232230
|
+
if (first.ok)
|
|
232231
|
+
for (const w of first.warnings)
|
|
232232
|
+
warnErr(w);
|
|
232233
|
+
else
|
|
232234
|
+
warnErr(`Compile failed — fix it and save; the page loads once it compiles:
|
|
232235
|
+
${first.error}`);
|
|
232236
|
+
const watcher = watchProject(root, async () => {
|
|
232237
|
+
const result = await server2.recompile();
|
|
232238
|
+
if (!result.ok)
|
|
232239
|
+
warnErr(`Compile failed (the page keeps the last good version):
|
|
232240
|
+
${result.error}`);
|
|
232241
|
+
else if (result.changed) {
|
|
232242
|
+
note(`✓ v${result.v}`);
|
|
232243
|
+
for (const w of result.warnings)
|
|
232244
|
+
warnErr(w);
|
|
232245
|
+
}
|
|
232246
|
+
}, { warn: warnErr, debug: process.env.LECODES_DEV_DEBUG === "1" ? note : undefined });
|
|
232247
|
+
const url = server2.liteUrl;
|
|
232248
|
+
log("");
|
|
232249
|
+
log(`${c.bold("LeCodes Web")} — ${name}`);
|
|
232250
|
+
log("");
|
|
232251
|
+
log(`In the browser: ${c.bold(url)}`);
|
|
232252
|
+
if (lan)
|
|
232253
|
+
note(`On this network: ${url.replace("http://localhost:", `http://${lan}:`)}`);
|
|
232254
|
+
log("");
|
|
232255
|
+
log(`Watching ${root} — save a file to reload. Ctrl+C to stop.`);
|
|
232256
|
+
if (!flags["no-open"])
|
|
232257
|
+
openBrowser(url);
|
|
232258
|
+
process.on("SIGINT", () => {
|
|
232259
|
+
watcher.close();
|
|
232260
|
+
server2.close();
|
|
232261
|
+
process.exit(0);
|
|
232262
|
+
});
|
|
232263
|
+
}
|
|
232264
|
+
});
|
|
232265
|
+
var build2 = defineCommand({
|
|
232266
|
+
name: "build",
|
|
232267
|
+
summary: "Build the project into a folder any static hosting serves",
|
|
232268
|
+
description: 'Writes the page, the bundle, the page\'s own files and the assets the bundle names into one folder (dist/web). Every url in it is relative: the folder works at a domain\'s root and inside a folder of one.\n\napp.json `web` sets the page: `title`, `themeColor`, `lang`, and `urls` — "hash" (default: a route is `#/product/42`, which any hosting serves) or "path" (`/product/42` under `base`: the hosting has to answer every path with the page; a 404.html is written for the ones that do it that way).',
|
|
232269
|
+
flags: {
|
|
232270
|
+
out: str("the folder to write (cleaned first)", { alias: "o", value: "<dir>", default: DEFAULT_OUT }),
|
|
232271
|
+
entry: bundleFlags.entry,
|
|
232272
|
+
sourcemap: bool("inline the source map (errors name the app's own lines; the bundle grows by its sources)"),
|
|
232273
|
+
"no-minify": bundleFlags["no-minify"]
|
|
232274
|
+
},
|
|
232275
|
+
examples: ["lecodes web build", "lecodes web build -o public"],
|
|
232276
|
+
run: async ({ flags }) => {
|
|
232277
|
+
const { root, manifest, name } = project();
|
|
232278
|
+
const lite = litePage();
|
|
232279
|
+
note("Building…");
|
|
232280
|
+
const result = await buildWeb({
|
|
232281
|
+
root,
|
|
232282
|
+
manifest,
|
|
232283
|
+
name,
|
|
232284
|
+
lite,
|
|
232285
|
+
out: flags.out,
|
|
232286
|
+
entry: flags.entry,
|
|
232287
|
+
sourcemap: flags.sourcemap,
|
|
232288
|
+
minify: flags["no-minify"] ? false : undefined
|
|
232289
|
+
});
|
|
232290
|
+
for (const w of result.warnings)
|
|
232291
|
+
warnErr(w);
|
|
232292
|
+
const total = result.files.reduce((sum, f5) => sum + f5.bytes, 0);
|
|
232293
|
+
const shown = relative13(process.cwd(), result.out) || ".";
|
|
232294
|
+
log("");
|
|
232295
|
+
for (const f5 of [...result.files].sort((a2, b) => b.bytes - a2.bytes).slice(0, 8))
|
|
232296
|
+
log(` ${kb(f5.bytes).padStart(10)} ${f5.path}`);
|
|
232297
|
+
if (result.files.length > 8)
|
|
232298
|
+
log(c.dim(` …and ${result.files.length - 8} more`));
|
|
232299
|
+
log("");
|
|
232300
|
+
log(`${c.green("✓")} ${c.bold(shown)} — ${result.files.length} files, ${kb(total)}`);
|
|
232301
|
+
note(`Look at it: lecodes web preview${flags.out === DEFAULT_OUT ? "" : ` ${flags.out}`}`);
|
|
232302
|
+
}
|
|
232303
|
+
});
|
|
232304
|
+
var preview = defineCommand({
|
|
232305
|
+
name: "preview",
|
|
232306
|
+
summary: "Serve a built folder, as a static hosting would",
|
|
232307
|
+
usage: "[dir]",
|
|
232308
|
+
flags: {
|
|
232309
|
+
port: num("server port", { default: DEFAULT_PORT4 + 1 }),
|
|
232310
|
+
"no-open": bool("don't open the browser")
|
|
232311
|
+
},
|
|
232312
|
+
examples: ["lecodes web preview", "lecodes web preview public --port 8080"],
|
|
232313
|
+
run: async ({ args, flags }) => {
|
|
232314
|
+
const dir = args[0] ?? DEFAULT_OUT;
|
|
232315
|
+
const server2 = await startPreview(dir, flags.port || DEFAULT_PORT4 + 1).catch((e2) => {
|
|
232316
|
+
const msg = e2 instanceof Error ? e2.message : String(e2);
|
|
232317
|
+
throw new CliError(msg.includes("EADDRINUSE") ? `Port ${flags.port} is already in use — pass --port <n>.` : msg);
|
|
232318
|
+
});
|
|
232319
|
+
log(`${c.bold("LeCodes Web")} — ${dir}`);
|
|
232320
|
+
log(`In the browser: ${c.bold(server2.url)}`);
|
|
232321
|
+
log("Ctrl+C to stop.");
|
|
232322
|
+
if (!flags["no-open"])
|
|
232323
|
+
openBrowser(server2.url);
|
|
232324
|
+
process.on("SIGINT", () => {
|
|
232325
|
+
server2.close();
|
|
232326
|
+
process.exit(0);
|
|
232327
|
+
});
|
|
232328
|
+
}
|
|
232329
|
+
});
|
|
232330
|
+
var web_default = defineCommand({
|
|
232331
|
+
name: "web",
|
|
232332
|
+
summary: "The project as a web page (Web Lite): serve it with reload on save, build it for a hosting",
|
|
232333
|
+
description: "Web Lite shows the app with the browser's own layout, text and scroll: what a site ships. UI apps for now — a project's 3D and 2D scenes are not shown yet.",
|
|
232334
|
+
commands: [dev, build2, preview]
|
|
232335
|
+
});
|
|
232336
|
+
|
|
231452
232337
|
// src/commands/index.ts
|
|
231453
232338
|
var commandGroups = (opts) => [
|
|
231454
232339
|
{ title: "Project", commands: [init_default, create_default, clone_default, link_default, status_default, diff_default, pull_default, push_default, login_default] },
|
|
231455
232340
|
{ title: "Run & test", commands: [
|
|
231456
232341
|
dev_default,
|
|
232342
|
+
web_default,
|
|
231457
232343
|
compile_default,
|
|
231458
232344
|
render_default,
|
|
231459
232345
|
test_default,
|
|
@@ -231485,9 +232371,9 @@ var commandGroups = (opts) => [
|
|
|
231485
232371
|
// src/index.ts
|
|
231486
232372
|
init_peers();
|
|
231487
232373
|
var STANDALONE2 = (() => {
|
|
231488
|
-
const dir =
|
|
232374
|
+
const dir = dirname27(process.execPath);
|
|
231489
232375
|
const chisel = process.platform === "win32" ? "chisel.exe" : "chisel";
|
|
231490
|
-
return
|
|
232376
|
+
return existsSync53(join53(dir, "runtime")) && existsSync53(join53(dir, chisel));
|
|
231491
232377
|
})();
|
|
231492
232378
|
var INSTALLED = STANDALONE2 && (() => {
|
|
231493
232379
|
const exe = process.platform === "win32" ? "lecodes.exe" : "lecodes";
|
|
@@ -231502,8 +232388,8 @@ var INSTALLED = STANDALONE2 && (() => {
|
|
|
231502
232388
|
if (!d)
|
|
231503
232389
|
return false;
|
|
231504
232390
|
try {
|
|
231505
|
-
const cand =
|
|
231506
|
-
return
|
|
232391
|
+
const cand = join53(d, exe);
|
|
232392
|
+
return existsSync53(cand) && realpathSync2(cand) === self2;
|
|
231507
232393
|
} catch {
|
|
231508
232394
|
return false;
|
|
231509
232395
|
}
|